mirror of
https://github.com/luxfi/bft.git
synced 2026-10-30 17:01:54 +00:00
The module is luxfi/bft and that is now the only name it answers to. - simplex/ -> engine/. Upstream's module is named for the protocol, so its root package and its engine package shared that name; under luxfi/bft the engine package would have collided with the module's own root package. It is the consensus engine, so engine.Epoch and engine.NewEpoch read without stutter. - Identifiers keep the qualifier where it carries meaning and only change the word: a block produced by this protocol as against a pre-existing chain's block is a real distinction, so makeNonSimplexBlock became makeNonBFTBlock, lastNonSimplexInnerBlockHeight became lastNonBFTInnerBlockHeight, newSnowToSimplexChain became newSnowToBFTChain. Dropping the qualifier outright would have left names that assert nothing. - The canoto-tagged fields of StateMachineMetadata lose the prefix entirely: EpochInfo, ProtocolMetadata, Blacklist. The wire format is unchanged — canoto keys off the tag numbers, which still read 1,2,3,4,5, and the generated .canoto.go files were renamed in step with the source. The msm fuzz and encoding round-trip tests cover this. - Prose cites the protocol paper by reference number instead of by name, so the origin stays findable without the name. The HyperSDK sentence still says Avalanche because that sentence is about Avalanche's VM and is true. Also here, from review of the logger port: - monitor_test's Fatal panics instead of returning. It reported a state the caller had declared impossible and then carried on regardless, which made it the one Fatal in the tree that was weaker than the original. - TestInstanceNonValidatorBootstraps asserts the property it is named for — that the node was still a non-validator when the bootstrap finished — rather than counting how many times a debug line was emitted. The count tracks how often onEpochChange happened to observe the branch, so transitions can coalesce; it measured 2 here where it had been reliably 3, and a logger-cost explanation was tested and refuted. The count never checked the property either way: a node that wrongly became a validator mid-bootstrap would still have counted 3. - SilenceExceptKeywords documents last-call-wins, and Trace/Verbo now reaching hooks is recorded where a future hook author will see it. Co-authored-by: Hanzo Dev <[email protected]>
215 lines
6.5 KiB
Go
215 lines
6.5 KiB
Go
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package nonvalidator
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"slices"
|
|
|
|
"github.com/luxfi/bft/common"
|
|
"github.com/luxfi/log"
|
|
)
|
|
|
|
var (
|
|
errNoGenesis = errors.New("no genesis found")
|
|
errMissingSealingInfo = errors.New("no sealing block info for sealing block")
|
|
)
|
|
|
|
type epochMetadata struct {
|
|
nodes common.Nodes
|
|
eligibleSigners map[string][]byte
|
|
epoch uint64
|
|
signatureAggregator common.SignatureAggregator
|
|
prevSealingBlockHash common.Digest
|
|
}
|
|
|
|
type epochs map[uint64]*epochMetadata
|
|
|
|
func newEpochMetadata(epoch uint64, sealingMetadata *common.SealingBlockInfo, sigCreator common.SignatureAggregatorCreator) *epochMetadata {
|
|
if sealingMetadata == nil {
|
|
return nil
|
|
}
|
|
|
|
// We sort the validators so that LeaderForRound derives the same leader the validators do.
|
|
// Cloned because the validator set belongs to the caller.
|
|
nodes := slices.Clone(sealingMetadata.ValidatorSet)
|
|
common.SortNodes(nodes)
|
|
|
|
lookup := make(map[string][]byte, len(nodes))
|
|
for _, node := range nodes {
|
|
lookup[string(node.Id)] = []byte{}
|
|
}
|
|
|
|
return &epochMetadata{
|
|
nodes: nodes,
|
|
eligibleSigners: lookup,
|
|
epoch: epoch,
|
|
signatureAggregator: sigCreator(nodes),
|
|
prevSealingBlockHash: sealingMetadata.PrevSealingBlockHash,
|
|
}
|
|
}
|
|
|
|
// newEpochs creates a mapping of epoch numbers -> epoch metadata. The epoch metadata is used for verifying
|
|
// blocks and finalizations, and should only contain epochMetadata that we have validated.
|
|
func newEpochs(storage common.Storage, sigAggCreator common.SignatureAggregatorCreator) (epochs, error) {
|
|
lastBlockHeight := storage.NumBlocks()
|
|
if lastBlockHeight == 0 {
|
|
return nil, errNoGenesis
|
|
}
|
|
|
|
lastBlock, _, err := storage.Retrieve(lastBlockHeight - 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
epochs := make(map[uint64]*epochMetadata)
|
|
|
|
// A zero Epoch means this is before the first ever BFT block(ex. Genesis or Last Snowman Block)
|
|
if lastBlock.BlockHeader().Epoch == 0 {
|
|
return epochs, nil
|
|
}
|
|
|
|
var sealingBlock common.VerifiedBlock
|
|
if lastBlock.SealingBlockInfo() == nil {
|
|
sealingBlock, _, err = storage.Retrieve(lastBlock.BlockHeader().Epoch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sealingBlock.SealingBlockInfo() == nil {
|
|
return nil, errMissingSealingInfo
|
|
}
|
|
} else {
|
|
sealingBlock = lastBlock
|
|
}
|
|
|
|
lastAcceptedEpoch := newEpochMetadata(sealingBlock.BlockHeader().Seq, sealingBlock.SealingBlockInfo(), sigAggCreator)
|
|
epochs[lastAcceptedEpoch.epoch] = lastAcceptedEpoch
|
|
return epochs, nil
|
|
}
|
|
|
|
func (e epochs) highestEpoch() (uint64, common.Nodes) {
|
|
highest := uint64(0)
|
|
nodes := []common.Node{}
|
|
for epoch, info := range e {
|
|
if epoch > highest {
|
|
highest = epoch
|
|
nodes = info.nodes
|
|
}
|
|
}
|
|
|
|
return highest, nodes
|
|
}
|
|
|
|
// removeOldEpochs deletes all epochs strictly less than startEpoch.
|
|
func (e epochs) removeOldEpochs(minEpochToKeep uint64) {
|
|
for epoch := range e {
|
|
if epoch < minEpochToKeep {
|
|
delete(e, epoch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e epochs) canValidate(block common.Block) bool {
|
|
if block.SealingBlockInfo() == nil {
|
|
return false
|
|
}
|
|
|
|
// the sequence number is the next proposed epoch
|
|
_, ok := e[block.BlockHeader().Seq]
|
|
if ok {
|
|
// cannot validate twice
|
|
return false
|
|
}
|
|
|
|
digest := block.BlockHeader().Digest
|
|
for _, md := range e {
|
|
if bytes.Equal(md.prevSealingBlockHash[:], digest[:]) {
|
|
// We have validated the next epoch, and the next epoch has a backward pointer to this one
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// latestValidatorSetRetriever is an allows the epoch replicator to get the latest validator set.
|
|
// This is used to calculate the threshold of votes needed to validate an epoch.
|
|
type latestValidatorSetRetriever interface {
|
|
Validators() common.Nodes
|
|
}
|
|
|
|
// epochDigestCounter counts sealing block responses from validators for each epoch.
|
|
// It uses latestValidatorSetRetriever to determine when the required response threshold
|
|
// has been reached.
|
|
type epochDigestCounter struct {
|
|
logger common.Logger
|
|
|
|
// sealingBlockResponses stores sealing blocks that have been received by our non-validator.
|
|
// It maps the epoch they are creating, to the NodeIds that have sent them to us(and which digest they sent).
|
|
// Once we have collected f+1 messages for a finalization, the sealing block for that epoch is validated.
|
|
sealingBlockResponses map[uint64]map[string]common.Digest
|
|
|
|
// latestValidatorSetRetriever is used to calculate the threshold of votes needed to validate an epoch
|
|
latestValidatorSetRetriever latestValidatorSetRetriever
|
|
}
|
|
|
|
func newEpochReplicator(logger common.Logger, validatorSetRetriever latestValidatorSetRetriever) *epochDigestCounter {
|
|
return &epochDigestCounter{
|
|
sealingBlockResponses: make(map[uint64]map[string]common.Digest),
|
|
logger: logger,
|
|
latestValidatorSetRetriever: validatorSetRetriever,
|
|
}
|
|
}
|
|
|
|
// collectedSealingBlockInfo records a sealing block response for an unknown epoch
|
|
// and returns true once a threshold of matching responses has been collected for
|
|
// that epoch. Nil sealingBlockInfo values are ignored and return false.
|
|
func (e *epochDigestCounter) collectedSealingBlockInfo(sealingBlockInfo *common.SealingBlockInfo, bh common.BlockHeader, from common.NodeID) bool {
|
|
if sealingBlockInfo == nil {
|
|
return false
|
|
}
|
|
|
|
validators := e.latestValidatorSetRetriever.Validators()
|
|
|
|
if !validators.Contains(from) {
|
|
e.logger.Debug("Received a quorum round from a node that is not a validator", log.Stringer("from", from))
|
|
return false
|
|
}
|
|
|
|
e.logger.Debug("Collected a sealing block", log.Stringer("QR", sealingBlockInfo), log.Stringer("From", from))
|
|
|
|
threshold := common.F(len(e.latestValidatorSetRetriever.Validators())) + 1
|
|
newEpoch := bh.Seq
|
|
epochResponses, ok := e.sealingBlockResponses[newEpoch]
|
|
digest := bh.Digest
|
|
if !ok {
|
|
epochResponses = make(map[string]common.Digest)
|
|
e.sealingBlockResponses[newEpoch] = epochResponses
|
|
}
|
|
epochResponses[string(from)] = digest
|
|
|
|
// check if we have a threshold of responses
|
|
counts := make(map[common.Digest]uint64)
|
|
for _, digest := range epochResponses {
|
|
count := counts[digest]
|
|
count++
|
|
counts[digest] = count
|
|
|
|
if counts[digest] >= uint64(threshold) {
|
|
e.logger.Info("We received enough messages to validate a higher epoch", log.Stringer("EpochInfo", sealingBlockInfo), log.Int("Threshold", threshold), log.Uint64("Responses", counts[digest]))
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (e *epochDigestCounter) removeOldEpochs(minEpochToKeep uint64) {
|
|
for epoch := range e.sealingBlockResponses {
|
|
if epoch < minEpochToKeep {
|
|
delete(e.sealingBlockResponses, epoch)
|
|
}
|
|
}
|
|
}
|