// 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" ) // buildAssetDescriptorOpBlob constructs a minimal asset_descriptor_operation // binary blob (version 1, operation_type=register, with descriptor, no asset_id). func buildAssetDescriptorOpBlob() []byte { var buf bytes.Buffer enc := NewEncoder(&buf) // ver: uint8 = 1 enc.WriteUint8(1) // operation_type: uint8 = 0 (register) enc.WriteUint8(0) // opt_asset_id: absent (marker = 0) enc.WriteUint8(0) // opt_descriptor: present (marker = 1) enc.WriteUint8(1) // -- AssetDescriptorBase -- // ticker: string "LTHN" (varint len + bytes) enc.WriteVarint(4) enc.WriteBytes([]byte("LTHN")) // full_name: string "Lethean" (varint len + bytes) enc.WriteVarint(7) enc.WriteBytes([]byte("Lethean")) // total_max_supply: uint64 LE enc.WriteUint64LE(1000000) // current_supply: uint64 LE enc.WriteUint64LE(0) // decimal_point: uint8 enc.WriteUint8(12) // meta_info: string "" (empty) enc.WriteVarint(0) // owner_key: 32 bytes enc.WriteBytes(make([]byte, 32)) // etc: vector (empty) enc.WriteVarint(0) // -- end AssetDescriptorBase -- // amount_to_emit: uint64 LE enc.WriteUint64LE(0) // amount_to_burn: uint64 LE enc.WriteUint64LE(0) // etc: vector (empty) enc.WriteVarint(0) return buf.Bytes() } func TestReadAssetDescriptorOperation_Good(t *testing.T) { blob := buildAssetDescriptorOpBlob() dec := NewDecoder(bytes.NewReader(blob)) got := readAssetDescriptorOperation(dec) if dec.Err() != nil { t.Fatalf("readAssetDescriptorOperation failed: %v", dec.Err()) } if !bytes.Equal(got, blob) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob)) } op, err := DecodeAssetDescriptorOperation(blob) if err != nil { t.Fatalf("DecodeAssetDescriptorOperation failed: %v", err) } if op.Version != 1 || op.OperationType != 0 { t.Fatalf("unexpected operation header: %+v", op) } if op.Descriptor == nil { t.Fatal("expected descriptor to be present") } if op.Descriptor.Ticker != "LTHN" || op.Descriptor.FullName != "Lethean" { t.Fatalf("unexpected descriptor contents: %+v", op.Descriptor) } if op.Descriptor.TotalMaxSupply != 1000000 || op.Descriptor.DecimalPoint != 12 { t.Fatalf("unexpected descriptor values: %+v", op.Descriptor) } } func TestReadAssetDescriptorOperation_Bad(t *testing.T) { // Truncated blob — should error. dec := NewDecoder(bytes.NewReader([]byte{1, 0})) _ = readAssetDescriptorOperation(dec) if dec.Err() == nil { t.Fatal("expected error for truncated asset descriptor operation") } } // buildAssetDescriptorOpEmitBlob constructs an emit operation (version 1, // operation_type=1, with asset_id, no descriptor). func buildAssetDescriptorOpEmitBlob() []byte { var buf bytes.Buffer enc := NewEncoder(&buf) // ver: uint8 = 1 enc.WriteUint8(1) // operation_type: uint8 = 1 (emit) enc.WriteUint8(1) // opt_asset_id: present (marker = 1) + 32-byte hash enc.WriteUint8(1) enc.WriteBytes(bytes.Repeat([]byte{0xAB}, 32)) // opt_descriptor: absent (marker = 0) enc.WriteUint8(0) // amount_to_emit: uint64 LE = 500000 enc.WriteUint64LE(500000) // amount_to_burn: uint64 LE = 0 enc.WriteUint64LE(0) // etc: vector (empty) enc.WriteVarint(0) return buf.Bytes() } func TestReadAssetDescriptorOperationEmit_Good(t *testing.T) { blob := buildAssetDescriptorOpEmitBlob() dec := NewDecoder(bytes.NewReader(blob)) got := readAssetDescriptorOperation(dec) if dec.Err() != nil { t.Fatalf("readAssetDescriptorOperation (emit) failed: %v", dec.Err()) } if !bytes.Equal(got, blob) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob)) } op, err := DecodeAssetDescriptorOperation(blob) if err != nil { t.Fatalf("DecodeAssetDescriptorOperation (emit) failed: %v", err) } if op.Version != 1 || op.OperationType != 1 { t.Fatalf("unexpected operation header: %+v", op) } if op.Descriptor != nil { t.Fatalf("emit operation should not carry descriptor: %+v", op) } if op.AmountToEmit != 500000 || op.AmountToBurn != 0 { t.Fatalf("unexpected emit amounts: %+v", op) } if op.AssetID[0] != 0xAB || op.AssetID[31] != 0xAB { t.Fatalf("unexpected asset id: %x", op.AssetID) } } func TestVariantVectorWithTag40_Good(t *testing.T) { // Build a variant vector containing one element: tag 40 (asset_descriptor_operation). innerBlob := buildAssetDescriptorOpEmitBlob() var buf bytes.Buffer enc := NewEncoder(&buf) // count = 1 enc.WriteVarint(1) // tag enc.WriteUint8(tagAssetDescriptorOperation) // data enc.WriteBytes(innerBlob) raw := buf.Bytes() // Decode as raw variant vector. dec := NewDecoder(bytes.NewReader(raw)) got := decodeRawVariantVector(dec) if dec.Err() != nil { t.Fatalf("decodeRawVariantVector with tag 40 failed: %v", dec.Err()) } if !bytes.Equal(got, raw) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(raw)) } elements, err := DecodeVariantVector(raw) if err != nil { t.Fatalf("DecodeVariantVector failed: %v", err) } if len(elements) != 1 || elements[0].Tag != tagAssetDescriptorOperation { t.Fatalf("unexpected elements: %+v", elements) } if !bytes.Equal(elements[0].Data, innerBlob) { t.Fatalf("unexpected element payload length: got %d, want %d", len(elements[0].Data), len(innerBlob)) } } func buildAssetOperationProofBlob() []byte { var buf bytes.Buffer enc := NewEncoder(&buf) // ver: uint8 = 1 enc.WriteUint8(1) // gss: generic_schnorr_sig_s — 2 scalars (s, c) = 64 bytes enc.WriteBytes(make([]byte, 64)) // asset_id: 32-byte hash enc.WriteBytes(bytes.Repeat([]byte{0xCD}, 32)) // etc: vector (empty) enc.WriteVarint(0) return buf.Bytes() } func TestReadAssetOperationProof_Good(t *testing.T) { blob := buildAssetOperationProofBlob() dec := NewDecoder(bytes.NewReader(blob)) got := readAssetOperationProof(dec) if dec.Err() != nil { t.Fatalf("readAssetOperationProof failed: %v", dec.Err()) } if !bytes.Equal(got, blob) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob)) } } func TestReadAssetOperationProof_Bad(t *testing.T) { dec := NewDecoder(bytes.NewReader([]byte{1})) _ = readAssetOperationProof(dec) if dec.Err() == nil { t.Fatal("expected error for truncated asset operation proof") } } func buildAssetOperationOwnershipProofBlob() []byte { var buf bytes.Buffer enc := NewEncoder(&buf) // ver: uint8 = 1 enc.WriteUint8(1) // gss: generic_schnorr_sig_s — 2 scalars = 64 bytes enc.WriteBytes(make([]byte, 64)) // etc: vector (empty) enc.WriteVarint(0) return buf.Bytes() } func TestReadAssetOperationOwnershipProof_Good(t *testing.T) { blob := buildAssetOperationOwnershipProofBlob() dec := NewDecoder(bytes.NewReader(blob)) got := readAssetOperationOwnershipProof(dec) if dec.Err() != nil { t.Fatalf("readAssetOperationOwnershipProof failed: %v", dec.Err()) } if !bytes.Equal(got, blob) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob)) } } func buildAssetOperationOwnershipProofETHBlob() []byte { var buf bytes.Buffer enc := NewEncoder(&buf) // ver: uint8 = 1 enc.WriteUint8(1) // eth_sig: 65 bytes (r=32 + s=32 + v=1) enc.WriteBytes(make([]byte, 65)) // etc: vector (empty) enc.WriteVarint(0) return buf.Bytes() } func TestReadAssetOperationOwnershipProofETH_Good(t *testing.T) { blob := buildAssetOperationOwnershipProofETHBlob() dec := NewDecoder(bytes.NewReader(blob)) got := readAssetOperationOwnershipProofETH(dec) if dec.Err() != nil { t.Fatalf("readAssetOperationOwnershipProofETH failed: %v", dec.Err()) } if !bytes.Equal(got, blob) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob)) } } func TestVariantVectorWithProofTags_Good(t *testing.T) { // Build a variant vector with 3 elements: tags 49, 50, 51. proofBlob := buildAssetOperationProofBlob() ownershipBlob := buildAssetOperationOwnershipProofBlob() ethBlob := buildAssetOperationOwnershipProofETHBlob() var buf bytes.Buffer enc := NewEncoder(&buf) // count = 3 enc.WriteVarint(3) // tag 49 enc.WriteUint8(tagAssetOperationProof) enc.WriteBytes(proofBlob) // tag 50 enc.WriteUint8(tagAssetOperationOwnershipProof) enc.WriteBytes(ownershipBlob) // tag 51 enc.WriteUint8(tagAssetOperationOwnershipProofETH) enc.WriteBytes(ethBlob) raw := buf.Bytes() dec := NewDecoder(bytes.NewReader(raw)) got := decodeRawVariantVector(dec) if dec.Err() != nil { t.Fatalf("decodeRawVariantVector with proof tags failed: %v", dec.Err()) } if !bytes.Equal(got, raw) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(raw)) } } func TestV3TransactionRoundTrip_Good(t *testing.T) { // Build a v3 transaction with: // - 1 coinbase input (TxInputGenesis at height 201) // - 2 Zarcanum outputs // - extra containing: public_key (tag 22) + zarcanum_tx_data_v1 (tag 39) // - proofs containing: zc_balance_proof (tag 48) // - hardfork_id = 5 var buf bytes.Buffer enc := NewEncoder(&buf) // --- prefix --- // version = 3 enc.WriteVarint(3) // vin: 1 coinbase input enc.WriteVarint(1) // input count enc.WriteVariantTag(0) // txin_gen tag enc.WriteVarint(201) // height // extra: variant vector with 2 elements (public_key + zarcanum_tx_data_v1) enc.WriteVarint(2) // [0] public_key (tag 22): 32 bytes enc.WriteUint8(tagPublicKey) enc.WriteBytes(bytes.Repeat([]byte{0x11}, 32)) // [1] zarcanum_tx_data_v1 (tag 39): 8-byte LE fee enc.WriteUint8(tagZarcanumTxDataV1) enc.WriteUint64LE(10000) // vout: 2 Zarcanum outputs enc.WriteVarint(2) for range 2 { enc.WriteVariantTag(38) // OutputTypeZarcanum enc.WriteBytes(make([]byte, 32)) // stealth_address enc.WriteBytes(make([]byte, 32)) // concealing_point enc.WriteBytes(make([]byte, 32)) // amount_commitment enc.WriteBytes(make([]byte, 32)) // blinded_asset_id enc.WriteUint64LE(0) // encrypted_amount enc.WriteUint8(0) // mix_attr } // hardfork_id = 5 enc.WriteUint8(5) // --- suffix --- // attachment: empty enc.WriteVarint(0) // signatures: empty enc.WriteVarint(0) // proofs: 1 element — zc_balance_proof (tag 48, simplest: 96 bytes) enc.WriteVarint(1) enc.WriteUint8(tagZCBalanceProof) enc.WriteBytes(make([]byte, 96)) blob := buf.Bytes() // Decode dec := NewDecoder(bytes.NewReader(blob)) tx := DecodeTransaction(dec) if dec.Err() != nil { t.Fatalf("decode failed: %v", dec.Err()) } // Verify structural fields if tx.Version != 3 { t.Errorf("version: got %d, want 3", tx.Version) } if tx.HardforkID != 5 { t.Errorf("hardfork_id: got %d, want 5", tx.HardforkID) } if len(tx.Vin) != 1 { t.Fatalf("input count: got %d, want 1", len(tx.Vin)) } if len(tx.Vout) != 2 { t.Fatalf("output count: got %d, want 2", len(tx.Vout)) } // Re-encode var reenc bytes.Buffer enc2 := NewEncoder(&reenc) EncodeTransaction(enc2, &tx) if enc2.Err() != nil { t.Fatalf("encode failed: %v", enc2.Err()) } got := reenc.Bytes() if !bytes.Equal(got, blob) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes\ngot: %x\nwant: %x", len(got), len(blob), got[:min(len(got), 64)], blob[:min(len(blob), 64)]) } } func TestV3TransactionWithAssetOps_Good(t *testing.T) { // Build a v3 transaction whose extra includes an asset_descriptor_operation (tag 40) // and whose proofs include an asset_operation_proof (tag 49). assetOpBlob := buildAssetDescriptorOpEmitBlob() proofBlob := buildAssetOperationProofBlob() var buf bytes.Buffer enc := NewEncoder(&buf) // --- prefix --- enc.WriteVarint(3) // version // vin: 1 coinbase enc.WriteVarint(1) enc.WriteVariantTag(0) // txin_gen enc.WriteVarint(250) // height // extra: 2 elements — public_key + asset_descriptor_operation enc.WriteVarint(2) enc.WriteUint8(tagPublicKey) enc.WriteBytes(bytes.Repeat([]byte{0x22}, 32)) enc.WriteUint8(tagAssetDescriptorOperation) enc.WriteBytes(assetOpBlob) // vout: 2 Zarcanum outputs enc.WriteVarint(2) for range 2 { enc.WriteVariantTag(38) enc.WriteBytes(make([]byte, 32)) // stealth_address enc.WriteBytes(make([]byte, 32)) // concealing_point enc.WriteBytes(make([]byte, 32)) // amount_commitment enc.WriteBytes(make([]byte, 32)) // blinded_asset_id enc.WriteUint64LE(0) enc.WriteUint8(0) } // hardfork_id = 5 enc.WriteUint8(5) // --- suffix --- enc.WriteVarint(0) // attachment enc.WriteVarint(0) // signatures // proofs: 2 elements — zc_balance_proof + asset_operation_proof enc.WriteVarint(2) enc.WriteUint8(tagZCBalanceProof) enc.WriteBytes(make([]byte, 96)) enc.WriteUint8(tagAssetOperationProof) enc.WriteBytes(proofBlob) blob := buf.Bytes() // Decode dec := NewDecoder(bytes.NewReader(blob)) tx := DecodeTransaction(dec) if dec.Err() != nil { t.Fatalf("decode failed: %v", dec.Err()) } if tx.Version != 3 { t.Errorf("version: got %d, want 3", tx.Version) } if tx.HardforkID != 5 { t.Errorf("hardfork_id: got %d, want 5", tx.HardforkID) } // Re-encode and compare var reenc bytes.Buffer enc2 := NewEncoder(&reenc) EncodeTransaction(enc2, &tx) if enc2.Err() != nil { t.Fatalf("encode failed: %v", enc2.Err()) } if !bytes.Equal(reenc.Bytes(), blob) { t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(reenc.Bytes()), len(blob)) } } func TestV3TransactionDecode_Bad(t *testing.T) { // Truncated v3 transaction — version varint only. dec := NewDecoder(bytes.NewReader([]byte{0x03})) _ = DecodeTransaction(dec) if dec.Err() == nil { t.Fatal("expected error for truncated v3 transaction") } }