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 <charon@lethean.io>
This commit is contained in:
Claude 2026-03-16 20:53:39 +00:00
parent 9631efa5a8
commit 3e79f34a65
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 298 additions and 0 deletions

View file

@ -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<signature>
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<signature> (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<uint8>)
//
// 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<uint8>)
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<hash> — 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<AssetDescriptorBase>
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<uint8>
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<uint8>).
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<uint8>
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).

139
wire/transaction_v3_test.go Normal file
View file

@ -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<uint8> (empty)
enc.WriteVarint(0)
// -- end AssetDescriptorBase --
// amount_to_emit: uint64 LE
enc.WriteUint64LE(0)
// amount_to_burn: uint64 LE
enc.WriteUint64LE(0)
// etc: vector<uint8> (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<uint8> (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))
}
}