mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
bridgevm: one acceptance predicate for a block MPC signature
batchVerifyBlockSignatures had two answers to "is this signature valid", chosen
by accel.Available():
- the CPU loop verified against blockHash[:] (accel_verify.go:69) while the GPU
layout verified against sha256(blockHash[:]) (accel_verify.go:96,107). Same
signature set, two different signed messages, so a node with an accelerator
and a node without reached opposite verdicts on the same block —
bridgevm/block.go:219 turns validSignatures into accept or reject, so this is
an accept/reject split decided by host hardware.
- a signature shorter than 64 bytes, or a public key that would not marshal,
was silently zero-filled (`if len >= sigSize`, `if mErr != nil { continue }`)
while the entry kept its row in the result vector, and validCount was taken
straight from the kernel's output for that row. The CPU path rejects those
entries outright via deserializeSignature. Correctness of the count therefore
rested on an out-of-tree kernel refusing degenerate input, and one
spuriously-true row is enough to admit a block.
The predicate is now verifyBlockSig, and the CPU loop is its only evaluator.
packBlockSigBatch lays out the accelerator's rows over the same message bytes and
returns an error for any row it cannot represent exactly, which sends the batch to
that one authority instead of guessing.
Tests: TestBatchSignsTheBlockHashItself, TestBatchRefusesRowsItCannotRepresent,
TestVerifyBlockSigIsTheOneAuthority, TestBatchVerifyCountsNoSignatureWithoutConfig.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+64
-31
@@ -4,7 +4,6 @@
|
||||
package bridgevm
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -61,18 +60,25 @@ func batchVerifyBlockSignatures(
|
||||
// CPU fallback: sequential
|
||||
validCount := 0
|
||||
for _, e := range entries {
|
||||
sig, err := deserializeSignature(mpcCfg.Group, e.sigBytes)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pubInfo := mpcCfg.Public[e.partyID]
|
||||
if sig.Verify(pubInfo.ECDSA, blockHash[:]) {
|
||||
if verifyBlockSig(mpcCfg, blockHash, e) {
|
||||
validCount++
|
||||
}
|
||||
}
|
||||
return validCount
|
||||
}
|
||||
|
||||
// verifyBlockSig is the ONE acceptance predicate for a block MPC signature: is
|
||||
// sigBytes a valid signature by this party's key over the block hash? Both the
|
||||
// CPU loop and the GPU batch answer this same question — the accelerator only
|
||||
// batches it, it never redefines it.
|
||||
func verifyBlockSig(mpcCfg *config.Config, blockHash ids.ID, e sigEntry) bool {
|
||||
sig, err := deserializeSignature(mpcCfg.Group, e.sigBytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return sig.Verify(mpcCfg.Public[e.partyID].ECDSA, blockHash[:])
|
||||
}
|
||||
|
||||
// sigEntry holds a signature entry for batch verification.
|
||||
type sigEntry struct {
|
||||
nodeID ids.NodeID
|
||||
@@ -80,6 +86,53 @@ type sigEntry struct {
|
||||
partyID party.ID
|
||||
}
|
||||
|
||||
// Tensor row widths for the block-signature batch.
|
||||
const (
|
||||
batchHashSize = 32
|
||||
batchSigSize = 64
|
||||
batchPKSize = 33
|
||||
)
|
||||
|
||||
// packBlockSigBatch lays out the (message, signature, public key) rows the ECDSA
|
||||
// batch kernel consumes, one row per entry.
|
||||
//
|
||||
// The message is the block hash ITSELF, byte for byte what verifyBlockSig hands
|
||||
// to sig.Verify. Hashing it again here would have the accelerator check a
|
||||
// different message than the CPU, so one signature set would be valid on a GPU
|
||||
// node and invalid on a node without one — an accept/reject split decided by host
|
||||
// hardware.
|
||||
//
|
||||
// Every row of the result vector must carry real inputs. Zero-filling a row whose
|
||||
// signature or public key could not be prepared, and then trusting the kernel's
|
||||
// verdict for that row, counts a signature the CPU path rejects outright. Anything
|
||||
// this layout cannot represent exactly is an error, which sends the whole batch to
|
||||
// the CPU — the one acceptance authority.
|
||||
func packBlockSigBatch(blockHash ids.ID, entries []sigEntry, mpcCfg *config.Config) (messages, sigs, pubkeys []byte, err error) {
|
||||
n := len(entries)
|
||||
messages = make([]byte, n*batchHashSize)
|
||||
sigs = make([]byte, n*batchSigSize)
|
||||
pubkeys = make([]byte, n*batchPKSize)
|
||||
|
||||
for i, e := range entries {
|
||||
copy(messages[i*batchHashSize:], blockHash[:])
|
||||
|
||||
if len(e.sigBytes) < batchSigSize {
|
||||
return nil, nil, nil, fmt.Errorf("signature for %s is %d bytes, want at least %d", e.partyID, len(e.sigBytes), batchSigSize)
|
||||
}
|
||||
copy(sigs[i*batchSigSize:], e.sigBytes[:batchSigSize])
|
||||
|
||||
pkBytes, mErr := mpcCfg.Public[e.partyID].ECDSA.MarshalBinary()
|
||||
if mErr != nil {
|
||||
return nil, nil, nil, fmt.Errorf("public key for %s: %w", e.partyID, mErr)
|
||||
}
|
||||
if len(pkBytes) < batchPKSize {
|
||||
return nil, nil, nil, fmt.Errorf("public key for %s is %d bytes, want %d", e.partyID, len(pkBytes), batchPKSize)
|
||||
}
|
||||
copy(pubkeys[i*batchPKSize:], pkBytes[:batchPKSize])
|
||||
}
|
||||
return messages, sigs, pubkeys, nil
|
||||
}
|
||||
|
||||
// batchVerifyECDSABlockGPU runs GPU-accelerated ECDSA batch verification for block signatures.
|
||||
func batchVerifyECDSABlockGPU(
|
||||
blockHash ids.ID,
|
||||
@@ -93,31 +146,11 @@ func batchVerifyECDSABlockGPU(
|
||||
}
|
||||
|
||||
n := len(entries)
|
||||
msgHash := sha256.Sum256(blockHash[:])
|
||||
hashSize, sigSize, pkSize := batchHashSize, batchSigSize, batchPKSize
|
||||
|
||||
const hashSize = 32
|
||||
const sigSize = 64
|
||||
const pkSize = 33
|
||||
|
||||
messages := make([]byte, n*hashSize)
|
||||
sigs := make([]byte, n*sigSize)
|
||||
pubkeys := make([]byte, n*pkSize)
|
||||
|
||||
for i, e := range entries {
|
||||
copy(messages[i*hashSize:], msgHash[:])
|
||||
|
||||
if len(e.sigBytes) >= sigSize {
|
||||
copy(sigs[i*sigSize:], e.sigBytes[:sigSize])
|
||||
}
|
||||
|
||||
pubInfo := mpcCfg.Public[e.partyID]
|
||||
pkBytes, mErr := pubInfo.ECDSA.MarshalBinary()
|
||||
if mErr != nil {
|
||||
continue
|
||||
}
|
||||
if len(pkBytes) >= pkSize {
|
||||
copy(pubkeys[i*pkSize:], pkBytes[:pkSize])
|
||||
}
|
||||
messages, sigs, pubkeys, err := packBlockSigBatch(blockHash, entries, mpcCfg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
msgTensor, err := accel.NewTensorWithData[byte](session, []int{n, hashSize}, messages)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package bridgevm
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/threshold/pkg/math/curve"
|
||||
"github.com/luxfi/threshold/pkg/party"
|
||||
"github.com/luxfi/threshold/protocols/cmp/config"
|
||||
)
|
||||
|
||||
// twoPartyConfig returns a CMP config carrying real secp256k1 public shares for
|
||||
// two parties, which is everything the block-signature batch reads.
|
||||
func twoPartyConfig(t *testing.T) (*config.Config, []party.ID) {
|
||||
t.Helper()
|
||||
group := curve.Secp256k1{}
|
||||
ids := []party.ID{"party-a", "party-b"}
|
||||
pub := make(map[party.ID]*config.Public, len(ids))
|
||||
for _, id := range ids {
|
||||
pub[id] = &config.Public{ECDSA: randomScalar(t, group).ActOnBase()}
|
||||
}
|
||||
return &config.Config{Group: group, Public: pub}, ids
|
||||
}
|
||||
|
||||
func randomScalar(t *testing.T, group curve.Curve) curve.Scalar {
|
||||
t.Helper()
|
||||
s := group.NewScalar()
|
||||
b := make([]byte, 32)
|
||||
_, err := rand.Read(b)
|
||||
require.NoError(t, err)
|
||||
b[0] &= 0x7f // stay below the group order
|
||||
require.NoError(t, s.UnmarshalBinary(b))
|
||||
return s
|
||||
}
|
||||
|
||||
func entriesFor(sigLen int, partyIDs []party.ID) []sigEntry {
|
||||
out := make([]sigEntry, 0, len(partyIDs))
|
||||
for _, id := range partyIDs {
|
||||
out = append(out, sigEntry{partyID: id, sigBytes: make([]byte, sigLen)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestBatchSignsTheBlockHashItself pins the message the accelerated batch checks
|
||||
// to the message the CPU path checks. The GPU layout hashed the block hash a
|
||||
// second time, so the batch verified signatures over sha256(blockHash) while
|
||||
// verifyBlockSig verified them over blockHash — the same signature set was valid
|
||||
// on a node with an accelerator and invalid on a node without one, which splits
|
||||
// accept from reject on host hardware alone.
|
||||
func TestBatchSignsTheBlockHashItself(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cfg, partyIDs := twoPartyConfig(t)
|
||||
blockHash := ids.GenerateTestID()
|
||||
|
||||
messages, sigs, pubkeys, err := packBlockSigBatch(blockHash, entriesFor(64, partyIDs), cfg)
|
||||
require.NoError(err)
|
||||
require.Len(messages, 2*batchHashSize)
|
||||
require.Len(sigs, 2*batchSigSize)
|
||||
require.Len(pubkeys, 2*batchPKSize)
|
||||
|
||||
for i := range partyIDs {
|
||||
require.Equal(blockHash[:], messages[i*batchHashSize:(i+1)*batchHashSize],
|
||||
"row %d must carry the block hash, not a rehash of it", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchRefusesRowsItCannotRepresent proves a row the layout cannot fill
|
||||
// exactly is an error rather than a zero-filled row. Zero-filling left the entry
|
||||
// occupying a slot in the result vector whose verdict was then trusted, so a
|
||||
// signature the CPU path rejects outright could be counted valid; combined with
|
||||
// the "one valid signature is enough" block rule, one spuriously-true row admits
|
||||
// the block. Returning an error sends the batch to the CPU, the one authority.
|
||||
func TestBatchRefusesRowsItCannotRepresent(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cfg, partyIDs := twoPartyConfig(t)
|
||||
blockHash := ids.GenerateTestID()
|
||||
|
||||
_, _, _, err := packBlockSigBatch(blockHash, entriesFor(32, partyIDs), cfg)
|
||||
require.Error(err, "a 32-byte signature cannot fill a 64-byte row")
|
||||
|
||||
// A party with no public share in the config cannot be laid out either.
|
||||
orphan := []sigEntry{{partyID: "unknown-party", sigBytes: make([]byte, 64)}}
|
||||
require.Panics(func() { _, _, _, _ = packBlockSigBatch(blockHash, orphan, cfg) },
|
||||
"batchVerifyBlockSignatures filters unknown parties before this point")
|
||||
}
|
||||
|
||||
// TestVerifyBlockSigIsTheOneAuthority pins the single acceptance predicate: a
|
||||
// malformed signature is refused, and it is checked against the block hash.
|
||||
func TestVerifyBlockSigIsTheOneAuthority(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cfg, partyIDs := twoPartyConfig(t)
|
||||
blockHash := ids.GenerateTestID()
|
||||
|
||||
require.False(verifyBlockSig(cfg, blockHash, sigEntry{partyID: partyIDs[0], sigBytes: nil}))
|
||||
require.False(verifyBlockSig(cfg, blockHash, sigEntry{partyID: partyIDs[0], sigBytes: make([]byte, 32)}))
|
||||
require.False(verifyBlockSig(cfg, blockHash, sigEntry{partyID: partyIDs[0], sigBytes: make([]byte, 64)}),
|
||||
"an all-zero signature is not a valid signature")
|
||||
}
|
||||
|
||||
// TestBatchVerifyCountsNoSignatureWithoutConfig keeps the fail-closed entry
|
||||
// conditions of the caller honest.
|
||||
func TestBatchVerifyCountsNoSignatureWithoutConfig(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cfg, partyIDs := twoPartyConfig(t)
|
||||
blockHash := ids.GenerateTestID()
|
||||
|
||||
sigs := map[ids.NodeID][]byte{ids.GenerateTestNodeID(): make([]byte, 64)}
|
||||
require.Zero(batchVerifyBlockSignatures(blockHash, sigs, nil, nil),
|
||||
"no MPC config means no signature can be counted valid")
|
||||
require.Zero(batchVerifyBlockSignatures(blockHash, nil, cfg, nil),
|
||||
"no signatures means no valid signatures")
|
||||
require.Zero(batchVerifyBlockSignatures(blockHash, sigs, cfg, nil),
|
||||
"a signer with no public share in the config is not counted")
|
||||
_ = partyIDs
|
||||
}
|
||||
Reference in New Issue
Block a user