go-blockchain/wire/transaction_test.go
Virgil 5789325690 fix(blockchain): complete AX v0.8.0 polish pass
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 16:58:23 +00:00

989 lines
26 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 TestTransaction_CoinbaseTxEncodeDecode_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 TestTransaction_FullTxRoundTrip_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 TestTransaction_TransactionHash_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 TestTransaction_TxInputToKeyRoundTrip_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 TestTransaction_ExtraVariantTags_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 TestTransaction_TxWithSignaturesRoundTrip_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 TestTransaction_RefByIDRoundTrip_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 TestTransaction_HTLCInputRoundTrip_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 TestTransaction_MultisigInputRoundTrip_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 TestTransaction_MultisigTargetV1RoundTrip_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 TestTransaction_HTLCTargetV1RoundTrip_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 TestTransaction_MultisigTargetV2RoundTrip_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 TestTransaction_HF1MixedTxRoundTrip_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 TestTransaction_HTLCTargetV2RoundTrip_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())
}
}