From 76488e0beb394ac71e30d88c88c046b4192bc45c Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 5 Apr 2026 08:46:54 +0100 Subject: [PATCH] feat(wire): add alias entry readers + AX usage-example comments Add readExtraAliasEntryOld (tag 20) and readExtraAliasEntry (tag 33) wire readers so the node can deserialise blocks containing alias registrations. Without these readers, mainnet sync would fail on any block with an alias transaction. Three round-trip tests validate the new readers. Also apply AX-2 (comments as usage examples) across 12 files: add concrete usage-example comments to exported functions in config/, types/, wire/, chain/, difficulty/, and consensus/. Fix stale doc in consensus/doc.go that incorrectly referenced *config.ChainConfig. Co-Authored-By: Virgil --- chain/chain.go | 4 ++ chain/index.go | 6 ++ chain/levinconn.go | 12 ++++ chain/store.go | 12 ++++ config/hardfork.go | 6 ++ consensus/doc.go | 5 +- difficulty/difficulty.go | 2 + types/address.go | 2 + types/transaction.go | 4 ++ wire/hash.go | 6 ++ wire/transaction.go | 96 ++++++++++++++++++++++++++++ wire/transaction_test.go | 132 +++++++++++++++++++++++++++++++++++++++ 12 files changed, 285 insertions(+), 2 deletions(-) diff --git a/chain/chain.go b/chain/chain.go index 7ec0dad..9628e4b 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -27,6 +27,8 @@ func New(s *store.Store) *Chain { } // Height returns the number of stored blocks (0 if empty). +// +// h, err := blockchain.Height() func (c *Chain) Height() (uint64, error) { n, err := c.store.Count(groupBlocks) if err != nil { @@ -37,6 +39,8 @@ func (c *Chain) Height() (uint64, error) { // TopBlock returns the highest stored block and its metadata. // Returns an error if the chain is empty. +// +// blk, meta, err := blockchain.TopBlock() func (c *Chain) TopBlock() (*types.Block, *BlockMeta, error) { h, err := c.Height() if err != nil { diff --git a/chain/index.go b/chain/index.go index b35495d..6d342f8 100644 --- a/chain/index.go +++ b/chain/index.go @@ -18,6 +18,8 @@ import ( ) // MarkSpent records a key image as spent at the given block height. +// +// err := blockchain.MarkSpent(input.KeyImage, blockHeight) func (c *Chain) MarkSpent(ki types.KeyImage, height uint64) error { if err := c.store.Set(groupSpentKeys, ki.String(), strconv.FormatUint(height, 10)); err != nil { return coreerr.E("Chain.MarkSpent", fmt.Sprintf("chain: mark spent %s", ki), err) @@ -26,6 +28,8 @@ func (c *Chain) MarkSpent(ki types.KeyImage, height uint64) error { } // IsSpent checks whether a key image has been spent. +// +// spent, err := blockchain.IsSpent(keyImage) func (c *Chain) IsSpent(ki types.KeyImage) (bool, error) { _, err := c.store.Get(groupSpentKeys, ki.String()) if errors.Is(err, store.ErrNotFound) { @@ -92,6 +96,8 @@ func (c *Chain) GetOutput(amount uint64, gindex uint64) (types.Hash, uint32, err } // OutputCount returns the number of outputs indexed for the given amount. +// +// count, err := blockchain.OutputCount(1_000_000_000_000) func (c *Chain) OutputCount(amount uint64) (uint64, error) { n, err := c.store.Count(outputGroup(amount)) if err != nil { diff --git a/chain/levinconn.go b/chain/levinconn.go index 5a48f58..50a1bb2 100644 --- a/chain/levinconn.go +++ b/chain/levinconn.go @@ -26,6 +26,10 @@ func NewLevinP2PConn(conn *levinpkg.Connection, peerHeight uint64, localSync p2p return &LevinP2PConn{conn: conn, peerHeight: peerHeight, localSync: localSync} } +// PeerHeight returns the remote peer's advertised chain height, +// obtained during the Levin handshake. +// +// height := conn.PeerHeight() func (c *LevinP2PConn) PeerHeight() uint64 { return c.peerHeight } // handleMessage processes non-target messages received while waiting for @@ -50,6 +54,10 @@ func (c *LevinP2PConn) handleMessage(hdr levinpkg.Header, data []byte) error { return nil } +// RequestChain sends NOTIFY_REQUEST_CHAIN with our sparse block ID list +// and returns the start height and block IDs from the peer's response. +// +// startHeight, blockIDs, err := conn.RequestChain(historyBytes) func (c *LevinP2PConn) RequestChain(blockIDs [][]byte) (uint64, [][]byte, error) { req := p2p.RequestChain{BlockIDs: blockIDs} payload, err := req.Encode() @@ -81,6 +89,10 @@ func (c *LevinP2PConn) RequestChain(blockIDs [][]byte) (uint64, [][]byte, error) } } +// RequestObjects sends NOTIFY_REQUEST_GET_OBJECTS for the given block +// hashes and returns the raw block and transaction blobs. +// +// entries, err := conn.RequestObjects(batchHashes) func (c *LevinP2PConn) RequestObjects(blockHashes [][]byte) ([]BlockBlobEntry, error) { req := p2p.RequestGetObjects{Blocks: blockHashes} payload, err := req.Encode() diff --git a/chain/store.go b/chain/store.go index 1f1fcb8..c1b00ec 100644 --- a/chain/store.go +++ b/chain/store.go @@ -41,6 +41,8 @@ type blockRecord struct { } // PutBlock stores a block and updates the block_index. +// +// err := blockchain.PutBlock(&blk, &chain.BlockMeta{Hash: blockHash, Height: 100}) func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error { var buf bytes.Buffer enc := wire.NewEncoder(&buf) @@ -72,6 +74,8 @@ func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error { } // GetBlockByHeight retrieves a block by its height. +// +// blk, meta, err := blockchain.GetBlockByHeight(1000) func (c *Chain) GetBlockByHeight(height uint64) (*types.Block, *BlockMeta, error) { val, err := c.store.Get(groupBlocks, heightKey(height)) if err != nil { @@ -84,6 +88,8 @@ func (c *Chain) GetBlockByHeight(height uint64) (*types.Block, *BlockMeta, error } // GetBlockByHash retrieves a block by its hash. +// +// blk, meta, err := blockchain.GetBlockByHash(blockHash) func (c *Chain) GetBlockByHash(hash types.Hash) (*types.Block, *BlockMeta, error) { heightStr, err := c.store.Get(groupBlockIndex, hash.String()) if err != nil { @@ -106,6 +112,8 @@ type txRecord struct { } // PutTransaction stores a transaction with metadata. +// +// err := blockchain.PutTransaction(txHash, &tx, &chain.TxMeta{KeeperBlock: height}) func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxMeta) error { var buf bytes.Buffer enc := wire.NewEncoder(&buf) @@ -130,6 +138,8 @@ func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxM } // GetTransaction retrieves a transaction by hash. +// +// tx, meta, err := blockchain.GetTransaction(txHash) func (c *Chain) GetTransaction(hash types.Hash) (*types.Transaction, *TxMeta, error) { val, err := c.store.Get(groupTx, hash.String()) if err != nil { @@ -156,6 +166,8 @@ func (c *Chain) GetTransaction(hash types.Hash) (*types.Transaction, *TxMeta, er } // HasTransaction checks whether a transaction exists in the store. +// +// if blockchain.HasTransaction(txHash) { /* already stored */ } func (c *Chain) HasTransaction(hash types.Hash) bool { _, err := c.store.Get(groupTx, hash.String()) return err == nil diff --git a/config/hardfork.go b/config/hardfork.go index 4792025..cbb7285 100644 --- a/config/hardfork.go +++ b/config/hardfork.go @@ -71,6 +71,8 @@ var TestnetForks = []HardFork{ // // A fork with Height=0 is active from genesis (height 0). // A fork with Height=N is active at heights > N. +// +// version := config.VersionAtHeight(config.MainnetForks, 15000) // returns HF2 func VersionAtHeight(forks []HardFork, height uint64) uint8 { var version uint8 for _, hf := range forks { @@ -85,6 +87,8 @@ func VersionAtHeight(forks []HardFork, height uint64) uint8 { // IsHardForkActive reports whether the specified hardfork version is active // at the given block height. +// +// if config.IsHardForkActive(config.MainnetForks, config.HF4Zarcanum, height) { /* Zarcanum rules apply */ } func IsHardForkActive(forks []HardFork, version uint8, height uint64) bool { for _, hf := range forks { if hf.Version == version { @@ -97,6 +101,8 @@ func IsHardForkActive(forks []HardFork, version uint8, height uint64) bool { // HardforkActivationHeight returns the activation height for the given // hardfork version. The fork becomes active at heights strictly greater // than the returned value. Returns (0, false) if the version is not found. +// +// height, ok := config.HardforkActivationHeight(config.TestnetForks, config.HF5) func HardforkActivationHeight(forks []HardFork, version uint8) (uint64, bool) { for _, hf := range forks { if hf.Version == version { diff --git a/consensus/doc.go b/consensus/doc.go index ce1ae16..4874b42 100644 --- a/consensus/doc.go +++ b/consensus/doc.go @@ -14,6 +14,7 @@ // - Cryptographic: PoW hash verification (RandomX via CGo), // ring signature verification, proof verification. // -// All functions take *config.ChainConfig and a block height for -// hardfork-aware validation. The package has no dependency on chain/. +// All validation functions take a hardfork schedule ([]config.HardFork) +// and a block height for hardfork-aware gating. The package has no +// dependency on chain/ or any storage layer. package consensus diff --git a/difficulty/difficulty.go b/difficulty/difficulty.go index 156d012..c6cd0d1 100644 --- a/difficulty/difficulty.go +++ b/difficulty/difficulty.go @@ -61,6 +61,8 @@ var StarterDifficulty = big.NewInt(1) // // where each solve-time interval i is weighted by its position (1..n), // giving more influence to recent blocks. +// +// nextDiff := difficulty.NextDifficulty(timestamps, cumulativeDiffs, 120) func NextDifficulty(timestamps []uint64, cumulativeDiffs []*big.Int, target uint64) *big.Int { // Need at least 2 entries to compute one solve-time interval. if len(timestamps) < 2 || len(cumulativeDiffs) < 2 { diff --git a/types/address.go b/types/address.go index a608bc4..e2c24bb 100644 --- a/types/address.go +++ b/types/address.go @@ -78,6 +78,8 @@ func (a *Address) Encode(prefix uint64) string { // DecodeAddress parses a CryptoNote base58-encoded address string. It returns // the decoded address, the prefix that was used, and any error. +// +// addr, prefix, err := types.DecodeAddress("iTHN6...") func DecodeAddress(s string) (*Address, uint64, error) { raw, err := base58Decode(s) if err != nil { diff --git a/types/transaction.go b/types/transaction.go index 2b90b0f..6620209 100644 --- a/types/transaction.go +++ b/types/transaction.go @@ -123,6 +123,8 @@ type TxOutTarget interface { // AsTxOutToKey returns target as a TxOutToKey when it is a standard // transparent output target. +// +// toKey, ok := types.AsTxOutToKey(bare.Target) func AsTxOutToKey(target TxOutTarget) (TxOutToKey, bool) { v, ok := target.(TxOutToKey) return v, ok @@ -247,6 +249,8 @@ type TxOutputBare struct { // SpendKey returns the standard transparent spend key when the target is // TxOutToKey. Callers that only care about transparent key outputs can use // this instead of repeating a type assertion. +// +// if key, ok := bareOutput.SpendKey(); ok { /* use key */ } func (t TxOutputBare) SpendKey() (PublicKey, bool) { target, ok := AsTxOutToKey(t.Target) if !ok { diff --git a/wire/hash.go b/wire/hash.go index 8b22afa..7f85ff0 100644 --- a/wire/hash.go +++ b/wire/hash.go @@ -44,6 +44,8 @@ func BlockHashingBlob(b *types.Block) []byte { // varint length prefix, so the actual hash input is: // // varint(len(blob)) || blob +// +// blockID := wire.BlockHash(&blk) func BlockHash(b *types.Block) types.Hash { blob := BlockHashingBlob(b) var prefixed []byte @@ -58,6 +60,8 @@ func BlockHash(b *types.Block) types.Hash { // get_transaction_prefix_hash for all versions. The tx_id is always // Keccak-256 of the serialised prefix (version + inputs + outputs + extra, // in version-dependent field order). +// +// txID := wire.TransactionHash(&tx) func TransactionHash(tx *types.Transaction) types.Hash { return TransactionPrefixHash(tx) } @@ -65,6 +69,8 @@ func TransactionHash(tx *types.Transaction) types.Hash { // TransactionPrefixHash computes the hash of a transaction prefix. // This is Keccak-256 of the serialised transaction prefix (version + vin + // vout + extra, in version-dependent order). +// +// prefixHash := wire.TransactionPrefixHash(&tx) func TransactionPrefixHash(tx *types.Transaction) types.Hash { var buf bytes.Buffer enc := NewEncoder(&buf) diff --git a/wire/transaction.go b/wire/transaction.go index b6865f8..2e87493 100644 --- a/wire/transaction.go +++ b/wire/transaction.go @@ -572,6 +572,12 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { case tagTxPayer, tagTxReceiver: return readTxPayer(dec) + // Alias types + case tagExtraAliasEntryOld: + return readExtraAliasEntryOld(dec) + case tagExtraAliasEntry: + return readExtraAliasEntry(dec) + // Composite types case tagExtraAttachmentInfo: return readExtraAttachmentInfo(dec) @@ -780,6 +786,96 @@ func readTxServiceAttachment(dec *Decoder) []byte { return raw } +// readExtraAliasEntryOld reads extra_alias_entry_old (tag 20). +// Structure: alias(string) + address(spend_key(32) + view_key(32)) + +// text_comment(string) + sign(vector of generic_schnorr_sig_s, each 64 bytes). +func readExtraAliasEntryOld(dec *Decoder) []byte { + var raw []byte + + // m_alias: string + alias := readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, alias...) + + // m_address: spend_public_key(32) + view_public_key(32) = 64 bytes + addr := dec.ReadBytes(64) + if dec.err != nil { + return nil + } + raw = append(raw, addr...) + + // m_text_comment: string + comment := readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, comment...) + + // m_sign: vector (each is 2 scalars = 64 bytes) + v := readVariantVectorFixed(dec, 64) + if dec.err != nil { + return nil + } + raw = append(raw, v...) + + return raw +} + +// readExtraAliasEntry reads extra_alias_entry (tag 33). +// Structure: alias(string) + address(spend_key(32) + view_key(32) + optional flag) + +// text_comment(string) + sign(vector of generic_schnorr_sig_s, each 64 bytes) + +// view_key(optional secret_key, 32 bytes). +func readExtraAliasEntry(dec *Decoder) []byte { + var raw []byte + + // m_alias: string + alias := readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, alias...) + + // m_address: account_public_address with optional is_auditable flag + // Same wire format as tx_payer (tag 31): spend_key(32) + view_key(32) + optional + addr := readTxPayer(dec) + if dec.err != nil { + return nil + } + raw = append(raw, addr...) + + // m_text_comment: string + comment := readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, comment...) + + // m_sign: vector (each is 2 scalars = 64 bytes) + v := readVariantVectorFixed(dec, 64) + if dec.err != nil { + return nil + } + raw = append(raw, v...) + + // m_view_key: optional — uint8 marker + 32 bytes if present + marker := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, marker) + if marker != 0 { + key := dec.ReadBytes(32) + if dec.err != nil { + return nil + } + raw = append(raw, key...) + } + + return raw +} + // readSignedParts reads signed_parts (tag 17). // Structure: n_outs (varint) + n_extras (varint). func readSignedParts(dec *Decoder) []byte { diff --git a/wire/transaction_test.go b/wire/transaction_test.go index aa289ae..0011231 100644 --- a/wire/transaction_test.go +++ b/wire/transaction_test.go @@ -1077,3 +1077,135 @@ func TestEncodeTransaction_UnsupportedOutputType_Bad(t *testing.T) { }) } } + +// TestExtraAliasEntryOldRoundTrip_Good verifies that a variant vector +// containing an extra_alias_entry_old (tag 20) round-trips through +// decodeRawVariantVector without error. +func TestExtraAliasEntryOldRoundTrip_Good(t *testing.T) { + // Build a synthetic variant vector with one extra_alias_entry_old element. + // Format: count(1) + tag(20) + alias(string) + address(64 bytes) + + // text_comment(string) + sign(vector of 64-byte sigs). + var raw []byte + raw = append(raw, EncodeVarint(1)...) // 1 element + raw = append(raw, tagExtraAliasEntryOld) + + // m_alias: "test.lthn" + alias := []byte("test.lthn") + raw = append(raw, EncodeVarint(uint64(len(alias)))...) + raw = append(raw, alias...) + + // m_address: spend_key(32) + view_key(32) = 64 bytes + addr := make([]byte, 64) + for i := range addr { + addr[i] = byte(i) + } + raw = append(raw, addr...) + + // m_text_comment: "hello" + comment := []byte("hello") + raw = append(raw, EncodeVarint(uint64(len(comment)))...) + raw = append(raw, comment...) + + // m_sign: 1 signature (generic_schnorr_sig_s = 64 bytes) + raw = append(raw, EncodeVarint(1)...) // 1 signature + sig := make([]byte, 64) + for i := range sig { + sig[i] = byte(0xAA) + } + raw = append(raw, sig...) + + // Decode and round-trip. + dec := NewDecoder(bytes.NewReader(raw)) + decoded := decodeRawVariantVector(dec) + if dec.Err() != nil { + t.Fatalf("decode failed: %v", dec.Err()) + } + if !bytes.Equal(decoded, raw) { + t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw)) + } +} + +// TestExtraAliasEntryRoundTrip_Good verifies that a variant vector +// containing an extra_alias_entry (tag 33) round-trips through +// decodeRawVariantVector without error. +func TestExtraAliasEntryRoundTrip_Good(t *testing.T) { + // Build a synthetic variant vector with one extra_alias_entry element. + // Format: count(1) + tag(33) + alias(string) + address(tx_payer format) + + // text_comment(string) + sign(vector) + view_key(optional). + var raw []byte + raw = append(raw, EncodeVarint(1)...) // 1 element + raw = append(raw, tagExtraAliasEntry) + + // m_alias: "myalias" + alias := []byte("myalias") + raw = append(raw, EncodeVarint(uint64(len(alias)))...) + raw = append(raw, alias...) + + // m_address: tx_payer format = spend_key(32) + view_key(32) + optional marker + addr := make([]byte, 64) + for i := range addr { + addr[i] = byte(i + 10) + } + raw = append(raw, addr...) + // is_auditable optional marker: 0 = not present + raw = append(raw, 0x00) + + // m_text_comment: empty + raw = append(raw, EncodeVarint(0)...) + + // m_sign: 0 signatures + raw = append(raw, EncodeVarint(0)...) + + // m_view_key: optional, present (marker=1 + 32 bytes) + raw = append(raw, 0x01) + viewKey := make([]byte, 32) + for i := range viewKey { + viewKey[i] = byte(0xBB) + } + raw = append(raw, viewKey...) + + // Decode and round-trip. + dec := NewDecoder(bytes.NewReader(raw)) + decoded := decodeRawVariantVector(dec) + if dec.Err() != nil { + t.Fatalf("decode failed: %v", dec.Err()) + } + if !bytes.Equal(decoded, raw) { + t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw)) + } +} + +// TestExtraAliasEntryNoViewKey_Good verifies extra_alias_entry with +// the optional view_key marker set to 0 (not present). +func TestExtraAliasEntryNoViewKey_Good(t *testing.T) { + var raw []byte + raw = append(raw, EncodeVarint(1)...) // 1 element + raw = append(raw, tagExtraAliasEntry) + + // m_alias: "short" + alias := []byte("short") + raw = append(raw, EncodeVarint(uint64(len(alias)))...) + raw = append(raw, alias...) + + // m_address: keys + no auditable flag + raw = append(raw, make([]byte, 64)...) + raw = append(raw, 0x00) // not auditable + + // m_text_comment: empty + raw = append(raw, EncodeVarint(0)...) + + // m_sign: 0 signatures + raw = append(raw, EncodeVarint(0)...) + + // m_view_key: not present (marker=0) + raw = append(raw, 0x00) + + dec := NewDecoder(bytes.NewReader(raw)) + decoded := decodeRawVariantVector(dec) + if dec.Err() != nil { + t.Fatalf("decode failed: %v", dec.Err()) + } + if !bytes.Equal(decoded, raw) { + t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw)) + } +}