go-blockchain/wire/transaction_test.go

1212 lines
32 KiB
Go
Raw Permalink Normal View History

// 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))
}
}