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 <charon@lethean.io>
This commit is contained in:
Claude 2026-03-16 20:38:28 +00:00
parent 192d681ecd
commit f88d582c64
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 93 additions and 8 deletions

View file

@ -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)
}

View file

@ -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)
}