Files
zeekayandHanzo Dev 974d0d5422 rename: drop the upstream protocol name from the tree
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]>
2026-08-06 05:16:49 -07:00

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)
}
}
}