feat(wire): v2+ transaction serialisation with real testnet verification
Fix three bugs in the v2+ wire format and add complete variant tag handlers for Zarcanum proof and signature structures. Verified byte-identical round-trip against a real post-HF4 coinbase transaction from testnet block 101 (1323 bytes, tx hash 543bc3c2...3b61e0). Bugs fixed: - V2 suffix order was attachment+proofs, corrected to attachment+signatures+proofs - TransactionHash was hashing full blob, corrected to prefix-only (matching C++) - tagSignedParts was reading 4 fixed bytes, corrected to two varints New: TxInputZC type, SignaturesRaw field, tagZarcanumTxDataV1 handler, proof tags 46-48, signature tags 42-45, crypto blob readers for BPP/BPPE/ BGE/CLSAG GGX/GGXXG/aggregation proof/double Schnorr structures. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
dd04cc9dee
commit
8335b11a85
8 changed files with 856 additions and 34 deletions
|
|
@ -526,11 +526,52 @@ refresh.
|
|||
|
||||
---
|
||||
|
||||
## V2+ Transaction Serialisation
|
||||
|
||||
Completed and verified v2+ (Zarcanum/HF4+) transaction serialisation. The code
|
||||
is tested against a real post-HF4 coinbase transaction from testnet block 101
|
||||
(tx hash `543bc3c29e9f4c5d1fc566be03fb4da1f2ce2d70d4312fdcc3e4eed7ca3b61e0`,
|
||||
1323 bytes). Round-trip encoding produces byte-identical output and the
|
||||
prefix hash matches the known tx hash.
|
||||
|
||||
### Bugs fixed
|
||||
|
||||
- **V2 suffix field order** — was `attachment + proofs`, corrected to
|
||||
`attachment + signatures + proofs` (matching C++ serialisation order).
|
||||
- **`TransactionHash`** — was hashing the full transaction blob; corrected to
|
||||
delegate to `TransactionPrefixHash` for all versions (matching C++
|
||||
`get_transaction_hash`).
|
||||
- **`tagSignedParts` (17)** — was reading 4 fixed bytes (uint32 LE); corrected
|
||||
to read two varints (`n_outs` + `n_extras`), matching C++ `VARINT_FIELD`.
|
||||
|
||||
### New features
|
||||
|
||||
- **`TxInputZC`** — Zarcanum confidential input (tag 0x25). Wire format:
|
||||
`key_offsets + k_image + etc_details` (no amount field).
|
||||
- **`SignaturesRaw`** — new Transaction field for v2+ raw signature bytes.
|
||||
- **`tagZarcanumTxDataV1` (39)** — extra variant handler (varint fee).
|
||||
- **Proof tag handlers (46-48)** — `zc_asset_surjection_proof` (vector of
|
||||
BGE_proof_s), `zc_outs_range_proof` (BPP + aggregation proof),
|
||||
`zc_balance_proof` (double Schnorr, 96 bytes).
|
||||
- **Signature tag handlers (42-45)** — `NLSAG_sig`, `ZC_sig`, `void_sig`,
|
||||
`zarcanum_sig` with full crypto blob readers for CLSAG GGX/GGXXG, BPP/BPPE.
|
||||
|
||||
### Files
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `types/transaction.go` | Added `TxInputZC`, `SignaturesRaw` field |
|
||||
| `wire/transaction.go` | Fixed suffix, added InputTypeZC, 10 tag handlers |
|
||||
| `wire/transaction_v2_test.go` | New -- v2 round-trip, hash, fields, prefix tests |
|
||||
| `wire/hash.go` | Fixed `TransactionHash` to delegate to prefix hash |
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
**v2+ transaction serialisation is stubbed.** The v0/v1 wire format is complete
|
||||
and verified. The v2+ (Zarcanum) code paths compile but are untested -- they
|
||||
will be validated in Phase 2 when post-HF4 transactions appear on-chain.
|
||||
**V2+ spending transactions untested.** The v2 coinbase serialisation is
|
||||
verified, but `TxInputZC` and signature tags 42-45 remain untested with real
|
||||
spending transaction data (no spending txs on testnet yet).
|
||||
|
||||
**BPP range proof verification tested with real data.** The `cn_bpp_verify`
|
||||
bridge function (Bulletproofs++, 1 delta, `bpp_crypto_trait_ZC_out`) is verified
|
||||
|
|
|
|||
116
docs/plans/2026-02-21-v2-tx-serialisation-design.md
Normal file
116
docs/plans/2026-02-21-v2-tx-serialisation-design.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# V2+ Transaction Serialisation Design
|
||||
|
||||
## Context
|
||||
|
||||
Phase 1 implemented wire-format serialisation for v0/v1 (pre-HF4) transactions.
|
||||
The v2+ code paths (`encodePrefixV2`, `decodePrefixV2`, `encodeSuffixV2`,
|
||||
`decodeSuffixV2`, `encodeOutputsV2`, `decodeOutputsV2`) were written
|
||||
speculatively but **never tested against real chain data**.
|
||||
|
||||
The testnet has HF4 active at height 100 (currently ~511). Every coinbase from
|
||||
block 101 onwards is a version-2 transaction with Zarcanum outputs and proofs.
|
||||
|
||||
## Problem
|
||||
|
||||
Testing against real testnet data reveals three bugs and several missing handlers:
|
||||
|
||||
### Bugs
|
||||
|
||||
1. **V2 suffix order wrong**: Code writes `attachment + proofs`, but C++ wire
|
||||
format is `attachment + signatures + proofs`. The signatures variant vector
|
||||
is completely absent from `encodeSuffixV2`/`decodeSuffixV2`.
|
||||
|
||||
2. **`tagSignedParts` (17) reads 4 fixed bytes**: The C++ `signed_parts` struct
|
||||
uses `VARINT_FIELD` for both `n_outs` and `n_extras` — two varints, not a
|
||||
uint32 LE. This corrupts the stream when parsing real spending transactions.
|
||||
|
||||
3. **`tagZarcanumTxDataV1` (39) declared but unhandled**: The constant exists
|
||||
but `readVariantElementData` has no case for it. V2 transactions carry the
|
||||
fee in this extra variant. Wire format: single varint (fee).
|
||||
|
||||
### Missing Input Type
|
||||
|
||||
`InputTypeZC` (tag 0x25 / 37) — `txin_zc_input` — is the standard input for
|
||||
Zarcanum spending transactions. Wire format:
|
||||
|
||||
```
|
||||
key_offsets — variant vector (txout_ref_v elements)
|
||||
k_image — 32 bytes
|
||||
etc_details — variant vector (txin_etc_details_v elements)
|
||||
```
|
||||
|
||||
Note: unlike `txin_to_key`, there is **no amount field**.
|
||||
|
||||
### Missing Variant Tag Handlers
|
||||
|
||||
The `readVariantElementData` function needs handlers for proof and signature
|
||||
tags so `decodeRawVariantVector` can determine element boundaries:
|
||||
|
||||
**Proof tags:**
|
||||
- 46 (`zc_asset_surjection_proof`): vector of BGE_proof_s
|
||||
- 47 (`zc_outs_range_proof`): bpp_serialized + aggregation_proof_serialized
|
||||
- 48 (`zc_balance_proof`): generic_double_schnorr_sig_s (96 bytes fixed)
|
||||
|
||||
**Signature tags:**
|
||||
- 42 (`NLSAG_sig`): vector of 64-byte signatures
|
||||
- 43 (`ZC_sig`): 2 public_keys (64 bytes) + CLSAG_GGX_serialized
|
||||
- 44 (`void_sig`): 0 bytes (empty struct)
|
||||
- 45 (`zarcanum_sig`): 10 scalars (320 bytes) + bppe_serialized + public_key (32) + CLSAG_GGXXG_serialized
|
||||
|
||||
## Crypto Blob Wire Layouts
|
||||
|
||||
All crypto serialised structs use vectors of 32-byte scalars/points with varint
|
||||
length prefixes, plus fixed-size blobs:
|
||||
|
||||
```
|
||||
BGE_proof_s: A(32) + B(32) + vec(Pk) + vec(f) + y(32) + z(32)
|
||||
bpp_signature: vec(L) + vec(R) + A0(32)+A(32)+B(32)+r(32)+s(32)+delta(32)
|
||||
bppe_signature: vec(L) + vec(R) + A0(32)+A(32)+B(32)+r(32)+s(32)+d1(32)+d2(32)
|
||||
aggregation_proof: vec(commitments) + vec(y0s) + vec(y1s) + c(32)
|
||||
double_schnorr_sig: c(32) + y0(32) + y1(32)
|
||||
CLSAG_GGX: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32)
|
||||
CLSAG_GGXXG: c(32) + vec(r_g) + vec(r_x) + K1(32)+K2(32)+K3(32)+K4(32)
|
||||
```
|
||||
|
||||
Where `vec(X)` = `varint(count) + 32*count bytes`.
|
||||
|
||||
## Approach
|
||||
|
||||
Full variant-level parsing — add handlers for every v2+ variant tag in
|
||||
`readVariantElementData`, following the existing pattern. Each handler reads
|
||||
field-by-field based on the C++ serialisation order and returns raw bytes.
|
||||
|
||||
The alternative (reading suffix as a raw blob) doesn't work because we need to
|
||||
separate the three vectors (attachment, signatures, proofs) into distinct fields.
|
||||
|
||||
## Type Changes
|
||||
|
||||
Add `TxInputZC` struct to `types/transaction.go`:
|
||||
```go
|
||||
type TxInputZC struct {
|
||||
KeyOffsets []TxOutRef
|
||||
KeyImage KeyImage
|
||||
EtcDetails []byte
|
||||
}
|
||||
```
|
||||
|
||||
Add `SignaturesRaw []byte` field to `Transaction` for v2+ raw signatures.
|
||||
V0/v1 uses the structured `Signatures [][]Signature` field; v2+ uses
|
||||
`SignaturesRaw` (raw variant vector bytes).
|
||||
|
||||
## File Changes
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `types/transaction.go` | Add `TxInputZC`, `SignaturesRaw` field |
|
||||
| `wire/transaction.go` | Fix suffix, add InputTypeZC, add tag handlers, fix signed_parts |
|
||||
| `wire/transaction_v2_test.go` | New — v2 round-trip tests with real testnet data |
|
||||
|
||||
## Verification
|
||||
|
||||
1. Fetch block 101 coinbase blob from testnet via RPC
|
||||
2. Decode blob to Transaction struct
|
||||
3. Re-encode Transaction to bytes
|
||||
4. Assert byte-for-byte identity with original blob
|
||||
5. Hash prefix with Keccak-256, compare to known tx hash
|
||||
6. `go test -race ./...` — all tests pass
|
||||
169
docs/plans/2026-02-21-v2-tx-serialisation-plan.md
Normal file
169
docs/plans/2026-02-21-v2-tx-serialisation-plan.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# V2+ Transaction Serialisation — Implementation Plan
|
||||
|
||||
Design: `docs/plans/2026-02-21-v2-tx-serialisation-design.md`
|
||||
|
||||
## Step 1: Fix types
|
||||
|
||||
**File:** `types/transaction.go`
|
||||
|
||||
- Add `TxInputZC` struct with `KeyOffsets []TxOutRef`, `KeyImage KeyImage`, `EtcDetails []byte`
|
||||
- Add `InputType()` method returning `InputTypeZC`
|
||||
- Add `SignaturesRaw []byte` field to `Transaction` struct
|
||||
- Update doc comment on Transaction to mention SignaturesRaw for v2+
|
||||
|
||||
**Tests:** Compile-only — new types have no logic to test independently.
|
||||
|
||||
## Step 2: Fix v2 suffix
|
||||
|
||||
**File:** `wire/transaction.go`
|
||||
|
||||
Fix `encodeSuffixV2`:
|
||||
```go
|
||||
func encodeSuffixV2(enc *Encoder, tx *types.Transaction) {
|
||||
enc.WriteBytes(tx.Attachment)
|
||||
enc.WriteBytes(tx.SignaturesRaw)
|
||||
enc.WriteBytes(tx.Proofs)
|
||||
}
|
||||
```
|
||||
|
||||
Fix `decodeSuffixV2`:
|
||||
```go
|
||||
func decodeSuffixV2(dec *Decoder, tx *types.Transaction) {
|
||||
tx.Attachment = decodeRawVariantVector(dec)
|
||||
tx.SignaturesRaw = decodeRawVariantVector(dec)
|
||||
tx.Proofs = decodeRawVariantVector(dec)
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Add InputTypeZC handling
|
||||
|
||||
**File:** `wire/transaction.go`
|
||||
|
||||
In `encodeInputs`, add case for `types.TxInputZC`:
|
||||
```go
|
||||
case types.TxInputZC:
|
||||
encodeKeyOffsets(enc, v.KeyOffsets)
|
||||
enc.WriteBlob32((*[32]byte)(&v.KeyImage))
|
||||
enc.WriteBytes(v.EtcDetails)
|
||||
```
|
||||
|
||||
In `decodeInputs`, add case for `types.InputTypeZC`:
|
||||
```go
|
||||
case types.InputTypeZC:
|
||||
var in types.TxInputZC
|
||||
in.KeyOffsets = decodeKeyOffsets(dec)
|
||||
dec.ReadBlob32((*[32]byte)(&in.KeyImage))
|
||||
in.EtcDetails = decodeRawVariantVector(dec)
|
||||
vin = append(vin, in)
|
||||
```
|
||||
|
||||
## Step 4: Fix tagSignedParts handler
|
||||
|
||||
**File:** `wire/transaction.go`
|
||||
|
||||
Change `tagSignedParts` from reading 4 fixed bytes to reading two varints:
|
||||
```go
|
||||
case tagSignedParts:
|
||||
return readSignedParts(dec)
|
||||
```
|
||||
|
||||
New function:
|
||||
```go
|
||||
func readSignedParts(dec *Decoder) []byte {
|
||||
v1 := dec.ReadVarint() // n_outs
|
||||
if dec.err != nil { return nil }
|
||||
raw := EncodeVarint(v1)
|
||||
v2 := dec.ReadVarint() // n_extras
|
||||
if dec.err != nil { return nil }
|
||||
raw = append(raw, EncodeVarint(v2)...)
|
||||
return raw
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Add tagZarcanumTxDataV1 handler
|
||||
|
||||
**File:** `wire/transaction.go`
|
||||
|
||||
Add case in `readVariantElementData`:
|
||||
```go
|
||||
case tagZarcanumTxDataV1:
|
||||
v := dec.ReadVarint() // fee
|
||||
if dec.err != nil { return nil }
|
||||
return EncodeVarint(v)
|
||||
```
|
||||
|
||||
## Step 6: Add proof tag handlers (46, 47, 48)
|
||||
|
||||
**File:** `wire/transaction.go`
|
||||
|
||||
New constants:
|
||||
```go
|
||||
const (
|
||||
tagZCAssetSurjectionProof = 46
|
||||
tagZCOutsRangeProof = 47
|
||||
tagZCBalanceProof = 48
|
||||
)
|
||||
```
|
||||
|
||||
New reader functions:
|
||||
- `readBGEProof(dec)` — A(32)+B(32)+vec(32)+vec(32)+y(32)+z(32)
|
||||
- `readBPPSerialized(dec)` — vec(32)+vec(32)+192 bytes
|
||||
- `readAggregationProof(dec)` — vec(32)+vec(32)+vec(32)+32 bytes
|
||||
|
||||
Tag handlers:
|
||||
- 46: varint(count) + count * readBGEProof
|
||||
- 47: readBPPSerialized + readAggregationProof
|
||||
- 48: 96 fixed bytes
|
||||
|
||||
## Step 7: Add signature tag handlers (42, 43, 44, 45)
|
||||
|
||||
**File:** `wire/transaction.go`
|
||||
|
||||
New constants:
|
||||
```go
|
||||
const (
|
||||
tagNLSAGSig = 42
|
||||
tagZCSig = 43
|
||||
tagVoidSig = 44
|
||||
tagZarcanumSig = 45
|
||||
)
|
||||
```
|
||||
|
||||
New reader functions:
|
||||
- `readCLSAG_GGX(dec)` — 32+vec(32)+vec(32)+64 bytes
|
||||
- `readCLSAG_GGXXG(dec)` — 32+vec(32)+vec(32)+128 bytes
|
||||
- `readBPPESerialized(dec)` — vec(32)+vec(32)+224 bytes
|
||||
|
||||
Tag handlers:
|
||||
- 42: readVariantVectorFixed(dec, 64)
|
||||
- 43: 64 bytes + readCLSAG_GGX
|
||||
- 44: 0 bytes (empty)
|
||||
- 45: 320 bytes + readBPPESerialized + 32 bytes + readCLSAG_GGXXG
|
||||
|
||||
## Step 8: Test with real testnet data
|
||||
|
||||
**New file:** `wire/transaction_v2_test.go`
|
||||
|
||||
1. `TestV2CoinbaseRoundTrip` — fetch block 101 coinbase blob from testnet
|
||||
(or embed the hex literal), decode to Transaction, re-encode, assert
|
||||
byte-for-byte identity.
|
||||
|
||||
2. `TestV2CoinbaseTxHash` — decode prefix, hash with Keccak-256, compare
|
||||
to known tx hash `543bc3c29e9f4c5d1fc566be03fb4da1f2ce2d70d4312fdcc3e4eed7ca3b61e0`.
|
||||
|
||||
3. `TestV2SuffixFieldCount` — decode a v2 coinbase, verify that
|
||||
`Attachment` is a zero-count vector, `SignaturesRaw` is a zero-count
|
||||
vector, and `Proofs` is a 3-element vector.
|
||||
|
||||
## Step 9: Verify
|
||||
|
||||
```bash
|
||||
go test -race ./...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
All tests pass, no race conditions, no vet warnings.
|
||||
|
||||
## Step 10: Update docs
|
||||
|
||||
Update `docs/history.md` with v2+ serialisation completion.
|
||||
|
|
@ -83,6 +83,11 @@ type Transaction struct {
|
|||
// Each element corresponds to one input; inner slice is the ring.
|
||||
Signatures [][]Signature
|
||||
|
||||
// SignaturesRaw holds the serialised variant vector of v2+ signatures
|
||||
// (NLSAG_sig, ZC_sig, void_sig, zarcanum_sig). Stored as raw wire bytes.
|
||||
// V0/v1 uses the structured Signatures field; v2+ uses this field.
|
||||
SignaturesRaw []byte
|
||||
|
||||
// Attachment holds the serialised variant vector of transaction attachments.
|
||||
// Stored as raw wire bytes.
|
||||
Attachment []byte
|
||||
|
|
@ -147,6 +152,23 @@ type TxInputToKey struct {
|
|||
// InputType returns the wire variant tag for key inputs.
|
||||
func (t TxInputToKey) InputType() uint8 { return InputTypeToKey }
|
||||
|
||||
// TxInputZC is a Zarcanum confidential input (HF4+). Unlike TxInputToKey,
|
||||
// there is no amount field — amounts are hidden by commitments.
|
||||
// Wire order: key_offsets, k_image, etc_details.
|
||||
type TxInputZC struct {
|
||||
// KeyOffsets contains the output references forming the decoy ring.
|
||||
KeyOffsets []TxOutRef
|
||||
|
||||
// KeyImage prevents double-spending of this input.
|
||||
KeyImage KeyImage
|
||||
|
||||
// EtcDetails holds the serialised variant vector of input-level details.
|
||||
EtcDetails []byte
|
||||
}
|
||||
|
||||
// InputType returns the wire variant tag for ZC inputs.
|
||||
func (t TxInputZC) InputType() uint8 { return InputTypeZC }
|
||||
|
||||
// TxOutputBare is a transparent (pre-Zarcanum) transaction output.
|
||||
type TxOutputBare struct {
|
||||
// Amount in atomic units.
|
||||
|
|
|
|||
14
wire/hash.go
14
wire/hash.go
|
|
@ -52,16 +52,14 @@ func BlockHash(b *types.Block) types.Hash {
|
|||
return types.Hash(Keccak256(prefixed))
|
||||
}
|
||||
|
||||
// TransactionHash computes the full transaction hash (tx_id).
|
||||
// TransactionHash computes the transaction hash (tx_id).
|
||||
//
|
||||
// For v0/v1 transactions this is Keccak-256 of the full serialised transaction
|
||||
// (prefix + signatures + attachment). For v2+ it delegates to the prefix hash
|
||||
// (Zano computes v2+ hashes from prefix data only).
|
||||
// In the C++ daemon, get_transaction_hash delegates to
|
||||
// get_transaction_prefix_hash for all versions. The tx_id is always
|
||||
// Keccak-256 of the serialised prefix (version + inputs + outputs + extra,
|
||||
// in version-dependent field order).
|
||||
func TransactionHash(tx *types.Transaction) types.Hash {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransaction(enc, tx)
|
||||
return types.Hash(Keccak256(buf.Bytes()))
|
||||
return TransactionPrefixHash(tx)
|
||||
}
|
||||
|
||||
// TransactionPrefixHash computes the hash of a transaction prefix.
|
||||
|
|
|
|||
|
|
@ -130,16 +130,17 @@ func decodeSuffixV1(dec *Decoder, tx *types.Transaction) {
|
|||
tx.Attachment = decodeRawVariantVector(dec)
|
||||
}
|
||||
|
||||
// --- v2+ suffix (attachment + signatures_raw + proofs) ---
|
||||
// --- v2+ suffix (attachment + signatures + proofs) ---
|
||||
|
||||
func encodeSuffixV2(enc *Encoder, tx *types.Transaction) {
|
||||
enc.WriteBytes(tx.Attachment)
|
||||
// v2+ signatures and proofs are stored as raw wire bytes
|
||||
enc.WriteBytes(tx.SignaturesRaw)
|
||||
enc.WriteBytes(tx.Proofs)
|
||||
}
|
||||
|
||||
func decodeSuffixV2(dec *Decoder, tx *types.Transaction) {
|
||||
tx.Attachment = decodeRawVariantVector(dec)
|
||||
tx.SignaturesRaw = decodeRawVariantVector(dec)
|
||||
tx.Proofs = decodeRawVariantVector(dec)
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +158,10 @@ func encodeInputs(enc *Encoder, vin []types.TxInput) {
|
|||
encodeKeyOffsets(enc, v.KeyOffsets)
|
||||
enc.WriteBlob32((*[32]byte)(&v.KeyImage))
|
||||
enc.WriteBytes(v.EtcDetails)
|
||||
case types.TxInputZC:
|
||||
encodeKeyOffsets(enc, v.KeyOffsets)
|
||||
enc.WriteBlob32((*[32]byte)(&v.KeyImage))
|
||||
enc.WriteBytes(v.EtcDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +187,12 @@ func decodeInputs(dec *Decoder) []types.TxInput {
|
|||
dec.ReadBlob32((*[32]byte)(&in.KeyImage))
|
||||
in.EtcDetails = decodeRawVariantVector(dec)
|
||||
vin = append(vin, in)
|
||||
case types.InputTypeZC:
|
||||
var in types.TxInputZC
|
||||
in.KeyOffsets = decodeKeyOffsets(dec)
|
||||
dec.ReadBlob32((*[32]byte)(&in.KeyImage))
|
||||
in.EtcDetails = decodeRawVariantVector(dec)
|
||||
vin = append(vin, in)
|
||||
default:
|
||||
dec.err = fmt.Errorf("wire: unsupported input tag 0x%02x", tag)
|
||||
return vin
|
||||
|
|
@ -393,7 +404,7 @@ const (
|
|||
tagUnlockTime = 14 // etc_tx_details_unlock_time — varint
|
||||
tagExpirationTime = 15 // etc_tx_details_expiration_time — varint
|
||||
tagTxDetailsFlags = 16 // etc_tx_details_flags — varint
|
||||
tagSignedParts = 17 // signed_parts — uint32 LE
|
||||
tagSignedParts = 17 // signed_parts — two varints (n_outs, n_extras)
|
||||
tagExtraAttachmentInfo = 18 // extra_attachment_info — string + hash + varint
|
||||
tagExtraUserData = 19 // extra_user_data — string
|
||||
tagExtraAliasEntryOld = 20 // extra_alias_entry_old — complex
|
||||
|
|
@ -409,7 +420,18 @@ const (
|
|||
tagTxPayer = 31 // tx_payer — 2 keys + optional flag
|
||||
tagTxReceiver = 32 // tx_receiver — 2 keys + optional flag
|
||||
tagExtraAliasEntry = 33 // extra_alias_entry — complex
|
||||
tagZarcanumTxDataV1 = 39 // zarcanum_tx_data_v1 — complex
|
||||
tagZarcanumTxDataV1 = 39 // zarcanum_tx_data_v1 — varint (fee)
|
||||
|
||||
// Signature variant tags (signature_v).
|
||||
tagNLSAGSig = 42 // NLSAG_sig — vector<signature>
|
||||
tagZCSig = 43 // ZC_sig — 2 public_keys + CLSAG_GGX
|
||||
tagVoidSig = 44 // void_sig — empty
|
||||
tagZarcanumSig = 45 // zarcanum_sig — complex
|
||||
|
||||
// Proof variant tags (proof_v).
|
||||
tagZCAssetSurjectionProof = 46 // vector<BGE_proof_s>
|
||||
tagZCOutsRangeProof = 47 // bpp_serialized + aggregation_proof
|
||||
tagZCBalanceProof = 48 // generic_double_schnorr_sig_s (96 bytes)
|
||||
)
|
||||
|
||||
// readVariantElementData reads the data portion of a variant element (after the
|
||||
|
|
@ -435,8 +457,10 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte {
|
|||
// Fixed-size integer fields
|
||||
case tagTxCryptoChecksum: // two uint32 LE
|
||||
return dec.ReadBytes(8)
|
||||
case tagSignedParts, tagUint32: // uint32 LE
|
||||
case tagUint32: // uint32 LE
|
||||
return dec.ReadBytes(4)
|
||||
case tagSignedParts: // two varints: n_outs + n_extras
|
||||
return readSignedParts(dec)
|
||||
case tagEtcTxFlags16, tagUint16: // uint16 LE
|
||||
return dec.ReadBytes(2)
|
||||
|
||||
|
|
@ -460,6 +484,32 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte {
|
|||
case tagTxServiceAttachment:
|
||||
return readTxServiceAttachment(dec)
|
||||
|
||||
// Zarcanum extra variant
|
||||
case tagZarcanumTxDataV1: // fee (varint)
|
||||
v := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
return EncodeVarint(v)
|
||||
|
||||
// Signature variants
|
||||
case tagNLSAGSig: // vector<signature> (64 bytes each)
|
||||
return readVariantVectorFixed(dec, 64)
|
||||
case tagZCSig: // 2 public_keys + CLSAG_GGX_serialized
|
||||
return readZCSig(dec)
|
||||
case tagVoidSig: // empty struct
|
||||
return []byte{}
|
||||
case tagZarcanumSig: // complex: 10 scalars + bppe + public_key + CLSAG_GGXXG
|
||||
return readZarcanumSig(dec)
|
||||
|
||||
// Proof variants
|
||||
case tagZCAssetSurjectionProof: // vector<BGE_proof_s>
|
||||
return readZCAssetSurjectionProof(dec)
|
||||
case tagZCOutsRangeProof: // bpp_serialized + aggregation_proof
|
||||
return readZCOutsRangeProof(dec)
|
||||
case tagZCBalanceProof: // generic_double_schnorr_sig_s (3 scalars = 96 bytes)
|
||||
return dec.ReadBytes(96)
|
||||
|
||||
default:
|
||||
dec.err = fmt.Errorf("wire: unsupported variant tag 0x%02x (%d)", tag, tag)
|
||||
return nil
|
||||
|
|
@ -605,3 +655,276 @@ func readTxServiceAttachment(dec *Decoder) []byte {
|
|||
raw = append(raw, b)
|
||||
return raw
|
||||
}
|
||||
|
||||
// readSignedParts reads signed_parts (tag 17).
|
||||
// Structure: n_outs (varint) + n_extras (varint).
|
||||
func readSignedParts(dec *Decoder) []byte {
|
||||
v1 := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := EncodeVarint(v1)
|
||||
v2 := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, EncodeVarint(v2)...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// --- crypto blob readers ---
|
||||
// These read variable-length serialised crypto structures and return raw bytes.
|
||||
// All vectors are varint(count) + 32*count bytes (scalars or points).
|
||||
|
||||
// readVectorOfPoints reads a vector of 32-byte points/scalars.
|
||||
// Returns raw bytes including the varint count prefix.
|
||||
func readVectorOfPoints(dec *Decoder) []byte {
|
||||
return readVariantVectorFixed(dec, 32)
|
||||
}
|
||||
|
||||
// readBPPSerialized reads a bpp_signature_serialized.
|
||||
// Wire: vec(L) + vec(R) + A0(32) + A(32) + B(32) + r(32) + s(32) + delta(32).
|
||||
func readBPPSerialized(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// L: vector of points
|
||||
v := readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
// R: vector of points
|
||||
v = readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
// 6 fixed scalars: A0, A, B, r, s, delta
|
||||
b := dec.ReadBytes(6 * 32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// readBPPESerialized reads a bppe_signature_serialized.
|
||||
// Wire: vec(L) + vec(R) + A0(32) + A(32) + B(32) + r(32) + s(32) + delta_1(32) + delta_2(32).
|
||||
func readBPPESerialized(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
v := readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
v = readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
// 7 fixed scalars: A0, A, B, r, s, delta_1, delta_2
|
||||
b := dec.ReadBytes(7 * 32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// readBGEProof reads a BGE_proof_s.
|
||||
// Wire: A(32) + B(32) + vec(Pk) + vec(f) + y(32) + z(32).
|
||||
func readBGEProof(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// A + B: 2 fixed points
|
||||
b := dec.ReadBytes(64)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
// Pk: vector of points
|
||||
v := readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
// f: vector of scalars
|
||||
v = readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
// y + z: 2 fixed scalars
|
||||
b = dec.ReadBytes(64)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// readAggregationProof reads a vector_UG_aggregation_proof_serialized.
|
||||
// Wire: vec(commitments) + vec(y0s) + vec(y1s) + c(32).
|
||||
func readAggregationProof(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// 3 vectors of points/scalars
|
||||
for range 3 {
|
||||
v := readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
}
|
||||
// c: 1 fixed scalar
|
||||
b := dec.ReadBytes(32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// readCLSAG_GGX reads a CLSAG_GGX_signature_serialized.
|
||||
// Wire: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32).
|
||||
func readCLSAG_GGX(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// c: 1 fixed scalar
|
||||
b := dec.ReadBytes(32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
// r_g, r_x: 2 vectors
|
||||
for range 2 {
|
||||
v := readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
}
|
||||
// K1 + K2: 2 fixed points
|
||||
b = dec.ReadBytes(64)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// readCLSAG_GGXXG reads a CLSAG_GGXXG_signature_serialized.
|
||||
// Wire: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32) + K3(32) + K4(32).
|
||||
func readCLSAG_GGXXG(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// c: 1 fixed scalar
|
||||
b := dec.ReadBytes(32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
// r_g, r_x: 2 vectors
|
||||
for range 2 {
|
||||
v := readVectorOfPoints(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
}
|
||||
// K1 + K2 + K3 + K4: 4 fixed points
|
||||
b = dec.ReadBytes(128)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// --- signature variant readers ---
|
||||
|
||||
// readZCSig reads ZC_sig (tag 43).
|
||||
// Wire: pseudo_out_amount_commitment(32) + pseudo_out_blinded_asset_id(32) + CLSAG_GGX.
|
||||
func readZCSig(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// 2 public keys
|
||||
b := dec.ReadBytes(64)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
// CLSAG_GGX_serialized
|
||||
v := readCLSAG_GGX(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// readZarcanumSig reads zarcanum_sig (tag 45).
|
||||
// Wire: d(32) + C(32) + C'(32) + E(32) + c(32) + y0(32) + y1(32) + y2(32) + y3(32) + y4(32)
|
||||
//
|
||||
// + bppe_serialized + pseudo_out_amount_commitment(32) + CLSAG_GGXXG.
|
||||
func readZarcanumSig(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// 10 fixed scalars/points
|
||||
b := dec.ReadBytes(10 * 32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
// E_range_proof: bppe_signature_serialized
|
||||
v := readBPPESerialized(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
// pseudo_out_amount_commitment: 1 public key
|
||||
b = dec.ReadBytes(32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
// clsag_ggxxg: CLSAG_GGXXG_signature_serialized
|
||||
v = readCLSAG_GGXXG(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// --- proof variant readers ---
|
||||
|
||||
// readZCAssetSurjectionProof reads zc_asset_surjection_proof (tag 46).
|
||||
// Wire: varint(count) + count * BGE_proof_s.
|
||||
func readZCAssetSurjectionProof(dec *Decoder) []byte {
|
||||
count := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := EncodeVarint(count)
|
||||
for i := uint64(0); i < count; i++ {
|
||||
b := readBGEProof(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b...)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// readZCOutsRangeProof reads zc_outs_range_proof (tag 47).
|
||||
// Wire: bpp_signature_serialized + vector_UG_aggregation_proof_serialized.
|
||||
func readZCOutsRangeProof(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// bpp
|
||||
v := readBPPSerialized(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
// aggregation_proof
|
||||
v = readAggregationProof(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
return raw
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,8 +115,9 @@ func TestFullTxRoundTrip_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTransactionHash_Good(t *testing.T) {
|
||||
// TransactionHash for v0/v1 should equal TransactionPrefixHash
|
||||
// (confirmed from C++ source: get_transaction_hash delegates to prefix hash).
|
||||
// TransactionHash should equal TransactionPrefixHash for all versions.
|
||||
// Confirmed from C++ source: get_transaction_hash delegates to
|
||||
// get_transaction_prefix_hash for all transaction versions.
|
||||
tx := types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
|
||||
|
|
@ -130,25 +131,20 @@ func TestTransactionHash_Good(t *testing.T) {
|
|||
|
||||
txHash := TransactionHash(&tx)
|
||||
prefixHash := TransactionPrefixHash(&tx)
|
||||
// For v1, full tx hash should differ from prefix hash because
|
||||
// TransactionHash encodes the full transaction (prefix + suffix).
|
||||
// The prefix is a subset of the full encoding.
|
||||
|
||||
// TransactionHash always delegates to TransactionPrefixHash.
|
||||
if txHash != prefixHash {
|
||||
t.Error("TransactionHash should equal TransactionPrefixHash")
|
||||
}
|
||||
|
||||
// Verify manual consistency.
|
||||
var prefBuf bytes.Buffer
|
||||
enc1 := NewEncoder(&prefBuf)
|
||||
EncodeTransactionPrefix(enc1, &tx)
|
||||
enc := NewEncoder(&prefBuf)
|
||||
EncodeTransactionPrefix(enc, &tx)
|
||||
|
||||
var fullBuf bytes.Buffer
|
||||
enc2 := NewEncoder(&fullBuf)
|
||||
EncodeTransaction(enc2, &tx)
|
||||
|
||||
// Prefix hash is over prefix bytes only.
|
||||
if Keccak256(prefBuf.Bytes()) != [32]byte(prefixHash) {
|
||||
t.Error("TransactionPrefixHash does not match manual prefix encoding")
|
||||
}
|
||||
// TransactionHash is over full encoding.
|
||||
if Keccak256(fullBuf.Bytes()) != [32]byte(txHash) {
|
||||
t.Error("TransactionHash does not match manual full encoding")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxInputToKeyRoundTrip_Good(t *testing.T) {
|
||||
|
|
@ -245,8 +241,8 @@ func TestExtraVariantTags_Good(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "signed_parts",
|
||||
// count=1, tag=17, uint32 LE
|
||||
data: []byte{0x01, tagSignedParts, 0xFF, 0x00, 0x00, 0x00},
|
||||
// count=1, tag=17, two varints: n_outs=2, n_extras=1
|
||||
data: []byte{0x01, tagSignedParts, 0x02, 0x01},
|
||||
},
|
||||
{
|
||||
name: "etc_tx_flags16",
|
||||
|
|
|
|||
157
wire/transaction_v2_test.go
Normal file
157
wire/transaction_v2_test.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// 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 wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
)
|
||||
|
||||
// Block 101 coinbase transaction from testnet (post-HF4, version 2).
|
||||
// TX hash: 543bc3c29e9f4c5d1fc566be03fb4da1f2ce2d70d4312fdcc3e4eed7ca3b61e0
|
||||
// 1323 bytes.
|
||||
const testnetCoinbaseV2Hex = "020100650616f8b44403a658f6a5cf66a20edeb5cda69913a6057d5bda321e39e4ce69c197c41316362e302e312e325b666131363038632d64697274795d15000b02e35a0b020e620e6f02260b219a40a8799bd76dc19a4db2d3eda28997bcb85341475b33db8dc928a81e1bdf0cd0bbe4f58a988e6ca84e1925b45be8ada18b17908187da9a78d9e58553834ced9654f46406bb69a1cd6d39a574dac9ed62fb4f144640a6680ca7de072fc974c32d3eaafafc623bf483e858d42e8bf4ec7df064ada2e34934469cff6b62685e583b2120b1052c0026875db29b157807ad75cb803dddc54892e4c96238e36773ae1e311a3032698c47a29ba8b274a9f113960a09f09b88d03c72ece51ad8034b7542a9e7c186a5f95d37fff60a0f2cf5885d2279f6e3750feb276a4d8f65d1725ad255e47bfd65c5cc74c32d3eaafafc623bf483e858d42e8bf4ec7df064ada2e34934469cff6b6268ee18b9ce799d5705000000032e002f0747c3d2db565bf368c9879dd1b08899a5e69bc956bc0a89b0cb456a54e066aa8589c6a14ac578409af177d9605f03fe61dfae8067338a40ca551b34580350f694bf389b62ae684146d33632e1ca5bf51dc6fed884780443489ee8fd68a535ddcf5299c9ca2f400fd0a978b1c0ef55a03922549ef78b8b5cd268bd4df5eb32f1fc4d4543466be7e9b9ceb6051955a815427bd773ad9a5cc9fec49fae4c0ab46fc753d1c86e8c04cea4b903fc8c42f404bc8c85b25eb2468f8de75171e2bf802ae96daa934cdb68b0609eb8942b3691c2bf1a122068bcd18d8ef4dd08abb292978007599626cfd549b317eb92667ab1783a730cab6528326b581034a60b8f2582ff61d8ac1ec8395699a170dc9ca5ae2a87cd70388083475e38763aa327e31b27bd691ebd37e22b4eda43001e908fdb211de548ed942766139c8197201d9cc53c744a88ee20995c5f0b64d1bc48293f9c3b8799b2866e473915871df9d55ac065c58ebd51887763709d9e9992d317c12bd27ad933452d06b821b4ee282de78e7cc56102ad119d2f1fb9a79a709614cb24dcb83119bb5734a70c923c4c9586afe1e5bdfedc244f7568a3cd9de95d9d240fb01ee0e0695f6d2066d085457054e78dead4b47ed0fcf2de7c5a9802352f7ab65667c468e32329964723a2084ae0dd38ae9779ff7e0870d6b664275b8207e545f4f80537e7cc4c9f81098eefa42e5efc1c8586bc9e7a48652c1f1e1e28e86b6e7ea80079dd6db7c78d083235ede6ae359631dbcb543d3a6e8b8692fb013832acb9b93ee717c72e832fd78c3a2d5f4fcb380fe3e21825ce80bda2c30b499ae87ce5e9daedde3f8885bcd7463fccaa88d375007c8c94c0f95faa4d0c9e14811442ecb3dc78bd46dafba93e648324b7d0a36d0c02c61a3937e37ff91acd74bd2877bb47e236c36315744a8031339a02c41481d52b5157b6954e712187a14edcd6faf0e6adbb8ec66f2d9260bd238106e076d3098e02943b66ec6f0ff40b3a9e7c9d0f6064317ff7ecc86a4891cfef46e82b0d9f940b4915135e692c042cbcedfe3220c71d6a8935a83b675d93dc70e9035181183f0402527d9d6113e2238807fa6f4646def4d88675ed76862f40918e7249de1d338a053abaed2c406e2824c3df27e147de78a0080a0ca633d4710c068ee126071dbd09c81ec2b948b73997f38c89ca1cd49a2338b49380bbe32c9ee6035b5c2532800e3096fcda7a6274966a8e4a72980655e3683a0ea9317585777add458cb728b2850ef1b6204572195b08407da42101436bfb768f5f13a27fc3766a41cc7812d4f1006e8c64688a9ce9d8666f29abbff012bc86a4b985697cccde23ec916b012ff40a"
|
||||
|
||||
func TestV2CoinbaseRoundTrip_Good(t *testing.T) {
|
||||
blob, err := hex.DecodeString(testnetCoinbaseV2Hex)
|
||||
if err != nil {
|
||||
t.Fatalf("bad test hex: %v", err)
|
||||
}
|
||||
|
||||
// Decode
|
||||
dec := NewDecoder(bytes.NewReader(blob))
|
||||
tx := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode failed: %v", dec.Err())
|
||||
}
|
||||
|
||||
// Re-encode
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransaction(enc, &tx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode failed: %v", enc.Err())
|
||||
}
|
||||
|
||||
// Byte-for-byte comparison
|
||||
got := buf.Bytes()
|
||||
if !bytes.Equal(got, blob) {
|
||||
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob))
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2CoinbaseTxHash_Good(t *testing.T) {
|
||||
blob, _ := hex.DecodeString(testnetCoinbaseV2Hex)
|
||||
|
||||
// Decode full transaction to get the prefix boundary.
|
||||
dec := NewDecoder(bytes.NewReader(blob))
|
||||
tx := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode failed: %v", dec.Err())
|
||||
}
|
||||
|
||||
// Hash the prefix
|
||||
got := TransactionHash(&tx)
|
||||
|
||||
wantHex := "543bc3c29e9f4c5d1fc566be03fb4da1f2ce2d70d4312fdcc3e4eed7ca3b61e0"
|
||||
wantBytes, _ := hex.DecodeString(wantHex)
|
||||
var want types.Hash
|
||||
copy(want[:], wantBytes)
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("tx hash mismatch:\n got %x\n want %x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2CoinbaseFields_Good(t *testing.T) {
|
||||
blob, _ := hex.DecodeString(testnetCoinbaseV2Hex)
|
||||
dec := NewDecoder(bytes.NewReader(blob))
|
||||
tx := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode failed: %v", dec.Err())
|
||||
}
|
||||
|
||||
// Version
|
||||
if tx.Version != types.VersionPostHF4 {
|
||||
t.Errorf("version: got %d, want %d", tx.Version, types.VersionPostHF4)
|
||||
}
|
||||
|
||||
// Inputs: should be 1 coinbase input
|
||||
if len(tx.Vin) != 1 {
|
||||
t.Fatalf("input count: got %d, want 1", len(tx.Vin))
|
||||
}
|
||||
gen, ok := tx.Vin[0].(types.TxInputGenesis)
|
||||
if !ok {
|
||||
t.Fatalf("input[0] type: got %T, want TxInputGenesis", tx.Vin[0])
|
||||
}
|
||||
if gen.Height != 101 {
|
||||
t.Errorf("coinbase height: got %d, want 101", gen.Height)
|
||||
}
|
||||
|
||||
// Outputs: should be 2 Zarcanum outputs
|
||||
if len(tx.Vout) != 2 {
|
||||
t.Fatalf("output count: got %d, want 2", len(tx.Vout))
|
||||
}
|
||||
for i, out := range tx.Vout {
|
||||
if _, ok := out.(types.TxOutputZarcanum); !ok {
|
||||
t.Errorf("output[%d] type: got %T, want TxOutputZarcanum", i, out)
|
||||
}
|
||||
}
|
||||
|
||||
// Suffix: attachment=empty, signatures=empty, proofs=3 elements
|
||||
if !bytes.Equal(tx.Attachment, EncodeVarint(0)) {
|
||||
t.Errorf("attachment: expected empty vector, got %d bytes", len(tx.Attachment))
|
||||
}
|
||||
if !bytes.Equal(tx.SignaturesRaw, EncodeVarint(0)) {
|
||||
t.Errorf("signatures_raw: expected empty vector, got %d bytes", len(tx.SignaturesRaw))
|
||||
}
|
||||
|
||||
// Proofs should start with varint(3) = 0x03
|
||||
if len(tx.Proofs) == 0 {
|
||||
t.Fatal("proofs: expected non-empty")
|
||||
}
|
||||
proofsCount, n, err := DecodeVarint(tx.Proofs)
|
||||
if err != nil {
|
||||
t.Fatalf("proofs: failed to decode count varint: %v", err)
|
||||
}
|
||||
if proofsCount != 3 {
|
||||
t.Errorf("proofs count: got %d, want 3", proofsCount)
|
||||
}
|
||||
|
||||
// First proof should be tag 0x2E (46) = zc_asset_surjection_proof
|
||||
if n < len(tx.Proofs) && tx.Proofs[n] != 0x2E {
|
||||
t.Errorf("proofs[0] tag: got 0x%02x, want 0x2E", tx.Proofs[n])
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2PrefixDecode_Good(t *testing.T) {
|
||||
blob, _ := hex.DecodeString(testnetCoinbaseV2Hex)
|
||||
dec := NewDecoder(bytes.NewReader(blob))
|
||||
tx := DecodeTransactionPrefix(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("prefix decode failed: %v", dec.Err())
|
||||
}
|
||||
|
||||
// Prefix re-encodes correctly
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransactionPrefix(enc, &tx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("prefix encode failed: %v", enc.Err())
|
||||
}
|
||||
|
||||
// The prefix should be a proper prefix of the full blob
|
||||
prefix := buf.Bytes()
|
||||
if len(prefix) >= len(blob) {
|
||||
t.Fatalf("prefix (%d bytes) not shorter than full blob (%d bytes)", len(prefix), len(blob))
|
||||
}
|
||||
if !bytes.Equal(prefix, blob[:len(prefix)]) {
|
||||
t.Fatal("prefix bytes don't match start of full blob")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue