From 3880f712f14c05cefd654e87820ece110c061625 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Fri, 13 Mar 2026 18:19:58 +0000 Subject: [PATCH 1/7] initial commit --- cmd/keeper/main.go | 11 +--------- core/block_validator.go | 33 +++------------------------- core/blockchain.go | 22 ++++--------------- core/blockchain_insert.go | 31 ++++++++++++++++++++++----- core/blockchain_test.go | 2 +- core/stateless.go | 45 +++++++++++++++++---------------------- core/types.go | 2 +- eth/catalyst/witness.go | 5 ++--- 8 files changed, 58 insertions(+), 93 deletions(-) diff --git a/cmd/keeper/main.go b/cmd/keeper/main.go index df6881acbf..14316e6659 100644 --- a/cmd/keeper/main.go +++ b/cmd/keeper/main.go @@ -53,17 +53,8 @@ func main() { } vmConfig := vm.Config{} - crossStateRoot, crossReceiptRoot, err := core.ExecuteStateless(context.Background(), chainConfig, vmConfig, payload.Block, payload.Witness) - if err != nil { + if err := core.ExecuteStateless(context.Background(), chainConfig, vmConfig, payload.Block, payload.Witness); err != nil { fmt.Fprintf(os.Stderr, "stateless self-validation failed: %v\n", err) os.Exit(10) } - if crossStateRoot != payload.Block.Root() { - fmt.Fprintf(os.Stderr, "stateless self-validation root mismatch (cross: %x local: %x)\n", crossStateRoot, payload.Block.Root()) - os.Exit(11) - } - if crossReceiptRoot != payload.Block.ReceiptHash() { - fmt.Fprintf(os.Stderr, "stateless self-validation receipt root mismatch (cross: %x local: %x)\n", crossReceiptRoot, payload.Block.ReceiptHash()) - os.Exit(12) - } } diff --git a/core/block_validator.go b/core/block_validator.go index 008444fbbc..0b2d48c2dc 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -20,7 +20,6 @@ import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" @@ -33,16 +32,11 @@ import ( // BlockValidator implements Validator. type BlockValidator struct { config *params.ChainConfig // Chain configuration options - bc *BlockChain // Canonical block chain } // NewBlockValidator returns a new block validator which is safe for re-use -func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain) *BlockValidator { - validator := &BlockValidator{ - config: config, - bc: blockchain, - } - return validator +func NewBlockValidator(config *params.ChainConfig) *BlockValidator { + return &BlockValidator{config: config} } // ValidateBody validates the given block's uncles and verifies the block @@ -53,17 +47,9 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { if v.config.IsOsaka(block.Number(), block.Time()) && block.Size() > params.MaxBlockSize { return ErrBlockOversized } - // Check whether the block is already imported. - if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { - return ErrKnownBlock - } - // Header validity is known at this point. Here we verify that uncles, transactions // and withdrawals given in the block body match the header. header := block.Header() - if err := v.bc.engine.VerifyUncles(v.bc, block); err != nil { - return err - } if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash { return fmt.Errorf("uncle root hash mismatch (header value %x, calculated %x)", header.UncleHash, hash) } @@ -110,20 +96,12 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { return errors.New("data blobs present in block body") } } - - // Ancestor block must be known. - if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { - if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { - return consensus.ErrUnknownAncestor - } - return consensus.ErrPrunedAncestor - } return nil } // ValidateState validates the various changes that happen after a state transition, // such as amount of used gas, the receipt roots and the state root itself. -func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult, stateless bool) error { +func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult) error { if res == nil { return errors.New("nil ProcessResult value") } @@ -141,11 +119,6 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD if rbloom != header.Bloom { return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom) } - // In stateless mode, return early because the receipt and state root are not - // provided through the witness, rather the cross validator needs to return it. - if stateless { - return nil - } // The receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]])) receiptSha := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil)) if receiptSha != header.ReceiptHash { diff --git a/core/blockchain.go b/core/blockchain.go index 8df2365072..c4ca7fea15 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -423,7 +423,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, return nil, err } bc.flushInterval.Store(int64(cfg.TrieTimeLimit)) - bc.validator = NewBlockValidator(chainConfig, bc) + bc.validator = NewBlockValidator(chainConfig) bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc) bc.processor = NewStateProcessor(bc.hc) @@ -1908,7 +1908,7 @@ func (bc *BlockChain) insertChain(ctx context.Context, chain types.Blocks, setHe defer close(abort) // Peek the error for the first block to decide the directing import logic - it := newInsertIterator(chain, results, bc.validator) + it := newInsertIterator(chain, results, bc.validator, bc) block, err := it.next() // Left-trim all the known blocks that don't need to build snapshot @@ -2259,7 +2259,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, vstart := time.Now() _, _, spanEnd = telemetry.StartSpan(ctx, "bc.validator.ValidateState") - err = bc.validator.ValidateState(block, statedb, res, false) + err = bc.validator.ValidateState(block, statedb, res) spanEnd(&err) if err != nil { bc.reportBadBlock(block, res, err) @@ -2276,24 +2276,10 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, if witness := statedb.Witness(); witness != nil && config.StatelessSelfValidation { log.Warn("Running stateless self-validation", "block", block.Number(), "hash", block.Hash()) - // Remove critical computed fields from the block to force true recalculation - context := block.Header() - context.Root = common.Hash{} - context.ReceiptHash = common.Hash{} - - task := types.NewBlockWithHeader(context).WithBody(*block.Body()) - // Run the stateless self-cross-validation - crossStateRoot, crossReceiptRoot, err := ExecuteStateless(ctx, bc.chainConfig, bc.cfg.VmConfig, task, witness) - if err != nil { + if err := ExecuteStateless(ctx, bc.chainConfig, bc.cfg.VmConfig, block, witness); err != nil { return nil, fmt.Errorf("stateless self-validation failed: %v", err) } - if crossStateRoot != block.Root() { - return nil, fmt.Errorf("stateless self-validation root mismatch (cross: %x local: %x)", crossStateRoot, block.Root()) - } - if crossReceiptRoot != block.ReceiptHash() { - return nil, fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, block.ReceiptHash()) - } } var ( diff --git a/core/blockchain_insert.go b/core/blockchain_insert.go index 07a250a1bb..6a981fcfa7 100644 --- a/core/blockchain_insert.go +++ b/core/blockchain_insert.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/mclock" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -95,19 +96,21 @@ type insertIterator struct { results <-chan error // Verification result sink from the consensus engine errors []error // Header verification errors for the blocks - index int // Current offset of the iterator - validator Validator // Validator to run if verification succeeds + index int // Current offset of the iterator + validator Validator // Validator to run if verification succeeds + bc *BlockChain // Blockchain for admission checks } // newInsertIterator creates a new iterator based on the given blocks, which are // assumed to be a contiguous chain. -func newInsertIterator(chain types.Blocks, results <-chan error, validator Validator) *insertIterator { +func newInsertIterator(chain types.Blocks, results <-chan error, validator Validator, bc *BlockChain) *insertIterator { return &insertIterator{ chain: chain, results: results, errors: make([]error, 0, len(chain)), index: -1, validator: validator, + bc: bc, } } @@ -127,8 +130,26 @@ func (it *insertIterator) next() (*types.Block, error) { if it.errors[it.index] != nil { return it.chain[it.index], it.errors[it.index] } - // Block header valid, run body validation and return - return it.chain[it.index], it.validator.ValidateBody(it.chain[it.index]) + + block := it.chain[it.index] + + // Skip blocks we've already imported and fully processed. + if it.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { + return block, ErrKnownBlock + } + // Verify uncle blocks against chain history (pre-mereg only) + if err := it.bc.engine.VerifyUncles(it.bc, block); err != nil { + return block, err + } + // Ensure the parent block is known and its state is available. + if !it.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { + if !it.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { + return block, consensus.ErrUnknownAncestor + } + return block, consensus.ErrPrunedAncestor + } + // Validate the block body against header + return block, it.validator.ValidateBody(block) } // previous returns the previous header that was being processed, or nil. diff --git a/core/blockchain_test.go b/core/blockchain_test.go index ce592f0267..0d90c62bcf 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -166,7 +166,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { blockchain.reportBadBlock(block, res, err) return err } - err = blockchain.validator.ValidateState(block, statedb, res, false) + err = blockchain.validator.ValidateState(block, statedb, res) if err != nil { blockchain.reportBadBlock(block, res, err) return err diff --git a/core/stateless.go b/core/stateless.go index 88d8ed8138..0eb236676e 100644 --- a/core/stateless.go +++ b/core/stateless.go @@ -27,56 +27,51 @@ import ( "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" ) -// ExecuteStateless runs a stateless execution based on a witness, verifies -// everything it can locally and returns the state root and receipt root, that -// need the other side to explicitly check. +// ExecuteStateless runs a stateless execution based on a witness, fully +// validating the block including header, body, state root and receipt root. // // This method is a bit of a sore thumb here, but: // - It cannot be placed in core/stateless, because state.New prodces a circular dep // - It cannot be placed outside of core, because it needs to construct a dud headerchain // // TODO(karalabe): Would be nice to resolve both issues above somehow and move it. -func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig vm.Config, block *types.Block, witness *stateless.Witness) (common.Hash, common.Hash, error) { - // Sanity check if the supplied block accidentally contains a set root or - // receipt hash. If so, be very loud, but still continue. - if block.Root() != (common.Hash{}) { - log.Error("stateless runner received state root it's expected to calculate (faulty consensus client)", "block", block.Number()) - } - if block.ReceiptHash() != (common.Hash{}) { - log.Error("stateless runner received receipt root it's expected to calculate (faulty consensus client)", "block", block.Number()) - } +func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig vm.Config, block *types.Block, witness *stateless.Witness) error { // Create and populate the state database to serve as the stateless backend memdb := witness.MakeHashDB() db, err := state.New(witness.Root(), state.NewDatabase(triedb.NewDatabase(memdb, triedb.HashDefaults), nil)) if err != nil { - return common.Hash{}, common.Hash{}, err + return err } // Create a blockchain that is idle, but can be used to access headers through + engine := beacon.New(ethash.NewFaker()) chain := &HeaderChain{ config: config, chainDb: memdb, headerCache: lru.NewCache[common.Hash, *types.Header](256), - engine: beacon.New(ethash.NewFaker()), + engine: engine, + } + // Verify the block header against the parent header from the witness + if err := engine.VerifyHeader(chain, block.Header()); err != nil { + return err } processor := NewStateProcessor(chain) - validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block + validator := NewBlockValidator(config) - // Run the stateless blocks processing and self-validate certain fields + // Verify the block body (transactions, withdrawals, blob gas) against the header + if err := validator.ValidateBody(block); err != nil { + return err + } + // Run the stateless block processing and self-validate all fields res, err := processor.Process(ctx, block, db, vmconfig) if err != nil { - return common.Hash{}, common.Hash{}, err + return err } - if err = validator.ValidateState(block, db, res, true); err != nil { - return common.Hash{}, common.Hash{}, err + if err = validator.ValidateState(block, db, res); err != nil { + return err } - // Almost everything validated, but receipt and state root needs to be returned - receiptRoot := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil)) - stateRoot := db.IntermediateRoot(config.IsEIP158(block.Number())) - return stateRoot, receiptRoot, nil + return nil } diff --git a/core/types.go b/core/types.go index 87bbfcff58..a7681ce6d2 100644 --- a/core/types.go +++ b/core/types.go @@ -33,7 +33,7 @@ type Validator interface { ValidateBody(block *types.Block) error // ValidateState validates the given statedb and optionally the process result. - ValidateState(block *types.Block, state *state.StateDB, res *ProcessResult, stateless bool) error + ValidateState(block *types.Block, state *state.StateDB, res *ProcessResult) error } // Prefetcher is an interface for pre-caching transaction signatures and state. diff --git a/eth/catalyst/witness.go b/eth/catalyst/witness.go index 14ca29e079..f6a51bfd77 100644 --- a/eth/catalyst/witness.go +++ b/eth/catalyst/witness.go @@ -284,11 +284,10 @@ func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, v api.lastNewPayloadUpdate.Store(time.Now().Unix()) log.Trace("Executing block statelessly", "number", block.Number(), "hash", params.BlockHash) - stateRoot, receiptRoot, err := core.ExecuteStateless(context.Background(), api.config(), vm.Config{}, block, witness) - if err != nil { + if err := core.ExecuteStateless(context.Background(), api.config(), vm.Config{}, block, witness); err != nil { log.Warn("ExecuteStatelessPayload: execution failed", "err", err) errorMsg := err.Error() return engine.StatelessPayloadStatusV1{Status: engine.INVALID, ValidationError: &errorMsg}, nil } - return engine.StatelessPayloadStatusV1{Status: engine.VALID, StateRoot: stateRoot, ReceiptsRoot: receiptRoot}, nil + return engine.StatelessPayloadStatusV1{Status: engine.VALID, StateRoot: block.Root(), ReceiptsRoot: block.ReceiptHash()}, nil } From 8d2e4ab796f00f3676ee625eecbca4f33d20b8a4 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Fri, 13 Mar 2026 18:59:45 +0000 Subject: [PATCH 2/7] fix --- eth/catalyst/api_test.go | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index db0505101f..9a3faa6c03 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -1690,26 +1690,17 @@ func TestWitnessCreationAndConsumption(t *testing.T) { t.Fatalf("witness missing from payload") } // Test stateless execution of the created witness - wantStateRoot := envelope.ExecutionPayload.StateRoot - wantReceiptRoot := envelope.ExecutionPayload.ReceiptsRoot - - envelope.ExecutionPayload.StateRoot = common.Hash{} - envelope.ExecutionPayload.ReceiptsRoot = common.Hash{} - res, err := api.ExecuteStatelessPayloadV3(*envelope.ExecutionPayload, []common.Hash{}, &common.Hash{42}, *envelope.Witness) if err != nil { t.Fatalf("error executing stateless payload witness: %v", err) } - if res.StateRoot != wantStateRoot { - t.Fatalf("stateless state root mismatch: have %v, want %v", res.StateRoot, wantStateRoot) + if res.StateRoot != envelope.ExecutionPayload.StateRoot { + t.Fatalf("stateless state root mismatch: have %v, want %v", res.StateRoot, envelope.ExecutionPayload.StateRoot) } - if res.ReceiptsRoot != wantReceiptRoot { - t.Fatalf("stateless receipt root mismatch: have %v, want %v", res.ReceiptsRoot, wantReceiptRoot) + if res.ReceiptsRoot != envelope.ExecutionPayload.ReceiptsRoot { + t.Fatalf("stateless receipt root mismatch: have %v, want %v", res.ReceiptsRoot, envelope.ExecutionPayload.ReceiptsRoot) } // Test block insertion with witness creation - envelope.ExecutionPayload.StateRoot = wantStateRoot - envelope.ExecutionPayload.ReceiptsRoot = wantReceiptRoot - res2, err := api.NewPayloadWithWitnessV3(context.Background(), *envelope.ExecutionPayload, []common.Hash{}, &common.Hash{42}) if err != nil { t.Fatalf("error executing stateless payload witness: %v", err) @@ -1718,21 +1709,15 @@ func TestWitnessCreationAndConsumption(t *testing.T) { t.Fatalf("witness missing from payload") } // Test stateless execution of the created witness - wantStateRoot = envelope.ExecutionPayload.StateRoot - wantReceiptRoot = envelope.ExecutionPayload.ReceiptsRoot - - envelope.ExecutionPayload.StateRoot = common.Hash{} - envelope.ExecutionPayload.ReceiptsRoot = common.Hash{} - res, err = api.ExecuteStatelessPayloadV3(*envelope.ExecutionPayload, []common.Hash{}, &common.Hash{42}, *res2.Witness) if err != nil { t.Fatalf("error executing stateless payload witness: %v", err) } - if res.StateRoot != wantStateRoot { - t.Fatalf("stateless state root mismatch: have %v, want %v", res.StateRoot, wantStateRoot) + if res.StateRoot != envelope.ExecutionPayload.StateRoot { + t.Fatalf("stateless state root mismatch: have %v, want %v", res.StateRoot, envelope.ExecutionPayload.StateRoot) } - if res.ReceiptsRoot != wantReceiptRoot { - t.Fatalf("stateless receipt root mismatch: have %v, want %v", res.ReceiptsRoot, wantReceiptRoot) + if res.ReceiptsRoot != envelope.ExecutionPayload.ReceiptsRoot { + t.Fatalf("stateless receipt root mismatch: have %v, want %v", res.ReceiptsRoot, envelope.ExecutionPayload.ReceiptsRoot) } } From 33f77c4d7c690d61e75fb32e396a317bd591ccf4 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 13 Mar 2026 19:38:11 +0000 Subject: [PATCH 3/7] Apply suggestion from @kevaundray --- core/blockchain_insert.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/blockchain_insert.go b/core/blockchain_insert.go index 6a981fcfa7..1601c72a00 100644 --- a/core/blockchain_insert.go +++ b/core/blockchain_insert.go @@ -137,7 +137,7 @@ func (it *insertIterator) next() (*types.Block, error) { if it.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { return block, ErrKnownBlock } - // Verify uncle blocks against chain history (pre-mereg only) + // Verify uncle blocks against chain history (pre-merge only) if err := it.bc.engine.VerifyUncles(it.bc, block); err != nil { return block, err } From e2ef91d1067b17045cc40d581c5eb86b5732d989 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Fri, 13 Mar 2026 19:50:50 +0000 Subject: [PATCH 4/7] nits --- core/stateless.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/core/stateless.go b/core/stateless.go index 0eb236676e..1002bcc12f 100644 --- a/core/stateless.go +++ b/core/stateless.go @@ -46,6 +46,7 @@ func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig if err != nil { return err } + // Create a blockchain that is idle, but can be used to access headers through engine := beacon.New(ethash.NewFaker()) chain := &HeaderChain{ @@ -54,24 +55,26 @@ func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig headerCache: lru.NewCache[common.Hash, *types.Header](256), engine: engine, } - // Verify the block header against the parent header from the witness - if err := engine.VerifyHeader(chain, block.Header()); err != nil { - return err - } processor := NewStateProcessor(chain) validator := NewBlockValidator(config) - // Verify the block body (transactions, withdrawals, blob gas) against the header + // Pre-execution: Verify the block header against the parent header + if err := engine.VerifyHeader(chain, block.Header()); err != nil { + return err + } + + // Pre-execution: Verify the block body against the header if err := validator.ValidateBody(block); err != nil { return err } - // Run the stateless block processing and self-validate all fields + + // Process the block by executing all transactions res, err := processor.Process(ctx, block, db, vmconfig) if err != nil { return err } - if err = validator.ValidateState(block, db, res); err != nil { - return err - } - return nil + + // Post-execution: Validate gas, bloom, receipts, state root and + // other post execution artifacts + return validator.ValidateState(block, db, res) } From a9b4718d6db407d14018be3b13b3bf5baa662542 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:11:51 +0100 Subject: [PATCH 5/7] rework, keep cross-validation working as-is --- cmd/keeper/main.go | 3 ++- core/block_validator.go | 33 ++++++++++++++++++++++++--- core/blockchain.go | 22 ++++++++++++++---- core/blockchain_insert.go | 31 ++++--------------------- core/blockchain_test.go | 2 +- core/stateless.go | 48 ++++++++++++++++++++++----------------- core/types.go | 2 +- eth/catalyst/witness.go | 5 ++-- 8 files changed, 87 insertions(+), 59 deletions(-) diff --git a/cmd/keeper/main.go b/cmd/keeper/main.go index 14316e6659..15fea02243 100644 --- a/cmd/keeper/main.go +++ b/cmd/keeper/main.go @@ -53,7 +53,8 @@ func main() { } vmConfig := vm.Config{} - if err := core.ExecuteStateless(context.Background(), chainConfig, vmConfig, payload.Block, payload.Witness); err != nil { + _, _, err = core.ExecuteStateless(context.Background(), chainConfig, vmConfig, payload.Block, payload.Witness, true) + if err != nil { fmt.Fprintf(os.Stderr, "stateless self-validation failed: %v\n", err) os.Exit(10) } diff --git a/core/block_validator.go b/core/block_validator.go index 0b2d48c2dc..008444fbbc 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" @@ -32,11 +33,16 @@ import ( // BlockValidator implements Validator. type BlockValidator struct { config *params.ChainConfig // Chain configuration options + bc *BlockChain // Canonical block chain } // NewBlockValidator returns a new block validator which is safe for re-use -func NewBlockValidator(config *params.ChainConfig) *BlockValidator { - return &BlockValidator{config: config} +func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain) *BlockValidator { + validator := &BlockValidator{ + config: config, + bc: blockchain, + } + return validator } // ValidateBody validates the given block's uncles and verifies the block @@ -47,9 +53,17 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { if v.config.IsOsaka(block.Number(), block.Time()) && block.Size() > params.MaxBlockSize { return ErrBlockOversized } + // Check whether the block is already imported. + if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { + return ErrKnownBlock + } + // Header validity is known at this point. Here we verify that uncles, transactions // and withdrawals given in the block body match the header. header := block.Header() + if err := v.bc.engine.VerifyUncles(v.bc, block); err != nil { + return err + } if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash { return fmt.Errorf("uncle root hash mismatch (header value %x, calculated %x)", header.UncleHash, hash) } @@ -96,12 +110,20 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { return errors.New("data blobs present in block body") } } + + // Ancestor block must be known. + if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { + if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { + return consensus.ErrUnknownAncestor + } + return consensus.ErrPrunedAncestor + } return nil } // ValidateState validates the various changes that happen after a state transition, // such as amount of used gas, the receipt roots and the state root itself. -func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult) error { +func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult, stateless bool) error { if res == nil { return errors.New("nil ProcessResult value") } @@ -119,6 +141,11 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD if rbloom != header.Bloom { return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom) } + // In stateless mode, return early because the receipt and state root are not + // provided through the witness, rather the cross validator needs to return it. + if stateless { + return nil + } // The receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]])) receiptSha := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil)) if receiptSha != header.ReceiptHash { diff --git a/core/blockchain.go b/core/blockchain.go index c4ca7fea15..3664da8316 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -423,7 +423,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, return nil, err } bc.flushInterval.Store(int64(cfg.TrieTimeLimit)) - bc.validator = NewBlockValidator(chainConfig) + bc.validator = NewBlockValidator(chainConfig, bc) bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc) bc.processor = NewStateProcessor(bc.hc) @@ -1908,7 +1908,7 @@ func (bc *BlockChain) insertChain(ctx context.Context, chain types.Blocks, setHe defer close(abort) // Peek the error for the first block to decide the directing import logic - it := newInsertIterator(chain, results, bc.validator, bc) + it := newInsertIterator(chain, results, bc.validator) block, err := it.next() // Left-trim all the known blocks that don't need to build snapshot @@ -2259,7 +2259,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, vstart := time.Now() _, _, spanEnd = telemetry.StartSpan(ctx, "bc.validator.ValidateState") - err = bc.validator.ValidateState(block, statedb, res) + err = bc.validator.ValidateState(block, statedb, res, false) spanEnd(&err) if err != nil { bc.reportBadBlock(block, res, err) @@ -2276,10 +2276,24 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, if witness := statedb.Witness(); witness != nil && config.StatelessSelfValidation { log.Warn("Running stateless self-validation", "block", block.Number(), "hash", block.Hash()) + // Remove critical computed fields from the block to force true recalculation + context := block.Header() + context.Root = common.Hash{} + context.ReceiptHash = common.Hash{} + + task := types.NewBlockWithHeader(context).WithBody(*block.Body()) + // Run the stateless self-cross-validation - if err := ExecuteStateless(ctx, bc.chainConfig, bc.cfg.VmConfig, block, witness); err != nil { + crossStateRoot, crossReceiptRoot, err := ExecuteStateless(ctx, bc.chainConfig, bc.cfg.VmConfig, task, witness, false) + if err != nil { return nil, fmt.Errorf("stateless self-validation failed: %v", err) } + if crossStateRoot != block.Root() { + return nil, fmt.Errorf("stateless self-validation root mismatch (cross: %x local: %x)", crossStateRoot, block.Root()) + } + if crossReceiptRoot != block.ReceiptHash() { + return nil, fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, block.ReceiptHash()) + } } var ( diff --git a/core/blockchain_insert.go b/core/blockchain_insert.go index 1601c72a00..07a250a1bb 100644 --- a/core/blockchain_insert.go +++ b/core/blockchain_insert.go @@ -21,7 +21,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -96,21 +95,19 @@ type insertIterator struct { results <-chan error // Verification result sink from the consensus engine errors []error // Header verification errors for the blocks - index int // Current offset of the iterator - validator Validator // Validator to run if verification succeeds - bc *BlockChain // Blockchain for admission checks + index int // Current offset of the iterator + validator Validator // Validator to run if verification succeeds } // newInsertIterator creates a new iterator based on the given blocks, which are // assumed to be a contiguous chain. -func newInsertIterator(chain types.Blocks, results <-chan error, validator Validator, bc *BlockChain) *insertIterator { +func newInsertIterator(chain types.Blocks, results <-chan error, validator Validator) *insertIterator { return &insertIterator{ chain: chain, results: results, errors: make([]error, 0, len(chain)), index: -1, validator: validator, - bc: bc, } } @@ -130,26 +127,8 @@ func (it *insertIterator) next() (*types.Block, error) { if it.errors[it.index] != nil { return it.chain[it.index], it.errors[it.index] } - - block := it.chain[it.index] - - // Skip blocks we've already imported and fully processed. - if it.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { - return block, ErrKnownBlock - } - // Verify uncle blocks against chain history (pre-merge only) - if err := it.bc.engine.VerifyUncles(it.bc, block); err != nil { - return block, err - } - // Ensure the parent block is known and its state is available. - if !it.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { - if !it.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { - return block, consensus.ErrUnknownAncestor - } - return block, consensus.ErrPrunedAncestor - } - // Validate the block body against header - return block, it.validator.ValidateBody(block) + // Block header valid, run body validation and return + return it.chain[it.index], it.validator.ValidateBody(it.chain[it.index]) } // previous returns the previous header that was being processed, or nil. diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 0d90c62bcf..ce592f0267 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -166,7 +166,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { blockchain.reportBadBlock(block, res, err) return err } - err = blockchain.validator.ValidateState(block, statedb, res) + err = blockchain.validator.ValidateState(block, statedb, res, false) if err != nil { blockchain.reportBadBlock(block, res, err) return err diff --git a/core/stateless.go b/core/stateless.go index 1002bcc12f..7cca42b01c 100644 --- a/core/stateless.go +++ b/core/stateless.go @@ -18,6 +18,7 @@ package core import ( "context" + "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" @@ -28,53 +29,58 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" ) -// ExecuteStateless runs a stateless execution based on a witness, fully -// validating the block including header, body, state root and receipt root. +// ExecuteStateless runs a stateless execution based on a witness, verifies +// everything it can locally and returns the state root and receipt root, that +// need the other side to explicitly check. // // This method is a bit of a sore thumb here, but: // - It cannot be placed in core/stateless, because state.New prodces a circular dep // - It cannot be placed outside of core, because it needs to construct a dud headerchain // // TODO(karalabe): Would be nice to resolve both issues above somehow and move it. -func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig vm.Config, block *types.Block, witness *stateless.Witness) error { +func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig vm.Config, block *types.Block, witness *stateless.Witness, validateHeader bool) (common.Hash, common.Hash, error) { // Create and populate the state database to serve as the stateless backend memdb := witness.MakeHashDB() db, err := state.New(witness.Root(), state.NewDatabase(triedb.NewDatabase(memdb, triedb.HashDefaults), nil)) if err != nil { - return err + return common.Hash{}, common.Hash{}, err } - // Create a blockchain that is idle, but can be used to access headers through - engine := beacon.New(ethash.NewFaker()) chain := &HeaderChain{ config: config, chainDb: memdb, headerCache: lru.NewCache[common.Hash, *types.Header](256), - engine: engine, + engine: beacon.New(ethash.NewFaker()), } processor := NewStateProcessor(chain) - validator := NewBlockValidator(config) + validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block - // Pre-execution: Verify the block header against the parent header - if err := engine.VerifyHeader(chain, block.Header()); err != nil { - return err + if validateHeader { + if err := beacon.New(ethash.NewFaker()).VerifyHeader(chain, block.Header()); err != nil { + return common.Hash{}, common.Hash{}, fmt.Errorf("error verifying header in stateless validation: %w", err) + } + + validator := NewBlockValidator(config, nil) + if err := validator.ValidateBody(block); err != nil { + return common.Hash{}, common.Hash{}, fmt.Errorf("error validating body in stateless validation: %w", err) + } } - // Pre-execution: Verify the block body against the header - if err := validator.ValidateBody(block); err != nil { - return err - } - - // Process the block by executing all transactions + // Run the stateless blocks processing and self-validate certain fields res, err := processor.Process(ctx, block, db, vmconfig) if err != nil { - return err + return common.Hash{}, common.Hash{}, err } + if err = validator.ValidateState(block, db, res, validateHeader); err != nil { + return common.Hash{}, common.Hash{}, err + } + // Almost everything validated, but receipt and state root needs to be returned + receiptRoot := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil)) + stateRoot := db.IntermediateRoot(config.IsEIP158(block.Number())) - // Post-execution: Validate gas, bloom, receipts, state root and - // other post execution artifacts - return validator.ValidateState(block, db, res) + return stateRoot, receiptRoot, nil } diff --git a/core/types.go b/core/types.go index a7681ce6d2..87bbfcff58 100644 --- a/core/types.go +++ b/core/types.go @@ -33,7 +33,7 @@ type Validator interface { ValidateBody(block *types.Block) error // ValidateState validates the given statedb and optionally the process result. - ValidateState(block *types.Block, state *state.StateDB, res *ProcessResult) error + ValidateState(block *types.Block, state *state.StateDB, res *ProcessResult, stateless bool) error } // Prefetcher is an interface for pre-caching transaction signatures and state. diff --git a/eth/catalyst/witness.go b/eth/catalyst/witness.go index f6a51bfd77..aaaf69a0cd 100644 --- a/eth/catalyst/witness.go +++ b/eth/catalyst/witness.go @@ -284,10 +284,11 @@ func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, v api.lastNewPayloadUpdate.Store(time.Now().Unix()) log.Trace("Executing block statelessly", "number", block.Number(), "hash", params.BlockHash) - if err := core.ExecuteStateless(context.Background(), api.config(), vm.Config{}, block, witness); err != nil { + stateRoot, receiptRoot, err := core.ExecuteStateless(context.Background(), api.config(), vm.Config{}, block, witness, false) + if err != nil { log.Warn("ExecuteStatelessPayload: execution failed", "err", err) errorMsg := err.Error() return engine.StatelessPayloadStatusV1{Status: engine.INVALID, ValidationError: &errorMsg}, nil } - return engine.StatelessPayloadStatusV1{Status: engine.VALID, StateRoot: block.Root(), ReceiptsRoot: block.ReceiptHash()}, nil + return engine.StatelessPayloadStatusV1{Status: engine.VALID, StateRoot: stateRoot, ReceiptsRoot: receiptRoot}, nil } From 58819e6ba08de3081ddd8e47680e91f80b61be1c Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:42:44 +0100 Subject: [PATCH 6/7] fix panic --- core/block_validator.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/core/block_validator.go b/core/block_validator.go index 008444fbbc..5c8cfce743 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -53,16 +53,19 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { if v.config.IsOsaka(block.Number(), block.Time()) && block.Size() > params.MaxBlockSize { return ErrBlockOversized } - // Check whether the block is already imported. - if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { - return ErrKnownBlock - } - // Header validity is known at this point. Here we verify that uncles, transactions // and withdrawals given in the block body match the header. header := block.Header() - if err := v.bc.engine.VerifyUncles(v.bc, block); err != nil { - return err + + // Chain-dependent checks: skip in stateless mode (no blockchain available). + if v.bc != nil { + // Check whether the block is already imported. + if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { + return ErrKnownBlock + } + if err := v.bc.engine.VerifyUncles(v.bc, block); err != nil { + return err + } } if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash { return fmt.Errorf("uncle root hash mismatch (header value %x, calculated %x)", header.UncleHash, hash) @@ -111,12 +114,14 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } } - // Ancestor block must be known. - if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { - if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { - return consensus.ErrUnknownAncestor + // Ancestor block must be known (skip in stateless mode). + if v.bc != nil { + if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { + if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { + return consensus.ErrUnknownAncestor + } + return consensus.ErrPrunedAncestor } - return consensus.ErrPrunedAncestor } return nil } From f7ed82b12c18e2ca4f4736406f689f0607d859c3 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:45:53 +0100 Subject: [PATCH 7/7] cleanup --- core/stateless.go | 1 - eth/catalyst/api_test.go | 31 +++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/stateless.go b/core/stateless.go index 7cca42b01c..e69bbec2d2 100644 --- a/core/stateless.go +++ b/core/stateless.go @@ -81,6 +81,5 @@ func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig // Almost everything validated, but receipt and state root needs to be returned receiptRoot := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil)) stateRoot := db.IntermediateRoot(config.IsEIP158(block.Number())) - return stateRoot, receiptRoot, nil } diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 9a3faa6c03..db0505101f 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -1690,17 +1690,26 @@ func TestWitnessCreationAndConsumption(t *testing.T) { t.Fatalf("witness missing from payload") } // Test stateless execution of the created witness + wantStateRoot := envelope.ExecutionPayload.StateRoot + wantReceiptRoot := envelope.ExecutionPayload.ReceiptsRoot + + envelope.ExecutionPayload.StateRoot = common.Hash{} + envelope.ExecutionPayload.ReceiptsRoot = common.Hash{} + res, err := api.ExecuteStatelessPayloadV3(*envelope.ExecutionPayload, []common.Hash{}, &common.Hash{42}, *envelope.Witness) if err != nil { t.Fatalf("error executing stateless payload witness: %v", err) } - if res.StateRoot != envelope.ExecutionPayload.StateRoot { - t.Fatalf("stateless state root mismatch: have %v, want %v", res.StateRoot, envelope.ExecutionPayload.StateRoot) + if res.StateRoot != wantStateRoot { + t.Fatalf("stateless state root mismatch: have %v, want %v", res.StateRoot, wantStateRoot) } - if res.ReceiptsRoot != envelope.ExecutionPayload.ReceiptsRoot { - t.Fatalf("stateless receipt root mismatch: have %v, want %v", res.ReceiptsRoot, envelope.ExecutionPayload.ReceiptsRoot) + if res.ReceiptsRoot != wantReceiptRoot { + t.Fatalf("stateless receipt root mismatch: have %v, want %v", res.ReceiptsRoot, wantReceiptRoot) } // Test block insertion with witness creation + envelope.ExecutionPayload.StateRoot = wantStateRoot + envelope.ExecutionPayload.ReceiptsRoot = wantReceiptRoot + res2, err := api.NewPayloadWithWitnessV3(context.Background(), *envelope.ExecutionPayload, []common.Hash{}, &common.Hash{42}) if err != nil { t.Fatalf("error executing stateless payload witness: %v", err) @@ -1709,15 +1718,21 @@ func TestWitnessCreationAndConsumption(t *testing.T) { t.Fatalf("witness missing from payload") } // Test stateless execution of the created witness + wantStateRoot = envelope.ExecutionPayload.StateRoot + wantReceiptRoot = envelope.ExecutionPayload.ReceiptsRoot + + envelope.ExecutionPayload.StateRoot = common.Hash{} + envelope.ExecutionPayload.ReceiptsRoot = common.Hash{} + res, err = api.ExecuteStatelessPayloadV3(*envelope.ExecutionPayload, []common.Hash{}, &common.Hash{42}, *res2.Witness) if err != nil { t.Fatalf("error executing stateless payload witness: %v", err) } - if res.StateRoot != envelope.ExecutionPayload.StateRoot { - t.Fatalf("stateless state root mismatch: have %v, want %v", res.StateRoot, envelope.ExecutionPayload.StateRoot) + if res.StateRoot != wantStateRoot { + t.Fatalf("stateless state root mismatch: have %v, want %v", res.StateRoot, wantStateRoot) } - if res.ReceiptsRoot != envelope.ExecutionPayload.ReceiptsRoot { - t.Fatalf("stateless receipt root mismatch: have %v, want %v", res.ReceiptsRoot, envelope.ExecutionPayload.ReceiptsRoot) + if res.ReceiptsRoot != wantReceiptRoot { + t.Fatalf("stateless receipt root mismatch: have %v, want %v", res.ReceiptsRoot, wantReceiptRoot) } }