From f88d582c64cda3939d9df90beedc2d093147e7d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 20:38:28 +0000 Subject: [PATCH] feat(consensus): verify NLSAG signatures for HTLC inputs verifyV1Signatures now counts and verifies TxInputHTLC alongside TxInputToKey. HTLC inputs use the same ring signature scheme. Co-Authored-By: Charon --- consensus/verify.go | 31 +++++++++++++----- consensus/verify_test.go | 70 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/consensus/verify.go b/consensus/verify.go index 9378f58..665d704 100644 --- a/consensus/verify.go +++ b/consensus/verify.go @@ -58,11 +58,13 @@ func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork, } // verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions. +// Both TxInputToKey and TxInputHTLC use NLSAG ring signatures. func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) error { - // Count key inputs. + // Count ring-sig inputs (TxInputToKey and TxInputHTLC). var keyInputCount int for _, vin := range tx.Vin { - if _, ok := vin.(types.TxInputToKey); ok { + switch vin.(type) { + case types.TxInputToKey, types.TxInputHTLC: keyInputCount++ } } @@ -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 { + // Extract the common ring-sig fields from either input type. + 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 } // 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 fmt.Errorf("consensus: failed to fetch ring outputs for input %d: %w", sigIdx, err) @@ -116,7 +131,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 fmt.Errorf("consensus: ring signature verification failed for input %d", sigIdx) } diff --git a/consensus/verify_test.go b/consensus/verify_test.go index f6adab4..53562eb 100644 --- a/consensus/verify_test.go +++ b/consensus/verify_test.go @@ -6,6 +6,7 @@ import ( "testing" "forge.lthn.ai/core/go-blockchain/config" + "forge.lthn.ai/core/go-blockchain/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,3 +24,72 @@ func TestVerifyTransactionSignatures_Bad_MissingSigs(t *testing.T) { err := VerifyTransactionSignatures(tx, config.MainnetForks, 100, nil, nil) assert.Error(t, err) } + +// --- HTLC signature verification tests (Task 9) --- + +func TestVerifyV1Signatures_MixedHTLC_Good(t *testing.T) { + // Structural check only (getRingOutputs = nil). + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}}, + types.TxInputHTLC{Amount: 50, KeyImage: types.KeyImage{2}}, + }, + Signatures: [][]types.Signature{ + {{1}}, // sig for TxInputToKey + {{2}}, // sig for TxInputHTLC + }, + } + err := VerifyTransactionSignatures(tx, config.MainnetForks, 20000, nil, nil) + require.NoError(t, err) +} + +func TestVerifyV1Signatures_MixedHTLC_Bad(t *testing.T) { + // Wrong signature count. + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}}, + types.TxInputHTLC{Amount: 50, KeyImage: types.KeyImage{2}}, + }, + Signatures: [][]types.Signature{ + {{1}}, // only 1 sig for 2 ring inputs + }, + } + err := VerifyTransactionSignatures(tx, config.MainnetForks, 20000, nil, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "signature count") +} + +func TestVerifyV1Signatures_HTLCOnly_Good(t *testing.T) { + // Transaction with only HTLC inputs. + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputHTLC{Amount: 50, KeyImage: types.KeyImage{1}}, + types.TxInputHTLC{Amount: 30, KeyImage: types.KeyImage{2}}, + }, + Signatures: [][]types.Signature{ + {{1}}, + {{2}}, + }, + } + err := VerifyTransactionSignatures(tx, config.MainnetForks, 20000, nil, nil) + require.NoError(t, err) +} + +func TestVerifyV1Signatures_MultisigSkipped_Good(t *testing.T) { + // Multisig inputs do not participate in NLSAG signatures. + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}}, + types.TxInputMultisig{Amount: 50}, + }, + Signatures: [][]types.Signature{ + {{1}}, // only 1 sig, multisig is not counted + }, + } + err := VerifyTransactionSignatures(tx, config.MainnetForks, 20000, nil, nil) + require.NoError(t, err) +}