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]>
134 lines
5.4 KiB
Go
134 lines
5.4 KiB
Go
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package metadata
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/bft/common"
|
|
"github.com/luxfi/log"
|
|
)
|
|
|
|
// blockBuildingDecision represents the decision of whether we should build a block at the current time,
|
|
// and if so, whether we should also transition to a new epoch along the way.
|
|
type blockBuildingDecision struct {
|
|
buildInnerBlock bool
|
|
transitionEpoch bool
|
|
pChainHeight uint64
|
|
validatorSet NodeBLSMappings
|
|
}
|
|
|
|
// PChainProgressListener listens for changes in the P-chain height.
|
|
type PChainProgressListener interface {
|
|
// WaitForProgress should block until either the context is cancelled, or the P-chain height has increased from the provided pChainHeight.
|
|
WaitForProgress(ctx context.Context, pChainHeight uint64) error
|
|
}
|
|
|
|
type blockBuildingDecider struct {
|
|
logger common.Logger
|
|
maxBlockBuildingWaitTime time.Duration
|
|
pChainListener PChainProgressListener
|
|
waitForPendingBlock func(ctx context.Context)
|
|
// hasValidatorSetChanged should return whether the validator set has changed since the
|
|
// P-chain height referenced by the last block in the chain and until the provided P-chain height.
|
|
hasValidatorSetChanged func(pChainHeight uint64) (bool, NodeBLSMappings, error)
|
|
getPChainHeight func() uint64
|
|
}
|
|
|
|
// shouldBuildBlock determines whether we should build a block at the current time,
|
|
// based on the current P-chain height and whether we should transition to a new epoch.
|
|
// It returns a blockBuildingDecision, the P-chain height sampled at the time of deciding,
|
|
// and an error if the decision cannot be made.
|
|
// The P-chain height is returned because sampling the P-chain height afterwards might be inconsistent with the decision that was made.
|
|
func (bbd *blockBuildingDecider) shouldBuildBlock(
|
|
ctx context.Context,
|
|
) (blockBuildingDecision, error) {
|
|
for {
|
|
pChainHeight := bbd.getPChainHeight()
|
|
|
|
shouldTransitionEpoch, newValidatorSet, err := bbd.hasValidatorSetChanged(pChainHeight)
|
|
if err != nil {
|
|
return blockBuildingDecision{}, err
|
|
}
|
|
|
|
if shouldTransitionEpoch {
|
|
// If we should transition to a new epoch, maybe we can also build a block along the way.
|
|
return bbd.buildBlockWithEpochTransition(ctx, pChainHeight, newValidatorSet)
|
|
}
|
|
|
|
// Else, we don't need to transition to a new epoch, but maybe we should build a block.
|
|
// We wait for either the P-chain height to change, or for a block to be ready to be built.
|
|
bbd.waitForPChainChangeOrPendingBlock(ctx, pChainHeight)
|
|
|
|
// If the context was cancelled in the meantime, abandon evaluation.
|
|
if ctx.Err() != nil {
|
|
return blockBuildingDecision{}, ctx.Err()
|
|
}
|
|
|
|
// If we've reached here, either the P-chain height has changed, or a block is ready to be built.
|
|
|
|
// If the P-chain height changed, re-evaluate again whether we should transition to a new epoch,
|
|
// or continue waiting to build a block.
|
|
if bbd.getPChainHeight() != pChainHeight {
|
|
continue
|
|
}
|
|
|
|
// Else, we have reached here because a block is ready to be built, and the P-chain height has not changed,
|
|
// which means we should build a block.
|
|
|
|
return blockBuildingDecision{buildInnerBlock: true, pChainHeight: pChainHeight}, nil
|
|
}
|
|
}
|
|
|
|
// waitForPChainChangeOrPendingBlock waits until either the given P-chain height changes from the provided pChainHeight,
|
|
// or a block is ready to be built, or if `ctx` gets cancelled.
|
|
func (bbd *blockBuildingDecider) waitForPChainChangeOrPendingBlock(ctx context.Context, pChainHeight uint64) {
|
|
pChainAwareContext, cancel := context.WithCancel(ctx)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
defer wg.Wait()
|
|
defer cancel()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
err := bbd.pChainListener.WaitForProgress(pChainAwareContext, pChainHeight)
|
|
if err != nil && pChainAwareContext.Err() == nil {
|
|
bbd.logger.Warn("error while waiting for P-chain progress", log.Err(err))
|
|
}
|
|
cancel()
|
|
}()
|
|
|
|
bbd.waitForPendingBlock(pChainAwareContext)
|
|
}
|
|
|
|
// buildBlockWithEpochTransition decides if we should build a block while transitioning to a new epoch.
|
|
// It waits up to a limited amount of time (bbd.maxBlockBuildingWaitTime) for a block to be ready to be built,
|
|
// and if no block is ready by then, it returns the decision to transition epoch without building a block.
|
|
// Otherwise, it returns the decision to build a block and transition epoch along the way.
|
|
func (bbd *blockBuildingDecider) buildBlockWithEpochTransition(ctx context.Context, pChainHeight uint64, validatorSet NodeBLSMappings) (blockBuildingDecision, error) {
|
|
impatientContext, cancel := context.WithTimeout(ctx, bbd.maxBlockBuildingWaitTime)
|
|
defer cancel()
|
|
|
|
// We should transition to a new epoch, so we wait some time just in case we can also build a block along the way.
|
|
// waitForPendingBlock will return in case a block is ready to be built, or when the context times out.
|
|
bbd.waitForPendingBlock(impatientContext)
|
|
|
|
if ctx.Err() != nil {
|
|
return blockBuildingDecision{}, ctx.Err()
|
|
}
|
|
|
|
if impatientContext.Err() != nil {
|
|
// We have returned from waitForPendingBlock because impatientContext has timed out,
|
|
// which means we don't need to build a block.
|
|
return blockBuildingDecision{transitionEpoch: true, pChainHeight: pChainHeight, validatorSet: validatorSet}, nil
|
|
}
|
|
|
|
// Block is ready to be built
|
|
return blockBuildingDecision{buildInnerBlock: true, transitionEpoch: true, pChainHeight: pChainHeight, validatorSet: validatorSet}, nil
|
|
}
|