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