feat(wire): add alias entry readers + AX usage-example comments
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run

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 <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-05 08:46:54 +01:00
parent caf83faf39
commit 76488e0beb
12 changed files with 285 additions and 2 deletions

View file

@ -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 {

View file

@ -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 {

View file

@ -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()

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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)

View file

@ -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<generic_schnorr_sig_s> (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<generic_schnorr_sig_s> (each is 2 scalars = 64 bytes)
v := readVariantVectorFixed(dec, 64)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
// m_view_key: optional<crypto::secret_key> — 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 {

View file

@ -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))
}
}