Files
bft/common/block_scheduler.go
zeekayandHanzo Dev d678c2d213 transform: publish upstream simplex as luxfi/bft on luxfi/log
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]>
2026-08-06 04:21:17 -07:00

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