fix: re-org checks

This commit is contained in:
0xjvn 2026-04-29 13:42:34 +05:30
parent 2d5da60371
commit c3f0049a21
2 changed files with 77 additions and 4 deletions

View file

@ -81,6 +81,7 @@ var (
TooLargeRequest = &EngineAPIError{code: -38004, msg: "Too large request"} TooLargeRequest = &EngineAPIError{code: -38004, msg: "Too large request"}
InvalidParams = &EngineAPIError{code: -32602, msg: "Invalid parameters"} InvalidParams = &EngineAPIError{code: -32602, msg: "Invalid parameters"}
UnsupportedFork = &EngineAPIError{code: -38005, msg: "Unsupported fork"} UnsupportedFork = &EngineAPIError{code: -38005, msg: "Unsupported fork"}
TooDeepReorg = &EngineAPIError{code: -38006, msg: "Too deep reorg"}
STATUS_INVALID = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: INVALID}, PayloadID: nil} STATUS_INVALID = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: INVALID}, PayloadID: nil}
STATUS_SYNCING = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: SYNCING}, PayloadID: nil} STATUS_SYNCING = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: SYNCING}, PayloadID: nil}

View file

@ -82,6 +82,10 @@ const (
// beaconUpdateWarnFrequency is the frequency at which to warn the user that // beaconUpdateWarnFrequency is the frequency at which to warn the user that
// the beacon client is offline. // the beacon client is offline.
beaconUpdateWarnFrequency = 5 * time.Minute beaconUpdateWarnFrequency = 5 * time.Minute
// maxReorgDepth is the maximum number of blocks the client is willing to
// reorg. Reorg requests deeper than this will be rejected with -38006.
maxReorgDepth = 90000
) )
type ConsensusAPI struct { type ConsensusAPI struct {
@ -313,6 +317,10 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
} }
if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash { if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash {
// Block is not canonical, set head. // Block is not canonical, set head.
// Before reorging, check if the reorg depth exceeds our supported limit.
if err := api.checkReorgDepth(block); err != nil {
return engine.ForkChoiceResponse{}, err
}
if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil {
return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err
} }
@ -321,10 +329,20 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
// generating the payload. It's a special corner case that a few slots are // generating the payload. It's a special corner case that a few slots are
// missing and we are requested to generate the payload in slot. // missing and we are requested to generate the payload in slot.
} else { } else {
// If the head block is already in our canonical chain, the beacon client is // Block is canonical but not the current head. Per spec, the no-reorg
// probably resyncing. Ignore the update. // skip is only valid when headBlockHash is an ancestor of the last
log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number) // known finalized block.
return valid(nil), nil if api.isAncestorOfFinalized(block) {
log.Info("Ignoring beacon update to old head (ancestor of finalized)", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number)
return valid(nil), nil
}
// Head is canonical but beyond finalized - must reorg (set head).
if err := api.checkReorgDepth(block); err != nil {
return engine.ForkChoiceResponse{}, err
}
if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil {
return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err
}
} }
api.eth.SetSynced() api.eth.SetSynced()
@ -388,6 +406,60 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
return valid(nil), nil return valid(nil), nil
} }
// checkReorgDepth verifies that the reorg required to switch to the given block
// does not exceed maxReorgDepth. It walks back from the target block until it
// finds a canonical ancestor, then computes the depth as the number of canonical
// blocks that would be unwound.
func (api *ConsensusAPI) checkReorgDepth(target *types.Block) error {
currentHead := api.eth.BlockChain().CurrentBlock()
if currentHead == nil {
return nil
}
if target.NumberU64() >= currentHead.Number.Uint64() {
return nil
}
ancestor := target
for ancestor != nil && ancestor.NumberU64() > 0 {
if rawdb.ReadCanonicalHash(api.eth.ChainDb(), ancestor.NumberU64()) == ancestor.Hash() {
break
}
if currentHead.Number.Uint64()-ancestor.NumberU64() > maxReorgDepth {
return engine.TooDeepReorg.With(fmt.Errorf("reorg depth exceeds maximum supported depth %d", maxReorgDepth))
}
ancestor = api.eth.BlockChain().GetBlock(ancestor.ParentHash(), ancestor.NumberU64()-1)
}
if ancestor == nil {
return nil
}
depth := currentHead.Number.Uint64() - ancestor.NumberU64()
if depth > maxReorgDepth {
log.Warn("Reorg depth exceeds supported limit", "depth", depth, "max", maxReorgDepth, "target", target.Hash(), "ancestor", ancestor.Hash())
return engine.TooDeepReorg.With(fmt.Errorf("reorg depth %d exceeds maximum supported depth %d", depth, maxReorgDepth))
}
return nil
}
func (api *ConsensusAPI) isAncestorOfFinalized(block *types.Block) bool {
finalHeader := api.eth.BlockChain().CurrentFinalBlock()
if finalHeader == nil {
return false // no finalized block known
}
if block.Hash() == finalHeader.Hash() {
return false
}
if block.NumberU64() >= finalHeader.Number.Uint64() {
return false // block is at or beyond finalized
}
// Walk finalized ancestry back to block's height and check hash
cursor := api.eth.BlockChain().GetHeaderByHash(finalHeader.Hash())
for cursor != nil && cursor.Number.Uint64() > block.NumberU64() {
cursor = api.eth.BlockChain().GetHeaderByHash(cursor.ParentHash)
}
return cursor != nil && cursor.Hash() == block.Hash()
}
// ExchangeTransitionConfigurationV1 checks the given configuration against // ExchangeTransitionConfigurationV1 checks the given configuration against
// the configuration of the node. // the configuration of the node.
func (api *ConsensusAPI) ExchangeTransitionConfigurationV1(config engine.TransitionConfigurationV1) (*engine.TransitionConfigurationV1, error) { func (api *ConsensusAPI) ExchangeTransitionConfigurationV1(config engine.TransitionConfigurationV1) (*engine.TransitionConfigurationV1, error) {