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)