From 3e79f34a653721a0a2ed71b26d92476af845f00a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 20:53:39 +0000 Subject: [PATCH] feat(wire): add asset_descriptor_operation tag 40 reader Reads the CHAIN_TRANSITION_VER structure for asset deploy/emit/update/burn operations. Stores as opaque bytes for bit-identical round-tripping. Required for HF5 block deserialisation. Co-Authored-By: Charon --- wire/transaction.go | 159 ++++++++++++++++++++++++++++++++++++ wire/transaction_v3_test.go | 139 +++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 wire/transaction_v3_test.go diff --git a/wire/transaction.go b/wire/transaction.go index 583b75f..2af5be2 100644 --- a/wire/transaction.go +++ b/wire/transaction.go @@ -536,6 +536,9 @@ const ( tagExtraAliasEntry = 33 // extra_alias_entry — complex tagZarcanumTxDataV1 = 39 // zarcanum_tx_data_v1 — varint (fee) + // Asset descriptor operation (HF5). + tagAssetDescriptorOperation = 40 // asset_descriptor_operation + // Signature variant tags (signature_v). tagNLSAGSig = 42 // NLSAG_sig — vector tagZCSig = 43 // ZC_sig — 2 public_keys + CLSAG_GGX @@ -604,6 +607,10 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { case tagZarcanumTxDataV1: // fee — FIELD(fee) → serialize_int → 8-byte LE return dec.ReadBytes(8) + // Asset descriptor operation (HF5) + case tagAssetDescriptorOperation: + return readAssetDescriptorOperation(dec) + // Signature variants case tagNLSAGSig: // vector (64 bytes each) return readVariantVectorFixed(dec, 64) @@ -784,6 +791,158 @@ func readSignedParts(dec *Decoder) []byte { return raw } +// --- asset operation readers (HF5) --- + +// readAssetDescriptorOperation reads asset_descriptor_operation (tag 40). +// Structure (CHAIN_TRANSITION_VER, version 0 and 1): +// +// ver (uint8) + operation_type (uint8) +// + opt_asset_id (uint8 marker + 32 bytes if present) +// + opt_descriptor (uint8 marker + AssetDescriptorBase if present) +// + amount_to_emit (uint64 LE) + amount_to_burn (uint64 LE) +// + etc (vector) +// +// AssetDescriptorBase: +// +// ticker (string) + full_name (string) + total_max_supply (uint64 LE) +// + current_supply (uint64 LE) + decimal_point (uint8) + meta_info (string) +// + owner_key (32 bytes) + etc (vector) +func readAssetDescriptorOperation(dec *Decoder) []byte { + var raw []byte + + // ver: uint8 (CHAIN_TRANSITION_VER version byte) + ver := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, ver) + + // operation_type: uint8 + opType := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, opType) + + // opt_asset_id: optional — uint8 marker, then 32 bytes if present + marker := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, marker) + if marker != 0 { + b := dec.ReadBytes(32) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + } + + // opt_descriptor: optional + marker = dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, marker) + if marker != 0 { + b := readAssetDescriptorBase(dec) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + } + + // amount_to_emit: uint64 LE + b := dec.ReadBytes(8) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + + // amount_to_burn: uint64 LE + b = dec.ReadBytes(8) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + + // etc: vector + v := readVariantVectorFixed(dec, 1) + if dec.err != nil { + return nil + } + raw = append(raw, v...) + + return raw +} + +// readAssetDescriptorBase reads the AssetDescriptorBase structure. +// Wire: ticker (string) + full_name (string) + total_max_supply (uint64 LE) +// +// + current_supply (uint64 LE) + decimal_point (uint8) + meta_info (string) +// + owner_key (32 bytes) + etc (vector). +func readAssetDescriptorBase(dec *Decoder) []byte { + var raw []byte + + // ticker: string + s := readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, s...) + + // full_name: string + s = readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, s...) + + // total_max_supply: uint64 LE + b := dec.ReadBytes(8) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + + // current_supply: uint64 LE + b = dec.ReadBytes(8) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + + // decimal_point: uint8 + dp := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, dp) + + // meta_info: string + s = readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, s...) + + // owner_key: 32 bytes (crypto::public_key) + b = dec.ReadBytes(32) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + + // etc: vector + v := readVariantVectorFixed(dec, 1) + if dec.err != nil { + return nil + } + raw = append(raw, v...) + + return raw +} + // --- crypto blob readers --- // These read variable-length serialised crypto structures and return raw bytes. // All vectors are varint(count) + 32*count bytes (scalars or points). diff --git a/wire/transaction_v3_test.go b/wire/transaction_v3_test.go new file mode 100644 index 0000000..3a208d1 --- /dev/null +++ b/wire/transaction_v3_test.go @@ -0,0 +1,139 @@ +// 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)) + } +} + +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)) + } +} + +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)) + } +}