From f27825cfc9ec2112cc83ab42830d27dc2048a987 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 17 Mar 2026 08:34:04 +0000 Subject: [PATCH] fix(dx): audit and fix error handling, file I/O, wire compilation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: update error handling guidance from fmt.Errorf to coreerr.E() and document go-io convention for file I/O - wire/transaction.go: fix TxOutTarget interface compilation — add encodeTarget/decodeTarget helpers with support for TxOutToKey, TxOutMultisig, and TxOutHTLC target types - wire/transaction.go: add HTLC and multisig input encode/decode (TxInputHTLC, TxInputMultisig) with string encoding helpers - wire/transaction.go: add asset operation tag constants (40, 49-51) and reader functions for HF5 confidential asset operations - consensus/block.go: replace fmt.Errorf with coreerr.E() for checkBlockVersion and ValidateTransactionInBlock - chain/ring.go: replace fmt.Errorf with coreerr.E() in GetRingOutputs - consensus/v2sig_test.go: replace os.ReadFile with coreio.Read - crypto/*.go: replace all fmt.Errorf and errors.New with coreerr.E() across keygen, pow, keyimage, signature, and clsag packages - types/types_test.go: add tests for HashFromHex, PublicKeyFromHex, IsZero, and String methods (types coverage 74.5% -> 89.1%) Co-Authored-By: Virgil --- CLAUDE.md | 4 +- chain/ring.go | 2 +- consensus/block.go | 6 +- consensus/v2sig_test.go | 8 +- crypto/clsag.go | 11 +- crypto/keygen.go | 13 +- crypto/keyimage.go | 5 +- crypto/pow.go | 4 +- crypto/signature.go | 7 +- types/types_test.go | 106 +++++++++++++ wire/transaction.go | 334 ++++++++++++++++++++++++++++++++++++++-- 11 files changed, 458 insertions(+), 42 deletions(-) create mode 100644 types/types_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 8a6cb68..cc6067e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,8 +40,8 @@ go test -tags integration ./... # integration tests (need C++ te - `go test -race ./...`, `go vet ./...`, and `go mod tidy` must all pass before commit - Conventional commits: `type(scope): description` - Co-Author trailer: `Co-Authored-By: Charon ` -- Error strings: `package: description` format (e.g. `types: invalid hex for hash`) -- Error wrapping: `fmt.Errorf("package: description: %w", err)` +- Error handling: `coreerr.E("Caller", "description", err)` via `coreerr "forge.lthn.ai/core/go-log"` — not `fmt.Errorf` or `errors.New` +- File I/O: `coreio "forge.lthn.ai/core/go-io"` — not `os.ReadFile`/`os.WriteFile` - Import order: stdlib, then `golang.org/x`, then `forge.lthn.ai`, blank lines between groups - No emojis in code or comments diff --git a/chain/ring.go b/chain/ring.go index acee086..06645b0 100644 --- a/chain/ring.go +++ b/chain/ring.go @@ -38,7 +38,7 @@ func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicK case types.TxOutputBare: toKey, ok := out.Target.(types.TxOutToKey) if !ok { - return nil, fmt.Errorf("ring output %d: unsupported target type %T", i, out.Target) + return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: unsupported target type %T", i, out.Target), nil) } pubs[i] = toKey.Key default: diff --git a/consensus/block.go b/consensus/block.go index 5345b6f..dfaf16b 100644 --- a/consensus/block.go +++ b/consensus/block.go @@ -152,8 +152,8 @@ func expectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 { func checkBlockVersion(blk *types.Block, forks []config.HardFork, height uint64) error { expected := expectedBlockMajorVersion(forks, height) if blk.MajorVersion != expected { - return fmt.Errorf("%w: got %d, want %d at height %d", - ErrBlockMajorVersion, blk.MajorVersion, expected, height) + return coreerr.E("checkBlockVersion", fmt.Sprintf("got %d, want %d at height %d", + blk.MajorVersion, expected, height), ErrBlockMajorVersion) } return nil } @@ -226,7 +226,7 @@ func IsPreHardforkFreeze(forks []config.HardFork, version uint8, height uint64) func ValidateTransactionInBlock(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error { // Pre-hardfork freeze: reject non-coinbase transactions in the freeze window. if !isCoinbase(tx) && IsPreHardforkFreeze(forks, config.HF5, height) { - return fmt.Errorf("%w: height %d is within HF5 freeze window", ErrPreHardforkFreeze, height) + return coreerr.E("ValidateTransactionInBlock", fmt.Sprintf("height %d is within HF5 freeze window", height), ErrPreHardforkFreeze) } return ValidateTransaction(tx, txBlob, forks, height) diff --git a/consensus/v2sig_test.go b/consensus/v2sig_test.go index 78f066c..774f9a2 100644 --- a/consensus/v2sig_test.go +++ b/consensus/v2sig_test.go @@ -8,9 +8,11 @@ package consensus import ( "bytes" "encoding/hex" - "os" + "strings" "testing" + coreio "forge.lthn.ai/core/go-io" + "forge.lthn.ai/core/go-blockchain/config" "forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/wire" @@ -21,10 +23,10 @@ import ( // loadTestTx loads and decodes a hex-encoded transaction from testdata. func loadTestTx(t *testing.T, filename string) *types.Transaction { t.Helper() - hexData, err := os.ReadFile(filename) + hexStr, err := coreio.Read(coreio.Local, filename) require.NoError(t, err, "read %s", filename) - blob, err := hex.DecodeString(string(bytes.TrimSpace(hexData))) + blob, err := hex.DecodeString(strings.TrimSpace(hexStr)) require.NoError(t, err, "decode hex") dec := wire.NewDecoder(bytes.NewReader(blob)) diff --git a/crypto/clsag.go b/crypto/clsag.go index 9791f60..7cdfa1c 100644 --- a/crypto/clsag.go +++ b/crypto/clsag.go @@ -8,8 +8,9 @@ package crypto import "C" import ( - "errors" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // PointMul8 multiplies a curve point by the cofactor 8. @@ -20,7 +21,7 @@ func PointMul8(pk [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&result[0])), ) if rc != 0 { - return result, errors.New("crypto: point_mul8 failed") + return result, coreerr.E("PointMul8", "point_mul8 failed", nil) } return result, nil } @@ -34,7 +35,7 @@ func PointDiv8(pk [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&result[0])), ) if rc != 0 { - return result, errors.New("crypto: point_div8 failed") + return result, coreerr.E("PointDiv8", "point_div8 failed", nil) } return result, nil } @@ -48,7 +49,7 @@ func PointSub(a, b [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&result[0])), ) if rc != 0 { - return result, errors.New("crypto: point_sub failed") + return result, coreerr.E("PointSub", "point_sub failed", nil) } return result, nil } @@ -81,7 +82,7 @@ func GenerateCLSAGGG(hash [32]byte, ring []byte, ringSize int, (*C.uint8_t)(unsafe.Pointer(&sig[0])), ) if rc != 0 { - return nil, errors.New("crypto: generate_CLSAG_GG failed") + return nil, coreerr.E("GenerateCLSAGGG", "generate_CLSAG_GG failed", nil) } return sig, nil } diff --git a/crypto/keygen.go b/crypto/keygen.go index 5c11dd3..66f3065 100644 --- a/crypto/keygen.go +++ b/crypto/keygen.go @@ -8,9 +8,10 @@ package crypto import "C" import ( - "errors" "fmt" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // GenerateKeys creates a new random key pair. @@ -20,7 +21,7 @@ func GenerateKeys() (pub [32]byte, sec [32]byte, err error) { (*C.uint8_t)(unsafe.Pointer(&sec[0])), ) if rc != 0 { - err = fmt.Errorf("crypto: generate_keys failed (rc=%d)", rc) + err = coreerr.E("GenerateKeys", fmt.Sprintf("generate_keys failed (rc=%d)", rc), nil) } return } @@ -33,7 +34,7 @@ func SecretToPublic(sec [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&pub[0])), ) if rc != 0 { - return pub, fmt.Errorf("crypto: secret_to_public failed (rc=%d)", rc) + return pub, coreerr.E("SecretToPublic", fmt.Sprintf("secret_to_public failed (rc=%d)", rc), nil) } return pub, nil } @@ -52,7 +53,7 @@ func GenerateKeyDerivation(pub [32]byte, sec [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&d[0])), ) if rc != 0 { - return d, errors.New("crypto: generate_key_derivation failed") + return d, coreerr.E("GenerateKeyDerivation", "generate_key_derivation failed", nil) } return d, nil } @@ -67,7 +68,7 @@ func DerivePublicKey(derivation [32]byte, index uint64, base [32]byte) ([32]byte (*C.uint8_t)(unsafe.Pointer(&derived[0])), ) if rc != 0 { - return derived, errors.New("crypto: derive_public_key failed") + return derived, coreerr.E("DerivePublicKey", "derive_public_key failed", nil) } return derived, nil } @@ -82,7 +83,7 @@ func DeriveSecretKey(derivation [32]byte, index uint64, base [32]byte) ([32]byte (*C.uint8_t)(unsafe.Pointer(&derived[0])), ) if rc != 0 { - return derived, errors.New("crypto: derive_secret_key failed") + return derived, coreerr.E("DeriveSecretKey", "derive_secret_key failed", nil) } return derived, nil } diff --git a/crypto/keyimage.go b/crypto/keyimage.go index 961e116..26c74ee 100644 --- a/crypto/keyimage.go +++ b/crypto/keyimage.go @@ -8,8 +8,9 @@ package crypto import "C" import ( - "errors" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // GenerateKeyImage computes the key image for a public/secret key pair. @@ -22,7 +23,7 @@ func GenerateKeyImage(pub [32]byte, sec [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&ki[0])), ) if rc != 0 { - return ki, errors.New("crypto: generate_key_image failed") + return ki, coreerr.E("GenerateKeyImage", "generate_key_image failed", nil) } return ki, nil } diff --git a/crypto/pow.go b/crypto/pow.go index be8e84e..74f0a0a 100644 --- a/crypto/pow.go +++ b/crypto/pow.go @@ -10,6 +10,8 @@ import "C" import ( "fmt" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // RandomXHash computes the RandomX PoW hash. The key is the cache @@ -23,7 +25,7 @@ func RandomXHash(key, input []byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&output[0])), ) if ret != 0 { - return output, fmt.Errorf("crypto: RandomX hash failed with code %d", ret) + return output, coreerr.E("RandomXHash", fmt.Sprintf("RandomX hash failed with code %d", ret), nil) } return output, nil } diff --git a/crypto/signature.go b/crypto/signature.go index 664639a..f9699db 100644 --- a/crypto/signature.go +++ b/crypto/signature.go @@ -8,8 +8,9 @@ package crypto import "C" import ( - "errors" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // GenerateSignature creates a standard (non-ring) signature. @@ -22,7 +23,7 @@ func GenerateSignature(hash [32]byte, pub [32]byte, sec [32]byte) ([64]byte, err (*C.uint8_t)(unsafe.Pointer(&sig[0])), ) if rc != 0 { - return sig, errors.New("crypto: generate_signature failed") + return sig, coreerr.E("GenerateSignature", "generate_signature failed", nil) } return sig, nil } @@ -60,7 +61,7 @@ func GenerateRingSignature(hash [32]byte, image [32]byte, pubs [][32]byte, (*C.uint8_t)(unsafe.Pointer(&flatSigs[0])), ) if rc != 0 { - return nil, errors.New("crypto: generate_ring_signature failed") + return nil, coreerr.E("GenerateRingSignature", "generate_ring_signature failed", nil) } sigs := make([][64]byte, n) diff --git a/types/types_test.go b/types/types_test.go new file mode 100644 index 0000000..cecc21d --- /dev/null +++ b/types/types_test.go @@ -0,0 +1,106 @@ +// 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 types + +import ( + "strings" + "testing" +) + +func TestHashFromHex_Good(t *testing.T) { + hexStr := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + h, err := HashFromHex(hexStr) + if err != nil { + t.Fatalf("HashFromHex: unexpected error: %v", err) + } + if h[0] != 0x01 || h[1] != 0x23 { + t.Errorf("HashFromHex: got [0]=%02x [1]=%02x, want 01 23", h[0], h[1]) + } + if h.String() != hexStr { + t.Errorf("String: got %q, want %q", h.String(), hexStr) + } +} + +func TestHashFromHex_Bad(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"short", "0123"}, + {"invalid_chars", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}, + {"odd_length", "012"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := HashFromHex(tt.input) + if err == nil { + t.Error("expected error") + } + }) + } +} + +func TestHash_IsZero_Good(t *testing.T) { + var zero Hash + if !zero.IsZero() { + t.Error("zero hash: IsZero() should be true") + } + + nonZero := Hash{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + if nonZero.IsZero() { + t.Error("non-zero hash: IsZero() should be false") + } +} + +func TestPublicKeyFromHex_Good(t *testing.T) { + hexStr := strings.Repeat("ab", 32) + pk, err := PublicKeyFromHex(hexStr) + if err != nil { + t.Fatalf("PublicKeyFromHex: unexpected error: %v", err) + } + for i := range pk { + if pk[i] != 0xAB { + t.Fatalf("PublicKeyFromHex: byte %d = %02x, want 0xAB", i, pk[i]) + } + } + if pk.String() != hexStr { + t.Errorf("String: got %q, want %q", pk.String(), hexStr) + } +} + +func TestPublicKeyFromHex_Bad(t *testing.T) { + _, err := PublicKeyFromHex("tooshort") + if err == nil { + t.Error("expected error for short hex") + } +} + +func TestPublicKey_IsZero_Good(t *testing.T) { + var zero PublicKey + if !zero.IsZero() { + t.Error("zero key: IsZero() should be true") + } + nonZero := PublicKey{1} + if nonZero.IsZero() { + t.Error("non-zero key: IsZero() should be false") + } +} + +func TestSecretKey_String_Good(t *testing.T) { + sk := SecretKey{0xFF} + s := sk.String() + if !strings.HasPrefix(s, "ff") { + t.Errorf("String: got %q, want prefix ff", s) + } +} + +func TestKeyImage_String_Good(t *testing.T) { + ki := KeyImage{0xDE} + s := ki.String() + if !strings.HasPrefix(s, "de") { + t.Errorf("String: got %q, want prefix de", s) + } +} diff --git a/wire/transaction.go b/wire/transaction.go index 54c04fb..bac34be 100644 --- a/wire/transaction.go +++ b/wire/transaction.go @@ -164,6 +164,18 @@ func encodeInputs(enc *Encoder, vin []types.TxInput) { encodeKeyOffsets(enc, v.KeyOffsets) enc.WriteBlob32((*[32]byte)(&v.KeyImage)) enc.WriteBytes(v.EtcDetails) + case types.TxInputHTLC: + // Wire order: HTLCOrigin string BEFORE parent fields (C++ quirk). + encodeString(enc, v.HTLCOrigin) + enc.WriteVarint(v.Amount) + encodeKeyOffsets(enc, v.KeyOffsets) + enc.WriteBlob32((*[32]byte)(&v.KeyImage)) + enc.WriteBytes(v.EtcDetails) + case types.TxInputMultisig: + enc.WriteVarint(v.Amount) + enc.WriteBlob32((*[32]byte)(&v.MultisigOutID)) + enc.WriteVarint(v.SigsCount) + enc.WriteBytes(v.EtcDetails) } } } @@ -195,6 +207,21 @@ func decodeInputs(dec *Decoder) []types.TxInput { dec.ReadBlob32((*[32]byte)(&in.KeyImage)) in.EtcDetails = decodeRawVariantVector(dec) vin = append(vin, in) + case types.InputTypeHTLC: + var in types.TxInputHTLC + in.HTLCOrigin = decodeString(dec) + in.Amount = dec.ReadVarint() + in.KeyOffsets = decodeKeyOffsets(dec) + dec.ReadBlob32((*[32]byte)(&in.KeyImage)) + in.EtcDetails = decodeRawVariantVector(dec) + vin = append(vin, in) + case types.InputTypeMultisig: + var in types.TxInputMultisig + in.Amount = dec.ReadVarint() + dec.ReadBlob32((*[32]byte)(&in.MultisigOutID)) + in.SigsCount = dec.ReadVarint() + in.EtcDetails = decodeRawVariantVector(dec) + vin = append(vin, in) default: dec.err = coreerr.E("decodeInputs", fmt.Sprintf("wire: unsupported input tag 0x%02x", tag), nil) return vin @@ -241,6 +268,87 @@ func decodeKeyOffsets(dec *Decoder) []types.TxOutRef { return refs } +// --- string encoding --- + +// encodeString writes a varint-prefixed string. +func encodeString(enc *Encoder, s string) { + enc.WriteVarint(uint64(len(s))) + if len(s) > 0 { + enc.WriteBytes([]byte(s)) + } +} + +// decodeString reads a varint-prefixed string. +func decodeString(dec *Decoder) string { + n := dec.ReadVarint() + if n == 0 || dec.Err() != nil { + return "" + } + data := dec.ReadBytes(int(n)) + if dec.Err() != nil { + return "" + } + return string(data) +} + +// --- output targets --- + +// encodeTarget serialises a txout_target_v variant (tag + fields). +func encodeTarget(enc *Encoder, target types.TxOutTarget) { + enc.WriteVariantTag(target.TargetType()) + switch t := target.(type) { + case types.TxOutToKey: + enc.WriteBlob32((*[32]byte)(&t.Key)) + enc.WriteUint8(t.MixAttr) + case types.TxOutMultisig: + enc.WriteVarint(t.MinimumSigs) + enc.WriteVarint(uint64(len(t.Keys))) + for i := range t.Keys { + enc.WriteBlob32((*[32]byte)(&t.Keys[i])) + } + case types.TxOutHTLC: + enc.WriteBlob32((*[32]byte)(&t.HTLCHash)) + enc.WriteUint8(t.Flags) + enc.WriteVarint(t.Expiration) + enc.WriteBlob32((*[32]byte)(&t.PKRedeem)) + enc.WriteBlob32((*[32]byte)(&t.PKRefund)) + } +} + +// decodeTarget deserialises a txout_target_v from the given tag. +// Returns nil for unsupported tags (caller should set dec.err). +func decodeTarget(dec *Decoder, tag uint8) types.TxOutTarget { + switch tag { + case types.TargetTypeToKey: + var t types.TxOutToKey + dec.ReadBlob32((*[32]byte)(&t.Key)) + t.MixAttr = dec.ReadUint8() + return t + case types.TargetTypeMultisig: + var t types.TxOutMultisig + t.MinimumSigs = dec.ReadVarint() + n := dec.ReadVarint() + if dec.Err() != nil { + return nil + } + t.Keys = make([]types.PublicKey, n) + for i := uint64(0); i < n; i++ { + dec.ReadBlob32((*[32]byte)(&t.Keys[i])) + } + return t + case types.TargetTypeHTLC: + var t types.TxOutHTLC + dec.ReadBlob32((*[32]byte)(&t.HTLCHash)) + t.Flags = dec.ReadUint8() + t.Expiration = dec.ReadVarint() + dec.ReadBlob32((*[32]byte)(&t.PKRedeem)) + dec.ReadBlob32((*[32]byte)(&t.PKRefund)) + return t + default: + return nil + } +} + // --- outputs --- // encodeOutputsV1 serialises v0/v1 outputs. In v0/v1, outputs are tx_out_bare @@ -251,10 +359,7 @@ func encodeOutputsV1(enc *Encoder, vout []types.TxOutput) { switch v := out.(type) { case types.TxOutputBare: enc.WriteVarint(v.Amount) - // Target is a variant (txout_target_v) - enc.WriteVariantTag(types.TargetTypeToKey) - enc.WriteBlob32((*[32]byte)(&v.Target.Key)) - enc.WriteUint8(v.Target.MixAttr) + encodeTarget(enc, v.Target) } } } @@ -272,14 +377,15 @@ func decodeOutputsV1(dec *Decoder) []types.TxOutput { if dec.Err() != nil { return vout } - switch tag { - case types.TargetTypeToKey: - dec.ReadBlob32((*[32]byte)(&out.Target.Key)) - out.Target.MixAttr = dec.ReadUint8() - default: + target := decodeTarget(dec, tag) + if dec.Err() != nil { + return vout + } + if target == nil { dec.err = coreerr.E("decodeOutputsV1", fmt.Sprintf("wire: unsupported target tag 0x%02x", tag), nil) return vout } + out.Target = target vout = append(vout, out) } return vout @@ -293,9 +399,7 @@ func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) { switch v := out.(type) { case types.TxOutputBare: enc.WriteVarint(v.Amount) - enc.WriteVariantTag(types.TargetTypeToKey) - enc.WriteBlob32((*[32]byte)(&v.Target.Key)) - enc.WriteUint8(v.Target.MixAttr) + encodeTarget(enc, v.Target) case types.TxOutputZarcanum: enc.WriteBlob32((*[32]byte)(&v.StealthAddress)) enc.WriteBlob32((*[32]byte)(&v.ConcealingPoint)) @@ -323,13 +427,18 @@ func decodeOutputsV2(dec *Decoder) []types.TxOutput { var out types.TxOutputBare out.Amount = dec.ReadVarint() targetTag := dec.ReadVariantTag() - if targetTag == types.TargetTypeToKey { - dec.ReadBlob32((*[32]byte)(&out.Target.Key)) - out.Target.MixAttr = dec.ReadUint8() - } else { + if dec.Err() != nil { + return vout + } + target := decodeTarget(dec, targetTag) + if dec.Err() != nil { + return vout + } + if target == nil { dec.err = coreerr.E("decodeOutputsV2", fmt.Sprintf("wire: unsupported target tag 0x%02x", targetTag), nil) return vout } + out.Target = target vout = append(vout, out) case types.OutputTypeZarcanum: var out types.TxOutputZarcanum @@ -434,6 +543,14 @@ const ( tagZCAssetSurjectionProof = 46 // vector tagZCOutsRangeProof = 47 // bpp_serialized + aggregation_proof tagZCBalanceProof = 48 // generic_double_schnorr_sig_s (96 bytes) + + // Asset operation tags (HF5 confidential assets). + tagAssetOperationProof = 49 // asset_operation_proof + tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof + tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth + + // Extra variant tags (asset operations). + tagAssetDescriptorOperation = 40 // asset_descriptor_operation ) // readVariantElementData reads the data portion of a variant element (after the @@ -510,6 +627,16 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { case tagZCBalanceProof: // generic_double_schnorr_sig_s (3 scalars = 96 bytes) return dec.ReadBytes(96) + // Asset operation variants (HF5) + case tagAssetDescriptorOperation: + return readAssetDescriptorOperation(dec) + case tagAssetOperationProof: + return readAssetOperationProof(dec) + case tagAssetOperationOwnershipProof: + return readAssetOperationOwnershipProof(dec) + case tagAssetOperationOwnershipProofETH: + return readAssetOperationOwnershipProofETH(dec) + default: dec.err = coreerr.E("readVariantElementData", fmt.Sprintf("wire: unsupported variant tag 0x%02x (%d)", tag, tag), nil) return nil @@ -928,3 +1055,178 @@ func readZCOutsRangeProof(dec *Decoder) []byte { raw = append(raw, v...) return raw } + +// --- asset operation readers (HF5) --- + +// readAssetDescriptorOperation reads asset_descriptor_operation (tag 40). +// Wire: ver(uint8) + operation_type(uint8) + opt_asset_id(optional 32-byte hash) +// + opt_descriptor(optional AssetDescriptorBase) + amount_to_emit(uint64 LE) +// + amount_to_burn(uint64 LE) + etc(vector). +func readAssetDescriptorOperation(dec *Decoder) []byte { + var raw []byte + // ver: uint8 + 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 { + // AssetDescriptorBase: ticker(string) + full_name(string) + // + total_max_supply(uint64 LE) + current_supply(uint64 LE) + // + decimal_point(uint8) + meta_info(string) + owner_key(32) + // + etc(vector) + for range 2 { + s := readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, s...) + } + b := dec.ReadBytes(8 + 8 + 1) // total_max_supply + current_supply + decimal_point + if dec.err != nil { + return nil + } + raw = append(raw, b...) + s := readStringBlob(dec) // meta_info + if dec.err != nil { + return nil + } + raw = append(raw, s...) + b = dec.ReadBytes(32) // owner_key + if dec.err != nil { + return nil + } + raw = append(raw, b...) + v := readVariantVectorFixed(dec, 1) // etc + if dec.err != nil { + return nil + } + raw = append(raw, v...) + } + // 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 +} + +// readAssetOperationProof reads asset_operation_proof (tag 49). +// Wire: ver(uint8) + gss(generic_schnorr_sig_s = 64 bytes) + asset_id(32 bytes) +// + etc(vector). +func readAssetOperationProof(dec *Decoder) []byte { + var raw []byte + // ver: uint8 + b := dec.ReadBytes(1) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // gss: generic_schnorr_sig_s (s + c = 64 bytes) + b = dec.ReadBytes(64) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // asset_id: 32 bytes + 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 +} + +// readAssetOperationOwnershipProof reads asset_operation_ownership_proof (tag 50). +// Wire: ver(uint8) + gss(generic_schnorr_sig_s = 64 bytes) + etc(vector). +func readAssetOperationOwnershipProof(dec *Decoder) []byte { + var raw []byte + // ver: uint8 + b := dec.ReadBytes(1) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // gss: generic_schnorr_sig_s (64 bytes) + b = dec.ReadBytes(64) + 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 +} + +// readAssetOperationOwnershipProofETH reads asset_operation_ownership_proof_eth (tag 51). +// Wire: ver(uint8) + eth_sig(65 bytes: r=32 + s=32 + v=1) + etc(vector). +func readAssetOperationOwnershipProofETH(dec *Decoder) []byte { + var raw []byte + // ver: uint8 + b := dec.ReadBytes(1) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // eth_sig: 65 bytes + b = dec.ReadBytes(65) + 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 +}