mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-08 07:58:40 +00:00
Fix the post-sync deadlock where blocks validated via BAL in newPayload were never written to the database, causing ForkchoiceUpdated to fail finding them and triggering infinite sync cycles. Changes: - Export WriteBlockWithoutState and call it after ProcessBlockWithBAL in newPayload, so FCU can find blocks via GetBlockByHash - Guard SetCanonical against recoverAncestors for partial state nodes (they can't re-execute blocks, only apply BAL diffs) - Auto-disable log indexing when partial state is enabled (no receipts) - Fix BAL type field accesses to match upstream bal-devnet-2 types (StorageChanges, CodeChanges, BalanceChanges, Validate signature) - Update newPayload signature (BAL now comes from ExecutableData params) - Add partial sync scripts and documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
226 lines
8.6 KiB
Go
226 lines
8.6 KiB
Go
// Copyright 2025 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package core
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/state/partial"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/core/types/bal"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
)
|
|
|
|
// ErrDeepReorg is returned when a chain reorganization exceeds the BAL retention depth.
|
|
// When this error is returned, the partial state node needs to resync state from full peers.
|
|
var ErrDeepReorg = errors.New("reorg depth exceeds BAL retention")
|
|
|
|
// ProcessBlockWithBAL processes a block using BAL instead of execution.
|
|
// This is the entry point for partial state block processing.
|
|
//
|
|
// # Trust Model - Why We Don't Re-Verify Consensus Attestations
|
|
//
|
|
// Post-Merge (PoS) Architecture Trust Boundary:
|
|
// - Consensus Layer (CL): Responsible for block proposal, attestations (2/3+ sync committee
|
|
// threshold), finality proofs, proposer signatures, and all consensus rules
|
|
// - Execution Layer (EL): Responsible for transaction execution, state computation, receipts
|
|
//
|
|
// Blocks received via Engine API (engine_newPayloadV5) have ALREADY been attested by the CL
|
|
// before being sent to the EL. The EL trusts the CL for consensus validation - this is the
|
|
// fundamental trust model of the Merge architecture (see eth/catalyst/api.go).
|
|
//
|
|
// For partial state nodes:
|
|
// - Normal operation: Blocks arrive via Engine API, already consensus-validated by CL
|
|
// - We validate: BAL hash matches header commitment, computed state root matches header
|
|
// - We trust: CL has verified proposer signatures, attestations, and finality
|
|
//
|
|
// This is identical to how full nodes operate - they also don't re-verify CL attestations.
|
|
// The only difference is we apply BAL diffs instead of re-executing transactions.
|
|
//
|
|
// Future consideration: If supporting light client sync where blocks come from untrusted
|
|
// P2P sources, use beacon light client verification via CommitteeChain.VerifySignedHeader()
|
|
// or HeadTracker.ValidateOptimistic() (see beacon/light/).
|
|
func (bc *BlockChain) ProcessBlockWithBAL(
|
|
block *types.Block,
|
|
accessList *bal.BlockAccessList,
|
|
) error {
|
|
// Sanity check
|
|
if bc.partialState == nil {
|
|
return errors.New("partial state not enabled")
|
|
}
|
|
|
|
// Note: No consensus attestation verification here - blocks via Engine API are
|
|
// pre-attested by the Consensus Layer. See function documentation above.
|
|
|
|
// 1. Validate BAL structure
|
|
if err := accessList.Validate(len(block.Transactions())); err != nil {
|
|
return fmt.Errorf("invalid BAL structure: %w", err)
|
|
}
|
|
|
|
// 2. Verify BAL hash matches header commitment
|
|
// TODO(EIP-7928): Uncomment when BlockAccessListHash is added to Header
|
|
// balHash := accessList.Hash()
|
|
// if balHash != block.Header().BlockAccessListHash {
|
|
// return fmt.Errorf("BAL hash mismatch: got %x, want %x",
|
|
// balHash, block.Header().BlockAccessListHash)
|
|
// }
|
|
|
|
// 3. Get parent state root
|
|
parent := bc.GetBlock(block.ParentHash(), block.NumberU64()-1)
|
|
if parent == nil {
|
|
return errors.New("parent block not found")
|
|
}
|
|
parentRoot := parent.Root()
|
|
|
|
// 4. Apply BAL diffs and compute new state root
|
|
newRoot, err := bc.partialState.ApplyBALAndComputeRoot(parentRoot, accessList)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to apply BAL: %w", err)
|
|
}
|
|
|
|
// 5. Verify computed root matches header
|
|
if newRoot != block.Root() {
|
|
return fmt.Errorf("state root mismatch: computed %x, header %x",
|
|
newRoot, block.Root())
|
|
}
|
|
|
|
// 6. Block is stored via normal chain insertion
|
|
// BAL storage for reorgs is handled separately via BALHistory
|
|
|
|
log.Debug("Processed block with BAL",
|
|
"number", block.NumberU64(),
|
|
"hash", block.Hash().Hex(),
|
|
"root", newRoot.Hex(),
|
|
"accounts", len(*accessList))
|
|
|
|
return nil
|
|
}
|
|
|
|
// SupportsPartialState returns true if partial state processing is enabled.
|
|
func (bc *BlockChain) SupportsPartialState() bool {
|
|
return bc.partialState != nil
|
|
}
|
|
|
|
// PartialState returns the partial state manager, or nil if not enabled.
|
|
func (bc *BlockChain) PartialState() *partial.PartialState {
|
|
return bc.partialState
|
|
}
|
|
|
|
// HandlePartialReorg handles chain reorganization for partial state nodes.
|
|
// It reverts state to the common ancestor and then applies BALs from the new chain.
|
|
//
|
|
// Parameters:
|
|
// - commonAncestor: The most recent block that both chains share
|
|
// - newBlocks: Ordered list of blocks from the new chain (oldest to newest)
|
|
// - getBAL: Function to retrieve BAL for a given block (from BALHistory or Engine API)
|
|
func (bc *BlockChain) HandlePartialReorg(
|
|
commonAncestor *types.Block,
|
|
newBlocks []*types.Block,
|
|
getBAL func(blockHash common.Hash, blockNum uint64) (*bal.BlockAccessList, error),
|
|
) error {
|
|
if bc.partialState == nil {
|
|
return errors.New("partial state not enabled")
|
|
}
|
|
|
|
currentHead := bc.CurrentBlock()
|
|
reorgDepth := currentHead.Number.Uint64() - commonAncestor.Number().Uint64()
|
|
|
|
// Check if reorg exceeds BAL retention depth
|
|
// If so, we need to resync state from full peers because we don't have the BALs
|
|
if history := bc.partialState.History(); history != nil {
|
|
retention := history.Retention()
|
|
if retention > 0 && reorgDepth > retention {
|
|
log.Warn("Reorg exceeds BAL retention depth, partial resync required",
|
|
"reorgDepth", reorgDepth,
|
|
"retention", retention,
|
|
"ancestor", commonAncestor.Number())
|
|
return ErrDeepReorg
|
|
}
|
|
}
|
|
|
|
// Step 1: Revert state to common ancestor
|
|
// Simply set state root to ancestor's root (we have all account trie data)
|
|
bc.partialState.SetRoot(commonAncestor.Root())
|
|
|
|
log.Debug("Reverted partial state to ancestor",
|
|
"ancestor", commonAncestor.Number(),
|
|
"ancestorRoot", commonAncestor.Root().Hex(),
|
|
"reorgDepth", reorgDepth)
|
|
|
|
// Step 2: Apply new chain's blocks using their BALs
|
|
for _, block := range newBlocks {
|
|
// Get BAL for this block
|
|
accessList, err := getBAL(block.Hash(), block.NumberU64())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get BAL for block %d: %w", block.NumberU64(), err)
|
|
}
|
|
if accessList == nil {
|
|
return fmt.Errorf("block %d missing BAL for reorg", block.NumberU64())
|
|
}
|
|
|
|
// Apply BAL to move state forward on new chain
|
|
if err := bc.ProcessBlockWithBAL(block, accessList); err != nil {
|
|
return fmt.Errorf("failed to apply block %d during reorg: %w",
|
|
block.NumberU64(), err)
|
|
}
|
|
}
|
|
|
|
if len(newBlocks) > 0 {
|
|
log.Info("Completed partial state reorg",
|
|
"ancestor", commonAncestor.Number(),
|
|
"newHead", newBlocks[len(newBlocks)-1].NumberU64(),
|
|
"reorgDepth", reorgDepth)
|
|
} else {
|
|
log.Info("Completed partial state reorg (reset to ancestor)",
|
|
"ancestor", commonAncestor.Number(),
|
|
"reorgDepth", reorgDepth)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TriggerPartialResync initiates a state resync when a reorg exceeds BAL retention.
|
|
// This is called when HandlePartialReorg returns ErrDeepReorg.
|
|
//
|
|
// The resync fetches state from full peers using snap sync, downloading:
|
|
// - Full account trie (all balances, nonces, code hashes)
|
|
// - Storage only for tracked contracts (per ContractFilter configuration)
|
|
//
|
|
// This is similar to initial partial state sync, but starting from the reorg ancestor
|
|
// rather than genesis.
|
|
func (bc *BlockChain) TriggerPartialResync(ancestor *types.Header) error {
|
|
if bc.partialState == nil {
|
|
return errors.New("partial state not enabled")
|
|
}
|
|
|
|
log.Info("Triggering partial state resync due to deep reorg",
|
|
"ancestor", ancestor.Number,
|
|
"root", ancestor.Root.Hex())
|
|
|
|
// TODO(partial-state): Implement resync coordination with downloader.
|
|
// This requires extending eth/downloader to support targeted state sync.
|
|
// For now, return an error indicating manual intervention may be needed.
|
|
//
|
|
// The implementation should:
|
|
// 1. Pause normal block processing
|
|
// 2. Use snap sync to fetch state at ancestor.Root
|
|
// 3. Apply ContractFilter to only store tracked contract storage
|
|
// 4. Resume normal operation once state is available
|
|
return errors.New("partial state resync not yet implemented - manual intervention required")
|
|
}
|