go-ethereum/core/blockchain_partial.go
CPerezz c3c4dfd838
core, eth: fix post-sync block processing and BAL type compatibility
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>
2026-04-17 12:04:09 +02:00

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