feat(consensus): wire up NLSAG ring signature verification

Connect crypto.CheckRingSignature() to verifyV1Signatures() so
pre-HF4 transactions have their ring signatures cryptographically
verified when a RingOutputsFn callback is provided.

Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
Claude 2026-02-21 20:26:33 +00:00
parent ca3d8e04d1
commit 2c6211d78e
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 138 additions and 1 deletions

View file

@ -9,7 +9,9 @@ import (
"fmt"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
)
// RingOutputsFn fetches the public keys for a ring at the given amount
@ -60,7 +62,51 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err
return nil
}
// TODO: Wire up crypto.CheckRingSignature() for each input.
prefixHash := wire.TransactionPrefixHash(tx)
var sigIdx int
for _, vin := range tx.Vin {
inp, ok := vin.(types.TxInputToKey)
if !ok {
continue
}
// Extract absolute global indices from key offsets.
offsets := make([]uint64, len(inp.KeyOffsets))
for i, ref := range inp.KeyOffsets {
offsets[i] = ref.GlobalIndex
}
ringKeys, err := getRingOutputs(inp.Amount, offsets)
if err != nil {
return fmt.Errorf("consensus: failed to fetch ring outputs for input %d: %w",
sigIdx, err)
}
ringSigs := tx.Signatures[sigIdx]
if len(ringSigs) != len(ringKeys) {
return fmt.Errorf("consensus: input %d has %d signatures but ring size %d",
sigIdx, len(ringSigs), len(ringKeys))
}
// Convert typed slices to raw byte arrays for the crypto bridge.
pubs := make([][32]byte, len(ringKeys))
for i, pk := range ringKeys {
pubs[i] = [32]byte(pk)
}
sigs := make([][64]byte, len(ringSigs))
for i, s := range ringSigs {
sigs[i] = [64]byte(s)
}
if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(inp.KeyImage), pubs, sigs) {
return fmt.Errorf("consensus: ring signature verification failed for input %d", sigIdx)
}
sigIdx++
}
return nil
}

View file

@ -0,0 +1,91 @@
// 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 consensus
import (
"testing"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVerifyV1Signatures_Good_MockRing(t *testing.T) {
pub, sec, err := crypto.GenerateKeys()
require.NoError(t, err)
ki, err := crypto.GenerateKeyImage(pub, sec)
require.NoError(t, err)
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{
Amount: 100,
KeyOffsets: []types.TxOutRef{
{Tag: types.RefTypeGlobalIndex, GlobalIndex: 0},
},
KeyImage: types.KeyImage(ki),
},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey(pub)}},
},
}
prefixHash := wire.TransactionPrefixHash(tx)
sigs, err := crypto.GenerateRingSignature(
[32]byte(prefixHash), ki, [][32]byte{pub}, sec, 0)
require.NoError(t, err)
tx.Signatures = [][]types.Signature{make([]types.Signature, 1)}
tx.Signatures[0][0] = types.Signature(sigs[0])
getRing := func(amount uint64, offsets []uint64) ([]types.PublicKey, error) {
return []types.PublicKey{types.PublicKey(pub)}, nil
}
err = VerifyTransactionSignatures(tx, config.MainnetForks, 100, getRing)
require.NoError(t, err)
}
func TestVerifyV1Signatures_Bad_WrongSig(t *testing.T) {
pub, sec, err := crypto.GenerateKeys()
require.NoError(t, err)
ki, err := crypto.GenerateKeyImage(pub, sec)
require.NoError(t, err)
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{
Amount: 100,
KeyOffsets: []types.TxOutRef{
{Tag: types.RefTypeGlobalIndex, GlobalIndex: 0},
},
KeyImage: types.KeyImage(ki),
},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey(pub)}},
},
Signatures: [][]types.Signature{
{types.Signature{}},
},
}
getRing := func(amount uint64, offsets []uint64) ([]types.PublicKey, error) {
return []types.PublicKey{types.PublicKey(pub)}, nil
}
err = VerifyTransactionSignatures(tx, config.MainnetForks, 100, getRing)
assert.Error(t, err)
}