2026-02-20 17:16:08 +00:00
|
|
|
// 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"
|
|
|
|
|
|
2026-03-22 01:49:26 +00:00
|
|
|
"dappco.re/go/core/blockchain/types"
|
2026-02-20 17:16:08 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-16 20:23:27 +00:00
|
|
|
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])
|
2026-02-20 17:16:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-02-21 19:09:34 +00:00
|
|
|
// TransactionHash should equal TransactionPrefixHash for all versions.
|
|
|
|
|
// Confirmed from C++ source: get_transaction_hash delegates to
|
|
|
|
|
// get_transaction_prefix_hash for all transaction versions.
|
2026-02-20 17:16:08 +00:00
|
|
|
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)
|
|
|
|
|
|
2026-02-21 19:09:34 +00:00
|
|
|
// 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)
|
2026-02-20 17:16:08 +00:00
|
|
|
|
|
|
|
|
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",
|
2026-02-21 19:09:34 +00:00
|
|
|
// count=1, tag=17, two varints: n_outs=2, n_extras=1
|
|
|
|
|
data: []byte{0x01, tagSignedParts, 0x02, 0x01},
|
2026-02-20 17:16:08 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 20:28:55 +00:00
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 20:31:29 +00:00
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 20:41:28 +00:00
|
|
|
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()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 20:31:29 +00:00
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-04 19:56:50 +00:00
|
|
|
|
|
|
|
|
type unsupportedTxInput struct{}
|
|
|
|
|
|
|
|
|
|
func (unsupportedTxInput) InputType() uint8 { return 250 }
|
|
|
|
|
|
|
|
|
|
type unsupportedTxOutTarget struct{}
|
|
|
|
|
|
|
|
|
|
func (unsupportedTxOutTarget) TargetType() uint8 { return 250 }
|
|
|
|
|
|
2026-04-04 21:22:15 +00:00
|
|
|
type unsupportedTxOutput struct{}
|
|
|
|
|
|
|
|
|
|
func (unsupportedTxOutput) OutputType() uint8 { return 250 }
|
|
|
|
|
|
2026-04-04 19:56:50 +00:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-04 21:22:15 +00:00
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 08:46:54 +01:00
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
|
}
|
|
|
|
|
}
|