go-blockchain/wire/transaction_test.go
Snider 76488e0beb
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
feat(wire): add alias entry readers + AX usage-example comments
Add readExtraAliasEntryOld (tag 20) and readExtraAliasEntry (tag 33)
wire readers so the node can deserialise blocks containing alias
registrations. Without these readers, mainnet sync would fail on any
block with an alias transaction. Three round-trip tests validate the
new readers.

Also apply AX-2 (comments as usage examples) across 12 files: add
concrete usage-example comments to exported functions in config/,
types/, wire/, chain/, difficulty/, and consensus/. Fix stale doc
in consensus/doc.go that incorrectly referenced *config.ChainConfig.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-05 08:46:54 +01:00

1211 lines
32 KiB
Go

// 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"
"dappco.re/go/core/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)
}
toKey, ok := bare.Target.(types.TxOutToKey)
if !ok {
t.Fatalf("target type: got %T, want TxOutToKey", bare.Target)
}
if toKey.Key[0] != 0xDE || toKey.Key[1] != 0xAD {
t.Errorf("target key: got %x, want DE AD...", toKey.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 should equal TransactionPrefixHash for all versions.
// Confirmed from C++ source: get_transaction_hash delegates to
// get_transaction_prefix_hash for all transaction versions.
tx := types.Transaction{
Version: 1,
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
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)
// TransactionHash always delegates to TransactionPrefixHash.
if txHash != prefixHash {
t.Error("TransactionHash should equal TransactionPrefixHash")
}
// Verify manual consistency.
var prefBuf bytes.Buffer
enc := NewEncoder(&prefBuf)
EncodeTransactionPrefix(enc, &tx)
if Keccak256(prefBuf.Bytes()) != [32]byte(prefixHash) {
t.Error("TransactionPrefixHash does not match manual prefix 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, two varints: n_outs=2, n_extras=1
data: []byte{0x01, tagSignedParts, 0x02, 0x01},
},
{
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)
}
}
func TestHTLCInputRoundTrip_Good(t *testing.T) {
tx := types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputHTLC{
HTLCOrigin: "test_origin",
Amount: 42000,
KeyOffsets: []types.TxOutRef{
{Tag: types.RefTypeGlobalIndex, GlobalIndex: 100},
},
KeyImage: types.KeyImage{0xAA},
EtcDetails: EncodeVarint(0),
},
},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 41000,
Target: types.TxOutToKey{Key: types.PublicKey{0x01}},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeTransactionPrefix(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
if len(got.Vin) != 1 {
t.Fatalf("vin count: got %d, want 1", len(got.Vin))
}
htlc, ok := got.Vin[0].(types.TxInputHTLC)
if !ok {
t.Fatalf("vin[0] type: got %T, want TxInputHTLC", got.Vin[0])
}
if htlc.HTLCOrigin != "test_origin" {
t.Errorf("HTLCOrigin: got %q, want %q", htlc.HTLCOrigin, "test_origin")
}
if htlc.Amount != 42000 {
t.Errorf("Amount: got %d, want 42000", htlc.Amount)
}
if htlc.KeyImage[0] != 0xAA {
t.Errorf("KeyImage[0]: got 0x%02x, want 0xAA", htlc.KeyImage[0])
}
// Byte-level round-trip.
var rtBuf bytes.Buffer
enc2 := NewEncoder(&rtBuf)
EncodeTransactionPrefix(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 TestMultisigInputRoundTrip_Good(t *testing.T) {
tx := types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputMultisig{
Amount: 50000,
MultisigOutID: types.Hash{0xBB},
SigsCount: 3,
EtcDetails: EncodeVarint(0),
},
},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 49000,
Target: types.TxOutToKey{Key: types.PublicKey{0x02}},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeTransactionPrefix(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
if len(got.Vin) != 1 {
t.Fatalf("vin count: got %d, want 1", len(got.Vin))
}
msig, ok := got.Vin[0].(types.TxInputMultisig)
if !ok {
t.Fatalf("vin[0] type: got %T, want TxInputMultisig", got.Vin[0])
}
if msig.Amount != 50000 {
t.Errorf("Amount: got %d, want 50000", msig.Amount)
}
if msig.MultisigOutID[0] != 0xBB {
t.Errorf("MultisigOutID[0]: got 0x%02x, want 0xBB", msig.MultisigOutID[0])
}
if msig.SigsCount != 3 {
t.Errorf("SigsCount: got %d, want 3", msig.SigsCount)
}
// Byte-level round-trip.
var rtBuf bytes.Buffer
enc2 := NewEncoder(&rtBuf)
EncodeTransactionPrefix(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 TestMultisigTargetV1RoundTrip_Good(t *testing.T) {
tx := types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 1}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 5000,
Target: types.TxOutMultisig{
MinimumSigs: 2,
Keys: []types.PublicKey{{0x01}, {0x02}, {0x03}},
},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeTransactionPrefix(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
bare, ok := got.Vout[0].(types.TxOutputBare)
if !ok {
t.Fatalf("vout[0] type: got %T, want TxOutputBare", got.Vout[0])
}
msig, ok := bare.Target.(types.TxOutMultisig)
if !ok {
t.Fatalf("target type: got %T, want TxOutMultisig", bare.Target)
}
if msig.MinimumSigs != 2 {
t.Errorf("MinimumSigs: got %d, want 2", msig.MinimumSigs)
}
if len(msig.Keys) != 3 {
t.Errorf("Keys count: got %d, want 3", len(msig.Keys))
}
// Byte-level round-trip.
var rtBuf bytes.Buffer
enc2 := NewEncoder(&rtBuf)
EncodeTransactionPrefix(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 TestHTLCTargetV1RoundTrip_Good(t *testing.T) {
tx := types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 1}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 7000,
Target: types.TxOutHTLC{
HTLCHash: types.Hash{0xCC},
Flags: 1, // RIPEMD160
Expiration: 20000,
PKRedeem: types.PublicKey{0xDD},
PKRefund: types.PublicKey{0xEE},
},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeTransactionPrefix(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
bare, ok := got.Vout[0].(types.TxOutputBare)
if !ok {
t.Fatalf("vout[0] type: got %T, want TxOutputBare", got.Vout[0])
}
htlc, ok := bare.Target.(types.TxOutHTLC)
if !ok {
t.Fatalf("target type: got %T, want TxOutHTLC", bare.Target)
}
if htlc.HTLCHash[0] != 0xCC {
t.Errorf("HTLCHash[0]: got 0x%02x, want 0xCC", htlc.HTLCHash[0])
}
if htlc.Flags != 1 {
t.Errorf("Flags: got %d, want 1", htlc.Flags)
}
if htlc.Expiration != 20000 {
t.Errorf("Expiration: got %d, want 20000", htlc.Expiration)
}
if htlc.PKRedeem[0] != 0xDD {
t.Errorf("PKRedeem[0]: got 0x%02x, want 0xDD", htlc.PKRedeem[0])
}
if htlc.PKRefund[0] != 0xEE {
t.Errorf("PKRefund[0]: got 0x%02x, want 0xEE", htlc.PKRefund[0])
}
// Byte-level round-trip.
var rtBuf bytes.Buffer
enc2 := NewEncoder(&rtBuf)
EncodeTransactionPrefix(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 TestMultisigTargetV2RoundTrip_Good(t *testing.T) {
tx := types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 1}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 5000,
Target: types.TxOutMultisig{
MinimumSigs: 2,
Keys: []types.PublicKey{{0x01}, {0x02}},
},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeTransactionPrefix(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
bare, ok := got.Vout[0].(types.TxOutputBare)
if !ok {
t.Fatalf("vout[0] type: got %T, want TxOutputBare", got.Vout[0])
}
msig, ok := bare.Target.(types.TxOutMultisig)
if !ok {
t.Fatalf("target type: got %T, want TxOutMultisig", bare.Target)
}
if msig.MinimumSigs != 2 {
t.Errorf("MinimumSigs: got %d, want 2", msig.MinimumSigs)
}
if len(msig.Keys) != 2 {
t.Errorf("Keys count: got %d, want 2", len(msig.Keys))
}
// Byte-level round-trip.
var rtBuf bytes.Buffer
enc2 := NewEncoder(&rtBuf)
EncodeTransactionPrefix(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 TestHF1MixedTxRoundTrip_Good(t *testing.T) {
// Construct a v1 transaction with HTLC input, multisig input, multisig output,
// and HTLC output target -- covering all HF1 types in a single round-trip.
tx := types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{
Amount: 100000,
KeyOffsets: []types.TxOutRef{
{Tag: types.RefTypeGlobalIndex, GlobalIndex: 42},
},
KeyImage: types.KeyImage{0x01},
EtcDetails: EncodeVarint(0),
},
types.TxInputHTLC{
HTLCOrigin: "htlc_preimage_data",
Amount: 50000,
KeyOffsets: []types.TxOutRef{
{Tag: types.RefTypeGlobalIndex, GlobalIndex: 99},
},
KeyImage: types.KeyImage{0x02},
EtcDetails: EncodeVarint(0),
},
types.TxInputMultisig{
Amount: 30000,
MultisigOutID: types.Hash{0xFF},
SigsCount: 2,
EtcDetails: EncodeVarint(0),
},
},
Vout: []types.TxOutput{
types.TxOutputBare{
Amount: 70000,
Target: types.TxOutToKey{Key: types.PublicKey{0xAA}},
},
types.TxOutputBare{
Amount: 50000,
Target: types.TxOutMultisig{
MinimumSigs: 2,
Keys: []types.PublicKey{{0xBB}, {0xCC}},
},
},
types.TxOutputBare{
Amount: 40000,
Target: types.TxOutHTLC{
HTLCHash: types.Hash{0xDD},
Flags: 0,
Expiration: 15000,
PKRedeem: types.PublicKey{0xEE},
PKRefund: types.PublicKey{0xFF},
},
},
},
Extra: EncodeVarint(0),
}
// Encode.
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
encoded := buf.Bytes()
// Decode.
dec := NewDecoder(bytes.NewReader(encoded))
got := DecodeTransactionPrefix(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
// Verify inputs.
if len(got.Vin) != 3 {
t.Fatalf("vin count: got %d, want 3", len(got.Vin))
}
if _, ok := got.Vin[0].(types.TxInputToKey); !ok {
t.Errorf("vin[0]: got %T, want TxInputToKey", got.Vin[0])
}
htlcIn, ok := got.Vin[1].(types.TxInputHTLC)
if !ok {
t.Fatalf("vin[1]: got %T, want TxInputHTLC", got.Vin[1])
}
if htlcIn.HTLCOrigin != "htlc_preimage_data" {
t.Errorf("HTLCOrigin: got %q, want %q", htlcIn.HTLCOrigin, "htlc_preimage_data")
}
if htlcIn.Amount != 50000 {
t.Errorf("HTLC Amount: got %d, want 50000", htlcIn.Amount)
}
msigIn, ok := got.Vin[2].(types.TxInputMultisig)
if !ok {
t.Fatalf("vin[2]: got %T, want TxInputMultisig", got.Vin[2])
}
if msigIn.Amount != 30000 {
t.Errorf("Multisig Amount: got %d, want 30000", msigIn.Amount)
}
if msigIn.SigsCount != 2 {
t.Errorf("SigsCount: got %d, want 2", msigIn.SigsCount)
}
// Verify outputs.
if len(got.Vout) != 3 {
t.Fatalf("vout count: got %d, want 3", len(got.Vout))
}
bare0, ok := got.Vout[0].(types.TxOutputBare)
if !ok {
t.Fatalf("vout[0]: got %T, want TxOutputBare", got.Vout[0])
}
if _, ok := bare0.Target.(types.TxOutToKey); !ok {
t.Errorf("vout[0] target: got %T, want TxOutToKey", bare0.Target)
}
bare1, ok := got.Vout[1].(types.TxOutputBare)
if !ok {
t.Fatalf("vout[1]: got %T, want TxOutputBare", got.Vout[1])
}
msigTgt, ok := bare1.Target.(types.TxOutMultisig)
if !ok {
t.Fatalf("vout[1] target: got %T, want TxOutMultisig", bare1.Target)
}
if msigTgt.MinimumSigs != 2 {
t.Errorf("MinimumSigs: got %d, want 2", msigTgt.MinimumSigs)
}
if len(msigTgt.Keys) != 2 {
t.Errorf("Keys count: got %d, want 2", len(msigTgt.Keys))
}
bare2, ok := got.Vout[2].(types.TxOutputBare)
if !ok {
t.Fatalf("vout[2]: got %T, want TxOutputBare", got.Vout[2])
}
htlcTgt, ok := bare2.Target.(types.TxOutHTLC)
if !ok {
t.Fatalf("vout[2] target: got %T, want TxOutHTLC", bare2.Target)
}
if htlcTgt.Expiration != 15000 {
t.Errorf("Expiration: got %d, want 15000", htlcTgt.Expiration)
}
// Re-encode and verify bit-identical.
var buf2 bytes.Buffer
enc2 := NewEncoder(&buf2)
EncodeTransactionPrefix(enc2, &got)
if enc2.Err() != nil {
t.Fatalf("re-encode error: %v", enc2.Err())
}
if !bytes.Equal(encoded, buf2.Bytes()) {
t.Errorf("round-trip not bit-identical: encoded %d bytes, re-encoded %d bytes",
len(encoded), len(buf2.Bytes()))
}
}
func TestHTLCTargetV2RoundTrip_Good(t *testing.T) {
tx := types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 1}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 7000,
Target: types.TxOutHTLC{
HTLCHash: types.Hash{0xCC},
Flags: 0, // SHA256
Expiration: 15000,
PKRedeem: types.PublicKey{0xDD},
PKRefund: types.PublicKey{0xEE},
},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeTransactionPrefix(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
bare, ok := got.Vout[0].(types.TxOutputBare)
if !ok {
t.Fatalf("vout[0] type: got %T, want TxOutputBare", got.Vout[0])
}
htlc, ok := bare.Target.(types.TxOutHTLC)
if !ok {
t.Fatalf("target type: got %T, want TxOutHTLC", bare.Target)
}
if htlc.HTLCHash[0] != 0xCC {
t.Errorf("HTLCHash[0]: got 0x%02x, want 0xCC", htlc.HTLCHash[0])
}
if htlc.Flags != 0 {
t.Errorf("Flags: got %d, want 0", htlc.Flags)
}
if htlc.Expiration != 15000 {
t.Errorf("Expiration: got %d, want 15000", htlc.Expiration)
}
if htlc.PKRedeem[0] != 0xDD {
t.Errorf("PKRedeem[0]: got 0x%02x, want 0xDD", htlc.PKRedeem[0])
}
if htlc.PKRefund[0] != 0xEE {
t.Errorf("PKRefund[0]: got 0x%02x, want 0xEE", htlc.PKRefund[0])
}
// Byte-level round-trip.
var rtBuf bytes.Buffer
enc2 := NewEncoder(&rtBuf)
EncodeTransactionPrefix(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())
}
}
type unsupportedTxInput struct{}
func (unsupportedTxInput) InputType() uint8 { return 250 }
type unsupportedTxOutTarget struct{}
func (unsupportedTxOutTarget) TargetType() uint8 { return 250 }
type unsupportedTxOutput struct{}
func (unsupportedTxOutput) OutputType() uint8 { return 250 }
func TestEncodeTransaction_UnsupportedInput_Bad(t *testing.T) {
tx := types.Transaction{
Version: 1,
Vin: []types.TxInput{unsupportedTxInput{}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 1,
Target: types.TxOutToKey{Key: types.PublicKey{1}},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() == nil {
t.Fatal("expected encode error for unsupported input type")
}
}
func TestEncodeTransaction_UnsupportedOutputTarget_Bad(t *testing.T) {
tests := []struct {
name string
version uint64
}{
{name: "v1", version: types.VersionPreHF4},
{name: "v2", version: types.VersionPostHF4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tx := types.Transaction{
Version: tt.version,
Vin: []types.TxInput{types.TxInputGenesis{Height: 1}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 1,
Target: unsupportedTxOutTarget{},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() == nil {
t.Fatal("expected encode error for unsupported output target type")
}
})
}
}
func TestEncodeTransaction_UnsupportedOutputType_Bad(t *testing.T) {
tests := []struct {
name string
version uint64
}{
{name: "v1", version: types.VersionPreHF4},
{name: "v2", version: types.VersionPostHF4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tx := types.Transaction{
Version: tt.version,
Vin: []types.TxInput{types.TxInputGenesis{Height: 1}},
Vout: []types.TxOutput{unsupportedTxOutput{}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() == nil {
t.Fatal("expected encode error for unsupported output type")
}
})
}
}
// TestExtraAliasEntryOldRoundTrip_Good verifies that a variant vector
// containing an extra_alias_entry_old (tag 20) round-trips through
// decodeRawVariantVector without error.
func TestExtraAliasEntryOldRoundTrip_Good(t *testing.T) {
// Build a synthetic variant vector with one extra_alias_entry_old element.
// Format: count(1) + tag(20) + alias(string) + address(64 bytes) +
// text_comment(string) + sign(vector of 64-byte sigs).
var raw []byte
raw = append(raw, EncodeVarint(1)...) // 1 element
raw = append(raw, tagExtraAliasEntryOld)
// m_alias: "test.lthn"
alias := []byte("test.lthn")
raw = append(raw, EncodeVarint(uint64(len(alias)))...)
raw = append(raw, alias...)
// m_address: spend_key(32) + view_key(32) = 64 bytes
addr := make([]byte, 64)
for i := range addr {
addr[i] = byte(i)
}
raw = append(raw, addr...)
// m_text_comment: "hello"
comment := []byte("hello")
raw = append(raw, EncodeVarint(uint64(len(comment)))...)
raw = append(raw, comment...)
// m_sign: 1 signature (generic_schnorr_sig_s = 64 bytes)
raw = append(raw, EncodeVarint(1)...) // 1 signature
sig := make([]byte, 64)
for i := range sig {
sig[i] = byte(0xAA)
}
raw = append(raw, sig...)
// Decode and round-trip.
dec := NewDecoder(bytes.NewReader(raw))
decoded := decodeRawVariantVector(dec)
if dec.Err() != nil {
t.Fatalf("decode failed: %v", dec.Err())
}
if !bytes.Equal(decoded, raw) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw))
}
}
// TestExtraAliasEntryRoundTrip_Good verifies that a variant vector
// containing an extra_alias_entry (tag 33) round-trips through
// decodeRawVariantVector without error.
func TestExtraAliasEntryRoundTrip_Good(t *testing.T) {
// Build a synthetic variant vector with one extra_alias_entry element.
// Format: count(1) + tag(33) + alias(string) + address(tx_payer format) +
// text_comment(string) + sign(vector) + view_key(optional).
var raw []byte
raw = append(raw, EncodeVarint(1)...) // 1 element
raw = append(raw, tagExtraAliasEntry)
// m_alias: "myalias"
alias := []byte("myalias")
raw = append(raw, EncodeVarint(uint64(len(alias)))...)
raw = append(raw, alias...)
// m_address: tx_payer format = spend_key(32) + view_key(32) + optional marker
addr := make([]byte, 64)
for i := range addr {
addr[i] = byte(i + 10)
}
raw = append(raw, addr...)
// is_auditable optional marker: 0 = not present
raw = append(raw, 0x00)
// m_text_comment: empty
raw = append(raw, EncodeVarint(0)...)
// m_sign: 0 signatures
raw = append(raw, EncodeVarint(0)...)
// m_view_key: optional, present (marker=1 + 32 bytes)
raw = append(raw, 0x01)
viewKey := make([]byte, 32)
for i := range viewKey {
viewKey[i] = byte(0xBB)
}
raw = append(raw, viewKey...)
// Decode and round-trip.
dec := NewDecoder(bytes.NewReader(raw))
decoded := decodeRawVariantVector(dec)
if dec.Err() != nil {
t.Fatalf("decode failed: %v", dec.Err())
}
if !bytes.Equal(decoded, raw) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw))
}
}
// TestExtraAliasEntryNoViewKey_Good verifies extra_alias_entry with
// the optional view_key marker set to 0 (not present).
func TestExtraAliasEntryNoViewKey_Good(t *testing.T) {
var raw []byte
raw = append(raw, EncodeVarint(1)...) // 1 element
raw = append(raw, tagExtraAliasEntry)
// m_alias: "short"
alias := []byte("short")
raw = append(raw, EncodeVarint(uint64(len(alias)))...)
raw = append(raw, alias...)
// m_address: keys + no auditable flag
raw = append(raw, make([]byte, 64)...)
raw = append(raw, 0x00) // not auditable
// m_text_comment: empty
raw = append(raw, EncodeVarint(0)...)
// m_sign: 0 signatures
raw = append(raw, EncodeVarint(0)...)
// m_view_key: not present (marker=0)
raw = append(raw, 0x00)
dec := NewDecoder(bytes.NewReader(raw))
decoded := decodeRawVariantVector(dec)
if dec.Err() != nil {
t.Fatalf("decode failed: %v", dec.Err())
}
if !bytes.Equal(decoded, raw) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw))
}
}