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