mirror of
https://github.com/luxfi/bft.git
synced 2026-10-30 17:01:54 +00:00
Reapplies the fork's whole delta on top of the merge, as one reviewable diff rather than 43 conflict resolutions. Nothing here changes consensus behaviour. - module github.com/luxfi/bft, Go 1.26.4; root package simplex -> bft, which would otherwise collide with the simplex/ subpackage under this module name. - go.uber.org/zap -> github.com/luxfi/log, so common.Logger takes ...log.Field. zap.Error maps to log.Err: log.Error is luxfi/log's package-level logging function, not a field constructor, and the compiler is the only thing that catches that. Needs luxfi/log v1.6.1 for the Stringers constructor. - avalanchego/ -> host/. The package holds NodeID, VMBlock, Bitmask and size limits — the host node's types, vendored to avoid importing a whole node — so it is named for what it holds. Renaming it luxd/ would have swapped one brand for another and said just as little. - testutil/logger.go reimplemented without zapcore. Upstream layers a keyword-filtering Core, a debug-swallowing Encoder and a Tee because that is zap's extension model; luxfi/log has no such seam, so every policy is applied in one emit. That also fixes two defects the layering caused: SilenceExceptKeywords rebuilt the logger and discarded an installed Intercept hook, and the two sinks read different env vars (LOG_LEVEL vs log_level) so they disagreed about suppressing debug. Hooks run outside the lock, so a hook that logs fails the test instead of deadlocking it. Fatal panics rather than exiting, because exiting takes the suite down with no attribution. - simplex/monitor_test.go keeps a local logger: testutil imports simplex, so an internal test cannot import testutil back without closing a cycle. - README keeps the name Simplex throughout — it is the protocol's own name and the subject of reference [1]. Only the deployment target changed. The HyperSDK sentence still says Avalanche because that sentence is true. - Drops the fork's Makefile: it duplicated unit-tests.sh and fuzz.sh, and ran the suite with -timeout 30s when simplex alone needs over 100s. Co-authored-by: Hanzo Dev <[email protected]>
200 lines
5.6 KiB
Go
200 lines
5.6 KiB
Go
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package common
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/luxfi/log"
|
|
)
|
|
|
|
var ErrTooManyPendingVerifications = errors.New("too many blocks being verified to ingest another one")
|
|
|
|
type Scheduler interface {
|
|
Schedule(task Task)
|
|
Size() int
|
|
Close()
|
|
}
|
|
|
|
// BlockDependencyManager manages block verification tasks with dependencies on previous blocks and empty rounds.
|
|
// It schedules tasks when their dependencies are resolved.
|
|
type BlockDependencyManager struct {
|
|
lock sync.Mutex
|
|
logger Logger
|
|
scheduler Scheduler
|
|
|
|
dependencies []*TaskWithDependents
|
|
maxDeps uint64
|
|
closed atomic.Bool
|
|
}
|
|
|
|
type TaskWithDependents struct {
|
|
Task Task
|
|
|
|
blockSeq uint64 // the seq of the block being verified
|
|
prevBlock *Digest
|
|
emptyRounds map[uint64]struct{}
|
|
}
|
|
|
|
func (t *TaskWithDependents) isReady() bool {
|
|
return t.prevBlock == nil && len(t.emptyRounds) == 0
|
|
}
|
|
|
|
func (t *TaskWithDependents) String() string {
|
|
return fmt.Sprintf("BlockVerificationTask{blockSeq: %d, prevBlock: %v, emptyRounds: %v}", t.blockSeq, t.prevBlock, slices.Collect(maps.Keys(t.emptyRounds)))
|
|
}
|
|
|
|
func NewBlockVerificationScheduler(logger Logger, maxDeps uint64, scheduler Scheduler) *BlockDependencyManager {
|
|
b := &BlockDependencyManager{
|
|
logger: logger,
|
|
maxDeps: maxDeps,
|
|
scheduler: scheduler,
|
|
}
|
|
|
|
b.logger.Debug("Created BlockVerificationScheduler", log.Uint64("maxDeps", maxDeps))
|
|
return b
|
|
}
|
|
|
|
// ExecuteBlockDependents removes the given digest from dependent tasks and schedules any whose dependencies are now resolved.
|
|
func (bs *BlockDependencyManager) ExecuteBlockDependents(prev Digest) {
|
|
bs.lock.Lock()
|
|
defer bs.lock.Unlock()
|
|
|
|
remainingDeps := make([]*TaskWithDependents, 0, len(bs.dependencies))
|
|
|
|
for _, taskWithDeps := range bs.dependencies {
|
|
if taskWithDeps.prevBlock != nil && *taskWithDeps.prevBlock == prev {
|
|
taskWithDeps.prevBlock = nil
|
|
}
|
|
|
|
if taskWithDeps.isReady() {
|
|
bs.logger.Debug("Scheduling block verification task as all dependencies are met", log.Stringer("taskID", prev), log.Uint64("Seq", taskWithDeps.blockSeq))
|
|
bs.scheduler.Schedule(taskWithDeps.Task)
|
|
continue
|
|
}
|
|
|
|
remainingDeps = append(remainingDeps, taskWithDeps)
|
|
}
|
|
|
|
if len(remainingDeps) > 0 {
|
|
bs.logger.Debug("Block scheduler has unsatisfied dependencies",
|
|
log.Stringer("prev", prev),
|
|
log.Stringers("task", remainingDeps),
|
|
)
|
|
}
|
|
|
|
bs.dependencies = remainingDeps
|
|
}
|
|
|
|
// ExecuteEmptyRoundDependents removes the given empty round from dependent tasks and schedules any whose dependencies are now resolved.
|
|
func (bs *BlockDependencyManager) ExecuteEmptyRoundDependents(emptyRound uint64) {
|
|
bs.lock.Lock()
|
|
defer bs.lock.Unlock()
|
|
|
|
remainingDeps := make([]*TaskWithDependents, 0, len(bs.dependencies))
|
|
|
|
for _, taskWithDeps := range bs.dependencies {
|
|
delete(taskWithDeps.emptyRounds, emptyRound)
|
|
|
|
if taskWithDeps.isReady() {
|
|
bs.logger.Debug("Scheduling block verification task as all dependencies are met", log.Stringer("task", taskWithDeps))
|
|
bs.scheduler.Schedule(taskWithDeps.Task)
|
|
continue
|
|
}
|
|
|
|
remainingDeps = append(remainingDeps, taskWithDeps)
|
|
}
|
|
|
|
if len(remainingDeps) > 0 {
|
|
bs.logger.Debug("Block scheduler has unsatisfied dependencies",
|
|
log.Uint64("empty round", emptyRound),
|
|
log.Stringers("task", remainingDeps),
|
|
)
|
|
}
|
|
|
|
bs.dependencies = remainingDeps
|
|
}
|
|
|
|
func (bs *BlockDependencyManager) IsSequenceScheduled(seq uint64) bool {
|
|
bs.lock.Lock()
|
|
defer bs.lock.Unlock()
|
|
|
|
for _, dep := range bs.dependencies {
|
|
if dep.blockSeq == seq {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (bs *BlockDependencyManager) ScheduleTaskWithDependencies(task Task, blockSeq uint64, prev *Digest, emptyRounds []uint64) error {
|
|
bs.lock.Lock()
|
|
defer bs.lock.Unlock()
|
|
|
|
if bs.closed.Load() {
|
|
return nil
|
|
}
|
|
|
|
wrappedTask := func() Digest {
|
|
id := task()
|
|
bs.ExecuteBlockDependents(id)
|
|
return id
|
|
}
|
|
|
|
totalSize := uint64(len(bs.dependencies) + bs.scheduler.Size())
|
|
if totalSize >= bs.maxDeps {
|
|
bs.logger.Warn("Too many blocks being verified to ingest another one", log.Uint64("pendingBlocks", totalSize))
|
|
return fmt.Errorf("%w: %d pending verifications (max %d)", ErrTooManyPendingVerifications, totalSize, bs.maxDeps)
|
|
}
|
|
|
|
if prev == nil && len(emptyRounds) == 0 {
|
|
bs.logger.Debug("Scheduling block verification task with no dependencies", log.Uint64("blockSeq", blockSeq))
|
|
bs.scheduler.Schedule(wrappedTask)
|
|
return nil
|
|
}
|
|
|
|
bs.logger.Debug("Adding block verification task with dependencies", log.Any("prevBlock", prev), log.Uint64s("emptyRounds", emptyRounds), log.Uint64("blockSeq", blockSeq))
|
|
emptyRoundsSet := make(map[uint64]struct{})
|
|
for _, round := range emptyRounds {
|
|
emptyRoundsSet[round] = struct{}{}
|
|
}
|
|
|
|
bs.dependencies = append(bs.dependencies, &TaskWithDependents{
|
|
Task: wrappedTask,
|
|
prevBlock: prev,
|
|
emptyRounds: emptyRoundsSet,
|
|
blockSeq: blockSeq,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// We can remove all tasks that have an empty notarization dependency for a round that has been finalized.
|
|
func (bs *BlockDependencyManager) RemoveOldTasks(seq uint64) {
|
|
bs.lock.Lock()
|
|
defer bs.lock.Unlock()
|
|
|
|
remainingDeps := make([]*TaskWithDependents, 0, len(bs.dependencies))
|
|
for _, taskWithDeps := range bs.dependencies {
|
|
if taskWithDeps.blockSeq <= seq {
|
|
bs.logger.Debug("Removing block verification task as its block seq is less than or equal to finalized seq", log.Uint64("blockSeq", taskWithDeps.blockSeq), log.Uint64("finalizedSeq", seq))
|
|
continue
|
|
}
|
|
remainingDeps = append(remainingDeps, taskWithDeps)
|
|
}
|
|
|
|
bs.dependencies = remainingDeps
|
|
}
|
|
|
|
func (bs *BlockDependencyManager) Close() {
|
|
bs.closed.Store(true)
|
|
bs.scheduler.Close()
|
|
}
|