refactor: replace fmt.Errorf/os.* with go-io/go-log conventions
Some checks failed
Security Scan / security (push) Successful in 11s
Test / Test (push) Failing after 23s

Replace all fmt.Errorf and errors.New in production code with
coreerr.E("Caller.Method", "message", err) from go-log. Replace
os.MkdirAll with coreio.Local.EnsureDir from go-io. Sentinel errors
(consensus/errors.go, wire/varint.go) intentionally kept as errors.New
for errors.Is compatibility.

270 error call sites converted across 38 files. Test files untouched.
crypto/ directory (CGO) untouched.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-16 21:16:34 +00:00
parent 8d41b76db3
commit 71f0a5c1d5
41 changed files with 447 additions and 855 deletions

View file

@ -8,10 +8,8 @@
package chain
import (
"errors"
"fmt"
"forge.lthn.ai/core/go-blockchain/types"
coreerr "forge.lthn.ai/core/go-log"
store "forge.lthn.ai/core/go-store"
)
@ -29,7 +27,7 @@ func New(s *store.Store) *Chain {
func (c *Chain) Height() (uint64, error) {
n, err := c.store.Count(groupBlocks)
if err != nil {
return 0, fmt.Errorf("chain: height: %w", err)
return 0, coreerr.E("Chain.Height", "chain: height", err)
}
return uint64(n), nil
}
@ -42,7 +40,7 @@ func (c *Chain) TopBlock() (*types.Block, *BlockMeta, error) {
return nil, nil, err
}
if h == 0 {
return nil, nil, errors.New("chain: no blocks stored")
return nil, nil, coreerr.E("Chain.TopBlock", "chain: no blocks stored", nil)
}
return c.GetBlockByHeight(h - 1)
}

View file

@ -11,14 +11,16 @@ import (
"fmt"
"strconv"
store "forge.lthn.ai/core/go-store"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types"
store "forge.lthn.ai/core/go-store"
)
// MarkSpent records a key image as spent at the given block height.
func (c *Chain) MarkSpent(ki types.KeyImage, height uint64) error {
if err := c.store.Set(groupSpentKeys, ki.String(), strconv.FormatUint(height, 10)); err != nil {
return fmt.Errorf("chain: mark spent %s: %w", ki, err)
return coreerr.E("Chain.MarkSpent", fmt.Sprintf("chain: mark spent %s", ki), err)
}
return nil
}
@ -30,7 +32,7 @@ func (c *Chain) IsSpent(ki types.KeyImage) (bool, error) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("chain: check spent %s: %w", ki, err)
return false, coreerr.E("Chain.IsSpent", fmt.Sprintf("chain: check spent %s", ki), err)
}
return true, nil
}
@ -46,7 +48,7 @@ func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64,
grp := outputGroup(amount)
count, err := c.store.Count(grp)
if err != nil {
return 0, fmt.Errorf("chain: output count: %w", err)
return 0, coreerr.E("Chain.PutOutput", "chain: output count", err)
}
gindex := uint64(count)
@ -56,12 +58,12 @@ func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64,
}
val, err := json.Marshal(entry)
if err != nil {
return 0, fmt.Errorf("chain: marshal output: %w", err)
return 0, coreerr.E("Chain.PutOutput", "chain: marshal output", err)
}
key := strconv.FormatUint(gindex, 10)
if err := c.store.Set(grp, key, string(val)); err != nil {
return 0, fmt.Errorf("chain: store output: %w", err)
return 0, coreerr.E("Chain.PutOutput", "chain: store output", err)
}
return gindex, nil
}
@ -73,18 +75,18 @@ func (c *Chain) GetOutput(amount uint64, gindex uint64) (types.Hash, uint32, err
val, err := c.store.Get(grp, key)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return types.Hash{}, 0, fmt.Errorf("chain: output %d:%d not found", amount, gindex)
return types.Hash{}, 0, coreerr.E("Chain.GetOutput", fmt.Sprintf("chain: output %d:%d not found", amount, gindex), nil)
}
return types.Hash{}, 0, fmt.Errorf("chain: get output: %w", err)
return types.Hash{}, 0, coreerr.E("Chain.GetOutput", "chain: get output", err)
}
var entry outputEntry
if err := json.Unmarshal([]byte(val), &entry); err != nil {
return types.Hash{}, 0, fmt.Errorf("chain: unmarshal output: %w", err)
return types.Hash{}, 0, coreerr.E("Chain.GetOutput", "chain: unmarshal output", err)
}
hash, err := types.HashFromHex(entry.TxID)
if err != nil {
return types.Hash{}, 0, fmt.Errorf("chain: parse output tx_id: %w", err)
return types.Hash{}, 0, coreerr.E("Chain.GetOutput", "chain: parse output tx_id", err)
}
return hash, entry.OutNo, nil
}
@ -93,7 +95,7 @@ func (c *Chain) GetOutput(amount uint64, gindex uint64) (types.Hash, uint32, err
func (c *Chain) OutputCount(amount uint64) (uint64, error) {
n, err := c.store.Count(outputGroup(amount))
if err != nil {
return 0, fmt.Errorf("chain: output count: %w", err)
return 0, coreerr.E("Chain.OutputCount", "chain: output count", err)
}
return uint64(n), nil
}

View file

@ -6,9 +6,10 @@
package chain
import (
"fmt"
"log"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/p2p"
levinpkg "forge.lthn.ai/core/go-p2p/node/levin"
)
@ -39,10 +40,10 @@ func (c *LevinP2PConn) handleMessage(hdr levinpkg.Header, data []byte) error {
resp := p2p.TimedSyncRequest{PayloadData: c.localSync}
payload, err := resp.Encode()
if err != nil {
return fmt.Errorf("encode timed_sync response: %w", err)
return coreerr.E("LevinP2PConn.handleMessage", "encode timed_sync response", err)
}
if err := c.conn.WriteResponse(p2p.CommandTimedSync, payload, levinpkg.ReturnOK); err != nil {
return fmt.Errorf("write timed_sync response: %w", err)
return coreerr.E("LevinP2PConn.handleMessage", "write timed_sync response", err)
}
log.Printf("p2p: responded to timed_sync")
return nil
@ -55,24 +56,24 @@ func (c *LevinP2PConn) RequestChain(blockIDs [][]byte) (uint64, [][]byte, error)
req := p2p.RequestChain{BlockIDs: blockIDs}
payload, err := req.Encode()
if err != nil {
return 0, nil, fmt.Errorf("encode request_chain: %w", err)
return 0, nil, coreerr.E("LevinP2PConn.RequestChain", "encode request_chain", err)
}
// Send as notification (expectResponse=false) per CryptoNote protocol.
if err := c.conn.WritePacket(p2p.CommandRequestChain, payload, false); err != nil {
return 0, nil, fmt.Errorf("write request_chain: %w", err)
return 0, nil, coreerr.E("LevinP2PConn.RequestChain", "write request_chain", err)
}
// Read until we get RESPONSE_CHAIN_ENTRY.
for {
hdr, data, err := c.conn.ReadPacket()
if err != nil {
return 0, nil, fmt.Errorf("read response_chain: %w", err)
return 0, nil, coreerr.E("LevinP2PConn.RequestChain", "read response_chain", err)
}
if hdr.Command == p2p.CommandResponseChain {
var resp p2p.ResponseChainEntry
if err := resp.Decode(data); err != nil {
return 0, nil, fmt.Errorf("decode response_chain: %w", err)
return 0, nil, coreerr.E("LevinP2PConn.RequestChain", "decode response_chain", err)
}
return resp.StartHeight, resp.BlockIDs, nil
}
@ -86,23 +87,23 @@ func (c *LevinP2PConn) RequestObjects(blockHashes [][]byte) ([]BlockBlobEntry, e
req := p2p.RequestGetObjects{Blocks: blockHashes}
payload, err := req.Encode()
if err != nil {
return nil, fmt.Errorf("encode request_get_objects: %w", err)
return nil, coreerr.E("LevinP2PConn.RequestObjects", "encode request_get_objects", err)
}
if err := c.conn.WritePacket(p2p.CommandRequestObjects, payload, false); err != nil {
return nil, fmt.Errorf("write request_get_objects: %w", err)
return nil, coreerr.E("LevinP2PConn.RequestObjects", "write request_get_objects", err)
}
// Read until we get RESPONSE_GET_OBJECTS.
for {
hdr, data, err := c.conn.ReadPacket()
if err != nil {
return nil, fmt.Errorf("read response_get_objects: %w", err)
return nil, coreerr.E("LevinP2PConn.RequestObjects", "read response_get_objects", err)
}
if hdr.Command == p2p.CommandResponseObjects {
var resp p2p.ResponseGetObjects
if err := resp.Decode(data); err != nil {
return nil, fmt.Errorf("decode response_get_objects: %w", err)
return nil, coreerr.E("LevinP2PConn.RequestObjects", "decode response_get_objects", err)
}
entries := make([]BlockBlobEntry, len(resp.Blocks))
for i, b := range resp.Blocks {

View file

@ -9,6 +9,8 @@ import (
"context"
"fmt"
"log"
coreerr "forge.lthn.ai/core/go-log"
)
// P2PConnection abstracts the P2P communication needed for block sync.
@ -44,7 +46,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
localHeight, err := c.Height()
if err != nil {
return fmt.Errorf("p2p sync: get height: %w", err)
return coreerr.E("Chain.P2PSync", "p2p sync: get height", err)
}
peerHeight := conn.PeerHeight()
@ -55,7 +57,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
// Build sparse chain history.
history, err := c.SparseChainHistory()
if err != nil {
return fmt.Errorf("p2p sync: build history: %w", err)
return coreerr.E("Chain.P2PSync", "p2p sync: build history", err)
}
// Convert Hash to []byte for P2P.
@ -69,7 +71,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
// Request chain entry.
startHeight, blockIDs, err := conn.RequestChain(historyBytes)
if err != nil {
return fmt.Errorf("p2p sync: request chain: %w", err)
return coreerr.E("Chain.P2PSync", "p2p sync: request chain", err)
}
if len(blockIDs) == 0 {
@ -106,7 +108,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
entries, err := conn.RequestObjects(batch)
if err != nil {
return fmt.Errorf("p2p sync: request objects: %w", err)
return coreerr.E("Chain.P2PSync", "p2p sync: request objects", err)
}
currentHeight := fetchStart + uint64(i)
@ -118,12 +120,12 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
blockDiff, err := c.NextDifficulty(blockHeight, opts.Forks)
if err != nil {
return fmt.Errorf("p2p sync: compute difficulty for block %d: %w", blockHeight, err)
return coreerr.E("Chain.P2PSync", fmt.Sprintf("p2p sync: compute difficulty for block %d", blockHeight), err)
}
if err := c.processBlockBlobs(entry.Block, entry.Txs,
blockHeight, blockDiff, opts); err != nil {
return fmt.Errorf("p2p sync: process block %d: %w", blockHeight, err)
return coreerr.E("Chain.P2PSync", fmt.Sprintf("p2p sync: process block %d", blockHeight), err)
}
}
}

View file

@ -8,6 +8,8 @@ package chain
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/consensus"
"forge.lthn.ai/core/go-blockchain/types"
)
@ -20,17 +22,16 @@ func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicK
for i, gidx := range offsets {
txHash, outNo, err := c.GetOutput(amount, gidx)
if err != nil {
return nil, fmt.Errorf("ring output %d (amount=%d, gidx=%d): %w", i, amount, gidx, err)
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d (amount=%d, gidx=%d)", i, amount, gidx), err)
}
tx, _, err := c.GetTransaction(txHash)
if err != nil {
return nil, fmt.Errorf("ring output %d: tx %s: %w", i, txHash, err)
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: tx %s", i, txHash), err)
}
if int(outNo) >= len(tx.Vout) {
return nil, fmt.Errorf("ring output %d: tx %s has %d outputs, want index %d",
i, txHash, len(tx.Vout), outNo)
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: tx %s has %d outputs, want index %d", i, txHash, len(tx.Vout), outNo), nil)
}
switch out := tx.Vout[outNo].(type) {
@ -41,7 +42,7 @@ func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicK
}
pubs[i] = toKey.Key
default:
return nil, fmt.Errorf("ring output %d: unsupported output type %T", i, out)
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: unsupported output type %T", i, out), nil)
}
}
return pubs, nil
@ -57,17 +58,16 @@ func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, er
for i, gidx := range offsets {
txHash, outNo, err := c.GetOutput(0, gidx)
if err != nil {
return nil, fmt.Errorf("ZC ring output %d (gidx=%d): %w", i, gidx, err)
return nil, coreerr.E("Chain.GetZCRingOutputs", fmt.Sprintf("ZC ring output %d (gidx=%d)", i, gidx), err)
}
tx, _, err := c.GetTransaction(txHash)
if err != nil {
return nil, fmt.Errorf("ZC ring output %d: tx %s: %w", i, txHash, err)
return nil, coreerr.E("Chain.GetZCRingOutputs", fmt.Sprintf("ZC ring output %d: tx %s", i, txHash), err)
}
if int(outNo) >= len(tx.Vout) {
return nil, fmt.Errorf("ZC ring output %d: tx %s has %d outputs, want index %d",
i, txHash, len(tx.Vout), outNo)
return nil, coreerr.E("Chain.GetZCRingOutputs", fmt.Sprintf("ZC ring output %d: tx %s has %d outputs, want index %d", i, txHash, len(tx.Vout), outNo), nil)
}
switch out := tx.Vout[outNo].(type) {
@ -78,7 +78,7 @@ func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, er
BlindedAssetID: [32]byte(out.BlindedAssetID),
}
default:
return nil, fmt.Errorf("ZC ring output %d: expected TxOutputZarcanum, got %T", i, out)
return nil, coreerr.E("Chain.GetZCRingOutputs", fmt.Sprintf("ZC ring output %d: expected TxOutputZarcanum, got %T", i, out), nil)
}
}
return members, nil

View file

@ -13,9 +13,11 @@ import (
"fmt"
"strconv"
store "forge.lthn.ai/core/go-store"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
store "forge.lthn.ai/core/go-store"
)
// Storage group constants matching the design schema.
@ -44,7 +46,7 @@ func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error {
enc := wire.NewEncoder(&buf)
wire.EncodeBlock(enc, b)
if err := enc.Err(); err != nil {
return fmt.Errorf("chain: encode block %d: %w", meta.Height, err)
return coreerr.E("Chain.PutBlock", fmt.Sprintf("chain: encode block %d", meta.Height), err)
}
rec := blockRecord{
@ -53,17 +55,17 @@ func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error {
}
val, err := json.Marshal(rec)
if err != nil {
return fmt.Errorf("chain: marshal block %d: %w", meta.Height, err)
return coreerr.E("Chain.PutBlock", fmt.Sprintf("chain: marshal block %d", meta.Height), err)
}
if err := c.store.Set(groupBlocks, heightKey(meta.Height), string(val)); err != nil {
return fmt.Errorf("chain: store block %d: %w", meta.Height, err)
return coreerr.E("Chain.PutBlock", fmt.Sprintf("chain: store block %d", meta.Height), err)
}
// Update hash -> height index.
hashHex := meta.Hash.String()
if err := c.store.Set(groupBlockIndex, hashHex, strconv.FormatUint(meta.Height, 10)); err != nil {
return fmt.Errorf("chain: index block %d: %w", meta.Height, err)
return coreerr.E("Chain.PutBlock", fmt.Sprintf("chain: index block %d", meta.Height), err)
}
return nil
@ -74,9 +76,9 @@ func (c *Chain) GetBlockByHeight(height uint64) (*types.Block, *BlockMeta, error
val, err := c.store.Get(groupBlocks, heightKey(height))
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return nil, nil, fmt.Errorf("chain: block %d not found", height)
return nil, nil, coreerr.E("Chain.GetBlockByHeight", fmt.Sprintf("chain: block %d not found", height), nil)
}
return nil, nil, fmt.Errorf("chain: get block %d: %w", height, err)
return nil, nil, coreerr.E("Chain.GetBlockByHeight", fmt.Sprintf("chain: get block %d", height), err)
}
return decodeBlockRecord(val)
}
@ -86,13 +88,13 @@ func (c *Chain) GetBlockByHash(hash types.Hash) (*types.Block, *BlockMeta, error
heightStr, err := c.store.Get(groupBlockIndex, hash.String())
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return nil, nil, fmt.Errorf("chain: block %s not found", hash)
return nil, nil, coreerr.E("Chain.GetBlockByHash", fmt.Sprintf("chain: block %s not found", hash), nil)
}
return nil, nil, fmt.Errorf("chain: get block index %s: %w", hash, err)
return nil, nil, coreerr.E("Chain.GetBlockByHash", fmt.Sprintf("chain: get block index %s", hash), err)
}
height, err := strconv.ParseUint(heightStr, 10, 64)
if err != nil {
return nil, nil, fmt.Errorf("chain: parse height %q: %w", heightStr, err)
return nil, nil, coreerr.E("Chain.GetBlockByHash", fmt.Sprintf("chain: parse height %q", heightStr), err)
}
return c.GetBlockByHeight(height)
}
@ -109,7 +111,7 @@ func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxM
enc := wire.NewEncoder(&buf)
wire.EncodeTransaction(enc, tx)
if err := enc.Err(); err != nil {
return fmt.Errorf("chain: encode tx %s: %w", hash, err)
return coreerr.E("Chain.PutTransaction", fmt.Sprintf("chain: encode tx %s", hash), err)
}
rec := txRecord{
@ -118,11 +120,11 @@ func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxM
}
val, err := json.Marshal(rec)
if err != nil {
return fmt.Errorf("chain: marshal tx %s: %w", hash, err)
return coreerr.E("Chain.PutTransaction", fmt.Sprintf("chain: marshal tx %s", hash), err)
}
if err := c.store.Set(groupTx, hash.String(), string(val)); err != nil {
return fmt.Errorf("chain: store tx %s: %w", hash, err)
return coreerr.E("Chain.PutTransaction", fmt.Sprintf("chain: store tx %s", hash), err)
}
return nil
}
@ -132,23 +134,23 @@ func (c *Chain) GetTransaction(hash types.Hash) (*types.Transaction, *TxMeta, er
val, err := c.store.Get(groupTx, hash.String())
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return nil, nil, fmt.Errorf("chain: tx %s not found", hash)
return nil, nil, coreerr.E("Chain.GetTransaction", fmt.Sprintf("chain: tx %s not found", hash), nil)
}
return nil, nil, fmt.Errorf("chain: get tx %s: %w", hash, err)
return nil, nil, coreerr.E("Chain.GetTransaction", fmt.Sprintf("chain: get tx %s", hash), err)
}
var rec txRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil {
return nil, nil, fmt.Errorf("chain: unmarshal tx: %w", err)
return nil, nil, coreerr.E("Chain.GetTransaction", "chain: unmarshal tx", err)
}
blob, err := hex.DecodeString(rec.Blob)
if err != nil {
return nil, nil, fmt.Errorf("chain: decode tx hex: %w", err)
return nil, nil, coreerr.E("Chain.GetTransaction", "chain: decode tx hex", err)
}
dec := wire.NewDecoder(bytes.NewReader(blob))
tx := wire.DecodeTransaction(dec)
if err := dec.Err(); err != nil {
return nil, nil, fmt.Errorf("chain: decode tx wire: %w", err)
return nil, nil, coreerr.E("Chain.GetTransaction", "chain: decode tx wire", err)
}
return &tx, &rec.Meta, nil
}
@ -164,11 +166,11 @@ func (c *Chain) HasTransaction(hash types.Hash) bool {
func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
val, err := c.store.Get(groupBlocks, heightKey(height))
if err != nil {
return nil, fmt.Errorf("chain: block meta %d: %w", height, err)
return nil, coreerr.E("Chain.getBlockMeta", fmt.Sprintf("chain: block meta %d", height), err)
}
var rec blockRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil {
return nil, fmt.Errorf("chain: unmarshal block meta %d: %w", height, err)
return nil, coreerr.E("Chain.getBlockMeta", fmt.Sprintf("chain: unmarshal block meta %d", height), err)
}
return &rec.Meta, nil
}
@ -176,16 +178,16 @@ func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
func decodeBlockRecord(val string) (*types.Block, *BlockMeta, error) {
var rec blockRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil {
return nil, nil, fmt.Errorf("chain: unmarshal block: %w", err)
return nil, nil, coreerr.E("decodeBlockRecord", "chain: unmarshal block", err)
}
blob, err := hex.DecodeString(rec.Blob)
if err != nil {
return nil, nil, fmt.Errorf("chain: decode block hex: %w", err)
return nil, nil, coreerr.E("decodeBlockRecord", "chain: decode block hex", err)
}
dec := wire.NewDecoder(bytes.NewReader(blob))
blk := wire.DecodeBlock(dec)
if err := dec.Err(); err != nil {
return nil, nil, fmt.Errorf("chain: decode block wire: %w", err)
return nil, nil, coreerr.E("decodeBlockRecord", "chain: decode block wire", err)
}
return &blk, &rec.Meta, nil
}

View file

@ -10,12 +10,13 @@ import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log"
"regexp"
"strconv"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/consensus"
"forge.lthn.ai/core/go-blockchain/rpc"
@ -51,12 +52,12 @@ func DefaultSyncOptions() SyncOptions {
func (c *Chain) Sync(ctx context.Context, client *rpc.Client, opts SyncOptions) error {
localHeight, err := c.Height()
if err != nil {
return fmt.Errorf("sync: get local height: %w", err)
return coreerr.E("Chain.Sync", "sync: get local height", err)
}
remoteHeight, err := client.GetHeight()
if err != nil {
return fmt.Errorf("sync: get remote height: %w", err)
return coreerr.E("Chain.Sync", "sync: get remote height", err)
}
for localHeight < remoteHeight {
@ -71,22 +72,22 @@ func (c *Chain) Sync(ctx context.Context, client *rpc.Client, opts SyncOptions)
blocks, err := client.GetBlocksDetails(localHeight, batch)
if err != nil {
return fmt.Errorf("sync: fetch blocks at %d: %w", localHeight, err)
return coreerr.E("Chain.Sync", fmt.Sprintf("sync: fetch blocks at %d", localHeight), err)
}
if err := resolveBlockBlobs(blocks, client); err != nil {
return fmt.Errorf("sync: resolve blobs at %d: %w", localHeight, err)
return coreerr.E("Chain.Sync", fmt.Sprintf("sync: resolve blobs at %d", localHeight), err)
}
for _, bd := range blocks {
if err := c.processBlock(bd, opts); err != nil {
return fmt.Errorf("sync: process block %d: %w", bd.Height, err)
return coreerr.E("Chain.Sync", fmt.Sprintf("sync: process block %d", bd.Height), err)
}
}
localHeight, err = c.Height()
if err != nil {
return fmt.Errorf("sync: get height after batch: %w", err)
return coreerr.E("Chain.Sync", "sync: get height after batch", err)
}
}
@ -100,7 +101,7 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
blockBlob, err := hex.DecodeString(bd.Blob)
if err != nil {
return fmt.Errorf("decode block hex: %w", err)
return coreerr.E("Chain.processBlock", "decode block hex", err)
}
// Build a set of the block's regular tx hashes for lookup.
@ -110,7 +111,7 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
dec := wire.NewDecoder(bytes.NewReader(blockBlob))
blk := wire.DecodeBlock(dec)
if err := dec.Err(); err != nil {
return fmt.Errorf("decode block for tx hashes: %w", err)
return coreerr.E("Chain.processBlock", "decode block for tx hashes", err)
}
regularTxs := make(map[string]struct{}, len(blk.TxHashes))
@ -125,7 +126,7 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
}
txBlobBytes, err := hex.DecodeString(txInfo.Blob)
if err != nil {
return fmt.Errorf("decode tx hex %s: %w", txInfo.ID, err)
return coreerr.E("Chain.processBlock", fmt.Sprintf("decode tx hex %s", txInfo.ID), err)
}
txBlobs = append(txBlobs, txBlobBytes)
}
@ -136,11 +137,10 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
computedHash := wire.BlockHash(&blk)
daemonHash, err := types.HashFromHex(bd.ID)
if err != nil {
return fmt.Errorf("parse daemon block hash: %w", err)
return coreerr.E("Chain.processBlock", "parse daemon block hash", err)
}
if computedHash != daemonHash {
return fmt.Errorf("block hash mismatch: computed %s, daemon says %s",
computedHash, daemonHash)
return coreerr.E("Chain.processBlock", fmt.Sprintf("block hash mismatch: computed %s, daemon says %s", computedHash, daemonHash), nil)
}
return c.processBlockBlobs(blockBlob, txBlobs, bd.Height, diff, opts)
@ -155,7 +155,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
dec := wire.NewDecoder(bytes.NewReader(blockBlob))
blk := wire.DecodeBlock(dec)
if err := dec.Err(); err != nil {
return fmt.Errorf("decode block wire: %w", err)
return coreerr.E("Chain.processBlockBlobs", "decode block wire", err)
}
// Compute the block hash.
@ -165,11 +165,10 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
if height == 0 {
genesisHash, err := types.HashFromHex(GenesisHash)
if err != nil {
return fmt.Errorf("parse genesis hash: %w", err)
return coreerr.E("Chain.processBlockBlobs", "parse genesis hash", err)
}
if blockHash != genesisHash {
return fmt.Errorf("genesis hash %s does not match expected %s",
blockHash, GenesisHash)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("genesis hash %s does not match expected %s", blockHash, GenesisHash), nil)
}
}
@ -180,7 +179,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
// Validate miner transaction structure.
if err := consensus.ValidateMinerTx(&blk.MinerTx, height, opts.Forks); err != nil {
return fmt.Errorf("validate miner tx: %w", err)
return coreerr.E("Chain.processBlockBlobs", "validate miner tx", err)
}
// Calculate cumulative difficulty.
@ -188,7 +187,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
if height > 0 {
_, prevMeta, err := c.TopBlock()
if err != nil {
return fmt.Errorf("get prev block meta: %w", err)
return coreerr.E("Chain.processBlockBlobs", "get prev block meta", err)
}
cumulDiff = prevMeta.CumulativeDiff + difficulty
} else {
@ -199,13 +198,13 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
minerTxHash := wire.TransactionHash(&blk.MinerTx)
minerGindexes, err := c.indexOutputs(minerTxHash, &blk.MinerTx)
if err != nil {
return fmt.Errorf("index miner tx outputs: %w", err)
return coreerr.E("Chain.processBlockBlobs", "index miner tx outputs", err)
}
if err := c.PutTransaction(minerTxHash, &blk.MinerTx, &TxMeta{
KeeperBlock: height,
GlobalOutputIndexes: minerGindexes,
}); err != nil {
return fmt.Errorf("store miner tx: %w", err)
return coreerr.E("Chain.processBlockBlobs", "store miner tx", err)
}
// Process regular transactions from txBlobs.
@ -213,27 +212,27 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
txDec := wire.NewDecoder(bytes.NewReader(txBlobData))
tx := wire.DecodeTransaction(txDec)
if err := txDec.Err(); err != nil {
return fmt.Errorf("decode tx wire [%d]: %w", i, err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("decode tx wire [%d]", i), err)
}
txHash := wire.TransactionHash(&tx)
// Validate transaction semantics.
if err := consensus.ValidateTransaction(&tx, txBlobData, opts.Forks, height); err != nil {
return fmt.Errorf("validate tx %s: %w", txHash, err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("validate tx %s", txHash), err)
}
// Optionally verify signatures using the chain's output index.
if opts.VerifySignatures {
if err := consensus.VerifyTransactionSignatures(&tx, opts.Forks, height, c.GetRingOutputs, c.GetZCRingOutputs); err != nil {
return fmt.Errorf("verify tx signatures %s: %w", txHash, err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("verify tx signatures %s", txHash), err)
}
}
// Index outputs.
gindexes, err := c.indexOutputs(txHash, &tx)
if err != nil {
return fmt.Errorf("index tx outputs %s: %w", txHash, err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("index tx outputs %s", txHash), err)
}
// Mark key images as spent.
@ -241,11 +240,11 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
switch inp := vin.(type) {
case types.TxInputToKey:
if err := c.MarkSpent(inp.KeyImage, height); err != nil {
return fmt.Errorf("mark spent %s: %w", inp.KeyImage, err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
}
case types.TxInputZC:
if err := c.MarkSpent(inp.KeyImage, height); err != nil {
return fmt.Errorf("mark spent %s: %w", inp.KeyImage, err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
}
}
}
@ -255,7 +254,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
KeeperBlock: height,
GlobalOutputIndexes: gindexes,
}); err != nil {
return fmt.Errorf("store tx %s: %w", txHash, err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("store tx %s", txHash), err)
}
}
@ -330,13 +329,13 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
// Batch-fetch tx blobs.
txHexes, missed, err := client.GetTransactions(allHashes)
if err != nil {
return fmt.Errorf("fetch tx blobs: %w", err)
return coreerr.E("resolveBlockBlobs", "fetch tx blobs", err)
}
if len(missed) > 0 {
return fmt.Errorf("daemon missed %d tx(es): %v", len(missed), missed)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("daemon missed %d tx(es): %v", len(missed), missed), nil)
}
if len(txHexes) != len(allHashes) {
return fmt.Errorf("expected %d tx blobs, got %d", len(allHashes), len(txHexes))
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("expected %d tx blobs, got %d", len(allHashes), len(txHexes)), nil)
}
// Index fetched blobs by hash.
@ -364,16 +363,16 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
// Parse header from object_in_json.
hdr, err := parseBlockHeader(bd.ObjectInJSON)
if err != nil {
return fmt.Errorf("block %d: parse header: %w", bd.Height, err)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse header", bd.Height), err)
}
// Miner tx blob is transactions_details[0].
if len(bd.Transactions) == 0 {
return fmt.Errorf("block %d has no transactions_details", bd.Height)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d has no transactions_details", bd.Height), nil)
}
minerTxBlob, err := hex.DecodeString(bd.Transactions[0].Blob)
if err != nil {
return fmt.Errorf("block %d: decode miner tx hex: %w", bd.Height, err)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: decode miner tx hex", bd.Height), err)
}
// Collect regular tx hashes.
@ -381,7 +380,7 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
for _, txInfo := range bd.Transactions[1:] {
h, err := types.HashFromHex(txInfo.ID)
if err != nil {
return fmt.Errorf("block %d: parse tx hash %s: %w", bd.Height, txInfo.ID, err)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse tx hash %s", bd.Height, txInfo.ID), err)
}
txHashes = append(txHashes, h)
}
@ -411,17 +410,17 @@ var aggregatedRE = regexp.MustCompile(`"AGGREGATED"\s*:\s*\{([^}]+)\}`)
func parseBlockHeader(objectInJSON string) (*types.BlockHeader, error) {
m := aggregatedRE.FindStringSubmatch(objectInJSON)
if m == nil {
return nil, errors.New("AGGREGATED section not found in object_in_json")
return nil, coreerr.E("parseBlockHeader", "AGGREGATED section not found in object_in_json", nil)
}
var hj blockHeaderJSON
if err := json.Unmarshal([]byte("{"+m[1]+"}"), &hj); err != nil {
return nil, fmt.Errorf("unmarshal AGGREGATED: %w", err)
return nil, coreerr.E("parseBlockHeader", "unmarshal AGGREGATED", err)
}
prevID, err := types.HashFromHex(hj.PrevID)
if err != nil {
return nil, fmt.Errorf("parse prev_id: %w", err)
return nil, coreerr.E("parseBlockHeader", "parse prev_id", err)
}
return &types.BlockHeader{

View file

@ -7,9 +7,10 @@ package chain
import (
"bytes"
"errors"
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
@ -20,19 +21,18 @@ import (
func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
currentHeight, err := c.Height()
if err != nil {
return fmt.Errorf("validate: get height: %w", err)
return coreerr.E("Chain.ValidateHeader", "validate: get height", err)
}
// Height sequence check.
if expectedHeight != currentHeight {
return fmt.Errorf("validate: expected height %d but chain is at %d",
expectedHeight, currentHeight)
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: expected height %d but chain is at %d", expectedHeight, currentHeight), nil)
}
// Genesis block: prev_id must be zero.
if expectedHeight == 0 {
if !b.PrevID.IsZero() {
return errors.New("validate: genesis block has non-zero prev_id")
return coreerr.E("Chain.ValidateHeader", "validate: genesis block has non-zero prev_id", nil)
}
return nil
}
@ -40,11 +40,10 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
// Non-genesis: prev_id must match top block hash.
_, topMeta, err := c.TopBlock()
if err != nil {
return fmt.Errorf("validate: get top block: %w", err)
return coreerr.E("Chain.ValidateHeader", "validate: get top block", err)
}
if b.PrevID != topMeta.Hash {
return fmt.Errorf("validate: prev_id %s does not match top block %s",
b.PrevID, topMeta.Hash)
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: prev_id %s does not match top block %s", b.PrevID, topMeta.Hash), nil)
}
// Block size check.
@ -52,8 +51,7 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
enc := wire.NewEncoder(&buf)
wire.EncodeBlock(enc, b)
if enc.Err() == nil && uint64(buf.Len()) > config.MaxBlockSize {
return fmt.Errorf("validate: block size %d exceeds max %d",
buf.Len(), config.MaxBlockSize)
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: block size %d exceeds max %d", buf.Len(), config.MaxBlockSize), nil)
}
return nil

View file

@ -7,12 +7,13 @@ package blockchain
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"sync"
coreerr "forge.lthn.ai/core/go-log"
cli "forge.lthn.ai/core/cli/pkg/cli"
store "forge.lthn.ai/core/go-store"
@ -40,7 +41,7 @@ func runExplorer(dataDir, seed string, testnet bool) error {
dbPath := filepath.Join(dataDir, "chain.db")
s, err := store.New(dbPath)
if err != nil {
return fmt.Errorf("open store: %w", err)
return coreerr.E("runExplorer", "open store", err)
}
defer s.Close()

View file

@ -15,6 +15,8 @@ import (
"sync"
"syscall"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/chain"
"forge.lthn.ai/core/go-process"
store "forge.lthn.ai/core/go-store"
@ -56,7 +58,7 @@ func runSyncForeground(dataDir, seed string, testnet bool) error {
dbPath := filepath.Join(dataDir, "chain.db")
s, err := store.New(dbPath)
if err != nil {
return fmt.Errorf("open store: %w", err)
return coreerr.E("runSyncForeground", "open store", err)
}
defer s.Close()
@ -89,14 +91,14 @@ func runSyncDaemon(dataDir, seed string, testnet bool) error {
})
if err := d.Start(); err != nil {
return fmt.Errorf("daemon start: %w", err)
return coreerr.E("runSyncDaemon", "daemon start", err)
}
dbPath := filepath.Join(dataDir, "chain.db")
s, err := store.New(dbPath)
if err != nil {
_ = d.Stop()
return fmt.Errorf("open store: %w", err)
return coreerr.E("runSyncDaemon", "open store", err)
}
defer s.Close()
@ -125,16 +127,16 @@ func stopSyncDaemon(dataDir string) error {
pidFile := filepath.Join(dataDir, "sync.pid")
pid, running := process.ReadPID(pidFile)
if pid == 0 || !running {
return fmt.Errorf("no running sync daemon found")
return coreerr.E("stopSyncDaemon", "no running sync daemon found", nil)
}
proc, err := os.FindProcess(pid)
if err != nil {
return fmt.Errorf("find process %d: %w", pid, err)
return coreerr.E("stopSyncDaemon", fmt.Sprintf("find process %d", pid), err)
}
if err := proc.Signal(syscall.SIGTERM); err != nil {
return fmt.Errorf("signal process %d: %w", pid, err)
return coreerr.E("stopSyncDaemon", fmt.Sprintf("signal process %d", pid), err)
}
log.Printf("Sent SIGTERM to sync daemon (PID %d)", pid)

View file

@ -6,10 +6,12 @@
package blockchain
import (
"fmt"
"os"
"path/filepath"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config"
"github.com/spf13/cobra"
)
@ -60,8 +62,8 @@ func defaultDataDir() string {
}
func ensureDataDir(dataDir string) error {
if err := os.MkdirAll(dataDir, 0o755); err != nil {
return fmt.Errorf("create data dir: %w", err)
if err := coreio.Local.EnsureDir(dataDir); err != nil {
return coreerr.E("ensureDataDir", "create data dir", err)
}
return nil
}

View file

@ -9,6 +9,8 @@ import (
"fmt"
"slices"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/types"
)
@ -28,8 +30,8 @@ func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, rec
limit = config.PosBlockFutureTimeLimit
}
if blockTimestamp > adjustedTime+limit {
return fmt.Errorf("%w: %d > %d + %d", ErrTimestampFuture,
blockTimestamp, adjustedTime, limit)
return coreerr.E("CheckTimestamp", fmt.Sprintf("%d > %d + %d",
blockTimestamp, adjustedTime, limit), ErrTimestampFuture)
}
// Median check — only when we have enough history.
@ -39,8 +41,8 @@ func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, rec
median := medianTimestamp(recentTimestamps)
if blockTimestamp < median {
return fmt.Errorf("%w: %d < median %d", ErrTimestampOld,
blockTimestamp, median)
return coreerr.E("CheckTimestamp", fmt.Sprintf("%d < median %d",
blockTimestamp, median), ErrTimestampOld)
}
return nil
@ -64,16 +66,16 @@ func medianTimestamp(timestamps []uint64) uint64 {
// 2 inputs (TxInputGenesis + stake input).
func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFork) error {
if len(tx.Vin) == 0 {
return fmt.Errorf("%w: no inputs", ErrMinerTxInputs)
return coreerr.E("ValidateMinerTx", "no inputs", ErrMinerTxInputs)
}
// First input must be TxInputGenesis.
gen, ok := tx.Vin[0].(types.TxInputGenesis)
if !ok {
return fmt.Errorf("%w: first input is not txin_gen", ErrMinerTxInputs)
return coreerr.E("ValidateMinerTx", "first input is not txin_gen", ErrMinerTxInputs)
}
if gen.Height != height {
return fmt.Errorf("%w: got %d, expected %d", ErrMinerTxHeight, gen.Height, height)
return coreerr.E("ValidateMinerTx", fmt.Sprintf("got %d, expected %d", gen.Height, height), ErrMinerTxHeight)
}
// PoW blocks: exactly 1 input. PoS: exactly 2.
@ -87,12 +89,12 @@ func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFo
default:
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
if !hf4Active {
return fmt.Errorf("%w: invalid PoS stake input type", ErrMinerTxInputs)
return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs)
}
// Post-HF4: accept ZC inputs.
}
} else {
return fmt.Errorf("%w: %d inputs (expected 1 or 2)", ErrMinerTxInputs, len(tx.Vin))
return coreerr.E("ValidateMinerTx", fmt.Sprintf("%d inputs (expected 1 or 2)", len(tx.Vin)), ErrMinerTxInputs)
}
return nil
@ -119,7 +121,7 @@ func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSi
}
if outputSum > expected {
return fmt.Errorf("%w: outputs %d > expected %d", ErrRewardMismatch, outputSum, expected)
return coreerr.E("ValidateBlockReward", fmt.Sprintf("outputs %d > expected %d", outputSum, expected), ErrRewardMismatch)
}
return nil

View file

@ -9,6 +9,8 @@ import (
"fmt"
"math"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types"
)
@ -31,7 +33,7 @@ func TxFee(tx *types.Transaction) (uint64, error) {
}
if outputSum > inputSum {
return 0, fmt.Errorf("%w: inputs=%d, outputs=%d", ErrNegativeFee, inputSum, outputSum)
return 0, coreerr.E("TxFee", fmt.Sprintf("inputs=%d, outputs=%d", inputSum, outputSum), ErrNegativeFee)
}
return inputSum - outputSum, nil

View file

@ -6,10 +6,11 @@
package consensus
import (
"errors"
"fmt"
"math/bits"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config"
)
@ -43,7 +44,7 @@ func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
}
if blockSize > 2*effectiveMedian {
return 0, fmt.Errorf("consensus: block size %d too large for median %d", blockSize, effectiveMedian)
return 0, coreerr.E("BlockReward", fmt.Sprintf("consensus: block size %d too large for median %d", blockSize, effectiveMedian), nil)
}
// penalty = baseReward * (2*median - size) * size / median²
@ -56,7 +57,7 @@ func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
// Since hi1 should be 0 for reasonable block sizes, simplify:
if hi1 > 0 {
return 0, errors.New("consensus: reward overflow")
return 0, coreerr.E("BlockReward", "consensus: reward overflow", nil)
}
hi2, lo2 := bits.Mul64(baseReward, lo1)

View file

@ -8,6 +8,8 @@ package consensus
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/types"
)
@ -15,17 +17,11 @@ import (
// ValidateTransaction performs semantic validation on a regular (non-coinbase)
// transaction. Checks are ordered to match the C++ validate_tx_semantic().
func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error {
hf1Active := config.IsHardForkActive(forks, config.HF1, height)
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
// 0. Transaction version for current hardfork.
if err := checkTxVersion(tx, forks, height); err != nil {
return err
}
// 1. Blob size.
if uint64(len(txBlob)) >= config.MaxTransactionBlobSize {
return fmt.Errorf("%w: %d bytes", ErrTxTooLarge, len(txBlob))
return coreerr.E("ValidateTransaction", fmt.Sprintf("%d bytes", len(txBlob)), ErrTxTooLarge)
}
// 2. Input count.
@ -33,16 +29,16 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
return ErrNoInputs
}
if uint64(len(tx.Vin)) > config.TxMaxAllowedInputs {
return fmt.Errorf("%w: %d", ErrTooManyInputs, len(tx.Vin))
return coreerr.E("ValidateTransaction", fmt.Sprintf("%d", len(tx.Vin)), ErrTooManyInputs)
}
// 3. Input types — TxInputGenesis not allowed in regular transactions.
if err := checkInputTypes(tx, hf1Active, hf4Active); err != nil {
if err := checkInputTypes(tx, hf4Active); err != nil {
return err
}
// 4. Output validation.
if err := checkOutputs(tx, hf1Active, hf4Active); err != nil {
if err := checkOutputs(tx, hf4Active); err != nil {
return err
}
@ -69,55 +65,41 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
return nil
}
func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error {
func checkInputTypes(tx *types.Transaction, hf4Active bool) error {
for _, vin := range tx.Vin {
switch vin.(type) {
case types.TxInputToKey:
// Always valid.
case types.TxInputGenesis:
return fmt.Errorf("%w: txin_gen in regular transaction", ErrInvalidInputType)
case types.TxInputHTLC, types.TxInputMultisig:
if !hf1Active {
return fmt.Errorf("%w: tag %d pre-HF1", ErrInvalidInputType, vin.InputType())
}
return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType)
default:
// Future types (ZC, etc.) — accept if HF4+.
// Future types (multisig, HTLC, ZC) — accept if HF4+.
if !hf4Active {
return fmt.Errorf("%w: tag %d pre-HF4", ErrInvalidInputType, vin.InputType())
return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF4", vin.InputType()), ErrInvalidInputType)
}
}
}
return nil
}
func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error {
func checkOutputs(tx *types.Transaction, hf4Active bool) error {
if len(tx.Vout) == 0 {
return ErrNoOutputs
}
if hf4Active && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs {
return fmt.Errorf("%w: %d (min %d)", ErrTooFewOutputs, len(tx.Vout), config.TxMinAllowedOutputs)
return coreerr.E("checkOutputs", fmt.Sprintf("%d (min %d)", len(tx.Vout), config.TxMinAllowedOutputs), ErrTooFewOutputs)
}
if uint64(len(tx.Vout)) > config.TxMaxAllowedOutputs {
return fmt.Errorf("%w: %d", ErrTooManyOutputs, len(tx.Vout))
return coreerr.E("checkOutputs", fmt.Sprintf("%d", len(tx.Vout)), ErrTooManyOutputs)
}
for i, vout := range tx.Vout {
switch o := vout.(type) {
case types.TxOutputBare:
if o.Amount == 0 {
return fmt.Errorf("%w: output %d has zero amount", ErrInvalidOutput, i)
}
// Check target type gating.
switch o.Target.(type) {
case types.TxOutToKey:
// Always valid.
case types.TxOutMultisig, types.TxOutHTLC:
if !hf1Active {
return fmt.Errorf("%w: output %d has target type %d pre-HF1",
ErrInvalidOutput, i, o.Target.TargetType())
}
return coreerr.E("checkOutputs", fmt.Sprintf("output %d has zero amount", i), ErrInvalidOutput)
}
case types.TxOutputZarcanum:
// Validated by proof verification.
@ -127,45 +109,17 @@ func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error {
return nil
}
// checkTxVersion validates that the transaction version is correct for the
// current hardfork era. After HF5, version must be 3. Before HF5, version 3
// is rejected.
func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint64) error {
hf5Active := config.IsHardForkActive(forks, config.HF5, height)
if hf5Active {
// After HF5: must be version 3.
if tx.Version != types.VersionPostHF5 {
return fmt.Errorf("%w: got version %d, require %d after HF5",
ErrTxVersionInvalid, tx.Version, types.VersionPostHF5)
}
} else {
// Before HF5: version 3 is not allowed.
if tx.Version >= types.VersionPostHF5 {
return fmt.Errorf("%w: version %d not allowed before HF5",
ErrTxVersionInvalid, tx.Version)
}
}
return nil
}
func checkKeyImages(tx *types.Transaction) error {
seen := make(map[types.KeyImage]struct{})
for _, vin := range tx.Vin {
var ki types.KeyImage
switch v := vin.(type) {
case types.TxInputToKey:
ki = v.KeyImage
case types.TxInputHTLC:
ki = v.KeyImage
default:
toKey, ok := vin.(types.TxInputToKey)
if !ok {
continue
}
if _, exists := seen[ki]; exists {
return fmt.Errorf("%w: %s", ErrDuplicateKeyImage, ki)
if _, exists := seen[toKey.KeyImage]; exists {
return coreerr.E("checkKeyImages", toKey.KeyImage.String(), ErrDuplicateKeyImage)
}
seen[ki] = struct{}{}
seen[toKey.KeyImage] = struct{}{}
}
return nil
}

View file

@ -9,6 +9,8 @@ import (
"bytes"
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
)
@ -38,14 +40,14 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
dec := wire.NewDecoder(bytes.NewReader(raw))
count := dec.ReadVarint()
if dec.Err() != nil {
return nil, fmt.Errorf("read sig count: %w", dec.Err())
return nil, coreerr.E("parseV2Signatures", "read sig count", dec.Err())
}
entries := make([]v2SigEntry, 0, count)
for i := uint64(0); i < count; i++ {
tag := dec.ReadUint8()
if dec.Err() != nil {
return nil, fmt.Errorf("read sig tag %d: %w", i, dec.Err())
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("read sig tag %d", i), dec.Err())
}
entry := v2SigEntry{tag: tag}
@ -54,7 +56,7 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
case types.SigTypeZC:
zc, err := parseZCSig(dec)
if err != nil {
return nil, fmt.Errorf("parse ZC_sig %d: %w", i, err)
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("parse ZC_sig %d", i), err)
}
entry.zcSig = zc
@ -74,11 +76,11 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
skipZarcanumSig(dec)
default:
return nil, fmt.Errorf("unsupported sig tag 0x%02x", tag)
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("unsupported sig tag 0x%02x", tag), nil)
}
if dec.Err() != nil {
return nil, fmt.Errorf("parse sig %d (tag 0x%02x): %w", i, tag, dec.Err())
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("parse sig %d (tag 0x%02x)", i, tag), dec.Err())
}
entries = append(entries, entry)
}
@ -117,7 +119,7 @@ func parseZCSig(dec *wire.Decoder) (*zcSigData, error) {
}
if rgCount != rxCount {
return nil, fmt.Errorf("CLSAG r_g count %d != r_x count %d", rgCount, rxCount)
return nil, coreerr.E("parseZCSig", fmt.Sprintf("CLSAG r_g count %d != r_x count %d", rgCount, rxCount), nil)
}
zc.ringSize = int(rgCount)
@ -196,48 +198,48 @@ func parseV2Proofs(raw []byte) (*v2ProofData, error) {
dec := wire.NewDecoder(bytes.NewReader(raw))
count := dec.ReadVarint()
if dec.Err() != nil {
return nil, fmt.Errorf("read proof count: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "read proof count", dec.Err())
}
var data v2ProofData
for i := uint64(0); i < count; i++ {
tag := dec.ReadUint8()
if dec.Err() != nil {
return nil, fmt.Errorf("read proof tag %d: %w", i, dec.Err())
return nil, coreerr.E("parseV2Proofs", fmt.Sprintf("read proof tag %d", i), dec.Err())
}
switch tag {
case 46: // zc_asset_surjection_proof: varint(nBGE) + nBGE * BGE_proof
nBGE := dec.ReadVarint()
if dec.Err() != nil {
return nil, fmt.Errorf("parse BGE count: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "parse BGE count", dec.Err())
}
data.bgeProofs = make([][]byte, nBGE)
for j := uint64(0); j < nBGE; j++ {
data.bgeProofs[j] = readBGEProofBytes(dec)
if dec.Err() != nil {
return nil, fmt.Errorf("parse BGE proof %d: %w", j, dec.Err())
return nil, coreerr.E("parseV2Proofs", fmt.Sprintf("parse BGE proof %d", j), dec.Err())
}
}
case 47: // zc_outs_range_proof: bpp_serialized + aggregation_proof
data.bppProofBytes = readBPPBytes(dec)
if dec.Err() != nil {
return nil, fmt.Errorf("parse BPP proof: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "parse BPP proof", dec.Err())
}
data.bppCommitments = readAggregationCommitments(dec)
if dec.Err() != nil {
return nil, fmt.Errorf("parse aggregation proof: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "parse aggregation proof", dec.Err())
}
case 48: // zc_balance_proof: 96 bytes (c, y0, y1)
data.balanceProof = dec.ReadBytes(96)
if dec.Err() != nil {
return nil, fmt.Errorf("parse balance proof: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "parse balance proof", dec.Err())
}
default:
return nil, fmt.Errorf("unsupported proof tag 0x%02x", tag)
return nil, coreerr.E("parseV2Proofs", fmt.Sprintf("unsupported proof tag 0x%02x", tag), nil)
}
}

View file

@ -6,9 +6,10 @@
package consensus
import (
"errors"
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/types"
@ -58,20 +59,17 @@ func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork,
}
// verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions.
// Both TxInputToKey and TxInputHTLC use NLSAG ring signatures.
func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) error {
// Count ring-sig inputs (TxInputToKey and TxInputHTLC).
// Count key inputs.
var keyInputCount int
for _, vin := range tx.Vin {
switch vin.(type) {
case types.TxInputToKey, types.TxInputHTLC:
if _, ok := vin.(types.TxInputToKey); ok {
keyInputCount++
}
}
if len(tx.Signatures) != keyInputCount {
return fmt.Errorf("consensus: signature count %d != input count %d",
len(tx.Signatures), keyInputCount)
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: signature count %d != input count %d", len(tx.Signatures), keyInputCount), nil)
}
// Actual NLSAG verification requires the crypto bridge and ring outputs.
@ -84,40 +82,25 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err
var sigIdx int
for _, vin := range tx.Vin {
// Extract the common ring-sig fields from either input type.
var amount uint64
var keyOffsets []types.TxOutRef
var keyImage types.KeyImage
switch v := vin.(type) {
case types.TxInputToKey:
amount = v.Amount
keyOffsets = v.KeyOffsets
keyImage = v.KeyImage
case types.TxInputHTLC:
amount = v.Amount
keyOffsets = v.KeyOffsets
keyImage = v.KeyImage
default:
inp, ok := vin.(types.TxInputToKey)
if !ok {
continue
}
// Extract absolute global indices from key offsets.
offsets := make([]uint64, len(keyOffsets))
for i, ref := range keyOffsets {
offsets := make([]uint64, len(inp.KeyOffsets))
for i, ref := range inp.KeyOffsets {
offsets[i] = ref.GlobalIndex
}
ringKeys, err := getRingOutputs(amount, offsets)
ringKeys, err := getRingOutputs(inp.Amount, offsets)
if err != nil {
return fmt.Errorf("consensus: failed to fetch ring outputs for input %d: %w",
sigIdx, err)
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: failed to fetch ring outputs for input %d", sigIdx), err)
}
ringSigs := tx.Signatures[sigIdx]
if len(ringSigs) != len(ringKeys) {
return fmt.Errorf("consensus: input %d has %d signatures but ring size %d",
sigIdx, len(ringSigs), len(ringKeys))
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: input %d has %d signatures but ring size %d", sigIdx, len(ringSigs), len(ringKeys)), nil)
}
// Convert typed slices to raw byte arrays for the crypto bridge.
@ -131,8 +114,8 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err
sigs[i] = [64]byte(s)
}
if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(keyImage), pubs, sigs) {
return fmt.Errorf("consensus: ring signature verification failed for input %d", sigIdx)
if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(inp.KeyImage), pubs, sigs) {
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: ring signature verification failed for input %d", sigIdx), nil)
}
sigIdx++
@ -146,13 +129,12 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
// Parse the signature variant vector.
sigEntries, err := parseV2Signatures(tx.SignaturesRaw)
if err != nil {
return fmt.Errorf("consensus: %w", err)
return coreerr.E("verifyV2Signatures", "consensus", err)
}
// Match signatures to inputs: each input must have a corresponding signature.
if len(sigEntries) != len(tx.Vin) {
return fmt.Errorf("consensus: V2 signature count %d != input count %d",
len(sigEntries), len(tx.Vin))
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: V2 signature count %d != input count %d", len(sigEntries), len(tx.Vin)), nil)
}
// Validate that ZC inputs have ZC_sig and vice versa.
@ -160,13 +142,11 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
switch vin.(type) {
case types.TxInputZC:
if sigEntries[i].tag != types.SigTypeZC {
return fmt.Errorf("consensus: input %d is ZC but signature tag is 0x%02x",
i, sigEntries[i].tag)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is ZC but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
}
case types.TxInputToKey:
if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid {
return fmt.Errorf("consensus: input %d is to_key but signature tag is 0x%02x",
i, sigEntries[i].tag)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is to_key but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
}
}
}
@ -190,7 +170,7 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
zc := sigEntries[i].zcSig
if zc == nil {
return fmt.Errorf("consensus: input %d: missing ZC_sig data", i)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d: missing ZC_sig data", i), nil)
}
// Extract absolute global indices from key offsets.
@ -201,12 +181,11 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
ringMembers, err := getZCRingOutputs(offsets)
if err != nil {
return fmt.Errorf("consensus: failed to fetch ZC ring outputs for input %d: %w", i, err)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: failed to fetch ZC ring outputs for input %d", i), err)
}
if len(ringMembers) != zc.ringSize {
return fmt.Errorf("consensus: input %d: ring size %d from chain != %d from sig",
i, len(ringMembers), zc.ringSize)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d: ring size %d from chain != %d from sig", i, len(ringMembers), zc.ringSize), nil)
}
// Build flat ring: [stealth(32) | commitment(32) | blinded_asset_id(32)] per entry.
@ -225,20 +204,20 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
[32]byte(zcIn.KeyImage),
zc.clsagFlatSig,
) {
return fmt.Errorf("consensus: CLSAG GGX verification failed for input %d", i)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: CLSAG GGX verification failed for input %d", i), nil)
}
}
// Parse and verify proofs.
proofs, err := parseV2Proofs(tx.Proofs)
if err != nil {
return fmt.Errorf("consensus: %w", err)
return coreerr.E("verifyV2Signatures", "consensus", err)
}
// Verify BPP range proof if present.
if len(proofs.bppProofBytes) > 0 && len(proofs.bppCommitments) > 0 {
if !crypto.VerifyBPP(proofs.bppProofBytes, proofs.bppCommitments) {
return errors.New("consensus: BPP range proof verification failed")
return coreerr.E("verifyV2Signatures", "consensus: BPP range proof verification failed", nil)
}
}
@ -279,8 +258,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
}
if len(proofs.bgeProofs) != len(outputAssetIDs) {
return fmt.Errorf("consensus: BGE proof count %d != Zarcanum output count %d",
len(proofs.bgeProofs), len(outputAssetIDs))
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE proof count %d != Zarcanum output count %d", len(proofs.bgeProofs), len(outputAssetIDs)), nil)
}
// Collect pseudo-out asset IDs from ZC signatures and expand to full points.
@ -296,7 +274,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
for i, p := range pseudoOutAssetIDs {
full, err := crypto.PointMul8(p)
if err != nil {
return fmt.Errorf("consensus: mul8 pseudo-out asset ID %d: %w", i, err)
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: mul8 pseudo-out asset ID %d", i), err)
}
mul8PseudoOuts[i] = full
}
@ -307,7 +285,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
// mul8 the output's blinded asset ID.
mul8Out, err := crypto.PointMul8(outAssetID)
if err != nil {
return fmt.Errorf("consensus: mul8 output asset ID %d: %w", j, err)
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: mul8 output asset ID %d", j), err)
}
// ring[i] = mul8(pseudo_out_i) - mul8(output_j)
@ -315,13 +293,13 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
for i, mul8Pseudo := range mul8PseudoOuts {
diff, err := crypto.PointSub(mul8Pseudo, mul8Out)
if err != nil {
return fmt.Errorf("consensus: BGE ring[%d][%d] sub: %w", j, i, err)
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE ring[%d][%d] sub", j, i), err)
}
ring[i] = diff
}
if !crypto.VerifyBGE(context, ring, proofs.bgeProofs[j]) {
return fmt.Errorf("consensus: BGE proof verification failed for output %d", j)
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE proof verification failed for output %d", j), nil)
}
}

View file

@ -15,6 +15,8 @@ import (
"sync/atomic"
"time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/consensus"
"forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/rpc"
@ -139,18 +141,18 @@ func (m *Miner) Start(ctx context.Context) error {
// Parse difficulty.
diff, err := strconv.ParseUint(tmpl.Difficulty, 10, 64)
if err != nil {
return fmt.Errorf("mining: invalid difficulty %q: %w", tmpl.Difficulty, err)
return coreerr.E("Miner.Start", fmt.Sprintf("mining: invalid difficulty %q", tmpl.Difficulty), err)
}
// Decode the block template blob.
blobBytes, err := hex.DecodeString(tmpl.BlockTemplateBlob)
if err != nil {
return fmt.Errorf("mining: invalid template blob hex: %w", err)
return coreerr.E("Miner.Start", "mining: invalid template blob hex", err)
}
dec := wire.NewDecoder(bytes.NewReader(blobBytes))
block := wire.DecodeBlock(dec)
if dec.Err() != nil {
return fmt.Errorf("mining: decode template: %w", dec.Err())
return coreerr.E("Miner.Start", "mining: decode template", dec.Err())
}
// Update stats.
@ -202,7 +204,7 @@ func (m *Miner) mine(ctx context.Context, block *types.Block, headerHash [32]byt
powHash, err := crypto.RandomXHash(RandomXKey, input[:])
if err != nil {
return fmt.Errorf("mining: RandomX hash: %w", err)
return coreerr.E("Miner.mine", "mining: RandomX hash", err)
}
m.hashCount.Add(1)
@ -215,12 +217,12 @@ func (m *Miner) mine(ctx context.Context, block *types.Block, headerHash [32]byt
enc := wire.NewEncoder(&buf)
wire.EncodeBlock(enc, block)
if enc.Err() != nil {
return fmt.Errorf("mining: encode solution: %w", enc.Err())
return coreerr.E("Miner.mine", "mining: encode solution", enc.Err())
}
hexBlob := hex.EncodeToString(buf.Bytes())
if err := m.provider.SubmitBlock(hexBlob); err != nil {
return fmt.Errorf("mining: submit block: %w", err)
return coreerr.E("Miner.mine", "mining: submit block", err)
}
m.blocksFound.Add(1)

View file

@ -5,7 +5,11 @@
package rpc
import "fmt"
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
)
// GetLastBlockHeader returns the header of the most recent block.
func (c *Client) GetLastBlockHeader() (*BlockHeader, error) {
@ -17,7 +21,7 @@ func (c *Client) GetLastBlockHeader() (*BlockHeader, error) {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getlastblockheader: status %q", resp.Status)
return nil, coreerr.E("Client.GetLastBlockHeader", fmt.Sprintf("getlastblockheader: status %q", resp.Status), nil)
}
return &resp.BlockHeader, nil
}
@ -35,7 +39,7 @@ func (c *Client) GetBlockHeaderByHeight(height uint64) (*BlockHeader, error) {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getblockheaderbyheight: status %q", resp.Status)
return nil, coreerr.E("Client.GetBlockHeaderByHeight", fmt.Sprintf("getblockheaderbyheight: status %q", resp.Status), nil)
}
return &resp.BlockHeader, nil
}
@ -53,7 +57,7 @@ func (c *Client) GetBlockHeaderByHash(hash string) (*BlockHeader, error) {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getblockheaderbyhash: status %q", resp.Status)
return nil, coreerr.E("Client.GetBlockHeaderByHash", fmt.Sprintf("getblockheaderbyhash: status %q", resp.Status), nil)
}
return &resp.BlockHeader, nil
}
@ -73,7 +77,7 @@ func (c *Client) GetBlocksDetails(heightStart, count uint64) ([]BlockDetails, er
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("get_blocks_details: status %q", resp.Status)
return nil, coreerr.E("Client.GetBlocksDetails", fmt.Sprintf("get_blocks_details: status %q", resp.Status), nil)
}
return resp.Blocks, nil
}

View file

@ -14,6 +14,8 @@ import (
"net/http"
"net/url"
"time"
coreerr "forge.lthn.ai/core/go-log"
)
// Client is a Lethean daemon RPC client.
@ -86,27 +88,27 @@ func (c *Client) call(method string, params any, result any) error {
Params: params,
})
if err != nil {
return fmt.Errorf("marshal request: %w", err)
return coreerr.E("Client.call", "marshal request", err)
}
resp, err := c.httpClient.Post(c.url, "application/json", bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("post %s: %w", method, err)
return coreerr.E("Client.call", fmt.Sprintf("post %s", method), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http %d from %s", resp.StatusCode, method)
return coreerr.E("Client.call", fmt.Sprintf("http %d from %s", resp.StatusCode, method), nil)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
return coreerr.E("Client.call", "read response", err)
}
var rpcResp jsonRPCResponse
if err := json.Unmarshal(body, &rpcResp); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
return coreerr.E("Client.call", "unmarshal response", err)
}
if rpcResp.Error != nil {
@ -115,7 +117,7 @@ func (c *Client) call(method string, params any, result any) error {
if result != nil && len(rpcResp.Result) > 0 {
if err := json.Unmarshal(rpcResp.Result, result); err != nil {
return fmt.Errorf("unmarshal result: %w", err)
return coreerr.E("Client.call", "unmarshal result", err)
}
}
return nil
@ -125,28 +127,28 @@ func (c *Client) call(method string, params any, result any) error {
func (c *Client) legacyCall(path string, params any, result any) error {
reqBody, err := json.Marshal(params)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
return coreerr.E("Client.legacyCall", "marshal request", err)
}
url := c.baseURL + path
resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("post %s: %w", path, err)
return coreerr.E("Client.legacyCall", fmt.Sprintf("post %s", path), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http %d from %s", resp.StatusCode, path)
return coreerr.E("Client.legacyCall", fmt.Sprintf("http %d from %s", resp.StatusCode, path), nil)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
return coreerr.E("Client.legacyCall", "read response", err)
}
if result != nil {
if err := json.Unmarshal(body, result); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
return coreerr.E("Client.legacyCall", "unmarshal response", err)
}
}
return nil

View file

@ -5,7 +5,11 @@
package rpc
import "fmt"
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
)
// GetInfo returns the daemon status.
// Uses flags=0 for the cheapest query (no expensive calculations).
@ -21,7 +25,7 @@ func (c *Client) GetInfo() (*DaemonInfo, error) {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getinfo: status %q", resp.Status)
return nil, coreerr.E("Client.GetInfo", fmt.Sprintf("getinfo: status %q", resp.Status), nil)
}
return &resp.DaemonInfo, nil
}
@ -37,7 +41,7 @@ func (c *Client) GetHeight() (uint64, error) {
return 0, err
}
if resp.Status != "OK" {
return 0, fmt.Errorf("getheight: status %q", resp.Status)
return 0, coreerr.E("Client.GetHeight", fmt.Sprintf("getheight: status %q", resp.Status), nil)
}
return resp.Height, nil
}
@ -52,7 +56,7 @@ func (c *Client) GetBlockCount() (uint64, error) {
return 0, err
}
if resp.Status != "OK" {
return 0, fmt.Errorf("getblockcount: status %q", resp.Status)
return 0, coreerr.E("Client.GetBlockCount", fmt.Sprintf("getblockcount: status %q", resp.Status), nil)
}
return resp.Count, nil
}

View file

@ -5,7 +5,11 @@
package rpc
import "fmt"
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
)
// SubmitBlock submits a mined block to the daemon.
// The hexBlob is the hex-encoded serialised block.
@ -20,7 +24,7 @@ func (c *Client) SubmitBlock(hexBlob string) error {
return err
}
if resp.Status != "OK" {
return fmt.Errorf("submitblock: status %q", resp.Status)
return coreerr.E("Client.SubmitBlock", fmt.Sprintf("submitblock: status %q", resp.Status), nil)
}
return nil
}
@ -47,7 +51,7 @@ func (c *Client) GetBlockTemplate(walletAddr string) (*BlockTemplateResponse, er
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getblocktemplate: status %q", resp.Status)
return nil, coreerr.E("Client.GetBlockTemplate", fmt.Sprintf("getblocktemplate: status %q", resp.Status), nil)
}
return &resp, nil
}

View file

@ -5,7 +5,11 @@
package rpc
import "fmt"
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
)
// GetTxDetails returns detailed information about a transaction.
func (c *Client) GetTxDetails(txHash string) (*TxInfo, error) {
@ -20,7 +24,7 @@ func (c *Client) GetTxDetails(txHash string) (*TxInfo, error) {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("get_tx_details: status %q", resp.Status)
return nil, coreerr.E("Client.GetTxDetails", fmt.Sprintf("get_tx_details: status %q", resp.Status), nil)
}
return &resp.TxInfo, nil
}
@ -41,7 +45,7 @@ func (c *Client) GetTransactions(hashes []string) (txsHex []string, missed []str
return nil, nil, err
}
if resp.Status != "OK" {
return nil, nil, fmt.Errorf("gettransactions: status %q", resp.Status)
return nil, nil, coreerr.E("Client.GetTransactions", fmt.Sprintf("gettransactions: status %q", resp.Status), nil)
}
return resp.TxsAsHex, resp.MissedTx, nil
}

View file

@ -8,6 +8,8 @@ package rpc
import (
"encoding/hex"
"fmt"
coreerr "forge.lthn.ai/core/go-log"
)
// RandomOutputEntry is a decoy output returned by getrandom_outs.
@ -33,7 +35,7 @@ func (c *Client) GetRandomOutputs(amount uint64, count int) ([]RandomOutputEntry
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getrandom_outs: status %q", resp.Status)
return nil, coreerr.E("Client.GetRandomOutputs", fmt.Sprintf("getrandom_outs: status %q", resp.Status), nil)
}
return resp.Outs, nil
}
@ -53,7 +55,7 @@ func (c *Client) SendRawTransaction(txBlob []byte) error {
return err
}
if resp.Status != "OK" {
return fmt.Errorf("sendrawtransaction: status %q", resp.Status)
return coreerr.E("Client.SendRawTransaction", fmt.Sprintf("sendrawtransaction: status %q", resp.Status), nil)
}
return nil
}

View file

@ -14,6 +14,8 @@ import (
"net"
"time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/chain"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/p2p"
@ -54,7 +56,7 @@ func syncLoop(ctx context.Context, c *chain.Chain, cfg *config.ChainConfig, fork
func syncOnce(ctx context.Context, c *chain.Chain, cfg *config.ChainConfig, opts chain.SyncOptions, seed string) error {
conn, err := net.DialTimeout("tcp", seed, 10*time.Second)
if err != nil {
return fmt.Errorf("dial %s: %w", seed, err)
return coreerr.E("syncOnce", fmt.Sprintf("dial %s", seed), err)
}
defer conn.Close()
@ -81,23 +83,23 @@ func syncOnce(ctx context.Context, c *chain.Chain, cfg *config.ChainConfig, opts
}
payload, err := p2p.EncodeHandshakeRequest(&req)
if err != nil {
return fmt.Errorf("encode handshake: %w", err)
return coreerr.E("syncOnce", "encode handshake", err)
}
if err := lc.WritePacket(p2p.CommandHandshake, payload, true); err != nil {
return fmt.Errorf("write handshake: %w", err)
return coreerr.E("syncOnce", "write handshake", err)
}
hdr, data, err := lc.ReadPacket()
if err != nil {
return fmt.Errorf("read handshake: %w", err)
return coreerr.E("syncOnce", "read handshake", err)
}
if hdr.Command != uint32(p2p.CommandHandshake) {
return fmt.Errorf("unexpected command %d", hdr.Command)
return coreerr.E("syncOnce", fmt.Sprintf("unexpected command %d", hdr.Command), nil)
}
var resp p2p.HandshakeResponse
if err := resp.Decode(data); err != nil {
return fmt.Errorf("decode handshake: %w", err)
return coreerr.E("syncOnce", "decode handshake", err)
}
localSync := p2p.CoreSyncData{

View file

@ -10,8 +10,8 @@ import (
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
cli "forge.lthn.ai/core/cli/pkg/cli"
tea "github.com/charmbracelet/bubbletea"
"forge.lthn.ai/core/go-blockchain/chain"
"forge.lthn.ai/core/go-blockchain/types"

View file

@ -10,10 +10,11 @@
package types
import (
"errors"
"fmt"
"math/big"
coreerr "forge.lthn.ai/core/go-log"
"golang.org/x/crypto/sha3"
"forge.lthn.ai/core/go-blockchain/config"
@ -81,24 +82,24 @@ func (a *Address) Encode(prefix uint64) string {
func DecodeAddress(s string) (*Address, uint64, error) {
raw, err := base58Decode(s)
if err != nil {
return nil, 0, fmt.Errorf("types: base58 decode failed: %w", err)
return nil, 0, coreerr.E("DecodeAddress", "types: base58 decode failed", err)
}
// The minimum size is: 1 byte prefix varint + 32 + 32 + 1 flags + 4 checksum = 70.
if len(raw) < 70 {
return nil, 0, errors.New("types: address data too short")
return nil, 0, coreerr.E("DecodeAddress", "types: address data too short", nil)
}
// Decode the prefix varint.
prefix, prefixLen, err := decodeVarint(raw)
if err != nil {
return nil, 0, fmt.Errorf("types: invalid address prefix varint: %w", err)
return nil, 0, coreerr.E("DecodeAddress", "types: invalid address prefix varint", err)
}
// After the prefix we need exactly 32+32+1+4 = 69 bytes.
remaining := raw[prefixLen:]
if len(remaining) != 69 {
return nil, 0, fmt.Errorf("types: unexpected address data length: want 69 bytes after prefix, got %d", len(remaining))
return nil, 0, coreerr.E("DecodeAddress", fmt.Sprintf("types: unexpected address data length: want 69 bytes after prefix, got %d", len(remaining)), nil)
}
// Validate checksum: Keccak-256 of everything except the last 4 bytes.
@ -109,7 +110,7 @@ func DecodeAddress(s string) (*Address, uint64, error) {
expectedChecksum[1] != actualChecksum[1] ||
expectedChecksum[2] != actualChecksum[2] ||
expectedChecksum[3] != actualChecksum[3] {
return nil, 0, errors.New("types: address checksum mismatch")
return nil, 0, coreerr.E("DecodeAddress", "types: address checksum mismatch", nil)
}
addr := &Address{}
@ -214,7 +215,7 @@ func encodeBlock(block []byte, encodedSize int) []byte {
// base58Decode decodes a CryptoNote base58 string back into raw bytes.
func base58Decode(s string) ([]byte, error) {
if len(s) == 0 {
return nil, errors.New("types: empty base58 string")
return nil, coreerr.E("base58Decode", "types: empty base58 string", nil)
}
fullBlocks := len(s) / 11
@ -222,7 +223,7 @@ func base58Decode(s string) ([]byte, error) {
// Validate that the last block size maps to a valid byte count.
if lastBlockChars > 0 && base58ReverseBlockSizes[lastBlockChars] < 0 {
return nil, fmt.Errorf("types: invalid base58 string length %d", len(s))
return nil, coreerr.E("base58Decode", fmt.Sprintf("types: invalid base58 string length %d", len(s)), nil)
}
var result []byte
@ -257,7 +258,7 @@ func decodeBlock(s string, byteCount int) ([]byte, error) {
for _, c := range []byte(s) {
idx := base58CharIndex(c)
if idx < 0 {
return nil, fmt.Errorf("types: invalid base58 character %q", c)
return nil, coreerr.E("decodeBlock", fmt.Sprintf("types: invalid base58 character %q", c), nil)
}
num.Mul(num, base)
num.Add(num, big.NewInt(int64(idx)))
@ -266,7 +267,7 @@ func decodeBlock(s string, byteCount int) ([]byte, error) {
// Convert to fixed-size byte array, big-endian.
raw := num.Bytes()
if len(raw) > byteCount {
return nil, fmt.Errorf("types: base58 block overflow: decoded %d bytes, expected %d", len(raw), byteCount)
return nil, coreerr.E("decodeBlock", fmt.Sprintf("types: base58 block overflow: decoded %d bytes, expected %d", len(raw), byteCount), nil)
}
// Pad with leading zeroes if necessary.
@ -310,7 +311,7 @@ func encodeVarint(v uint64) []byte {
func decodeVarint(data []byte) (uint64, int, error) {
if len(data) == 0 {
return 0, 0, errors.New("types: cannot decode varint from empty data")
return 0, 0, coreerr.E("decodeVarint", "types: cannot decode varint from empty data", nil)
}
var v uint64
for i := 0; i < len(data) && i < 10; i++ {
@ -319,5 +320,5 @@ func decodeVarint(data []byte) (uint64, int, error) {
return v, i + 1, nil
}
}
return 0, 0, errors.New("types: varint overflow")
return 0, 0, coreerr.E("decodeVarint", "types: varint overflow", nil)
}

View file

@ -15,6 +15,8 @@ package types
import (
"encoding/hex"
"fmt"
coreerr "forge.lthn.ai/core/go-log"
)
// Hash is a 256-bit (32-byte) hash value, typically produced by Keccak-256.
@ -40,10 +42,10 @@ func HashFromHex(s string) (Hash, error) {
var h Hash
b, err := hex.DecodeString(s)
if err != nil {
return h, fmt.Errorf("types: invalid hex for hash: %w", err)
return h, coreerr.E("HashFromHex", "types: invalid hex for hash", err)
}
if len(b) != 32 {
return h, fmt.Errorf("types: hash hex must be 64 characters, got %d", len(s))
return h, coreerr.E("HashFromHex", fmt.Sprintf("types: hash hex must be 64 characters, got %d", len(s)), nil)
}
copy(h[:], b)
return h, nil
@ -65,10 +67,10 @@ func PublicKeyFromHex(s string) (PublicKey, error) {
var pk PublicKey
b, err := hex.DecodeString(s)
if err != nil {
return pk, fmt.Errorf("types: invalid hex for public key: %w", err)
return pk, coreerr.E("PublicKeyFromHex", "types: invalid hex for public key", err)
}
if len(b) != 32 {
return pk, fmt.Errorf("types: public key hex must be 64 characters, got %d", len(s))
return pk, coreerr.E("PublicKeyFromHex", fmt.Sprintf("types: public key hex must be 64 characters, got %d", len(s)), nil)
}
copy(pk[:], b)
return pk, nil

View file

@ -15,10 +15,10 @@ import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
coreerr "forge.lthn.ai/core/go-log"
"golang.org/x/crypto/argon2"
store "forge.lthn.ai/core/go-store"
@ -64,7 +64,7 @@ type Account struct {
func GenerateAccount() (*Account, error) {
spendPub, spendSec, err := crypto.GenerateKeys()
if err != nil {
return nil, fmt.Errorf("wallet: generate spend keys: %w", err)
return nil, coreerr.E("GenerateAccount", "wallet: generate spend keys", err)
}
return accountFromSpendKey(spendSec, spendPub)
}
@ -74,11 +74,11 @@ func GenerateAccount() (*Account, error) {
func RestoreFromSeed(phrase string) (*Account, error) {
key, err := MnemonicDecode(phrase)
if err != nil {
return nil, fmt.Errorf("wallet: restore from seed: %w", err)
return nil, coreerr.E("RestoreFromSeed", "wallet: restore from seed", err)
}
spendPub, err := crypto.SecretToPublic(key)
if err != nil {
return nil, fmt.Errorf("wallet: spend pub from secret: %w", err)
return nil, coreerr.E("RestoreFromSeed", "wallet: spend pub from secret", err)
}
return accountFromSpendKey(key, spendPub)
}
@ -88,7 +88,7 @@ func RestoreFromSeed(phrase string) (*Account, error) {
func RestoreViewOnly(viewSecret types.SecretKey, spendPublic types.PublicKey) (*Account, error) {
viewPub, err := crypto.SecretToPublic([32]byte(viewSecret))
if err != nil {
return nil, fmt.Errorf("wallet: view pub from secret: %w", err)
return nil, coreerr.E("RestoreViewOnly", "wallet: view pub from secret", err)
}
return &Account{
SpendPublicKey: spendPublic,
@ -115,28 +115,28 @@ func (a *Account) Address() types.Address {
func (a *Account) Save(s *store.Store, password string) error {
plaintext, err := json.Marshal(a)
if err != nil {
return fmt.Errorf("wallet: marshal account: %w", err)
return coreerr.E("Account.Save", "wallet: marshal account", err)
}
salt := make([]byte, saltLen)
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return fmt.Errorf("wallet: generate salt: %w", err)
return coreerr.E("Account.Save", "wallet: generate salt", err)
}
derived := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
block, err := aes.NewCipher(derived)
if err != nil {
return fmt.Errorf("wallet: aes cipher: %w", err)
return coreerr.E("Account.Save", "wallet: aes cipher", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return fmt.Errorf("wallet: gcm: %w", err)
return coreerr.E("Account.Save", "wallet: gcm", err)
}
nonce := make([]byte, nonceLen)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return fmt.Errorf("wallet: generate nonce: %w", err)
return coreerr.E("Account.Save", "wallet: generate nonce", err)
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
@ -154,16 +154,16 @@ func (a *Account) Save(s *store.Store, password string) error {
func LoadAccount(s *store.Store, password string) (*Account, error) {
encoded, err := s.Get(groupAccount, keyAccount)
if err != nil {
return nil, fmt.Errorf("wallet: load account: %w", err)
return nil, coreerr.E("LoadAccount", "wallet: load account", err)
}
blob, err := hex.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("wallet: decode account hex: %w", err)
return nil, coreerr.E("LoadAccount", "wallet: decode account hex", err)
}
if len(blob) < saltLen+nonceLen+1 {
return nil, errors.New("wallet: account data too short")
return nil, coreerr.E("LoadAccount", "wallet: account data too short", nil)
}
salt := blob[:saltLen]
@ -174,21 +174,21 @@ func LoadAccount(s *store.Store, password string) (*Account, error) {
block, err := aes.NewCipher(derived)
if err != nil {
return nil, fmt.Errorf("wallet: aes cipher: %w", err)
return nil, coreerr.E("LoadAccount", "wallet: aes cipher", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("wallet: gcm: %w", err)
return nil, coreerr.E("LoadAccount", "wallet: gcm", err)
}
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("wallet: decrypt account: %w", err)
return nil, coreerr.E("LoadAccount", "wallet: decrypt account", err)
}
var acc Account
if err := json.Unmarshal(plaintext, &acc); err != nil {
return nil, fmt.Errorf("wallet: unmarshal account: %w", err)
return nil, coreerr.E("LoadAccount", "wallet: unmarshal account", err)
}
return &acc, nil
}
@ -201,7 +201,7 @@ func accountFromSpendKey(spendSec, spendPub [32]byte) (*Account, error) {
crypto.ScReduce32(&viewSec)
viewPub, err := crypto.SecretToPublic(viewSec)
if err != nil {
return nil, fmt.Errorf("wallet: view pub from secret: %w", err)
return nil, coreerr.E("accountFromSpendKey", "wallet: view pub from secret", err)
}
return &Account{
SpendPublicKey: types.PublicKey(spendPub),

View file

@ -12,10 +12,11 @@ package wallet
import (
"bytes"
"cmp"
"errors"
"fmt"
"slices"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/types"
@ -82,15 +83,14 @@ func (b *V1Builder) Build(req *BuildRequest) (*types.Transaction, error) {
destTotal += dst.Amount
}
if sourceTotal < destTotal+req.Fee {
return nil, fmt.Errorf("wallet: insufficient funds: have %d, need %d",
sourceTotal, destTotal+req.Fee)
return nil, coreerr.E("V1Builder.Build", fmt.Sprintf("wallet: insufficient funds: have %d, need %d", sourceTotal, destTotal+req.Fee), nil)
}
change := sourceTotal - destTotal - req.Fee
// 2. Generate one-time TX key pair.
txPub, txSec, err := crypto.GenerateKeys()
if err != nil {
return nil, fmt.Errorf("wallet: generate tx keys: %w", err)
return nil, coreerr.E("V1Builder.Build", "wallet: generate tx keys", err)
}
tx := &types.Transaction{Version: types.VersionPreHF4}
@ -101,7 +101,7 @@ func (b *V1Builder) Build(req *BuildRequest) (*types.Transaction, error) {
for i, src := range req.Sources {
input, meta, buildErr := b.buildInput(&src)
if buildErr != nil {
return nil, fmt.Errorf("wallet: input %d: %w", i, buildErr)
return nil, coreerr.E("V1Builder.Build", fmt.Sprintf("wallet: input %d", i), buildErr)
}
tx.Vin = append(tx.Vin, input)
metas[i] = meta
@ -112,7 +112,7 @@ func (b *V1Builder) Build(req *BuildRequest) (*types.Transaction, error) {
for _, dst := range req.Destinations {
out, outErr := deriveOutput(txSec, dst.Address, outputIdx, dst.Amount)
if outErr != nil {
return nil, fmt.Errorf("wallet: output %d: %w", outputIdx, outErr)
return nil, coreerr.E("V1Builder.Build", fmt.Sprintf("wallet: output %d", outputIdx), outErr)
}
tx.Vout = append(tx.Vout, out)
outputIdx++
@ -122,7 +122,7 @@ func (b *V1Builder) Build(req *BuildRequest) (*types.Transaction, error) {
if change > 0 {
out, outErr := deriveOutput(txSec, req.SenderAddress, outputIdx, change)
if outErr != nil {
return nil, fmt.Errorf("wallet: change output: %w", outErr)
return nil, coreerr.E("V1Builder.Build", "wallet: change output", outErr)
}
tx.Vout = append(tx.Vout, out)
}
@ -136,7 +136,7 @@ func (b *V1Builder) Build(req *BuildRequest) (*types.Transaction, error) {
for i, meta := range metas {
sigs, signErr := b.signer.SignInput(prefixHash, meta.ephemeral, meta.ring, meta.realIndex)
if signErr != nil {
return nil, fmt.Errorf("wallet: sign input %d: %w", i, signErr)
return nil, coreerr.E("V1Builder.Build", fmt.Sprintf("wallet: sign input %d", i), signErr)
}
tx.Signatures = append(tx.Signatures, sigs)
}
@ -168,7 +168,7 @@ func (b *V1Builder) buildInput(src *Transfer) (types.TxInputToKey, inputMeta, er
return m.GlobalIndex == src.GlobalIndex
})
if realIdx < 0 {
return types.TxInputToKey{}, inputMeta{}, errors.New("real output not found in ring")
return types.TxInputToKey{}, inputMeta{}, coreerr.E("V1Builder.buildInput", "real output not found in ring", nil)
}
// Build key offsets and public key list.
@ -203,13 +203,13 @@ func deriveOutput(txSec [32]byte, addr types.Address, index uint64, amount uint6
derivation, err := crypto.GenerateKeyDerivation(
[32]byte(addr.ViewPublicKey), txSec)
if err != nil {
return types.TxOutputBare{}, fmt.Errorf("key derivation: %w", err)
return types.TxOutputBare{}, coreerr.E("deriveOutput", "key derivation", err)
}
ephPub, err := crypto.DerivePublicKey(
derivation, index, [32]byte(addr.SpendPublicKey))
if err != nil {
return types.TxOutputBare{}, fmt.Errorf("derive public key: %w", err)
return types.TxOutputBare{}, coreerr.E("deriveOutput", "derive public key", err)
}
return types.TxOutputBare{
@ -224,7 +224,7 @@ func SerializeTransaction(tx *types.Transaction) ([]byte, error) {
enc := wire.NewEncoder(&buf)
wire.EncodeTransaction(enc, tx)
if err := enc.Err(); err != nil {
return nil, fmt.Errorf("wallet: encode tx: %w", err)
return nil, coreerr.E("SerializeTransaction", "wallet: encode tx", err)
}
return buf.Bytes(), nil
}

View file

@ -13,6 +13,8 @@ import (
"encoding/binary"
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
)
@ -46,7 +48,7 @@ func ParseTxExtra(raw []byte) (*TxExtra, error) {
count, n, err := wire.DecodeVarint(raw)
if err != nil {
return extra, fmt.Errorf("wallet: extra: invalid varint count: %w", err)
return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid varint count", err)
}
pos := n
@ -57,7 +59,7 @@ func ParseTxExtra(raw []byte) (*TxExtra, error) {
switch tag {
case extraTagPublicKey:
if pos+32 > len(raw) {
return extra, fmt.Errorf("wallet: extra: truncated public key at offset %d", pos)
return extra, coreerr.E("ParseTxExtra", fmt.Sprintf("wallet: extra: truncated public key at offset %d", pos), nil)
}
copy(extra.TxPublicKey[:], raw[pos:pos+32])
pos += 32
@ -65,7 +67,7 @@ func ParseTxExtra(raw []byte) (*TxExtra, error) {
case extraTagUnlockTime:
val, vn, vErr := wire.DecodeVarint(raw[pos:])
if vErr != nil {
return extra, fmt.Errorf("wallet: extra: invalid unlock_time varint: %w", vErr)
return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid unlock_time varint", vErr)
}
extra.UnlockTime = val
pos += vn
@ -73,7 +75,7 @@ func ParseTxExtra(raw []byte) (*TxExtra, error) {
case extraTagDerivationHint:
length, vn, vErr := wire.DecodeVarint(raw[pos:])
if vErr != nil {
return extra, fmt.Errorf("wallet: extra: invalid hint length varint: %w", vErr)
return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid hint length varint", vErr)
}
pos += vn
if length == 2 && pos+2 <= len(raw) {
@ -110,11 +112,11 @@ func skipExtraElement(data []byte, tag uint8) (int, error) {
// String types: varint(length) + length bytes.
case 7, 9, 11, 19:
if len(data) == 0 {
return 0, fmt.Errorf("wallet: extra: no data for string tag %d", tag)
return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: no data for string tag %d", tag), nil)
}
length, n, err := wire.DecodeVarint(data)
if err != nil {
return 0, fmt.Errorf("wallet: extra: invalid string length for tag %d: %w", tag, err)
return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: invalid string length for tag %d", tag), err)
}
return n + int(length), nil
@ -122,7 +124,7 @@ func skipExtraElement(data []byte, tag uint8) (int, error) {
case 14, 15, 16, 26, 27:
_, n, err := wire.DecodeVarint(data)
if err != nil {
return 0, fmt.Errorf("wallet: extra: invalid varint for tag %d: %w", tag, err)
return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: invalid varint for tag %d", tag), err)
}
return n, nil
@ -139,6 +141,6 @@ func skipExtraElement(data []byte, tag uint8) (int, error) {
return 64, nil // signature
default:
return 0, fmt.Errorf("wallet: extra: unknown tag %d", tag)
return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: unknown tag %d", tag), nil)
}
}

View file

@ -2,10 +2,11 @@ package wallet
import (
"encoding/binary"
"errors"
"fmt"
"hash/crc32"
"strings"
coreerr "forge.lthn.ai/core/go-log"
)
const numWords = 1626
@ -13,7 +14,7 @@ const numWords = 1626
// MnemonicEncode converts a 32-byte secret key to a 25-word mnemonic phrase.
func MnemonicEncode(key []byte) (string, error) {
if len(key) != 32 {
return "", fmt.Errorf("wallet: mnemonic encode requires 32 bytes, got %d", len(key))
return "", coreerr.E("MnemonicEncode", fmt.Sprintf("wallet: mnemonic encode requires 32 bytes, got %d", len(key)), nil)
}
words := make([]string, 0, 25)
@ -39,12 +40,12 @@ func MnemonicDecode(phrase string) ([32]byte, error) {
words := strings.Fields(phrase)
if len(words) != 25 {
return key, fmt.Errorf("wallet: mnemonic requires 25 words, got %d", len(words))
return key, coreerr.E("MnemonicDecode", fmt.Sprintf("wallet: mnemonic requires 25 words, got %d", len(words)), nil)
}
expected := checksumIndex(words[:24])
if words[24] != words[expected] {
return key, errors.New("wallet: mnemonic checksum failed")
return key, coreerr.E("MnemonicDecode", "wallet: mnemonic checksum failed", nil)
}
n := uint32(numWords)
@ -61,7 +62,7 @@ func MnemonicDecode(phrase string) ([32]byte, error) {
if !ok3 {
word = words[i*3+2]
}
return key, fmt.Errorf("wallet: unknown mnemonic word %q", word)
return key, coreerr.E("MnemonicDecode", fmt.Sprintf("wallet: unknown mnemonic word %q", word), nil)
}
val := uint32(w1) +

View file

@ -12,6 +12,8 @@ package wallet
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/rpc"
"forge.lthn.ai/core/go-blockchain/types"
)
@ -42,7 +44,7 @@ func NewRPCRingSelector(client *rpc.Client) *RPCRingSelector {
func (s *RPCRingSelector) SelectRing(amount uint64, realGlobalIndex uint64, ringSize int) ([]RingMember, error) {
outs, err := s.client.GetRandomOutputs(amount, ringSize+5)
if err != nil {
return nil, fmt.Errorf("wallet: get random outputs: %w", err)
return nil, coreerr.E("RPCRingSelector.SelectRing", "wallet: get random outputs", err)
}
var members []RingMember
@ -68,8 +70,7 @@ func (s *RPCRingSelector) SelectRing(amount uint64, realGlobalIndex uint64, ring
}
if len(members) < ringSize {
return nil, fmt.Errorf("wallet: insufficient decoys: got %d, need %d",
len(members), ringSize)
return nil, coreerr.E("RPCRingSelector.SelectRing", fmt.Sprintf("wallet: insufficient decoys: got %d, need %d", len(members), ringSize), nil)
}
return members, nil
}

View file

@ -10,10 +10,9 @@
package wallet
import (
"fmt"
"forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/types"
coreerr "forge.lthn.ai/core/go-log"
)
// Signer produces signatures for transaction inputs.
@ -35,7 +34,7 @@ func (s *NLSAGSigner) SignInput(prefixHash types.Hash, ephemeral KeyPair,
ki, err := crypto.GenerateKeyImage(
[32]byte(ephemeral.Public), [32]byte(ephemeral.Secret))
if err != nil {
return nil, fmt.Errorf("wallet: key image: %w", err)
return nil, coreerr.E("NLSAGSigner.SignInput", "wallet: key image", err)
}
pubs := make([][32]byte, len(ring))
@ -47,7 +46,7 @@ func (s *NLSAGSigner) SignInput(prefixHash types.Hash, ephemeral KeyPair,
[32]byte(prefixHash), ki, pubs,
[32]byte(ephemeral.Secret), realIndex)
if err != nil {
return nil, fmt.Errorf("wallet: ring signature: %w", err)
return nil, coreerr.E("NLSAGSigner.SignInput", "wallet: ring signature", err)
}
sigs := make([]types.Signature, len(rawSigs))

View file

@ -13,6 +13,8 @@ import (
"encoding/json"
"fmt"
coreerr "forge.lthn.ai/core/go-log"
store "forge.lthn.ai/core/go-store"
"forge.lthn.ai/core/go-blockchain/config"
@ -66,7 +68,7 @@ func (t *Transfer) IsSpendable(chainHeight uint64, _ bool) bool {
func putTransfer(s *store.Store, tr *Transfer) error {
val, err := json.Marshal(tr)
if err != nil {
return fmt.Errorf("wallet: marshal transfer: %w", err)
return coreerr.E("putTransfer", "wallet: marshal transfer", err)
}
return s.Set(groupTransfers, tr.KeyImage.String(), string(val))
}
@ -75,11 +77,11 @@ func putTransfer(s *store.Store, tr *Transfer) error {
func getTransfer(s *store.Store, ki types.KeyImage) (*Transfer, error) {
val, err := s.Get(groupTransfers, ki.String())
if err != nil {
return nil, fmt.Errorf("wallet: get transfer %s: %w", ki, err)
return nil, coreerr.E("getTransfer", fmt.Sprintf("wallet: get transfer %s", ki), err)
}
var tr Transfer
if err := json.Unmarshal([]byte(val), &tr); err != nil {
return nil, fmt.Errorf("wallet: unmarshal transfer: %w", err)
return nil, coreerr.E("getTransfer", "wallet: unmarshal transfer", err)
}
return &tr, nil
}
@ -100,7 +102,7 @@ func markTransferSpent(s *store.Store, ki types.KeyImage, height uint64) error {
func listTransfers(s *store.Store) ([]Transfer, error) {
pairs, err := s.GetAll(groupTransfers)
if err != nil {
return nil, fmt.Errorf("wallet: list transfers: %w", err)
return nil, coreerr.E("listTransfers", "wallet: list transfers", err)
}
transfers := make([]Transfer, 0, len(pairs))
for _, val := range pairs {

View file

@ -11,11 +11,12 @@ package wallet
import (
"cmp"
"errors"
"fmt"
"slices"
"strconv"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/chain"
"forge.lthn.ai/core/go-blockchain/rpc"
"forge.lthn.ai/core/go-blockchain/types"
@ -70,13 +71,13 @@ func (w *Wallet) Sync() error {
chainHeight, err := w.chain.Height()
if err != nil {
return fmt.Errorf("wallet: chain height: %w", err)
return coreerr.E("Wallet.Sync", "wallet: chain height", err)
}
for h := lastScanned; h < chainHeight; h++ {
blk, _, err := w.chain.GetBlockByHeight(h)
if err != nil {
return fmt.Errorf("wallet: get block %d: %w", h, err)
return coreerr.E("Wallet.Sync", fmt.Sprintf("wallet: get block %d", h), err)
}
// Scan miner tx.
@ -115,7 +116,7 @@ func (w *Wallet) scanTx(tx *types.Transaction, blockHeight uint64) error {
}
for i := range transfers {
if err := putTransfer(w.store, &transfers[i]); err != nil {
return fmt.Errorf("wallet: store transfer: %w", err)
return coreerr.E("Wallet.scanTx", "wallet: store transfer", err)
}
}
@ -167,7 +168,7 @@ func (w *Wallet) Balance() (confirmed, locked uint64, err error) {
// Send constructs and submits a transaction.
func (w *Wallet) Send(destinations []Destination, fee uint64) (*types.Transaction, error) {
if w.builder == nil || w.client == nil {
return nil, errors.New("wallet: no RPC client configured")
return nil, coreerr.E("Wallet.Send", "wallet: no RPC client configured", nil)
}
chainHeight, err := w.chain.Height()
@ -208,8 +209,7 @@ func (w *Wallet) Send(destinations []Destination, fee uint64) (*types.Transactio
}
}
if selectedSum < needed {
return nil, fmt.Errorf("wallet: insufficient balance: have %d, need %d",
selectedSum, needed)
return nil, coreerr.E("Wallet.Send", fmt.Sprintf("wallet: insufficient balance: have %d, need %d", selectedSum, needed), nil)
}
req := &BuildRequest{
@ -230,7 +230,7 @@ func (w *Wallet) Send(destinations []Destination, fee uint64) (*types.Transactio
}
if err := w.client.SendRawTransaction(blob); err != nil {
return nil, fmt.Errorf("wallet: submit tx: %w", err)
return nil, coreerr.E("Wallet.Send", "wallet: submit tx", err)
}
// Optimistically mark sources as spent.

View file

@ -1640,4 +1640,3 @@ func init() {
wordIndex[w] = i
}
}

View file

@ -9,6 +9,8 @@ import (
"encoding/binary"
"fmt"
"io"
coreerr "forge.lthn.ai/core/go-log"
)
// Decoder reads consensus-critical binary data from an io.Reader.
@ -80,7 +82,7 @@ func (d *Decoder) ReadBytes(n int) []byte {
return nil
}
if n < 0 || n > MaxBlobSize {
d.err = fmt.Errorf("wire: blob size %d exceeds maximum %d", n, MaxBlobSize)
d.err = coreerr.E("Decoder.ReadBytes", fmt.Sprintf("wire: blob size %d exceeds maximum %d", n, MaxBlobSize), nil)
return nil
}
buf := make([]byte, n)

View file

@ -8,6 +8,8 @@ package wire
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types"
)
@ -162,21 +164,6 @@ func encodeInputs(enc *Encoder, vin []types.TxInput) {
encodeKeyOffsets(enc, v.KeyOffsets)
enc.WriteBlob32((*[32]byte)(&v.KeyImage))
enc.WriteBytes(v.EtcDetails)
case types.TxInputHTLC:
// Wire order: hltc_origin (string) BEFORE parent fields (C++ quirk).
enc.WriteVarint(uint64(len(v.HTLCOrigin)))
if len(v.HTLCOrigin) > 0 {
enc.WriteBytes([]byte(v.HTLCOrigin))
}
enc.WriteVarint(v.Amount)
encodeKeyOffsets(enc, v.KeyOffsets)
enc.WriteBlob32((*[32]byte)(&v.KeyImage))
enc.WriteBytes(v.EtcDetails)
case types.TxInputMultisig:
enc.WriteVarint(v.Amount)
enc.WriteBlob32((*[32]byte)(&v.MultisigOutID))
enc.WriteVarint(v.SigsCount)
enc.WriteBytes(v.EtcDetails)
}
}
}
@ -208,27 +195,8 @@ func decodeInputs(dec *Decoder) []types.TxInput {
dec.ReadBlob32((*[32]byte)(&in.KeyImage))
in.EtcDetails = decodeRawVariantVector(dec)
vin = append(vin, in)
case types.InputTypeHTLC:
var in types.TxInputHTLC
// Wire order: hltc_origin (string) BEFORE parent fields.
originLen := dec.ReadVarint()
if originLen > 0 && dec.Err() == nil {
in.HTLCOrigin = string(dec.ReadBytes(int(originLen)))
}
in.Amount = dec.ReadVarint()
in.KeyOffsets = decodeKeyOffsets(dec)
dec.ReadBlob32((*[32]byte)(&in.KeyImage))
in.EtcDetails = decodeRawVariantVector(dec)
vin = append(vin, in)
case types.InputTypeMultisig:
var in types.TxInputMultisig
in.Amount = dec.ReadVarint()
dec.ReadBlob32((*[32]byte)(&in.MultisigOutID))
in.SigsCount = dec.ReadVarint()
in.EtcDetails = decodeRawVariantVector(dec)
vin = append(vin, in)
default:
dec.err = fmt.Errorf("wire: unsupported input tag 0x%02x", tag)
dec.err = coreerr.E("decodeInputs", fmt.Sprintf("wire: unsupported input tag 0x%02x", tag), nil)
return vin
}
}
@ -266,7 +234,7 @@ func decodeKeyOffsets(dec *Decoder) []types.TxOutRef {
dec.ReadBlob32((*[32]byte)(&refs[i].TxID))
refs[i].N = dec.ReadVarint()
default:
dec.err = fmt.Errorf("wire: unsupported ref tag 0x%02x", refs[i].Tag)
dec.err = coreerr.E("decodeKeyOffsets", fmt.Sprintf("wire: unsupported ref tag 0x%02x", refs[i].Tag), nil)
return refs
}
}
@ -284,26 +252,9 @@ func encodeOutputsV1(enc *Encoder, vout []types.TxOutput) {
case types.TxOutputBare:
enc.WriteVarint(v.Amount)
// Target is a variant (txout_target_v)
switch tgt := v.Target.(type) {
case types.TxOutToKey:
enc.WriteVariantTag(types.TargetTypeToKey)
enc.WriteBlob32((*[32]byte)(&tgt.Key))
enc.WriteUint8(tgt.MixAttr)
case types.TxOutMultisig:
enc.WriteVariantTag(types.TargetTypeMultisig)
enc.WriteVarint(tgt.MinimumSigs)
enc.WriteVarint(uint64(len(tgt.Keys)))
for i := range tgt.Keys {
enc.WriteBlob32((*[32]byte)(&tgt.Keys[i]))
}
case types.TxOutHTLC:
enc.WriteVariantTag(types.TargetTypeHTLC)
enc.WriteBlob32((*[32]byte)(&tgt.HTLCHash))
enc.WriteUint8(tgt.Flags)
enc.WriteVarint(tgt.Expiration)
enc.WriteBlob32((*[32]byte)(&tgt.PKRedeem))
enc.WriteBlob32((*[32]byte)(&tgt.PKRefund))
}
enc.WriteBlob32((*[32]byte)(&v.Target.Key))
enc.WriteUint8(v.Target.MixAttr)
}
}
}
@ -323,31 +274,10 @@ func decodeOutputsV1(dec *Decoder) []types.TxOutput {
}
switch tag {
case types.TargetTypeToKey:
var tgt types.TxOutToKey
dec.ReadBlob32((*[32]byte)(&tgt.Key))
tgt.MixAttr = dec.ReadUint8()
out.Target = tgt
case types.TargetTypeMultisig:
var tgt types.TxOutMultisig
tgt.MinimumSigs = dec.ReadVarint()
keyCount := dec.ReadVarint()
if keyCount > 0 && dec.Err() == nil {
tgt.Keys = make([]types.PublicKey, keyCount)
for j := uint64(0); j < keyCount; j++ {
dec.ReadBlob32((*[32]byte)(&tgt.Keys[j]))
}
}
out.Target = tgt
case types.TargetTypeHTLC:
var tgt types.TxOutHTLC
dec.ReadBlob32((*[32]byte)(&tgt.HTLCHash))
tgt.Flags = dec.ReadUint8()
tgt.Expiration = dec.ReadVarint()
dec.ReadBlob32((*[32]byte)(&tgt.PKRedeem))
dec.ReadBlob32((*[32]byte)(&tgt.PKRefund))
out.Target = tgt
dec.ReadBlob32((*[32]byte)(&out.Target.Key))
out.Target.MixAttr = dec.ReadUint8()
default:
dec.err = fmt.Errorf("wire: unsupported target tag 0x%02x", tag)
dec.err = coreerr.E("decodeOutputsV1", fmt.Sprintf("wire: unsupported target tag 0x%02x", tag), nil)
return vout
}
vout = append(vout, out)
@ -363,26 +293,9 @@ func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) {
switch v := out.(type) {
case types.TxOutputBare:
enc.WriteVarint(v.Amount)
switch tgt := v.Target.(type) {
case types.TxOutToKey:
enc.WriteVariantTag(types.TargetTypeToKey)
enc.WriteBlob32((*[32]byte)(&tgt.Key))
enc.WriteUint8(tgt.MixAttr)
case types.TxOutMultisig:
enc.WriteVariantTag(types.TargetTypeMultisig)
enc.WriteVarint(tgt.MinimumSigs)
enc.WriteVarint(uint64(len(tgt.Keys)))
for i := range tgt.Keys {
enc.WriteBlob32((*[32]byte)(&tgt.Keys[i]))
}
case types.TxOutHTLC:
enc.WriteVariantTag(types.TargetTypeHTLC)
enc.WriteBlob32((*[32]byte)(&tgt.HTLCHash))
enc.WriteUint8(tgt.Flags)
enc.WriteVarint(tgt.Expiration)
enc.WriteBlob32((*[32]byte)(&tgt.PKRedeem))
enc.WriteBlob32((*[32]byte)(&tgt.PKRefund))
}
enc.WriteBlob32((*[32]byte)(&v.Target.Key))
enc.WriteUint8(v.Target.MixAttr)
case types.TxOutputZarcanum:
enc.WriteBlob32((*[32]byte)(&v.StealthAddress))
enc.WriteBlob32((*[32]byte)(&v.ConcealingPoint))
@ -410,36 +323,11 @@ func decodeOutputsV2(dec *Decoder) []types.TxOutput {
var out types.TxOutputBare
out.Amount = dec.ReadVarint()
targetTag := dec.ReadVariantTag()
if dec.Err() != nil {
return vout
}
switch targetTag {
case types.TargetTypeToKey:
var tgt types.TxOutToKey
dec.ReadBlob32((*[32]byte)(&tgt.Key))
tgt.MixAttr = dec.ReadUint8()
out.Target = tgt
case types.TargetTypeMultisig:
var tgt types.TxOutMultisig
tgt.MinimumSigs = dec.ReadVarint()
keyCount := dec.ReadVarint()
if keyCount > 0 && dec.Err() == nil {
tgt.Keys = make([]types.PublicKey, keyCount)
for j := uint64(0); j < keyCount; j++ {
dec.ReadBlob32((*[32]byte)(&tgt.Keys[j]))
}
}
out.Target = tgt
case types.TargetTypeHTLC:
var tgt types.TxOutHTLC
dec.ReadBlob32((*[32]byte)(&tgt.HTLCHash))
tgt.Flags = dec.ReadUint8()
tgt.Expiration = dec.ReadVarint()
dec.ReadBlob32((*[32]byte)(&tgt.PKRedeem))
dec.ReadBlob32((*[32]byte)(&tgt.PKRefund))
out.Target = tgt
default:
dec.err = fmt.Errorf("wire: unsupported target tag 0x%02x", targetTag)
if targetTag == types.TargetTypeToKey {
dec.ReadBlob32((*[32]byte)(&out.Target.Key))
out.Target.MixAttr = dec.ReadUint8()
} else {
dec.err = coreerr.E("decodeOutputsV2", fmt.Sprintf("wire: unsupported target tag 0x%02x", targetTag), nil)
return vout
}
vout = append(vout, out)
@ -453,7 +341,7 @@ func decodeOutputsV2(dec *Decoder) []types.TxOutput {
out.MixAttr = dec.ReadUint8()
vout = append(vout, out)
default:
dec.err = fmt.Errorf("wire: unsupported output tag 0x%02x", tag)
dec.err = coreerr.E("decodeOutputsV2", fmt.Sprintf("wire: unsupported output tag 0x%02x", tag), nil)
return vout
}
}
@ -536,14 +424,6 @@ const (
tagExtraAliasEntry = 33 // extra_alias_entry — complex
tagZarcanumTxDataV1 = 39 // zarcanum_tx_data_v1 — varint (fee)
// Asset descriptor operation (HF5).
tagAssetDescriptorOperation = 40 // asset_descriptor_operation
// Asset operation proof tags (HF5).
tagAssetOperationProof = 49 // asset_operation_proof
tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof
tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth (Ethereum sig)
// Signature variant tags (signature_v).
tagNLSAGSig = 42 // NLSAG_sig — vector<signature>
tagZCSig = 43 // ZC_sig — 2 public_keys + CLSAG_GGX
@ -612,18 +492,6 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte {
case tagZarcanumTxDataV1: // fee — FIELD(fee) → serialize_int → 8-byte LE
return dec.ReadBytes(8)
// Asset descriptor operation (HF5)
case tagAssetDescriptorOperation:
return readAssetDescriptorOperation(dec)
// Asset operation proof variants (HF5)
case tagAssetOperationProof:
return readAssetOperationProof(dec)
case tagAssetOperationOwnershipProof:
return readAssetOperationOwnershipProof(dec)
case tagAssetOperationOwnershipProofETH:
return readAssetOperationOwnershipProofETH(dec)
// Signature variants
case tagNLSAGSig: // vector<signature> (64 bytes each)
return readVariantVectorFixed(dec, 64)
@ -643,7 +511,7 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte {
return dec.ReadBytes(96)
default:
dec.err = fmt.Errorf("wire: unsupported variant tag 0x%02x (%d)", tag, tag)
dec.err = coreerr.E("readVariantElementData", fmt.Sprintf("wire: unsupported variant tag 0x%02x (%d)", tag, tag), nil)
return nil
}
}
@ -804,261 +672,6 @@ func readSignedParts(dec *Decoder) []byte {
return raw
}
// --- asset operation readers (HF5) ---
// readAssetDescriptorOperation reads asset_descriptor_operation (tag 40).
// Structure (CHAIN_TRANSITION_VER, version 0 and 1):
//
// ver (uint8) + operation_type (uint8)
// + opt_asset_id (uint8 marker + 32 bytes if present)
// + opt_descriptor (uint8 marker + AssetDescriptorBase if present)
// + amount_to_emit (uint64 LE) + amount_to_burn (uint64 LE)
// + etc (vector<uint8>)
//
// AssetDescriptorBase:
//
// ticker (string) + full_name (string) + total_max_supply (uint64 LE)
// + current_supply (uint64 LE) + decimal_point (uint8) + meta_info (string)
// + owner_key (32 bytes) + etc (vector<uint8>)
func readAssetDescriptorOperation(dec *Decoder) []byte {
var raw []byte
// ver: uint8 (CHAIN_TRANSITION_VER version byte)
ver := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, ver)
// operation_type: uint8
opType := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, opType)
// opt_asset_id: optional<hash> — uint8 marker, then 32 bytes if present
marker := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, marker)
if marker != 0 {
b := dec.ReadBytes(32)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
}
// opt_descriptor: optional<AssetDescriptorBase>
marker = dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, marker)
if marker != 0 {
b := readAssetDescriptorBase(dec)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
}
// amount_to_emit: uint64 LE
b := dec.ReadBytes(8)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// amount_to_burn: uint64 LE
b = dec.ReadBytes(8)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// etc: vector<uint8>
v := readVariantVectorFixed(dec, 1)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
return raw
}
// readAssetDescriptorBase reads the AssetDescriptorBase structure.
// Wire: ticker (string) + full_name (string) + total_max_supply (uint64 LE)
//
// + current_supply (uint64 LE) + decimal_point (uint8) + meta_info (string)
// + owner_key (32 bytes) + etc (vector<uint8>).
func readAssetDescriptorBase(dec *Decoder) []byte {
var raw []byte
// ticker: string
s := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, s...)
// full_name: string
s = readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, s...)
// total_max_supply: uint64 LE
b := dec.ReadBytes(8)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// current_supply: uint64 LE
b = dec.ReadBytes(8)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// decimal_point: uint8
dp := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, dp)
// meta_info: string
s = readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, s...)
// owner_key: 32 bytes (crypto::public_key)
b = dec.ReadBytes(32)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// etc: vector<uint8>
v := readVariantVectorFixed(dec, 1)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
return raw
}
// readAssetOperationProof reads asset_operation_proof (tag 49).
// Structure (CHAIN_TRANSITION_VER, version 1):
//
// ver (uint8) + gss (generic_schnorr_sig_s: 64 bytes)
// + asset_id (32 bytes) + etc (vector<uint8>).
func readAssetOperationProof(dec *Decoder) []byte {
var raw []byte
// ver: uint8
ver := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, ver)
// gss: generic_schnorr_sig_s — 2 scalars (s, c) = 64 bytes
b := dec.ReadBytes(64)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// asset_id: 32-byte hash
b = dec.ReadBytes(32)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// etc: vector<uint8>
v := readVariantVectorFixed(dec, 1)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
return raw
}
// readAssetOperationOwnershipProof reads asset_operation_ownership_proof (tag 50).
// Structure (CHAIN_TRANSITION_VER, version 1):
//
// ver (uint8) + gss (generic_schnorr_sig_s: 64 bytes)
// + etc (vector<uint8>).
func readAssetOperationOwnershipProof(dec *Decoder) []byte {
var raw []byte
// ver: uint8
ver := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, ver)
// gss: generic_schnorr_sig_s — 2 scalars (s, c) = 64 bytes
b := dec.ReadBytes(64)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// etc: vector<uint8>
v := readVariantVectorFixed(dec, 1)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
return raw
}
// readAssetOperationOwnershipProofETH reads asset_operation_ownership_proof_eth (tag 51).
// Structure (CHAIN_TRANSITION_VER, version 1):
//
// ver (uint8) + eth_sig (65 bytes: r(32) + s(32) + v(1))
// + etc (vector<uint8>).
func readAssetOperationOwnershipProofETH(dec *Decoder) []byte {
var raw []byte
// ver: uint8
ver := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, ver)
// eth_sig: crypto::eth_signature — r(32) + s(32) + v(1) = 65 bytes
b := dec.ReadBytes(65)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// etc: vector<uint8>
v := readVariantVectorFixed(dec, 1)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
return raw
}
// --- crypto blob readers ---
// These read variable-length serialised crypto structures and return raw bytes.
// All vectors are varint(count) + 32*count bytes (scalars or points).
@ -1247,7 +860,7 @@ func readZCSig(dec *Decoder) []byte {
// readZarcanumSig reads zarcanum_sig (tag 45).
// Wire: d(32) + C(32) + C'(32) + E(32) + c(32) + y0(32) + y1(32) + y2(32) + y3(32) + y4(32)
//
// + bppe_serialized + pseudo_out_amount_commitment(32) + CLSAG_GGXXG.
// - bppe_serialized + pseudo_out_amount_commitment(32) + CLSAG_GGXXG.
func readZarcanumSig(dec *Decoder) []byte {
var raw []byte
// 10 fixed scalars/points