diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 299d7efd7b..bdc95d19ac 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -8,6 +8,7 @@ import ( "slices" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types/bal" @@ -22,6 +23,12 @@ type ProcessResultWithMetrics struct { StateTransitionMetrics *state.BALStateTransitionMetrics ExecTime time.Duration PostProcessTime time.Duration + + // Preimages holds the SHA3 preimages seen by the VM across the whole block + // (the pre/post-transaction system executions and every transaction). They + // are gathered here so the caller can install them into the state + // transition without the execution helpers needing a reference to it. + Preimages map[common.Hash][]byte } // errResult wraps an error into a new ProcessResultWithMetrics instance @@ -112,6 +119,24 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar p.chain.Engine().Finalize(p.chain, block.Header(), evm.StateDB, block.Body(), uint32(lastBALIdx), postBAL) + // Gather the SHA3 preimages seen across the block. postTxState is a copy of + // the block statedb taken after pre-transaction processing, so its set + // already covers both the pre-tx system execution and the post-tx execution + // (PostExecution + Finalize) performed above; the per-transaction sets are + // carried on each result. The merged set is returned to the caller, which + // owns the state transition and installs them there. + preimages := make(map[common.Hash][]byte) + for hash, preimage := range postTxState.Preimages() { + preimages[hash] = preimage + } + for i := range results { + for hash, preimage := range results[i].preimages { + if _, ok := preimages[hash]; !ok { + preimages[hash] = preimage + } + } + } + blockAccessList := bal.NewConstructionBlockAccessList() blockAccessList.Merge(preTxBAL) blockAccessList.Merge(postBAL) @@ -137,6 +162,7 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar }, PostProcessTime: tPostprocess, ExecTime: tExec, + Preimages: preimages, } } @@ -150,6 +176,10 @@ type txExecResult struct { txState uint64 blockAccessList *bal.ConstructionBlockAccessList + + // preimages holds the SHA3 preimages recorded while executing this + // transaction against its own statedb copy. + preimages map[common.Hash][]byte } // resultHandler polls until all transactions have finished executing and the @@ -303,7 +333,11 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st prestate := startingState.Copy() workers.Go(func() error { prestate = prestate.WithReader(state.NewReaderWithAccessList(statedb.Reader(), prepared, balIdx)) - txResCh <- *p.execTx(block, tx, balIdx, prestate, signer) + res := p.execTx(block, tx, balIdx, prestate, signer) + // Carry this transaction's preimages on the result; they're merged + // into the block-level set in prepareExecResult. + res.preimages = prestate.Preimages() + txResCh <- *res return nil }) } @@ -314,6 +348,10 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st if res.ProcessResult.Error != nil { return nil, res.ProcessResult.Error } + // Install the block's preimages into the state transition so they're + // persisted when the block is committed. Process is the sole owner of the + // state transition, keeping the execution helpers free of it. + stateTransition.AddPreimages(res.Preimages) // TODO: remove preprocess metric ? res.PreProcessTime = tPreprocess return res, nil diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 29135494e5..07b62258c9 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -47,6 +47,12 @@ type BALStateTransition struct { stateUpdate *StateUpdate + // preimages holds the SHA3 preimages seen by the VM across the whole block: + // the pre/post-transaction system executions and every individual + // transaction execution. It is populated once via AddPreimages after the + // parallel execution has been collected. + preimages map[common.Hash][]byte + metrics BALStateTransitionMetrics maxBALIdx int @@ -108,6 +114,7 @@ func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Databas tries: sync.Map{}, deletions: make(map[common.Address]struct{}), stateUpdate: nil, + preimages: make(map[common.Hash][]byte), maxBALIdx: len(block.Transactions()) + 1, }, nil } @@ -533,7 +540,23 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { return s.rootHash } -func (s *BALStateTransition) Preimages() map[common.Hash][]byte { - // TODO: implement this - return make(map[common.Hash][]byte) +// AddPreimages merges a set of SHA3 preimages into the block-level preimage +// set, leaving any hash already present untouched (mirroring the +// first-write-wins behaviour of StateDB.AddPreimage). The parallel processor +// collects the preimages from the per-transaction and pre/post-transaction +// executions and installs them here once execution has completed. +func (s *BALStateTransition) AddPreimages(preimages map[common.Hash][]byte) { + for hash, preimage := range preimages { + if _, ok := s.preimages[hash]; !ok { + s.preimages[hash] = preimage + } + } +} + +// Preimages returns the SHA3 preimages seen by the VM over the course of the +// block. Unlike the sequential processor, which reads them from a single +// statedb, the BAL processor executes the block across several statedbs and +// installs the combined set via AddPreimages. +func (s *BALStateTransition) Preimages() map[common.Hash][]byte { + return s.preimages } diff --git a/core/state/bal_state_transition_test.go b/core/state/bal_state_transition_test.go new file mode 100644 index 0000000000..5ec6ad6e56 --- /dev/null +++ b/core/state/bal_state_transition_test.go @@ -0,0 +1,83 @@ +// Copyright 2026 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 . + +package state + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// newPreimageTransition returns a BALStateTransition with only the preimage +// accumulator initialised, which is all that AddPreimages/Preimages touch. +func newPreimageTransition() *BALStateTransition { + return &BALStateTransition{preimages: make(map[common.Hash][]byte)} +} + +// TestBALStateTransitionPreimagesUnion verifies that AddPreimages folds the +// preimages contributed by the various block executions (the per-transaction +// copies plus the pre/post-tx execution) into a single set returned by +// Preimages. +func TestBALStateTransitionPreimagesUnion(t *testing.T) { + s := newPreimageTransition() + + // Empty/nil inputs are no-ops. + s.AddPreimages(nil) + s.AddPreimages(map[common.Hash][]byte{}) + if got := len(s.Preimages()); got != 0 { + t.Fatalf("expected empty set, got %d entries", got) + } + + s.AddPreimages(map[common.Hash][]byte{ + {0x01}: []byte("pre"), + {0x02}: []byte("tx1"), + {0x03}: []byte("tx2"), + {0x04}: []byte("post"), + }) + + want := map[common.Hash][]byte{ + {0x01}: []byte("pre"), + {0x02}: []byte("tx1"), + {0x03}: []byte("tx2"), + {0x04}: []byte("post"), + } + got := s.Preimages() + if len(got) != len(want) { + t.Fatalf("preimage count: got %d, want %d", len(got), len(want)) + } + for h, v := range want { + if !bytes.Equal(got[h], v) { + t.Errorf("preimage %x: got %q, want %q", h, got[h], v) + } + } +} + +// TestBALStateTransitionPreimagesFirstWriteWins verifies that a preimage for a +// hash already present is not overwritten across successive AddPreimages calls, +// matching StateDB.AddPreimage. +func TestBALStateTransitionPreimagesFirstWriteWins(t *testing.T) { + s := newPreimageTransition() + key := common.Hash{0xaa} + + s.AddPreimages(map[common.Hash][]byte{key: []byte("first")}) + s.AddPreimages(map[common.Hash][]byte{key: []byte("second")}) + + if got := s.Preimages()[key]; !bytes.Equal(got, []byte("first")) { + t.Fatalf("expected first write to win, got %q", got) + } +}