mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
The two-tier consensus (v1.36) advances VM.Accept at a BARE MAJORITY (Nova), which is reorgable and can fork under f=1 equivocation. The Ethereum `finalized`/`safe` tags and warp cross-chain exports were still reading the accept tip — the "semantic collapse" the split exists to prevent. Re-point every EXPORT surface onto the export-final (Quasar, ⅔-by-stake) frontier consensus already exposes. #1 CRITICAL — eth `finalized`/`safe` tags: - eth.Ethereum gains a DURABLE, monotone lastQuasarHeight (SetLastQuasarHeight/LastQuasarHeight), pushed by consensus strictly AFTER accept. The plugin VM's SetLastQuasarFinalized persists it to acceptedBlockDB and reloads it on boot (readLastQuasarHeight), so the export surface never regresses on restart. - api_backend HeaderByNumber/BlockByNumber resolve Finalized(-3)/Safe(-4) to GetBlockByNumber( LastQuasarHeight) BEFORE the IsAccepted() catch-all — NEVER a block above the Quasar height. `latest`/`pending` stay on the accept tip (correct — not finality tags). - Test: TestFinalizedSafeResolveToQuasarNeverAboveIt proves finalized == the Quasar height, monotone, and never above the accepted tip. #2 HIGH — warp cross-chain export double-spend: - warp.verifyBlockMessage now requires the block be EXPORT-final (height ≤ BlockClient. LastQuasarHeight), so a validator signs a block hash only at Quasar finality, never on a reorgable Nova accept (two conflicting Nova forks could otherwise each aggregate a valid export). - WarpQuorumNumeratorMinimum floored 33 → 67 (⅔): any warp message needs ≥⅔ stake to verify, so at most one of two conflicting exports can validate (two ⅔ sets intersect on >⅓ honest stake) — restoring the ⅔-accept safety the pre-split accept tier gave for free. The consensus engine pushes the frontier via NetworkConfig.QuasarObserver; the node bridges it into the VM (separate node commit). Nova-only chains (no export surface) are unaffected. Co-authored-by: Hanzo Dev <[email protected]>
475 lines
18 KiB
Go
475 lines
18 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package warp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/crypto/bls"
|
|
evmconsensus "github.com/luxfi/evm/consensus"
|
|
"github.com/luxfi/evm/precompile/precompileconfig"
|
|
"github.com/luxfi/evm/predicate"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/ids"
|
|
log "github.com/luxfi/log"
|
|
"github.com/luxfi/math"
|
|
consensuscontext "github.com/luxfi/runtime"
|
|
validators "github.com/luxfi/validators"
|
|
"github.com/luxfi/warp"
|
|
"github.com/luxfi/warp/payload"
|
|
)
|
|
|
|
const (
|
|
WarpDefaultQuorumNumerator uint64 = 67
|
|
// WarpQuorumNumeratorMinimum is floored at ⅔ (67/100). Under the two-tier consensus, local
|
|
// Accept is a BARE MAJORITY (Nova) which is reorgable and can fork under f=1 equivocation. A
|
|
// warp message is a CROSS-CHAIN EXPORT: if a route could require < ⅔ stake to verify, the two
|
|
// conflicting Nova forks {1,2,E}/{3,4,E} could EACH aggregate a valid warp export (a
|
|
// double-spend). Requiring ≥⅔ stake to verify ANY warp message means at most one of two
|
|
// conflicting exports can ever validate (two ⅔-stake sets must intersect on >⅓ honest stake),
|
|
// restoring the ⅔-accept safety the pre-split Accept tier gave for free. (Block-hash exports
|
|
// are ALSO gated on Quasar finality at the signing side — see warp.verifyBlockMessage.)
|
|
WarpQuorumNumeratorMinimum uint64 = 67
|
|
WarpQuorumDenominator uint64 = 100
|
|
)
|
|
|
|
var (
|
|
_ precompileconfig.Config = (*Config)(nil)
|
|
_ precompileconfig.Predicater = (*Config)(nil)
|
|
_ precompileconfig.Accepter = (*Config)(nil)
|
|
)
|
|
|
|
var (
|
|
errOverflowSignersGasCost = errors.New("overflow calculating warp signers gas cost")
|
|
errInvalidPredicateBytes = errors.New("cannot unpack predicate bytes")
|
|
errInvalidWarpMsg = errors.New("cannot unpack warp message")
|
|
errCannotParseWarpMsg = errors.New("cannot parse warp message")
|
|
errInvalidWarpMsgPayload = errors.New("cannot unpack warp message payload")
|
|
errInvalidAddressedPayload = errors.New("cannot unpack addressed payload")
|
|
errInvalidBlockHashPayload = errors.New("cannot unpack block hash payload")
|
|
errCannotGetNumSigners = errors.New("cannot fetch num signers from warp message")
|
|
errWarpCannotBeActivated = errors.New("warp cannot be activated before Durango")
|
|
errFailedVerification = errors.New("cannot verify warp signature")
|
|
errCannotRetrieveValidatorSet = errors.New("cannot retrieve validator set")
|
|
)
|
|
|
|
// Config implements the precompileconfig.Config interface and
|
|
// adds specific configuration for Warp.
|
|
type Config struct {
|
|
precompileconfig.Upgrade
|
|
QuorumNumerator uint64 `json:"quorumNumerator"`
|
|
RequirePrimaryNetworkSigners bool `json:"requirePrimaryNetworkSigners"`
|
|
}
|
|
|
|
// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables
|
|
// Warp with the given quorum numerator.
|
|
func NewConfig(blockTimestamp *uint64, quorumNumerator uint64, requirePrimaryNetworkSigners bool) *Config {
|
|
return &Config{
|
|
Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp},
|
|
QuorumNumerator: quorumNumerator,
|
|
RequirePrimaryNetworkSigners: requirePrimaryNetworkSigners,
|
|
}
|
|
}
|
|
|
|
// NewDefaultConfig returns a config for a network upgrade at [blockTimestamp] that enables
|
|
// Warp with the default quorum numerator (0 denotes using the default).
|
|
func NewDefaultConfig(blockTimestamp *uint64) *Config {
|
|
return NewConfig(blockTimestamp, 0, false)
|
|
}
|
|
|
|
// NewDisableConfig returns config for a network upgrade at [blockTimestamp]
|
|
// that disables Warp.
|
|
func NewDisableConfig(blockTimestamp *uint64) *Config {
|
|
return &Config{
|
|
Upgrade: precompileconfig.Upgrade{
|
|
BlockTimestamp: blockTimestamp,
|
|
Disable: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Key returns the key for the Warp precompileconfig.
|
|
// This should be the same key as used in the precompile module.
|
|
func (*Config) Key() string { return ConfigKey }
|
|
|
|
// Verify tries to verify Config and returns an error accordingly.
|
|
func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error {
|
|
if c.Timestamp() != nil {
|
|
// If Warp attempts to activate before Durango, fail verification
|
|
timestamp := *c.Timestamp()
|
|
if !chainConfig.IsDurango(timestamp) {
|
|
return errWarpCannotBeActivated
|
|
}
|
|
}
|
|
|
|
if c.QuorumNumerator > WarpQuorumDenominator {
|
|
return fmt.Errorf("cannot specify quorum numerator (%d) > quorum denominator (%d)", c.QuorumNumerator, WarpQuorumDenominator)
|
|
}
|
|
// If a non-default quorum numerator is specified and it is less than the minimum, return an error
|
|
if c.QuorumNumerator != 0 && c.QuorumNumerator < WarpQuorumNumeratorMinimum {
|
|
return fmt.Errorf("cannot specify quorum numerator (%d) < min quorum numerator (%d)", c.QuorumNumerator, WarpQuorumNumeratorMinimum)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Equal returns true if [s] is a [*Config] and it has been configured identical to [c].
|
|
func (c *Config) Equal(s precompileconfig.Config) bool {
|
|
// typecast before comparison
|
|
other, ok := (s).(*Config)
|
|
if !ok {
|
|
return false
|
|
}
|
|
equals := c.Upgrade.Equal(&other.Upgrade)
|
|
return equals && c.QuorumNumerator == other.QuorumNumerator
|
|
}
|
|
|
|
func (c *Config) Accept(acceptCtx *precompileconfig.AcceptContext, blockHash common.Hash, blockNumber uint64, txHash common.Hash, logIndex int, topics []common.Hash, logData []byte) error {
|
|
msg, err := UnpackSendWarpEventDataToMessage(logData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse warp log data into signed core (TxHash: %s, LogIndex: %d): %w", txHash, logIndex, err)
|
|
}
|
|
log.Debug(
|
|
"Accepted warp signed core",
|
|
"blockHash", blockHash,
|
|
"blockNumber", blockNumber,
|
|
"txHash", txHash,
|
|
"logIndex", logIndex,
|
|
"logData", common.Bytes2Hex(logData),
|
|
"warpMessageID", msg.ID(),
|
|
)
|
|
if err := acceptCtx.Warp.AddMessage(msg); err != nil {
|
|
return fmt.Errorf("failed to add warp message during accept (TxHash: %s, LogIndex: %d): %w", txHash, logIndex, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PredicateGas returns the amount of gas necessary to verify the predicate
|
|
// PredicateGas charges for:
|
|
// 1. Base cost of the message
|
|
// 2. Size of the message
|
|
// 3. Number of signers
|
|
// 4. Lookup of the validator set
|
|
//
|
|
// If the payload of the warp message fails parsing, return a non-nil error invalidating the transaction.
|
|
func (c *Config) PredicateGas(predicateBytes []byte) (uint64, error) {
|
|
totalGas := GasCostPerSignatureVerification
|
|
bytesGasCost, overflow := math.SafeMul(GasCostPerWarpMessageBytes, uint64(len(predicateBytes)))
|
|
if overflow {
|
|
return 0, fmt.Errorf("overflow calculating gas cost for warp message bytes of size %d", len(predicateBytes))
|
|
}
|
|
totalGas, overflow = math.SafeAdd(totalGas, bytesGasCost)
|
|
if overflow {
|
|
return 0, fmt.Errorf("overflow adding bytes gas cost of size %d", len(predicateBytes))
|
|
}
|
|
|
|
unpackedPredicateBytes, err := predicate.UnpackPredicate(predicateBytes)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w: %s", errInvalidPredicateBytes, err)
|
|
}
|
|
warpMessage, err := warp.ParseEnvelope(unpackedPredicateBytes)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w: %s", errInvalidWarpMsg, err)
|
|
}
|
|
_, err = payload.ParsePayload(warpMessage.Message.Payload)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w: %s", errInvalidWarpMsgPayload, err)
|
|
}
|
|
|
|
// The Beam (BLS aggregate lane) is a concrete field post-ZAP; count signers.
|
|
numSigners := uint64(warpMessage.Beam.Signers.Len())
|
|
if numSigners == 0 {
|
|
return 0, fmt.Errorf("%w: no signers in bit set", errCannotGetNumSigners)
|
|
}
|
|
signerGas, overflow := math.SafeMul(uint64(numSigners), GasCostPerWarpSigner)
|
|
if overflow {
|
|
return 0, errOverflowSignersGasCost
|
|
}
|
|
totalGas, overflow = math.SafeAdd(totalGas, signerGas)
|
|
if overflow {
|
|
return 0, fmt.Errorf("overflow adding signer gas (PrevTotal: %d, VerificationGas: %d)", totalGas, signerGas)
|
|
}
|
|
|
|
return totalGas, nil
|
|
}
|
|
|
|
// ValidatorOutputGetter is an optional interface that can be implemented
|
|
// by validator states to provide full validator output including public keys
|
|
type ValidatorOutputGetter interface {
|
|
GetValidatorSetWithOutput(ctx context.Context, height uint64, chainID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error)
|
|
}
|
|
|
|
// VerifyPredicate returns whether the predicate described by [predicateBytes] passes verification.
|
|
func (c *Config) VerifyPredicate(predicateContext *precompileconfig.PredicateContext, predicateBytes []byte) error {
|
|
unpackedPredicateBytes, err := predicate.UnpackPredicate(predicateBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errInvalidPredicateBytes, err)
|
|
}
|
|
|
|
// Note: PredicateGas should be called before VerifyPredicate, so we should never reach an error case here.
|
|
warpMsg, err := warp.ParseEnvelope(unpackedPredicateBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errCannotParseWarpMsg, err)
|
|
}
|
|
|
|
// Reject a message stamped with a different origin network than this node's,
|
|
// matching warp.VerifyEnvelope. The bespoke verify path must enforce the same
|
|
// checks (one and one way); D folds NetworkID, so this is defense in depth
|
|
// against cross-network replay.
|
|
if localNetworkID := consensuscontext.GetNetworkID(predicateContext.ConsensusCtx); warpMsg.Message.NetworkID != localNetworkID {
|
|
return fmt.Errorf("%w: message network ID %d does not match local network ID %d", errFailedVerification, warpMsg.Message.NetworkID, localNetworkID)
|
|
}
|
|
|
|
quorumNumerator := WarpDefaultQuorumNumerator
|
|
if c.QuorumNumerator != 0 {
|
|
quorumNumerator = c.QuorumNumerator
|
|
}
|
|
|
|
// Get ValidatorState from context
|
|
validatorState := consensuscontext.GetValidatorState(predicateContext.ConsensusCtx)
|
|
if validatorState == nil {
|
|
return fmt.Errorf("validator state not found in context")
|
|
}
|
|
|
|
// Get the source chain ID from the warp message
|
|
sourceChainID := warpMsg.Message.SourceChainID
|
|
|
|
// Get network ID from validator state for the source chain
|
|
sourceNetworkID, err := validatorState.GetNetworkID(sourceChainID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get network ID for source chain: %w", err)
|
|
}
|
|
|
|
// Get the receiving chain ID (the chain this VM is running on)
|
|
receivingChainID := evmconsensus.GetChainID(predicateContext.ConsensusCtx)
|
|
|
|
// Determine which chain's validators to use
|
|
// The logic is:
|
|
// 1. For non-primary network sources (sourceNetworkID != Empty): Use source chain's validators
|
|
// 2. For P-Chain sources: Use receiving chain's validators (P-Chain exempt)
|
|
// 3. For other primary network sources (e.g., C-Chain):
|
|
// - With RequirePrimaryNetworkSigners=true: Use primary network's validators
|
|
// - With RequirePrimaryNetworkSigners=false: Use receiving chain's validators
|
|
var requestedChainID ids.ID
|
|
if sourceNetworkID != ids.Empty && sourceNetworkID != constants.PrimaryNetworkID {
|
|
// Source is from a non-primary network - use that chain's validators
|
|
requestedChainID = sourceChainID
|
|
} else if sourceChainID == constants.PlatformChainID {
|
|
// P-Chain source - always use receiving chain's validators
|
|
requestedChainID = receivingChainID
|
|
} else if c.RequirePrimaryNetworkSigners {
|
|
// Other primary network source with RequirePrimaryNetworkSigners
|
|
// Use primary network validators
|
|
requestedChainID = constants.PrimaryNetworkID
|
|
} else {
|
|
// Other primary network source without RequirePrimaryNetworkSigners
|
|
// Use receiving chain's validators
|
|
requestedChainID = receivingChainID
|
|
}
|
|
|
|
pChainHeight := predicateContext.ProposerVMBlockCtx.PChainHeight
|
|
|
|
// Build warp validators - try to get full validator output with public keys
|
|
var allValidators []*warp.Validator
|
|
var totalWeight uint64
|
|
|
|
// Check if the validator state supports getting full output with public keys
|
|
if outputGetter, ok := validatorState.(ValidatorOutputGetter); ok {
|
|
// Use the full validator output which includes public keys
|
|
vdrOutputs, err := outputGetter.GetValidatorSetWithOutput(predicateContext.ConsensusCtx, pChainHeight, requestedChainID)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errCannotRetrieveValidatorSet, err)
|
|
}
|
|
|
|
allValidators = make([]*warp.Validator, 0, len(vdrOutputs))
|
|
for nodeID, output := range vdrOutputs {
|
|
totalWeight += output.Weight
|
|
|
|
vdr := &warp.Validator{
|
|
NodeID: nodeID,
|
|
Weight: output.Weight,
|
|
}
|
|
|
|
// Parse public key if available
|
|
if len(output.PublicKey) > 0 {
|
|
pk, pkErr := warp.ParsePublicKey(output.PublicKey)
|
|
if pkErr == nil {
|
|
vdr.PublicKey = pk
|
|
vdr.PublicKeyBytes = output.PublicKey
|
|
} else {
|
|
log.Debug("failed to parse validator public key", "nodeID", nodeID, "err", pkErr)
|
|
}
|
|
}
|
|
|
|
allValidators = append(allValidators, vdr)
|
|
}
|
|
} else {
|
|
// Fallback: get full output (signature verification will fail without public keys)
|
|
vdrOutputs, err := validatorState.GetValidatorSet(predicateContext.ConsensusCtx, pChainHeight, requestedChainID)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errCannotRetrieveValidatorSet, err)
|
|
}
|
|
|
|
allValidators = make([]*warp.Validator, 0, len(vdrOutputs))
|
|
for nodeID, output := range vdrOutputs {
|
|
totalWeight += output.Weight
|
|
vdr := &warp.Validator{
|
|
NodeID: nodeID,
|
|
Weight: output.Weight,
|
|
}
|
|
// Parse public key if available
|
|
if len(output.PublicKey) > 0 {
|
|
pk, pkErr := warp.ParsePublicKey(output.PublicKey)
|
|
if pkErr == nil {
|
|
vdr.PublicKey = pk
|
|
vdr.PublicKeyBytes = output.PublicKey
|
|
}
|
|
}
|
|
allValidators = append(allValidators, vdr)
|
|
}
|
|
}
|
|
|
|
// Aggregate validators by public key - this handles the case where multiple
|
|
// validators share the same public key. We sum their weights and keep one
|
|
// representative validator per unique public key.
|
|
pkeyToWeight := make(map[string]uint64)
|
|
pkeyToValidator := make(map[string]*warp.Validator)
|
|
|
|
for _, vdr := range allValidators {
|
|
if len(vdr.PublicKeyBytes) == 0 {
|
|
continue // Skip validators without public keys
|
|
}
|
|
pkStr := string(vdr.PublicKeyBytes)
|
|
pkeyToWeight[pkStr] += vdr.Weight
|
|
if _, exists := pkeyToValidator[pkStr]; !exists {
|
|
pkeyToValidator[pkStr] = vdr
|
|
}
|
|
}
|
|
|
|
// Build canonical validator set with aggregated weights
|
|
canonicalValidators := make([]*warp.Validator, 0, len(pkeyToValidator))
|
|
for pkStr, vdr := range pkeyToValidator {
|
|
// Create a copy with aggregated weight
|
|
aggregatedVdr := &warp.Validator{
|
|
NodeID: vdr.NodeID,
|
|
Weight: pkeyToWeight[pkStr],
|
|
PublicKey: vdr.PublicKey,
|
|
PublicKeyBytes: vdr.PublicKeyBytes,
|
|
}
|
|
canonicalValidators = append(canonicalValidators, aggregatedVdr)
|
|
}
|
|
|
|
// Sort validators by public key bytes for canonical ordering
|
|
// This matches the order used when creating signatures
|
|
sort.Slice(canonicalValidators, func(i, j int) bool {
|
|
return bytes.Compare(canonicalValidators[i].PublicKeyBytes, canonicalValidators[j].PublicKeyBytes) < 0
|
|
})
|
|
|
|
// The Beam (BLS aggregate lane) is a concrete field post-ZAP.
|
|
beam := warpMsg.Beam
|
|
|
|
// Calculate signed weight using canonical validators (with aggregated weights).
|
|
signedWeight, err := beamSignedWeight(beam, canonicalValidators)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errFailedVerification, err)
|
|
}
|
|
|
|
// Verify quorum. NOTE: totalWeight is the weight of ALL source validators
|
|
// (including those without a BLS public key) — the correct quorum
|
|
// denominator. This is why the precompile cannot route through
|
|
// warp.VerifyEnvelope, whose GetCanonicalValidatorSet both rejects the
|
|
// shared-public-key aggregation done above and excludes keyless validators
|
|
// from the denominator.
|
|
err = warp.VerifyWeight(signedWeight, totalWeight, quorumNumerator, WarpQuorumDenominator)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errFailedVerification, err)
|
|
}
|
|
|
|
// Verify the BLS Beam over the ZAP digest D = warpMsg.Message.ID(). The Beam
|
|
// signs warp.BeamSigningBytes(D), authenticating the entire Message
|
|
// (network ID, source chain, PQ lineage, payload) — not stale RLP bytes.
|
|
err = verifyBeam(beam, warpMsg.Message.ID(), canonicalValidators)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errFailedVerification, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// beamSignedWeight returns the total weight of the canonical validators whose
|
|
// bit is set in the Beam's signer bitset. It mirrors the warp package's
|
|
// internal (unexported) signedWeight; the precompile needs it directly because
|
|
// it supplies its own canonical set (shared-public-key validators aggregated
|
|
// by weight) which warp.GetCanonicalValidatorSet would reject.
|
|
func beamSignedWeight(beam warp.BitSetSignature, validators []*warp.Validator) (uint64, error) {
|
|
signers := beam.Signers
|
|
if signers.HighestSetBit() > len(validators) {
|
|
return 0, fmt.Errorf("signer index %d exceeds validator count %d", signers.HighestSetBit()-1, len(validators))
|
|
}
|
|
var weight uint64
|
|
for i := 0; i < signers.BitLen(); i++ {
|
|
if !signers.Contains(i) {
|
|
continue
|
|
}
|
|
if i >= len(validators) {
|
|
return 0, fmt.Errorf("signer index %d exceeds validator count %d", i, len(validators))
|
|
}
|
|
newWeight, overflow := math.SafeAdd(weight, validators[i].Weight)
|
|
if overflow {
|
|
return 0, errors.New("weight overflow")
|
|
}
|
|
weight = newWeight
|
|
}
|
|
return weight, nil
|
|
}
|
|
|
|
// verifyBeam checks the Beam BLS aggregate over warp.BeamSigningBytes(d),
|
|
// aggregating the public keys of the canonical validators selected by the
|
|
// signer bitset. It mirrors the warp package's internal (unexported) verify
|
|
// over the exported digest, for the same reason as beamSignedWeight.
|
|
func verifyBeam(beam warp.BitSetSignature, d ids.ID, validators []*warp.Validator) error {
|
|
signers := beam.Signers
|
|
if signers.Len() == 0 {
|
|
return errors.New("no signers")
|
|
}
|
|
if signers.HighestSetBit() > len(validators) {
|
|
return fmt.Errorf("signer index %d exceeds validator count %d", signers.HighestSetBit()-1, len(validators))
|
|
}
|
|
|
|
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))
|
|
}
|
|
pk := validators[i].PublicKey
|
|
if pk == nil {
|
|
return fmt.Errorf("validator %d has nil public key", i)
|
|
}
|
|
pks = append(pks, pk)
|
|
}
|
|
if len(pks) == 0 {
|
|
return errors.New("no valid signers")
|
|
}
|
|
|
|
aggPK, err := bls.AggregatePublicKeys(pks)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to aggregate public keys: %w", err)
|
|
}
|
|
sig, err := bls.SignatureFromBytes(beam.Signature[:])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse signature: %w", err)
|
|
}
|
|
if !bls.Verify(aggPK, sig, warp.BeamSigningBytes(d)) {
|
|
return warp.ErrInvalidSignature
|
|
}
|
|
return nil
|
|
}
|