Files
evmgpu/warp/aggregator_test.go
zeekayandHanzo Dev 0c11cf1960 warp: name the message for what it now is
The identifiers still said unsigned, but warp no longer has an unsigned
message: the Message that the previous commit migrated to is the signed
subject, and the thing that carries a signature beside it is the
Envelope. Locals and parameters that held one drop the prefix, the
LocalSigner method that signs one becomes SignMessage, and the two
places where a test builds a deliberately malformed message or reads
one back out of an event name it invalidMsg and unpacked rather than
shadowing the valid message in the enclosing test.

PackSendWarpMessageEvent keeps unsignedMessageID, which mirrors the
field name in the SendWarpMessage ABI event rather than a Go type.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-06 03:52:57 -07:00

281 lines
7.0 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package warp
import (
"context"
"errors"
"fmt"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/warp"
"github.com/stretchr/testify/require"
)
// mockSignatureGetter implements SignatureGetter for testing
type mockSignatureGetter struct {
signatures map[ids.NodeID][]byte
errors map[ids.NodeID]error
secretKeys map[ids.NodeID]*bls.SecretKey
}
func newMockSignatureGetter() *mockSignatureGetter {
return &mockSignatureGetter{
signatures: make(map[ids.NodeID][]byte),
errors: make(map[ids.NodeID]error),
secretKeys: make(map[ids.NodeID]*bls.SecretKey),
}
}
func (m *mockSignatureGetter) GetSignature(ctx context.Context, nodeID ids.NodeID, msg *warp.Message) ([]byte, error) {
if err, ok := m.errors[nodeID]; ok {
return nil, err
}
if sig, ok := m.signatures[nodeID]; ok {
return sig, nil
}
// Sign with secret key if available
if sk, ok := m.secretKeys[nodeID]; ok {
sig, err := sk.Sign(warp.BeamSigningBytes(msg.ID()))
if err != nil {
return nil, err
}
return bls.SignatureToBytes(sig), nil
}
return nil, errors.New("no signature available")
}
func (m *mockSignatureGetter) addSigner(nodeID ids.NodeID, sk *bls.SecretKey) {
m.secretKeys[nodeID] = sk
}
func (m *mockSignatureGetter) addError(nodeID ids.NodeID, err error) {
m.errors[nodeID] = err
}
func TestSignatureAggregator_AggregateSignatures(t *testing.T) {
require := require.New(t)
// Create test validators with BLS keys
numValidators := 3
validators := make([]*ValidatorInfo, numValidators)
secretKeys := make([]*bls.SecretKey, numValidators)
mockGetter := newMockSignatureGetter()
for i := 0; i < numValidators; i++ {
sk, err := bls.NewSecretKey()
require.NoError(err)
secretKeys[i] = sk
nodeID := ids.GenerateTestNodeID()
validators[i] = &ValidatorInfo{
NodeID: nodeID,
PublicKey: sk.PublicKey(),
Weight: 100,
Index: i,
}
mockGetter.addSigner(nodeID, sk)
}
aggregator := NewSignatureAggregator(mockGetter)
// Create test message
networkID := uint32(1)
sourceChainID := ids.GenerateTestID()
payload := []byte("test payload")
msg, err := warp.NewMessage(networkID, sourceChainID, payload)
require.NoError(err)
// Test successful aggregation with 67% quorum
signedMsgBytes, err := aggregator.AggregateSignatures(
context.Background(),
msg,
validators,
67, // 67%
100,
)
require.NoError(err)
require.NotEmpty(signedMsgBytes)
// Verify the signed message can be parsed
signedMsg, err := warp.ParseEnvelope(signedMsgBytes)
require.NoError(err)
require.NotNil(signedMsg)
// Verify the Beam aggregate over the ZAP digest D. BitSetSignature.verify is
// unexported post-ZAP, so check the aggregate directly against the same
// domain warp.Signer signs.
require.NoError(verifyBeamAggregate(signedMsg, toWarpValidators(validators)))
}
// verifyBeamAggregate aggregates the public keys of the validators selected by
// the envelope's signer bitset and checks the Beam against
// warp.BeamSigningBytes(D).
func verifyBeamAggregate(env *warp.Envelope, validators []*warp.Validator) error {
signers := env.Beam.Signers
pks := make([]*bls.PublicKey, 0, signers.Len())
for i := 0; i < signers.BitLen(); i++ {
if !signers.Contains(i) {
continue
}
if i >= len(validators) {
return fmt.Errorf("signer index %d exceeds validator count %d", i, len(validators))
}
pks = append(pks, validators[i].PublicKey)
}
if len(pks) == 0 {
return errors.New("no signers")
}
aggPK, err := bls.AggregatePublicKeys(pks)
if err != nil {
return err
}
sig, err := bls.SignatureFromBytes(env.Beam.Signature[:])
if err != nil {
return err
}
if !bls.Verify(aggPK, sig, warp.BeamSigningBytes(env.Message.ID())) {
return warp.ErrInvalidSignature
}
return nil
}
func TestSignatureAggregator_InsufficientQuorum(t *testing.T) {
require := require.New(t)
// Create 3 validators
numValidators := 3
validators := make([]*ValidatorInfo, numValidators)
secretKeys := make([]*bls.SecretKey, numValidators)
mockGetter := newMockSignatureGetter()
for i := 0; i < numValidators; i++ {
sk, err := bls.NewSecretKey()
require.NoError(err)
secretKeys[i] = sk
nodeID := ids.GenerateTestNodeID()
validators[i] = &ValidatorInfo{
NodeID: nodeID,
PublicKey: sk.PublicKey(),
Weight: 100,
Index: i,
}
// Only add signer for first validator
if i == 0 {
mockGetter.addSigner(nodeID, sk)
} else {
mockGetter.addError(nodeID, errors.New("validator unavailable"))
}
}
aggregator := NewSignatureAggregator(mockGetter)
// Create test message
networkID := uint32(1)
sourceChainID := ids.GenerateTestID()
payload := []byte("test payload")
msg, err := warp.NewMessage(networkID, sourceChainID, payload)
require.NoError(err)
// Try to aggregate with 67% quorum - should fail (only 1/3 validators available)
_, err = aggregator.AggregateSignatures(
context.Background(),
msg,
validators,
67,
100,
)
require.Error(err)
require.Contains(err.Error(), "insufficient quorum")
}
func TestSignatureAggregator_NoValidators(t *testing.T) {
require := require.New(t)
mockGetter := newMockSignatureGetter()
aggregator := NewSignatureAggregator(mockGetter)
networkID := uint32(1)
sourceChainID := ids.GenerateTestID()
payload := []byte("test payload")
msg, err := warp.NewMessage(networkID, sourceChainID, payload)
require.NoError(err)
// Empty validator set
_, err = aggregator.AggregateSignatures(
context.Background(),
msg,
[]*ValidatorInfo{},
67,
100,
)
require.Error(err)
require.ErrorIs(err, errNoValidators)
}
func TestSignatureAggregator_AllValidatorsFail(t *testing.T) {
require := require.New(t)
// Create validators but make all fail
numValidators := 3
validators := make([]*ValidatorInfo, numValidators)
mockGetter := newMockSignatureGetter()
for i := 0; i < numValidators; i++ {
sk, err := bls.NewSecretKey()
require.NoError(err)
nodeID := ids.GenerateTestNodeID()
validators[i] = &ValidatorInfo{
NodeID: nodeID,
PublicKey: sk.PublicKey(),
Weight: 100,
Index: i,
}
mockGetter.addError(nodeID, errors.New("connection failed"))
}
aggregator := NewSignatureAggregator(mockGetter)
networkID := uint32(1)
sourceChainID := ids.GenerateTestID()
payload := []byte("test payload")
msg, err := warp.NewMessage(networkID, sourceChainID, payload)
require.NoError(err)
_, err = aggregator.AggregateSignatures(
context.Background(),
msg,
validators,
67,
100,
)
require.Error(err)
require.ErrorIs(err, errNoSignatures)
}
// toWarpValidators converts ValidatorInfo to warp.Validator for verification
func toWarpValidators(infos []*ValidatorInfo) []*warp.Validator {
result := make([]*warp.Validator, len(infos))
for i, info := range infos {
result[i] = &warp.Validator{
PublicKey: info.PublicKey,
PublicKeyBytes: bls.PublicKeyToCompressedBytes(info.PublicKey),
Weight: info.Weight,
NodeID: info.NodeID,
}
}
return result
}