diff --git a/core/blockchain.go b/core/blockchain.go index 29eca4800d..edc89cab1e 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2129,13 +2129,16 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * useAsyncReads := bc.cfg.BALExecutionMode != bal.BALExecutionNoBatchIO al := block.AccessList() - accessListReader := bal.NewAccessListReader(*al) - prefetchReader, err := sdb.ReaderWithPrefetch(parentRoot, accessListReader.StorageKeys(useAsyncReads), bc.cfg.PrefetchWorkers, bc.cfg.BlockingPrefetch) + // Preprocess the access list once for the whole block; the resulting + // structure is read-only and shared by the prefetch reader, the state + // transition and every per-transaction execution reader. + prepared := bal.NewPreparedAccessList(*al) + prefetchReader, err := sdb.ReaderWithPrefetch(parentRoot, prepared.StorageKeys(useAsyncReads), bc.cfg.PrefetchWorkers, bc.cfg.BlockingPrefetch) if err != nil { return nil, err } - stateTransition, err := state.NewBALStateTransition(block, prefetchReader, sdb, parentRoot) + stateTransition, err := state.NewBALStateTransition(block, prefetchReader, sdb, parentRoot, prepared) if err != nil { return nil, err } diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 3407aabad7..d2b67a6a86 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -44,14 +44,14 @@ func NewParallelStateProcessor(chain *HeaderChain, vmConfig *vm.Config) Parallel // performs post-tx state transition (system contracts and withdrawals) // and calculates the ProcessResult, returning it to be sent on resCh // by resultHandler -func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, preTxBal *bal.ConstructionBlockAccessList, statedb *state.StateDB, results []txExecResult) *ProcessResultWithMetrics { +func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, preTxBal *bal.ConstructionBlockAccessList, prepared *bal.PreparedAccessList, statedb *state.StateDB, results []txExecResult) *ProcessResultWithMetrics { tExec := time.Since(tExecStart) tPostprocessStart := time.Now() header := block.Header() vmContext := NewEVMBlockContext(header, p.chain, nil) lastBALIdx := len(block.Transactions()) + 1 - postTxState := statedb.WithReader(state.NewReaderWithBlockLevelAccessList(statedb.Reader(), *block.AccessList(), lastBALIdx)) + postTxState := statedb.WithReader(state.NewReaderWithPreparedAccessList(statedb.Reader(), prepared, lastBALIdx)) cfg := vm.Config{ NoBaseFee: p.vmCfg.NoBaseFee, @@ -148,7 +148,7 @@ type txExecResult struct { // resultHandler polls until all transactions have finished executing and the // state root calculation is complete. The result is emitted on resCh. -func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxBAL *bal.ConstructionBlockAccessList, statedb *state.StateDB, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { +func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxBAL *bal.ConstructionBlockAccessList, prepared *bal.PreparedAccessList, statedb *state.StateDB, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { // 1. if the block has transactions, receive the execution results from all of them and return an error on resCh if any txs err'd // 2. once all txs are executed, compute the post-tx state transition and produce the ProcessResult sending it on resCh (or an error if the post-tx state didn't match what is reported in the BAL) var results []txExecResult @@ -187,7 +187,7 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxBAL *bal } } - execResults := p.prepareExecResult(block, tExecStart, preTxBAL, statedb, results) + execResults := p.prepareExecResult(block, tExecStart, preTxBAL, prepared, statedb, results) rootCalcRes := <-stateRootCalcResCh if execResults.ProcessResult.Error != nil { @@ -292,6 +292,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st ) startingState := statedb.Copy() + prepared := stateTransition.PreparedAccessList() preTxBal, err := p.processBlockPreTx(block, statedb, cfg) if err != nil { return nil, err @@ -302,7 +303,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st // execute transactions and state root calculation in parallel tExecStart = time.Now() - go p.resultHandler(block, preTxBal, statedb, tExecStart, txResCh, rootCalcResultCh, resCh) + go p.resultHandler(block, preTxBal, prepared, statedb, tExecStart, txResCh, rootCalcResultCh, resCh) var workers errgroup.Group workers.SetLimit(runtime.NumCPU()) for i, t := range block.Transactions() { @@ -310,7 +311,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st idx := i sdb := startingState.Copy() workers.Go(func() error { - startingState := sdb.WithReader(state.NewReaderWithBlockLevelAccessList(statedb.Reader(), *block.AccessList(), idx+1)) + startingState := sdb.WithReader(state.NewReaderWithPreparedAccessList(statedb.Reader(), prepared, idx+1)) res := p.execTx(block, tx, idx+1, startingState, signer) txResCh <- *res return nil diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index de643ab0d1..7e44496f7f 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -19,7 +19,7 @@ import ( // and commit for EIP 7928 access-list-containing blocks. An instance of // this object is only used for a single block. type BALStateTransition struct { - accessList bal.AccessListReader + accessList *bal.PreparedAccessList written bal.WrittenCounts db Database reader Reader @@ -86,14 +86,14 @@ type BALStateTransitionMetrics struct { TotalCommitTime time.Duration } -func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Database, parentRoot common.Hash) (*BALStateTransition, error) { +func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Database, parentRoot common.Hash, prepared *bal.PreparedAccessList) (*BALStateTransition, error) { stateTrie, err := db.OpenTrie(parentRoot) if err != nil { return nil, err } return &BALStateTransition{ - accessList: bal.NewAccessListReader(*block.AccessList()), + accessList: prepared, written: block.AccessList().WrittenCounts(), db: db, reader: prefetchReader, @@ -115,6 +115,13 @@ func (s *BALStateTransition) WrittenCounts() bal.WrittenCounts { return s.written } +// PreparedAccessList returns the shared, read-only preprocessed access list for +// the block. It is built once per block and reused by the parallel execution +// readers so the preprocessing is not repeated per transaction. +func (s *BALStateTransition) PreparedAccessList() *bal.PreparedAccessList { + return s.accessList +} + func (s *BALStateTransition) Error() error { return s.err } diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index 72727d35c3..b3390ad63c 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -210,20 +210,34 @@ func (r *prefetchStateReader) process(start, limit int) { // ReaderWithBlockLevelAccessList provides state access that reflects the // pre-transition state combined with the mutations made by transactions // prior to TxIndex. +// +// It is a cheap, per-transaction view over a shared, read-only +// bal.PreparedAccessList: constructing one is O(1) and every lookup is an +// allocation-free binary search. type ReaderWithBlockLevelAccessList struct { Reader - AccessList bal.AccessListReader - TxIndex int + prepared *bal.PreparedAccessList + TxIndex int } -func NewReaderWithBlockLevelAccessList(base Reader, accessList bal.BlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList { +// NewReaderWithPreparedAccessList wraps a base reader with a shared, already +// preprocessed access list. This is the cheap constructor used on the hot path: +// the prepared list is built once per block and borrowed by every per-tx reader. +func NewReaderWithPreparedAccessList(base Reader, prepared *bal.PreparedAccessList, txIndex int) *ReaderWithBlockLevelAccessList { return &ReaderWithBlockLevelAccessList{ - Reader: base, - AccessList: bal.NewAccessListReader(accessList), - TxIndex: txIndex, + Reader: base, + prepared: prepared, + TxIndex: txIndex, } } +// NewReaderWithBlockLevelAccessList wraps a base reader with a raw access list, +// preprocessing it on the spot. Prefer NewReaderWithPreparedAccessList when the +// prepared list can be built once and shared across multiple readers. +func NewReaderWithBlockLevelAccessList(base Reader, accessList bal.BlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList { + return NewReaderWithPreparedAccessList(base, bal.NewPreparedAccessList(accessList), txIndex) +} + // Account implements Reader, returning the account with the specific address. func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (acct *types.StateAccount, err error) { acct, err = r.Reader.Account(addr) @@ -231,9 +245,11 @@ func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (acct *typ return nil, err } - mut := r.AccessList.AccountMutations(addr, r.TxIndex) - if mut == nil { - return + balance := r.prepared.Balance(addr, r.TxIndex) + code := r.prepared.Code(addr, r.TxIndex) + nonce, hasNonce := r.prepared.Nonce(addr, r.TxIndex) + if balance == nil && code == nil && !hasNonce { + return acct, nil } if acct == nil { @@ -244,15 +260,18 @@ func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (acct *typ acct = acct.Copy() } - if mut.Balance != nil { - acct.Balance = mut.Balance + // balance and code alias the shared access list; this is safe because the + // EVM never mutates them in place (it replaces the pointer/slice wholesale, + // and the journal clones before stashing). + if balance != nil { + acct.Balance = balance } - if mut.Code != nil { - codeHash := crypto.Keccak256Hash(mut.Code) + if code != nil { + codeHash := crypto.Keccak256Hash(code) acct.CodeHash = codeHash[:] } - if mut.Nonce != nil { - acct.Nonce = *mut.Nonce + if hasNonce { + acct.Nonce = nonce } return } @@ -260,9 +279,8 @@ func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (acct *typ // Storage implements Reader, returning the storage slot with the specific // address and slot key. func (r *ReaderWithBlockLevelAccessList) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { - val := r.AccessList.Storage(addr, slot, r.TxIndex) - if val != nil { - return *val, nil + if val, ok := r.prepared.StorageAt(addr, slot, r.TxIndex); ok { + return val, nil } return r.Reader.Storage(addr, slot) } @@ -270,9 +288,8 @@ func (r *ReaderWithBlockLevelAccessList) Storage(addr common.Address, slot commo // Has implements Reader, returning the flag indicating whether the contract // code with specified address and hash exists or not. func (r *ReaderWithBlockLevelAccessList) Has(addr common.Address, codeHash common.Hash) bool { - mut := r.AccessList.AccountMutations(addr, r.TxIndex) - if mut != nil && mut.Code != nil { - return crypto.Keccak256Hash(mut.Code) == codeHash + if code := r.prepared.Code(addr, r.TxIndex); code != nil { + return crypto.Keccak256Hash(code) == codeHash } return r.Reader.Has(addr, codeHash) } @@ -280,10 +297,8 @@ func (r *ReaderWithBlockLevelAccessList) Has(addr common.Address, codeHash commo // Code implements Reader, returning the contract code with specified address // and hash. func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) []byte { - mut := r.AccessList.AccountMutations(addr, r.TxIndex) - if mut != nil && mut.Code != nil && crypto.Keccak256Hash(mut.Code) == codeHash { - // TODO: need to copy here? - return mut.Code + if code := r.prepared.Code(addr, r.TxIndex); code != nil && crypto.Keccak256Hash(code) == codeHash { + return code } return r.Reader.Code(addr, codeHash) } @@ -291,9 +306,8 @@ func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash comm // CodeSize implements Reader, returning the contract code size with specified // address and hash. func (r *ReaderWithBlockLevelAccessList) CodeSize(addr common.Address, codeHash common.Hash) int { - mut := r.AccessList.AccountMutations(addr, r.TxIndex) - if mut != nil && mut.Code != nil && crypto.Keccak256Hash(mut.Code) == codeHash { - return len(mut.Code) + if code := r.prepared.Code(addr, r.TxIndex); code != nil && crypto.Keccak256Hash(code) == codeHash { + return len(code) } return r.Reader.CodeSize(addr, codeHash) } diff --git a/core/types/bal/bal_reader.go b/core/types/bal/bal_reader.go index e6cffc922e..07b32c8ab1 100644 --- a/core/types/bal/bal_reader.go +++ b/core/types/bal/bal_reader.go @@ -1,53 +1,176 @@ package bal import ( - "bytes" + "sort" + "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" ) -// AccessListReader exposes utilities to read state mutations and accesses from an access list -type AccessListReader map[common.Address]*AccountAccess - -func NewAccessListReader(bal BlockAccessList) (reader AccessListReader) { - reader = make(AccessListReader) - for _, accountAccess := range bal { - reader[accountAccess.Address] = &accountAccess - } - return +// PreparedAccessList is an immutable, per-block preprocessed view of a +// BlockAccessList optimized for repeated point-in-time reads. +// +// It is built once per block (NewPreparedAccessList) before parallel +// transaction execution begins. The change slices it holds are the +// already-sorted slices decoded from the BlockAccessList, borrowed by +// reference (never copied, never mutated). After construction the structure +// is read-only and therefore safe for concurrent use by all per-transaction +// readers without any synchronization. +// +// Each lookup binary-searches the relevant change slice for the last mutation +// strictly before the queried block-access index, which is O(log K) and +// allocation-free, in contrast to the previous map-backed reader that +// re-walked every change array from index 0 and re-allocated an aggregate +// mutation object on every call. +type PreparedAccessList struct { + accounts map[common.Address]*preparedAccount } -// AccountMutations returns the aggregate mutation for an account up until (and not including) the given block access -// list index. -func (a AccessListReader) AccountMutations(addr common.Address, idx int) (res *AccountMutations) { - diff, exist := a[addr] - if !exist { - return nil - } +type preparedAccount struct { + // The following slices are borrowed directly from the decoded + // AccountAccess. They are validated to be strictly sorted ascending by + // BlockAccessIndex (see bal_encoding.go), which is exactly the key we + // binary-search on. + balances []encodingBalanceChange + nonces []encodingAccountNonce + codes []encodingCodeChange + storage map[common.Hash]*preparedSlot - res = &AccountMutations{} + // access is retained to back the once-per-block aggregate helpers + // (StorageKeys, AllDestructions) without re-deriving anything. + access *AccountAccess +} - for i := 0; i < len(diff.BalanceChanges) && diff.BalanceChanges[i].BlockAccessIndex < uint32(idx); i++ { - res.Balance = diff.BalanceChanges[i].PostBalance.Clone() - } +type preparedSlot struct { + changes []encodingStorageWrite // borrowed, sorted asc by BlockAccessIndex +} - for i := 0; i < len(diff.CodeChanges) && diff.CodeChanges[i].BlockAccessIndex < uint32(idx); i++ { - res.Code = bytes.Clone(diff.CodeChanges[i].NewCode) - } - - for i := 0; i < len(diff.NonceChanges) && diff.NonceChanges[i].BlockAccessIndex < uint32(idx); i++ { - res.Nonce = new(uint64) - *res.Nonce = diff.NonceChanges[i].PostNonce - } - - if len(diff.StorageChanges) > 0 { - res.StorageWrites = make(map[common.Hash]common.Hash) - for _, slotWrites := range diff.StorageChanges { - for i := 0; i < len(slotWrites.SlotChanges) && slotWrites.SlotChanges[i].BlockAccessIndex < uint32(idx); i++ { - res.StorageWrites[slotWrites.Slot.Bytes32()] = slotWrites.SlotChanges[i].PostValue.Bytes32() +// NewPreparedAccessList preprocesses a BlockAccessList into a PreparedAccessList. +// It performs a single linear pass and borrows the underlying change slices by +// reference; the provided list must not be mutated afterwards. +func NewPreparedAccessList(list BlockAccessList) *PreparedAccessList { + accounts := make(map[common.Address]*preparedAccount, len(list)) + for i := range list { + a := &list[i] // index; do not range-copy the AccountAccess + pa := &preparedAccount{ + balances: a.BalanceChanges, + nonces: a.NonceChanges, + codes: a.CodeChanges, + access: a, + } + if len(a.StorageChanges) > 0 { + pa.storage = make(map[common.Hash]*preparedSlot, len(a.StorageChanges)) + for j := range a.StorageChanges { + sc := &a.StorageChanges[j] + pa.storage[sc.Slot.Bytes32()] = &preparedSlot{changes: sc.SlotChanges} } } + accounts[a.Address] = pa } + return &PreparedAccessList{accounts: accounts} +} +// lastBefore returns the position of the last element in a slice of n elements +// sorted ascending by BlockAccessIndex whose key is strictly less than idx, or +// -1 if no such element exists. keyAt returns the BlockAccessIndex at position k. +func lastBefore(n int, idx uint32, keyAt func(k int) uint32) int { + // sort.Search returns the smallest position whose key is >= idx; everything + // before it is strictly less than idx, so the answer is that position - 1. + return sort.Search(n, func(k int) bool { return keyAt(k) >= idx }) - 1 +} + +// Balance returns the post-balance in effect immediately before the given block +// access index, or nil if the account's balance was not changed before idx. +// The returned pointer aliases the access list and must not be mutated. +func (p *PreparedAccessList) Balance(addr common.Address, idx int) *uint256.Int { + a := p.accounts[addr] + if a == nil { + return nil + } + k := lastBefore(len(a.balances), uint32(idx), func(i int) uint32 { return a.balances[i].BlockAccessIndex }) + if k < 0 { + return nil + } + return a.balances[k].PostBalance +} + +// Nonce returns the post-nonce in effect immediately before the given block +// access index. The boolean is false if the nonce was not changed before idx. +func (p *PreparedAccessList) Nonce(addr common.Address, idx int) (uint64, bool) { + a := p.accounts[addr] + if a == nil { + return 0, false + } + k := lastBefore(len(a.nonces), uint32(idx), func(i int) uint32 { return a.nonces[i].BlockAccessIndex }) + if k < 0 { + return 0, false + } + return a.nonces[k].PostNonce, true +} + +// Code returns the contract code in effect immediately before the given block +// access index, or nil if the code was not changed before idx. The returned +// slice aliases the access list and must not be mutated. +func (p *PreparedAccessList) Code(addr common.Address, idx int) []byte { + a := p.accounts[addr] + if a == nil { + return nil + } + k := lastBefore(len(a.codes), uint32(idx), func(i int) uint32 { return a.codes[i].BlockAccessIndex }) + if k < 0 { + return nil + } + return a.codes[k].NewCode +} + +// StorageAt returns the post-value of a storage slot immediately before the +// given block access index. The boolean is false if the slot was not written +// before idx. +func (p *PreparedAccessList) StorageAt(addr common.Address, slot common.Hash, idx int) (common.Hash, bool) { + a := p.accounts[addr] + if a == nil { + return common.Hash{}, false + } + s := a.storage[slot] + if s == nil { + return common.Hash{}, false + } + k := lastBefore(len(s.changes), uint32(idx), func(i int) uint32 { return s.changes[i].BlockAccessIndex }) + if k < 0 { + return common.Hash{}, false + } + return s.changes[k].PostValue.Bytes32(), true +} + +// AccountMutations returns the aggregate mutation for an account up until (and +// not including) the given block access list index, or nil if the account was +// not mutated before idx. +func (p *PreparedAccessList) AccountMutations(addr common.Address, idx int) *AccountMutations { + a := p.accounts[addr] + if a == nil { + return nil + } + res := &AccountMutations{} + if bal := p.Balance(addr, idx); bal != nil { + res.Balance = bal.Clone() + } + if code := p.Code(addr, idx); code != nil { + res.Code = code + } + if nonce, ok := p.Nonce(addr, idx); ok { + res.Nonce = new(uint64) + *res.Nonce = nonce + } + for slot, s := range a.storage { + k := lastBefore(len(s.changes), uint32(idx), func(i int) uint32 { return s.changes[i].BlockAccessIndex }) + if k < 0 { + continue + } + if res.StorageWrites == nil { + res.StorageWrites = make(map[common.Hash]common.Hash) + } + res.StorageWrites[slot] = s.changes[k].PostValue.Bytes32() + } if res.Code == nil && res.Nonce == nil && len(res.StorageWrites) == 0 && res.Balance == nil { return nil } @@ -56,11 +179,12 @@ func (a AccessListReader) AccountMutations(addr common.Address, idx int) (res *A type StorageKeys map[common.Address][]common.Hash -// StorageKeys returns the set of accounts and storage keys mutated in the access list. -// If reads is set, the un-mutated accounts/keys are included in the result. -func (a AccessListReader) StorageKeys(reads bool) (keys StorageKeys) { +// StorageKeys returns the set of accounts and storage keys mutated in the access +// list. If reads is set, the un-mutated accounts/keys are included in the result. +func (p *PreparedAccessList) StorageKeys(reads bool) (keys StorageKeys) { keys = make(StorageKeys) - for addr, acct := range a { + for addr, a := range p.accounts { + acct := a.access for _, storageChange := range acct.StorageChanges { keys[addr] = append(keys[addr], storageChange.Slot.Bytes32()) } @@ -74,36 +198,23 @@ func (a AccessListReader) StorageKeys(reads bool) (keys StorageKeys) { return } -// Storage returns the value of a storage key at the start of executing an index. -// If the slot has no mutations in the access list, it returns nil. -func (a AccessListReader) Storage(addr common.Address, key common.Hash, idx int) (val *common.Hash) { - storageMuts := a.AccountMutations(addr, idx) - if storageMuts != nil { - res, ok := storageMuts.StorageWrites[key] - if ok { - return &res - } - } - return nil -} - -// Mutations returns the aggregate state mutations from bal indices [0, idx) -func (a AccessListReader) Mutations(idx int) *StateMutations { +// Mutations returns the aggregate state mutations from bal indices [0, idx). +func (p *PreparedAccessList) Mutations(idx int) *StateMutations { res := make(StateMutations) - for addr := range a { - if mut := a.AccountMutations(addr, idx); mut != nil { + for addr := range p.accounts { + if mut := p.AccountMutations(addr, idx); mut != nil { res[addr] = *mut } } return &res } -// AllDestructions returns all accounts that experienced a destruction, regardless of whether -// they were later resurrected and exist after the block. It excludes ephemeral contracts from -// the result. -func (a AccessListReader) AllDestructions() (res []common.Address) { - for addr, access := range a { - for _, nonce := range access.NonceChanges { +// AllDestructions returns all accounts that experienced a destruction, regardless +// of whether they were later resurrected and exist after the block. It excludes +// ephemeral contracts from the result. +func (p *PreparedAccessList) AllDestructions() (res []common.Address) { + for addr, a := range p.accounts { + for _, nonce := range a.access.NonceChanges { if nonce.PostNonce == 0 { res = append(res, addr) break