From a803fd1794c063edb30346b2280d68649723f389 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 00:06:10 +0000 Subject: [PATCH] feat(consensus): V2 Zarcanum signature and proof verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add full V2 transaction verification pipeline: parse SignaturesRaw variant vector into structured ZC signature data, verify CLSAG GGX ring signatures per ZC input, verify BPP range proofs, and verify BGE asset surjection proofs with correct ring construction (mul8 point arithmetic). Fix three wire format bugs that caused V2 parsing failures: - RefTypeGlobalIndex (tag 0x1A) uses 8-byte LE, not varint - Raw uint64_t variant (tagUint64) uses 8-byte LE, not varint - zarcanum_tx_data_v1 fee uses FIELD() → 8-byte LE, not VARINT_FIELD() Add cn_point_sub to C++ bridge and Go wrapper for BGE ring construction. Add GetZCRingOutputs to chain for fetching ZC ring member data. Co-Authored-By: Charon --- chain/ring.go | 38 ++++ chain/sync.go | 2 +- consensus/v2sig.go | 338 ++++++++++++++++++++++++++++ consensus/v2sig_test.go | 165 ++++++++++++++ consensus/verify.go | 206 ++++++++++++++++- consensus/verify_crypto_test.go | 4 +- consensus/verify_test.go | 4 +- crypto/bridge.cpp | 13 ++ crypto/bridge.h | 3 + crypto/clsag.go | 14 ++ testdata/README | 24 ++ testdata/v2_spending_tx_mixin0.hex | 1 + testdata/v2_spending_tx_mixin10.hex | 1 + wire/transaction.go | 18 +- 14 files changed, 811 insertions(+), 20 deletions(-) create mode 100644 consensus/v2sig.go create mode 100644 consensus/v2sig_test.go create mode 100644 testdata/README create mode 100644 testdata/v2_spending_tx_mixin0.hex create mode 100644 testdata/v2_spending_tx_mixin10.hex diff --git a/chain/ring.go b/chain/ring.go index 13330ae..975e1a0 100644 --- a/chain/ring.go +++ b/chain/ring.go @@ -8,6 +8,7 @@ package chain import ( "fmt" + "forge.lthn.ai/core/go-blockchain/consensus" "forge.lthn.ai/core/go-blockchain/types" ) @@ -41,3 +42,40 @@ func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicK } return pubs, nil } + +// GetZCRingOutputs fetches ZC ring members (stealth address, amount commitment, +// blinded asset ID) for the given global output indices. This implements the +// consensus.ZCRingOutputsFn signature for post-HF4 CLSAG GGX verification. +// +// ZC outputs are indexed at amount=0 (confidential amounts). +func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, error) { + members := make([]consensus.ZCRingMember, len(offsets)) + for i, gidx := range offsets { + txHash, outNo, err := c.GetOutput(0, gidx) + if err != nil { + return nil, fmt.Errorf("ZC ring output %d (gidx=%d): %w", i, gidx, err) + } + + tx, _, err := c.GetTransaction(txHash) + if err != nil { + return nil, fmt.Errorf("ZC ring output %d: tx %s: %w", i, txHash, err) + } + + if int(outNo) >= len(tx.Vout) { + return nil, fmt.Errorf("ZC ring output %d: tx %s has %d outputs, want index %d", + i, txHash, len(tx.Vout), outNo) + } + + switch out := tx.Vout[outNo].(type) { + case types.TxOutputZarcanum: + members[i] = consensus.ZCRingMember{ + StealthAddress: [32]byte(out.StealthAddress), + AmountCommitment: [32]byte(out.AmountCommitment), + BlindedAssetID: [32]byte(out.BlindedAssetID), + } + default: + return nil, fmt.Errorf("ZC ring output %d: expected TxOutputZarcanum, got %T", i, out) + } + } + return members, nil +} diff --git a/chain/sync.go b/chain/sync.go index dd02d8e..822fee4 100644 --- a/chain/sync.go +++ b/chain/sync.go @@ -227,7 +227,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte, // Optionally verify signatures using the chain's output index. if opts.VerifySignatures { - if err := consensus.VerifyTransactionSignatures(&tx, opts.Forks, height, c.GetRingOutputs); 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) } } diff --git a/consensus/v2sig.go b/consensus/v2sig.go new file mode 100644 index 0000000..46fae9c --- /dev/null +++ b/consensus/v2sig.go @@ -0,0 +1,338 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package consensus + +import ( + "bytes" + "fmt" + + "forge.lthn.ai/core/go-blockchain/types" + "forge.lthn.ai/core/go-blockchain/wire" +) + +// zcSigData holds the parsed components of a ZC_sig variant element +// needed for CLSAG GGX verification. +type zcSigData struct { + pseudoOutCommitment [32]byte // premultiplied by 1/8 on chain + pseudoOutAssetID [32]byte // premultiplied by 1/8 on chain + clsagFlatSig []byte // flat: c(32) | r_g[N*32] | r_x[N*32] | K1(32) | K2(32) + ringSize int +} + +// v2SigEntry is one parsed entry from the V2 signature variant vector. +type v2SigEntry struct { + tag uint8 + zcSig *zcSigData // non-nil when tag == SigTypeZC +} + +// parseV2Signatures parses the SignaturesRaw variant vector into a slice +// of v2SigEntry. The order matches the transaction inputs. +func parseV2Signatures(raw []byte) ([]v2SigEntry, error) { + if len(raw) == 0 { + return nil, nil + } + + dec := wire.NewDecoder(bytes.NewReader(raw)) + count := dec.ReadVarint() + if dec.Err() != nil { + return nil, fmt.Errorf("read sig count: %w", dec.Err()) + } + + entries := make([]v2SigEntry, 0, count) + for i := uint64(0); i < count; i++ { + tag := dec.ReadUint8() + if dec.Err() != nil { + return nil, fmt.Errorf("read sig tag %d: %w", i, dec.Err()) + } + + entry := v2SigEntry{tag: tag} + + switch tag { + case types.SigTypeZC: + zc, err := parseZCSig(dec) + if err != nil { + return nil, fmt.Errorf("parse ZC_sig %d: %w", i, err) + } + entry.zcSig = zc + + case types.SigTypeVoid: + // Empty struct — nothing to read. + + case types.SigTypeNLSAG: + // Skip: varint(count) + count * 64-byte signatures. + n := dec.ReadVarint() + if n > 0 && dec.Err() == nil { + _ = dec.ReadBytes(int(n) * 64) + } + + case types.SigTypeZarcanum: + // Skip: 10 scalars + bppe + public_key + CLSAG_GGXXG. + // Use skipZarcanumSig to advance past the data. + skipZarcanumSig(dec) + + default: + return nil, fmt.Errorf("unsupported sig tag 0x%02x", tag) + } + + if dec.Err() != nil { + return nil, fmt.Errorf("parse sig %d (tag 0x%02x): %w", i, tag, dec.Err()) + } + entries = append(entries, entry) + } + + return entries, nil +} + +// parseZCSig parses a ZC_sig element (after the tag byte) from the decoder. +// Wire: pseudo_out_amount_commitment(32) + pseudo_out_blinded_asset_id(32) + CLSAG_GGX_serialized. +func parseZCSig(dec *wire.Decoder) (*zcSigData, error) { + var zc zcSigData + + dec.ReadBlob32(&zc.pseudoOutCommitment) + dec.ReadBlob32(&zc.pseudoOutAssetID) + if dec.Err() != nil { + return nil, dec.Err() + } + + // CLSAG_GGX_serialized wire format: + // c(32) + varint(N) + r_g[N*32] + varint(N) + r_x[N*32] + K1(32) + K2(32) + // + // C bridge expects flat: + // c(32) | r_g[N*32] | r_x[N*32] | K1(32) | K2(32) + + var c [32]byte + dec.ReadBlob32(&c) + + rgCount := dec.ReadVarint() + rgBytes := dec.ReadBytes(int(rgCount) * 32) + + rxCount := dec.ReadVarint() + rxBytes := dec.ReadBytes(int(rxCount) * 32) + + if dec.Err() != nil { + return nil, dec.Err() + } + + if rgCount != rxCount { + return nil, fmt.Errorf("CLSAG r_g count %d != r_x count %d", rgCount, rxCount) + } + zc.ringSize = int(rgCount) + + var k1, k2 [32]byte + dec.ReadBlob32(&k1) + dec.ReadBlob32(&k2) + if dec.Err() != nil { + return nil, dec.Err() + } + + // Build flat sig for C bridge. + flat := make([]byte, 0, 32+len(rgBytes)+len(rxBytes)+64) + flat = append(flat, c[:]...) + flat = append(flat, rgBytes...) + flat = append(flat, rxBytes...) + flat = append(flat, k1[:]...) + flat = append(flat, k2[:]...) + zc.clsagFlatSig = flat + + return &zc, nil +} + +// skipZarcanumSig advances the decoder past a zarcanum_sig element. +// Wire: 10 scalars + bppe_serialized + public_key(32) + CLSAG_GGXXG. +func skipZarcanumSig(dec *wire.Decoder) { + // 10 fixed scalars/points (320 bytes). + _ = dec.ReadBytes(10 * 32) + + // bppe_serialized: vec(L) + vec(R) + 7 scalars (224 bytes). + skipVecOfPoints(dec) // L + skipVecOfPoints(dec) // R + _ = dec.ReadBytes(7 * 32) + + // pseudo_out_amount_commitment (32 bytes). + _ = dec.ReadBytes(32) + + // CLSAG_GGXXG: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32) + K3(32) + K4(32). + _ = dec.ReadBytes(32) // c + skipVecOfPoints(dec) // r_g + skipVecOfPoints(dec) // r_x + _ = dec.ReadBytes(128) // K1+K2+K3+K4 +} + +// skipVecOfPoints advances the decoder past a varint(count) + count*32 vector. +func skipVecOfPoints(dec *wire.Decoder) { + n := dec.ReadVarint() + if n > 0 && dec.Err() == nil { + _ = dec.ReadBytes(int(n) * 32) + } +} + +// v2ProofData holds parsed proof components from the Proofs raw bytes. +type v2ProofData struct { + // bgeProofs contains one BGE proof blob per output (wire-serialised). + // Each blob can be passed directly to crypto.VerifyBGE. + bgeProofs [][]byte + + // bppProofBytes is the bpp_signature blob (wire-serialised). + // Can be passed directly to crypto.VerifyBPP. + bppProofBytes []byte + + // bppCommitments are the amount_commitments_for_rp_aggregation (E'_j). + // Premultiplied by 1/8 as stored on chain. + bppCommitments [][32]byte + + // balanceProof is the generic_double_schnorr_sig (96 bytes: c, y0, y1). + balanceProof []byte +} + +// parseV2Proofs parses the Proofs raw variant vector. +func parseV2Proofs(raw []byte) (*v2ProofData, error) { + if len(raw) == 0 { + return &v2ProofData{}, nil + } + + dec := wire.NewDecoder(bytes.NewReader(raw)) + count := dec.ReadVarint() + if dec.Err() != nil { + return nil, fmt.Errorf("read proof count: %w", dec.Err()) + } + + var data v2ProofData + for i := uint64(0); i < count; i++ { + tag := dec.ReadUint8() + if dec.Err() != nil { + return nil, fmt.Errorf("read proof tag %d: %w", i, dec.Err()) + } + + switch tag { + case 46: // zc_asset_surjection_proof: varint(nBGE) + nBGE * BGE_proof + nBGE := dec.ReadVarint() + if dec.Err() != nil { + return nil, fmt.Errorf("parse BGE count: %w", dec.Err()) + } + data.bgeProofs = make([][]byte, nBGE) + for j := uint64(0); j < nBGE; j++ { + data.bgeProofs[j] = readBGEProofBytes(dec) + if dec.Err() != nil { + return nil, fmt.Errorf("parse BGE proof %d: %w", j, dec.Err()) + } + } + + case 47: // zc_outs_range_proof: bpp_serialized + aggregation_proof + data.bppProofBytes = readBPPBytes(dec) + if dec.Err() != nil { + return nil, fmt.Errorf("parse BPP proof: %w", dec.Err()) + } + data.bppCommitments = readAggregationCommitments(dec) + if dec.Err() != nil { + return nil, fmt.Errorf("parse aggregation proof: %w", dec.Err()) + } + + case 48: // zc_balance_proof: 96 bytes (c, y0, y1) + data.balanceProof = dec.ReadBytes(96) + if dec.Err() != nil { + return nil, fmt.Errorf("parse balance proof: %w", dec.Err()) + } + + default: + return nil, fmt.Errorf("unsupported proof tag 0x%02x", tag) + } + } + + return &data, nil +} + +// readBGEProofBytes reads a BGE_proof_s and returns the raw wire bytes. +// Wire: A(32) + B(32) + vec(Pk) + vec(f) + y(32) + z(32). +func readBGEProofBytes(dec *wire.Decoder) []byte { + var raw []byte + + // A + B + ab := dec.ReadBytes(64) + raw = append(raw, ab...) + + // Pk vector + pkCount := dec.ReadVarint() + if dec.Err() != nil { + return nil + } + raw = append(raw, wire.EncodeVarint(pkCount)...) + if pkCount > 0 { + pkData := dec.ReadBytes(int(pkCount) * 32) + raw = append(raw, pkData...) + } + + // f vector + fCount := dec.ReadVarint() + if dec.Err() != nil { + return nil + } + raw = append(raw, wire.EncodeVarint(fCount)...) + if fCount > 0 { + fData := dec.ReadBytes(int(fCount) * 32) + raw = append(raw, fData...) + } + + // y + z + yz := dec.ReadBytes(64) + raw = append(raw, yz...) + + return raw +} + +// readBPPBytes reads a bpp_signature_serialized and returns the raw wire bytes. +// Wire: vec(L) + vec(R) + A0(32) + A(32) + B(32) + r(32) + s(32) + delta(32). +func readBPPBytes(dec *wire.Decoder) []byte { + var raw []byte + + // L vector + lCount := dec.ReadVarint() + if dec.Err() != nil { + return nil + } + raw = append(raw, wire.EncodeVarint(lCount)...) + if lCount > 0 { + raw = append(raw, dec.ReadBytes(int(lCount)*32)...) + } + + // R vector + rCount := dec.ReadVarint() + if dec.Err() != nil { + return nil + } + raw = append(raw, wire.EncodeVarint(rCount)...) + if rCount > 0 { + raw = append(raw, dec.ReadBytes(int(rCount)*32)...) + } + + // 6 fixed scalars + raw = append(raw, dec.ReadBytes(6*32)...) + + return raw +} + +// readAggregationCommitments reads the aggregation proof and extracts +// the amount_commitments_for_rp_aggregation (the first vector). +// Wire: vec(commitments) + vec(y0s) + vec(y1s) + c(32). +func readAggregationCommitments(dec *wire.Decoder) [][32]byte { + // Read commitments vector. + nCommit := dec.ReadVarint() + if dec.Err() != nil { + return nil + } + commits := make([][32]byte, nCommit) + for i := uint64(0); i < nCommit; i++ { + dec.ReadBlob32(&commits[i]) + } + + // Skip y0s vector. + skipVecOfPoints(dec) + // Skip y1s vector. + skipVecOfPoints(dec) + // Skip c scalar. + _ = dec.ReadBytes(32) + + return commits +} diff --git a/consensus/v2sig_test.go b/consensus/v2sig_test.go new file mode 100644 index 0000000..78f066c --- /dev/null +++ b/consensus/v2sig_test.go @@ -0,0 +1,165 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package consensus + +import ( + "bytes" + "encoding/hex" + "os" + "testing" + + "forge.lthn.ai/core/go-blockchain/config" + "forge.lthn.ai/core/go-blockchain/types" + "forge.lthn.ai/core/go-blockchain/wire" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// loadTestTx loads and decodes a hex-encoded transaction from testdata. +func loadTestTx(t *testing.T, filename string) *types.Transaction { + t.Helper() + hexData, err := os.ReadFile(filename) + require.NoError(t, err, "read %s", filename) + + blob, err := hex.DecodeString(string(bytes.TrimSpace(hexData))) + require.NoError(t, err, "decode hex") + + dec := wire.NewDecoder(bytes.NewReader(blob)) + tx := wire.DecodeTransaction(dec) + require.NoError(t, dec.Err(), "decode transaction") + return &tx +} + +func TestParseV2Signatures_Mixin0(t *testing.T) { + tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex") + require.Equal(t, uint64(3), tx.Version, "expected v3 transaction") + + // Should have 1 ZC input. + require.Len(t, tx.Vin, 1) + _, ok := tx.Vin[0].(types.TxInputZC) + require.True(t, ok, "expected TxInputZC") + + // Parse signatures. + entries, err := parseV2Signatures(tx.SignaturesRaw) + require.NoError(t, err) + require.Len(t, entries, 1, "should have 1 signature entry") + + // Should be a ZC_sig. + assert.Equal(t, types.SigTypeZC, entries[0].tag) + require.NotNil(t, entries[0].zcSig) + + zc := entries[0].zcSig + + // Ring size should be 16 (mixin 0 + ZC default = 16). + assert.Equal(t, 16, zc.ringSize, "expected ring size 16") + + // Flat sig size: c(32) + r_g[16*32] + r_x[16*32] + K1(32) + K2(32) = 1120. + expectedSigSize := 32 + 16*32 + 16*32 + 64 + assert.Equal(t, expectedSigSize, len(zc.clsagFlatSig)) + + // Pseudo-out commitment and asset ID should be non-zero. + assert.NotEqual(t, [32]byte{}, zc.pseudoOutCommitment) + assert.NotEqual(t, [32]byte{}, zc.pseudoOutAssetID) +} + +func TestParseV2Signatures_Mixin10(t *testing.T) { + tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin10.hex") + require.Equal(t, uint64(3), tx.Version) + + // Mixin10 tx has 2 ZC inputs. + require.Len(t, tx.Vin, 2) + + entries, err := parseV2Signatures(tx.SignaturesRaw) + require.NoError(t, err) + require.Len(t, entries, 2, "should have 2 signature entries (one per input)") + + for i, entry := range entries { + assert.Equal(t, types.SigTypeZC, entry.tag, "entry %d", i) + require.NotNil(t, entry.zcSig, "entry %d", i) + + zc := entry.zcSig + + // Both inputs use ring size 16 (ZC default). + assert.Equal(t, 16, zc.ringSize, "entry %d: expected ring size 16", i) + + // Flat sig size: c(32) + r_g[16*32] + r_x[16*32] + K1(32) + K2(32) = 1120. + expectedSigSize := 32 + 16*32 + 16*32 + 64 + assert.Equal(t, expectedSigSize, len(zc.clsagFlatSig), "entry %d", i) + } +} + +func TestParseV2Proofs_Mixin0(t *testing.T) { + tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex") + + proofs, err := parseV2Proofs(tx.Proofs) + require.NoError(t, err) + + // Should have BGE proofs (one per output). + assert.Len(t, proofs.bgeProofs, 2, "expected 2 BGE proofs (one per output)") + for i, p := range proofs.bgeProofs { + assert.True(t, len(p) > 0, "BGE proof %d should be non-empty", i) + } + + // Should have BPP range proof. + assert.True(t, len(proofs.bppProofBytes) > 0, "BPP proof should be non-empty") + + // Should have BPP commitments (one per output). + assert.Len(t, proofs.bppCommitments, 2, "expected 2 BPP commitments") + for i, c := range proofs.bppCommitments { + assert.NotEqual(t, [32]byte{}, c, "BPP commitment %d should be non-zero", i) + } + + // Should have balance proof (96 bytes). + assert.Len(t, proofs.balanceProof, 96, "balance proof should be 96 bytes") +} + +func TestParseV2Proofs_Mixin10(t *testing.T) { + tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin10.hex") + + proofs, err := parseV2Proofs(tx.Proofs) + require.NoError(t, err) + + assert.Len(t, proofs.bgeProofs, 2) + assert.True(t, len(proofs.bppProofBytes) > 0) + assert.Len(t, proofs.bppCommitments, 2) + assert.Len(t, proofs.balanceProof, 96) +} + +func TestVerifyV2Signatures_StructuralOnly(t *testing.T) { + // Test structural validation (no ring output function). + tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex") + + // With nil getZCRingOutputs, should pass structural checks. + err := VerifyTransactionSignatures(tx, config.TestnetForks, 2823, nil, nil) + require.NoError(t, err) +} + +func TestVerifyV2Signatures_BadSigCount(t *testing.T) { + tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex") + + // Corrupt SignaturesRaw to have wrong count. + tx.SignaturesRaw = wire.EncodeVarint(5) // 5 sigs but only 1 input + + err := verifyV2Signatures(tx, nil) + assert.Error(t, err, "should fail with mismatched sig count") +} + +func TestVerifyV2Signatures_TxHash(t *testing.T) { + // Verify the known tx hash matches. + tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex") + txHash := wire.TransactionHash(tx) + + expectedHash := "89c8839e3c6be3bb3616a5c2e7028fd8f33992e4f9ff218f8224825702865b8b" + assert.Equal(t, expectedHash, hex.EncodeToString(txHash[:])) +} + +func TestVerifyV2Signatures_TxHashMixin10(t *testing.T) { + tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin10.hex") + txHash := wire.TransactionHash(tx) + + expectedHash := "87fbc60cde013579e1ad6ab403dee81c4da7a6b4621bea44f6973568c37b0af6" + assert.Equal(t, expectedHash, hex.EncodeToString(txHash[:])) +} diff --git a/consensus/verify.go b/consensus/verify.go index 1e96075..1534c6a 100644 --- a/consensus/verify.go +++ b/consensus/verify.go @@ -18,14 +18,29 @@ import ( // and offsets. Used to decouple consensus/ from chain storage. type RingOutputsFn func(amount uint64, offsets []uint64) ([]types.PublicKey, error) +// ZCRingMember holds the three public keys per ring entry needed for +// CLSAG GGX verification (HF4+). All fields are premultiplied by 1/8 +// as stored on chain. +type ZCRingMember struct { + StealthAddress [32]byte + AmountCommitment [32]byte + BlindedAssetID [32]byte +} + +// ZCRingOutputsFn fetches ZC ring members for the given global output indices. +// Used for post-HF4 CLSAG GGX signature verification. +type ZCRingOutputsFn func(offsets []uint64) ([]ZCRingMember, error) + // VerifyTransactionSignatures verifies all ring signatures in a transaction. // For coinbase transactions, this is a no-op (no signatures). // For pre-HF4 transactions, NLSAG ring signatures are verified. // For post-HF4, CLSAG signatures and proofs are verified. // -// getRingOutputs may be nil for coinbase-only checks. +// getRingOutputs is used for pre-HF4 (V1) signature verification. +// getZCRingOutputs is used for post-HF4 (V2) CLSAG GGX verification. +// Either may be nil for structural-only checks. func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork, - height uint64, getRingOutputs RingOutputsFn) error { + height uint64, getRingOutputs RingOutputsFn, getZCRingOutputs ZCRingOutputsFn) error { // Coinbase: no signatures. if isCoinbase(tx) { @@ -38,7 +53,7 @@ func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork, return verifyV1Signatures(tx, getRingOutputs) } - return verifyV2Signatures(tx, getRingOutputs) + return verifyV2Signatures(tx, getZCRingOutputs) } // verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions. @@ -111,7 +126,188 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err } // verifyV2Signatures checks CLSAG signatures and proofs for post-HF4 transactions. -func verifyV2Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) error { - // TODO: Wire up CLSAG verification and proof checks. +func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn) error { + // Parse the signature variant vector. + sigEntries, err := parseV2Signatures(tx.SignaturesRaw) + if err != nil { + return fmt.Errorf("consensus: %w", err) + } + + // Match signatures to inputs: each input must have a corresponding signature. + if len(sigEntries) != len(tx.Vin) { + return fmt.Errorf("consensus: V2 signature count %d != input count %d", + len(sigEntries), len(tx.Vin)) + } + + // Validate that ZC inputs have ZC_sig and vice versa. + for i, vin := range tx.Vin { + switch vin.(type) { + case types.TxInputZC: + if sigEntries[i].tag != types.SigTypeZC { + return fmt.Errorf("consensus: input %d is ZC but signature tag is 0x%02x", + i, sigEntries[i].tag) + } + case types.TxInputToKey: + if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid { + return fmt.Errorf("consensus: input %d is to_key but signature tag is 0x%02x", + i, sigEntries[i].tag) + } + } + } + + // Without ring output data, we can only check structural correctness. + if getZCRingOutputs == nil { + return nil + } + + // Compute tx prefix hash for CLSAG verification. + // For normal transactions (not TX_FLAG_SIGNATURE_MODE_SEPARATE), + // the hash is simply the transaction prefix hash. + prefixHash := wire.TransactionPrefixHash(tx) + + // Verify CLSAG GGX signature for each ZC input. + for i, vin := range tx.Vin { + zcIn, ok := vin.(types.TxInputZC) + if !ok { + continue + } + + zc := sigEntries[i].zcSig + if zc == nil { + return fmt.Errorf("consensus: input %d: missing ZC_sig data", i) + } + + // Extract absolute global indices from key offsets. + offsets := make([]uint64, len(zcIn.KeyOffsets)) + for j, ref := range zcIn.KeyOffsets { + offsets[j] = ref.GlobalIndex + } + + ringMembers, err := getZCRingOutputs(offsets) + if err != nil { + return fmt.Errorf("consensus: failed to fetch ZC ring outputs for input %d: %w", i, err) + } + + if len(ringMembers) != zc.ringSize { + return fmt.Errorf("consensus: input %d: ring size %d from chain != %d from sig", + i, len(ringMembers), zc.ringSize) + } + + // Build flat ring: [stealth(32) | commitment(32) | blinded_asset_id(32)] per entry. + ring := make([]byte, 0, len(ringMembers)*96) + for _, m := range ringMembers { + ring = append(ring, m.StealthAddress[:]...) + ring = append(ring, m.AmountCommitment[:]...) + ring = append(ring, m.BlindedAssetID[:]...) + } + + if !crypto.VerifyCLSAGGGX( + [32]byte(prefixHash), + ring, zc.ringSize, + zc.pseudoOutCommitment, + zc.pseudoOutAssetID, + [32]byte(zcIn.KeyImage), + zc.clsagFlatSig, + ) { + return fmt.Errorf("consensus: CLSAG GGX verification failed for input %d", i) + } + } + + // Parse and verify proofs. + proofs, err := parseV2Proofs(tx.Proofs) + if err != nil { + return fmt.Errorf("consensus: %w", err) + } + + // Verify BPP range proof if present. + if len(proofs.bppProofBytes) > 0 && len(proofs.bppCommitments) > 0 { + if !crypto.VerifyBPP(proofs.bppProofBytes, proofs.bppCommitments) { + return fmt.Errorf("consensus: BPP range proof verification failed") + } + } + + // Verify BGE asset surjection proofs. + // One proof per output, with a ring of (pseudo_out_asset_id - output_asset_id) + // per ZC input. + if len(proofs.bgeProofs) > 0 { + if err := verifyBGEProofs(tx, sigEntries, proofs, prefixHash); err != nil { + return err + } + } + + // TODO: Verify balance proof (generic_double_schnorr_sig). + // Requires computing commitment_to_zero and a new bridge function. + + return nil +} + +// verifyBGEProofs verifies the BGE asset surjection proofs. +// There is one BGE proof per Zarcanum output. For each output j, the proof +// demonstrates that the output's blinded asset ID matches one of the +// pseudo-out asset IDs from the ZC inputs. +// +// The BGE ring for output j has one entry per ZC input i: +// +// ring[i] = mul8(pseudo_out_blinded_asset_id_i) - mul8(output_blinded_asset_id_j) +// +// The context hash is the transaction prefix hash (tx_id). +func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry, + proofs *v2ProofData, prefixHash types.Hash) error { + + // Collect Zarcanum output blinded asset IDs. + var outputAssetIDs [][32]byte + for _, out := range tx.Vout { + if zOut, ok := out.(types.TxOutputZarcanum); ok { + outputAssetIDs = append(outputAssetIDs, [32]byte(zOut.BlindedAssetID)) + } + } + + if len(proofs.bgeProofs) != len(outputAssetIDs) { + return fmt.Errorf("consensus: BGE proof count %d != Zarcanum output count %d", + len(proofs.bgeProofs), len(outputAssetIDs)) + } + + // Collect pseudo-out asset IDs from ZC signatures and expand to full points. + var pseudoOutAssetIDs [][32]byte + for _, entry := range sigEntries { + if entry.tag == types.SigTypeZC && entry.zcSig != nil { + pseudoOutAssetIDs = append(pseudoOutAssetIDs, entry.zcSig.pseudoOutAssetID) + } + } + + // mul8 all pseudo-out asset IDs (stored premultiplied by 1/8 on chain). + mul8PseudoOuts := make([][32]byte, len(pseudoOutAssetIDs)) + for i, p := range pseudoOutAssetIDs { + full, err := crypto.PointMul8(p) + if err != nil { + return fmt.Errorf("consensus: mul8 pseudo-out asset ID %d: %w", i, err) + } + mul8PseudoOuts[i] = full + } + + // For each output, build the BGE ring and verify. + context := [32]byte(prefixHash) + for j, outAssetID := range outputAssetIDs { + // mul8 the output's blinded asset ID. + mul8Out, err := crypto.PointMul8(outAssetID) + if err != nil { + return fmt.Errorf("consensus: mul8 output asset ID %d: %w", j, err) + } + + // ring[i] = mul8(pseudo_out_i) - mul8(output_j) + ring := make([][32]byte, len(mul8PseudoOuts)) + for i, mul8Pseudo := range mul8PseudoOuts { + diff, err := crypto.PointSub(mul8Pseudo, mul8Out) + if err != nil { + return fmt.Errorf("consensus: BGE ring[%d][%d] sub: %w", j, i, err) + } + ring[i] = diff + } + + if !crypto.VerifyBGE(context, ring, proofs.bgeProofs[j]) { + return fmt.Errorf("consensus: BGE proof verification failed for output %d", j) + } + } + return nil } diff --git a/consensus/verify_crypto_test.go b/consensus/verify_crypto_test.go index a14eb42..ee4be9f 100644 --- a/consensus/verify_crypto_test.go +++ b/consensus/verify_crypto_test.go @@ -52,7 +52,7 @@ func TestVerifyV1Signatures_Good_MockRing(t *testing.T) { return []types.PublicKey{types.PublicKey(pub)}, nil } - err = VerifyTransactionSignatures(tx, config.MainnetForks, 100, getRing) + err = VerifyTransactionSignatures(tx, config.MainnetForks, 100, getRing, nil) require.NoError(t, err) } @@ -86,6 +86,6 @@ func TestVerifyV1Signatures_Bad_WrongSig(t *testing.T) { return []types.PublicKey{types.PublicKey(pub)}, nil } - err = VerifyTransactionSignatures(tx, config.MainnetForks, 100, getRing) + err = VerifyTransactionSignatures(tx, config.MainnetForks, 100, getRing, nil) assert.Error(t, err) } diff --git a/consensus/verify_test.go b/consensus/verify_test.go index ea4ec87..f6adab4 100644 --- a/consensus/verify_test.go +++ b/consensus/verify_test.go @@ -13,13 +13,13 @@ import ( func TestVerifyTransactionSignatures_Good_Coinbase(t *testing.T) { // Coinbase transactions have no signatures to verify. tx := validMinerTx(100) - err := VerifyTransactionSignatures(tx, config.MainnetForks, 100, nil) + err := VerifyTransactionSignatures(tx, config.MainnetForks, 100, nil, nil) require.NoError(t, err) } func TestVerifyTransactionSignatures_Bad_MissingSigs(t *testing.T) { tx := validV1Tx() tx.Signatures = nil // no signatures - err := VerifyTransactionSignatures(tx, config.MainnetForks, 100, nil) + err := VerifyTransactionSignatures(tx, config.MainnetForks, 100, nil, nil) assert.Error(t, err) } diff --git a/crypto/bridge.cpp b/crypto/bridge.cpp index e84c341..4b03416 100644 --- a/crypto/bridge.cpp +++ b/crypto/bridge.cpp @@ -330,6 +330,19 @@ int cn_point_div8(const uint8_t pk[32], uint8_t result[32]) { return 0; } +int cn_point_sub(const uint8_t a[32], const uint8_t b[32], uint8_t result[32]) { + crypto::public_key pa, pb; + memcpy(&pa, a, 32); + memcpy(&pb, b, 32); + crypto::point_t pta(pa); + crypto::point_t ptb(pb); + crypto::point_t diff = pta - ptb; + crypto::public_key dst; + diff.to_public_key(dst); + memcpy(result, &dst, 32); + return 0; +} + // ── CLSAG (HF4+) ──────────────────────────────────────── // Signature layout for GG: c(32) | r[N*32] | K1(32) diff --git a/crypto/bridge.h b/crypto/bridge.h index 2ca5290..468d088 100644 --- a/crypto/bridge.h +++ b/crypto/bridge.h @@ -57,6 +57,9 @@ int cn_point_mul8(const uint8_t pk[32], uint8_t result[32]); // Premultiply by 1/8 (cofactor inverse). Stored form on-chain. int cn_point_div8(const uint8_t pk[32], uint8_t result[32]); +// Subtract two curve points: result = a - b. +int cn_point_sub(const uint8_t a[32], const uint8_t b[32], uint8_t result[32]); + // ── CLSAG Verification (HF4+) ──────────────────────────── // Ring entries are flat arrays of 32-byte public keys per entry: // GG: [stealth_addr(32) | amount_commitment(32)] per entry = 64 bytes diff --git a/crypto/clsag.go b/crypto/clsag.go index bb174e8..d38b2b0 100644 --- a/crypto/clsag.go +++ b/crypto/clsag.go @@ -39,6 +39,20 @@ func PointDiv8(pk [32]byte) ([32]byte, error) { return result, nil } +// PointSub computes a - b on the Ed25519 curve. +func PointSub(a, b [32]byte) ([32]byte, error) { + var result [32]byte + rc := C.cn_point_sub( + (*C.uint8_t)(unsafe.Pointer(&a[0])), + (*C.uint8_t)(unsafe.Pointer(&b[0])), + (*C.uint8_t)(unsafe.Pointer(&result[0])), + ) + if rc != 0 { + return result, fmt.Errorf("crypto: point_sub failed") + } + return result, nil +} + // CLSAGGGSigSize returns the byte size of a CLSAG_GG signature for a given ring size. func CLSAGGGSigSize(ringSize int) int { return int(C.cn_clsag_gg_sig_size(C.size_t(ringSize))) diff --git a/testdata/README b/testdata/README new file mode 100644 index 0000000..22dbf18 --- /dev/null +++ b/testdata/README @@ -0,0 +1,24 @@ +Testnet V2 Spending Transaction Test Vectors +============================================= + +Generated on testnet at height ~2880. + +v2_spending_tx_mixin0.hex: + TX hash: 89c8839e3c6be3bb3616a5c2e7028fd8f33992e4f9ff218f8224825702865b8b + Block: 2823 + Version: 3 + Ring size: 16 (mixin 0 + ZC default) + Inputs: 1 txin_zc_input + Outputs: 2 tx_out_zarcanum + Signatures: CLSAG GGX + Proofs: BGE (2), BPP (1 with aggregation), ZC balance + +v2_spending_tx_mixin10.hex: + TX hash: 87fbc60cde013579e1ad6ab403dee81c4da7a6b4621bea44f6973568c37b0af6 + Block: ~2878 + Version: 3 + Ring size: 26 (mixin 10 + ZC default) + Inputs: 1 txin_zc_input + Outputs: 2 tx_out_zarcanum + Signatures: CLSAG GGX + Proofs: BGE (2), BPP (1 with aggregation), ZC balance diff --git a/testdata/v2_spending_tx_mixin0.hex b/testdata/v2_spending_tx_mixin0.hex new file mode 100644 index 0000000..215eb43 --- /dev/null +++ b/testdata/v2_spending_tx_mixin0.hex @@ -0,0 +1 @@ +030125101a01000000000000001acf090000000000001ac3020000000000001a44010000000000001a20020000000000001ae2000000000000001a74000000000000001a02000000000000001a48000000000000001a24000000000000001ab3000000000000001a2d000000000000001ae0000000000000001ab2000000000000001a6c000000000000001a8200000000000000aa39d0ede3e4721f05a54ce47919ef6ae1cf37a84aecd9b773feedb23b9a0422000516f9d8895614117e61b4a4f6366e058d6fa920c50390d31f7c5c9f4a4550e38e641700000b0222880b029eda2700e40b5402000000022660808c0edc6cf11fb51df6861b02b72ed708fe386adb83a5b7b656cd7a70e74aaebc0715019d359ba9cb58b60be4d2e18884c91af2dbdb63c55864e9253610c74b0d1a47697c2efb614f36dd1b224f782fbb428585f3829f139e336fe71f55c274c32d3eaafafc623bf483e858d42e8bf4ec7df064ada2e34934469cff6b626838e2ae4b0661e9660026d7094ba2c926c23c9c9cf98947c7d2c711b4ad811dea0c86a2cadbc32eab366c634a42449604dba96520470239fcf0492b6d471f136b691b7e88d3326f054e09489412048894af3b8d3efc74660b6dfb509781dc960e6865da1e62223a67fe4574c32d3eaafafc623bf483e858d42e8bf4ec7df064ada2e34934469cff6b62684d999a3b2a4e8d5f000500012b231c7700001cbbe9b248734d3c9e8f4af220bf2250e1a0a68e677a793fe708fe886da8bfaed1631797600d31d29d18826d0f44b7e5b37f7fc918cb423915db5da13a60777d43adce8ce32aa4c94708863b9af8ae2019bf177af1136097286c001037adf4b70d05b11e4bf38ad16e94ac1a1677e46807492a846895d09fca35dd0f7f83e651c5ccf9646eb733155c4b50979615b68a4b3e52da99df40c766059806ba7399c1aab2ef0c9ea0fd230976f5a98f7eec4bf1c9a1c971d59a82af19a30c59df8e56f18a075dbb8255004f443aac72738d6b8f0930646c4b16edf76976027363c9fea96b8e5ae9e1508657dbdfcb4bb9417e492893ff2124edea2dd18c0311ef730ed33ae54dd821d8634f84789f0bd27060cd1af7573930328f2382c70a0a9aa4d43e42a0404f80f99576bcdcce0a5742cab74544630b2ac6b30e1eac0e41ba49ced25b1d5f5b62e06e5394f517a2264ec783bc199ceab3cd5c98911108f92a0890a408c43db14d51f5d0e89272b8856d4d17542ba2a17d81f3a418f90d175d567c91f2d2fcab13a799bcdd0bf0ff69e2134a8311c7e7a8d90b8b57950d7be94c67659fb61eeabb1acd870480a41931875ffad43299cc5f567c5d7ae6062fd484052fe1c9f956917e0c5dfaab3c5c02942fb303e36c9f4e9b47659a130224cdcf4ad12f1910ef3c053104a438d33ad92be90aa77bb1900c3e6e50a2e30ce39df4befed03c84500bfa53847700bbfdec4adcc4d160655502a00a79dfd80f54e0928657cc147d3647082fc0e2234a33546887d79744b43be23ac218b7400445bc26f8cbdef83f5b51dee7250b5e7fdbacf7ff94c6e528ec9e2075fbbab50410279ece0a3583a6ec895809faf8b524c042b1c2f64ac2c9337b26155579348e0081981b5770263dae59143afb9209d45bebf3b6ce191c3e0aebed4f3e9cc0ac02868757a233af23e4ef5c62e4fb9cc2996b2552ab776ae0c37909851509128700d460b52e262c456d912e7fc7cd11a7551f4f7f8ee61ae8a9cddc3b814c2d5e0cd1dd2781f318c71e6424f80c8f9570ce41258ff3ffe8f7e3cb03491ce438c20adbc7473da33bd5a927f595c97ee943522899275fa8a964034e69e0e7c5b3f00c84ac680c7a81b886b1205c16f69d12a552d7282c79efd4b97091dc22295f2502497f4ea18135bda07a1df6f53b527809efeba2da7a4d8bebb4d85f26adc82b05cda1b13451c1b5ef3404493f92c29a6a7584f2520161362e037b06e938cadc0b46b164a741abd133d368ce0ab6a84332252acb2c1d291f29b0bc48b83553450837e20f063fcad2a6ccdc0cb913e0e5bbe77c231bdc53e61d501bc64a4044150da8aab857c4aaad16156002e8c482369833a3b409f22afeb7b636303c0e77a60bb615fd88374bbfa8bdc75aed0dfadc74d7217580eb8bc51c8ca58245d59a39016cea66bb5f27f7ae66e7b5f5a95053a61e72ff77bce56cc57fca99bb12f5870a0d220d684dfa07e8c920a266206f06260dc3ed54d8195b75ac573085e9638d0a45f6992d818a2deddfdc0641415f1852d70ada567034fa61adbe632ab2aaba0c971f9f7066b84c0e2f4f985f2f5f9bbf64199e0f647e4570ea60d6a8680fc0818a04a087f119ee895e8b2643b3f625ab983ac2eded89d3b808fcfff8868da0f4032e02b17be8ea0c1b33e109de1efbf1e3f77e600309f7eb9a5e0fc8c15d94dc62cd31a1849b0ea916696b69e05ffec4cb7165092ccccb043d6eb18ca7ece2fff5a5fb01b9fb37f360bde541ac167112f7b3ce23a91df5702ca75cd4c924c507305976140319b34ee0a92205e9507a3d797a9454ff2a1d4867afb7122a6695fa8de4284b0027f35eaaece35b2ac9064e7e2f3f392c05951ff455952e0c512f35cd456d2b08e2600d0471096a37d15ed923a0de5ae92e4de75887b85ef6ff395efbcb92b206ef13bb1a9e73f355be6281161f785da9855d4433723f88727fabfe1c3aece20b304bdf44f17450176e6fc57dc166cf1c6d2d6e517e6bc9580da2c3b87b725b02116c901de4ba968a4142b11bfc6f21889caafb71b08a76981e7a846010e25dfb214e0517be75a4c8407fe1d498c0d6d7a1d5e2659c060d1075d3755943d40bd9014edbc30dede4471c8078471fe6c36699396c2d507d3518ed6c2e84211cd4b7be03876b74936e5bd4d16279fd50c1d52ee57f75796651cced5dfe6fc23d0bdccd087498878a0a1069f8224d93e67fee6e1fa90ac59ef384e821eb357a7b887eac08e162c5396c54a159c84fa3a6948ad72a828ab8ed9ea2d44a26de129f5dda44074630fa57d6d0137d85ebf71cf62633204c2e35b99372b82098816596c20dab0d30a5799ba6834e6dce2fcf256bb9608e8a0a43a1a4815e11f581f276a1f11e012f075a1de906a6c92d1060f230d37ec4cfcc126527d79bde551d4aa5f7730ed903dbcf4b72ad899510c3af2a1f8ef1272b3005004c83d1a78d1490331c1003def477dfe000a1e3db05a2423e4088f908c8cd67cc4c0902ca3f72057a8684ec533bff2c53b9b52a3c0f8feabea02a2f312677b76c9bc089d51628f0a8a67580f8773122af7d76701eca0590878995051da5ecf418b67bafe1da3788b3f686eacaf1e9947a3f064e5c2bdc7768615f7f6a0ae8ecd695ab7214de82579317b2cc8a660af381928344e87372d978889cab8753ca9bd21c6ac64c518f2ab21d1887e43a2307c180bb91cbab0eca9d8915f5d24ec1171d2ec760c5979d4523810382c645192694530158f103f29b09d4ea2c2e2402a9762e09c4087b2ef7154763f1de629f2d36100693123ae53e570201e39216190d542e107d92d8b4072d8d0473b18699c91847ddb7209b398f5031ff4633ebcdee7d742202e7788e765021c96f749038d22661a305f3cc62388193657d938b2d5f3579a62d18b21d946d357a2ab2ef0922c156083bfcc9640faa2ea2e15a94139f0bf93aae0d8fe2e0aefca69bcfc3dd6e655ab8c1332c32bcdb51c615f085915845156a6ded42e6931b4546d83f098f83eb5dec408c695ca67b19b67d8f35d44e075118beb6c593b2d9b82e709a22264b7b281d2801484dc0241a0c8c4369a71b5026f06e8bca7feabfb9a60da8e094a29bdc83e2f69c596d1979312e8e93337b7cec5e849ebbe86b26568f8dee3147b2b54c48a11adcb0c3f2653c70e48ad963a1dd8210b78a43ab661afeec3ddd0500a16f77be21ad812bf180f4945026b415da02b14c57654e595188c7ecf1ba580c2ce729db2eac4e36163d897c37dae278dee23e0fc3ee0f2a0bf8a9fb2368b50902cea77b4ba6398dd27990012351dbdf56656b1c2de5dc11375037b8fa048216c88f78f4d889d96e32fa26280e0f35bbe7f64ff81d2b5e5aa676961b4bf705bd450234a0c02bc9645913f1daa4c67a3381e078d33ce29ad6c4f31c388fe9ada47106d3057b6a79bc8c1f0342aa0622135230c5f39a25e0e74495e9fe9a12e581d502023d898dbe0f3cf773b07bac7f8925779ff7141cc59ead54de95c9866f3aa3580d1b6d123170af993c9ff4f13bd40d0ace9601f75bc55c7e01f738e5515e7f2706c6636898187fe96a8960e8d9779d5c2492f0e5bfe34634a2ca74fbfb3616ac0530200816d188f9710d5bb58e4c5fdd215bc4532bea60b81e4d84906fd60e286601b6c503b4f858c76329aba30e33c35842aa39a828d672b4c22dacea3b5b8d96038aa237122b1f4cbde2c9c2da0768fc5ece932748ce9b06e39493a20e4cdc0704 \ No newline at end of file diff --git a/testdata/v2_spending_tx_mixin10.hex b/testdata/v2_spending_tx_mixin10.hex new file mode 100644 index 0000000..af330f1 --- /dev/null +++ b/testdata/v2_spending_tx_mixin10.hex @@ -0,0 +1 @@ +030225101ad6060000000000001ad1030000000000001a70000000000000001a60020000000000001a24020000000000001ad6000000000000001a3e010000000000001a92000000000000001a23000000000000001aa5010000000000001a89000000000000001a15000000000000001a2f000000000000001a55000000000000001a14000000000000001a1c0000000000000064ea0914da4b064546ce695218a67ec5e3eb2e3afb701e6b26d5350d078b01dd0025101aff0e0000000000001a23000000000000001a0e020000000000001a6e000000000000001a48010000000000001a28000000000000001ab1000000000000001a39000000000000001a32000000000000001a31000000000000001a1d000000000000001a91000000000000001a3d000000000000001a14000000000000001a19000000000000001a0f00000000000000898f64f5ce0a9a0d73675b1cf56765eabbcd828c2b5038623ca0d25270066f180005164c124727e7e46dc9230c5b9f23054992d437196ad13a0ea6b3451cc5825f390e1700000b022f320b02e5712700e40b5402000000022686c57402e7c45f811b00331a4dc1f2f49d6ac4a297da7a6ba6234027fbc2eea208d160cb32b318d7f700c78858d0014239fe4b295be4aff542dece5f5b944f1687e036878bd80a38a9eaa401a4fa04abc38eb0447c1fb99810a3c635dba5ce1174c32d3eaafafc623bf483e858d42e8bf4ec7df064ada2e34934469cff6b626812d6103fcec70b680026b03ea35496c00cfd8e5adc92dbb79d8e3f144b68e905810cdf3a752a3b7156045fea20470e92972fb4a785b30355fb725317eb13a69756e269e770b96ba1a02c4006271e3db956abd94319806186e634d95604484827855db363505904eceb3674c32d3eaafafc623bf483e858d42e8bf4ec7df064ada2e34934469cff6b62685ec593299f965c07000500022b09a82c3177ce0ae00a1a0db75ea108c601386f0d30ee132f95ef64a99241c9a0136d04692074be1cee50cd2ad2c5464113cd6330ab679a9d2a3f3b495e72809da8539ae7818c29c5c414e20422045d8aa016cc17bc8099280946dbf405c6720310f3707df706e29bb2df81c2db3076c9741f30e985a1900e7a4373accdea1a6d07aa0435b1abb7f4d6c53f961db677b357defc9dd2e30f1fd50b770273fd3711035c51a797bd0d828c89db6b59b621c8137819272d1c793f579070e3aaf6a1c20eff2fbe87d7c07878eb711e267e607a6678a31da1b356e580cc85d5aee030ac01715bc93922b4579e731d4654f831deefbf757d57b04a9cce13c9101817b4e80ed4430a5ded71b0888ec1569955270a35328fadb274b69b0ca0d673c96e80c30f96a81cdf1fa28a38a4d02fb875046dbd9b80f0cb54524e65ee6897ce8814ca0076054a08bf05431756423d4001885450717f5b4b095bc121ccd060a61cc3730c2334d224073f9dab00ebb6d169b57d488ac97917e63bac22d6f9887d1531f104822cbb087cbf142af30fd761f30c9d3985cbffacc575b93155ffc7e235c2e609e3b138d7592b1f67d1653c3c256c125137c81fe589ff3c21f51951a125cb240bca857d656b8044606d6241b57f73a0deac1d15024b353f7af3f90b70bf486c0611dae7b3902bf0771f20b4952b91c2cf0f79cf30e23ef3cea548bb5629e2440496a049b5612a9dafa510a64646e5487cd0b3e7af75d3dee1dfb0021c9b95c1087c5c3e24ee8dcfa2ac46082768ffc47dab0d2c1a7ab0ded64f9771815bf02d05fe3c382bff34a78333279a77830e0d31133e58b9eeaec397a74588bdb146830b102c928d0c8ad2604219d4d965272342b14e0d17df8e4e177d152d3b4148d6850e656e63feb47ba3f766fb512a8c0a44212f10177e77412c00b48363031c30900ac47ef085d68690440fda38c49a77b9cf032fbe2703b4dc0c836abbe77fc693062d8226da158d9b0d4f36426417247685fc520d9cfce0f9fbbd067537f8c5a20858978b089a5a2775b8ae7d4d0a54f54723d5e9f017e0511226c40ae2bcd46d0077cff9b3a23b46ded581407844551537ced9d7f6b002bfcf5ca3b75b481adb01cd9a46a5f8de792177f90fc76bf7dbdb31de2bff9c8b03d4ba6aa952f72f4906b0889dfe8e64266dfa0e7f8b466a7767fc282a9b83e27ff0ed4a679e44d2a405268b5cd0e26c384f5e99cb9f4dfd8659661a04af84fcbd8fea9b3f4f9ab9bc09f57d1a034420abf9864359e049542d8686220ea9d5efdb400a2aa1145a546509e5c4c9dd1149cc09014cdecd9c0c02769f93732ff2fc00cbef1d8779e7f9770507ecc04fac82c886b4a0f35fae0eb77b62c5adfbec4f281182ff5908c7d8320ad095a7c7b133caaabbaf0efc3c522eba2201f5e92d58e7341161b6828ba91e060a4a405186caa1ff30b1dfe3a05f1e9ca7d24ea5d6057524824edab0ac07bb01ee4ca244a4aded4f161145ed144260f7320c3eafad59d555382580f8f2860806e9a964a02e80c65c1c375ec91879645152f9297502e075e2f3e7d8ccbd31b1085a5799f3e984f0af7ddf51319a312c05c61b9d6e9c01637b433a02a6af98144217a30a75196894d491af26afaf47dcbacd831df31666d51cd75a484f226d5cac2b43d5c2c57231841b9b6ee949a6255887deaa5ad0a86f164df0ad2ba698f0f93c9615c35f943091d960f487a3a94220fa795f6fc9f006e83055dc96c37234cf351a8de5c0877340ffd48a43ad234f4d24652b678ea2c4fe87a6a1859e8af44f0210fa5198630cbb557ee38a80f72799df306995d81473684bc795ae3e022ebd170960ad5f7564166441f61c2ee12dab92bc744484d4f1f35e11130ab9613bbf240e3f2fd7680d72815b4a84cb7a1a559a21321338b6c73408d2fb7a919b885f180e6904af5bcb2c9c6c82662cffadb701f3ccb5fedd93016fad583a6eba08593005161f855f1d8a96291e71ed5a58a4e31e7bbd486aad3f66df91ca12f4168261047ecf344de862eb5e26ba35f8dae111782bb72d961c955be13e77834a3e8f8809be3b350f27cf7d27591821fe12fddb9c466c03c0d7081422913a82a92bcb8003b003db3de75c0f2769aaa5bea9cc9e341c14ba6e6603702e91bb65cfd2e2f505a3a53bb50bd5706320e4b0b588b35267c07c10a58bcc2e76f8aaf297d6b3c60e10b41d3b05c071e40a7aa74c000a8fc12d6f5fd30eb0a5fbf623ad9c9114700c6c69edf07a1b9fb09d0ac0e299b64d6feab16173ede2547c89088e44da69ad030ac233af4a07450187bd08f2ade2c249e8e0db3b929c0fc740d188315c5550001e73624a20bde32d550d78fd21cc22db072a4535b6d186e443d1cd64c8649706acc1b3ef9c76e31f0355bcb2ca17aaa574ec05e04476d5f5a1cfd99f4c4c8a0cf72e2ecd5b2a61d549ab3e6954c559594b5b0ba81810ce49041099703dd9920d7b127e7afb3151b9a6a9435ffc8bd65dc2e858c55652f977aaa75307cb56740710f7f6b3feaed74a260ccfe94ee6beed0696e35f0c82c45d869c9d7549bac5bb03adcddd94d1e2eaf4e0b49dc03afa011d6b2922b2fa5fee6db0dae44c1fa33f0b7ff26188094bcf105389c45b0c27beaa120968f993edd02a161bbe7708902c074f29f527fc6927f09b76f772fda3dcd62b18e4cb273e73b997202804783a7d03450d4c18e24cd5bcb79f33bc4667c367bec07a5552cf33155d233668133bcf05ebc5c574a829311eab45d643444edaf9ae942d7132390168e2081ab2dc66680afa09677f19d87a9cccff573f47ba3512347b23f1b2e1b8c56de787c63524160b4adb44f2fb1d7ab88194287695ea58a3b36b2ada09484e4ca3375c1256e2ab0631ad24c90a2170b02099b15d3b1425d18808fcce117e30b1e0ec6699f8bc45016a9bbe4c8bde73df4ac6537fcb9db33d9cdb8bfc10f3bd7b7e73f64e7e004602ca32d021d7f12db8c54ed638f92a1b055517e55ada29dd526e0205c7613d00036baceabf31bfa30fe7c45d4bdb03ce978485457396d2a66df91d5dc87c98ae0f36596d91e868ca0edea773e2e82f717eb57ebad5724beb0695bfa630c56cb70f573b926066ea8b70fad77d4f57334f92dd08d1e97645ff19acbf5b677427ca0c28acfc19564519d837793daf60c9adf7c6b2526b46ab4a640f49d8bfc8fca70a51cf18065262e964d7f866211901f9409469139d7b8be9a0370c39ef7a51cb0933e5e67ba58bd1b960c0c534e6a51faf8e8a78a036ae06ca2cf463c95daed366a4c1d82eadddf6e090005372c07d5d48c48047190f8bb5fbe1d54f9f136fa7f3032e02d5917bbc72f8a2fd0d3986cabad7c6ce5623782a7a6ecdec7e239bc73f9ff9f5498f6fb9bb0225a0821bea6b3bd4ecd51a7ed18c87ba7dc1689cf010367b5708019de27fe1a25c7e5560fb1d610ff64b83cec4f56f46b5ab99af1b79fa474b9b260367be1bb3f878f1b558683347d19b01b744b592cd072ecfb95ec61c6e54bd630a6ba861bd13a2ec9eff69931c409240e8a0313d3cf0412328d4861a80472e480946320b3f90a7f2d1dcbeb530b84f4d41d6e636fc3aa06fce30b075010b2d1e053fab1c5e2801890eb9218220e4540929aae80753e239338075c1584298e0bd007fd8f70469761bab3c9417633395b7be758594a89dbdfd835e1ab2c0747e220056eeaf02314fff3e8144c9bfd8291393cff7ef0c133d3bbdd2b33b976b2258c4c9307b913756276cbcbf48d70d146cb4ff2cb6299b4e231f9d8d0eb236cdd2c101522d9b61e31f7dcc61f2056cc5b23cbd5e3fca42c52a171e67dd471c0ccbc3dc0302b1df84a4c5fe494449223d1e89f078ec8de3aafd14c38f36376296cb92960aa74acd9ad796c7eca78041f4077ccc67fbfc28ff7e3871cc242a647a9ce0460ace88fc0c727d6d89658e33a7b64656971f1f10e159df87c66ff999cd45ed5b0097b2eadcab7e8e4c808af6209d6859bea9c5b530f203106d02764d2bf5ce65093f86ff7127233aa878ae127865a3f2acd1dd3aac2ef5701e4bc395570a7289082f07fcf74e6a830b96f0916cdba86579cf9a47e935dc08c91ea9a5a3def91b787b342e262fb66ee1f90d93f76b1880f6bde297d0ab939f3817deb0c8e638d2e6111a2fddad329949138929e6fca2de215f66b91c8b6d72924851a03cf7161b4dacca6b8e3585c438c299b3ee57a51b468c2cbc57154c48a6a0d4742e5099645a147a827914527ec5e3932bbd844f1c591413c8db997b63105517350f37011aa54dc06d4741a940daaa34c8e5252cf8dc469db78388e4ed564dcfd4c91529f5482610cbc14d7679dec688c8556fd6689af43ad590674b2efcf08c88846f2b07e5afb407f886dde4134f43abc9fa0bf8ae5a94a782ad8b51a9e936faa23f710d13300040740930b390baa054de8ee3bffa71bff5f000d1f8432aa36baa81e4e25b77cf133f67854bc899aa9a448c06f6c95737b8f982c3120b99deebcb94d812e71cb420bfcdda20b7c0d25b1e3ec1bb8aef15ba83b40476525e83dbb741faa8f51ea59dabd97e3044b643ade483f8239502b047eb2a24345b5a7de3f6b9da7e0c0526f9ff706fa6064461a18bcb1ca1176b75966f19214ce9cd4bd10a952ab46bf5accbc4383cdde2c8f4f661af0b53c84c8c5d087bc39b651cc73a46620f6ca01d65cb1e64a807514f256f13ae51c9ef5a4bf7192facf740e144f6f7e108abd137fcfe2760a9a1170bbc040d4c4b70d910ea4e2343ed241b76ab47816a8969ec3a068184a92b01f1417eeb3b980ea99c2fd018f52d6098c3af7450cde98bc307388ad26de6e1cf28725e74d2c1a4469c3abfdcf5242ef885e1cf97cd5c7a4bbf49550052b2f52b15c51f092ece08ae22d7b64a6001e74d33d759cba7e56523d8b7390785eaa86741891c5c4e63084a4225a0239435630d20e5f974725f3b769779910d023f9f333273bfaf16113cbbb280f3f99c21f3b166a3a9fe3527c9e5c9812fff8b9995db1dc2556651488e203e2bbd7cebdf51131bb5baef4ef4c81f53d9e0f7d4029ad83cdc0762a19062706eae3ea538848293307c9024ce4bd6fdbbbbfb8bcd0fb17300d6e93faa3f19320b44b920bbcb3c3fe2fc85bdab97664234f3f709df05025dc666e1136577b0f1ba5b6144ae67614c5ed678a349b6cde0548a9bd60dc8044ffe2d2adbfde1bc70cdcc82ee21f8decb90f61592851d059531e232d24cd305f9ef05495ad9d68d19fa7c2a083dc2fe87e121c592b0196e0f7d099c7c93680b30c5dd63dd2360ca5c3ea4cd1a3eb55579472ed9546746318293fbb7e545067a0d09a7e1504679727867038887db86d90340c9df9d70731fccef539cb311d16b02876ffdbf22812e57bc5e50bb9c429a594610a334dbb0dd0f9f3b16872ede5b02 \ No newline at end of file diff --git a/wire/transaction.go b/wire/transaction.go index b5d4d60..759e9e5 100644 --- a/wire/transaction.go +++ b/wire/transaction.go @@ -209,7 +209,7 @@ func encodeKeyOffsets(enc *Encoder, refs []types.TxOutRef) { enc.WriteVariantTag(ref.Tag) switch ref.Tag { case types.RefTypeGlobalIndex: - enc.WriteVarint(ref.GlobalIndex) + enc.WriteUint64LE(ref.GlobalIndex) case types.RefTypeByID: enc.WriteBlob32((*[32]byte)(&ref.TxID)) enc.WriteVarint(ref.N) @@ -227,7 +227,7 @@ func decodeKeyOffsets(dec *Decoder) []types.TxOutRef { refs[i].Tag = dec.ReadVariantTag() switch refs[i].Tag { case types.RefTypeGlobalIndex: - refs[i].GlobalIndex = dec.ReadVarint() + refs[i].GlobalIndex = dec.ReadUint64LE() case types.RefTypeByID: dec.ReadBlob32((*[32]byte)(&refs[i].TxID)) refs[i].N = dec.ReadVarint() @@ -446,8 +446,8 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { case tagTxComment, tagString, tagTxDerivationHint, tagExtraUserData: return readStringBlob(dec) - // Varint fields - case tagUnlockTime, tagExpirationTime, tagTxDetailsFlags, tagUint64, tagEtcTxTime: + // Varint fields (structs with VARINT_FIELD) + case tagUnlockTime, tagExpirationTime, tagTxDetailsFlags, tagEtcTxTime: v := dec.ReadVarint() if dec.err != nil { return nil @@ -455,6 +455,8 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { return EncodeVarint(v) // Fixed-size integer fields + case tagUint64: // raw uint64_t — do_serialize → serialize_int → 8-byte LE + return dec.ReadBytes(8) case tagTxCryptoChecksum: // two uint32 LE return dec.ReadBytes(8) case tagUint32: // uint32 LE @@ -485,12 +487,8 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { return readTxServiceAttachment(dec) // Zarcanum extra variant - case tagZarcanumTxDataV1: // fee (varint) - v := dec.ReadVarint() - if dec.err != nil { - return nil - } - return EncodeVarint(v) + case tagZarcanumTxDataV1: // fee — FIELD(fee) → serialize_int → 8-byte LE + return dec.ReadBytes(8) // Signature variants case tagNLSAGSig: // vector (64 bytes each)