From 70fab6f7d0aade9bbd5654dfff6553e5d7e97ece Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 21:32:33 +0000 Subject: [PATCH] fix: restore HF5 asset tags, HTLC/multisig inputs, and tx version check after conventions sweep The conventions sweep (71f0a5c) overwrote HF5 code and removed HTLC/multisig input handling. This commit restores: - wire: HF5 asset wire tags (40/49/50/51) and reader functions for asset_descriptor_operation, asset_operation_proof, asset_operation_ownership_proof, and asset_operation_ownership_proof_eth - wire: HTLC and multisig input encode/decode with string field helpers - consensus: checkTxVersion enforcing version 3 after HF5 / rejecting before - consensus: HF1-gated acceptance of HTLC and multisig input/output types - consensus: HTLC key image deduplication in checkKeyImages - consensus: HTLC ring signature counting in verifyV1Signatures - chain: corrected error assertion in TestChain_GetBlockByHeight_NotFound All 14 packages pass go test -race ./... Co-Authored-By: Charon --- chain/chain_test.go | 5 +- consensus/tx.go | 67 +++++++++-- consensus/verify.go | 41 +++++-- go.mod | 4 +- wire/transaction.go | 285 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 375 insertions(+), 27 deletions(-) diff --git a/chain/chain_test.go b/chain/chain_test.go index 5ec3975..b6e81c3 100644 --- a/chain/chain_test.go +++ b/chain/chain_test.go @@ -219,8 +219,9 @@ func TestChain_GetBlockByHeight_NotFound(t *testing.T) { if err == nil { t.Fatal("GetBlockByHeight(99): expected error, got nil") } - if got := err.Error(); got != "chain: block 99 not found" { - t.Errorf("error message: got %q, want %q", got, "chain: block 99 not found") + want := "Chain.GetBlockByHeight: chain: block 99 not found" + if got := err.Error(); got != want { + t.Errorf("error message: got %q, want %q", got, want) } } diff --git a/consensus/tx.go b/consensus/tx.go index f978b2f..bcfa9da 100644 --- a/consensus/tx.go +++ b/consensus/tx.go @@ -19,6 +19,11 @@ import ( func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error { hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height) + // 0. Transaction version. + if err := checkTxVersion(tx, forks, height); err != nil { + return err + } + // 1. Blob size. if uint64(len(txBlob)) >= config.MaxTransactionBlobSize { return coreerr.E("ValidateTransaction", fmt.Sprintf("%d bytes", len(txBlob)), ErrTxTooLarge) @@ -32,13 +37,15 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha return coreerr.E("ValidateTransaction", fmt.Sprintf("%d", len(tx.Vin)), ErrTooManyInputs) } + hf1Active := config.IsHardForkActive(forks, config.HF1, height) + // 3. Input types — TxInputGenesis not allowed in regular transactions. - if err := checkInputTypes(tx, hf4Active); err != nil { + if err := checkInputTypes(tx, hf1Active, hf4Active); err != nil { return err } // 4. Output validation. - if err := checkOutputs(tx, hf4Active); err != nil { + if err := checkOutputs(tx, hf1Active, hf4Active); err != nil { return err } @@ -65,15 +72,43 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha return nil } -func checkInputTypes(tx *types.Transaction, hf4Active bool) error { +// checkTxVersion validates that the transaction version is appropriate for the +// current hardfork era. +// +// After HF5: transaction version must be >= VersionPostHF5 (3). +// Before HF5: transaction version 3 is rejected (too early). +func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint64) error { + hf5Active := config.IsHardForkActive(forks, config.HF5, height) + + if hf5Active && tx.Version < types.VersionPostHF5 { + return coreerr.E("checkTxVersion", + fmt.Sprintf("version %d too low after HF5 at height %d", tx.Version, height), + ErrTxVersionInvalid) + } + + if !hf5Active && tx.Version >= types.VersionPostHF5 { + return coreerr.E("checkTxVersion", + fmt.Sprintf("version %d not allowed before HF5 at height %d", tx.Version, height), + ErrTxVersionInvalid) + } + + return nil +} + +func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error { for _, vin := range tx.Vin { switch vin.(type) { case types.TxInputToKey: // Always valid. case types.TxInputGenesis: return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType) + case types.TxInputHTLC, types.TxInputMultisig: + // HTLC and multisig inputs require at least HF1. + if !hf1Active { + return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF1", vin.InputType()), ErrInvalidInputType) + } default: - // Future types (multisig, HTLC, ZC) — accept if HF4+. + // Future types (ZC) — accept if HF4+. if !hf4Active { return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF4", vin.InputType()), ErrInvalidInputType) } @@ -82,7 +117,7 @@ func checkInputTypes(tx *types.Transaction, hf4Active bool) error { return nil } -func checkOutputs(tx *types.Transaction, hf4Active bool) error { +func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error { if len(tx.Vout) == 0 { return ErrNoOutputs } @@ -101,6 +136,13 @@ func checkOutputs(tx *types.Transaction, hf4Active bool) error { if o.Amount == 0 { return coreerr.E("checkOutputs", fmt.Sprintf("output %d has zero amount", i), ErrInvalidOutput) } + // HTLC and Multisig output targets require at least HF1. + switch o.Target.(type) { + case types.TxOutHTLC, types.TxOutMultisig: + if !hf1Active { + return coreerr.E("checkOutputs", fmt.Sprintf("output %d: HTLC/multisig target pre-HF1", i), ErrInvalidOutput) + } + } case types.TxOutputZarcanum: // Validated by proof verification. } @@ -112,14 +154,19 @@ func checkOutputs(tx *types.Transaction, hf4Active bool) error { func checkKeyImages(tx *types.Transaction) error { seen := make(map[types.KeyImage]struct{}) for _, vin := range tx.Vin { - toKey, ok := vin.(types.TxInputToKey) - if !ok { + var ki types.KeyImage + switch v := vin.(type) { + case types.TxInputToKey: + ki = v.KeyImage + case types.TxInputHTLC: + ki = v.KeyImage + default: continue } - if _, exists := seen[toKey.KeyImage]; exists { - return coreerr.E("checkKeyImages", toKey.KeyImage.String(), ErrDuplicateKeyImage) + if _, exists := seen[ki]; exists { + return coreerr.E("checkKeyImages", ki.String(), ErrDuplicateKeyImage) } - seen[toKey.KeyImage] = struct{}{} + seen[ki] = struct{}{} } return nil } diff --git a/consensus/verify.go b/consensus/verify.go index a7f87d0..8fd1925 100644 --- a/consensus/verify.go +++ b/consensus/verify.go @@ -60,16 +60,18 @@ func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork, // verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions. func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) error { - // Count key inputs. - var keyInputCount int + // Count ring-signing inputs (TxInputToKey and TxInputHTLC contribute + // ring signatures; TxInputMultisig does not). + var ringInputCount int for _, vin := range tx.Vin { - if _, ok := vin.(types.TxInputToKey); ok { - keyInputCount++ + switch vin.(type) { + case types.TxInputToKey, types.TxInputHTLC: + ringInputCount++ } } - if len(tx.Signatures) != keyInputCount { - return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: signature count %d != input count %d", len(tx.Signatures), keyInputCount), nil) + if len(tx.Signatures) != ringInputCount { + return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: signature count %d != input count %d", len(tx.Signatures), ringInputCount), nil) } // Actual NLSAG verification requires the crypto bridge and ring outputs. @@ -82,18 +84,31 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err var sigIdx int for _, vin := range tx.Vin { - inp, ok := vin.(types.TxInputToKey) - if !ok { - continue + // Extract amount and key offsets from ring-signing input types. + var amount uint64 + var keyOffsets []types.TxOutRef + var keyImage types.KeyImage + + switch v := vin.(type) { + case types.TxInputToKey: + amount = v.Amount + keyOffsets = v.KeyOffsets + keyImage = v.KeyImage + case types.TxInputHTLC: + amount = v.Amount + keyOffsets = v.KeyOffsets + keyImage = v.KeyImage + default: + continue // TxInputMultisig and others do not use NLSAG } // Extract absolute global indices from key offsets. - offsets := make([]uint64, len(inp.KeyOffsets)) - for i, ref := range inp.KeyOffsets { + offsets := make([]uint64, len(keyOffsets)) + for i, ref := range keyOffsets { offsets[i] = ref.GlobalIndex } - ringKeys, err := getRingOutputs(inp.Amount, offsets) + ringKeys, err := getRingOutputs(amount, offsets) if err != nil { return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: failed to fetch ring outputs for input %d", sigIdx), err) } @@ -114,7 +129,7 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err sigs[i] = [64]byte(s) } - if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(inp.KeyImage), pubs, sigs) { + if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(keyImage), pubs, sigs) { return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: ring signature verification failed for input %d", sigIdx), nil) } diff --git a/go.mod b/go.mod index 803a716..e249f67 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.26.0 require ( forge.lthn.ai/core/cli v0.3.1 + forge.lthn.ai/core/go-io v0.1.2 + forge.lthn.ai/core/go-log v0.0.4 forge.lthn.ai/core/go-p2p v0.1.3 forge.lthn.ai/core/go-process v0.2.3 forge.lthn.ai/core/go-store v0.1.6 @@ -18,8 +20,6 @@ require ( forge.lthn.ai/core/go-crypt v0.1.7 // indirect forge.lthn.ai/core/go-i18n v0.1.4 // indirect forge.lthn.ai/core/go-inference v0.1.4 // indirect - forge.lthn.ai/core/go-io v0.1.2 // indirect - forge.lthn.ai/core/go-log v0.0.4 // indirect github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect diff --git a/wire/transaction.go b/wire/transaction.go index 06826a0..829e071 100644 --- a/wire/transaction.go +++ b/wire/transaction.go @@ -160,6 +160,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) is serialised before parent fields. + encodeStringField(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) case types.TxInputZC: encodeKeyOffsets(enc, v.KeyOffsets) enc.WriteBlob32((*[32]byte)(&v.KeyImage)) @@ -189,6 +201,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 = decodeStringField(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) case types.InputTypeZC: var in types.TxInputZC in.KeyOffsets = decodeKeyOffsets(dec) @@ -503,6 +530,12 @@ const ( tagVoidSig = 44 // void_sig — empty tagZarcanumSig = 45 // zarcanum_sig — complex + // Asset operation tags (HF5 confidential assets). + tagAssetDescriptorOperation = 40 // asset_descriptor_operation + tagAssetOperationProof = 49 // asset_operation_proof + tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof + tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth + // Proof variant tags (proof_v). tagZCAssetSurjectionProof = 46 // vector tagZCOutsRangeProof = 47 // bpp_serialized + aggregation_proof @@ -575,6 +608,16 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { case tagZarcanumSig: // complex: 10 scalars + bppe + public_key + CLSAG_GGXXG return readZarcanumSig(dec) + // Asset operation variants (HF5) + case tagAssetDescriptorOperation: + return readAssetDescriptorOperation(dec) + case tagAssetOperationProof: + return readAssetOperationProof(dec) + case tagAssetOperationOwnershipProof: + return readAssetOperationOwnershipProof(dec) + case tagAssetOperationOwnershipProofETH: + return readAssetOperationOwnershipProofETH(dec) + // Proof variants case tagZCAssetSurjectionProof: // vector return readZCAssetSurjectionProof(dec) @@ -589,6 +632,28 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { } } +// encodeStringField writes a string as a varint length prefix followed by +// the UTF-8 bytes. +func encodeStringField(enc *Encoder, s string) { + enc.WriteVarint(uint64(len(s))) + if len(s) > 0 { + enc.WriteBytes([]byte(s)) + } +} + +// decodeStringField reads a varint-prefixed string and returns the Go string. +func decodeStringField(dec *Decoder) string { + length := dec.ReadVarint() + if dec.err != nil || length == 0 { + return "" + } + data := dec.ReadBytes(int(length)) + if dec.err != nil { + return "" + } + return string(data) +} + // readStringBlob reads a varint-prefixed string and returns the raw bytes // including the length varint. func readStringBlob(dec *Decoder) []byte { @@ -965,6 +1030,226 @@ func readZarcanumSig(dec *Decoder) []byte { // --- proof variant readers --- +// --- HF5 asset operation readers --- + +// readAssetDescriptorOperation reads asset_descriptor_operation (tag 40). +// Wire: version(uint8) + operation_type(uint8) + opt_asset_id(uint8 marker +// + 32 bytes if present) + opt_descriptor(uint8 marker + descriptor if +// present) + amount_to_emit(uint64 LE) + amount_to_burn(uint64 LE) + +// etc(vector). +// +// Descriptor (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 + 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: uint8 presence marker + 32 bytes if present + assetMarker := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, assetMarker) + if assetMarker != 0 { + b := dec.ReadBytes(32) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + } + + // opt_descriptor: uint8 presence marker + descriptor if present + descMarker := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, descMarker) + if descMarker != 0 { + // AssetDescriptorBase + // 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 + 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...) + } + + // 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: version(uint8) + generic_schnorr_sig_s(64 bytes) + asset_id(32 bytes) +// + etc(vector). +func readAssetOperationProof(dec *Decoder) []byte { + var raw []byte + + // ver: uint8 + ver := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, ver) + + // gss: generic_schnorr_sig_s — 2 scalars (s, c) = 64 bytes + b := dec.ReadBytes(64) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + + // asset_id: 32-byte hash + 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: version(uint8) + generic_schnorr_sig_s(64 bytes) + etc(vector). +func readAssetOperationOwnershipProof(dec *Decoder) []byte { + var raw []byte + + // ver: uint8 + ver := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, ver) + + // gss: generic_schnorr_sig_s — 2 scalars (s, c) = 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: version(uint8) + eth_sig(65 bytes) + etc(vector). +func readAssetOperationOwnershipProofETH(dec *Decoder) []byte { + var raw []byte + + // ver: uint8 + ver := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, ver) + + // eth_sig: 65 bytes (r=32 + s=32 + v=1) + 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 +} + +// --- proof variant readers --- + // readZCAssetSurjectionProof reads zc_asset_surjection_proof (tag 46). // Wire: varint(count) + count * BGE_proof_s. func readZCAssetSurjectionProof(dec *Decoder) []byte {