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

View file

@ -11,14 +11,16 @@ import (
"fmt" "fmt"
"strconv" "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/types"
store "forge.lthn.ai/core/go-store"
) )
// MarkSpent records a key image as spent at the given block height. // MarkSpent records a key image as spent at the given block height.
func (c *Chain) MarkSpent(ki types.KeyImage, height uint64) error { func (c *Chain) MarkSpent(ki types.KeyImage, height uint64) error {
if err := c.store.Set(groupSpentKeys, ki.String(), strconv.FormatUint(height, 10)); err != nil { 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 return nil
} }
@ -30,7 +32,7 @@ func (c *Chain) IsSpent(ki types.KeyImage) (bool, error) {
return false, nil return false, nil
} }
if err != 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 return true, nil
} }
@ -46,7 +48,7 @@ func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64,
grp := outputGroup(amount) grp := outputGroup(amount)
count, err := c.store.Count(grp) count, err := c.store.Count(grp)
if err != nil { 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) gindex := uint64(count)
@ -56,12 +58,12 @@ func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64,
} }
val, err := json.Marshal(entry) val, err := json.Marshal(entry)
if err != nil { 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) key := strconv.FormatUint(gindex, 10)
if err := c.store.Set(grp, key, string(val)); err != nil { 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 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) val, err := c.store.Get(grp, key)
if err != nil { if err != nil {
if errors.Is(err, store.ErrNotFound) { 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 var entry outputEntry
if err := json.Unmarshal([]byte(val), &entry); err != nil { 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) hash, err := types.HashFromHex(entry.TxID)
if err != nil { 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 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) { func (c *Chain) OutputCount(amount uint64) (uint64, error) {
n, err := c.store.Count(outputGroup(amount)) n, err := c.store.Count(outputGroup(amount))
if err != nil { 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 return uint64(n), nil
} }

View file

@ -6,9 +6,10 @@
package chain package chain
import ( import (
"fmt"
"log" "log"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/p2p" "forge.lthn.ai/core/go-blockchain/p2p"
levinpkg "forge.lthn.ai/core/go-p2p/node/levin" 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} resp := p2p.TimedSyncRequest{PayloadData: c.localSync}
payload, err := resp.Encode() payload, err := resp.Encode()
if err != nil { 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 { 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") log.Printf("p2p: responded to timed_sync")
return nil return nil
@ -55,24 +56,24 @@ func (c *LevinP2PConn) RequestChain(blockIDs [][]byte) (uint64, [][]byte, error)
req := p2p.RequestChain{BlockIDs: blockIDs} req := p2p.RequestChain{BlockIDs: blockIDs}
payload, err := req.Encode() payload, err := req.Encode()
if err != nil { 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. // Send as notification (expectResponse=false) per CryptoNote protocol.
if err := c.conn.WritePacket(p2p.CommandRequestChain, payload, false); err != nil { 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. // Read until we get RESPONSE_CHAIN_ENTRY.
for { for {
hdr, data, err := c.conn.ReadPacket() hdr, data, err := c.conn.ReadPacket()
if err != nil { 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 { if hdr.Command == p2p.CommandResponseChain {
var resp p2p.ResponseChainEntry var resp p2p.ResponseChainEntry
if err := resp.Decode(data); err != nil { 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 return resp.StartHeight, resp.BlockIDs, nil
} }
@ -86,23 +87,23 @@ func (c *LevinP2PConn) RequestObjects(blockHashes [][]byte) ([]BlockBlobEntry, e
req := p2p.RequestGetObjects{Blocks: blockHashes} req := p2p.RequestGetObjects{Blocks: blockHashes}
payload, err := req.Encode() payload, err := req.Encode()
if err != nil { 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 { 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. // Read until we get RESPONSE_GET_OBJECTS.
for { for {
hdr, data, err := c.conn.ReadPacket() hdr, data, err := c.conn.ReadPacket()
if err != nil { 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 { if hdr.Command == p2p.CommandResponseObjects {
var resp p2p.ResponseGetObjects var resp p2p.ResponseGetObjects
if err := resp.Decode(data); err != nil { 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)) entries := make([]BlockBlobEntry, len(resp.Blocks))
for i, b := range resp.Blocks { for i, b := range resp.Blocks {

View file

@ -9,6 +9,8 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
coreerr "forge.lthn.ai/core/go-log"
) )
// P2PConnection abstracts the P2P communication needed for block sync. // 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() localHeight, err := c.Height()
if err != nil { 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() peerHeight := conn.PeerHeight()
@ -55,7 +57,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
// Build sparse chain history. // Build sparse chain history.
history, err := c.SparseChainHistory() history, err := c.SparseChainHistory()
if err != nil { 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. // Convert Hash to []byte for P2P.
@ -69,7 +71,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
// Request chain entry. // Request chain entry.
startHeight, blockIDs, err := conn.RequestChain(historyBytes) startHeight, blockIDs, err := conn.RequestChain(historyBytes)
if err != nil { 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 { if len(blockIDs) == 0 {
@ -106,7 +108,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
entries, err := conn.RequestObjects(batch) entries, err := conn.RequestObjects(batch)
if err != nil { 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) 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) blockDiff, err := c.NextDifficulty(blockHeight, opts.Forks)
if err != nil { 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, if err := c.processBlockBlobs(entry.Block, entry.Txs,
blockHeight, blockDiff, opts); err != nil { 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 ( import (
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/consensus" "forge.lthn.ai/core/go-blockchain/consensus"
"forge.lthn.ai/core/go-blockchain/types" "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 { for i, gidx := range offsets {
txHash, outNo, err := c.GetOutput(amount, gidx) txHash, outNo, err := c.GetOutput(amount, gidx)
if err != nil { 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) tx, _, err := c.GetTransaction(txHash)
if err != nil { 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) { if int(outNo) >= len(tx.Vout) {
return nil, fmt.Errorf("ring output %d: tx %s has %d outputs, want index %d", 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)
i, txHash, len(tx.Vout), outNo)
} }
switch out := tx.Vout[outNo].(type) { switch out := tx.Vout[outNo].(type) {
@ -41,7 +42,7 @@ func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicK
} }
pubs[i] = toKey.Key pubs[i] = toKey.Key
default: 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 return pubs, nil
@ -57,17 +58,16 @@ func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, er
for i, gidx := range offsets { for i, gidx := range offsets {
txHash, outNo, err := c.GetOutput(0, gidx) txHash, outNo, err := c.GetOutput(0, gidx)
if err != nil { 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) tx, _, err := c.GetTransaction(txHash)
if err != nil { 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) { if int(outNo) >= len(tx.Vout) {
return nil, fmt.Errorf("ZC ring output %d: tx %s has %d outputs, want index %d", 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)
i, txHash, len(tx.Vout), outNo)
} }
switch out := tx.Vout[outNo].(type) { switch out := tx.Vout[outNo].(type) {
@ -78,7 +78,7 @@ func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, er
BlindedAssetID: [32]byte(out.BlindedAssetID), BlindedAssetID: [32]byte(out.BlindedAssetID),
} }
default: 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 return members, nil

View file

@ -13,9 +13,11 @@ import (
"fmt" "fmt"
"strconv" "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/types"
"forge.lthn.ai/core/go-blockchain/wire" "forge.lthn.ai/core/go-blockchain/wire"
store "forge.lthn.ai/core/go-store"
) )
// Storage group constants matching the design schema. // 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) enc := wire.NewEncoder(&buf)
wire.EncodeBlock(enc, b) wire.EncodeBlock(enc, b)
if err := enc.Err(); err != nil { 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{ rec := blockRecord{
@ -53,17 +55,17 @@ func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error {
} }
val, err := json.Marshal(rec) val, err := json.Marshal(rec)
if err != nil { 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 { 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. // Update hash -> height index.
hashHex := meta.Hash.String() hashHex := meta.Hash.String()
if err := c.store.Set(groupBlockIndex, hashHex, strconv.FormatUint(meta.Height, 10)); err != nil { 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 return nil
@ -74,9 +76,9 @@ func (c *Chain) GetBlockByHeight(height uint64) (*types.Block, *BlockMeta, error
val, err := c.store.Get(groupBlocks, heightKey(height)) val, err := c.store.Get(groupBlocks, heightKey(height))
if err != nil { if err != nil {
if errors.Is(err, store.ErrNotFound) { 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) 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()) heightStr, err := c.store.Get(groupBlockIndex, hash.String())
if err != nil { if err != nil {
if errors.Is(err, store.ErrNotFound) { 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) height, err := strconv.ParseUint(heightStr, 10, 64)
if err != nil { 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) return c.GetBlockByHeight(height)
} }
@ -109,7 +111,7 @@ func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxM
enc := wire.NewEncoder(&buf) enc := wire.NewEncoder(&buf)
wire.EncodeTransaction(enc, tx) wire.EncodeTransaction(enc, tx)
if err := enc.Err(); err != nil { 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{ rec := txRecord{
@ -118,11 +120,11 @@ func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxM
} }
val, err := json.Marshal(rec) val, err := json.Marshal(rec)
if err != nil { 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 { 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 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()) val, err := c.store.Get(groupTx, hash.String())
if err != nil { if err != nil {
if errors.Is(err, store.ErrNotFound) { 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 var rec txRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil { 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) blob, err := hex.DecodeString(rec.Blob)
if err != nil { 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)) dec := wire.NewDecoder(bytes.NewReader(blob))
tx := wire.DecodeTransaction(dec) tx := wire.DecodeTransaction(dec)
if err := dec.Err(); err != nil { 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 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) { func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
val, err := c.store.Get(groupBlocks, heightKey(height)) val, err := c.store.Get(groupBlocks, heightKey(height))
if err != nil { 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 var rec blockRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil { 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 return &rec.Meta, nil
} }
@ -176,16 +178,16 @@ func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
func decodeBlockRecord(val string) (*types.Block, *BlockMeta, error) { func decodeBlockRecord(val string) (*types.Block, *BlockMeta, error) {
var rec blockRecord var rec blockRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil { 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) blob, err := hex.DecodeString(rec.Blob)
if err != nil { 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)) dec := wire.NewDecoder(bytes.NewReader(blob))
blk := wire.DecodeBlock(dec) blk := wire.DecodeBlock(dec)
if err := dec.Err(); err != nil { 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 return &blk, &rec.Meta, nil
} }

View file

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

View file

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

View file

@ -7,12 +7,13 @@ package blockchain
import ( import (
"context" "context"
"fmt"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"sync" "sync"
coreerr "forge.lthn.ai/core/go-log"
cli "forge.lthn.ai/core/cli/pkg/cli" cli "forge.lthn.ai/core/cli/pkg/cli"
store "forge.lthn.ai/core/go-store" 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") dbPath := filepath.Join(dataDir, "chain.db")
s, err := store.New(dbPath) s, err := store.New(dbPath)
if err != nil { if err != nil {
return fmt.Errorf("open store: %w", err) return coreerr.E("runExplorer", "open store", err)
} }
defer s.Close() defer s.Close()
@ -68,7 +69,7 @@ func runExplorer(dataDir, seed string, testnet bool) error {
frame.Footer(hints) frame.Footer(hints)
frame.Run() frame.Run()
cancel() // Signal syncLoop to stop. cancel() // Signal syncLoop to stop.
wg.Wait() // Wait for it before closing store. wg.Wait() // Wait for it before closing store.
return nil return nil
} }

View file

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

View file

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

View file

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

View file

@ -9,6 +9,8 @@ import (
"fmt" "fmt"
"math" "math"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/types"
) )
@ -31,7 +33,7 @@ func TxFee(tx *types.Transaction) (uint64, error) {
} }
if outputSum > inputSum { 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 return inputSum - outputSum, nil

View file

@ -6,10 +6,11 @@
package consensus package consensus
import ( import (
"errors"
"fmt" "fmt"
"math/bits" "math/bits"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config" "forge.lthn.ai/core/go-blockchain/config"
) )
@ -43,7 +44,7 @@ func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
} }
if blockSize > 2*effectiveMedian { 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² // 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: // Since hi1 should be 0 for reasonable block sizes, simplify:
if hi1 > 0 { 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) hi2, lo2 := bits.Mul64(baseReward, lo1)

View file

@ -8,6 +8,8 @@ package consensus
import ( import (
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config" "forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/types"
) )
@ -15,17 +17,11 @@ import (
// ValidateTransaction performs semantic validation on a regular (non-coinbase) // ValidateTransaction performs semantic validation on a regular (non-coinbase)
// transaction. Checks are ordered to match the C++ validate_tx_semantic(). // transaction. Checks are ordered to match the C++ validate_tx_semantic().
func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error { 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) 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. // 1. Blob size.
if uint64(len(txBlob)) >= config.MaxTransactionBlobSize { 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. // 2. Input count.
@ -33,16 +29,16 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
return ErrNoInputs return ErrNoInputs
} }
if uint64(len(tx.Vin)) > config.TxMaxAllowedInputs { 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. // 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 return err
} }
// 4. Output validation. // 4. Output validation.
if err := checkOutputs(tx, hf1Active, hf4Active); err != nil { if err := checkOutputs(tx, hf4Active); err != nil {
return err return err
} }
@ -69,55 +65,41 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
return nil return nil
} }
func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error { func checkInputTypes(tx *types.Transaction, hf4Active bool) error {
for _, vin := range tx.Vin { for _, vin := range tx.Vin {
switch vin.(type) { switch vin.(type) {
case types.TxInputToKey: case types.TxInputToKey:
// Always valid. // Always valid.
case types.TxInputGenesis: case types.TxInputGenesis:
return fmt.Errorf("%w: txin_gen in regular transaction", ErrInvalidInputType) return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType)
case types.TxInputHTLC, types.TxInputMultisig:
if !hf1Active {
return fmt.Errorf("%w: tag %d pre-HF1", ErrInvalidInputType, vin.InputType())
}
default: default:
// Future types (ZC, etc.) — accept if HF4+. // Future types (multisig, HTLC, ZC) — accept if HF4+.
if !hf4Active { 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 return nil
} }
func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error { func checkOutputs(tx *types.Transaction, hf4Active bool) error {
if len(tx.Vout) == 0 { if len(tx.Vout) == 0 {
return ErrNoOutputs return ErrNoOutputs
} }
if hf4Active && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs { 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 { 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 { for i, vout := range tx.Vout {
switch o := vout.(type) { switch o := vout.(type) {
case types.TxOutputBare: case types.TxOutputBare:
if o.Amount == 0 { if o.Amount == 0 {
return fmt.Errorf("%w: output %d has zero amount", ErrInvalidOutput, i) return coreerr.E("checkOutputs", fmt.Sprintf("output %d has zero amount", i), ErrInvalidOutput)
}
// 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())
}
} }
case types.TxOutputZarcanum: case types.TxOutputZarcanum:
// Validated by proof verification. // Validated by proof verification.
@ -127,45 +109,17 @@ func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error {
return nil 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 { func checkKeyImages(tx *types.Transaction) error {
seen := make(map[types.KeyImage]struct{}) seen := make(map[types.KeyImage]struct{})
for _, vin := range tx.Vin { for _, vin := range tx.Vin {
var ki types.KeyImage toKey, ok := vin.(types.TxInputToKey)
switch v := vin.(type) { if !ok {
case types.TxInputToKey:
ki = v.KeyImage
case types.TxInputHTLC:
ki = v.KeyImage
default:
continue continue
} }
if _, exists := seen[ki]; exists { if _, exists := seen[toKey.KeyImage]; exists {
return fmt.Errorf("%w: %s", ErrDuplicateKeyImage, ki) return coreerr.E("checkKeyImages", toKey.KeyImage.String(), ErrDuplicateKeyImage)
} }
seen[ki] = struct{}{} seen[toKey.KeyImage] = struct{}{}
} }
return nil return nil
} }

View file

@ -9,6 +9,8 @@ import (
"bytes" "bytes"
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire" "forge.lthn.ai/core/go-blockchain/wire"
) )
@ -38,14 +40,14 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
dec := wire.NewDecoder(bytes.NewReader(raw)) dec := wire.NewDecoder(bytes.NewReader(raw))
count := dec.ReadVarint() count := dec.ReadVarint()
if dec.Err() != nil { 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) entries := make([]v2SigEntry, 0, count)
for i := uint64(0); i < count; i++ { for i := uint64(0); i < count; i++ {
tag := dec.ReadUint8() tag := dec.ReadUint8()
if dec.Err() != nil { 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} entry := v2SigEntry{tag: tag}
@ -54,7 +56,7 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
case types.SigTypeZC: case types.SigTypeZC:
zc, err := parseZCSig(dec) zc, err := parseZCSig(dec)
if err != nil { 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 entry.zcSig = zc
@ -74,11 +76,11 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
skipZarcanumSig(dec) skipZarcanumSig(dec)
default: 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 { 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) entries = append(entries, entry)
} }
@ -117,7 +119,7 @@ func parseZCSig(dec *wire.Decoder) (*zcSigData, error) {
} }
if rgCount != rxCount { 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) zc.ringSize = int(rgCount)
@ -155,9 +157,9 @@ func skipZarcanumSig(dec *wire.Decoder) {
_ = dec.ReadBytes(32) _ = dec.ReadBytes(32)
// CLSAG_GGXXG: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32) + K3(32) + K4(32). // CLSAG_GGXXG: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32) + K3(32) + K4(32).
_ = dec.ReadBytes(32) // c _ = dec.ReadBytes(32) // c
skipVecOfPoints(dec) // r_g skipVecOfPoints(dec) // r_g
skipVecOfPoints(dec) // r_x skipVecOfPoints(dec) // r_x
_ = dec.ReadBytes(128) // K1+K2+K3+K4 _ = dec.ReadBytes(128) // K1+K2+K3+K4
} }
@ -196,48 +198,48 @@ func parseV2Proofs(raw []byte) (*v2ProofData, error) {
dec := wire.NewDecoder(bytes.NewReader(raw)) dec := wire.NewDecoder(bytes.NewReader(raw))
count := dec.ReadVarint() count := dec.ReadVarint()
if dec.Err() != nil { 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 var data v2ProofData
for i := uint64(0); i < count; i++ { for i := uint64(0); i < count; i++ {
tag := dec.ReadUint8() tag := dec.ReadUint8()
if dec.Err() != nil { 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 { switch tag {
case 46: // zc_asset_surjection_proof: varint(nBGE) + nBGE * BGE_proof case 46: // zc_asset_surjection_proof: varint(nBGE) + nBGE * BGE_proof
nBGE := dec.ReadVarint() nBGE := dec.ReadVarint()
if dec.Err() != nil { 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) data.bgeProofs = make([][]byte, nBGE)
for j := uint64(0); j < nBGE; j++ { for j := uint64(0); j < nBGE; j++ {
data.bgeProofs[j] = readBGEProofBytes(dec) data.bgeProofs[j] = readBGEProofBytes(dec)
if dec.Err() != nil { 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 case 47: // zc_outs_range_proof: bpp_serialized + aggregation_proof
data.bppProofBytes = readBPPBytes(dec) data.bppProofBytes = readBPPBytes(dec)
if dec.Err() != nil { 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) data.bppCommitments = readAggregationCommitments(dec)
if dec.Err() != nil { 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) case 48: // zc_balance_proof: 96 bytes (c, y0, y1)
data.balanceProof = dec.ReadBytes(96) data.balanceProof = dec.ReadBytes(96)
if dec.Err() != nil { 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: 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 package consensus
import ( import (
"errors"
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/config" "forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/crypto" "forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/types" "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. // 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 { func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) error {
// Count ring-sig inputs (TxInputToKey and TxInputHTLC). // Count key inputs.
var keyInputCount int var keyInputCount int
for _, vin := range tx.Vin { for _, vin := range tx.Vin {
switch vin.(type) { if _, ok := vin.(types.TxInputToKey); ok {
case types.TxInputToKey, types.TxInputHTLC:
keyInputCount++ keyInputCount++
} }
} }
if len(tx.Signatures) != keyInputCount { if len(tx.Signatures) != keyInputCount {
return fmt.Errorf("consensus: signature count %d != input count %d", return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: signature count %d != input count %d", len(tx.Signatures), keyInputCount), nil)
len(tx.Signatures), keyInputCount)
} }
// Actual NLSAG verification requires the crypto bridge and ring outputs. // 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 var sigIdx int
for _, vin := range tx.Vin { for _, vin := range tx.Vin {
// Extract the common ring-sig fields from either input type. inp, ok := vin.(types.TxInputToKey)
var amount uint64 if !ok {
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:
continue continue
} }
// Extract absolute global indices from key offsets. // Extract absolute global indices from key offsets.
offsets := make([]uint64, len(keyOffsets)) offsets := make([]uint64, len(inp.KeyOffsets))
for i, ref := range keyOffsets { for i, ref := range inp.KeyOffsets {
offsets[i] = ref.GlobalIndex offsets[i] = ref.GlobalIndex
} }
ringKeys, err := getRingOutputs(amount, offsets) ringKeys, err := getRingOutputs(inp.Amount, offsets)
if err != nil { if err != nil {
return fmt.Errorf("consensus: failed to fetch ring outputs for input %d: %w", return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: failed to fetch ring outputs for input %d", sigIdx), err)
sigIdx, err)
} }
ringSigs := tx.Signatures[sigIdx] ringSigs := tx.Signatures[sigIdx]
if len(ringSigs) != len(ringKeys) { if len(ringSigs) != len(ringKeys) {
return fmt.Errorf("consensus: input %d has %d signatures but ring size %d", return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: input %d has %d signatures but ring size %d", sigIdx, len(ringSigs), len(ringKeys)), nil)
sigIdx, len(ringSigs), len(ringKeys))
} }
// Convert typed slices to raw byte arrays for the crypto bridge. // 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) sigs[i] = [64]byte(s)
} }
if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(keyImage), pubs, sigs) { if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(inp.KeyImage), pubs, sigs) {
return fmt.Errorf("consensus: ring signature verification failed for input %d", sigIdx) return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: ring signature verification failed for input %d", sigIdx), nil)
} }
sigIdx++ sigIdx++
@ -146,13 +129,12 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
// Parse the signature variant vector. // Parse the signature variant vector.
sigEntries, err := parseV2Signatures(tx.SignaturesRaw) sigEntries, err := parseV2Signatures(tx.SignaturesRaw)
if err != nil { 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. // Match signatures to inputs: each input must have a corresponding signature.
if len(sigEntries) != len(tx.Vin) { if len(sigEntries) != len(tx.Vin) {
return fmt.Errorf("consensus: V2 signature count %d != input count %d", return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: V2 signature count %d != input count %d", len(sigEntries), len(tx.Vin)), nil)
len(sigEntries), len(tx.Vin))
} }
// Validate that ZC inputs have ZC_sig and vice versa. // Validate that ZC inputs have ZC_sig and vice versa.
@ -160,13 +142,11 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
switch vin.(type) { switch vin.(type) {
case types.TxInputZC: case types.TxInputZC:
if sigEntries[i].tag != types.SigTypeZC { if sigEntries[i].tag != types.SigTypeZC {
return fmt.Errorf("consensus: input %d is ZC but signature tag is 0x%02x", return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is ZC but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
i, sigEntries[i].tag)
} }
case types.TxInputToKey: case types.TxInputToKey:
if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid { 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", return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is to_key but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
i, sigEntries[i].tag)
} }
} }
} }
@ -190,7 +170,7 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
zc := sigEntries[i].zcSig zc := sigEntries[i].zcSig
if zc == nil { 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. // Extract absolute global indices from key offsets.
@ -201,12 +181,11 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
ringMembers, err := getZCRingOutputs(offsets) ringMembers, err := getZCRingOutputs(offsets)
if err != nil { 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 { if len(ringMembers) != zc.ringSize {
return fmt.Errorf("consensus: input %d: ring size %d from chain != %d from sig", return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d: ring size %d from chain != %d from sig", i, len(ringMembers), zc.ringSize), nil)
i, len(ringMembers), zc.ringSize)
} }
// Build flat ring: [stealth(32) | commitment(32) | blinded_asset_id(32)] per entry. // 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), [32]byte(zcIn.KeyImage),
zc.clsagFlatSig, 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. // Parse and verify proofs.
proofs, err := parseV2Proofs(tx.Proofs) proofs, err := parseV2Proofs(tx.Proofs)
if err != nil { if err != nil {
return fmt.Errorf("consensus: %w", err) return coreerr.E("verifyV2Signatures", "consensus", err)
} }
// Verify BPP range proof if present. // Verify BPP range proof if present.
if len(proofs.bppProofBytes) > 0 && len(proofs.bppCommitments) > 0 { if len(proofs.bppProofBytes) > 0 && len(proofs.bppCommitments) > 0 {
if !crypto.VerifyBPP(proofs.bppProofBytes, proofs.bppCommitments) { 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) { if len(proofs.bgeProofs) != len(outputAssetIDs) {
return fmt.Errorf("consensus: BGE proof count %d != Zarcanum output count %d", return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE proof count %d != Zarcanum output count %d", len(proofs.bgeProofs), len(outputAssetIDs)), nil)
len(proofs.bgeProofs), len(outputAssetIDs))
} }
// Collect pseudo-out asset IDs from ZC signatures and expand to full points. // 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 { for i, p := range pseudoOutAssetIDs {
full, err := crypto.PointMul8(p) full, err := crypto.PointMul8(p)
if err != nil { 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 mul8PseudoOuts[i] = full
} }
@ -307,7 +285,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
// mul8 the output's blinded asset ID. // mul8 the output's blinded asset ID.
mul8Out, err := crypto.PointMul8(outAssetID) mul8Out, err := crypto.PointMul8(outAssetID)
if err != nil { 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) // 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 { for i, mul8Pseudo := range mul8PseudoOuts {
diff, err := crypto.PointSub(mul8Pseudo, mul8Out) diff, err := crypto.PointSub(mul8Pseudo, mul8Out)
if err != nil { 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 ring[i] = diff
} }
if !crypto.VerifyBGE(context, ring, proofs.bgeProofs[j]) { 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" "sync/atomic"
"time" "time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/consensus" "forge.lthn.ai/core/go-blockchain/consensus"
"forge.lthn.ai/core/go-blockchain/crypto" "forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/rpc" "forge.lthn.ai/core/go-blockchain/rpc"
@ -139,18 +141,18 @@ func (m *Miner) Start(ctx context.Context) error {
// Parse difficulty. // Parse difficulty.
diff, err := strconv.ParseUint(tmpl.Difficulty, 10, 64) diff, err := strconv.ParseUint(tmpl.Difficulty, 10, 64)
if err != nil { 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. // Decode the block template blob.
blobBytes, err := hex.DecodeString(tmpl.BlockTemplateBlob) blobBytes, err := hex.DecodeString(tmpl.BlockTemplateBlob)
if err != nil { 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)) dec := wire.NewDecoder(bytes.NewReader(blobBytes))
block := wire.DecodeBlock(dec) block := wire.DecodeBlock(dec)
if dec.Err() != nil { 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. // 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[:]) powHash, err := crypto.RandomXHash(RandomXKey, input[:])
if err != nil { if err != nil {
return fmt.Errorf("mining: RandomX hash: %w", err) return coreerr.E("Miner.mine", "mining: RandomX hash", err)
} }
m.hashCount.Add(1) 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) enc := wire.NewEncoder(&buf)
wire.EncodeBlock(enc, block) wire.EncodeBlock(enc, block)
if enc.Err() != nil { 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()) hexBlob := hex.EncodeToString(buf.Bytes())
if err := m.provider.SubmitBlock(hexBlob); err != nil { 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) m.blocksFound.Add(1)

View file

@ -26,8 +26,8 @@ func (d *CoreSyncData) MarshalSection() levin.Section {
"current_height": levin.Uint64Val(d.CurrentHeight), "current_height": levin.Uint64Val(d.CurrentHeight),
"top_id": levin.StringVal(d.TopID[:]), "top_id": levin.StringVal(d.TopID[:]),
"last_checkpoint_height": levin.Uint64Val(d.LastCheckpointHeight), "last_checkpoint_height": levin.Uint64Val(d.LastCheckpointHeight),
"core_time": levin.Uint64Val(d.CoreTime), "core_time": levin.Uint64Val(d.CoreTime),
"client_version": levin.StringVal([]byte(d.ClientVersion)), "client_version": levin.StringVal([]byte(d.ClientVersion)),
"non_pruning_mode_enabled": levin.BoolVal(d.NonPruningMode), "non_pruning_mode_enabled": levin.BoolVal(d.NonPruningMode),
} }
} }

View file

@ -5,19 +5,23 @@
package rpc package rpc
import "fmt" import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
)
// GetLastBlockHeader returns the header of the most recent block. // GetLastBlockHeader returns the header of the most recent block.
func (c *Client) GetLastBlockHeader() (*BlockHeader, error) { func (c *Client) GetLastBlockHeader() (*BlockHeader, error) {
var resp struct { var resp struct {
BlockHeader BlockHeader `json:"block_header"` BlockHeader BlockHeader `json:"block_header"`
Status string `json:"status"` Status string `json:"status"`
} }
if err := c.call("getlastblockheader", struct{}{}, &resp); err != nil { if err := c.call("getlastblockheader", struct{}{}, &resp); err != nil {
return nil, err return nil, err
} }
if resp.Status != "OK" { 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 return &resp.BlockHeader, nil
} }
@ -29,13 +33,13 @@ func (c *Client) GetBlockHeaderByHeight(height uint64) (*BlockHeader, error) {
}{Height: height} }{Height: height}
var resp struct { var resp struct {
BlockHeader BlockHeader `json:"block_header"` BlockHeader BlockHeader `json:"block_header"`
Status string `json:"status"` Status string `json:"status"`
} }
if err := c.call("getblockheaderbyheight", params, &resp); err != nil { if err := c.call("getblockheaderbyheight", params, &resp); err != nil {
return nil, err return nil, err
} }
if resp.Status != "OK" { 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 return &resp.BlockHeader, nil
} }
@ -47,13 +51,13 @@ func (c *Client) GetBlockHeaderByHash(hash string) (*BlockHeader, error) {
}{Hash: hash} }{Hash: hash}
var resp struct { var resp struct {
BlockHeader BlockHeader `json:"block_header"` BlockHeader BlockHeader `json:"block_header"`
Status string `json:"status"` Status string `json:"status"`
} }
if err := c.call("getblockheaderbyhash", params, &resp); err != nil { if err := c.call("getblockheaderbyhash", params, &resp); err != nil {
return nil, err return nil, err
} }
if resp.Status != "OK" { 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 return &resp.BlockHeader, nil
} }
@ -73,7 +77,7 @@ func (c *Client) GetBlocksDetails(heightStart, count uint64) ([]BlockDetails, er
return nil, err return nil, err
} }
if resp.Status != "OK" { 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 return resp.Blocks, nil
} }

View file

@ -14,6 +14,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
coreerr "forge.lthn.ai/core/go-log"
) )
// Client is a Lethean daemon RPC client. // Client is a Lethean daemon RPC client.
@ -66,10 +68,10 @@ type jsonRPCRequest struct {
} }
type jsonRPCResponse struct { type jsonRPCResponse struct {
JSONRPC string `json:"jsonrpc"` JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"` ID json.RawMessage `json:"id"`
Result json.RawMessage `json:"result"` Result json.RawMessage `json:"result"`
Error *jsonRPCError `json:"error,omitempty"` Error *jsonRPCError `json:"error,omitempty"`
} }
type jsonRPCError struct { type jsonRPCError struct {
@ -86,27 +88,27 @@ func (c *Client) call(method string, params any, result any) error {
Params: params, Params: params,
}) })
if err != nil { 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)) resp, err := c.httpClient.Post(c.url, "application/json", bytes.NewReader(reqBody))
if err != nil { 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() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { 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) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return fmt.Errorf("read response: %w", err) return coreerr.E("Client.call", "read response", err)
} }
var rpcResp jsonRPCResponse var rpcResp jsonRPCResponse
if err := json.Unmarshal(body, &rpcResp); err != nil { 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 { 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 result != nil && len(rpcResp.Result) > 0 {
if err := json.Unmarshal(rpcResp.Result, result); err != nil { 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 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 { func (c *Client) legacyCall(path string, params any, result any) error {
reqBody, err := json.Marshal(params) reqBody, err := json.Marshal(params)
if err != nil { if err != nil {
return fmt.Errorf("marshal request: %w", err) return coreerr.E("Client.legacyCall", "marshal request", err)
} }
url := c.baseURL + path url := c.baseURL + path
resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(reqBody)) resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(reqBody))
if err != nil { 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() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { 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) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return fmt.Errorf("read response: %w", err) return coreerr.E("Client.legacyCall", "read response", err)
} }
if result != nil { if result != nil {
if err := json.Unmarshal(body, result); err != 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 return nil

View file

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

View file

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

View file

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

View file

@ -48,22 +48,22 @@ type DaemonInfo struct {
// BlockDetails is a full block with metadata as returned by get_blocks_details. // BlockDetails is a full block with metadata as returned by get_blocks_details.
type BlockDetails struct { type BlockDetails struct {
Height uint64 `json:"height"` Height uint64 `json:"height"`
Timestamp uint64 `json:"timestamp"` Timestamp uint64 `json:"timestamp"`
ActualTimestamp uint64 `json:"actual_timestamp"` ActualTimestamp uint64 `json:"actual_timestamp"`
BaseReward uint64 `json:"base_reward"` BaseReward uint64 `json:"base_reward"`
SummaryReward uint64 `json:"summary_reward"` SummaryReward uint64 `json:"summary_reward"`
TotalFee uint64 `json:"total_fee"` TotalFee uint64 `json:"total_fee"`
ID string `json:"id"` ID string `json:"id"`
PrevID string `json:"prev_id"` PrevID string `json:"prev_id"`
Difficulty string `json:"difficulty"` Difficulty string `json:"difficulty"`
CumulativeDiffPrecise string `json:"cumulative_diff_precise"` CumulativeDiffPrecise string `json:"cumulative_diff_precise"`
Type uint64 `json:"type"` Type uint64 `json:"type"`
IsOrphan bool `json:"is_orphan"` IsOrphan bool `json:"is_orphan"`
CumulativeSize uint64 `json:"block_cumulative_size"` CumulativeSize uint64 `json:"block_cumulative_size"`
Blob string `json:"blob"` Blob string `json:"blob"`
ObjectInJSON string `json:"object_in_json"` ObjectInJSON string `json:"object_in_json"`
Transactions []TxInfo `json:"transactions_details"` Transactions []TxInfo `json:"transactions_details"`
} }
// TxInfo is transaction metadata as returned by get_tx_details. // TxInfo is transaction metadata as returned by get_tx_details.

View file

@ -8,12 +8,14 @@ package rpc
import ( import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
) )
// RandomOutputEntry is a decoy output returned by getrandom_outs. // RandomOutputEntry is a decoy output returned by getrandom_outs.
type RandomOutputEntry struct { type RandomOutputEntry struct {
GlobalIndex uint64 `json:"global_index"` GlobalIndex uint64 `json:"global_index"`
PublicKey string `json:"public_key"` PublicKey string `json:"public_key"`
} }
// GetRandomOutputs fetches random decoy outputs for ring construction. // GetRandomOutputs fetches random decoy outputs for ring construction.
@ -33,7 +35,7 @@ func (c *Client) GetRandomOutputs(amount uint64, count int) ([]RandomOutputEntry
return nil, err return nil, err
} }
if resp.Status != "OK" { 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 return resp.Outs, nil
} }
@ -53,7 +55,7 @@ func (c *Client) SendRawTransaction(txBlob []byte) error {
return err return err
} }
if resp.Status != "OK" { 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 return nil
} }

View file

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

View file

@ -10,8 +10,8 @@ import (
"strings" "strings"
"time" "time"
tea "github.com/charmbracelet/bubbletea"
cli "forge.lthn.ai/core/cli/pkg/cli" 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/chain"
"forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/types"
@ -23,7 +23,7 @@ var _ cli.FrameModel = (*ExplorerModel)(nil)
type explorerView int type explorerView int
const ( const (
viewBlockList explorerView = iota viewBlockList explorerView = iota
viewBlockDetail viewBlockDetail
viewTxDetail viewTxDetail
) )

View file

@ -10,10 +10,11 @@
package types package types
import ( import (
"errors"
"fmt" "fmt"
"math/big" "math/big"
coreerr "forge.lthn.ai/core/go-log"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
"forge.lthn.ai/core/go-blockchain/config" "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) { func DecodeAddress(s string) (*Address, uint64, error) {
raw, err := base58Decode(s) raw, err := base58Decode(s)
if err != nil { 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. // The minimum size is: 1 byte prefix varint + 32 + 32 + 1 flags + 4 checksum = 70.
if len(raw) < 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. // Decode the prefix varint.
prefix, prefixLen, err := decodeVarint(raw) prefix, prefixLen, err := decodeVarint(raw)
if err != nil { 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. // After the prefix we need exactly 32+32+1+4 = 69 bytes.
remaining := raw[prefixLen:] remaining := raw[prefixLen:]
if len(remaining) != 69 { 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. // 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[1] != actualChecksum[1] ||
expectedChecksum[2] != actualChecksum[2] || expectedChecksum[2] != actualChecksum[2] ||
expectedChecksum[3] != actualChecksum[3] { 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{} addr := &Address{}
@ -214,7 +215,7 @@ func encodeBlock(block []byte, encodedSize int) []byte {
// base58Decode decodes a CryptoNote base58 string back into raw bytes. // base58Decode decodes a CryptoNote base58 string back into raw bytes.
func base58Decode(s string) ([]byte, error) { func base58Decode(s string) ([]byte, error) {
if len(s) == 0 { 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 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. // Validate that the last block size maps to a valid byte count.
if lastBlockChars > 0 && base58ReverseBlockSizes[lastBlockChars] < 0 { 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 var result []byte
@ -257,7 +258,7 @@ func decodeBlock(s string, byteCount int) ([]byte, error) {
for _, c := range []byte(s) { for _, c := range []byte(s) {
idx := base58CharIndex(c) idx := base58CharIndex(c)
if idx < 0 { 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.Mul(num, base)
num.Add(num, big.NewInt(int64(idx))) 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. // Convert to fixed-size byte array, big-endian.
raw := num.Bytes() raw := num.Bytes()
if len(raw) > byteCount { 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. // Pad with leading zeroes if necessary.
@ -310,7 +311,7 @@ func encodeVarint(v uint64) []byte {
func decodeVarint(data []byte) (uint64, int, error) { func decodeVarint(data []byte) (uint64, int, error) {
if len(data) == 0 { 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 var v uint64
for i := 0; i < len(data) && i < 10; i++ { 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 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 ( import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
) )
// Hash is a 256-bit (32-byte) hash value, typically produced by Keccak-256. // 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 var h Hash
b, err := hex.DecodeString(s) b, err := hex.DecodeString(s)
if err != nil { 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 { 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) copy(h[:], b)
return h, nil return h, nil
@ -65,10 +67,10 @@ func PublicKeyFromHex(s string) (PublicKey, error) {
var pk PublicKey var pk PublicKey
b, err := hex.DecodeString(s) b, err := hex.DecodeString(s)
if err != nil { 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 { 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) copy(pk[:], b)
return pk, nil return pk, nil

View file

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

View file

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

View file

@ -13,6 +13,8 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire" "forge.lthn.ai/core/go-blockchain/wire"
) )
@ -46,7 +48,7 @@ func ParseTxExtra(raw []byte) (*TxExtra, error) {
count, n, err := wire.DecodeVarint(raw) count, n, err := wire.DecodeVarint(raw)
if err != nil { 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 pos := n
@ -57,7 +59,7 @@ func ParseTxExtra(raw []byte) (*TxExtra, error) {
switch tag { switch tag {
case extraTagPublicKey: case extraTagPublicKey:
if pos+32 > len(raw) { 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]) copy(extra.TxPublicKey[:], raw[pos:pos+32])
pos += 32 pos += 32
@ -65,7 +67,7 @@ func ParseTxExtra(raw []byte) (*TxExtra, error) {
case extraTagUnlockTime: case extraTagUnlockTime:
val, vn, vErr := wire.DecodeVarint(raw[pos:]) val, vn, vErr := wire.DecodeVarint(raw[pos:])
if vErr != nil { 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 extra.UnlockTime = val
pos += vn pos += vn
@ -73,7 +75,7 @@ func ParseTxExtra(raw []byte) (*TxExtra, error) {
case extraTagDerivationHint: case extraTagDerivationHint:
length, vn, vErr := wire.DecodeVarint(raw[pos:]) length, vn, vErr := wire.DecodeVarint(raw[pos:])
if vErr != nil { 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 pos += vn
if length == 2 && pos+2 <= len(raw) { 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. // String types: varint(length) + length bytes.
case 7, 9, 11, 19: case 7, 9, 11, 19:
if len(data) == 0 { 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) length, n, err := wire.DecodeVarint(data)
if err != nil { 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 return n + int(length), nil
@ -122,7 +124,7 @@ func skipExtraElement(data []byte, tag uint8) (int, error) {
case 14, 15, 16, 26, 27: case 14, 15, 16, 26, 27:
_, n, err := wire.DecodeVarint(data) _, n, err := wire.DecodeVarint(data)
if err != nil { 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 return n, nil
@ -139,6 +141,6 @@ func skipExtraElement(data []byte, tag uint8) (int, error) {
return 64, nil // signature return 64, nil // signature
default: 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 ( import (
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"strings" "strings"
coreerr "forge.lthn.ai/core/go-log"
) )
const numWords = 1626 const numWords = 1626
@ -13,7 +14,7 @@ const numWords = 1626
// MnemonicEncode converts a 32-byte secret key to a 25-word mnemonic phrase. // MnemonicEncode converts a 32-byte secret key to a 25-word mnemonic phrase.
func MnemonicEncode(key []byte) (string, error) { func MnemonicEncode(key []byte) (string, error) {
if len(key) != 32 { 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) words := make([]string, 0, 25)
@ -39,12 +40,12 @@ func MnemonicDecode(phrase string) ([32]byte, error) {
words := strings.Fields(phrase) words := strings.Fields(phrase)
if len(words) != 25 { 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]) expected := checksumIndex(words[:24])
if words[24] != words[expected] { 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) n := uint32(numWords)
@ -61,7 +62,7 @@ func MnemonicDecode(phrase string) ([32]byte, error) {
if !ok3 { if !ok3 {
word = words[i*3+2] 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) + val := uint32(w1) +

View file

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

View file

@ -10,10 +10,9 @@
package wallet package wallet
import ( import (
"fmt"
"forge.lthn.ai/core/go-blockchain/crypto" "forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/types"
coreerr "forge.lthn.ai/core/go-log"
) )
// Signer produces signatures for transaction inputs. // Signer produces signatures for transaction inputs.
@ -35,7 +34,7 @@ func (s *NLSAGSigner) SignInput(prefixHash types.Hash, ephemeral KeyPair,
ki, err := crypto.GenerateKeyImage( ki, err := crypto.GenerateKeyImage(
[32]byte(ephemeral.Public), [32]byte(ephemeral.Secret)) [32]byte(ephemeral.Public), [32]byte(ephemeral.Secret))
if err != nil { 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)) 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(prefixHash), ki, pubs,
[32]byte(ephemeral.Secret), realIndex) [32]byte(ephemeral.Secret), realIndex)
if err != nil { 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)) sigs := make([]types.Signature, len(rawSigs))

View file

@ -13,6 +13,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
store "forge.lthn.ai/core/go-store" store "forge.lthn.ai/core/go-store"
"forge.lthn.ai/core/go-blockchain/config" "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 { func putTransfer(s *store.Store, tr *Transfer) error {
val, err := json.Marshal(tr) val, err := json.Marshal(tr)
if err != nil { 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)) 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) { func getTransfer(s *store.Store, ki types.KeyImage) (*Transfer, error) {
val, err := s.Get(groupTransfers, ki.String()) val, err := s.Get(groupTransfers, ki.String())
if err != nil { 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 var tr Transfer
if err := json.Unmarshal([]byte(val), &tr); err != nil { 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 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) { func listTransfers(s *store.Store) ([]Transfer, error) {
pairs, err := s.GetAll(groupTransfers) pairs, err := s.GetAll(groupTransfers)
if err != nil { 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)) transfers := make([]Transfer, 0, len(pairs))
for _, val := range pairs { for _, val := range pairs {

View file

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

View file

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

View file

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

View file

@ -8,6 +8,8 @@ package wire
import ( import (
"fmt" "fmt"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/types"
) )
@ -162,21 +164,6 @@ func encodeInputs(enc *Encoder, vin []types.TxInput) {
encodeKeyOffsets(enc, v.KeyOffsets) encodeKeyOffsets(enc, v.KeyOffsets)
enc.WriteBlob32((*[32]byte)(&v.KeyImage)) enc.WriteBlob32((*[32]byte)(&v.KeyImage))
enc.WriteBytes(v.EtcDetails) 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)) dec.ReadBlob32((*[32]byte)(&in.KeyImage))
in.EtcDetails = decodeRawVariantVector(dec) in.EtcDetails = decodeRawVariantVector(dec)
vin = append(vin, in) 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: 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 return vin
} }
} }
@ -266,7 +234,7 @@ func decodeKeyOffsets(dec *Decoder) []types.TxOutRef {
dec.ReadBlob32((*[32]byte)(&refs[i].TxID)) dec.ReadBlob32((*[32]byte)(&refs[i].TxID))
refs[i].N = dec.ReadVarint() refs[i].N = dec.ReadVarint()
default: 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 return refs
} }
} }
@ -284,26 +252,9 @@ func encodeOutputsV1(enc *Encoder, vout []types.TxOutput) {
case types.TxOutputBare: case types.TxOutputBare:
enc.WriteVarint(v.Amount) enc.WriteVarint(v.Amount)
// Target is a variant (txout_target_v) // Target is a variant (txout_target_v)
switch tgt := v.Target.(type) { enc.WriteVariantTag(types.TargetTypeToKey)
case types.TxOutToKey: enc.WriteBlob32((*[32]byte)(&v.Target.Key))
enc.WriteVariantTag(types.TargetTypeToKey) enc.WriteUint8(v.Target.MixAttr)
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))
}
} }
} }
} }
@ -323,31 +274,10 @@ func decodeOutputsV1(dec *Decoder) []types.TxOutput {
} }
switch tag { switch tag {
case types.TargetTypeToKey: case types.TargetTypeToKey:
var tgt types.TxOutToKey dec.ReadBlob32((*[32]byte)(&out.Target.Key))
dec.ReadBlob32((*[32]byte)(&tgt.Key)) out.Target.MixAttr = dec.ReadUint8()
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: 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 return vout
} }
vout = append(vout, out) vout = append(vout, out)
@ -363,26 +293,9 @@ func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) {
switch v := out.(type) { switch v := out.(type) {
case types.TxOutputBare: case types.TxOutputBare:
enc.WriteVarint(v.Amount) enc.WriteVarint(v.Amount)
switch tgt := v.Target.(type) { enc.WriteVariantTag(types.TargetTypeToKey)
case types.TxOutToKey: enc.WriteBlob32((*[32]byte)(&v.Target.Key))
enc.WriteVariantTag(types.TargetTypeToKey) enc.WriteUint8(v.Target.MixAttr)
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))
}
case types.TxOutputZarcanum: case types.TxOutputZarcanum:
enc.WriteBlob32((*[32]byte)(&v.StealthAddress)) enc.WriteBlob32((*[32]byte)(&v.StealthAddress))
enc.WriteBlob32((*[32]byte)(&v.ConcealingPoint)) enc.WriteBlob32((*[32]byte)(&v.ConcealingPoint))
@ -410,36 +323,11 @@ func decodeOutputsV2(dec *Decoder) []types.TxOutput {
var out types.TxOutputBare var out types.TxOutputBare
out.Amount = dec.ReadVarint() out.Amount = dec.ReadVarint()
targetTag := dec.ReadVariantTag() targetTag := dec.ReadVariantTag()
if dec.Err() != nil { if targetTag == types.TargetTypeToKey {
return vout dec.ReadBlob32((*[32]byte)(&out.Target.Key))
} out.Target.MixAttr = dec.ReadUint8()
switch targetTag { } else {
case types.TargetTypeToKey: dec.err = coreerr.E("decodeOutputsV2", fmt.Sprintf("wire: unsupported target tag 0x%02x", targetTag), nil)
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)
return vout return vout
} }
vout = append(vout, out) vout = append(vout, out)
@ -453,7 +341,7 @@ func decodeOutputsV2(dec *Decoder) []types.TxOutput {
out.MixAttr = dec.ReadUint8() out.MixAttr = dec.ReadUint8()
vout = append(vout, out) vout = append(vout, out)
default: 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 return vout
} }
} }
@ -509,40 +397,32 @@ func decodeRawVariantVector(dec *Decoder) []byte {
// These are used by readVariantElementData to determine element boundaries // These are used by readVariantElementData to determine element boundaries
// when reading raw variant vectors (extra, attachment, etc_details). // when reading raw variant vectors (extra, attachment, etc_details).
const ( const (
tagTxComment = 7 // tx_comment — string tagTxComment = 7 // tx_comment — string
tagTxPayerOld = 8 // tx_payer_old — 2 public keys tagTxPayerOld = 8 // tx_payer_old — 2 public keys
tagString = 9 // std::string — string tagString = 9 // std::string — string
tagTxCryptoChecksum = 10 // tx_crypto_checksum — two uint32 LE tagTxCryptoChecksum = 10 // tx_crypto_checksum — two uint32 LE
tagTxDerivationHint = 11 // tx_derivation_hint — string tagTxDerivationHint = 11 // tx_derivation_hint — string
tagTxServiceAttachment = 12 // tx_service_attachment — 3 strings + vector<key> + uint8 tagTxServiceAttachment = 12 // tx_service_attachment — 3 strings + vector<key> + uint8
tagUnlockTime = 14 // etc_tx_details_unlock_time — varint tagUnlockTime = 14 // etc_tx_details_unlock_time — varint
tagExpirationTime = 15 // etc_tx_details_expiration_time — varint tagExpirationTime = 15 // etc_tx_details_expiration_time — varint
tagTxDetailsFlags = 16 // etc_tx_details_flags — varint tagTxDetailsFlags = 16 // etc_tx_details_flags — varint
tagSignedParts = 17 // signed_parts — two varints (n_outs, n_extras) tagSignedParts = 17 // signed_parts — two varints (n_outs, n_extras)
tagExtraAttachmentInfo = 18 // extra_attachment_info — string + hash + varint tagExtraAttachmentInfo = 18 // extra_attachment_info — string + hash + varint
tagExtraUserData = 19 // extra_user_data — string tagExtraUserData = 19 // extra_user_data — string
tagExtraAliasEntryOld = 20 // extra_alias_entry_old — complex tagExtraAliasEntryOld = 20 // extra_alias_entry_old — complex
tagExtraPadding = 21 // extra_padding — vector<uint8> tagExtraPadding = 21 // extra_padding — vector<uint8>
tagPublicKey = 22 // crypto::public_key — 32 bytes tagPublicKey = 22 // crypto::public_key — 32 bytes
tagEtcTxFlags16 = 23 // etc_tx_flags16_t — uint16 LE tagEtcTxFlags16 = 23 // etc_tx_flags16_t — uint16 LE
tagUint16 = 24 // uint16_t — uint16 LE tagUint16 = 24 // uint16_t — uint16 LE
tagUint64 = 26 // uint64_t — varint tagUint64 = 26 // uint64_t — varint
tagEtcTxTime = 27 // etc_tx_time — varint tagEtcTxTime = 27 // etc_tx_time — varint
tagUint32 = 28 // uint32_t — uint32 LE tagUint32 = 28 // uint32_t — uint32 LE
tagTxReceiverOld = 29 // tx_receiver_old — 2 public keys tagTxReceiverOld = 29 // tx_receiver_old — 2 public keys
tagUnlockTime2 = 30 // etc_tx_details_unlock_time2 — vector of entries tagUnlockTime2 = 30 // etc_tx_details_unlock_time2 — vector of entries
tagTxPayer = 31 // tx_payer — 2 keys + optional flag tagTxPayer = 31 // tx_payer — 2 keys + optional flag
tagTxReceiver = 32 // tx_receiver — 2 keys + optional flag tagTxReceiver = 32 // tx_receiver — 2 keys + optional flag
tagExtraAliasEntry = 33 // extra_alias_entry — complex tagExtraAliasEntry = 33 // extra_alias_entry — complex
tagZarcanumTxDataV1 = 39 // zarcanum_tx_data_v1 — varint (fee) 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). // Signature variant tags (signature_v).
tagNLSAGSig = 42 // NLSAG_sig — vector<signature> tagNLSAGSig = 42 // NLSAG_sig — vector<signature>
@ -612,18 +492,6 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte {
case tagZarcanumTxDataV1: // fee — FIELD(fee) → serialize_int → 8-byte LE case tagZarcanumTxDataV1: // fee — FIELD(fee) → serialize_int → 8-byte LE
return dec.ReadBytes(8) 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 // Signature variants
case tagNLSAGSig: // vector<signature> (64 bytes each) case tagNLSAGSig: // vector<signature> (64 bytes each)
return readVariantVectorFixed(dec, 64) return readVariantVectorFixed(dec, 64)
@ -643,7 +511,7 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte {
return dec.ReadBytes(96) return dec.ReadBytes(96)
default: 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 return nil
} }
} }
@ -804,261 +672,6 @@ func readSignedParts(dec *Decoder) []byte {
return raw 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 --- // --- crypto blob readers ---
// These read variable-length serialised crypto structures and return raw bytes. // These read variable-length serialised crypto structures and return raw bytes.
// All vectors are varint(count) + 32*count bytes (scalars or points). // 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). // 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) // 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 { func readZarcanumSig(dec *Decoder) []byte {
var raw []byte var raw []byte
// 10 fixed scalars/points // 10 fixed scalars/points