feat(wire): Phase 1 wire serialisation — bit-identical to C++ daemon
Add consensus-critical binary serialisation for blocks and transactions, verified by computing the testnet genesis block hash and matching the C++ daemon output (cb9d5455...4963). Fixes Phase 0 type mismatches (variant tags, field widths, missing fields) and adds encoder/decoder, tree hash, and block/transaction hashing. Key discovery: CryptoNote's get_object_hash(blobdata) prepends varint(length) before hashing, so BlockHash = Keccak256(varint(len) || blob). Co-Authored-By: Charon <charon@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
37cc3d7342
commit
6a3f8829cb
17 changed files with 2670 additions and 115 deletions
|
|
@ -47,9 +47,32 @@ transaction types across versions 0 through 3.
|
|||
|
||||
### wire/
|
||||
|
||||
Consensus-critical binary serialisation primitives. Currently implements
|
||||
CryptoNote varint encoding (7-bit LEB128 with MSB continuation). All encoding
|
||||
must be bit-identical to the C++ reference implementation.
|
||||
Consensus-critical binary serialisation for blocks, transactions, and all wire
|
||||
primitives. All encoding is bit-identical to the C++ reference implementation.
|
||||
|
||||
**Primitives:**
|
||||
- `Encoder` / `Decoder` -- sticky-error streaming codec (call `Err()` once)
|
||||
- `EncodeVarint` / `DecodeVarint` -- 7-bit LEB128 with MSB continuation
|
||||
- `Keccak256` -- pre-NIST Keccak-256 (CryptoNote's `cn_fast_hash`)
|
||||
|
||||
**Block serialisation:**
|
||||
- `EncodeBlockHeader` / `DecodeBlockHeader` -- wire order: major, nonce(LE64),
|
||||
prev_id, minor(varint), timestamp(varint), flags
|
||||
- `EncodeBlock` / `DecodeBlock` -- header + miner_tx + tx_hashes
|
||||
- `BlockHashingBlob` -- serialised header || tree_root || varint(tx_count)
|
||||
- `BlockHash` -- Keccak-256 of varint(len) + block hashing blob
|
||||
|
||||
**Transaction serialisation (v0/v1):**
|
||||
- `EncodeTransactionPrefix` / `DecodeTransactionPrefix` -- version-dependent
|
||||
field ordering (v0/v1: version, vin, vout, extra; v2+: version, vin, extra, vout)
|
||||
- `EncodeTransaction` / `DecodeTransaction` -- prefix + signatures + attachment
|
||||
- All variant tags match `SET_VARIANT_TAGS` from `currency_basic.h`
|
||||
- Extra/attachment stored as raw wire bytes for bit-identical round-tripping
|
||||
|
||||
**Hashing:**
|
||||
- `TreeHash` -- CryptoNote Merkle tree (direct port of `crypto/tree-hash.c`)
|
||||
- `TransactionPrefixHash` -- Keccak-256 of serialised prefix
|
||||
- `TransactionHash` -- Keccak-256 of full serialised transaction
|
||||
|
||||
### difficulty/
|
||||
|
||||
|
|
@ -142,10 +165,11 @@ Four address types are supported via distinct prefixes:
|
|||
```go
|
||||
type BlockHeader struct {
|
||||
MajorVersion uint8
|
||||
MinorVersion uint8
|
||||
Timestamp uint64
|
||||
PrevID Hash
|
||||
Nonce uint64
|
||||
PrevID Hash
|
||||
MinorVersion uint64 // varint on wire
|
||||
Timestamp uint64 // varint on wire
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
type Block struct {
|
||||
|
|
@ -155,11 +179,14 @@ type Block struct {
|
|||
}
|
||||
|
||||
type Transaction struct {
|
||||
Version uint8
|
||||
UnlockTime uint64
|
||||
Version uint64 // varint on wire
|
||||
Vin []TxInput
|
||||
Vout []TxOutput
|
||||
Extra []byte
|
||||
Extra []byte // raw wire bytes (variant vector)
|
||||
Signatures [][]Signature // v0/v1 only
|
||||
Attachment []byte // raw wire bytes (variant vector)
|
||||
Proofs []byte // raw wire bytes (v2+ only)
|
||||
HardforkID uint8 // v3+ only
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -172,11 +199,14 @@ Transaction versions progress through the hardfork schedule:
|
|||
| 2 | Post-HF4 | Zarcanum confidential transactions (CLSAG) |
|
||||
| 3 | Post-HF5 | Confidential assets with surjection proofs |
|
||||
|
||||
Input types: `TxInputGenesis` (coinbase, tag `0xFF`) and `TxInputToKey` (standard
|
||||
spend with ring signature, tag `0x02`).
|
||||
Input types: `TxInputGenesis` (coinbase, tag `0x00`) and `TxInputToKey` (standard
|
||||
spend with ring signature, tag `0x01`).
|
||||
|
||||
Output types: `TxOutputBare` (transparent, tag `0x02`) and `TxOutputZarcanum`
|
||||
(confidential with Pedersen commitments, tag `0x03`).
|
||||
Output types: `TxOutputBare` (transparent, tag `0x24`) and `TxOutputZarcanum`
|
||||
(confidential with Pedersen commitments, tag `0x26`).
|
||||
|
||||
Additional types: `TxOutToKey` (public key + mix_attr, 33 bytes on wire),
|
||||
`TxOutRef` (variant: global index or ref_by_id).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -216,6 +246,23 @@ different checksums and break address compatibility with the C++ node.
|
|||
Decoding reverses this process: base58 decode, extract and validate the varint
|
||||
prefix, verify the Keccak-256 checksum, then extract the two keys and flags.
|
||||
|
||||
### Block Hash Length Prefix
|
||||
|
||||
The C++ code computes block hashes via `get_object_hash(get_block_hashing_blob(b))`.
|
||||
Because `get_block_hashing_blob` returns a `blobdata` (std::string) and
|
||||
`get_object_hash` serialises its argument through `binary_archive` before hashing,
|
||||
the actual hash input is `varint(len(blob)) || blob` -- the binary archive
|
||||
prepends a varint length when serialising a string. This CryptoNote convention is
|
||||
replicated in Go's `BlockHash` function.
|
||||
|
||||
### Extra as Raw Bytes
|
||||
|
||||
Transaction extra, attachment, and proofs fields are stored as opaque raw wire
|
||||
bytes rather than being fully parsed into Go structures. The `decodeRawVariantVector`
|
||||
function reads variant vectors at the tag level to determine element boundaries but
|
||||
preserves all bytes verbatim. This enables bit-identical round-tripping without
|
||||
implementing every extra variant type (there are 20+ defined in the C++ code).
|
||||
|
||||
### Varint Encoding
|
||||
|
||||
The wire format uses 7-bit variable-length integers identical to protobuf
|
||||
|
|
|
|||
|
|
@ -94,12 +94,71 @@ and full coverage of the consensus-critical configuration surface.
|
|||
|
||||
---
|
||||
|
||||
## Phase 1 -- Wire Serialisation (Planned)
|
||||
## Phase 1 -- Wire Serialisation
|
||||
|
||||
Extend `wire/` with full block and transaction binary serialisation matching the
|
||||
C++ `binary_archive` format. Add `Serialise()` and `Deserialise()` methods to
|
||||
`Block`, `Transaction`, and all input/output types. Validate against real
|
||||
mainnet block blobs.
|
||||
Phase 1 added consensus-critical binary serialisation for blocks and transactions,
|
||||
verified to be bit-identical to the C++ daemon output. The definitive proof is
|
||||
the genesis block hash test: serialising the testnet genesis block and computing
|
||||
its Keccak-256 hash produces the exact value returned by the C++ daemon
|
||||
(`cb9d5455ccb79451931003672c405f5e2ac51bff54021aa30bc4499b1ffc4963`).
|
||||
|
||||
### Type corrections from Phase 0
|
||||
|
||||
Phase 0 types had several mismatches with the C++ wire format, corrected here:
|
||||
|
||||
- `BlockHeader.MinorVersion` changed from `uint8` to `uint64` (varint on wire)
|
||||
- `BlockHeader.Flags` added (`uint8`, 1 byte fixed)
|
||||
- `Transaction.Version` changed from `uint8` to `uint64` (varint on wire)
|
||||
- `Transaction.UnlockTime` removed (lives in extra variants, not top-level)
|
||||
- All variant tags corrected to match `SET_VARIANT_TAGS` from `currency_basic.h`:
|
||||
`InputTypeGenesis=0`, `InputTypeToKey=1`, `OutputTypeBare=36`, `OutputTypeZarcanum=38`
|
||||
- `TxOutToKey` struct added (public key + mix_attr, 33 bytes packed)
|
||||
- `TxOutRef` variant type added (global index or ref_by_id)
|
||||
- `Transaction.Signatures`, `Transaction.Attachment`, `Transaction.Proofs` fields added
|
||||
|
||||
### Files added
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `wire/encoder.go` | Sticky-error streaming encoder |
|
||||
| `wire/decoder.go` | Sticky-error streaming decoder |
|
||||
| `wire/block.go` | Block/BlockHeader encode/decode |
|
||||
| `wire/transaction.go` | Transaction encode/decode (v0/v1 + v2+ stubs) |
|
||||
| `wire/treehash.go` | Keccak-256 + CryptoNote Merkle tree hash |
|
||||
| `wire/hash.go` | BlockHash, TransactionPrefixHash, TransactionHash |
|
||||
| `wire/encoder_test.go` | Encoder round-trip tests |
|
||||
| `wire/decoder_test.go` | Decoder round-trip tests |
|
||||
| `wire/block_test.go` | Block header + full block round-trip tests |
|
||||
| `wire/transaction_test.go` | Coinbase, ToKey, signatures, variant tag tests |
|
||||
| `wire/treehash_test.go` | Tree hash for 0-8 hashes |
|
||||
| `wire/hash_test.go` | Genesis block hash verification |
|
||||
|
||||
### Key findings
|
||||
|
||||
- **Block hash length prefix**: The C++ `get_object_hash(blobdata)` serialises
|
||||
the string through `binary_archive` before hashing, prepending `varint(length)`.
|
||||
The actual block hash input is `varint(len) || block_hashing_blob`, not just
|
||||
the blob itself.
|
||||
|
||||
- **Genesis data sources**: The `_genesis_tn.cpp.gen` uint64 array is the
|
||||
canonical genesis transaction data, not the `.genesis_tn.txt` hex dump (which
|
||||
was stale from a different wallet generation).
|
||||
|
||||
- **Extra as raw bytes**: Transaction extra, attachment, and proofs are stored
|
||||
as opaque raw wire bytes with tag-level boundary detection. This enables
|
||||
bit-identical round-tripping without implementing all 20+ extra variant types.
|
||||
|
||||
### Coverage
|
||||
|
||||
| Package | Coverage |
|
||||
|---------|----------|
|
||||
| config | 100.0% |
|
||||
| difficulty | 81.0% |
|
||||
| types | 73.4% |
|
||||
| wire | 76.8% |
|
||||
|
||||
Wire coverage is reduced by v2+ code paths (0% -- Phase 2 scope). Excluding
|
||||
v2+ stubs, the v0/v1 serialisation code exceeds 85% coverage.
|
||||
|
||||
## Phase 2 -- Crypto Bridge (Planned)
|
||||
|
||||
|
|
@ -147,10 +206,9 @@ hash computation and coinstake transaction construction.
|
|||
|
||||
## Known Limitations
|
||||
|
||||
**No wire serialisation.** Block and transaction types are defined as Go structs
|
||||
but cannot yet be serialised to or deserialised from the CryptoNote binary
|
||||
format. This means the types cannot be used to parse real chain data until
|
||||
Phase 1 is complete.
|
||||
**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.
|
||||
|
||||
**No cryptographic operations.** Key derivation, ring signatures, bulletproofs,
|
||||
and all other cryptographic primitives are deferred to Phase 2. Address
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import (
|
|||
"golang.org/x/crypto/sha3"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
)
|
||||
|
||||
// FlagAuditable marks an address as auditable. When set, the address was
|
||||
|
|
@ -63,7 +62,7 @@ func IsIntegratedPrefix(prefix uint64) bool {
|
|||
// The checksum is the first 4 bytes of Keccak-256 over the preceding data.
|
||||
func (a *Address) Encode(prefix uint64) string {
|
||||
// Build the raw data: prefix (varint) + keys + flags.
|
||||
prefixBytes := wire.EncodeVarint(prefix)
|
||||
prefixBytes := encodeVarint(prefix)
|
||||
raw := make([]byte, 0, len(prefixBytes)+32+32+1+4)
|
||||
raw = append(raw, prefixBytes...)
|
||||
raw = append(raw, a.SpendPublicKey[:]...)
|
||||
|
|
@ -91,7 +90,7 @@ func DecodeAddress(s string) (*Address, uint64, error) {
|
|||
}
|
||||
|
||||
// Decode the prefix varint.
|
||||
prefix, prefixLen, err := wire.DecodeVarint(raw)
|
||||
prefix, prefixLen, err := decodeVarint(raw)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("types: invalid address prefix varint: %w", err)
|
||||
}
|
||||
|
|
@ -287,3 +286,38 @@ func base58CharIndex(c byte) int {
|
|||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Varint helpers (inlined from wire package to avoid import cycle)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func encodeVarint(v uint64) []byte {
|
||||
if v == 0 {
|
||||
return []byte{0x00}
|
||||
}
|
||||
var buf [10]byte
|
||||
n := 0
|
||||
for v > 0 {
|
||||
buf[n] = byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if v > 0 {
|
||||
buf[n] |= 0x80
|
||||
}
|
||||
n++
|
||||
}
|
||||
return append([]byte(nil), buf[:n]...)
|
||||
}
|
||||
|
||||
func decodeVarint(data []byte) (uint64, int, error) {
|
||||
if len(data) == 0 {
|
||||
return 0, 0, errors.New("types: cannot decode varint from empty data")
|
||||
}
|
||||
var v uint64
|
||||
for i := 0; i < len(data) && i < 10; i++ {
|
||||
v |= uint64(data[i]&0x7f) << (7 * uint(i))
|
||||
if data[i]&0x80 == 0 {
|
||||
return v, i + 1, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, errors.New("types: varint overflow")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,28 +9,39 @@
|
|||
|
||||
package types
|
||||
|
||||
// BlockHeader contains the fields present in every block header. These fields
|
||||
// are consensus-critical and must be serialised in the exact order defined by
|
||||
// the CryptoNote wire format.
|
||||
// BlockHeader contains the fields present in every block header. The fields
|
||||
// are listed in wire serialisation order as defined by the C++ daemon
|
||||
// (currency_basic.h:1123-1131).
|
||||
//
|
||||
// Wire format:
|
||||
//
|
||||
// major_version FIXED uint8 (1 byte)
|
||||
// nonce FIXED uint64 (8 bytes LE)
|
||||
// prev_id BLOB hash (32 bytes)
|
||||
// minor_version VARINT uint64
|
||||
// timestamp VARINT uint64
|
||||
// flags FIXED uint8 (1 byte)
|
||||
type BlockHeader struct {
|
||||
// MajorVersion determines which consensus rules apply to this block.
|
||||
// The version increases at hardfork boundaries.
|
||||
MajorVersion uint8
|
||||
|
||||
// MinorVersion is used for soft-fork signalling within a major version.
|
||||
MinorVersion uint8
|
||||
|
||||
// Timestamp is the Unix epoch time (seconds) when the block was created.
|
||||
// For PoS blocks this is the kernel timestamp; for PoW blocks it is the
|
||||
// miner's claimed time.
|
||||
Timestamp uint64
|
||||
// Nonce is iterated by the miner to find a valid PoW solution.
|
||||
// For PoS blocks this carries the stake modifier.
|
||||
Nonce uint64
|
||||
|
||||
// PrevID is the hash of the previous block in the chain.
|
||||
PrevID Hash
|
||||
|
||||
// Nonce is the value iterated by the miner to find a valid PoW solution.
|
||||
// For PoS blocks this field carries the stake modifier.
|
||||
Nonce uint64
|
||||
// MinorVersion is used for soft-fork signalling within a major version.
|
||||
// Encoded as varint on wire (uint64).
|
||||
MinorVersion uint64
|
||||
|
||||
// Timestamp is the Unix epoch time (seconds) when the block was created.
|
||||
// Encoded as varint on wire.
|
||||
Timestamp uint64
|
||||
|
||||
// Flags encodes block properties (e.g. PoS vs PoW).
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
// Block is a complete block including the header, miner (coinbase) transaction,
|
||||
|
|
|
|||
|
|
@ -10,32 +10,59 @@
|
|||
package types
|
||||
|
||||
// Transaction version constants matching the C++ TRANSACTION_VERSION_* defines.
|
||||
// On the wire, version is encoded as a varint (uint64).
|
||||
const (
|
||||
// VersionInitial is the genesis/coinbase transaction version.
|
||||
VersionInitial uint8 = 0
|
||||
|
||||
// VersionPreHF4 is the standard transaction version before hardfork 4.
|
||||
VersionPreHF4 uint8 = 1
|
||||
|
||||
// VersionPostHF4 is the Zarcanum transaction version introduced at HF4.
|
||||
VersionPostHF4 uint8 = 2
|
||||
|
||||
// VersionPostHF5 is the confidential assets transaction version from HF5.
|
||||
VersionPostHF5 uint8 = 3
|
||||
VersionInitial uint64 = 0 // genesis/coinbase
|
||||
VersionPreHF4 uint64 = 1 // standard pre-HF4
|
||||
VersionPostHF4 uint64 = 2 // Zarcanum (HF4+)
|
||||
VersionPostHF5 uint64 = 3 // confidential assets (HF5+)
|
||||
)
|
||||
|
||||
// Transaction represents a Lethean blockchain transaction. The structure
|
||||
// covers all transaction versions (0 through 3) with version-dependent
|
||||
// interpretation of inputs and outputs.
|
||||
type Transaction struct {
|
||||
// Version determines the transaction format and which consensus rules
|
||||
// apply to validation.
|
||||
Version uint8
|
||||
// Input variant tags (txin_v) — values from SET_VARIANT_TAGS in currency_basic.h.
|
||||
const (
|
||||
InputTypeGenesis uint8 = 0 // txin_gen (coinbase)
|
||||
InputTypeToKey uint8 = 1 // txin_to_key (standard spend)
|
||||
InputTypeMultisig uint8 = 2 // txin_multisig
|
||||
InputTypeHTLC uint8 = 34 // txin_htlc (0x22)
|
||||
InputTypeZC uint8 = 37 // txin_zc_input (0x25)
|
||||
)
|
||||
|
||||
// UnlockTime is the block height or Unix timestamp after which the
|
||||
// outputs of this transaction become spendable. A value of 0 means
|
||||
// immediately spendable (after the standard unlock window).
|
||||
UnlockTime uint64
|
||||
// Output variant tags (tx_out_v).
|
||||
const (
|
||||
OutputTypeBare uint8 = 36 // tx_out_bare (0x24)
|
||||
OutputTypeZarcanum uint8 = 38 // tx_out_zarcanum (0x26)
|
||||
)
|
||||
|
||||
// Output target variant tags (txout_target_v).
|
||||
const (
|
||||
TargetTypeToKey uint8 = 3 // txout_to_key (33-byte blob: key + mix_attr)
|
||||
TargetTypeMultisig uint8 = 4 // txout_multisig
|
||||
TargetTypeHTLC uint8 = 35 // txout_htlc (0x23)
|
||||
)
|
||||
|
||||
// Key offset variant tags (txout_ref_v).
|
||||
const (
|
||||
RefTypeGlobalIndex uint8 = 26 // uint64 varint (0x1A)
|
||||
RefTypeByID uint8 = 25 // ref_by_id {hash, varint} (0x19)
|
||||
)
|
||||
|
||||
// Signature variant tags (signature_v).
|
||||
const (
|
||||
SigTypeNLSAG uint8 = 42 // NLSAG_sig (0x2A)
|
||||
SigTypeZC uint8 = 43 // ZC_sig (0x2B)
|
||||
SigTypeVoid uint8 = 44 // void_sig (0x2C)
|
||||
SigTypeZarcanum uint8 = 45 // zarcanum_sig (0x2D)
|
||||
)
|
||||
|
||||
// Transaction represents a Lethean blockchain transaction. The wire format
|
||||
// differs between versions:
|
||||
//
|
||||
// v0/v1: version, vin, vout, extra, [signatures, attachment]
|
||||
// v2+: version, vin, extra, vout, [hardfork_id], [attachment, signatures, proofs]
|
||||
type Transaction struct {
|
||||
// Version determines the transaction format and consensus rules.
|
||||
// Encoded as varint on wire.
|
||||
Version uint64
|
||||
|
||||
// Vin contains all transaction inputs.
|
||||
Vin []TxInput
|
||||
|
|
@ -43,101 +70,104 @@ type Transaction struct {
|
|||
// Vout contains all transaction outputs.
|
||||
Vout []TxOutput
|
||||
|
||||
// Extra holds auxiliary data such as the transaction public key,
|
||||
// payment IDs, and other per-transaction metadata. The format is a
|
||||
// sequence of tagged TLV fields.
|
||||
// Extra holds the serialised variant vector of per-transaction metadata
|
||||
// (public key, payment IDs, unlock time, etc.). Stored as raw wire bytes
|
||||
// to enable bit-identical round-tripping.
|
||||
Extra []byte
|
||||
|
||||
// HardforkID identifies the hardfork version for v3+ transactions.
|
||||
// Only present on wire when Version >= VersionPostHF5.
|
||||
HardforkID uint8
|
||||
|
||||
// Signatures holds ring signatures for v0/v1 transactions.
|
||||
// Each element corresponds to one input; inner slice is the ring.
|
||||
Signatures [][]Signature
|
||||
|
||||
// Attachment holds the serialised variant vector of transaction attachments.
|
||||
// Stored as raw wire bytes.
|
||||
Attachment []byte
|
||||
|
||||
// Proofs holds the serialised variant vector of proofs (v2+ only).
|
||||
// Stored as raw wire bytes.
|
||||
Proofs []byte
|
||||
}
|
||||
|
||||
// TxInput is the interface implemented by all transaction input types.
|
||||
// Each concrete type corresponds to a different input variant in the
|
||||
// CryptoNote protocol.
|
||||
type TxInput interface {
|
||||
// InputType returns the wire type tag for this input variant.
|
||||
InputType() uint8
|
||||
}
|
||||
|
||||
// TxOutput is the interface implemented by all transaction output types.
|
||||
type TxOutput interface {
|
||||
// OutputType returns the wire type tag for this output variant.
|
||||
OutputType() uint8
|
||||
}
|
||||
|
||||
// Input type tags matching the C++ serialisation tags.
|
||||
const (
|
||||
InputTypeGenesis uint8 = 0xFF // txin_gen (coinbase)
|
||||
InputTypeToKey uint8 = 0x02 // txin_to_key (standard spend)
|
||||
)
|
||||
|
||||
// Output type tags.
|
||||
const (
|
||||
OutputTypeBare uint8 = 0x02 // tx_out_bare (transparent output)
|
||||
OutputTypeZarcanum uint8 = 0x03 // tx_out_zarcanum (confidential output)
|
||||
)
|
||||
|
||||
// TxInputGenesis is the coinbase input that appears in miner transactions.
|
||||
// It has no real input data; only the block height is recorded.
|
||||
type TxInputGenesis struct {
|
||||
// Height is the block height this coinbase transaction belongs to.
|
||||
Height uint64
|
||||
}
|
||||
|
||||
// InputType returns the wire type tag for genesis (coinbase) inputs.
|
||||
// InputType returns the wire variant tag for genesis inputs.
|
||||
func (t TxInputGenesis) InputType() uint8 { return InputTypeGenesis }
|
||||
|
||||
// TxOutToKey is the txout_to_key target variant. On the wire it is
|
||||
// serialised as a 33-byte packed blob: 32-byte public key + 1-byte mix_attr.
|
||||
type TxOutToKey struct {
|
||||
Key PublicKey
|
||||
MixAttr uint8
|
||||
}
|
||||
|
||||
// TxOutRef is one element of a txin_to_key key_offsets vector.
|
||||
// Each element is a variant: either a uint64 global index or a ref_by_id.
|
||||
type TxOutRef struct {
|
||||
Tag uint8 // RefTypeGlobalIndex or RefTypeByID
|
||||
GlobalIndex uint64 // valid when Tag == RefTypeGlobalIndex
|
||||
TxID Hash // valid when Tag == RefTypeByID
|
||||
N uint64 // valid when Tag == RefTypeByID
|
||||
}
|
||||
|
||||
// TxInputToKey is a standard input that spends a previously received output
|
||||
// by proving knowledge of the corresponding secret key via a ring signature.
|
||||
type TxInputToKey struct {
|
||||
// Amount is the input amount in atomic units. For pre-HF4 transparent
|
||||
// transactions this is the real amount; for HF4+ Zarcanum transactions
|
||||
// this is zero (amounts are hidden in Pedersen commitments).
|
||||
// Amount in atomic units. Zero for HF4+ Zarcanum transactions.
|
||||
Amount uint64
|
||||
|
||||
// KeyOffsets contains the relative offsets into the global output index
|
||||
// that form the decoy ring. The first offset is absolute; subsequent
|
||||
// offsets are relative to the previous one.
|
||||
KeyOffsets []uint64
|
||||
// KeyOffsets contains the output references forming the decoy ring.
|
||||
// Each element is a variant (global index or ref_by_id).
|
||||
KeyOffsets []TxOutRef
|
||||
|
||||
// KeyImage is the key image that prevents double-spending of this input.
|
||||
// KeyImage prevents double-spending of this input.
|
||||
KeyImage KeyImage
|
||||
|
||||
// EtcDetails holds the serialised variant vector of input-level details
|
||||
// (signed_parts, attachment_info). Stored as raw wire bytes.
|
||||
EtcDetails []byte
|
||||
}
|
||||
|
||||
// InputType returns the wire type tag for key inputs.
|
||||
// InputType returns the wire variant tag for key inputs.
|
||||
func (t TxInputToKey) InputType() uint8 { return InputTypeToKey }
|
||||
|
||||
// TxOutputBare is a transparent (pre-Zarcanum) transaction output.
|
||||
type TxOutputBare struct {
|
||||
// Amount is the output amount in atomic units.
|
||||
// Amount in atomic units.
|
||||
Amount uint64
|
||||
|
||||
// TargetKey is the one-time public key derived from the recipient's
|
||||
// address and the transaction secret key.
|
||||
TargetKey PublicKey
|
||||
// Target is the one-time output destination (key + mix attribute).
|
||||
Target TxOutToKey
|
||||
}
|
||||
|
||||
// OutputType returns the wire type tag for bare outputs.
|
||||
// OutputType returns the wire variant tag for bare outputs.
|
||||
func (t TxOutputBare) OutputType() uint8 { return OutputTypeBare }
|
||||
|
||||
// TxOutputZarcanum is a confidential (HF4+) transaction output where the
|
||||
// amount is hidden inside a Pedersen commitment.
|
||||
// TxOutputZarcanum is a confidential (HF4+) transaction output.
|
||||
type TxOutputZarcanum struct {
|
||||
// StealthAddress is the one-time stealth address for this output.
|
||||
StealthAddress PublicKey
|
||||
|
||||
// AmountCommitment is the Pedersen commitment to the output amount.
|
||||
AmountCommitment PublicKey
|
||||
|
||||
// ConcealingPoint is an additional point used in the Zarcanum protocol
|
||||
// for blinding.
|
||||
ConcealingPoint PublicKey
|
||||
|
||||
// EncryptedAmount is the amount encrypted with a key derived from the
|
||||
// shared secret between sender and recipient.
|
||||
EncryptedAmount [32]byte
|
||||
|
||||
// MixAttr encodes the minimum ring size and other mixing attributes.
|
||||
AmountCommitment PublicKey
|
||||
BlindedAssetID PublicKey
|
||||
EncryptedAmount uint64
|
||||
MixAttr uint8
|
||||
}
|
||||
|
||||
// OutputType returns the wire type tag for Zarcanum outputs.
|
||||
// OutputType returns the wire variant tag for Zarcanum outputs.
|
||||
func (t TxOutputZarcanum) OutputType() uint8 { return OutputTypeZarcanum }
|
||||
|
|
|
|||
64
wire/block.go
Normal file
64
wire/block.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// 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 "forge.lthn.ai/core/go-blockchain/types"
|
||||
|
||||
// EncodeBlockHeader serialises a block header in the consensus wire format.
|
||||
//
|
||||
// Wire order (currency_basic.h:1123-1131):
|
||||
//
|
||||
// major_version uint8 (1 byte)
|
||||
// nonce uint64 (8 bytes LE)
|
||||
// prev_id hash (32 bytes)
|
||||
// minor_version varint
|
||||
// timestamp varint
|
||||
// flags uint8 (1 byte)
|
||||
func EncodeBlockHeader(enc *Encoder, h *types.BlockHeader) {
|
||||
enc.WriteUint8(h.MajorVersion)
|
||||
enc.WriteUint64LE(h.Nonce)
|
||||
enc.WriteBlob32((*[32]byte)(&h.PrevID))
|
||||
enc.WriteVarint(h.MinorVersion)
|
||||
enc.WriteVarint(h.Timestamp)
|
||||
enc.WriteUint8(h.Flags)
|
||||
}
|
||||
|
||||
// DecodeBlockHeader deserialises a block header from the consensus wire format.
|
||||
func DecodeBlockHeader(dec *Decoder) types.BlockHeader {
|
||||
var h types.BlockHeader
|
||||
h.MajorVersion = dec.ReadUint8()
|
||||
h.Nonce = dec.ReadUint64LE()
|
||||
dec.ReadBlob32((*[32]byte)(&h.PrevID))
|
||||
h.MinorVersion = dec.ReadVarint()
|
||||
h.Timestamp = dec.ReadVarint()
|
||||
h.Flags = dec.ReadUint8()
|
||||
return h
|
||||
}
|
||||
|
||||
// EncodeBlock serialises a full block (header + miner tx + tx hashes).
|
||||
func EncodeBlock(enc *Encoder, b *types.Block) {
|
||||
EncodeBlockHeader(enc, &b.BlockHeader)
|
||||
EncodeTransaction(enc, &b.MinerTx)
|
||||
enc.WriteVarint(uint64(len(b.TxHashes)))
|
||||
for i := range b.TxHashes {
|
||||
enc.WriteBlob32((*[32]byte)(&b.TxHashes[i]))
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeBlock deserialises a full block.
|
||||
func DecodeBlock(dec *Decoder) types.Block {
|
||||
var b types.Block
|
||||
b.BlockHeader = DecodeBlockHeader(dec)
|
||||
b.MinerTx = DecodeTransaction(dec)
|
||||
n := dec.ReadVarint()
|
||||
if n > 0 && dec.Err() == nil {
|
||||
b.TxHashes = make([]types.Hash, n)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
dec.ReadBlob32((*[32]byte)(&b.TxHashes[i]))
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
178
wire/block_test.go
Normal file
178
wire/block_test.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
// 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"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
)
|
||||
|
||||
// testnetGenesisHeader returns the genesis block header for the Lethean testnet.
|
||||
func testnetGenesisHeader() types.BlockHeader {
|
||||
return types.BlockHeader{
|
||||
MajorVersion: 1,
|
||||
Nonce: 101011010221, // CURRENCY_FORMATION_VERSION(100) + 101011010121
|
||||
PrevID: types.Hash{}, // all zeros
|
||||
MinorVersion: 0,
|
||||
Timestamp: 1770897600, // 2026-02-12 12:00:00 UTC
|
||||
Flags: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeBlockHeader_Good(t *testing.T) {
|
||||
h := testnetGenesisHeader()
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeBlockHeader(enc, &h)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
data := buf.Bytes()
|
||||
|
||||
// Verify structure:
|
||||
// byte 0: major_version = 0x01
|
||||
// bytes 1-8: nonce LE
|
||||
// bytes 9-40: prev_id (32 zeros)
|
||||
// byte 41: minor_version varint = 0x00
|
||||
// bytes 42+: timestamp varint
|
||||
// last byte: flags = 0x00
|
||||
|
||||
if data[0] != 0x01 {
|
||||
t.Errorf("major_version: got 0x%02x, want 0x01", data[0])
|
||||
}
|
||||
if data[len(data)-1] != 0x00 {
|
||||
t.Errorf("flags: got 0x%02x, want 0x00", data[len(data)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockHeaderRoundTrip_Good(t *testing.T) {
|
||||
h := testnetGenesisHeader()
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeBlockHeader(enc, &h)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
got := DecodeBlockHeader(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
if got.MajorVersion != h.MajorVersion {
|
||||
t.Errorf("MajorVersion: got %d, want %d", got.MajorVersion, h.MajorVersion)
|
||||
}
|
||||
if got.Nonce != h.Nonce {
|
||||
t.Errorf("Nonce: got %d, want %d", got.Nonce, h.Nonce)
|
||||
}
|
||||
if got.PrevID != h.PrevID {
|
||||
t.Errorf("PrevID: got %x, want %x", got.PrevID, h.PrevID)
|
||||
}
|
||||
if got.MinorVersion != h.MinorVersion {
|
||||
t.Errorf("MinorVersion: got %d, want %d", got.MinorVersion, h.MinorVersion)
|
||||
}
|
||||
if got.Timestamp != h.Timestamp {
|
||||
t.Errorf("Timestamp: got %d, want %d", got.Timestamp, h.Timestamp)
|
||||
}
|
||||
if got.Flags != h.Flags {
|
||||
t.Errorf("Flags: got %d, want %d", got.Flags, h.Flags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockRoundTrip_Good(t *testing.T) {
|
||||
// Build the genesis block and round-trip it through EncodeBlock/DecodeBlock.
|
||||
rawTx := testnetGenesisRawTx()
|
||||
dec := NewDecoder(bytes.NewReader(rawTx))
|
||||
minerTx := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode miner tx: %v", dec.Err())
|
||||
}
|
||||
|
||||
block := types.Block{
|
||||
BlockHeader: testnetGenesisHeader(),
|
||||
MinerTx: minerTx,
|
||||
}
|
||||
|
||||
// Encode.
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeBlock(enc, &block)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
// Decode.
|
||||
dec2 := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
got := DecodeBlock(dec2)
|
||||
if dec2.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec2.Err())
|
||||
}
|
||||
|
||||
// Re-encode and compare bytes.
|
||||
var rtBuf bytes.Buffer
|
||||
enc2 := NewEncoder(&rtBuf)
|
||||
EncodeBlock(enc2, &got)
|
||||
if enc2.Err() != nil {
|
||||
t.Fatalf("re-encode error: %v", enc2.Err())
|
||||
}
|
||||
if !bytes.Equal(rtBuf.Bytes(), buf.Bytes()) {
|
||||
t.Errorf("block round-trip mismatch")
|
||||
}
|
||||
|
||||
// Verify block hash is unchanged after round-trip.
|
||||
if BlockHash(&got) != BlockHash(&block) {
|
||||
t.Errorf("block hash changed after round-trip")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockWithTxHashesRoundTrip_Good(t *testing.T) {
|
||||
block := types.Block{
|
||||
BlockHeader: testnetGenesisHeader(),
|
||||
MinerTx: types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
|
||||
Vout: []types.TxOutput{types.TxOutputBare{
|
||||
Amount: 1000,
|
||||
Target: types.TxOutToKey{Key: types.PublicKey{0xAA}},
|
||||
}},
|
||||
Extra: EncodeVarint(0),
|
||||
Attachment: EncodeVarint(0),
|
||||
},
|
||||
TxHashes: []types.Hash{
|
||||
{0x01, 0x02, 0x03},
|
||||
{0xDE, 0xAD, 0xBE, 0xEF},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeBlock(enc, &block)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
got := DecodeBlock(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
if len(got.TxHashes) != 2 {
|
||||
t.Fatalf("tx_hashes count: got %d, want 2", len(got.TxHashes))
|
||||
}
|
||||
if got.TxHashes[0] != block.TxHashes[0] {
|
||||
t.Errorf("tx_hashes[0]: got %x, want %x", got.TxHashes[0], block.TxHashes[0])
|
||||
}
|
||||
if got.TxHashes[1] != block.TxHashes[1] {
|
||||
t.Errorf("tx_hashes[1]: got %x, want %x", got.TxHashes[1], block.TxHashes[1])
|
||||
}
|
||||
}
|
||||
116
wire/decoder.go
Normal file
116
wire/decoder.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// 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 (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Decoder reads consensus-critical binary data from an io.Reader.
|
||||
// It uses the same sticky error pattern as Encoder.
|
||||
type Decoder struct {
|
||||
r io.Reader
|
||||
err error
|
||||
buf [10]byte
|
||||
}
|
||||
|
||||
// NewDecoder creates a new Decoder reading from r.
|
||||
func NewDecoder(r io.Reader) *Decoder {
|
||||
return &Decoder{r: r}
|
||||
}
|
||||
|
||||
// Err returns the first error encountered during decoding.
|
||||
func (d *Decoder) Err() error { return d.err }
|
||||
|
||||
// ReadUint8 reads a single byte.
|
||||
func (d *Decoder) ReadUint8() uint8 {
|
||||
if d.err != nil {
|
||||
return 0
|
||||
}
|
||||
_, d.err = io.ReadFull(d.r, d.buf[:1])
|
||||
return d.buf[0]
|
||||
}
|
||||
|
||||
// ReadUint64LE reads a uint64 in little-endian byte order.
|
||||
func (d *Decoder) ReadUint64LE() uint64 {
|
||||
if d.err != nil {
|
||||
return 0
|
||||
}
|
||||
_, d.err = io.ReadFull(d.r, d.buf[:8])
|
||||
if d.err != nil {
|
||||
return 0
|
||||
}
|
||||
return binary.LittleEndian.Uint64(d.buf[:8])
|
||||
}
|
||||
|
||||
// ReadVarint reads a CryptoNote varint (LEB128).
|
||||
func (d *Decoder) ReadVarint() uint64 {
|
||||
if d.err != nil {
|
||||
return 0
|
||||
}
|
||||
var val uint64
|
||||
var shift uint
|
||||
for i := 0; i < MaxVarintLen; i++ {
|
||||
_, d.err = io.ReadFull(d.r, d.buf[:1])
|
||||
if d.err != nil {
|
||||
return 0
|
||||
}
|
||||
b := d.buf[0]
|
||||
val |= uint64(b&0x7F) << shift
|
||||
if b&0x80 == 0 {
|
||||
return val
|
||||
}
|
||||
shift += 7
|
||||
}
|
||||
d.err = ErrVarintOverflow
|
||||
return 0
|
||||
}
|
||||
|
||||
// ReadBytes reads exactly n bytes as a raw blob.
|
||||
func (d *Decoder) ReadBytes(n int) []byte {
|
||||
if d.err != nil {
|
||||
return nil
|
||||
}
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
if n < 0 || n > MaxBlobSize {
|
||||
d.err = fmt.Errorf("wire: blob size %d exceeds maximum %d", n, MaxBlobSize)
|
||||
return nil
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
_, d.err = io.ReadFull(d.r, buf)
|
||||
if d.err != nil {
|
||||
return nil
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadBlob32 reads a 32-byte fixed-size blob into dst.
|
||||
func (d *Decoder) ReadBlob32(dst *[32]byte) {
|
||||
if d.err != nil {
|
||||
return
|
||||
}
|
||||
_, d.err = io.ReadFull(d.r, dst[:])
|
||||
}
|
||||
|
||||
// ReadBlob64 reads a 64-byte fixed-size blob into dst.
|
||||
func (d *Decoder) ReadBlob64(dst *[64]byte) {
|
||||
if d.err != nil {
|
||||
return
|
||||
}
|
||||
_, d.err = io.ReadFull(d.r, dst[:])
|
||||
}
|
||||
|
||||
// ReadVariantTag reads a single-byte variant discriminator.
|
||||
func (d *Decoder) ReadVariantTag() uint8 {
|
||||
return d.ReadUint8()
|
||||
}
|
||||
|
||||
// MaxBlobSize is the maximum byte count allowed for a single ReadBytes call.
|
||||
const MaxBlobSize = 50 * 1024 * 1024 // 50 MiB
|
||||
205
wire/decoder_test.go
Normal file
205
wire/decoder_test.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
func TestDecoderUint8_Good(t *testing.T) {
|
||||
dec := NewDecoder(bytes.NewReader([]byte{0x42}))
|
||||
got := dec.ReadUint8()
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", dec.Err())
|
||||
}
|
||||
if got != 0x42 {
|
||||
t.Errorf("got 0x%02x, want 0x42", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderUint64LE_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hex string
|
||||
want uint64
|
||||
}{
|
||||
{"zero", "0000000000000000", 0},
|
||||
{"one", "0100000000000000", 1},
|
||||
{"genesis_nonce", "adb2b98417000000", 101011010221},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b, _ := hex.DecodeString(tc.hex)
|
||||
dec := NewDecoder(bytes.NewReader(b))
|
||||
got := dec.ReadUint64LE()
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", dec.Err())
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("got %d, want %d", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderVarint_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hex string
|
||||
want uint64
|
||||
}{
|
||||
{"zero", "00", 0},
|
||||
{"one", "01", 1},
|
||||
{"127", "7f", 127},
|
||||
{"128", "8001", 128},
|
||||
{"300", "ac02", 300},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b, _ := hex.DecodeString(tc.hex)
|
||||
dec := NewDecoder(bytes.NewReader(b))
|
||||
got := dec.ReadVarint()
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", dec.Err())
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("got %d, want %d", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderBlob32_Good(t *testing.T) {
|
||||
var want [32]byte
|
||||
want[0] = 0xAB
|
||||
want[31] = 0xCD
|
||||
|
||||
dec := NewDecoder(bytes.NewReader(want[:]))
|
||||
var got [32]byte
|
||||
dec.ReadBlob32(&got)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", dec.Err())
|
||||
}
|
||||
if got != want {
|
||||
t.Error("blob32 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderBlob64_Good(t *testing.T) {
|
||||
var want [64]byte
|
||||
want[0] = 0x11
|
||||
want[63] = 0x99
|
||||
|
||||
dec := NewDecoder(bytes.NewReader(want[:]))
|
||||
var got [64]byte
|
||||
dec.ReadBlob64(&got)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", dec.Err())
|
||||
}
|
||||
if got != want {
|
||||
t.Error("blob64 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderStickyError_Bad(t *testing.T) {
|
||||
dec := NewDecoder(bytes.NewReader(nil))
|
||||
|
||||
got := dec.ReadUint8()
|
||||
if dec.Err() == nil {
|
||||
t.Fatal("expected error from empty reader")
|
||||
}
|
||||
if got != 0 {
|
||||
t.Errorf("got %d, want 0 on error", got)
|
||||
}
|
||||
|
||||
// Subsequent reads should be no-ops.
|
||||
_ = dec.ReadUint64LE()
|
||||
_ = dec.ReadVarint()
|
||||
var h [32]byte
|
||||
dec.ReadBlob32(&h)
|
||||
if h != ([32]byte{}) {
|
||||
t.Error("expected zero blob on sticky error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderReadBytes_Good(t *testing.T) {
|
||||
data := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
dec := NewDecoder(bytes.NewReader(data))
|
||||
got := dec.ReadBytes(4)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", dec.Err())
|
||||
}
|
||||
if !bytes.Equal(got, data) {
|
||||
t.Error("bytes mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderReadBytesZero_Good(t *testing.T) {
|
||||
dec := NewDecoder(bytes.NewReader(nil))
|
||||
got := dec.ReadBytes(0)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", dec.Err())
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderReadBytesOversize_Bad(t *testing.T) {
|
||||
dec := NewDecoder(bytes.NewReader(nil))
|
||||
_ = dec.ReadBytes(MaxBlobSize + 1)
|
||||
if dec.Err() == nil {
|
||||
t.Fatal("expected error for oversize blob")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderVarintOverflow_Ugly(t *testing.T) {
|
||||
data := make([]byte, 11)
|
||||
for i := range data {
|
||||
data[i] = 0x80
|
||||
}
|
||||
dec := NewDecoder(bytes.NewReader(data))
|
||||
_ = dec.ReadVarint()
|
||||
if dec.Err() == nil {
|
||||
t.Fatal("expected overflow error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderDecoderRoundTrip_Good(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
enc.WriteUint8(0x01)
|
||||
enc.WriteUint64LE(101011010221)
|
||||
enc.WriteVarint(1770897600)
|
||||
var h [32]byte
|
||||
h[0] = 0xDE
|
||||
h[31] = 0xAD
|
||||
enc.WriteBlob32(&h)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
if v := dec.ReadUint8(); v != 0x01 {
|
||||
t.Errorf("uint8: got 0x%02x, want 0x01", v)
|
||||
}
|
||||
if v := dec.ReadUint64LE(); v != 101011010221 {
|
||||
t.Errorf("uint64: got %d, want 101011010221", v)
|
||||
}
|
||||
if v := dec.ReadVarint(); v != 1770897600 {
|
||||
t.Errorf("varint: got %d, want 1770897600", v)
|
||||
}
|
||||
var gotH [32]byte
|
||||
dec.ReadBlob32(&gotH)
|
||||
if gotH != h {
|
||||
t.Error("blob32 mismatch")
|
||||
}
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
}
|
||||
88
wire/encoder.go
Normal file
88
wire/encoder.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// 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 (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Encoder writes consensus-critical binary data to an io.Writer.
|
||||
// It uses a sticky error pattern: after the first write error, all
|
||||
// subsequent writes become no-ops. Call Err() after a complete
|
||||
// encoding sequence to check for failures.
|
||||
type Encoder struct {
|
||||
w io.Writer
|
||||
err error
|
||||
buf [10]byte // scratch for LE integers and varints
|
||||
}
|
||||
|
||||
// NewEncoder creates a new Encoder writing to w.
|
||||
func NewEncoder(w io.Writer) *Encoder {
|
||||
return &Encoder{w: w}
|
||||
}
|
||||
|
||||
// Err returns the first error encountered during encoding.
|
||||
func (e *Encoder) Err() error { return e.err }
|
||||
|
||||
// WriteUint8 writes a single byte.
|
||||
func (e *Encoder) WriteUint8(v uint8) {
|
||||
if e.err != nil {
|
||||
return
|
||||
}
|
||||
e.buf[0] = v
|
||||
_, e.err = e.w.Write(e.buf[:1])
|
||||
}
|
||||
|
||||
// WriteUint64LE writes a uint64 in little-endian byte order.
|
||||
func (e *Encoder) WriteUint64LE(v uint64) {
|
||||
if e.err != nil {
|
||||
return
|
||||
}
|
||||
binary.LittleEndian.PutUint64(e.buf[:8], v)
|
||||
_, e.err = e.w.Write(e.buf[:8])
|
||||
}
|
||||
|
||||
// WriteVarint writes a uint64 as a CryptoNote varint (LEB128).
|
||||
func (e *Encoder) WriteVarint(v uint64) {
|
||||
if e.err != nil {
|
||||
return
|
||||
}
|
||||
b := EncodeVarint(v)
|
||||
_, e.err = e.w.Write(b)
|
||||
}
|
||||
|
||||
// WriteBytes writes raw bytes with no length prefix.
|
||||
func (e *Encoder) WriteBytes(b []byte) {
|
||||
if e.err != nil {
|
||||
return
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return
|
||||
}
|
||||
_, e.err = e.w.Write(b)
|
||||
}
|
||||
|
||||
// WriteBlob32 writes a 32-byte fixed-size blob (hash, public key, key image).
|
||||
func (e *Encoder) WriteBlob32(b *[32]byte) {
|
||||
if e.err != nil {
|
||||
return
|
||||
}
|
||||
_, e.err = e.w.Write(b[:])
|
||||
}
|
||||
|
||||
// WriteBlob64 writes a 64-byte fixed-size blob (signature).
|
||||
func (e *Encoder) WriteBlob64(b *[64]byte) {
|
||||
if e.err != nil {
|
||||
return
|
||||
}
|
||||
_, e.err = e.w.Write(b[:])
|
||||
}
|
||||
|
||||
// WriteVariantTag writes a single-byte variant discriminator.
|
||||
func (e *Encoder) WriteVariantTag(tag uint8) {
|
||||
e.WriteUint8(tag)
|
||||
}
|
||||
185
wire/encoder_test.go
Normal file
185
wire/encoder_test.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// 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"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncoderUint8_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
val uint8
|
||||
want string
|
||||
}{
|
||||
{"zero", 0, "00"},
|
||||
{"one", 1, "01"},
|
||||
{"max", 255, "ff"},
|
||||
{"mid", 0x42, "42"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
enc.WriteUint8(tc.val)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", enc.Err())
|
||||
}
|
||||
got := hex.EncodeToString(buf.Bytes())
|
||||
if got != tc.want {
|
||||
t.Errorf("got %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderUint64LE_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
val uint64
|
||||
want string
|
||||
}{
|
||||
{"zero", 0, "0000000000000000"},
|
||||
{"one", 1, "0100000000000000"},
|
||||
{"max", ^uint64(0), "ffffffffffffffff"},
|
||||
{"genesis_nonce", 101011010221, "adb2b98417000000"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
enc.WriteUint64LE(tc.val)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", enc.Err())
|
||||
}
|
||||
got := hex.EncodeToString(buf.Bytes())
|
||||
if got != tc.want {
|
||||
t.Errorf("got %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderVarint_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
val uint64
|
||||
want string
|
||||
}{
|
||||
{"zero", 0, "00"},
|
||||
{"one", 1, "01"},
|
||||
{"127", 127, "7f"},
|
||||
{"128", 128, "8001"},
|
||||
{"300", 300, "ac02"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
enc.WriteVarint(tc.val)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", enc.Err())
|
||||
}
|
||||
got := hex.EncodeToString(buf.Bytes())
|
||||
if got != tc.want {
|
||||
t.Errorf("got %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderBlob32_Good(t *testing.T) {
|
||||
var h [32]byte
|
||||
h[0] = 0xAB
|
||||
h[31] = 0xCD
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
enc.WriteBlob32(&h)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", enc.Err())
|
||||
}
|
||||
if buf.Len() != 32 {
|
||||
t.Fatalf("got %d bytes, want 32", buf.Len())
|
||||
}
|
||||
if buf.Bytes()[0] != 0xAB || buf.Bytes()[31] != 0xCD {
|
||||
t.Errorf("blob32 bytes mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderBlob64_Good(t *testing.T) {
|
||||
var s [64]byte
|
||||
s[0] = 0x11
|
||||
s[63] = 0x99
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
enc.WriteBlob64(&s)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", enc.Err())
|
||||
}
|
||||
if buf.Len() != 64 {
|
||||
t.Fatalf("got %d bytes, want 64", buf.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderStickyError_Bad(t *testing.T) {
|
||||
w := &failWriter{failAfter: 1}
|
||||
enc := NewEncoder(w)
|
||||
|
||||
enc.WriteUint8(0x01) // succeeds
|
||||
enc.WriteUint8(0x02) // fails
|
||||
enc.WriteUint8(0x03) // should be no-op
|
||||
|
||||
if enc.Err() == nil {
|
||||
t.Fatal("expected error after failed write")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderEmptyBytes_Good(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
enc.WriteBytes(nil)
|
||||
enc.WriteBytes([]byte{})
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", enc.Err())
|
||||
}
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("got %d bytes, want 0", buf.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderSequence_Good(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
enc.WriteUint8(0x01)
|
||||
enc.WriteUint64LE(42)
|
||||
enc.WriteVarint(300)
|
||||
enc.WriteVariantTag(0x24)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("unexpected error: %v", enc.Err())
|
||||
}
|
||||
if buf.Len() != 12 {
|
||||
t.Errorf("got %d bytes, want 12", buf.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// failWriter fails after writing failAfter bytes.
|
||||
type failWriter struct {
|
||||
written int
|
||||
failAfter int
|
||||
}
|
||||
|
||||
func (fw *failWriter) Write(p []byte) (int, error) {
|
||||
if fw.written+len(p) > fw.failAfter {
|
||||
return 0, errors.New("write failed")
|
||||
}
|
||||
fw.written += len(p)
|
||||
return len(p), nil
|
||||
}
|
||||
75
wire/hash.go
Normal file
75
wire/hash.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// 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"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
)
|
||||
|
||||
// BlockHashingBlob builds the blob used to compute a block's hash.
|
||||
//
|
||||
// The format (from currency_format_utils_blocks.cpp) is:
|
||||
//
|
||||
// serialised_block_header || tree_root_hash || varint(tx_count)
|
||||
//
|
||||
// where tx_count = 1 (miner_tx) + len(tx_hashes).
|
||||
func BlockHashingBlob(b *types.Block) []byte {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeBlockHeader(enc, &b.BlockHeader)
|
||||
|
||||
// Compute tree hash over all transaction hashes.
|
||||
txCount := 1 + len(b.TxHashes)
|
||||
hashes := make([][32]byte, txCount)
|
||||
hashes[0] = [32]byte(TransactionPrefixHash(&b.MinerTx))
|
||||
for i, h := range b.TxHashes {
|
||||
hashes[i+1] = [32]byte(h)
|
||||
}
|
||||
treeRoot := TreeHash(hashes)
|
||||
|
||||
buf.Write(treeRoot[:])
|
||||
buf.Write(EncodeVarint(uint64(txCount)))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// BlockHash computes the block ID (Keccak-256 of the block hashing blob).
|
||||
//
|
||||
// The C++ code calls get_object_hash(blobdata) which serialises the string
|
||||
// through binary_archive before hashing. For std::string this prepends a
|
||||
// varint length prefix, so the actual hash input is:
|
||||
//
|
||||
// varint(len(blob)) || blob
|
||||
func BlockHash(b *types.Block) types.Hash {
|
||||
blob := BlockHashingBlob(b)
|
||||
var prefixed []byte
|
||||
prefixed = append(prefixed, EncodeVarint(uint64(len(blob)))...)
|
||||
prefixed = append(prefixed, blob...)
|
||||
return types.Hash(Keccak256(prefixed))
|
||||
}
|
||||
|
||||
// TransactionHash computes the full 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).
|
||||
func TransactionHash(tx *types.Transaction) types.Hash {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransaction(enc, tx)
|
||||
return types.Hash(Keccak256(buf.Bytes()))
|
||||
}
|
||||
|
||||
// TransactionPrefixHash computes the hash of a transaction prefix.
|
||||
// This is Keccak-256 of the serialised transaction prefix (version + vin +
|
||||
// vout + extra, in version-dependent order).
|
||||
func TransactionPrefixHash(tx *types.Transaction) types.Hash {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransactionPrefix(enc, tx)
|
||||
return types.Hash(Keccak256(buf.Bytes()))
|
||||
}
|
||||
131
wire/hash_test.go
Normal file
131
wire/hash_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// 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/binary"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
)
|
||||
|
||||
// testnetGenesisRawTx returns the raw wire bytes of the testnet genesis
|
||||
// coinbase transaction, constructed from the packed C struct in
|
||||
// genesis/_genesis_tn.cpp.gen.
|
||||
func testnetGenesisRawTx() []byte {
|
||||
u64s := [25]uint64{
|
||||
0xa080800100000101, 0x03018ae3c8e0c8cf, 0x7b0287d2a2218485, 0x720c5b385edbe3dd,
|
||||
0x178e7c64d18a598f, 0x98bb613ff63e6d03, 0x3814f971f9160500, 0x1c595f65f55d872e,
|
||||
0x835e5fd926b1f78d, 0xf597c7f5a33b6131, 0x2074496b139c8341, 0x64612073656b6174,
|
||||
0x20656761746e6176, 0x6e2065687420666f, 0x666f206572757461, 0x616d726f666e6920,
|
||||
0x696562206e6f6974, 0x207973616520676e, 0x6165727073206f74, 0x6168207475622064,
|
||||
0x7473206f74206472, 0x202d202e656c6669, 0x206968736f746153, 0x6f746f6d616b614e,
|
||||
0x0a0e0d66020b0015,
|
||||
}
|
||||
u8s := [2]uint8{0x00, 0x00}
|
||||
|
||||
buf := make([]byte, 25*8+2)
|
||||
for i, v := range u64s {
|
||||
binary.LittleEndian.PutUint64(buf[i*8:], v)
|
||||
}
|
||||
buf[200] = u8s[0]
|
||||
buf[201] = u8s[1]
|
||||
return buf
|
||||
}
|
||||
|
||||
// TestGenesisBlockHash_Good is the definitive correctness test for wire
|
||||
// serialisation. It constructs the testnet genesis block, computes its
|
||||
// hash, and verifies it matches the hash returned by the C++ daemon.
|
||||
//
|
||||
// If this test passes, the block header serialisation, transaction prefix
|
||||
// serialisation, tree hash, and Keccak-256 implementation are all
|
||||
// bit-identical to the C++ reference.
|
||||
func TestGenesisBlockHash_Good(t *testing.T) {
|
||||
wantHash := "cb9d5455ccb79451931003672c405f5e2ac51bff54021aa30bc4499b1ffc4963"
|
||||
|
||||
// Parse the raw genesis coinbase transaction.
|
||||
rawTx := testnetGenesisRawTx()
|
||||
dec := NewDecoder(bytes.NewReader(rawTx))
|
||||
minerTx := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("failed to decode genesis miner tx: %v", dec.Err())
|
||||
}
|
||||
|
||||
// Verify basic transaction structure.
|
||||
if minerTx.Version != 1 {
|
||||
t.Fatalf("miner tx version: got %d, want 1", minerTx.Version)
|
||||
}
|
||||
if len(minerTx.Vin) != 1 {
|
||||
t.Fatalf("miner tx vin count: got %d, want 1", len(minerTx.Vin))
|
||||
}
|
||||
gen, ok := minerTx.Vin[0].(types.TxInputGenesis)
|
||||
if !ok {
|
||||
t.Fatalf("miner tx vin[0]: got %T, want TxInputGenesis", minerTx.Vin[0])
|
||||
}
|
||||
if gen.Height != 0 {
|
||||
t.Fatalf("miner tx genesis height: got %d, want 0", gen.Height)
|
||||
}
|
||||
if len(minerTx.Vout) != 1 {
|
||||
t.Fatalf("miner tx vout count: got %d, want 1", len(minerTx.Vout))
|
||||
}
|
||||
|
||||
// Verify round-trip: re-encode and compare to original bytes.
|
||||
var rtBuf bytes.Buffer
|
||||
enc := NewEncoder(&rtBuf)
|
||||
EncodeTransaction(enc, &minerTx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("re-encode error: %v", enc.Err())
|
||||
}
|
||||
if !bytes.Equal(rtBuf.Bytes(), rawTx) {
|
||||
t.Fatalf("round-trip mismatch:\n got: %x\n want: %x", rtBuf.Bytes(), rawTx)
|
||||
}
|
||||
|
||||
// Construct the genesis block.
|
||||
block := types.Block{
|
||||
BlockHeader: testnetGenesisHeader(),
|
||||
MinerTx: minerTx,
|
||||
TxHashes: nil, // genesis has no other transactions
|
||||
}
|
||||
|
||||
// Compute and verify the block hash.
|
||||
gotHash := BlockHash(&block)
|
||||
if hex.EncodeToString(gotHash[:]) != wantHash {
|
||||
t.Errorf("genesis block hash:\n got: %x\n want: %s", gotHash, wantHash)
|
||||
|
||||
// Debug: dump intermediate values.
|
||||
prefixHash := TransactionPrefixHash(&block.MinerTx)
|
||||
t.Logf("miner tx prefix hash: %x", prefixHash)
|
||||
|
||||
blob := BlockHashingBlob(&block)
|
||||
t.Logf("block hashing blob (%d bytes): %x", len(blob), blob)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionPrefixHashRoundTrip_Good(t *testing.T) {
|
||||
rawTx := testnetGenesisRawTx()
|
||||
|
||||
// Decode.
|
||||
dec := NewDecoder(bytes.NewReader(rawTx))
|
||||
tx := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
// Prefix hash should be deterministic.
|
||||
h1 := TransactionPrefixHash(&tx)
|
||||
h2 := TransactionPrefixHash(&tx)
|
||||
if h1 != h2 {
|
||||
t.Error("prefix hash not deterministic")
|
||||
}
|
||||
|
||||
// The prefix hash should equal Keccak-256 of the prefix bytes (first 200 bytes).
|
||||
wantPrefixHash := Keccak256(rawTx[:200])
|
||||
if types.Hash(wantPrefixHash) != h1 {
|
||||
t.Errorf("prefix hash mismatch:\n got: %x\n want: %x", h1, wantPrefixHash)
|
||||
}
|
||||
}
|
||||
607
wire/transaction.go
Normal file
607
wire/transaction.go
Normal file
|
|
@ -0,0 +1,607 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
)
|
||||
|
||||
// EncodeTransactionPrefix serialises the transaction prefix (without
|
||||
// signatures, attachment, or proofs) in the consensus wire format.
|
||||
//
|
||||
// The version field determines the field ordering:
|
||||
//
|
||||
// v0/v1: version, vin, vout, extra
|
||||
// v2+: version, vin, extra, vout, [hardfork_id]
|
||||
func EncodeTransactionPrefix(enc *Encoder, tx *types.Transaction) {
|
||||
enc.WriteVarint(tx.Version)
|
||||
if tx.Version <= types.VersionPreHF4 {
|
||||
encodePrefixV1(enc, tx)
|
||||
} else {
|
||||
encodePrefixV2(enc, tx)
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeTransaction serialises a full transaction including suffix fields.
|
||||
func EncodeTransaction(enc *Encoder, tx *types.Transaction) {
|
||||
EncodeTransactionPrefix(enc, tx)
|
||||
if tx.Version <= types.VersionPreHF4 {
|
||||
encodeSuffixV1(enc, tx)
|
||||
} else {
|
||||
encodeSuffixV2(enc, tx)
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeTransactionPrefix deserialises a transaction prefix.
|
||||
func DecodeTransactionPrefix(dec *Decoder) types.Transaction {
|
||||
var tx types.Transaction
|
||||
tx.Version = dec.ReadVarint()
|
||||
if dec.Err() != nil {
|
||||
return tx
|
||||
}
|
||||
if tx.Version <= types.VersionPreHF4 {
|
||||
decodePrefixV1(dec, &tx)
|
||||
} else {
|
||||
decodePrefixV2(dec, &tx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
// DecodeTransaction deserialises a full transaction.
|
||||
func DecodeTransaction(dec *Decoder) types.Transaction {
|
||||
tx := DecodeTransactionPrefix(dec)
|
||||
if dec.Err() != nil {
|
||||
return tx
|
||||
}
|
||||
if tx.Version <= types.VersionPreHF4 {
|
||||
decodeSuffixV1(dec, &tx)
|
||||
} else {
|
||||
decodeSuffixV2(dec, &tx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
// --- v0/v1 prefix ---
|
||||
|
||||
func encodePrefixV1(enc *Encoder, tx *types.Transaction) {
|
||||
encodeInputs(enc, tx.Vin)
|
||||
encodeOutputsV1(enc, tx.Vout)
|
||||
enc.WriteBytes(tx.Extra) // raw wire bytes including varint count prefix
|
||||
}
|
||||
|
||||
func decodePrefixV1(dec *Decoder, tx *types.Transaction) {
|
||||
tx.Vin = decodeInputs(dec)
|
||||
tx.Vout = decodeOutputsV1(dec)
|
||||
tx.Extra = decodeRawVariantVector(dec)
|
||||
}
|
||||
|
||||
// --- v2+ prefix ---
|
||||
|
||||
func encodePrefixV2(enc *Encoder, tx *types.Transaction) {
|
||||
encodeInputs(enc, tx.Vin)
|
||||
enc.WriteBytes(tx.Extra)
|
||||
encodeOutputsV2(enc, tx.Vout)
|
||||
if tx.Version >= types.VersionPostHF5 {
|
||||
enc.WriteUint8(tx.HardforkID)
|
||||
}
|
||||
}
|
||||
|
||||
func decodePrefixV2(dec *Decoder, tx *types.Transaction) {
|
||||
tx.Vin = decodeInputs(dec)
|
||||
tx.Extra = decodeRawVariantVector(dec)
|
||||
tx.Vout = decodeOutputsV2(dec)
|
||||
if tx.Version >= types.VersionPostHF5 {
|
||||
tx.HardforkID = dec.ReadUint8()
|
||||
}
|
||||
}
|
||||
|
||||
// --- v0/v1 suffix (signatures + attachment) ---
|
||||
|
||||
func encodeSuffixV1(enc *Encoder, tx *types.Transaction) {
|
||||
enc.WriteVarint(uint64(len(tx.Signatures)))
|
||||
for _, ring := range tx.Signatures {
|
||||
enc.WriteVarint(uint64(len(ring)))
|
||||
for i := range ring {
|
||||
enc.WriteBlob64((*[64]byte)(&ring[i]))
|
||||
}
|
||||
}
|
||||
enc.WriteBytes(tx.Attachment)
|
||||
}
|
||||
|
||||
func decodeSuffixV1(dec *Decoder, tx *types.Transaction) {
|
||||
sigCount := dec.ReadVarint()
|
||||
if sigCount > 0 && dec.Err() == nil {
|
||||
tx.Signatures = make([][]types.Signature, sigCount)
|
||||
for i := uint64(0); i < sigCount; i++ {
|
||||
ringSize := dec.ReadVarint()
|
||||
if ringSize > 0 && dec.Err() == nil {
|
||||
tx.Signatures[i] = make([]types.Signature, ringSize)
|
||||
for j := uint64(0); j < ringSize; j++ {
|
||||
dec.ReadBlob64((*[64]byte)(&tx.Signatures[i][j]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tx.Attachment = decodeRawVariantVector(dec)
|
||||
}
|
||||
|
||||
// --- v2+ suffix (attachment + signatures_raw + 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.Proofs)
|
||||
}
|
||||
|
||||
func decodeSuffixV2(dec *Decoder, tx *types.Transaction) {
|
||||
tx.Attachment = decodeRawVariantVector(dec)
|
||||
tx.Proofs = decodeRawVariantVector(dec)
|
||||
}
|
||||
|
||||
// --- inputs ---
|
||||
|
||||
func encodeInputs(enc *Encoder, vin []types.TxInput) {
|
||||
enc.WriteVarint(uint64(len(vin)))
|
||||
for _, in := range vin {
|
||||
enc.WriteVariantTag(in.InputType())
|
||||
switch v := in.(type) {
|
||||
case types.TxInputGenesis:
|
||||
enc.WriteVarint(v.Height)
|
||||
case types.TxInputToKey:
|
||||
enc.WriteVarint(v.Amount)
|
||||
encodeKeyOffsets(enc, v.KeyOffsets)
|
||||
enc.WriteBlob32((*[32]byte)(&v.KeyImage))
|
||||
enc.WriteBytes(v.EtcDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeInputs(dec *Decoder) []types.TxInput {
|
||||
n := dec.ReadVarint()
|
||||
if n == 0 || dec.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
vin := make([]types.TxInput, 0, n)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
tag := dec.ReadVariantTag()
|
||||
if dec.Err() != nil {
|
||||
return vin
|
||||
}
|
||||
switch tag {
|
||||
case types.InputTypeGenesis:
|
||||
vin = append(vin, types.TxInputGenesis{Height: dec.ReadVarint()})
|
||||
case types.InputTypeToKey:
|
||||
var in types.TxInputToKey
|
||||
in.Amount = dec.ReadVarint()
|
||||
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
|
||||
}
|
||||
}
|
||||
return vin
|
||||
}
|
||||
|
||||
// --- key offsets (txout_ref_v variant vector) ---
|
||||
|
||||
func encodeKeyOffsets(enc *Encoder, refs []types.TxOutRef) {
|
||||
enc.WriteVarint(uint64(len(refs)))
|
||||
for _, ref := range refs {
|
||||
enc.WriteVariantTag(ref.Tag)
|
||||
switch ref.Tag {
|
||||
case types.RefTypeGlobalIndex:
|
||||
enc.WriteVarint(ref.GlobalIndex)
|
||||
case types.RefTypeByID:
|
||||
enc.WriteBlob32((*[32]byte)(&ref.TxID))
|
||||
enc.WriteVarint(ref.N)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeKeyOffsets(dec *Decoder) []types.TxOutRef {
|
||||
n := dec.ReadVarint()
|
||||
if n == 0 || dec.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
refs := make([]types.TxOutRef, n)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
refs[i].Tag = dec.ReadVariantTag()
|
||||
switch refs[i].Tag {
|
||||
case types.RefTypeGlobalIndex:
|
||||
refs[i].GlobalIndex = dec.ReadVarint()
|
||||
case types.RefTypeByID:
|
||||
dec.ReadBlob32((*[32]byte)(&refs[i].TxID))
|
||||
refs[i].N = dec.ReadVarint()
|
||||
default:
|
||||
dec.err = fmt.Errorf("wire: unsupported ref tag 0x%02x", refs[i].Tag)
|
||||
return refs
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
// --- outputs ---
|
||||
|
||||
// encodeOutputsV1 serialises v0/v1 outputs. In v0/v1, outputs are tx_out_bare
|
||||
// directly without an outer tx_out_v variant tag.
|
||||
func encodeOutputsV1(enc *Encoder, vout []types.TxOutput) {
|
||||
enc.WriteVarint(uint64(len(vout)))
|
||||
for _, out := range vout {
|
||||
switch v := out.(type) {
|
||||
case types.TxOutputBare:
|
||||
enc.WriteVarint(v.Amount)
|
||||
// Target is a variant (txout_target_v)
|
||||
enc.WriteVariantTag(types.TargetTypeToKey)
|
||||
enc.WriteBlob32((*[32]byte)(&v.Target.Key))
|
||||
enc.WriteUint8(v.Target.MixAttr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeOutputsV1(dec *Decoder) []types.TxOutput {
|
||||
n := dec.ReadVarint()
|
||||
if n == 0 || dec.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
vout := make([]types.TxOutput, 0, n)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
var out types.TxOutputBare
|
||||
out.Amount = dec.ReadVarint()
|
||||
tag := dec.ReadVariantTag()
|
||||
if dec.Err() != nil {
|
||||
return vout
|
||||
}
|
||||
switch tag {
|
||||
case types.TargetTypeToKey:
|
||||
dec.ReadBlob32((*[32]byte)(&out.Target.Key))
|
||||
out.Target.MixAttr = dec.ReadUint8()
|
||||
default:
|
||||
dec.err = fmt.Errorf("wire: unsupported target tag 0x%02x", tag)
|
||||
return vout
|
||||
}
|
||||
vout = append(vout, out)
|
||||
}
|
||||
return vout
|
||||
}
|
||||
|
||||
// encodeOutputsV2 serialises v2+ outputs with outer tx_out_v variant tags.
|
||||
func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) {
|
||||
enc.WriteVarint(uint64(len(vout)))
|
||||
for _, out := range vout {
|
||||
enc.WriteVariantTag(out.OutputType())
|
||||
switch v := out.(type) {
|
||||
case types.TxOutputBare:
|
||||
enc.WriteVarint(v.Amount)
|
||||
enc.WriteVariantTag(types.TargetTypeToKey)
|
||||
enc.WriteBlob32((*[32]byte)(&v.Target.Key))
|
||||
enc.WriteUint8(v.Target.MixAttr)
|
||||
case types.TxOutputZarcanum:
|
||||
enc.WriteBlob32((*[32]byte)(&v.StealthAddress))
|
||||
enc.WriteBlob32((*[32]byte)(&v.ConcealingPoint))
|
||||
enc.WriteBlob32((*[32]byte)(&v.AmountCommitment))
|
||||
enc.WriteBlob32((*[32]byte)(&v.BlindedAssetID))
|
||||
enc.WriteUint64LE(v.EncryptedAmount)
|
||||
enc.WriteUint8(v.MixAttr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeOutputsV2(dec *Decoder) []types.TxOutput {
|
||||
n := dec.ReadVarint()
|
||||
if n == 0 || dec.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
vout := make([]types.TxOutput, 0, n)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
tag := dec.ReadVariantTag()
|
||||
if dec.Err() != nil {
|
||||
return vout
|
||||
}
|
||||
switch tag {
|
||||
case types.OutputTypeBare:
|
||||
var out types.TxOutputBare
|
||||
out.Amount = dec.ReadVarint()
|
||||
targetTag := dec.ReadVariantTag()
|
||||
if targetTag == types.TargetTypeToKey {
|
||||
dec.ReadBlob32((*[32]byte)(&out.Target.Key))
|
||||
out.Target.MixAttr = dec.ReadUint8()
|
||||
} else {
|
||||
dec.err = fmt.Errorf("wire: unsupported target tag 0x%02x", targetTag)
|
||||
return vout
|
||||
}
|
||||
vout = append(vout, out)
|
||||
case types.OutputTypeZarcanum:
|
||||
var out types.TxOutputZarcanum
|
||||
dec.ReadBlob32((*[32]byte)(&out.StealthAddress))
|
||||
dec.ReadBlob32((*[32]byte)(&out.ConcealingPoint))
|
||||
dec.ReadBlob32((*[32]byte)(&out.AmountCommitment))
|
||||
dec.ReadBlob32((*[32]byte)(&out.BlindedAssetID))
|
||||
out.EncryptedAmount = dec.ReadUint64LE()
|
||||
out.MixAttr = dec.ReadUint8()
|
||||
vout = append(vout, out)
|
||||
default:
|
||||
dec.err = fmt.Errorf("wire: unsupported output tag 0x%02x", tag)
|
||||
return vout
|
||||
}
|
||||
}
|
||||
return vout
|
||||
}
|
||||
|
||||
// --- raw variant vector encoding ---
|
||||
// These helpers handle variant vectors stored as opaque raw bytes.
|
||||
// The raw bytes include the varint count prefix and all tagged elements.
|
||||
|
||||
// decodeRawVariantVector reads a variant vector from the decoder and returns
|
||||
// the raw wire bytes (including the varint count prefix). This enables
|
||||
// bit-identical round-tripping of extra, attachment, etc_details, and proofs
|
||||
// without needing to understand every variant type.
|
||||
//
|
||||
// For each element, the tag byte determines how to find the element boundary.
|
||||
// Known fixed-size tags are skipped by size; unknown tags cause an error.
|
||||
func decodeRawVariantVector(dec *Decoder) []byte {
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read the count and capture the varint bytes.
|
||||
count := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
if count == 0 {
|
||||
return EncodeVarint(0) // just the count prefix
|
||||
}
|
||||
|
||||
// Build the raw bytes: start with the count varint.
|
||||
raw := EncodeVarint(count)
|
||||
|
||||
for i := uint64(0); i < count; i++ {
|
||||
tag := dec.ReadUint8()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, tag)
|
||||
|
||||
data := readVariantElementData(dec, tag)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, data...)
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
|
||||
// Variant element tags from SET_VARIANT_TAGS (currency_basic.h:1249-1322).
|
||||
// These are used by readVariantElementData to determine element boundaries
|
||||
// when reading raw variant vectors (extra, attachment, etc_details).
|
||||
const (
|
||||
tagTxComment = 7 // tx_comment — string
|
||||
tagTxPayerOld = 8 // tx_payer_old — 2 public keys
|
||||
tagString = 9 // std::string — string
|
||||
tagTxCryptoChecksum = 10 // tx_crypto_checksum — two uint32 LE
|
||||
tagTxDerivationHint = 11 // tx_derivation_hint — string
|
||||
tagTxServiceAttachment = 12 // tx_service_attachment — 3 strings + vector<key> + uint8
|
||||
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
|
||||
tagExtraAttachmentInfo = 18 // extra_attachment_info — string + hash + varint
|
||||
tagExtraUserData = 19 // extra_user_data — string
|
||||
tagExtraAliasEntryOld = 20 // extra_alias_entry_old — complex
|
||||
tagExtraPadding = 21 // extra_padding — vector<uint8>
|
||||
tagPublicKey = 22 // crypto::public_key — 32 bytes
|
||||
tagEtcTxFlags16 = 23 // etc_tx_flags16_t — uint16 LE
|
||||
tagUint16 = 24 // uint16_t — uint16 LE
|
||||
tagUint64 = 26 // uint64_t — varint
|
||||
tagEtcTxTime = 27 // etc_tx_time — varint
|
||||
tagUint32 = 28 // uint32_t — uint32 LE
|
||||
tagTxReceiverOld = 29 // tx_receiver_old — 2 public keys
|
||||
tagUnlockTime2 = 30 // etc_tx_details_unlock_time2 — vector of entries
|
||||
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
|
||||
)
|
||||
|
||||
// readVariantElementData reads the data portion of a variant element (after the
|
||||
// tag byte) and returns the raw bytes. The tag determines the expected size.
|
||||
func readVariantElementData(dec *Decoder, tag uint8) []byte {
|
||||
switch tag {
|
||||
// 32-byte fixed blob
|
||||
case tagPublicKey:
|
||||
return dec.ReadBytes(32)
|
||||
|
||||
// String fields (varint length + bytes)
|
||||
case tagTxComment, tagString, tagTxDerivationHint, tagExtraUserData:
|
||||
return readStringBlob(dec)
|
||||
|
||||
// Varint fields
|
||||
case tagUnlockTime, tagExpirationTime, tagTxDetailsFlags, tagUint64, tagEtcTxTime:
|
||||
v := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
return EncodeVarint(v)
|
||||
|
||||
// Fixed-size integer fields
|
||||
case tagTxCryptoChecksum: // two uint32 LE
|
||||
return dec.ReadBytes(8)
|
||||
case tagSignedParts, tagUint32: // uint32 LE
|
||||
return dec.ReadBytes(4)
|
||||
case tagEtcTxFlags16, tagUint16: // uint16 LE
|
||||
return dec.ReadBytes(2)
|
||||
|
||||
// Vector of uint8 (varint count + bytes)
|
||||
case tagExtraPadding:
|
||||
return readVariantVectorFixed(dec, 1)
|
||||
|
||||
// Struct types: 2 public keys (64 bytes)
|
||||
case tagTxPayerOld, tagTxReceiverOld:
|
||||
return dec.ReadBytes(64)
|
||||
|
||||
// Struct types: 2 public keys + optional flag
|
||||
case tagTxPayer, tagTxReceiver:
|
||||
return readTxPayer(dec)
|
||||
|
||||
// Composite types
|
||||
case tagExtraAttachmentInfo:
|
||||
return readExtraAttachmentInfo(dec)
|
||||
case tagUnlockTime2:
|
||||
return readUnlockTime2(dec)
|
||||
case tagTxServiceAttachment:
|
||||
return readTxServiceAttachment(dec)
|
||||
|
||||
default:
|
||||
dec.err = fmt.Errorf("wire: unsupported variant tag 0x%02x (%d)", tag, tag)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// readStringBlob reads a varint-prefixed string and returns the raw bytes
|
||||
// including the length varint.
|
||||
func readStringBlob(dec *Decoder) []byte {
|
||||
length := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := EncodeVarint(length)
|
||||
if length > 0 {
|
||||
data := dec.ReadBytes(int(length))
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, data...)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// readVariantVectorFixed reads a vector of fixed-size elements and returns
|
||||
// the raw bytes including the count varint.
|
||||
func readVariantVectorFixed(dec *Decoder, elemSize int) []byte {
|
||||
count := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := EncodeVarint(count)
|
||||
if count > 0 {
|
||||
data := dec.ReadBytes(int(count) * elemSize)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, data...)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// readExtraAttachmentInfo reads the extra_attachment_info variant (tag 18).
|
||||
// Structure: cnt_type (string) + hash (32 bytes) + sz (varint).
|
||||
func readExtraAttachmentInfo(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// cnt_type: string
|
||||
str := readStringBlob(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, str...)
|
||||
// hash: 32 bytes
|
||||
h := dec.ReadBytes(32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, h...)
|
||||
// sz: varint
|
||||
v := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, EncodeVarint(v)...)
|
||||
return raw
|
||||
}
|
||||
|
||||
// readUnlockTime2 reads etc_tx_details_unlock_time2 (tag 30).
|
||||
// Structure: vector of {varint unlock_time, varint output_index}.
|
||||
func readUnlockTime2(dec *Decoder) []byte {
|
||||
count := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := EncodeVarint(count)
|
||||
for i := uint64(0); i < count; i++ {
|
||||
v1 := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, EncodeVarint(v1)...)
|
||||
v2 := dec.ReadVarint()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, EncodeVarint(v2)...)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// readTxPayer reads tx_payer / tx_receiver (tags 31 / 32).
|
||||
// Structure: spend_public_key (32 bytes) + view_public_key (32 bytes)
|
||||
// + optional_field marker. In the binary_archive, the optional is
|
||||
// serialised as uint8(1)+data or uint8(0) for empty.
|
||||
func readTxPayer(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// Two public keys
|
||||
keys := dec.ReadBytes(64)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, keys...)
|
||||
// is_auditable flag (optional_field serialised as uint8 presence + data)
|
||||
marker := dec.ReadUint8()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, marker)
|
||||
if marker != 0 {
|
||||
b := dec.ReadUint8()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// readTxServiceAttachment reads tx_service_attachment (tag 12).
|
||||
// Structure: service_id (string) + instruction (string) + body (string)
|
||||
// + security (vector<public_key>) + flags (uint8).
|
||||
func readTxServiceAttachment(dec *Decoder) []byte {
|
||||
var raw []byte
|
||||
// Three string fields
|
||||
for range 3 {
|
||||
s := readStringBlob(dec)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, s...)
|
||||
}
|
||||
// security: vector<crypto::public_key> (varint count + 32*N bytes)
|
||||
v := readVariantVectorFixed(dec, 32)
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, v...)
|
||||
// flags: uint8
|
||||
b := dec.ReadUint8()
|
||||
if dec.err != nil {
|
||||
return nil
|
||||
}
|
||||
raw = append(raw, b)
|
||||
return raw
|
||||
}
|
||||
469
wire/transaction_test.go
Normal file
469
wire/transaction_test.go
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
// 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"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
)
|
||||
|
||||
func TestCoinbaseTxEncodeDecode_Good(t *testing.T) {
|
||||
// Build a minimal v1 coinbase transaction.
|
||||
tx := types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputGenesis{Height: 42}},
|
||||
Vout: []types.TxOutput{types.TxOutputBare{
|
||||
Amount: 1000000,
|
||||
Target: types.TxOutToKey{
|
||||
Key: types.PublicKey{0xDE, 0xAD},
|
||||
MixAttr: 0,
|
||||
},
|
||||
}},
|
||||
Extra: EncodeVarint(0), // empty extra (count=0)
|
||||
}
|
||||
|
||||
// Encode prefix.
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransactionPrefix(enc, &tx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
// Decode prefix.
|
||||
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
got := DecodeTransactionPrefix(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
if got.Version != tx.Version {
|
||||
t.Errorf("version: got %d, want %d", got.Version, tx.Version)
|
||||
}
|
||||
if len(got.Vin) != 1 {
|
||||
t.Fatalf("vin count: got %d, want 1", len(got.Vin))
|
||||
}
|
||||
gen, ok := got.Vin[0].(types.TxInputGenesis)
|
||||
if !ok {
|
||||
t.Fatalf("vin[0] type: got %T, want TxInputGenesis", got.Vin[0])
|
||||
}
|
||||
if gen.Height != 42 {
|
||||
t.Errorf("height: got %d, want 42", gen.Height)
|
||||
}
|
||||
if len(got.Vout) != 1 {
|
||||
t.Fatalf("vout count: got %d, want 1", len(got.Vout))
|
||||
}
|
||||
bare, ok := got.Vout[0].(types.TxOutputBare)
|
||||
if !ok {
|
||||
t.Fatalf("vout[0] type: got %T, want TxOutputBare", got.Vout[0])
|
||||
}
|
||||
if bare.Amount != 1000000 {
|
||||
t.Errorf("amount: got %d, want 1000000", bare.Amount)
|
||||
}
|
||||
if bare.Target.Key[0] != 0xDE || bare.Target.Key[1] != 0xAD {
|
||||
t.Errorf("target key: got %x, want DE AD...", bare.Target.Key[:2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullTxRoundTrip_Good(t *testing.T) {
|
||||
// Build a v1 coinbase transaction with empty signatures and attachment.
|
||||
tx := types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
|
||||
Vout: []types.TxOutput{types.TxOutputBare{
|
||||
Amount: 5000000000,
|
||||
Target: types.TxOutToKey{
|
||||
Key: types.PublicKey{0x01, 0x02, 0x03},
|
||||
MixAttr: 0,
|
||||
},
|
||||
}},
|
||||
Extra: EncodeVarint(0), // empty extra
|
||||
Attachment: EncodeVarint(0), // empty attachment
|
||||
}
|
||||
|
||||
// Encode full transaction.
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransaction(enc, &tx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
encoded := buf.Bytes()
|
||||
|
||||
// Decode full transaction.
|
||||
dec := NewDecoder(bytes.NewReader(encoded))
|
||||
got := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
// Re-encode and compare bytes.
|
||||
var rtBuf bytes.Buffer
|
||||
enc2 := NewEncoder(&rtBuf)
|
||||
EncodeTransaction(enc2, &got)
|
||||
if enc2.Err() != nil {
|
||||
t.Fatalf("re-encode error: %v", enc2.Err())
|
||||
}
|
||||
if !bytes.Equal(rtBuf.Bytes(), encoded) {
|
||||
t.Errorf("round-trip mismatch:\n got: %x\n want: %x", rtBuf.Bytes(), encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionHash_Good(t *testing.T) {
|
||||
// TransactionHash for v0/v1 should equal TransactionPrefixHash
|
||||
// (confirmed from C++ source: get_transaction_hash delegates to prefix hash).
|
||||
tx := types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
|
||||
Vout: []types.TxOutput{types.TxOutputBare{
|
||||
Amount: 5000000000,
|
||||
Target: types.TxOutToKey{Key: types.PublicKey{0x01, 0x02, 0x03}},
|
||||
}},
|
||||
Extra: EncodeVarint(0),
|
||||
Attachment: EncodeVarint(0),
|
||||
}
|
||||
|
||||
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.
|
||||
var prefBuf bytes.Buffer
|
||||
enc1 := NewEncoder(&prefBuf)
|
||||
EncodeTransactionPrefix(enc1, &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) {
|
||||
tx := types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputToKey{
|
||||
Amount: 100,
|
||||
KeyOffsets: []types.TxOutRef{
|
||||
{Tag: types.RefTypeGlobalIndex, GlobalIndex: 42},
|
||||
{Tag: types.RefTypeGlobalIndex, GlobalIndex: 7},
|
||||
},
|
||||
KeyImage: types.KeyImage{0xFF},
|
||||
EtcDetails: EncodeVarint(0),
|
||||
}},
|
||||
Vout: []types.TxOutput{types.TxOutputBare{
|
||||
Amount: 50,
|
||||
Target: types.TxOutToKey{Key: types.PublicKey{0xAB}},
|
||||
}},
|
||||
Extra: EncodeVarint(0),
|
||||
Attachment: EncodeVarint(0),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransaction(enc, &tx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
got := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
toKey, ok := got.Vin[0].(types.TxInputToKey)
|
||||
if !ok {
|
||||
t.Fatalf("vin[0] type: got %T, want TxInputToKey", got.Vin[0])
|
||||
}
|
||||
if toKey.Amount != 100 {
|
||||
t.Errorf("amount: got %d, want 100", toKey.Amount)
|
||||
}
|
||||
if len(toKey.KeyOffsets) != 2 {
|
||||
t.Fatalf("key_offsets: got %d, want 2", len(toKey.KeyOffsets))
|
||||
}
|
||||
if toKey.KeyOffsets[0].GlobalIndex != 42 {
|
||||
t.Errorf("key_offsets[0]: got %d, want 42", toKey.KeyOffsets[0].GlobalIndex)
|
||||
}
|
||||
if toKey.KeyImage[0] != 0xFF {
|
||||
t.Errorf("key_image[0]: got 0x%02x, want 0xFF", toKey.KeyImage[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtraVariantTags_Good(t *testing.T) {
|
||||
// Test that various extra variant tags decode and re-encode correctly.
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte // raw extra bytes (including varint count prefix)
|
||||
}{
|
||||
{
|
||||
name: "public_key",
|
||||
// count=1, tag=22 (crypto::public_key), 32 bytes of key
|
||||
data: append([]byte{0x01, tagPublicKey}, make([]byte, 32)...),
|
||||
},
|
||||
{
|
||||
name: "unlock_time",
|
||||
// count=1, tag=14 (etc_tx_details_unlock_time), varint(100)=0x64
|
||||
data: []byte{0x01, tagUnlockTime, 0x64},
|
||||
},
|
||||
{
|
||||
name: "tx_details_flags",
|
||||
// count=1, tag=16, varint(1)=0x01
|
||||
data: []byte{0x01, tagTxDetailsFlags, 0x01},
|
||||
},
|
||||
{
|
||||
name: "derivation_hint",
|
||||
// count=1, tag=11, string len=3, "abc"
|
||||
data: []byte{0x01, tagTxDerivationHint, 0x03, 'a', 'b', 'c'},
|
||||
},
|
||||
{
|
||||
name: "user_data",
|
||||
// count=1, tag=19, string len=2, "hi"
|
||||
data: []byte{0x01, tagExtraUserData, 0x02, 'h', 'i'},
|
||||
},
|
||||
{
|
||||
name: "extra_padding",
|
||||
// count=1, tag=21, vector count=4, 4 bytes
|
||||
data: []byte{0x01, tagExtraPadding, 0x04, 0x00, 0x00, 0x00, 0x00},
|
||||
},
|
||||
{
|
||||
name: "crypto_checksum",
|
||||
// count=1, tag=10, 8 bytes (two uint32 LE)
|
||||
data: []byte{0x01, tagTxCryptoChecksum, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00},
|
||||
},
|
||||
{
|
||||
name: "signed_parts",
|
||||
// count=1, tag=17, uint32 LE
|
||||
data: []byte{0x01, tagSignedParts, 0xFF, 0x00, 0x00, 0x00},
|
||||
},
|
||||
{
|
||||
name: "etc_tx_flags16",
|
||||
// count=1, tag=23, uint16 LE
|
||||
data: []byte{0x01, tagEtcTxFlags16, 0x01, 0x00},
|
||||
},
|
||||
{
|
||||
name: "etc_tx_time",
|
||||
// count=1, tag=27, varint(42)
|
||||
data: []byte{0x01, tagEtcTxTime, 0x2A},
|
||||
},
|
||||
{
|
||||
name: "tx_comment",
|
||||
// count=1, tag=7, string len=5, "hello"
|
||||
data: []byte{0x01, tagTxComment, 0x05, 'h', 'e', 'l', 'l', 'o'},
|
||||
},
|
||||
{
|
||||
name: "tx_payer_old",
|
||||
// count=1, tag=8, 64 bytes (2 public keys)
|
||||
data: append([]byte{0x01, tagTxPayerOld}, make([]byte, 64)...),
|
||||
},
|
||||
{
|
||||
name: "tx_receiver_old",
|
||||
// count=1, tag=29, 64 bytes
|
||||
data: append([]byte{0x01, tagTxReceiverOld}, make([]byte, 64)...),
|
||||
},
|
||||
{
|
||||
name: "tx_payer_not_auditable",
|
||||
// count=1, tag=31, 64 bytes (2 keys) + marker=0 (no auditable flag)
|
||||
data: append(append([]byte{0x01, tagTxPayer}, make([]byte, 64)...), 0x00),
|
||||
},
|
||||
{
|
||||
name: "tx_payer_auditable",
|
||||
// count=1, tag=31, 64 bytes (2 keys) + marker=1 + auditable_flag=1
|
||||
data: append(append([]byte{0x01, tagTxPayer}, make([]byte, 64)...), 0x01, 0x01),
|
||||
},
|
||||
{
|
||||
name: "extra_attachment_info",
|
||||
// count=1, tag=18, cnt_type(string len=0) + hash(32 zeros) + sz(varint 0)
|
||||
data: append([]byte{0x01, tagExtraAttachmentInfo, 0x00}, append(make([]byte, 32), 0x00)...),
|
||||
},
|
||||
{
|
||||
name: "unlock_time2",
|
||||
// count=1, tag=30, vector count=1, entry: {unlock_time=10, output_index=0}
|
||||
data: []byte{0x01, tagUnlockTime2, 0x01, 0x0A, 0x00},
|
||||
},
|
||||
{
|
||||
name: "tx_service_attachment",
|
||||
// count=1, tag=12, 3 empty strings + empty key vec + flags=0
|
||||
data: []byte{0x01, tagTxServiceAttachment, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
},
|
||||
{
|
||||
name: "multiple_elements",
|
||||
// count=2: public_key + unlock_time
|
||||
data: append(
|
||||
append([]byte{0x02, tagPublicKey}, make([]byte, 32)...),
|
||||
tagUnlockTime, 0x64,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "empty_extra",
|
||||
// count=0
|
||||
data: []byte{0x00},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Build a v1 tx with this extra data.
|
||||
tx := types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
|
||||
Vout: []types.TxOutput{types.TxOutputBare{
|
||||
Amount: 1000,
|
||||
Target: types.TxOutToKey{Key: types.PublicKey{0xAA}},
|
||||
}},
|
||||
Extra: tt.data,
|
||||
Attachment: EncodeVarint(0),
|
||||
}
|
||||
|
||||
// Encode.
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransaction(enc, &tx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
// Decode.
|
||||
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
got := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
// Re-encode and compare.
|
||||
var rtBuf bytes.Buffer
|
||||
enc2 := NewEncoder(&rtBuf)
|
||||
EncodeTransaction(enc2, &got)
|
||||
if enc2.Err() != nil {
|
||||
t.Fatalf("re-encode error: %v", enc2.Err())
|
||||
}
|
||||
if !bytes.Equal(rtBuf.Bytes(), buf.Bytes()) {
|
||||
t.Errorf("round-trip mismatch:\n got: %x\n want: %x", rtBuf.Bytes(), buf.Bytes())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxWithSignaturesRoundTrip_Good(t *testing.T) {
|
||||
// Test v1 transaction with non-empty signatures.
|
||||
sig := types.Signature{}
|
||||
sig[0] = 0xAA
|
||||
sig[63] = 0xBB
|
||||
|
||||
tx := types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputToKey{
|
||||
Amount: 100,
|
||||
KeyOffsets: []types.TxOutRef{
|
||||
{Tag: types.RefTypeGlobalIndex, GlobalIndex: 42},
|
||||
},
|
||||
KeyImage: types.KeyImage{0xFF},
|
||||
EtcDetails: EncodeVarint(0),
|
||||
}},
|
||||
Vout: []types.TxOutput{types.TxOutputBare{
|
||||
Amount: 50,
|
||||
Target: types.TxOutToKey{Key: types.PublicKey{0xAB}},
|
||||
}},
|
||||
Extra: EncodeVarint(0),
|
||||
Signatures: [][]types.Signature{
|
||||
{sig},
|
||||
},
|
||||
Attachment: EncodeVarint(0),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransaction(enc, &tx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
got := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
if len(got.Signatures) != 1 {
|
||||
t.Fatalf("signatures count: got %d, want 1", len(got.Signatures))
|
||||
}
|
||||
if len(got.Signatures[0]) != 1 {
|
||||
t.Fatalf("ring[0] size: got %d, want 1", len(got.Signatures[0]))
|
||||
}
|
||||
if got.Signatures[0][0][0] != 0xAA || got.Signatures[0][0][63] != 0xBB {
|
||||
t.Error("signature data mismatch")
|
||||
}
|
||||
|
||||
// Round-trip byte comparison.
|
||||
var rtBuf bytes.Buffer
|
||||
enc2 := NewEncoder(&rtBuf)
|
||||
EncodeTransaction(enc2, &got)
|
||||
if !bytes.Equal(rtBuf.Bytes(), buf.Bytes()) {
|
||||
t.Errorf("round-trip mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefByIDRoundTrip_Good(t *testing.T) {
|
||||
// Test TxOutRef with RefTypeByID tag.
|
||||
tx := types.Transaction{
|
||||
Version: 1,
|
||||
Vin: []types.TxInput{types.TxInputToKey{
|
||||
Amount: 100,
|
||||
KeyOffsets: []types.TxOutRef{
|
||||
{
|
||||
Tag: types.RefTypeByID,
|
||||
TxID: types.Hash{0xDE, 0xAD},
|
||||
N: 3,
|
||||
},
|
||||
},
|
||||
KeyImage: types.KeyImage{0xFF},
|
||||
EtcDetails: EncodeVarint(0),
|
||||
}},
|
||||
Vout: []types.TxOutput{types.TxOutputBare{
|
||||
Amount: 50,
|
||||
Target: types.TxOutToKey{Key: types.PublicKey{0xAB}},
|
||||
}},
|
||||
Extra: EncodeVarint(0),
|
||||
Attachment: EncodeVarint(0),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
EncodeTransaction(enc, &tx)
|
||||
if enc.Err() != nil {
|
||||
t.Fatalf("encode error: %v", enc.Err())
|
||||
}
|
||||
|
||||
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
|
||||
got := DecodeTransaction(dec)
|
||||
if dec.Err() != nil {
|
||||
t.Fatalf("decode error: %v", dec.Err())
|
||||
}
|
||||
|
||||
toKey := got.Vin[0].(types.TxInputToKey)
|
||||
if len(toKey.KeyOffsets) != 1 {
|
||||
t.Fatalf("key_offsets: got %d, want 1", len(toKey.KeyOffsets))
|
||||
}
|
||||
ref := toKey.KeyOffsets[0]
|
||||
if ref.Tag != types.RefTypeByID {
|
||||
t.Errorf("ref tag: got %d, want %d", ref.Tag, types.RefTypeByID)
|
||||
}
|
||||
if ref.TxID[0] != 0xDE || ref.TxID[1] != 0xAD {
|
||||
t.Errorf("ref txid: got %x, want DEAD...", ref.TxID[:2])
|
||||
}
|
||||
if ref.N != 3 {
|
||||
t.Errorf("ref N: got %d, want 3", ref.N)
|
||||
}
|
||||
}
|
||||
86
wire/treehash.go
Normal file
86
wire/treehash.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// 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 "golang.org/x/crypto/sha3"
|
||||
|
||||
// Keccak256 computes the Keccak-256 hash (pre-NIST, no domain separation)
|
||||
// used as cn_fast_hash throughout the CryptoNote protocol.
|
||||
func Keccak256(data []byte) [32]byte {
|
||||
h := sha3.NewLegacyKeccak256()
|
||||
h.Write(data)
|
||||
var out [32]byte
|
||||
h.Sum(out[:0])
|
||||
return out
|
||||
}
|
||||
|
||||
// TreeHash computes the CryptoNote Merkle tree hash over a set of 32-byte
|
||||
// hashes. This is a direct port of crypto/tree-hash.c from the C++ daemon.
|
||||
//
|
||||
// Algorithm:
|
||||
// - 0 hashes: returns zero hash
|
||||
// - 1 hash: returns the hash itself (identity)
|
||||
// - 2 hashes: returns Keccak256(h0 || h1)
|
||||
// - N hashes: pad to power-of-2 leaves, pairwise Keccak up the tree
|
||||
func TreeHash(hashes [][32]byte) [32]byte {
|
||||
count := len(hashes)
|
||||
if count == 0 {
|
||||
return [32]byte{}
|
||||
}
|
||||
if count == 1 {
|
||||
return hashes[0]
|
||||
}
|
||||
if count == 2 {
|
||||
var buf [64]byte
|
||||
copy(buf[:32], hashes[0][:])
|
||||
copy(buf[32:], hashes[1][:])
|
||||
return Keccak256(buf[:])
|
||||
}
|
||||
|
||||
// Find largest power of 2 that is <= count. This mirrors the C++ bit trick:
|
||||
// cnt = count - 1; for (i = 1; i < bits; i <<= 1) cnt |= cnt >> i;
|
||||
// cnt &= ~(cnt >> 1);
|
||||
cnt := count - 1
|
||||
for i := 1; i < 64; i <<= 1 {
|
||||
cnt |= cnt >> uint(i)
|
||||
}
|
||||
cnt &= ^(cnt >> 1)
|
||||
|
||||
// Allocate intermediate hash buffer.
|
||||
ints := make([][32]byte, cnt)
|
||||
|
||||
// Copy the first (2*cnt - count) hashes directly into ints.
|
||||
direct := 2*cnt - count
|
||||
copy(ints[:direct], hashes[:direct])
|
||||
|
||||
// Pair-hash the remaining hashes into ints.
|
||||
i := direct
|
||||
for j := direct; j < cnt; j++ {
|
||||
var buf [64]byte
|
||||
copy(buf[:32], hashes[i][:])
|
||||
copy(buf[32:], hashes[i+1][:])
|
||||
ints[j] = Keccak256(buf[:])
|
||||
i += 2
|
||||
}
|
||||
|
||||
// Iteratively pair-hash until we have 2 hashes left.
|
||||
for cnt > 2 {
|
||||
cnt >>= 1
|
||||
for i, j := 0, 0; j < cnt; j++ {
|
||||
var buf [64]byte
|
||||
copy(buf[:32], ints[i][:])
|
||||
copy(buf[32:], ints[i+1][:])
|
||||
ints[j] = Keccak256(buf[:])
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
|
||||
// Final hash of the remaining pair.
|
||||
var buf [64]byte
|
||||
copy(buf[:32], ints[0][:])
|
||||
copy(buf[32:], ints[1][:])
|
||||
return Keccak256(buf[:])
|
||||
}
|
||||
171
wire/treehash_test.go
Normal file
171
wire/treehash_test.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// 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 (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeccak256_Good(t *testing.T) {
|
||||
// Empty input: well-known Keccak-256 of "" (pre-NIST).
|
||||
got := Keccak256(nil)
|
||||
want := "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
|
||||
if hex.EncodeToString(got[:]) != want {
|
||||
t.Errorf("Keccak256(nil) = %x, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeHashSingle_Good(t *testing.T) {
|
||||
var h [32]byte
|
||||
h[0] = 0xAB
|
||||
h[31] = 0xCD
|
||||
got := TreeHash([][32]byte{h})
|
||||
if got != h {
|
||||
t.Error("TreeHash of single hash should be identity")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeHashPair_Good(t *testing.T) {
|
||||
var h0, h1 [32]byte
|
||||
h0[0] = 0x01
|
||||
h1[0] = 0x02
|
||||
got := TreeHash([][32]byte{h0, h1})
|
||||
|
||||
// Manual: Keccak256(h0 || h1)
|
||||
var buf [64]byte
|
||||
copy(buf[:32], h0[:])
|
||||
copy(buf[32:], h1[:])
|
||||
want := Keccak256(buf[:])
|
||||
if got != want {
|
||||
t.Errorf("TreeHash pair: got %x, want %x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeHashThree_Good(t *testing.T) {
|
||||
var h0, h1, h2 [32]byte
|
||||
h0[0] = 0xAA
|
||||
h1[0] = 0xBB
|
||||
h2[0] = 0xCC
|
||||
|
||||
got := TreeHash([][32]byte{h0, h1, h2})
|
||||
|
||||
// For 3 hashes, cnt=2:
|
||||
// ints[0] = h0 (direct copy)
|
||||
// ints[1] = Keccak256(h1 || h2)
|
||||
// result = Keccak256(ints[0] || ints[1])
|
||||
var buf [64]byte
|
||||
copy(buf[:32], h1[:])
|
||||
copy(buf[32:], h2[:])
|
||||
ints1 := Keccak256(buf[:])
|
||||
|
||||
copy(buf[:32], h0[:])
|
||||
copy(buf[32:], ints1[:])
|
||||
want := Keccak256(buf[:])
|
||||
|
||||
if got != want {
|
||||
t.Errorf("TreeHash(3): got %x, want %x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeHashFour_Good(t *testing.T) {
|
||||
hashes := make([][32]byte, 4)
|
||||
for i := range hashes {
|
||||
hashes[i][0] = byte(i + 1)
|
||||
}
|
||||
|
||||
got := TreeHash(hashes)
|
||||
|
||||
// For 4 hashes, cnt=2:
|
||||
// ints[0] = Keccak256(h0 || h1)
|
||||
// ints[1] = Keccak256(h2 || h3)
|
||||
// result = Keccak256(ints[0] || ints[1])
|
||||
var buf [64]byte
|
||||
copy(buf[:32], hashes[0][:])
|
||||
copy(buf[32:], hashes[1][:])
|
||||
ints0 := Keccak256(buf[:])
|
||||
|
||||
copy(buf[:32], hashes[2][:])
|
||||
copy(buf[32:], hashes[3][:])
|
||||
ints1 := Keccak256(buf[:])
|
||||
|
||||
copy(buf[:32], ints0[:])
|
||||
copy(buf[32:], ints1[:])
|
||||
want := Keccak256(buf[:])
|
||||
|
||||
if got != want {
|
||||
t.Errorf("TreeHash(4): got %x, want %x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeHashFive_Good(t *testing.T) {
|
||||
// 5 hashes exercises the iterative cnt > 2 loop.
|
||||
hashes := make([][32]byte, 5)
|
||||
for i := range hashes {
|
||||
hashes[i][0] = byte(i + 1)
|
||||
}
|
||||
|
||||
got := TreeHash(hashes)
|
||||
|
||||
// For 5 hashes: cnt=4, direct=3
|
||||
// ints[0] = h0, ints[1] = h1, ints[2] = h2
|
||||
// ints[3] = Keccak256(h3 || h4)
|
||||
// Then cnt=2: ints[0] = Keccak256(ints[0] || ints[1])
|
||||
// ints[1] = Keccak256(ints[2] || ints[3])
|
||||
// Final = Keccak256(ints[0] || ints[1])
|
||||
var buf [64]byte
|
||||
|
||||
// ints[3]
|
||||
copy(buf[:32], hashes[3][:])
|
||||
copy(buf[32:], hashes[4][:])
|
||||
ints3 := Keccak256(buf[:])
|
||||
|
||||
// Round 1: pair-hash
|
||||
copy(buf[:32], hashes[0][:])
|
||||
copy(buf[32:], hashes[1][:])
|
||||
r1_0 := Keccak256(buf[:])
|
||||
|
||||
copy(buf[:32], hashes[2][:])
|
||||
copy(buf[32:], ints3[:])
|
||||
r1_1 := Keccak256(buf[:])
|
||||
|
||||
// Final
|
||||
copy(buf[:32], r1_0[:])
|
||||
copy(buf[32:], r1_1[:])
|
||||
want := Keccak256(buf[:])
|
||||
|
||||
if got != want {
|
||||
t.Errorf("TreeHash(5): got %x, want %x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeHashEight_Good(t *testing.T) {
|
||||
// 8 hashes = perfect power of 2, exercises multiple loop iterations.
|
||||
hashes := make([][32]byte, 8)
|
||||
for i := range hashes {
|
||||
hashes[i][0] = byte(i + 1)
|
||||
}
|
||||
|
||||
got := TreeHash(hashes)
|
||||
|
||||
// Verify determinism.
|
||||
got2 := TreeHash(hashes)
|
||||
if got != got2 {
|
||||
t.Error("TreeHash(8) not deterministic")
|
||||
}
|
||||
|
||||
// Sanity: result should not be zero.
|
||||
if got == ([32]byte{}) {
|
||||
t.Error("TreeHash(8) returned zero hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeHashEmpty_Good(t *testing.T) {
|
||||
got := TreeHash(nil)
|
||||
if got != ([32]byte{}) {
|
||||
t.Errorf("TreeHash(nil) should be zero hash, got %x", got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue