mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 13:21:37 +00:00
Merge a6024b73ff into 7c9032dff6
This commit is contained in:
commit
3e105eb003
49 changed files with 2590 additions and 300 deletions
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
)
|
||||
|
||||
var _ = (*executableDataMarshaling)(nil)
|
||||
|
|
@ -36,7 +35,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) {
|
|||
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
|
||||
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
|
||||
SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
|
||||
BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
|
||||
BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"`
|
||||
}
|
||||
var enc ExecutableData
|
||||
enc.ParentHash = e.ParentHash
|
||||
|
|
@ -87,7 +86,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error {
|
|||
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
|
||||
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
|
||||
SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
|
||||
BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
|
||||
BlockAccessList *hexutil.Bytes `json:"blockAccessList,omitempty"`
|
||||
}
|
||||
var dec ExecutableData
|
||||
if err := json.Unmarshal(input, &dec); err != nil {
|
||||
|
|
@ -165,7 +164,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error {
|
|||
e.SlotNumber = (*uint64)(dec.SlotNumber)
|
||||
}
|
||||
if dec.BlockAccessList != nil {
|
||||
e.BlockAccessList = dec.BlockAccessList
|
||||
e.BlockAccessList = *dec.BlockAccessList
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,18 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"math/big"
|
||||
"slices"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
|
|
@ -83,25 +86,25 @@ type payloadAttributesMarshaling struct {
|
|||
|
||||
// ExecutableData is the data necessary to execute an EL payload.
|
||||
type ExecutableData struct {
|
||||
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
|
||||
FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
|
||||
StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
|
||||
ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
|
||||
LogsBloom []byte `json:"logsBloom" gencodec:"required"`
|
||||
Random common.Hash `json:"prevRandao" gencodec:"required"`
|
||||
Number uint64 `json:"blockNumber" gencodec:"required"`
|
||||
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
|
||||
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
|
||||
Timestamp uint64 `json:"timestamp" gencodec:"required"`
|
||||
ExtraData []byte `json:"extraData" gencodec:"required"`
|
||||
BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
|
||||
BlockHash common.Hash `json:"blockHash" gencodec:"required"`
|
||||
Transactions [][]byte `json:"transactions" gencodec:"required"`
|
||||
Withdrawals []*types.Withdrawal `json:"withdrawals"`
|
||||
BlobGasUsed *uint64 `json:"blobGasUsed"`
|
||||
ExcessBlobGas *uint64 `json:"excessBlobGas"`
|
||||
SlotNumber *uint64 `json:"slotNumber,omitempty"`
|
||||
BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
|
||||
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
|
||||
FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
|
||||
StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
|
||||
ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
|
||||
LogsBloom []byte `json:"logsBloom" gencodec:"required"`
|
||||
Random common.Hash `json:"prevRandao" gencodec:"required"`
|
||||
Number uint64 `json:"blockNumber" gencodec:"required"`
|
||||
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
|
||||
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
|
||||
Timestamp uint64 `json:"timestamp" gencodec:"required"`
|
||||
ExtraData []byte `json:"extraData" gencodec:"required"`
|
||||
BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
|
||||
BlockHash common.Hash `json:"blockHash" gencodec:"required"`
|
||||
Transactions [][]byte `json:"transactions" gencodec:"required"`
|
||||
Withdrawals []*types.Withdrawal `json:"withdrawals"`
|
||||
BlobGasUsed *uint64 `json:"blobGasUsed"`
|
||||
ExcessBlobGas *uint64 `json:"excessBlobGas"`
|
||||
SlotNumber *uint64 `json:"slotNumber,omitempty"`
|
||||
BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"`
|
||||
}
|
||||
|
||||
// JSON type overrides for executableData.
|
||||
|
|
@ -314,13 +317,14 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H
|
|||
}
|
||||
|
||||
// If Amsterdam is enabled, data.BlockAccessList is always non-nil,
|
||||
// even for empty blocks with no state transitions.
|
||||
// even for empty blocks with no state transitions. The wire format is
|
||||
// the RLP-encoded access list; the header hash is keccak256(rlp).
|
||||
//
|
||||
// If Amsterdam is not enabled yet, blockAccessListHash is expected
|
||||
// to be nil.
|
||||
var blockAccessListHash *common.Hash
|
||||
if data.BlockAccessList != nil {
|
||||
hash := data.BlockAccessList.Hash()
|
||||
hash := crypto.Keccak256Hash(data.BlockAccessList)
|
||||
blockAccessListHash = &hash
|
||||
}
|
||||
header := &types.Header{
|
||||
|
|
@ -347,32 +351,50 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H
|
|||
SlotNumber: data.SlotNumber,
|
||||
BlockAccessListHash: blockAccessListHash,
|
||||
}
|
||||
return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), nil
|
||||
body := types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}
|
||||
if data.BlockAccessList != nil {
|
||||
balHash := crypto.Keccak256Hash(data.BlockAccessList)
|
||||
header.BlockAccessListHash = &balHash
|
||||
var accessList bal.BlockAccessList
|
||||
if err := rlp.DecodeBytes(data.BlockAccessList, &accessList); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode BAL: %w\n", err)
|
||||
}
|
||||
block := types.NewBlockWithHeader(header).WithBody(body).WithAccessList(&accessList)
|
||||
return block, nil
|
||||
}
|
||||
return types.NewBlockWithHeader(header).WithBody(body), nil
|
||||
}
|
||||
|
||||
// BlockToExecutableData constructs the ExecutableData structure by filling the
|
||||
// fields from the given block. It assumes the given block is post-merge block.
|
||||
func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.BlobTxSidecar, requests [][]byte) *ExecutionPayloadEnvelope {
|
||||
data := &ExecutableData{
|
||||
BlockHash: block.Hash(),
|
||||
ParentHash: block.ParentHash(),
|
||||
FeeRecipient: block.Coinbase(),
|
||||
StateRoot: block.Root(),
|
||||
Number: block.NumberU64(),
|
||||
GasLimit: block.GasLimit(),
|
||||
GasUsed: block.GasUsed(),
|
||||
BaseFeePerGas: block.BaseFee(),
|
||||
Timestamp: block.Time(),
|
||||
ReceiptsRoot: block.ReceiptHash(),
|
||||
LogsBloom: block.Bloom().Bytes(),
|
||||
Transactions: encodeTransactions(block.Transactions()),
|
||||
Random: block.MixDigest(),
|
||||
ExtraData: block.Extra(),
|
||||
Withdrawals: block.Withdrawals(),
|
||||
BlobGasUsed: block.BlobGasUsed(),
|
||||
ExcessBlobGas: block.ExcessBlobGas(),
|
||||
SlotNumber: block.SlotNumber(),
|
||||
BlockAccessList: block.AccessList(),
|
||||
BlockHash: block.Hash(),
|
||||
ParentHash: block.ParentHash(),
|
||||
FeeRecipient: block.Coinbase(),
|
||||
StateRoot: block.Root(),
|
||||
Number: block.NumberU64(),
|
||||
GasLimit: block.GasLimit(),
|
||||
GasUsed: block.GasUsed(),
|
||||
BaseFeePerGas: block.BaseFee(),
|
||||
Timestamp: block.Time(),
|
||||
ReceiptsRoot: block.ReceiptHash(),
|
||||
LogsBloom: block.Bloom().Bytes(),
|
||||
Transactions: encodeTransactions(block.Transactions()),
|
||||
Random: block.MixDigest(),
|
||||
ExtraData: block.Extra(),
|
||||
Withdrawals: block.Withdrawals(),
|
||||
BlobGasUsed: block.BlobGasUsed(),
|
||||
ExcessBlobGas: block.ExcessBlobGas(),
|
||||
SlotNumber: block.SlotNumber(),
|
||||
}
|
||||
// Per Engine API spec (Amsterdam): blockAccessList is the RLP-encoded
|
||||
// access list, serialized as a hex string. Encode it to bytes here.
|
||||
if al := block.AccessList(); al != nil {
|
||||
var buf bytes.Buffer
|
||||
if err := rlp.Encode(&buf, al); err == nil {
|
||||
data.BlockAccessList = buf.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
// Add blobs.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
# https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0
|
||||
a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz
|
||||
|
||||
# version:spec-tests-bal v7.3.1
|
||||
# https://github.com/ethereum/execution-specs/releases
|
||||
# https://github.com/ethereum/execution-specs/releases/download/tests-bal%40v7.3.1
|
||||
3c9bd8799a506a96f74162863efdf5eaa00226e645db6523346fdb7c5ba0bf62 fixtures_bal.tar.gz
|
||||
|
||||
# version:golang 1.25.10
|
||||
# https://go.dev/dl/
|
||||
20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz
|
||||
|
|
|
|||
18
build/ci.go
18
build/ci.go
|
|
@ -160,6 +160,9 @@ var (
|
|||
|
||||
// This is where the tests should be unpacked.
|
||||
executionSpecTestsDir = "tests/spec-tests"
|
||||
|
||||
// This is where the bal-specific release of the tests should be unpacked.
|
||||
executionSpecTestsBALDir = "tests/spec-tests-bal"
|
||||
)
|
||||
|
||||
var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin"))
|
||||
|
|
@ -398,6 +401,7 @@ func doTest(cmdline []string) {
|
|||
// Get test fixtures.
|
||||
if !*short {
|
||||
downloadSpecTestFixtures(csdb, *cachedir)
|
||||
downloadBALSpecTestFixtures(csdb, *cachedir)
|
||||
}
|
||||
|
||||
// Configure the toolchain.
|
||||
|
|
@ -463,6 +467,20 @@ func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string
|
|||
return filepath.Join(cachedir, base)
|
||||
}
|
||||
|
||||
// downloadBALSpecTestFixtures downloads and extracts the bal-specific execution-spec-tests fixtures.
|
||||
func downloadBALSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string {
|
||||
ext := ".tar.gz"
|
||||
base := "fixtures_bal"
|
||||
archivePath := filepath.Join(cachedir, base+ext)
|
||||
if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := build.ExtractArchive(archivePath, executionSpecTestsBALDir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return filepath.Join(cachedir, base)
|
||||
}
|
||||
|
||||
// doCheckGenerate ensures that re-generating generated files does not cause
|
||||
// any mutations in the source file tree.
|
||||
func doCheckGenerate() {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ func runBlockTest(ctx *cli.Context, fname string) ([]testResult, error) {
|
|||
test := tests[name]
|
||||
result := &testResult{Name: name, Pass: true}
|
||||
var finalRoot *common.Hash
|
||||
if err := test.Run(false, rawdb.PathScheme, ctx.Bool(WitnessCrossCheckFlag.Name), tracer, func(res error, chain *core.BlockChain) {
|
||||
if err := test.Run(false, rawdb.PathScheme, ctx.Bool(WitnessCrossCheckFlag.Name), true, tracer, func(res error, chain *core.BlockChain) {
|
||||
if ctx.Bool(DumpFlag.Name) {
|
||||
if s, _ := chain.State(); s != nil {
|
||||
result.State = dump(s)
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ func Transaction(ctx *cli.Context) error {
|
|||
}
|
||||
// For Prague txs, validate the floor data gas.
|
||||
if rules.IsPrague {
|
||||
floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList())
|
||||
floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations())))
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
results = append(results, r)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
|
@ -241,6 +242,28 @@ func makeFullNode(ctx *cli.Context) *node.Node {
|
|||
cfg.Eth.OverrideUBT = &v
|
||||
}
|
||||
|
||||
if ctx.IsSet(utils.BALExecutionModeFlag.Name) {
|
||||
val := ctx.String(utils.BALExecutionModeFlag.Name)
|
||||
switch val {
|
||||
case utils.BalExecutionModeOptimized:
|
||||
cfg.Eth.BALExecutionMode = bal.BALExecutionOptimized
|
||||
case utils.BalExecutionModeNoBatchIO:
|
||||
cfg.Eth.BALExecutionMode = bal.BALExecutionNoBatchIO
|
||||
case utils.BalExecutionModeSequential:
|
||||
cfg.Eth.BALExecutionMode = bal.BALExecutionSequential
|
||||
default:
|
||||
utils.Fatalf("invalid option for --bal.executionmode: %s. acceptable values are full|nobatchio|sequential", val)
|
||||
}
|
||||
}
|
||||
cfg.Eth.BlockingPrefetch = ctx.Bool(utils.BlockingPrefetchFlag.Name)
|
||||
|
||||
prefetchWorkers := ctx.Uint(utils.PrefetchWorkersFlag.Name)
|
||||
if ctx.IsSet(utils.PrefetchWorkersFlag.Name) && prefetchWorkers == 0 {
|
||||
prefetchWorkers = uint(runtime.NumCPU())
|
||||
log.Warn(fmt.Sprintf("invalid value for --bal.prefetchworkers. got 0. sanitizing to %d", prefetchWorkers))
|
||||
}
|
||||
cfg.Eth.PrefetchWorkers = prefetchWorkers
|
||||
|
||||
// Start metrics export if enabled.
|
||||
utils.SetupMetrics(&cfg.Metrics)
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@ var (
|
|||
utils.BinTrieGroupDepthFlag,
|
||||
utils.LightKDFFlag,
|
||||
utils.EthRequiredBlocksFlag,
|
||||
utils.BALExecutionModeFlag,
|
||||
utils.PrefetchWorkersFlag,
|
||||
utils.BlockingPrefetchFlag,
|
||||
utils.CacheFlag,
|
||||
utils.CacheDatabaseFlag,
|
||||
utils.CacheTrieFlag,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
godebug "runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -243,6 +244,22 @@ var (
|
|||
Usage: "Comma separated block number-to-hash mappings to require for peering (<number>=<hash>)",
|
||||
Category: flags.EthCategory,
|
||||
}
|
||||
BALExecutionModeFlag = &cli.StringFlag{
|
||||
Name: "bal.executionmode",
|
||||
Usage: "EIP-7928 block-access-list execution mode (no-op placeholder)",
|
||||
Category: flags.EthCategory,
|
||||
}
|
||||
PrefetchWorkersFlag = &cli.UintFlag{
|
||||
Name: "bal.prefetchworkers",
|
||||
Usage: "The number of concurrent state loading tasks to perform when prefetching BAL state. Default to the number of cpus",
|
||||
Value: uint(runtime.NumCPU()),
|
||||
Category: flags.MiscCategory,
|
||||
}
|
||||
BlockingPrefetchFlag = &cli.BoolFlag{
|
||||
Name: "bal.blockingprefetch",
|
||||
Usage: "only relevant when executing in parallel with a BAL: if true, the prefetcher will block tx/state-root calculation until all scheduled fetching tasks have completed.",
|
||||
Category: flags.MiscCategory,
|
||||
}
|
||||
BloomFilterSizeFlag = &cli.Uint64Flag{
|
||||
Name: "bloomfilter.size",
|
||||
Usage: "Megabytes of memory allocated to bloom-filter for pruning",
|
||||
|
|
@ -1120,6 +1137,12 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server.
|
|||
}
|
||||
)
|
||||
|
||||
const (
|
||||
BalExecutionModeOptimized = "full"
|
||||
BalExecutionModeNoBatchIO = "nobatchio"
|
||||
BalExecutionModeSequential = "sequential"
|
||||
)
|
||||
|
||||
var (
|
||||
// TestnetFlags is the flag group of all built-in supported testnets.
|
||||
TestnetFlags = []cli.Flag{
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ package core
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"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"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
|
|
@ -143,9 +143,14 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type StateRootSource interface {
|
||||
IntermediateRoot(deleteEmptyObjects bool) common.Hash
|
||||
Error() error
|
||||
}
|
||||
|
||||
// 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, state StateRootSource, res *ProcessResult, stateless bool) error {
|
||||
if res == nil {
|
||||
return errors.New("nil ProcessResult value")
|
||||
}
|
||||
|
|
@ -201,8 +206,8 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
|
|||
}
|
||||
// Validate the state root against the received state root and throw
|
||||
// an error if they don't match.
|
||||
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
|
||||
return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error())
|
||||
if root := state.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
|
||||
return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, state.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"io"
|
||||
"math/big"
|
||||
"runtime"
|
||||
|
|
@ -225,6 +226,10 @@ type BlockChainConfig struct {
|
|||
// Execution configs
|
||||
StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose)
|
||||
EnableWitnessStats bool // Whether trie access statistics collection is enabled
|
||||
|
||||
BALExecutionMode bal.BALExecutionMode
|
||||
BlockingPrefetch bool
|
||||
PrefetchWorkers int
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default config.
|
||||
|
|
@ -365,12 +370,13 @@ type BlockChain struct {
|
|||
stopping atomic.Bool // false if chain is running, true when stopped
|
||||
procInterrupt atomic.Bool // interrupt signaler for block processing
|
||||
|
||||
engine consensus.Engine
|
||||
validator Validator // Block and state validator interface
|
||||
prefetcher Prefetcher
|
||||
processor Processor // Block transaction processor interface
|
||||
logger *tracing.Hooks
|
||||
stateSizer *state.SizeTracker // State size tracking
|
||||
engine consensus.Engine
|
||||
validator Validator // Block and state validator interface
|
||||
prefetcher Prefetcher
|
||||
processor Processor // Block transaction processor interface
|
||||
parallelProcessor ParallelStateProcessor // block processor for use with access lists
|
||||
logger *tracing.Hooks
|
||||
stateSizer *state.SizeTracker // State size tracking
|
||||
|
||||
lastForkReadyAlert time.Time // Last time there was a fork readiness print out
|
||||
slowBlockThreshold time.Duration // Block execution time threshold beyond which detailed statistics will be logged
|
||||
|
|
@ -433,6 +439,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
|
|||
bc.validator = NewBlockValidator(chainConfig, bc)
|
||||
bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc)
|
||||
bc.processor = NewStateProcessor(bc.hc)
|
||||
bc.parallelProcessor = *NewParallelStateProcessor(bc.hc, bc.GetVMConfig())
|
||||
|
||||
genesisHeader := bc.GetHeaderByNumber(0)
|
||||
if genesisHeader == nil {
|
||||
|
|
@ -1660,7 +1667,7 @@ func (bc *BlockChain) writeKnownBlock(block *types.Block) error {
|
|||
|
||||
// writeBlockWithState writes block, metadata and corresponding state data to the
|
||||
// database.
|
||||
func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb *state.StateDB) error {
|
||||
func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb state.Committer) error {
|
||||
if !bc.HasHeader(block.ParentHash(), block.NumberU64()-1) {
|
||||
return consensus.ErrUnknownAncestor
|
||||
}
|
||||
|
|
@ -1774,7 +1781,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
|
|||
|
||||
// writeBlockAndSetHead is the internal implementation of WriteBlockAndSetHead.
|
||||
// This function expects the chain mutex to be held.
|
||||
func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) {
|
||||
func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state state.Committer, emitHeadEvent bool) (status WriteStatus, err error) {
|
||||
if err := bc.writeBlockWithState(block, receipts, state); err != nil {
|
||||
return NonStatTy, err
|
||||
}
|
||||
|
|
@ -2129,16 +2136,136 @@ type ExecuteConfig struct {
|
|||
EnableWitnessStats bool
|
||||
}
|
||||
|
||||
func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block *types.Block, setHead bool) (procRes *blockProcessingResult, blockEndErr error) {
|
||||
var (
|
||||
startTime = time.Now()
|
||||
procTime time.Duration
|
||||
statedb *state.StateDB
|
||||
)
|
||||
|
||||
sdb := state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)
|
||||
|
||||
useAsyncReads := bc.cfg.BALExecutionMode != bal.BALExecutionNoBatchIO
|
||||
al := block.AccessList()
|
||||
// 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.NewAccessListReader(*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, prepared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statedb, err = state.NewWithReader(parentRoot, sdb, prefetchReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bc.logger != nil && bc.logger.OnBlockStart != nil {
|
||||
bc.logger.OnBlockStart(tracing.BlockEvent{
|
||||
Block: block,
|
||||
Finalized: bc.CurrentFinalBlock(),
|
||||
Safe: bc.CurrentSafeBlock(),
|
||||
})
|
||||
}
|
||||
if bc.logger != nil && bc.logger.OnBlockEnd != nil {
|
||||
defer func() {
|
||||
bc.logger.OnBlockEnd(blockEndErr)
|
||||
}()
|
||||
}
|
||||
|
||||
res, err := bc.parallelProcessor.Process(block, stateTransition, statedb, bc.cfg.VmConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bc.validator.ValidateState(block, stateTransition, res.ProcessResult, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
procTime = time.Since(startTime)
|
||||
writeStart := time.Now()
|
||||
// Write the block to the chain and get the status.
|
||||
var (
|
||||
//wstart = time.Now()
|
||||
status WriteStatus
|
||||
)
|
||||
if !setHead {
|
||||
// Don't set the head, only insert the block
|
||||
err = bc.writeBlockWithState(block, res.ProcessResult.Receipts, stateTransition)
|
||||
} else {
|
||||
status, err = bc.writeBlockAndSetHead(block, res.ProcessResult.Receipts, res.ProcessResult.Logs, stateTransition, false)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writeTime := time.Since(writeStart)
|
||||
var stats ExecuteStats
|
||||
|
||||
wc := stateTransition.WrittenCounts()
|
||||
d := stateTransition.Deletions()
|
||||
//codeLoaded, codeLoadBytes := prefetchReader.(state.CodeLoadTracker).CodeLoads()
|
||||
//stats.AccountLoaded = al.UniqueAccountCount()
|
||||
stats.AccountUpdated = wc.Accounts - d.Accounts
|
||||
stats.AccountDeleted = d.Accounts
|
||||
//stats.StorageLoaded = al.UniqueStorageSlotCount()
|
||||
stats.StorageUpdated = wc.StorageSlots - d.Storage
|
||||
stats.StorageDeleted = d.Storage
|
||||
//stats.CodeLoaded = codeLoaded
|
||||
//stats.CodeLoadBytes = codeLoadBytes
|
||||
stats.CodeUpdated = wc.Codes
|
||||
stats.CodeUpdateBytes = wc.CodeBytes
|
||||
|
||||
//stats.ExecWall = res.ExecTime
|
||||
//stats.PostProcess = res.PostProcessTime
|
||||
|
||||
if m := res.StateTransitionMetrics; m != nil {
|
||||
stats.AccountHashes = m.AccountUpdate + m.StateUpdate + m.StateHash
|
||||
stats.AccountCommits = m.AccountCommits
|
||||
stats.StorageCommits = m.StorageCommits
|
||||
stats.DatabaseCommit = m.TrieDBCommits
|
||||
//stats.Prefetch = m.StatePrefetch
|
||||
}
|
||||
//stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed
|
||||
|
||||
stats.StateReadCacheStats = prefetchReader.(state.ReaderStater).GetStats()
|
||||
|
||||
elapsed := time.Since(startTime) + 1 // prevent zero division
|
||||
stats.TotalTime = elapsed
|
||||
stats.MgasPerSecond = float64(res.ProcessResult.GasUsed) * 1000 / float64(elapsed)
|
||||
stats.BlockWrite = writeTime
|
||||
|
||||
// TODO: reinstate
|
||||
//stats.balTransitionStats = res.StateTransitionMetrics
|
||||
|
||||
return &blockProcessingResult{
|
||||
usedGas: res.ProcessResult.GasUsed,
|
||||
procTime: procTime,
|
||||
status: status,
|
||||
witness: nil,
|
||||
stats: &stats,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ProcessBlock executes and validates the given block. If there was no error
|
||||
// it writes the block and associated state to database.
|
||||
func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, block *types.Block, config ExecuteConfig) (result *blockProcessingResult, blockEndErr error) {
|
||||
var (
|
||||
err error
|
||||
startTime = time.Now()
|
||||
statedb *state.StateDB
|
||||
interrupt atomic.Bool
|
||||
sdb state.Database
|
||||
err error
|
||||
startTime = time.Now()
|
||||
statedb *state.StateDB
|
||||
interrupt atomic.Bool
|
||||
sdb state.Database
|
||||
blockHasAccessList = block.AccessList() != nil
|
||||
)
|
||||
|
||||
if blockHasAccessList && bc.cfg.BALExecutionMode != bal.BALExecutionSequential {
|
||||
return bc.processBlockWithAccessList(parentRoot, block, config.WriteHead)
|
||||
}
|
||||
defer interrupt.Store(true) // terminate the prefetch at the end
|
||||
|
||||
if bc.chainConfig.IsUBT(block.Number(), block.Time()) {
|
||||
|
|
|
|||
318
core/parallel_state_processor.go
Normal file
318
core/parallel_state_processor.go
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// ProcessResultWithMetrics wraps ProcessResult with timing breakdown for BAL block processing.
|
||||
type ProcessResultWithMetrics struct {
|
||||
ProcessResult *ProcessResult
|
||||
PreProcessTime time.Duration
|
||||
StateTransitionMetrics *state.BALStateTransitionMetrics
|
||||
ExecTime time.Duration
|
||||
PostProcessTime time.Duration
|
||||
}
|
||||
|
||||
// errResult wraps an error into a new ProcessResultWithMetrics instance
|
||||
func errResult(err error) *ProcessResultWithMetrics {
|
||||
return &ProcessResultWithMetrics{ProcessResult: &ProcessResult{Error: err}}
|
||||
}
|
||||
|
||||
// ParallelStateProcessor is used to execute and verify blocks containing
|
||||
// access lists.
|
||||
type ParallelStateProcessor struct {
|
||||
*StateProcessor
|
||||
vmCfg *vm.Config
|
||||
}
|
||||
|
||||
// NewParallelStateProcessor returns a new ParallelStateProcessor instance.
|
||||
func NewParallelStateProcessor(chain *HeaderChain, vmConfig *vm.Config) *ParallelStateProcessor {
|
||||
return &ParallelStateProcessor{
|
||||
StateProcessor: NewStateProcessor(chain),
|
||||
vmCfg: vmConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// execVMConfig returns the subset of the configured VM options that is safe to
|
||||
// reuse across the parallel per-transaction and post-transaction executions.
|
||||
// Only the fields explicitly copied here are propagated (mirroring the original
|
||||
// per-tx behaviour); notably the full caller-supplied config is used only for
|
||||
// pre-execution in processBlockPreTx.
|
||||
func (p *ParallelStateProcessor) execVMConfig() vm.Config {
|
||||
return vm.Config{
|
||||
NoBaseFee: p.vmCfg.NoBaseFee,
|
||||
EnablePreimageRecording: p.vmCfg.EnablePreimageRecording,
|
||||
ExtraEips: slices.Clone(p.vmCfg.ExtraEips),
|
||||
}
|
||||
}
|
||||
|
||||
// called by resultHandler when all transactions have successfully executed.
|
||||
// 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, accessList *bal.AccessListReader, statedb *state.StateDB, results []txExecResult) *ProcessResultWithMetrics {
|
||||
tExec := time.Since(tExecStart)
|
||||
tPostprocessStart := time.Now()
|
||||
header := block.Header()
|
||||
|
||||
// The post-execution changes are recorded at the BAL index immediately
|
||||
// following the last transaction.
|
||||
lastBALIdx := len(block.Transactions()) + 1
|
||||
postTxState := statedb.WithReader(state.NewReaderWithAccessList(statedb.Reader(), accessList, lastBALIdx))
|
||||
|
||||
evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), postTxState, p.chainConfig(), p.execVMConfig())
|
||||
|
||||
// 1. order the receipts by tx index
|
||||
// 2. correctly calculate the cumulative gas used per receipt, returning bad block error if it goes over the allowed
|
||||
slices.SortFunc(results, func(a, b txExecResult) int {
|
||||
return cmp.Compare(a.receipt.TransactionIndex, b.receipt.TransactionIndex)
|
||||
})
|
||||
|
||||
var (
|
||||
// Per-dimension cumulative sums for 2D block gas (EIP-8037).
|
||||
sumRegular uint64
|
||||
sumState uint64
|
||||
cumulativeReceipt uint64 // cumulative receipt gas (what users pay)
|
||||
|
||||
allLogs []*types.Log
|
||||
allReceipts []*types.Receipt
|
||||
)
|
||||
for _, result := range results {
|
||||
sumRegular += result.txRegular
|
||||
sumState += result.txState
|
||||
|
||||
cumulativeReceipt += result.execGas
|
||||
result.receipt.CumulativeGasUsed = cumulativeReceipt
|
||||
allLogs = append(allLogs, result.receipt.Logs...)
|
||||
allReceipts = append(allReceipts, result.receipt)
|
||||
}
|
||||
// Block gas = max(sum_regular, sum_state) per EIP-8037.
|
||||
blockGasUsed := max(sumRegular, sumState)
|
||||
if blockGasUsed > header.GasLimit {
|
||||
return errResult(fmt.Errorf("gas limit exceeded"))
|
||||
}
|
||||
|
||||
requests, postBAL, err := PostExecution(context.Background(), p.chainConfig(), block.Number(), block.Time(), allLogs, evm, uint32(lastBALIdx))
|
||||
if err != nil {
|
||||
return errResult(err)
|
||||
}
|
||||
|
||||
p.chain.Engine().Finalize(p.chain, block.Header(), evm.StateDB, block.Body(), uint32(lastBALIdx), postBAL)
|
||||
|
||||
blockAccessList := bal.NewConstructionBlockAccessList()
|
||||
blockAccessList.Merge(preTxBAL)
|
||||
blockAccessList.Merge(postBAL)
|
||||
for _, res := range results {
|
||||
blockAccessList.Merge(res.blockAccessList)
|
||||
}
|
||||
|
||||
// TODO: do we move validation to ValidateState?
|
||||
if block.AccessList().Hash() != blockAccessList.ToEncodingObj().Hash() {
|
||||
// TODO: expose json string method on encoding block access list and log it here
|
||||
return errResult(fmt.Errorf("invalid block access list: mismatch between local and remote block access list"))
|
||||
}
|
||||
|
||||
tPostprocess := time.Since(tPostprocessStart)
|
||||
|
||||
return &ProcessResultWithMetrics{
|
||||
ProcessResult: &ProcessResult{
|
||||
Receipts: allReceipts,
|
||||
Requests: requests,
|
||||
Logs: allLogs,
|
||||
GasUsed: blockGasUsed,
|
||||
Bal: blockAccessList,
|
||||
},
|
||||
PostProcessTime: tPostprocess,
|
||||
ExecTime: tExec,
|
||||
}
|
||||
}
|
||||
|
||||
type txExecResult struct {
|
||||
receipt *types.Receipt
|
||||
err error // non-EVM error which would render the block invalid
|
||||
execGas uint64 // gas reported on the receipt (what the user pays)
|
||||
|
||||
// Per-tx dimensional gas for Amsterdam 2D gas accounting (EIP-8037).
|
||||
txRegular uint64
|
||||
txState uint64
|
||||
|
||||
blockAccessList *bal.ConstructionBlockAccessList
|
||||
}
|
||||
|
||||
// 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, prepared *bal.AccessListReader, 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
|
||||
cumulativeStateGas uint64
|
||||
cumulativeRegularGas uint64
|
||||
execErr error
|
||||
)
|
||||
|
||||
if numTx := len(block.Transactions()); numTx > 0 {
|
||||
for completed := 0; completed < numTx; completed++ {
|
||||
res := <-txResCh
|
||||
if execErr != nil {
|
||||
// A block-invalidating result was already seen; keep draining so
|
||||
// the worker goroutines don't block on their sends.
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case res.err != nil:
|
||||
execErr = res.err
|
||||
default:
|
||||
bottleneck := max(cumulativeRegularGas+res.txRegular, cumulativeStateGas+res.txState)
|
||||
if bottleneck > block.GasLimit() {
|
||||
execErr = fmt.Errorf("block used too much gas in bottleneck dimension: %d. block gas limit is %d", bottleneck, block.GasLimit())
|
||||
continue
|
||||
}
|
||||
cumulativeRegularGas += res.txRegular
|
||||
cumulativeStateGas += res.txState
|
||||
results = append(results, res)
|
||||
}
|
||||
}
|
||||
|
||||
if execErr != nil {
|
||||
// Drain stateRootCalcResCh so the calcAndVerifyRoot goroutine can exit.
|
||||
<-stateRootCalcResCh
|
||||
resCh <- errResult(execErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
execResults := p.prepareExecResult(block, tExecStart, preTxBAL, prepared, statedb, results)
|
||||
rootCalcRes := <-stateRootCalcResCh
|
||||
|
||||
switch {
|
||||
case execResults.ProcessResult.Error != nil:
|
||||
resCh <- execResults
|
||||
case rootCalcRes.err != nil:
|
||||
resCh <- errResult(rootCalcRes.err)
|
||||
default:
|
||||
execResults.StateTransitionMetrics = rootCalcRes.metrics
|
||||
resCh <- execResults
|
||||
}
|
||||
}
|
||||
|
||||
type stateRootCalculationResult struct {
|
||||
err error
|
||||
metrics *state.BALStateTransitionMetrics
|
||||
}
|
||||
|
||||
// calcAndVerifyRoot performs the post-state root hash calculation, verifying
|
||||
// it against what is reported by the block and returning a result on resCh.
|
||||
func (p *ParallelStateProcessor) calcAndVerifyRoot(block *types.Block, stateTransition *state.BALStateTransition, resCh chan stateRootCalculationResult) {
|
||||
root := stateTransition.IntermediateRoot(false)
|
||||
|
||||
res := stateRootCalculationResult{
|
||||
metrics: stateTransition.Metrics(),
|
||||
}
|
||||
if root != block.Root() {
|
||||
res.err = fmt.Errorf("state root mismatch. local: %x. remote: %x", root, block.Root())
|
||||
}
|
||||
resCh <- res
|
||||
}
|
||||
|
||||
// execTx executes a single transaction returning a result which includes state accessed/modified.
|
||||
func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transaction, balIdx int, db *state.StateDB, signer types.Signer) *txExecResult {
|
||||
header := block.Header()
|
||||
evmContext := NewEVMBlockContext(header, p.chain, nil)
|
||||
evm := vm.NewEVM(evmContext, db, p.chainConfig(), p.execVMConfig())
|
||||
|
||||
msg, err := TransactionToMessage(tx, signer, header.BaseFee)
|
||||
if err != nil {
|
||||
return &txExecResult{err: fmt.Errorf("could not apply tx %d [%v]: %w", balIdx, tx.Hash().Hex(), err)}
|
||||
}
|
||||
sender, err := signer.Sender(tx)
|
||||
if err != nil {
|
||||
return &txExecResult{err: fmt.Errorf("could not recover sender for tx at bal idx %d: %w", balIdx, err)}
|
||||
}
|
||||
|
||||
gp := NewGasPool(block.GasLimit())
|
||||
// TODO: make precompiled addresses be resolvable from chain config + block
|
||||
db.Prepare(evm.GetRules(), sender, block.Coinbase(), tx.To(), vm.PrecompiledAddressesCancun, tx.AccessList())
|
||||
db.SetTxContext(tx.Hash(), balIdx-1, uint32(balIdx))
|
||||
|
||||
receipt, txBAL, err := ApplyTransactionWithEVM(msg, gp, db, block.Number(), block.Hash(), evmContext.Time, tx, evm)
|
||||
if err != nil {
|
||||
return &txExecResult{err: fmt.Errorf("could not apply tx %d [%v]: %w", balIdx, tx.Hash().Hex(), err)}
|
||||
}
|
||||
|
||||
return &txExecResult{
|
||||
receipt: receipt,
|
||||
execGas: receipt.GasUsed,
|
||||
txRegular: gp.cumulativeRegular,
|
||||
txState: gp.cumulativeState,
|
||||
blockAccessList: txBAL,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, cfg vm.Config) *bal.ConstructionBlockAccessList {
|
||||
header := block.Header()
|
||||
evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), statedb, p.chainConfig(), cfg)
|
||||
return PreExecution(context.Background(), block.BeaconRoot(), block.ParentHash(), p.chainConfig(), evm, block.Number(), block.Time())
|
||||
}
|
||||
|
||||
// Process performs EVM execution and state root computation for a block which is known
|
||||
// to contain an access list.
|
||||
func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *state.BALStateTransition, statedb *state.StateDB, cfg vm.Config) (*ProcessResultWithMetrics, error) {
|
||||
header := block.Header()
|
||||
signer := types.MakeSigner(p.chainConfig(), header.Number, header.Time)
|
||||
|
||||
var (
|
||||
resCh = make(chan *ProcessResultWithMetrics)
|
||||
rootCalcResultCh = make(chan stateRootCalculationResult)
|
||||
txResCh = make(chan txExecResult)
|
||||
)
|
||||
|
||||
// Pre-transaction processing: system-contract updates and the pre-tx BAL.
|
||||
pStart := time.Now()
|
||||
startingState := statedb.Copy()
|
||||
prepared := stateTransition.PreparedAccessList()
|
||||
preTxBAL := p.processBlockPreTx(block, statedb, cfg)
|
||||
tPreprocess := time.Since(pStart)
|
||||
|
||||
// Execute transactions and the state-root calculation in parallel.
|
||||
tExecStart := time.Now()
|
||||
go p.resultHandler(block, preTxBAL, prepared, statedb, tExecStart, txResCh, rootCalcResultCh, resCh)
|
||||
|
||||
// Workers execute transactions concurrently against per-tx state copies.
|
||||
// Each worker reports completion (and any block-invalidating error) on
|
||||
// txResCh, which resultHandler drains. Worker errors therefore flow through
|
||||
// the channel rather than the errgroup, so the group is used purely to bound
|
||||
// concurrency and Wait() is intentionally not called.
|
||||
var workers errgroup.Group
|
||||
workers.SetLimit(runtime.NumCPU())
|
||||
for i, tx := range block.Transactions() {
|
||||
balIdx := i + 1
|
||||
prestate := startingState.Copy()
|
||||
workers.Go(func() error {
|
||||
prestate = prestate.WithReader(state.NewReaderWithAccessList(statedb.Reader(), prepared, balIdx))
|
||||
txResCh <- *p.execTx(block, tx, balIdx, prestate, signer)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
go p.calcAndVerifyRoot(block, stateTransition, rootCalcResultCh)
|
||||
|
||||
res := <-resCh
|
||||
if res.ProcessResult.Error != nil {
|
||||
return nil, res.ProcessResult.Error
|
||||
}
|
||||
// TODO: remove preprocess metric ?
|
||||
res.PreProcessTime = tPreprocess
|
||||
return res, nil
|
||||
}
|
||||
537
core/state/bal_state_transition.go
Normal file
537
core/state/bal_state_transition.go
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||
"github.com/holiman/uint256"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// BALStateTransition is responsible for performing the state root update
|
||||
// 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
|
||||
written bal.WrittenCounts
|
||||
db Database
|
||||
reader Reader
|
||||
stateTrie Trie
|
||||
parentRoot common.Hash
|
||||
|
||||
// the computed state root of the block
|
||||
rootHash common.Hash
|
||||
// the state modifications performed by the block
|
||||
diffs bal.StateMutations
|
||||
|
||||
// a map of common.Address -> *types.StateAccount containing the block
|
||||
// prestate of all accounts that will be modified
|
||||
prestates sync.Map
|
||||
|
||||
postStates map[common.Address]*types.StateAccount
|
||||
// a map of common.Address -> Trie containing the account tries for all
|
||||
// accounts with mutated storage
|
||||
tries sync.Map //map[common.Address]Trie
|
||||
deletions map[common.Address]struct{}
|
||||
|
||||
// Deletion counters; not derivable from the BAL alone (selfdestruct vs
|
||||
// balance drain is indistinguishable without prestate).
|
||||
accountDeleted int
|
||||
storageDeleted atomic.Int64
|
||||
|
||||
stateUpdate *StateUpdate
|
||||
|
||||
metrics BALStateTransitionMetrics
|
||||
maxBALIdx int
|
||||
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics {
|
||||
return &s.metrics
|
||||
}
|
||||
|
||||
// DeletionCounts holds per-block deletion counters for accounts/storage
|
||||
type DeletionCounts struct {
|
||||
Accounts int
|
||||
Storage int
|
||||
}
|
||||
|
||||
func (s *BALStateTransition) Deletions() DeletionCounts {
|
||||
return DeletionCounts{
|
||||
Accounts: s.accountDeleted,
|
||||
Storage: int(s.storageDeleted.Load()),
|
||||
}
|
||||
}
|
||||
|
||||
type BALStateTransitionMetrics struct {
|
||||
// trie hashing metrics
|
||||
AccountUpdate time.Duration
|
||||
StatePrefetch time.Duration
|
||||
StateUpdate time.Duration
|
||||
StateHash time.Duration
|
||||
|
||||
// commit metrics
|
||||
AccountCommits time.Duration
|
||||
StorageCommits time.Duration
|
||||
SnapshotCommits time.Duration
|
||||
TrieDBCommits time.Duration
|
||||
TotalCommitTime time.Duration
|
||||
}
|
||||
|
||||
func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Database, parentRoot common.Hash, prepared *bal.AccessListReader) (*BALStateTransition, error) {
|
||||
stateTrie, err := db.OpenTrie(parentRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BALStateTransition{
|
||||
accessList: prepared,
|
||||
written: block.AccessList().WrittenCounts(),
|
||||
db: db,
|
||||
reader: prefetchReader,
|
||||
stateTrie: stateTrie,
|
||||
parentRoot: parentRoot,
|
||||
rootHash: common.Hash{},
|
||||
diffs: make(bal.StateMutations),
|
||||
prestates: sync.Map{},
|
||||
postStates: make(map[common.Address]*types.StateAccount),
|
||||
tries: sync.Map{},
|
||||
deletions: make(map[common.Address]struct{}),
|
||||
stateUpdate: nil,
|
||||
maxBALIdx: len(block.Transactions()) + 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WrittenCounts returns the cached BAL write counts (computed once per block).
|
||||
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.AccessListReader {
|
||||
return s.accessList
|
||||
}
|
||||
|
||||
func (s *BALStateTransition) Error() error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *BALStateTransition) setError(err error) {
|
||||
if s.err == nil {
|
||||
s.err = err
|
||||
}
|
||||
}
|
||||
|
||||
// isAccountDeleted checks whether the state account was deleted in this block. Post selfdestruct-removal,
|
||||
// deletions can only occur if an account which has a balance becomes the target of a CREATE2 initcode
|
||||
// which calls SENDALL, clearing the account and marking it for deletion.
|
||||
func isAccountDeleted(prestate *types.StateAccount, mutations bal.AccountMutations) bool {
|
||||
// TODO: figure out how to simplify this method
|
||||
if mutations.Code != nil && len(mutations.Code) != 0 {
|
||||
return false
|
||||
}
|
||||
if mutations.Nonce != nil && *mutations.Nonce != 0 {
|
||||
return false
|
||||
}
|
||||
if mutations.StorageWrites != nil && len(mutations.StorageWrites) > 0 {
|
||||
return false
|
||||
}
|
||||
if mutations.Balance != nil {
|
||||
if mutations.Balance.IsZero() {
|
||||
if prestate.Nonce != 0 || prestate.Balance.IsZero() || common.BytesToHash(prestate.CodeHash) != types.EmptyCodeHash {
|
||||
return false
|
||||
}
|
||||
// consider an empty account with storage to be deleted, so we don't check root here
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// updateAccount applies the block state mutations to a given account returning
|
||||
// the updated state account and new code (if the account code changed)
|
||||
func (s *BALStateTransition) updateAccount(addr common.Address) (*types.StateAccount, []byte) {
|
||||
a, _ := s.prestates.Load(addr)
|
||||
acct := a.(*types.StateAccount)
|
||||
|
||||
acct, diff := acct.Copy(), s.diffs[addr]
|
||||
code := diff.Code
|
||||
|
||||
if diff.Nonce != nil {
|
||||
acct.Nonce = *diff.Nonce
|
||||
}
|
||||
if diff.Balance != nil {
|
||||
acct.Balance = new(uint256.Int).Set(diff.Balance)
|
||||
}
|
||||
if tr, ok := s.tries.Load(addr); ok {
|
||||
acct.Root = tr.(Trie).Hash()
|
||||
}
|
||||
return acct, code
|
||||
}
|
||||
|
||||
func (s *BALStateTransition) commitAccount(addr common.Address) (*AccountUpdate, *trienode.NodeSet, error) {
|
||||
op := &AccountUpdate{
|
||||
Address: addr,
|
||||
Data: s.postStates[addr], // TODO: cache the updated state account somewhere
|
||||
}
|
||||
var prestate *types.StateAccount
|
||||
if ps, exist := s.prestates.Load(addr); exist {
|
||||
op.Origin = ps.(*types.StateAccount)
|
||||
}
|
||||
|
||||
if s.diffs[addr].Code != nil {
|
||||
code := ContractCode{
|
||||
Hash: crypto.Keccak256Hash(s.diffs[addr].Code),
|
||||
Blob: s.diffs[addr].Code,
|
||||
}
|
||||
if prestate == nil {
|
||||
code.OriginHash = types.EmptyCodeHash
|
||||
} else {
|
||||
code.OriginHash = common.BytesToHash(prestate.CodeHash)
|
||||
}
|
||||
op.Code = &code
|
||||
}
|
||||
|
||||
if len(s.diffs[addr].StorageWrites) == 0 {
|
||||
return op, nil, nil
|
||||
}
|
||||
|
||||
op.Storages = make(map[common.Hash]common.Hash)
|
||||
op.StoragesOriginByHash = make(map[common.Hash]common.Hash)
|
||||
op.StoragesOriginByKey = make(map[common.Hash]common.Hash)
|
||||
|
||||
for key, value := range s.diffs[addr].StorageWrites {
|
||||
hash := crypto.Keccak256Hash(key[:])
|
||||
op.Storages[hash] = value
|
||||
origin, err := s.reader.Storage(addr, key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
op.StoragesOriginByHash[hash] = origin
|
||||
op.StoragesOriginByKey[key] = origin
|
||||
}
|
||||
tr, _ := s.tries.Load(addr)
|
||||
root, nodes := tr.(Trie).Commit(false)
|
||||
s.postStates[addr].Root = root
|
||||
return op, nodes, nil
|
||||
}
|
||||
|
||||
// CommitWithUpdate flushes mutated trie nodes and state accounts to disk.
|
||||
func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *StateUpdate, error) {
|
||||
// 1) create a stateUpdate object
|
||||
// Commit objects to the trie, measuring the elapsed time
|
||||
var (
|
||||
//commitStart = time.Now()
|
||||
accountTrieNodesUpdated int
|
||||
accountTrieNodesDeleted int
|
||||
storageTrieNodesUpdated int
|
||||
storageTrieNodesDeleted int
|
||||
|
||||
lock sync.Mutex // protect two maps below
|
||||
nodes = trienode.NewMergedNodeSet() // aggregated trie nodes
|
||||
updates = make(map[common.Hash]*AccountUpdate, len(s.diffs)) // aggregated account updates
|
||||
|
||||
// merge aggregates the dirty trie nodes into the global set.
|
||||
//
|
||||
// Given that some accounts may be destroyed and then recreated within
|
||||
// the same block, it's possible that a node set with the same owner
|
||||
// may already exist. In such cases, these two sets are combined, with
|
||||
// the later one overwriting the previous one if any nodes are modified
|
||||
// or deleted in both sets.
|
||||
//
|
||||
// merge run concurrently across all the state objects and account trie.
|
||||
merge = func(set *trienode.NodeSet) error {
|
||||
if set == nil {
|
||||
return nil
|
||||
}
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
updates, deletes := set.Size()
|
||||
if set.Owner == (common.Hash{}) {
|
||||
accountTrieNodesUpdated += updates
|
||||
accountTrieNodesDeleted += deletes
|
||||
} else {
|
||||
storageTrieNodesUpdated += updates
|
||||
storageTrieNodesDeleted += deletes
|
||||
}
|
||||
return nodes.Merge(set)
|
||||
}
|
||||
)
|
||||
|
||||
destructedPrestates := make(map[common.Address]*types.StateAccount)
|
||||
s.prestates.Range(func(key, value any) bool {
|
||||
addr := key.(common.Address)
|
||||
acct := value.(*types.StateAccount)
|
||||
destructedPrestates[addr] = acct
|
||||
return true
|
||||
})
|
||||
|
||||
deletes, delNodes, err := handleDestruction(s.db, s.stateTrie, s.parentRoot, noStorageWiping, slices.Values(s.accessList.AllDestructions()), destructedPrestates)
|
||||
if err != nil {
|
||||
return common.Hash{}, nil, err
|
||||
}
|
||||
for _, set := range delNodes {
|
||||
if err := merge(set); err != nil {
|
||||
return common.Hash{}, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handle all state updates afterwards, concurrently to one another to shave
|
||||
// off some milliseconds from the commit operation. Also accumulate the code
|
||||
// writes to run in parallel with the computations.
|
||||
var (
|
||||
start = time.Now()
|
||||
root common.Hash
|
||||
workers errgroup.Group
|
||||
)
|
||||
// Schedule the account trie first since that will be the biggest, so give
|
||||
// it the most time to crunch.
|
||||
//
|
||||
// TODO(karalabe): This account trie commit is *very* heavy. 5-6ms at chain
|
||||
// heads, which seems excessive given that it doesn't do hashing, it just
|
||||
// shuffles some data. For comparison, the *hashing* at chain head is 2-3ms.
|
||||
// We need to investigate what's happening as it seems something's wonky.
|
||||
// Obviously it's not an end of the world issue, just something the original
|
||||
// code didn't anticipate for.
|
||||
workers.Go(func() error {
|
||||
// Write the account trie changes, measuring the amount of wasted time
|
||||
newroot, set := s.stateTrie.Commit(true)
|
||||
root = newroot
|
||||
|
||||
if err := merge(set); err != nil {
|
||||
return err
|
||||
}
|
||||
s.metrics.AccountCommits = time.Since(start)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Schedule each of the storage tries that need to be updated, so they can
|
||||
// run concurrently to one another.
|
||||
//
|
||||
// TODO(karalabe): Experimentally, the account commit takes approximately the
|
||||
// same time as all the storage commits combined, so we could maybe only have
|
||||
// 2 threads in total. But that kind of depends on the account commit being
|
||||
// more expensive than it should be, so let's fix that and revisit this todo.
|
||||
for addr, _ := range s.diffs {
|
||||
if _, isDeleted := s.deletions[addr]; isDeleted {
|
||||
continue
|
||||
}
|
||||
|
||||
address := addr
|
||||
// Run the storage updates concurrently to one another
|
||||
workers.Go(func() error {
|
||||
// Write any storage changes in the state object to its storage trie
|
||||
update, set, err := s.commitAccount(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := merge(set); err != nil {
|
||||
return err
|
||||
}
|
||||
lock.Lock()
|
||||
updates[crypto.Keccak256Hash(address[:])] = update
|
||||
s.metrics.StorageCommits = time.Since(start) // overwrite with the longest storage commit runtime
|
||||
lock.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
// Wait for everything to finish and update the metrics
|
||||
if err := workers.Wait(); err != nil {
|
||||
return common.Hash{}, nil, err
|
||||
}
|
||||
|
||||
storageDeleted := s.storageDeleted.Load()
|
||||
accountUpdatedMeter.Mark(int64(s.written.Accounts - s.accountDeleted))
|
||||
storageUpdatedMeter.Mark(int64(s.written.StorageSlots) - storageDeleted)
|
||||
accountDeletedMeter.Mark(int64(s.accountDeleted))
|
||||
storageDeletedMeter.Mark(storageDeleted)
|
||||
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
|
||||
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
|
||||
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
|
||||
storageTriesDeletedMeter.Mark(int64(storageTrieNodesDeleted))
|
||||
|
||||
storageKeyType := StorageKeyHashed
|
||||
if noStorageWiping {
|
||||
storageKeyType = StorageKeyPlain
|
||||
}
|
||||
update := NewStateUpdate(storageKeyType, s.parentRoot, root, block, deletes, updates, nodes)
|
||||
|
||||
if err := s.db.Commit(update); err != nil {
|
||||
return common.Hash{}, nil, err
|
||||
}
|
||||
// TODO: fix the following metrics:
|
||||
/*
|
||||
snapshotCommits, trieDBCommits, err := flushStateUpdate(s.db, block, ret)
|
||||
if err != nil {
|
||||
return common.Hash{}, nil, err
|
||||
}
|
||||
|
||||
s.metrics.SnapshotCommits, s.metrics.TrieDBCommits = snapshotCommits, trieDBCommits
|
||||
s.metrics.TotalCommitTime = time.Since(commitStart)
|
||||
*/
|
||||
return root, update, nil
|
||||
}
|
||||
|
||||
func (s *BALStateTransition) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, error) {
|
||||
hash, _, err := s.CommitWithUpdate(block, deleteEmptyObjects, noStorageWiping)
|
||||
return hash, err
|
||||
}
|
||||
|
||||
// IntermediateRoot applies block state mutations and computes the updated state
|
||||
// trie root.
|
||||
func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash {
|
||||
if s.rootHash != (common.Hash{}) {
|
||||
return s.rootHash
|
||||
}
|
||||
|
||||
// State root calculation proceeds as follows:
|
||||
|
||||
// 1 (a): load the origin storage values for all slots which were modified during the block (this is needed for computing the stateUpdate)
|
||||
// 1 (b): update each mutated account, producing the post-block state object by applying the state mutations to the prestate (retrieved in 1a).
|
||||
// 1 (c): prefetch the intermediate trie nodes of the mutated state set from the account trie.
|
||||
//
|
||||
// 2: compute the post-state root of the account trie
|
||||
//
|
||||
// Steps 1/2 are performed sequentially, with steps 1a-d performed in parallel
|
||||
|
||||
start := time.Now()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
s.diffs = *s.accessList.Mutations(s.maxBALIdx + 1)
|
||||
|
||||
for addr, d := range s.diffs {
|
||||
wg.Add(1)
|
||||
address := addr
|
||||
diff := d
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// 1 (b): update each mutated account, producing the post-block state object by applying the state mutations to the prestate (retrieved in 1a).
|
||||
acct, err := s.reader.Account(address)
|
||||
if err != nil {
|
||||
s.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if acct == nil {
|
||||
acct = types.NewEmptyStateAccount()
|
||||
}
|
||||
s.prestates.Store(address, acct)
|
||||
|
||||
if len(diff.StorageWrites) > 0 {
|
||||
tr, err := s.db.OpenStorageTrie(s.parentRoot, address, acct.Root, s.stateTrie)
|
||||
if err != nil {
|
||||
s.setError(err)
|
||||
return
|
||||
}
|
||||
s.tries.Store(address, tr)
|
||||
|
||||
var (
|
||||
updateKeys, updateValues [][]byte
|
||||
deleteKeys [][]byte
|
||||
)
|
||||
for key, val := range diff.StorageWrites {
|
||||
if val != (common.Hash{}) {
|
||||
updateKeys = append(updateKeys, key[:])
|
||||
updateValues = append(updateValues, common.TrimLeftZeroes(val[:]))
|
||||
} else {
|
||||
deleteKeys = append(deleteKeys, key[:])
|
||||
}
|
||||
}
|
||||
|
||||
if err := tr.UpdateStorageBatch(address, updateKeys, updateValues); err != nil {
|
||||
s.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, key := range deleteKeys {
|
||||
if err := tr.DeleteStorage(address, key); err != nil {
|
||||
s.setError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hashStart := time.Now()
|
||||
tr.Hash()
|
||||
s.metrics.StateHash = time.Since(hashStart)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
// 1 (c): prefetch the intermediate trie nodes of the mutated state set from the account trie.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
prefetchStart := time.Now()
|
||||
var prefetchAddrs []common.Address
|
||||
for addr, _ := range s.diffs {
|
||||
prefetchAddrs = append(prefetchAddrs, addr)
|
||||
}
|
||||
if err := s.stateTrie.PrefetchAccount(prefetchAddrs); err != nil {
|
||||
s.setError(err)
|
||||
return
|
||||
}
|
||||
s.metrics.StatePrefetch = time.Since(prefetchStart)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
s.metrics.AccountUpdate = time.Since(start)
|
||||
|
||||
// 2: compute the post-state root of the account trie
|
||||
stateUpdateStart := time.Now()
|
||||
for mutatedAddr, _ := range s.diffs {
|
||||
p, _ := s.prestates.Load(mutatedAddr)
|
||||
prestate := p.(*types.StateAccount)
|
||||
|
||||
isDeleted := isAccountDeleted(prestate, s.diffs[mutatedAddr])
|
||||
if isDeleted {
|
||||
if err := s.stateTrie.DeleteAccount(mutatedAddr); err != nil {
|
||||
s.setError(err)
|
||||
return common.Hash{}
|
||||
}
|
||||
s.deletions[mutatedAddr] = struct{}{}
|
||||
s.accountDeleted++
|
||||
} else {
|
||||
acct, code := s.updateAccount(mutatedAddr)
|
||||
|
||||
if code != nil {
|
||||
codeHash := crypto.Keccak256Hash(code)
|
||||
acct.CodeHash = codeHash.Bytes()
|
||||
if err := s.stateTrie.UpdateContractCode(mutatedAddr, codeHash, code); err != nil {
|
||||
s.setError(err)
|
||||
return common.Hash{}
|
||||
}
|
||||
}
|
||||
if err := s.stateTrie.UpdateAccount(mutatedAddr, acct, len(code)); err != nil {
|
||||
s.setError(err)
|
||||
return common.Hash{}
|
||||
}
|
||||
s.postStates[mutatedAddr] = acct
|
||||
}
|
||||
}
|
||||
|
||||
s.metrics.StateUpdate = time.Since(stateUpdateStart)
|
||||
|
||||
stateTrieHashStart := time.Now()
|
||||
s.rootHash = s.stateTrie.Hash()
|
||||
s.metrics.StateHash = time.Since(stateTrieHashStart)
|
||||
return s.rootHash
|
||||
}
|
||||
|
||||
func (s *BALStateTransition) Preimages() map[common.Hash][]byte {
|
||||
// TODO: implement this
|
||||
return make(map[common.Hash][]byte)
|
||||
}
|
||||
|
|
@ -54,6 +54,10 @@ type Database interface {
|
|||
// Reader returns a state reader associated with the specified state root.
|
||||
Reader(root common.Hash) (Reader, error)
|
||||
|
||||
// ReaderWithPrefetch returns a reader which asynchronously fetches block
|
||||
// access list state in the background.
|
||||
ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error)
|
||||
|
||||
// Iteratee returns a state iteratee associated with the specified state root,
|
||||
// through which the account iterator and storage iterator can be created.
|
||||
Iteratee(root common.Hash) (Iteratee, error)
|
||||
|
|
@ -107,12 +111,18 @@ type Trie interface {
|
|||
// in the trie with provided address.
|
||||
UpdateAccount(address common.Address, account *types.StateAccount, codeLen int) error
|
||||
|
||||
// UpdateAccountBatch attempts to update a list accounts in the batch manner.
|
||||
UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLengths []int) error
|
||||
|
||||
// UpdateStorage associates key with value in the trie. If value has length zero,
|
||||
// any existing value is deleted from the trie. The value bytes must not be modified
|
||||
// by the caller while they are stored in the trie. If a node was not found in the
|
||||
// database, a trie.MissingNodeError is returned.
|
||||
UpdateStorage(addr common.Address, key, value []byte) error
|
||||
|
||||
// UpdateStorageBatch attempts to update a list storages in the batch manner.
|
||||
UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error
|
||||
|
||||
// DeleteAccount abstracts an account deletion from the trie.
|
||||
DeleteAccount(address common.Address) error
|
||||
|
||||
|
|
|
|||
|
|
@ -223,6 +223,10 @@ type HistoricDB struct {
|
|||
codedb *CodeDB
|
||||
}
|
||||
|
||||
func (db *HistoricDB) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// Type returns the trie type of the underlying database.
|
||||
func (db *HistoricDB) Type() DatabaseType {
|
||||
// TODO(rjl493456442) support UBT in the future
|
||||
|
|
|
|||
|
|
@ -185,3 +185,22 @@ func (db *MPTDatabase) Commit(update *StateUpdate) error {
|
|||
func (db *MPTDatabase) Iteratee(root common.Hash) (Iteratee, error) {
|
||||
return newStateIteratee(true, root, db.triedb, db.snap)
|
||||
}
|
||||
|
||||
func (db *MPTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) {
|
||||
base, err := db.StateReader(stateRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Construct the state reader with native cache and associated statistics
|
||||
r := newStateReaderWithStats(newStateReaderWithCache(base))
|
||||
|
||||
// Construct the state reader with background prefetching
|
||||
pr := newPrefetchStateReader(r, accessList, threads)
|
||||
if block {
|
||||
if err := pr.Wait(); err != nil {
|
||||
panic("this should unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
return newReaderWithPrefetch(db.codedb.Reader(), pr, pr), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,10 @@ func (db *UBTDatabase) Reader(stateRoot common.Hash) (Reader, error) {
|
|||
return newReader(db.codedb.Reader(), sr), nil
|
||||
}
|
||||
|
||||
func (db *UBTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// ReadersWithCacheStats creates a pair of state readers that share the same
|
||||
// underlying state reader and internal state cache, while maintaining separate
|
||||
// statistics respectively.
|
||||
|
|
|
|||
|
|
@ -560,6 +560,7 @@ func (r *stateReaderWithStats) GetStateStats() StateReaderStats {
|
|||
type reader struct {
|
||||
ContractCodeReader
|
||||
StateReader
|
||||
PrefetcherMetricer
|
||||
}
|
||||
|
||||
// newReader constructs a reader with the supplied code reader and state reader.
|
||||
|
|
@ -570,6 +571,14 @@ func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader {
|
|||
}
|
||||
}
|
||||
|
||||
func newReaderWithPrefetch(codeReader ContractCodeReader, stateReader StateReader, metricer PrefetcherMetricer) *reader {
|
||||
return &reader{
|
||||
ContractCodeReader: codeReader,
|
||||
StateReader: stateReader,
|
||||
PrefetcherMetricer: metricer,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCodeStats returns the statistics of code access.
|
||||
func (r *reader) GetCodeStats() ContractCodeReaderStats {
|
||||
if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok {
|
||||
|
|
|
|||
|
|
@ -16,14 +16,6 @@
|
|||
|
||||
package state
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
)
|
||||
|
||||
// The EIP27928 reader utilizes a hierarchical architecture to optimize state
|
||||
// access during block execution:
|
||||
//
|
||||
|
|
@ -39,15 +31,13 @@ import (
|
|||
// This layer provides a "unified view" by merging the pre-transition state
|
||||
// with mutated states from preceding transactions in the block.
|
||||
//
|
||||
// - Tracking Layer: Finally, the readerTracker wraps the execution reader to
|
||||
// capture all state reads made during a specific transaction. These individual
|
||||
// reads are subsequently merged to construct a comprehensive access list
|
||||
// for the entire block.
|
||||
//
|
||||
// The architecture can be illustrated by the diagram below:
|
||||
//
|
||||
|
||||
// [ Block Level Access List ] <────────────────┐
|
||||
// ▲ │ (Merge)
|
||||
// │ │
|
||||
// ┌──────────────┴──────────────┐ ┌──────────────┴──────────────┐
|
||||
// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │
|
||||
// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │ (Unified View)
|
||||
// │ (Pre-state + Mutations) │ │ (Pre-state + Mutations) │
|
||||
// └──────────────┬──────────────┘ └──────────────┬──────────────┘
|
||||
// │ │
|
||||
|
|
@ -63,11 +53,16 @@ import (
|
|||
// │ (State & Contract Code) │
|
||||
// └─────────────────────────────┘
|
||||
|
||||
// Note: The block producer, which is responsible for generating the block
|
||||
// along with the block-level access list, does not maintain the internal
|
||||
// hierarchy (e.g., PrefetchStateReader or ReaderWithBlockLevelAL).
|
||||
// Instead, it directly utilizes the readerTracker, wrapped around the
|
||||
// base reader, to construct the access list.
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
)
|
||||
|
||||
type fetchTask struct {
|
||||
addr common.Address
|
||||
|
|
@ -78,16 +73,27 @@ func (t *fetchTask) weight() int { return 1 + len(t.slots) }
|
|||
|
||||
type prefetchStateReader struct {
|
||||
StateReader
|
||||
|
||||
tasks []*fetchTask
|
||||
nThreads int
|
||||
done chan struct{}
|
||||
term chan struct{}
|
||||
closeOnce sync.Once
|
||||
start time.Time
|
||||
metrics PrefetchMetrics
|
||||
}
|
||||
|
||||
// nolint:unused
|
||||
func newPrefetchStateReader(reader StateReader, accessList map[common.Address][]common.Hash, nThreads int) *prefetchStateReader {
|
||||
type PrefetchMetrics struct {
|
||||
// the total amount of time it took to complete the scheduled workload
|
||||
Elapsed time.Duration
|
||||
}
|
||||
|
||||
// PrefetcherMetricer is an object that can expose metrics related to the state
|
||||
// prefetching.
|
||||
type PrefetcherMetricer interface {
|
||||
Metrics() PrefetchMetrics
|
||||
}
|
||||
|
||||
func newPrefetchStateReader(reader StateReader, accessList bal.StorageKeys, nThreads int) *prefetchStateReader {
|
||||
tasks := make([]*fetchTask, 0, len(accessList))
|
||||
for addr, slots := range accessList {
|
||||
tasks = append(tasks, &fetchTask{
|
||||
|
|
@ -105,11 +111,16 @@ func newPrefetchStateReaderInternal(reader StateReader, tasks []*fetchTask, nThr
|
|||
nThreads: nThreads,
|
||||
done: make(chan struct{}),
|
||||
term: make(chan struct{}),
|
||||
start: time.Now(),
|
||||
}
|
||||
go r.prefetch()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *prefetchStateReader) Metrics() PrefetchMetrics {
|
||||
return r.metrics
|
||||
}
|
||||
|
||||
func (r *prefetchStateReader) Close() {
|
||||
r.closeOnce.Do(func() {
|
||||
close(r.term)
|
||||
|
|
@ -127,7 +138,10 @@ func (r *prefetchStateReader) Wait() error {
|
|||
}
|
||||
|
||||
func (r *prefetchStateReader) prefetch() {
|
||||
defer close(r.done)
|
||||
defer func() {
|
||||
r.metrics = PrefetchMetrics{time.Since(r.start)}
|
||||
close(r.done)
|
||||
}()
|
||||
|
||||
if len(r.tasks) == 0 {
|
||||
return
|
||||
|
|
@ -196,52 +210,104 @@ 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.AccessListReader: constructing one is O(1) and every lookup is an
|
||||
// allocation-free binary search.
|
||||
type ReaderWithBlockLevelAccessList struct {
|
||||
Reader
|
||||
AccessList *bal.ConstructionBlockAccessList
|
||||
TxIndex int
|
||||
prepared *bal.AccessListReader
|
||||
TxIndex int
|
||||
}
|
||||
|
||||
// NewReaderWithBlockLevelAccessList constructs a reader for accessing states
|
||||
// with the mutations made by transactions prior to txIndex.
|
||||
//
|
||||
// The txIndex refers to the call frame as such:
|
||||
// - 0 for pre‑execution system contract calls.
|
||||
// - 1 … n for transactions (in block order).
|
||||
// - n + 1 for post‑execution system contract calls.
|
||||
func NewReaderWithBlockLevelAccessList(base Reader, accessList *bal.ConstructionBlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList {
|
||||
// NewReaderWithAccessList 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 NewReaderWithAccessList(base Reader, prepared *bal.AccessListReader, txIndex int) *ReaderWithBlockLevelAccessList {
|
||||
return &ReaderWithBlockLevelAccessList{
|
||||
Reader: base,
|
||||
AccessList: 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 NewReaderWithAccessList when the
|
||||
// prepared list can be built once and shared across multiple readers.
|
||||
func NewReaderWithBlockLevelAccessList(base Reader, accessList bal.BlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList {
|
||||
return NewReaderWithAccessList(base, bal.NewAccessListReader(accessList), txIndex)
|
||||
}
|
||||
|
||||
// Account implements Reader, returning the account with the specific address.
|
||||
func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (*types.StateAccount, error) {
|
||||
panic("implement me")
|
||||
func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (acct *types.StateAccount, err error) {
|
||||
acct, err = r.Reader.Account(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
acct = types.NewEmptyStateAccount()
|
||||
} else {
|
||||
// the account returned by the underlying reader is a reference
|
||||
// copy it to avoid mutating the reader's instance
|
||||
acct = acct.Copy()
|
||||
}
|
||||
|
||||
// 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 code != nil {
|
||||
codeHash := crypto.Keccak256Hash(code)
|
||||
acct.CodeHash = codeHash[:]
|
||||
}
|
||||
if hasNonce {
|
||||
acct.Nonce = nonce
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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) {
|
||||
panic("implement me")
|
||||
if val, ok := r.prepared.StorageAt(addr, slot, r.TxIndex); ok {
|
||||
return val, nil
|
||||
}
|
||||
return r.Reader.Storage(addr, slot)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
panic("implement me")
|
||||
if code := r.prepared.Code(addr, r.TxIndex); code != nil {
|
||||
return crypto.Keccak256Hash(code) == codeHash
|
||||
}
|
||||
return r.Reader.Has(addr, codeHash)
|
||||
}
|
||||
|
||||
// Code implements Reader, returning the contract code with specified address
|
||||
// and hash.
|
||||
func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) ([]byte, error) {
|
||||
panic("implement me")
|
||||
func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) []byte {
|
||||
if code := r.prepared.Code(addr, r.TxIndex); code != nil && crypto.Keccak256Hash(code) == codeHash {
|
||||
return code
|
||||
}
|
||||
return r.Reader.Code(addr, codeHash)
|
||||
}
|
||||
|
||||
// CodeSize implements Reader, returning the contract code size with specified
|
||||
// address and hash.
|
||||
func (r *ReaderWithBlockLevelAccessList) CodeSize(addr common.Address, codeHash common.Hash) (int, error) {
|
||||
panic("implement me")
|
||||
func (r *ReaderWithBlockLevelAccessList) CodeSize(addr common.Address, codeHash common.Hash) int {
|
||||
if code := r.prepared.Code(addr, r.TxIndex); code != nil && crypto.Keccak256Hash(code) == codeHash {
|
||||
return len(code)
|
||||
}
|
||||
return r.Reader.CodeSize(addr, codeHash)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
|
|
@ -182,6 +183,13 @@ func New(root common.Hash, db Database) (*StateDB, error) {
|
|||
return NewWithReader(root, db, reader)
|
||||
}
|
||||
|
||||
// WithReader returns a copy of the statedb instance with the specified reader.
|
||||
func (s *StateDB) WithReader(reader Reader) *StateDB {
|
||||
cpy := s.Copy()
|
||||
cpy.reader = reader
|
||||
return cpy
|
||||
}
|
||||
|
||||
// NewWithReader creates a new state for the specified state root. Unlike New,
|
||||
// this function accepts an additional Reader which is bound to the given root.
|
||||
func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, error) {
|
||||
|
|
@ -1110,12 +1118,15 @@ func (s *StateDB) clearJournalAndRefund() {
|
|||
|
||||
// deleteStorage is designed to delete the storage trie of a designated account.
|
||||
func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) {
|
||||
return deleteStorage(s.db, s.originalRoot, addrHash, root)
|
||||
}
|
||||
func deleteStorage(db Database, originalRoot common.Hash, addrHash common.Hash, root common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) {
|
||||
var (
|
||||
nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
|
||||
storages = make(map[common.Hash]common.Hash) // the set for storage mutations (value is nil)
|
||||
storageOrigins = make(map[common.Hash]common.Hash) // the set for tracking the original value of slot
|
||||
)
|
||||
iteratee, err := s.db.Iteratee(s.originalRoot)
|
||||
iteratee, err := db.Iteratee(originalRoot)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
|
@ -1544,3 +1555,72 @@ func (s *StateDB) Witness() *stateless.Witness {
|
|||
func (s *StateDB) AccessEvents() *AccessEvents {
|
||||
return s.accessEvents
|
||||
}
|
||||
|
||||
// handleDestruction processes all destruction markers and deletes the account
|
||||
// and associated storage slots if necessary. There are four potential scenarios
|
||||
// as following:
|
||||
//
|
||||
// (a) the account was not existent and be marked as destructed
|
||||
// (b) the account was not existent and be marked as destructed,
|
||||
// however, it's resurrected later in the same block.
|
||||
// (c) the account was existent and be marked as destructed
|
||||
// (d) the account was existent and be marked as destructed,
|
||||
// however it's resurrected later in the same block.
|
||||
//
|
||||
// In case (a), nothing needs be deleted, nil to nil transition can be ignored.
|
||||
// In case (b), nothing needs be deleted, nil is used as the original value for
|
||||
// newly created account and storages
|
||||
// In case (c), **original** account along with its storages should be deleted,
|
||||
// with their values be tracked as original value.
|
||||
// In case (d), **original** account along with its storages should be deleted,
|
||||
// with their values be tracked as original value.
|
||||
func handleDestruction(db Database, trie Trie, root common.Hash, noStorageWiping bool, destructions iter.Seq[common.Address], prestates map[common.Address]*types.StateAccount) (map[common.Hash]*AccountDelete, []*trienode.NodeSet, error) {
|
||||
var (
|
||||
nodes []*trienode.NodeSet
|
||||
deletes = make(map[common.Hash]*AccountDelete)
|
||||
)
|
||||
for addr := range destructions {
|
||||
prestate := prestates[addr]
|
||||
// The account was non-existent, and it's marked as destructed in the scope
|
||||
// of block. It can be either case (a) or (b) and will be interpreted as
|
||||
// null->null state transition.
|
||||
// - for (a), skip it without doing anything
|
||||
// - for (b), the resurrected account with nil as original will be handled afterwards
|
||||
if prestate == nil {
|
||||
continue
|
||||
}
|
||||
// The account was existent, it can be either case (c) or (d).
|
||||
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
||||
op := &AccountDelete{
|
||||
Address: addr,
|
||||
Origin: prestate,
|
||||
}
|
||||
deletes[addrHash] = op
|
||||
|
||||
// Short circuit if the origin storage was empty.
|
||||
if prestate.Root == types.EmptyRootHash || db.TrieDB().IsUBT() {
|
||||
continue
|
||||
}
|
||||
if noStorageWiping {
|
||||
return nil, nil, fmt.Errorf("unexpected storage wiping, %x", addr)
|
||||
}
|
||||
// Remove storage slots belonging to the account.
|
||||
storages, storagesOrigin, set, err := deleteStorage(db, prestate.Root, addrHash, root)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err)
|
||||
}
|
||||
op.Storages = storages
|
||||
op.StoragesOrigin = storagesOrigin
|
||||
|
||||
// Aggregate the associated trie node changes.
|
||||
nodes = append(nodes, set)
|
||||
}
|
||||
return deletes, nodes, nil
|
||||
}
|
||||
|
||||
// TODO: find better location for this
|
||||
type Committer interface {
|
||||
Commit(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, error)
|
||||
CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *StateUpdate, error)
|
||||
Preimages() map[common.Hash][]byte
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,8 +151,11 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
|
|||
return gas, nil
|
||||
}
|
||||
|
||||
// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623).
|
||||
func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) {
|
||||
// FloorDataGas computes the minimum gas required for a transaction based on its
|
||||
// data tokens (EIP-7623). On Amsterdam it also includes the EIP-8131 per-auth
|
||||
// tx-content floor and the EIP-8279 per-auth Block Access List floor, which
|
||||
// together form the static floor seed extended at runtime by EIP-8279.
|
||||
func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList, numAuths uint64) (uint64, error) {
|
||||
var (
|
||||
tokens uint64
|
||||
tokenCost uint64
|
||||
|
|
@ -203,7 +206,23 @@ func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList)
|
|||
return 0, ErrGasUintOverflow
|
||||
}
|
||||
// Minimum gas required for a transaction based on its data tokens (EIP-7623).
|
||||
return params.TxGas + tokens*tokenCost, nil
|
||||
floor := params.TxGas + tokens*tokenCost
|
||||
|
||||
// EIP-8131 / EIP-8279: each EIP-7702 authorization contributes a static
|
||||
// per-auth floor. EIP-8131 prices the 101-byte authorization tuple
|
||||
// (FloorCostPerAuth) and EIP-8279 adds the worst-case BAL bytes the auth
|
||||
// writes when applied (BALBytesPerAuthorization at FloorGasPerByte). The
|
||||
// per-auth BAL term is folded into the static floor because set_delegation
|
||||
// runs outside the EVM's out-of-gas handler and cannot extend the floor at
|
||||
// runtime.
|
||||
if rules.IsAmsterdam && numAuths > 0 {
|
||||
const perAuth = params.FloorCostPerAuth + params.BALBytesPerAuthorization*params.FloorGasPerByte
|
||||
if (math.MaxUint64-floor)/perAuth < numAuths {
|
||||
return 0, ErrGasUintOverflow
|
||||
}
|
||||
floor += numAuths * perAuth
|
||||
}
|
||||
return floor, nil
|
||||
}
|
||||
|
||||
// toWordSize returns the ceiled word size required for init code payment calculation.
|
||||
|
|
@ -349,11 +368,17 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err
|
|||
// 5. Run Script section
|
||||
// 6. Derive new state root
|
||||
type stateTransition struct {
|
||||
gp *GasPool
|
||||
msg *Message
|
||||
gasRemaining vm.GasBudget
|
||||
state vm.StateDB
|
||||
evm *vm.EVM
|
||||
gp *GasPool
|
||||
msg *Message
|
||||
gasRemaining vm.GasBudget
|
||||
initReservoir uint64 // initial state-gas reservoir carved out of GasLimit (EIP-8037)
|
||||
state vm.StateDB
|
||||
evm *vm.EVM
|
||||
|
||||
// floorGas is the EIP-8279 Block Access List floor accumulator, seeded with
|
||||
// the static floor and extended at runtime via the EVM. It is nil before
|
||||
// Amsterdam. settleGas reads its final value to apply the receipt floor.
|
||||
floorGas *vm.FloorGasAccumulator
|
||||
}
|
||||
|
||||
// newStateTransition initialises and returns a new state transition object.
|
||||
|
|
@ -392,7 +417,7 @@ func (st *stateTransition) to() common.Address {
|
|||
// - Amsterdam+ (EIP-8037): two-dimensional budget. Regular gas is
|
||||
// capped at `MaxTxGas` (EIP-7825, 16_777_216); any excess from
|
||||
// `msg.GasLimit` above that cap becomes the state-gas reservoir.
|
||||
func (st *stateTransition) buyGas() error {
|
||||
func (st *stateTransition) buyGas(intrinsic vm.GasCosts) error {
|
||||
mgval := new(uint256.Int).SetUint64(st.msg.GasLimit)
|
||||
_, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice)
|
||||
if overflow {
|
||||
|
|
@ -446,10 +471,20 @@ func (st *stateTransition) buyGas() error {
|
|||
}
|
||||
isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time)
|
||||
|
||||
// Reserve the gas budget in the block gas pool
|
||||
// Reserve the gas budget in the block gas pool. This block-inclusion check
|
||||
// must run before the sender's balance is debited below, so it cannot be
|
||||
// deferred past buyGas.
|
||||
var err error
|
||||
if isAmsterdam {
|
||||
err = st.gp.CheckGasAmsterdam(min(st.msg.GasLimit, params.MaxTxGas), st.msg.GasLimit)
|
||||
// EIP-8037 per-tx 2D block-inclusion check (fork.py): the worst-case
|
||||
// regular contribution is min(MaxTxGas, tx.gas - intrinsic.state) and
|
||||
// the worst-case state contribution is tx.gas - intrinsic.regular.
|
||||
// Each dimension subtracts the other's intrinsic counterpart. The
|
||||
// intrinsic gas is computed once by execute() and passed in, so it is
|
||||
// shared with the charge below rather than recomputed.
|
||||
regularReservation := min(st.msg.GasLimit-min(st.msg.GasLimit, intrinsic.StateGas), params.MaxTxGas)
|
||||
stateReservation := st.msg.GasLimit - min(st.msg.GasLimit, intrinsic.RegularGas)
|
||||
err = st.gp.CheckGasAmsterdam(regularReservation, stateReservation)
|
||||
} else {
|
||||
err = st.gp.CheckGasLegacy(st.msg.GasLimit)
|
||||
}
|
||||
|
|
@ -462,7 +497,8 @@ func (st *stateTransition) buyGas() error {
|
|||
if isAmsterdam {
|
||||
limit = min(st.msg.GasLimit, params.MaxTxGas)
|
||||
}
|
||||
st.gasRemaining = vm.NewGasBudget(limit, st.msg.GasLimit-limit)
|
||||
st.initReservoir = st.msg.GasLimit - limit
|
||||
st.gasRemaining = vm.NewGasBudget(limit, st.initReservoir)
|
||||
|
||||
if st.evm.Config.Tracer.HasGasHook() {
|
||||
st.evm.Config.Tracer.EmitGasChange(tracing.Gas{}, st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance)
|
||||
|
|
@ -491,7 +527,7 @@ func (st *stateTransition) buyGas() error {
|
|||
//
|
||||
// The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass
|
||||
// subsets of these checks for simulation paths (eth_call, eth_estimateGas).
|
||||
func (st *stateTransition) preCheck() error {
|
||||
func (st *stateTransition) preCheck(intrinsic vm.GasCosts) error {
|
||||
// Only check transactions that are not fake
|
||||
msg := st.msg
|
||||
if !msg.SkipNonceChecks {
|
||||
|
|
@ -585,7 +621,7 @@ func (st *stateTransition) preCheck() error {
|
|||
return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From)
|
||||
}
|
||||
}
|
||||
return st.buyGas()
|
||||
return st.buyGas(intrinsic)
|
||||
}
|
||||
|
||||
// execute transitions the state by applying the current message and
|
||||
|
|
@ -600,14 +636,10 @@ func (st *stateTransition) preCheck() error {
|
|||
// If a consensus error is encountered, it is returned directly with a
|
||||
// nil EVM execution result.
|
||||
func (st *stateTransition) execute() (*ExecutionResult, error) {
|
||||
// Validate the message and pre-pay gas.
|
||||
if err := st.preCheck(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Charge intrinsic gas (with overflow detection inside IntrinsicGas).
|
||||
// Under Amsterdam the cost is two-dimensional and Charge debits both
|
||||
// regular and state in one step.
|
||||
// Compute the intrinsic gas once up front. It is a pure function of the
|
||||
// message and rules (no state access), and is needed both by the EIP-8037
|
||||
// block-inclusion check in preCheck/buyGas and by the intrinsic charge
|
||||
// below, so it is computed here and threaded through.
|
||||
var (
|
||||
msg = st.msg
|
||||
rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time)
|
||||
|
|
@ -618,6 +650,14 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate the message and pre-pay gas.
|
||||
if err := st.preCheck(cost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Charge intrinsic gas. Under Amsterdam the cost is two-dimensional and
|
||||
// Charge debits both regular and state in one step.
|
||||
prior, sufficient := st.gasRemaining.Charge(cost)
|
||||
if !sufficient {
|
||||
return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas)
|
||||
|
|
@ -629,7 +669,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
// Validate the EIP-7623 calldata floor against the gas limit. The floor inflates
|
||||
// the total gas usage at tx end, so the gas limit must be sufficient to cover that.
|
||||
if rules.IsPrague {
|
||||
floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList)
|
||||
floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList, uint64(len(msg.SetCodeAuthorizations)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -645,6 +685,15 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// EIP-8279: seed the per-transaction Block Access List floor accumulator
|
||||
// with the static floor and bound it by the transaction gas limit. The
|
||||
// accumulator is extended at runtime as opcodes contribute BAL bytes; at
|
||||
// settlement the receipt gas becomes max(execution_gas, floor_gas_used).
|
||||
if rules.IsAmsterdam {
|
||||
st.floorGas = vm.NewFloorGasAccumulator(floorDataGas, msg.GasLimit)
|
||||
st.evm.SetFloorGas(st.floorGas)
|
||||
}
|
||||
|
||||
if rules.IsEIP4762 {
|
||||
st.evm.AccessEvents.AddTxOrigin(msg.From)
|
||||
|
||||
|
|
@ -674,11 +723,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
ret []byte
|
||||
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
|
||||
result vm.GasBudget
|
||||
|
||||
// Capture the forwarded regular-gas amount BEFORE ForwardAll consumes
|
||||
// it, so Absorb can back out state-gas spillover from UsedRegularGas
|
||||
// per EIP-8037.
|
||||
forwarded = st.gasRemaining.RegularGas
|
||||
)
|
||||
if contractCreation {
|
||||
// Check whether the init code size has been exceeded.
|
||||
|
|
@ -687,13 +731,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
}
|
||||
// Execute the transaction's creation.
|
||||
ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
|
||||
st.gasRemaining.Absorb(result, forwarded)
|
||||
|
||||
// If the contract creation failed, refund the account-creation state
|
||||
// gas pre-charged in IntrinsicGas.
|
||||
if rules.IsAmsterdam && vmerr != nil {
|
||||
st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
|
||||
}
|
||||
st.gasRemaining.Absorb(result)
|
||||
} else {
|
||||
// Increment the nonce for the next transaction.
|
||||
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)
|
||||
|
|
@ -711,7 +749,30 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
|
|||
}
|
||||
// Execute the transaction's call.
|
||||
ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value)
|
||||
st.gasRemaining.Absorb(result, forwarded)
|
||||
st.gasRemaining.Absorb(result)
|
||||
}
|
||||
|
||||
// EIP-8037 (fork.py:1086): on any transaction error, the state gas
|
||||
// consumed during *execution* is discarded — those state changes are
|
||||
// reverted, so the charge is restored to the reservoir and not counted
|
||||
// toward block_state_gas_used. The intrinsic state gas (CREATE new-account
|
||||
// and EIP-7702 authorization charges) is tracked separately by the spec and
|
||||
// is NOT discarded here; the CREATE new-account portion is refunded above
|
||||
// via its dedicated RefundState. The frame-level Exit forms already refund
|
||||
// state gas on a reverting/halting sub-call, but a top-level frame that
|
||||
// ends in a code-deposit halt (or any other tx-level vmerr) can leave
|
||||
// accumulated execution UsedStateGas that must be discarded here.
|
||||
if rules.IsAmsterdam && vmerr != nil {
|
||||
executionStateGas := st.gasRemaining.UsedStateGas - int64(cost.StateGas)
|
||||
if executionStateGas > 0 {
|
||||
st.gasRemaining.RefundState(uint64(executionStateGas))
|
||||
}
|
||||
// Additionally, a failed CREATE transaction refunds the intrinsic
|
||||
// account-creation state gas pre-charged in IntrinsicGas (fork.py:1093:
|
||||
// when tx.to is Bytes0 the NEW_ACCOUNT charge is added to state_refund).
|
||||
if contractCreation {
|
||||
st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
|
||||
}
|
||||
}
|
||||
|
||||
// Settle down the gas usage and refund the ETH back if any remaining
|
||||
|
|
@ -786,6 +847,7 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g
|
|||
if st.gasRemaining.UsedStateGas < 0 {
|
||||
return 0, 0, fmt.Errorf("negative topmost frame state gas usage, %d", st.gasRemaining.UsedStateGas)
|
||||
}
|
||||
|
||||
txStateGas := uint64(st.gasRemaining.UsedStateGas)
|
||||
|
||||
// EIP-8037:
|
||||
|
|
@ -808,20 +870,35 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g
|
|||
gasLeft += refund
|
||||
gasUsed = gasUsedBeforeRefund - refund
|
||||
|
||||
// EIP-7623: tx_gas_used = max(tx_gas_used_after_refund, calldata_floor).
|
||||
// EIP-8279: the effective floor is the runtime accumulator (seeded with the
|
||||
// static floorDataGas and extended by the BAL bytes opcodes contributed). It
|
||||
// is always >= floorDataGas when the accumulator is active (Amsterdam); the
|
||||
// max keeps the pre-Amsterdam / accumulator-less path on the static floor.
|
||||
floorGas := floorDataGas
|
||||
if st.floorGas != nil {
|
||||
floorGas = max(floorGas, st.floorGas.FloorGasUsed())
|
||||
}
|
||||
|
||||
// EIP-7623: tx_gas_used = max(tx_gas_used_after_refund, floor).
|
||||
peakUsed = gasUsedBeforeRefund
|
||||
if rules.IsPrague && gasUsed < floorDataGas {
|
||||
diff := floorDataGas - gasUsed
|
||||
if rules.IsPrague && gasUsed < floorGas {
|
||||
diff := floorGas - gasUsed
|
||||
if st.evm.Config.Tracer.HasGasHook() {
|
||||
st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: gasLeft}, tracing.Gas{Regular: gasLeft - diff}, tracing.GasChangeTxDataFloor)
|
||||
}
|
||||
gasLeft -= diff
|
||||
gasUsed = floorDataGas
|
||||
peakUsed = max(peakUsed, floorDataGas)
|
||||
gasUsed = floorGas
|
||||
peakUsed = max(peakUsed, floorGas)
|
||||
}
|
||||
|
||||
if rules.IsAmsterdam {
|
||||
if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil {
|
||||
// EIP-7623/7976: the calldata floor applies to the block-level regular
|
||||
// gas dimension as well, mirroring its effect on the receipt gas. The
|
||||
// spec accumulates max(tx_regular_gas, floor) into block_gas_used, so the
|
||||
// block must never count fewer regular units than the floor the sender
|
||||
// was charged. EIP-8279 widens the floor to include BAL byte costs.
|
||||
blockRegularGas := max(txRegularGas, floorGas)
|
||||
if err = st.gp.ChargeGasAmsterdam(blockRegularGas, txStateGas, gasUsed); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
} else {
|
||||
|
|
@ -881,14 +958,17 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio
|
|||
// once, and only when the account did not exist before the tx
|
||||
//
|
||||
// - the delegation-indicator portion (AuthorizationCreationSize × CPSB) is
|
||||
// charged at most once, and only when the authority ends the tx delegated
|
||||
// having started it undelegated.
|
||||
func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]bool) error {
|
||||
// refunded when this auth writes no new indicator bytes (the authority is
|
||||
// already delegated, or the auth clears the delegation).
|
||||
func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization) error {
|
||||
authority, err := st.validateAuthorization(auth)
|
||||
if err != nil {
|
||||
if rules.IsAmsterdam {
|
||||
st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte)
|
||||
}
|
||||
// EIP-8037 (spec apply_authorization): an invalid authorization is
|
||||
// skipped without any state-gas refund. The per-auth intrinsic state
|
||||
// charge ((NEW_ACCOUNT + AUTH_BASE) * CPSB) was levied for every
|
||||
// authorization in the list regardless of validity, and only a
|
||||
// successfully-applied authorization that avoids creating new state
|
||||
// earns a refund below. Invalid auths therefore pay in full.
|
||||
return err
|
||||
}
|
||||
prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority))
|
||||
|
|
@ -898,29 +978,20 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se
|
|||
st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas)
|
||||
}
|
||||
} else {
|
||||
// EIP-8037 (spec apply_authorization): refund the per-auth intrinsic
|
||||
// state charge for state that does not actually get newly created.
|
||||
//
|
||||
// - NEW_ACCOUNT is refunded when the authority account already exists
|
||||
// (account_exists), since no new account is created.
|
||||
if st.state.Exist(authority) {
|
||||
st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
|
||||
}
|
||||
authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte
|
||||
|
||||
preDelegated, ok := delegates[authority]
|
||||
if !ok {
|
||||
preDelegated = curDelegated
|
||||
delegates[authority] = preDelegated
|
||||
}
|
||||
if auth.Address == (common.Address{}) {
|
||||
// Clearing writes no indicator, refill this auth's state charge.
|
||||
st.gasRemaining.RefundState(authBase)
|
||||
|
||||
// The indicator was created by an earlier auth within the same
|
||||
// transaction, refill the state charge as it's no longer justified.
|
||||
if curDelegated && !preDelegated {
|
||||
st.gasRemaining.RefundState(authBase)
|
||||
}
|
||||
} else if curDelegated || preDelegated {
|
||||
// The 23-byte slot is already occupied, overwriting it writes no
|
||||
// new bytes, refill the state charge.
|
||||
st.gasRemaining.RefundState(authBase)
|
||||
// - AUTH_BASE is refunded when no new delegation-indicator bytes are
|
||||
// written: either the authority already carries code/delegation
|
||||
// (code_hash != EMPTY, i.e. curDelegated) or this auth clears the
|
||||
// delegation (auth.address == 0). Exactly one refund per auth.
|
||||
if curDelegated || auth.Address == (common.Address{}) {
|
||||
st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -943,9 +1014,8 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se
|
|||
|
||||
// applyAuthorizations applies an EIP-7702 code delegation to the state.
|
||||
func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) {
|
||||
preDelegated := make(map[common.Address]bool)
|
||||
for _, auth := range auths {
|
||||
st.applyAuthorization(rules, &auth, preDelegated)
|
||||
st.applyAuthorization(rules, &auth)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ func TestFloorDataGas(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rules := params.Rules{IsAmsterdam: tt.amsterdam}
|
||||
got, err := FloorDataGas(rules, tt.data, tt.accessList)
|
||||
got, err := FloorDataGas(rules, tt.data, tt.accessList, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
|
|||
}
|
||||
// Ensure the transaction can cover floor data gas.
|
||||
if rules.IsPrague {
|
||||
floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList())
|
||||
floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations())))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,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 StateRootSource, res *ProcessResult, stateless bool) error
|
||||
}
|
||||
|
||||
// Prefetcher is an interface for pre-caching transaction signatures and state.
|
||||
|
|
@ -63,4 +63,6 @@ type ProcessResult struct {
|
|||
// BAL is only meaningful for post-Amsterdam blocks. Please ensure
|
||||
// fork validation is performed before accessing it.
|
||||
Bal *bal.ConstructionBlockAccessList
|
||||
|
||||
Error error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package bal
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
|
@ -223,3 +224,137 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList {
|
|||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type StorageMutations map[common.Hash]common.Hash
|
||||
|
||||
// AccountMutations contains mutations that were made to an account across
|
||||
// one or more access list indices.
|
||||
type AccountMutations struct {
|
||||
Balance *uint256.Int `json:"Balance,omitempty"`
|
||||
Nonce *uint64 `json:"Nonce,omitempty"`
|
||||
Code []byte `json:"Code,omitempty"`
|
||||
StorageWrites StorageMutations `json:"StorageWrites,omitempty"`
|
||||
}
|
||||
|
||||
// String returns a human-readable JSON representation of the account mutations.
|
||||
func (a *AccountMutations) String() string {
|
||||
var res bytes.Buffer
|
||||
enc := json.NewEncoder(&res)
|
||||
enc.SetIndent("", " ")
|
||||
enc.Encode(a)
|
||||
return res.String()
|
||||
}
|
||||
|
||||
// Copy returns a deep-copy of the instance.
|
||||
func (a *AccountMutations) Copy() *AccountMutations {
|
||||
res := &AccountMutations{
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
}
|
||||
if a.Nonce != nil {
|
||||
res.Nonce = new(uint64)
|
||||
*res.Nonce = *a.Nonce
|
||||
}
|
||||
if a.Code != nil {
|
||||
res.Code = bytes.Clone(a.Code)
|
||||
}
|
||||
if a.Balance != nil {
|
||||
res.Balance = new(uint256.Int).Set(a.Balance)
|
||||
}
|
||||
if a.StorageWrites != nil {
|
||||
res.StorageWrites = maps.Clone(a.StorageWrites)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Eq returns whether the calling instance is equal to the provided one.
|
||||
func (a *AccountMutations) Eq(other *AccountMutations) bool {
|
||||
if a.Balance != nil || other.Balance != nil {
|
||||
if a.Balance == nil || other.Balance == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !a.Balance.Eq(other.Balance) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (len(a.Code) != 0 || len(other.Code) != 0) && !bytes.Equal(a.Code, other.Code) {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Nonce != nil || other.Nonce != nil {
|
||||
if a.Nonce == nil || other.Nonce == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if *a.Nonce != *other.Nonce {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if a.StorageWrites != nil || other.StorageWrites != nil {
|
||||
if !maps.Equal(a.StorageWrites, other.StorageWrites) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type BALExecutionMode int
|
||||
|
||||
const (
|
||||
BALExecutionOptimized BALExecutionMode = iota
|
||||
BALExecutionNoBatchIO
|
||||
BALExecutionSequential
|
||||
)
|
||||
|
||||
// WrittenCounts groups per-block aggregate write counts derived from the BAL.
|
||||
type WrittenCounts struct {
|
||||
Accounts int
|
||||
StorageSlots int
|
||||
Codes int
|
||||
CodeBytes int
|
||||
}
|
||||
|
||||
// WrittenCounts walks the BAL once and returns the aggregate write counts.
|
||||
func (e BlockAccessList) WrittenCounts() WrittenCounts {
|
||||
var w WrittenCounts
|
||||
for i := range e {
|
||||
a := &e[i]
|
||||
if len(a.StorageChanges) > 0 || len(a.BalanceChanges) > 0 ||
|
||||
len(a.NonceChanges) > 0 || len(a.CodeChanges) > 0 {
|
||||
w.Accounts++
|
||||
}
|
||||
w.StorageSlots += len(a.StorageChanges)
|
||||
if n := len(a.CodeChanges); n > 0 {
|
||||
w.Codes++
|
||||
w.CodeBytes += len(a.CodeChanges[n-1].NewCode)
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
type StateMutations map[common.Address]AccountMutations
|
||||
|
||||
type StorageKeySet map[common.Hash]struct{}
|
||||
type StateAccesses map[common.Address]StorageKeySet
|
||||
|
||||
func (s StateAccesses) Eq(other StateAccesses) bool {
|
||||
if len(s) != len(other) {
|
||||
return false
|
||||
}
|
||||
|
||||
for addr, set := range s {
|
||||
otherSet, ok := other[addr]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !maps.Equal(set, otherSet) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
|
|||
obj.SlotChanges = make([]encodingStorageWrite, 0, len(slotWrites))
|
||||
|
||||
indices := slices.Collect(maps.Keys(slotWrites))
|
||||
slices.SortFunc(indices, cmp.Compare)
|
||||
slices.Sort(indices)
|
||||
for _, index := range indices {
|
||||
val := slotWrites[index]
|
||||
obj.SlotChanges = append(obj.SlotChanges, encodingStorageWrite{
|
||||
|
|
@ -420,7 +420,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
|
|||
|
||||
// Convert balance changes
|
||||
balanceIndices := slices.Collect(maps.Keys(a.BalanceChanges))
|
||||
slices.SortFunc(balanceIndices, cmp.Compare)
|
||||
slices.Sort(balanceIndices)
|
||||
for _, idx := range balanceIndices {
|
||||
res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{
|
||||
BlockAccessIndex: idx,
|
||||
|
|
@ -430,7 +430,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
|
|||
|
||||
// Convert nonce changes
|
||||
nonceIndices := slices.Collect(maps.Keys(a.NonceChanges))
|
||||
slices.SortFunc(nonceIndices, cmp.Compare)
|
||||
slices.Sort(nonceIndices)
|
||||
for _, idx := range nonceIndices {
|
||||
res.NonceChanges = append(res.NonceChanges, encodingAccountNonce{
|
||||
BlockAccessIndex: idx,
|
||||
|
|
@ -440,7 +440,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
|
|||
|
||||
// Convert code change
|
||||
codeIndices := slices.Collect(maps.Keys(a.CodeChange))
|
||||
slices.SortFunc(codeIndices, cmp.Compare)
|
||||
slices.Sort(codeIndices)
|
||||
for _, idx := range codeIndices {
|
||||
res.CodeChanges = append(res.CodeChanges, encodingCodeChange{
|
||||
BlockAccessIndex: idx,
|
||||
|
|
|
|||
196
core/types/bal/bal_reader.go
Normal file
196
core/types/bal/bal_reader.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
package bal
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// AccessListReader enables efficient state diff lookups from a block access
|
||||
// list during block execution.
|
||||
type AccessListReader struct {
|
||||
accounts map[common.Address]*preparedAccount
|
||||
}
|
||||
|
||||
type preparedAccount struct {
|
||||
storage map[common.Hash]preparedSlot
|
||||
AccountAccess
|
||||
}
|
||||
|
||||
type preparedSlot struct {
|
||||
changes []encodingStorageWrite // borrowed, sorted asc by BlockAccessIndex
|
||||
}
|
||||
|
||||
// NewAccessListReader instantiates an access list reader.
|
||||
func NewAccessListReader(list BlockAccessList) *AccessListReader {
|
||||
accounts := make(map[common.Address]*preparedAccount, len(list))
|
||||
for i := range list {
|
||||
a := list[i] // index; do not range-copy the AccountAccess
|
||||
pa := &preparedAccount{
|
||||
AccountAccess: 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 &AccessListReader{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 *AccessListReader) Balance(addr common.Address, idx int) *uint256.Int {
|
||||
a := p.accounts[addr]
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
k := lastBefore(len(a.BalanceChanges), uint32(idx), func(i int) uint32 { return a.BalanceChanges[i].BlockAccessIndex })
|
||||
if k < 0 {
|
||||
return nil
|
||||
}
|
||||
return a.BalanceChanges[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 *AccessListReader) Nonce(addr common.Address, idx int) (uint64, bool) {
|
||||
a := p.accounts[addr]
|
||||
if a == nil {
|
||||
return 0, false
|
||||
}
|
||||
k := lastBefore(len(a.NonceChanges), uint32(idx), func(i int) uint32 { return a.NonceChanges[i].BlockAccessIndex })
|
||||
if k < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return a.NonceChanges[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 *AccessListReader) Code(addr common.Address, idx int) []byte {
|
||||
a := p.accounts[addr]
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
k := lastBefore(len(a.CodeChanges), uint32(idx), func(i int) uint32 { return a.CodeChanges[i].BlockAccessIndex })
|
||||
if k < 0 {
|
||||
return nil
|
||||
}
|
||||
return a.CodeChanges[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 *AccessListReader) StorageAt(addr common.Address, slot common.Hash, idx int) (common.Hash, bool) {
|
||||
a := p.accounts[addr]
|
||||
if a == nil {
|
||||
return common.Hash{}, false
|
||||
}
|
||||
s, ok := a.storage[slot]
|
||||
if !ok {
|
||||
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 *AccessListReader) 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
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
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 (p *AccessListReader) StorageKeys(reads bool) (keys StorageKeys) {
|
||||
keys = make(StorageKeys)
|
||||
for addr, a := range p.accounts {
|
||||
for _, storageChange := range a.StorageChanges {
|
||||
keys[addr] = append(keys[addr], storageChange.Slot.Bytes32())
|
||||
}
|
||||
if !(reads && len(a.StorageReads) > 0) {
|
||||
continue
|
||||
}
|
||||
for _, storageRead := range a.StorageReads {
|
||||
keys[addr] = append(keys[addr], storageRead.Bytes32())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Mutations returns the aggregate state mutations from bal indices [0, idx).
|
||||
func (p *AccessListReader) Mutations(idx int) *StateMutations {
|
||||
res := make(StateMutations)
|
||||
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 (p *AccessListReader) AllDestructions() (res []common.Address) {
|
||||
for addr, a := range p.accounts {
|
||||
for _, nonce := range a.NonceChanges {
|
||||
if nonce.PostNonce == 0 {
|
||||
res = append(res, addr)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -153,9 +153,9 @@ func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.G
|
|||
}
|
||||
|
||||
// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state.
|
||||
func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
|
||||
func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) {
|
||||
prior := c.Gas
|
||||
c.Gas.Absorb(child, forwarded)
|
||||
c.Gas.Absorb(child)
|
||||
if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
|
||||
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -588,6 +588,8 @@ func enable7843(jt *JumpTable) {
|
|||
func enable8037(jt *JumpTable) {
|
||||
jt[CREATE].constantGas = params.CreateGasAmsterdam
|
||||
jt[CREATE2].constantGas = params.CreateGasAmsterdam
|
||||
jt[CREATE].dynamicGas = gasCreateEip8037
|
||||
jt[CREATE2].dynamicGas = gasCreate2Eip8037
|
||||
jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037
|
||||
jt[SSTORE].dynamicGas = gasSStore8037
|
||||
}
|
||||
|
|
|
|||
166
core/vm/evm.go
166
core/vm/evm.go
|
|
@ -131,6 +131,13 @@ type EVM struct {
|
|||
returnData []byte // Last CALL's return data for subsequent reuse
|
||||
|
||||
arena *stackArena
|
||||
|
||||
// floorGas is the per-transaction EIP-8279 Block Access List byte-floor
|
||||
// accumulator. It is set by the state transition at the start of each
|
||||
// transaction and extended at runtime as opcodes contribute BAL bytes. It
|
||||
// is nil before EIP-8279 (Amsterdam) or in contexts without BAL
|
||||
// construction, in which case the runtime extensions are no-ops.
|
||||
floorGas *FloorGasAccumulator
|
||||
}
|
||||
|
||||
// NewEVM constructs an EVM instance with the supplied block context, state
|
||||
|
|
@ -222,6 +229,26 @@ func (evm *EVM) SetTxContext(txCtx TxContext) {
|
|||
evm.TxContext = txCtx
|
||||
}
|
||||
|
||||
// SetFloorGas installs the per-transaction EIP-8279 floor accumulator. It is
|
||||
// called by the state transition once the static floor seed and gas limit are
|
||||
// known. Passing nil disables runtime floor extensions for the transaction.
|
||||
func (evm *EVM) SetFloorGas(acc *FloorGasAccumulator) {
|
||||
evm.floorGas = acc
|
||||
}
|
||||
|
||||
// FloorGas returns the active EIP-8279 floor accumulator, or nil if none is set.
|
||||
func (evm *EVM) FloorGas() *FloorGasAccumulator {
|
||||
return evm.floorGas
|
||||
}
|
||||
|
||||
// extendFloor extends the EIP-8279 floor accumulator by numBytes BAL bytes. It
|
||||
// is a no-op when no accumulator is installed (pre-Amsterdam or BAL-less
|
||||
// contexts). The returned error, when non-nil, is ErrOutOfGas and MUST abort
|
||||
// the operation before the matching BAL byte is inserted.
|
||||
func (evm *EVM) extendFloor(numBytes uint64) error {
|
||||
return evm.floorGas.extendFloor(numBytes)
|
||||
}
|
||||
|
||||
// Cancel cancels any running EVM operation. This may be called concurrently and
|
||||
// it's safe to be called multiple times.
|
||||
func (evm *EVM) Cancel() {
|
||||
|
|
@ -261,11 +288,17 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
|
|||
}
|
||||
syscall := isSystemCall(caller)
|
||||
|
||||
// EIP-7928: per the Amsterdam spec, delegation resolution happens before
|
||||
// the value-transfer check, so the delegated-to must appear in the BAL
|
||||
// even when the call later reverts with ErrInsufficientBalance. Touch the
|
||||
// target's code here (a no-op for non-delegated accounts) to record it.
|
||||
evm.resolveCode(addr)
|
||||
|
||||
// Fail if we're trying to transfer more than the available balance.
|
||||
if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
|
||||
return nil, gas, ErrInsufficientBalance
|
||||
}
|
||||
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
|
||||
snapshot := evm.StateDB.Snapshot()
|
||||
p, isPrecompile := evm.precompile(addr)
|
||||
if !evm.StateDB.Exist(addr) {
|
||||
if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) {
|
||||
|
|
@ -279,7 +312,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
|
|||
wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false)
|
||||
if _, ok := gas.ChargeRegular(wgas); !ok {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
return nil, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
return nil, gas.ExitHalt(), ErrOutOfGas
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,16 +322,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
|
|||
}
|
||||
evm.StateDB.CreateAccount(addr)
|
||||
}
|
||||
if evm.chainRules.IsAmsterdam && !value.IsZero() && evm.StateDB.Empty(addr) {
|
||||
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
|
||||
if !ok {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
return nil, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
}
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
|
||||
}
|
||||
}
|
||||
// Perform the value transfer only in non-syscall mode.
|
||||
// Calling this is required even for zero-value transfers,
|
||||
// to ensure the state clearing mechanism is applied.
|
||||
|
|
@ -324,7 +347,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
|
|||
}
|
||||
|
||||
// Calculate the remaining gas at the end of frame
|
||||
exitGas := gas.Exit(err, reservoir)
|
||||
exitGas := gas.Exit(err)
|
||||
if err != nil {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
|
||||
|
|
@ -356,11 +379,17 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
|
|||
if evm.depth > int(params.CallCreateDepth) {
|
||||
return nil, gas, ErrDepth
|
||||
}
|
||||
|
||||
// EIP-7928: per the Amsterdam spec, delegation resolution happens before
|
||||
// the value-transfer check, so the delegated-to must appear in the BAL
|
||||
// even when the call later reverts with ErrInsufficientBalance.
|
||||
evm.resolveCode(addr)
|
||||
|
||||
// Fail if we're trying to transfer more than the available balance
|
||||
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
|
||||
return nil, gas, ErrInsufficientBalance
|
||||
}
|
||||
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
|
||||
snapshot := evm.StateDB.Snapshot()
|
||||
|
||||
// It is allowed to call precompiles, even via delegatecall
|
||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||
|
|
@ -375,7 +404,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
|
|||
}
|
||||
|
||||
// Calculate the remaining gas at the end of frame
|
||||
exitGas := gas.Exit(err, reservoir)
|
||||
exitGas := gas.Exit(err)
|
||||
if err != nil {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
|
||||
|
|
@ -406,7 +435,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
|
|||
if evm.depth > int(params.CallCreateDepth) {
|
||||
return nil, gas, ErrDepth
|
||||
}
|
||||
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
|
||||
snapshot := evm.StateDB.Snapshot()
|
||||
|
||||
// It is allowed to call precompiles, even via delegatecall
|
||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||
|
|
@ -419,7 +448,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
|
|||
}
|
||||
|
||||
// Calculate the remaining gas at the end of frame
|
||||
exitGas := gas.Exit(err, reservoir)
|
||||
exitGas := gas.Exit(err)
|
||||
if err != nil {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
|
||||
|
|
@ -453,7 +482,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
|
|||
// after all empty accounts were deleted, so this is not required. However, if we omit this,
|
||||
// then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json.
|
||||
// We could change this, but for now it's left for legacy reasons
|
||||
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
|
||||
snapshot := evm.StateDB.Snapshot()
|
||||
|
||||
// We do an AddBalance of zero here, just in order to trigger a touch.
|
||||
// This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium,
|
||||
|
|
@ -471,7 +500,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
|
|||
}
|
||||
|
||||
// Calculate the remaining gas at the end of frame
|
||||
exitGas := gas.Exit(err, reservoir)
|
||||
exitGas := gas.Exit(err)
|
||||
if err != nil {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
if err != ErrExecutionReverted {
|
||||
|
|
@ -509,20 +538,28 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
}
|
||||
// Increment the caller's nonce after passing all validations
|
||||
evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator)
|
||||
reservoir := gas.StateGas
|
||||
|
||||
// Charge the contract creation init gas in verkle mode
|
||||
if evm.chainRules.IsEIP4762 {
|
||||
statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas)
|
||||
prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas})
|
||||
if !ok {
|
||||
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas
|
||||
}
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
|
||||
}
|
||||
}
|
||||
|
||||
// EIP-8279: an opcode-level CREATE/CREATE2 records the deployed address in
|
||||
// the BAL. The top-level creation transaction's contract address is part of
|
||||
// the implicit per-tx bytes covered by TX_BASE headroom, so only nested
|
||||
// creations extend the floor here.
|
||||
if evm.depth > 0 {
|
||||
if err = evm.extendFloor(params.BALBytesPerAddress); err != nil {
|
||||
return nil, common.Address{}, gas.ExitHalt(), err
|
||||
}
|
||||
}
|
||||
// We add this to the access list _before_ taking a snapshot. Even if the
|
||||
// creation fails, the access-list change should not be rolled back.
|
||||
if evm.chainRules.IsEIP2929 {
|
||||
|
|
@ -537,7 +574,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
if evm.StateDB.GetNonce(address) != 0 ||
|
||||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
|
||||
isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
|
||||
halt := gas.ExitHalt(reservoir)
|
||||
halt := gas.ExitHalt()
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution)
|
||||
}
|
||||
|
|
@ -551,18 +588,9 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
snapshot := evm.StateDB.Snapshot()
|
||||
if !evm.StateDB.Exist(address) {
|
||||
evm.StateDB.CreateAccount(address)
|
||||
|
||||
if evm.chainRules.IsAmsterdam && evm.depth > 0 {
|
||||
// Only charge state gas if we are not doing a create transaction.
|
||||
// Prevents double charging with IntrinsicGas.
|
||||
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
|
||||
if !ok {
|
||||
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
}
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
|
||||
}
|
||||
}
|
||||
// EIP-8037: the account-creation state gas is charged before the
|
||||
// opcode runs (gasCreateEip8037) for CREATE/CREATE2 opcodes, and in
|
||||
// IntrinsicGas for creation transactions, so there is no charge here.
|
||||
}
|
||||
// CreateContract means that regardless of whether the account previously existed
|
||||
// in the state trie or not, it _now_ becomes created as a _contract_ account.
|
||||
|
|
@ -570,6 +598,21 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
// acts inside that account.
|
||||
evm.StateDB.CreateContract(address)
|
||||
|
||||
// EIP-8279: an opcode-level CREATE/CREATE2 sets the new contract's nonce
|
||||
// (and, with a non-zero endowment, its balance), both recorded in the BAL.
|
||||
// The floor is extended after the collision check, before the state
|
||||
// mutation. The top-level creation transaction's nonce is covered by
|
||||
// TX_BASE headroom, so only nested creations extend the floor here.
|
||||
if evm.depth > 0 {
|
||||
if err = evm.extendFloor(params.BALBytesPerNonce); err != nil {
|
||||
return nil, common.Address{}, gas.ExitHalt(), err
|
||||
}
|
||||
if !value.IsZero() {
|
||||
if err = evm.extendFloor(params.BALBytesPerBalance); err != nil {
|
||||
return nil, common.Address{}, gas.ExitHalt(), err
|
||||
}
|
||||
}
|
||||
}
|
||||
if evm.chainRules.IsEIP158 {
|
||||
evm.StateDB.SetNonce(address, 1, tracing.NonceChangeNewContract)
|
||||
}
|
||||
|
|
@ -577,7 +620,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
if evm.chainRules.IsEIP4762 {
|
||||
consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas)
|
||||
if consumed < wanted {
|
||||
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
|
||||
return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas
|
||||
}
|
||||
prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
|
|
@ -595,14 +638,23 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
contract.SetCallCode(common.Hash{}, code)
|
||||
contract.IsDeployment = true
|
||||
|
||||
ret, err = evm.initNewContract(contract, address)
|
||||
var depositHalt bool
|
||||
ret, depositHalt, err = evm.initNewContract(contract, address)
|
||||
|
||||
// Special case: ErrCodeStoreOutOfGas pre-Homestead does NOT roll back
|
||||
// state and gas is preserved (i.e., treated as success).
|
||||
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
|
||||
evm.StateDB.RevertToSnapshot(snapshot)
|
||||
|
||||
exit := contract.Gas.Exit(err, reservoir)
|
||||
// EIP-8037: a code-deposit halt (initcode body succeeded, deposit step
|
||||
// failed) keeps the state gas the body consumed for discard at the tx
|
||||
// level, rather than refunding the reservoir like a mid-execution halt.
|
||||
var exit GasBudget
|
||||
if depositHalt && evm.chainRules.IsAmsterdam {
|
||||
exit = contract.Gas.ExitCodeDepositHalt()
|
||||
} else {
|
||||
exit = contract.Gas.Exit(err)
|
||||
}
|
||||
if err != ErrExecutionReverted {
|
||||
if evm.Config.Tracer.HasGasHook() {
|
||||
evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution)
|
||||
|
|
@ -617,54 +669,70 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
|
|||
|
||||
// initNewContract runs a new contract's creation code, performs checks on the
|
||||
// resulting code that is to be deployed, and consumes necessary gas.
|
||||
func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]byte, error) {
|
||||
ret, err := evm.Run(contract, nil, false)
|
||||
//
|
||||
// The returned depositHalt flag is true when the initcode body itself ran to
|
||||
// completion successfully but a subsequent code-deposit check failed (oversized
|
||||
// code, 0xEF prefix, or insufficient gas for the hash/deposit charge). Under
|
||||
// EIP-8037 this halt is metered differently from a mid-execution halt: the
|
||||
// state gas consumed by the (successful) body is kept rather than refunded.
|
||||
func (evm *EVM) initNewContract(contract *Contract, address common.Address) (ret []byte, depositHalt bool, err error) {
|
||||
ret, err = evm.Run(contract, nil, false)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
return ret, false, err
|
||||
}
|
||||
// Check prefix before gas calculation.
|
||||
// Reject code starting with 0xEF if EIP-3541 is enabled.
|
||||
if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon {
|
||||
return ret, ErrInvalidCode
|
||||
return ret, true, ErrInvalidCode
|
||||
}
|
||||
if evm.chainRules.IsEIP4762 {
|
||||
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas)
|
||||
contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
|
||||
if len(ret) > 0 && (consumed < wanted) {
|
||||
return ret, ErrCodeStoreOutOfGas
|
||||
return ret, true, ErrCodeStoreOutOfGas
|
||||
}
|
||||
if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
|
||||
return ret, err
|
||||
return ret, true, err
|
||||
}
|
||||
} else if evm.chainRules.IsAmsterdam {
|
||||
// Check max code size BEFORE charging gas so over-max code
|
||||
// does not consume state gas (which would inflate tx_state).
|
||||
if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
|
||||
return ret, err
|
||||
return ret, true, err
|
||||
}
|
||||
// Charge regular gas (hash cost) before state gas.
|
||||
regularCost := toWordSize(uint64(len(ret))) * params.Keccak256WordGas
|
||||
if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
|
||||
return ret, ErrCodeStoreOutOfGas
|
||||
return ret, true, ErrCodeStoreOutOfGas
|
||||
}
|
||||
// Charge state gas (code-deposit) afterwards.
|
||||
stateCost := uint64(len(ret)) * evm.Context.CostPerStateByte
|
||||
if !contract.chargeState(stateCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
|
||||
return ret, ErrCodeStoreOutOfGas
|
||||
return ret, true, ErrCodeStoreOutOfGas
|
||||
}
|
||||
} else {
|
||||
createDataCost := uint64(len(ret)) * params.CreateDataGas
|
||||
if !contract.chargeRegular(createDataCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
|
||||
return ret, ErrCodeStoreOutOfGas
|
||||
return ret, true, ErrCodeStoreOutOfGas
|
||||
}
|
||||
if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
|
||||
return ret, err
|
||||
return ret, true, err
|
||||
}
|
||||
}
|
||||
// EIP-8279: a successful opcode-level CREATE/CREATE2 deploy records the
|
||||
// deployed code in the BAL. Extend the floor by the code length before
|
||||
// set_code. A top-level creation transaction's deployed code is bounded by
|
||||
// the calldata floor on its init code, so only nested creations extend the
|
||||
// floor here.
|
||||
if evm.depth > 0 && len(ret) > 0 {
|
||||
if err := evm.extendFloor(uint64(len(ret))); err != nil {
|
||||
return ret, true, err
|
||||
}
|
||||
}
|
||||
if len(ret) > 0 {
|
||||
evm.StateDB.SetCode(address, ret, tracing.CodeChangeContractCreation)
|
||||
}
|
||||
return ret, nil
|
||||
return ret, false, nil
|
||||
}
|
||||
|
||||
// Create creates a new contract using code as deployment code.
|
||||
|
|
|
|||
76
core/vm/floor.go
Normal file
76
core/vm/floor.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package vm
|
||||
|
||||
import "github.com/ethereum/go-ethereum/params"
|
||||
|
||||
// FloorGasAccumulator implements the per-transaction floor accumulator defined
|
||||
// by EIP-8279 (Block Access List Byte Floor). It is an internal counter on the
|
||||
// execution environment, seeded with the EIP-8131 static floor and extended at
|
||||
// runtime by FloorGasPerByte for every byte an opcode adds to the EIP-7928
|
||||
// Block Access List.
|
||||
//
|
||||
// The accumulator is not part of the signed transaction, is not RLP-encoded,
|
||||
// gossiped, or persisted; no gas is reserved or deducted from the execution
|
||||
// budget. It is checked against the transaction's gas limit only to ensure the
|
||||
// sender can pay the floor if it ends up binding, and at transaction end the
|
||||
// receipt gas is settled as max(execution_gas_used, floor_gas_used).
|
||||
type FloorGasAccumulator struct {
|
||||
floorGasUsed uint64 // accumulated floor gas (static seed + runtime extensions)
|
||||
gasLimit uint64 // tx.gas; the accumulator must never climb past this
|
||||
}
|
||||
|
||||
// NewFloorGasAccumulator returns an accumulator seeded with the static floor
|
||||
// and bounded by the transaction gas limit.
|
||||
func NewFloorGasAccumulator(staticFloor, gasLimit uint64) *FloorGasAccumulator {
|
||||
return &FloorGasAccumulator{floorGasUsed: staticFloor, gasLimit: gasLimit}
|
||||
}
|
||||
|
||||
// FloorGasUsed returns the current value of the floor accumulator.
|
||||
func (f *FloorGasAccumulator) FloorGasUsed() uint64 {
|
||||
if f == nil {
|
||||
return 0
|
||||
}
|
||||
return f.floorGasUsed
|
||||
}
|
||||
|
||||
// extendFloor extends the floor accumulator by numBytes BAL bytes, each priced
|
||||
// at params.FloorGasPerByte. It MUST be called BEFORE the matching BAL
|
||||
// insertion or state mutation: if the new floor would exceed the transaction
|
||||
// gas limit it returns ErrOutOfGas, which aborts the operation before any
|
||||
// unpaid BAL byte exists. A nil accumulator (pre-EIP-8279, or contexts without
|
||||
// BAL construction) is a no-op.
|
||||
func (f *FloorGasAccumulator) extendFloor(numBytes uint64) error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
// numBytes is bounded by deployed-code length in the worst case; guard the
|
||||
// multiplication against overflow before checking against the gas limit.
|
||||
if numBytes > (^uint64(0))/params.FloorGasPerByte {
|
||||
return ErrOutOfGas
|
||||
}
|
||||
extension := numBytes * params.FloorGasPerByte
|
||||
if f.floorGasUsed > f.gasLimit-min(f.gasLimit, extension) {
|
||||
return ErrOutOfGas
|
||||
}
|
||||
newFloor := f.floorGasUsed + extension
|
||||
if newFloor > f.gasLimit {
|
||||
return ErrOutOfGas
|
||||
}
|
||||
f.floorGasUsed = newFloor
|
||||
return nil
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/math"
|
||||
"github.com/ethereum/go-ethereum/core/tracing"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
|
|
@ -367,6 +368,62 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
|
|||
return GasCosts{RegularGas: gas}, nil
|
||||
}
|
||||
|
||||
// gasCreateEip8037 is the CREATE gas calculator for Amsterdam. It charges the
|
||||
// account-creation cost as state gas (EIP-8037) here, before the opcode runs,
|
||||
// so the 63/64 gas-forwarding split sees the post-charge regular gas. The
|
||||
// charge is refunded to the reservoir in opCreate on any failure path that
|
||||
// does not create an account.
|
||||
func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
if evm.readOnly {
|
||||
return GasCosts{}, ErrWriteProtection
|
||||
}
|
||||
gas, err := memoryGasCost(mem, memorySize)
|
||||
if err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
size, overflow := stack.back(2).Uint64WithOverflow()
|
||||
if overflow {
|
||||
return GasCosts{}, ErrGasUintOverflow
|
||||
}
|
||||
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
// Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow.
|
||||
wordGas := params.InitCodeWordGas * ((size + 31) / 32)
|
||||
stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte
|
||||
return GasCosts{
|
||||
RegularGas: gas + wordGas,
|
||||
StateGas: stateGas,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// gasCreate2Eip8037 is the CREATE2 gas calculator for Amsterdam. See
|
||||
// gasCreateEip8037; CREATE2 additionally charges Keccak256WordGas for hashing
|
||||
// the init code.
|
||||
func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
if evm.readOnly {
|
||||
return GasCosts{}, ErrWriteProtection
|
||||
}
|
||||
gas, err := memoryGasCost(mem, memorySize)
|
||||
if err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
size, overflow := stack.back(2).Uint64WithOverflow()
|
||||
if overflow {
|
||||
return GasCosts{}, ErrGasUintOverflow
|
||||
}
|
||||
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
// Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow.
|
||||
wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * ((size + 31) / 32)
|
||||
stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte
|
||||
return GasCosts{
|
||||
RegularGas: gas + wordGas,
|
||||
StateGas: stateGas,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
|
||||
expByteLen := uint64((stack.back(1).BitLen() + 7) / 8)
|
||||
|
||||
|
|
@ -446,6 +503,34 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
|
|||
return 0, ErrOutOfGas
|
||||
}
|
||||
// Stateful check
|
||||
if evm.chainRules.IsAmsterdam {
|
||||
// EIP-8279: a CALL transferring non-zero value to a different account
|
||||
// records a balance change for the recipient in the BAL. Extend the
|
||||
// floor by the balance bytes before the transfer. A self-call moves no
|
||||
// value out of the executing account and adds no balance bytes.
|
||||
if transfersValue && address != contract.Address() {
|
||||
if err := evm.extendFloor(params.BALBytesPerBalance); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
// EIP-8037: the cost of creating a new account via a value-bearing
|
||||
// CALL is metered as state gas (NEW_ACCOUNT * CostPerStateByte),
|
||||
// not the legacy regular CallNewAccountGas. It drains the state
|
||||
// reservoir, spilling into regular gas only when the reservoir is
|
||||
// exhausted, mirroring the spec's inline charge_state_gas in
|
||||
// system.call.
|
||||
if transfersValue && evm.StateDB.Empty(address) {
|
||||
stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte
|
||||
regularAfterCall := contract.Gas.RegularGas - gas
|
||||
if stateGas > contract.Gas.StateGas && stateGas-contract.Gas.StateGas > regularAfterCall {
|
||||
return 0, ErrOutOfGas
|
||||
}
|
||||
if !contract.chargeState(stateGas, evm.Config.Tracer, tracing.GasChangeAccountCreation) {
|
||||
return 0, ErrOutOfGas
|
||||
}
|
||||
}
|
||||
return gas, nil
|
||||
}
|
||||
var stateGas uint64
|
||||
if evm.chainRules.IsEIP158 {
|
||||
if transfersValue && evm.StateDB.Empty(address) {
|
||||
|
|
@ -528,6 +613,10 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory
|
|||
address = common.Address(stack.peek().Bytes20())
|
||||
)
|
||||
if !evm.StateDB.AddressInAccessList(address) {
|
||||
// EIP-8279: a cold beneficiary access adds the address to the BAL.
|
||||
if err := evm.extendFloor(params.BALBytesPerAddress); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
// If the caller cannot afford the cost, this change will be rolled back
|
||||
evm.StateDB.AddAddressToAccessList(address)
|
||||
gas.RegularGas = params.ColdAccountAccessCostEIP2929
|
||||
|
|
@ -536,6 +625,14 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory
|
|||
if contract.Gas.RegularGas < gas.RegularGas {
|
||||
return gas, ErrOutOfGas
|
||||
}
|
||||
// EIP-8279: SELFDESTRUCT moving a non-zero balance to a different beneficiary
|
||||
// records the beneficiary's balance change in the BAL. A self-targeted
|
||||
// SELFDESTRUCT moves no value out and adds no balance bytes.
|
||||
if address != contract.Address() && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
|
||||
if err := evm.extendFloor(params.BALBytesPerBalance); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
}
|
||||
// Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist
|
||||
// in the current state yet still be considered non-existent by EIP-161 if its
|
||||
// nonce, balance, and code are all zero. Such accounts can appear temporarily
|
||||
|
|
@ -565,12 +662,29 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo
|
|||
)
|
||||
// Check slot presence in the access list
|
||||
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
|
||||
// EIP-8279: a cold SSTORE adds the storage key to the BAL. Extend the
|
||||
// floor before the slot is recorded; an out-of-gas here aborts the
|
||||
// opcode before the unpaid BAL byte exists.
|
||||
if err := evm.extendFloor(params.BALBytesPerStorageKey); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
cost = GasCosts{RegularGas: params.ColdSloadCostEIP2929}
|
||||
// If the caller cannot afford the cost, this change will be rolled back
|
||||
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
|
||||
}
|
||||
value := common.Hash(y.Bytes32())
|
||||
|
||||
// EIP-8279: an SSTORE that changes the slot's current value contributes a
|
||||
// post-value to the BAL. Charge the value bytes whenever the value differs,
|
||||
// mirroring the BAL StorageWrite. This may over-charge when the same slot is
|
||||
// written more than once in a transaction (the BAL records only one final
|
||||
// post-value per slot), which is safe: it never under-charges.
|
||||
if current != value {
|
||||
if err := evm.extendFloor(params.BALBytesPerStorageValue); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if current == value { // noop (1)
|
||||
// EIP 2200 original clause:
|
||||
// return params.SloadGasEIP2200, nil
|
||||
|
|
|
|||
|
|
@ -226,9 +226,11 @@ func (g GasBudget) ExitRevert() GasBudget {
|
|||
|
||||
// ExitHalt produces the leftover for an exceptional halt.
|
||||
//
|
||||
// - state_gas_reservoir is reset back to its value at the start of the child frame
|
||||
// - the gas_left initially given to the child is consumed (set to zero)
|
||||
func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
|
||||
// Per the updated EIP-8037, only the regular gas_left is burned (folded into
|
||||
// UsedRegularGas); the entire state-gas reservoir — including any portion that
|
||||
// spilled into the regular pool during execution — is refunded to the caller's
|
||||
// reservoir rather than reclassified as burned regular gas.
|
||||
func (g GasBudget) ExitHalt() GasBudget {
|
||||
reservoir := int64(g.StateGas) + g.UsedStateGas
|
||||
if reservoir < 0 {
|
||||
// Reservoir should never be negative. By construction it equals
|
||||
|
|
@ -237,21 +239,34 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
|
|||
reservoir = 0
|
||||
log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas)
|
||||
}
|
||||
// The portion of state gas charged from regular gas is also burned
|
||||
// together with the regular gas, rather than being returned to the
|
||||
// parent's state-gas reservoir.
|
||||
var spilled uint64
|
||||
if uint64(reservoir) > initStateReservoir {
|
||||
spilled = uint64(reservoir) - initStateReservoir
|
||||
}
|
||||
return GasBudget{
|
||||
RegularGas: 0,
|
||||
StateGas: initStateReservoir,
|
||||
UsedRegularGas: g.UsedRegularGas + g.RegularGas + spilled,
|
||||
StateGas: uint64(reservoir),
|
||||
UsedRegularGas: g.UsedRegularGas + g.RegularGas,
|
||||
UsedStateGas: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ExitCodeDepositHalt produces the leftover for a CREATE/CREATE2 frame whose
|
||||
// initcode body ran to completion but then failed during the code-deposit step
|
||||
// (oversized code, 0xEF prefix, or insufficient gas for the hash/deposit
|
||||
// charge). Per the spec's process_create_message exception handler, this path
|
||||
// differs from a mid-execution ExceptionalHalt: only the remaining regular gas
|
||||
// is burned, while the state gas the (successful) body consumed is KEPT in
|
||||
// UsedStateGas rather than refunded to the reservoir. The state changes are
|
||||
// reverted by the caller, but the state-gas accounting is propagated upward so
|
||||
// the top-level tx settlement can discard it as the state dimension (rather
|
||||
// than folding it back into the combined balance, which would wrongly deflate
|
||||
// the regular dimension).
|
||||
func (g GasBudget) ExitCodeDepositHalt() GasBudget {
|
||||
return GasBudget{
|
||||
RegularGas: 0,
|
||||
StateGas: g.StateGas,
|
||||
UsedRegularGas: g.UsedRegularGas + g.RegularGas,
|
||||
UsedStateGas: g.UsedStateGas,
|
||||
}
|
||||
}
|
||||
|
||||
// Exit dispatches on err to the appropriate exit-form constructor
|
||||
// for the post-evm.Run path:
|
||||
//
|
||||
|
|
@ -261,33 +276,24 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
|
|||
//
|
||||
// Soft validation failures (occurring BEFORE evm.Run) should call Preserved
|
||||
// directly instead of going through this dispatcher.
|
||||
func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget {
|
||||
func (g GasBudget) Exit(err error) GasBudget {
|
||||
switch {
|
||||
case err == nil:
|
||||
return g.ExitSuccess()
|
||||
case err == ErrExecutionReverted:
|
||||
return g.ExitRevert()
|
||||
default:
|
||||
return g.ExitHalt(initStateReservoir)
|
||||
return g.ExitHalt()
|
||||
}
|
||||
}
|
||||
|
||||
// Absorb merges a sub-call's leftover GasBudget into this (caller's) running
|
||||
// budget. Additionally, it does an EIP-8037 spillover correction:
|
||||
// state-gas that spilled into the regular pool inside the child frame is
|
||||
// excluded from the UsedRegularGas.
|
||||
//
|
||||
// spillover = forwarded - child.RegularGas - child.UsedRegularGas
|
||||
//
|
||||
// forwarded is the regular-gas amount that was passed to the child at call
|
||||
// entry (i.e., the regular initial of the child's GasBudget).
|
||||
func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) {
|
||||
spillover := forwarded - child.RegularGas - child.UsedRegularGas
|
||||
|
||||
g.UsedRegularGas -= child.RegularGas
|
||||
// budget. Under the updated EIP-8037, state-gas no longer spills into the
|
||||
// child's burned regular gas on halt, so the child's UsedRegularGas can be
|
||||
// folded in directly without a spillover correction.
|
||||
func (g *GasBudget) Absorb(child GasBudget) {
|
||||
g.RegularGas += child.RegularGas
|
||||
g.UsedRegularGas += child.UsedRegularGas
|
||||
g.StateGas = child.StateGas
|
||||
g.UsedStateGas += child.UsedStateGas
|
||||
|
||||
g.UsedRegularGas -= spillover
|
||||
}
|
||||
|
|
|
|||
|
|
@ -661,7 +661,14 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
scope.Stack.push(&stackvalue)
|
||||
|
||||
// Refund the leftover gas back to current frame
|
||||
scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
// EIP-8037: no account was created on any failure path, so refund the
|
||||
// account-creation state gas charged before the opcode ran (gasCreateEip8037)
|
||||
// back to the reservoir.
|
||||
if evm.chainRules.IsAmsterdam && suberr != nil {
|
||||
scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte)
|
||||
}
|
||||
|
||||
if suberr == ErrExecutionReverted {
|
||||
evm.returnData = res // set REVERT data to return data buffer
|
||||
|
|
@ -695,7 +702,14 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
scope.Stack.push(&stackvalue)
|
||||
|
||||
// Refund the leftover gas back to current frame
|
||||
scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
// EIP-8037: no account was created on any failure path, so refund the
|
||||
// account-creation state gas charged before the opcode ran (gasCreate2Eip8037)
|
||||
// back to the reservoir.
|
||||
if evm.chainRules.IsAmsterdam && suberr != nil {
|
||||
scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte)
|
||||
}
|
||||
|
||||
if suberr == ErrExecutionReverted {
|
||||
evm.returnData = res // set REVERT data to return data buffer
|
||||
|
|
@ -740,7 +754,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
if err == nil || err == ErrExecutionReverted {
|
||||
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
|
||||
}
|
||||
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
evm.returnData = ret
|
||||
return ret, nil
|
||||
|
|
@ -776,7 +790,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
|
||||
}
|
||||
|
||||
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
evm.returnData = ret
|
||||
return ret, nil
|
||||
|
|
@ -808,7 +822,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
if err == nil || err == ErrExecutionReverted {
|
||||
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
|
||||
}
|
||||
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
evm.returnData = ret
|
||||
return ret, nil
|
||||
|
|
@ -841,7 +855,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
|
||||
}
|
||||
|
||||
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
|
||||
|
||||
evm.returnData = ret
|
||||
return ret, nil
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
|
|||
slot := common.Hash(loc.Bytes32())
|
||||
// Check slot presence in the access list
|
||||
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
|
||||
// EIP-8279: a cold SLOAD adds the storage key to the BAL. Extend the
|
||||
// floor before the slot is recorded; an out-of-gas here aborts the
|
||||
// opcode before the unpaid BAL byte exists.
|
||||
if err := evm.extendFloor(params.BALBytesPerStorageKey); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
// If the caller cannot afford the cost, this change will be rolled back
|
||||
// If he does afford it, we can skip checking the same thing later on, during execution
|
||||
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
|
||||
|
|
@ -126,6 +132,10 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo
|
|||
addr := common.Address(stack.peek().Bytes20())
|
||||
// Check slot presence in the access list
|
||||
if !evm.StateDB.AddressInAccessList(addr) {
|
||||
// EIP-8279: a cold account access adds the address to the BAL.
|
||||
if err := evm.extendFloor(params.BALBytesPerAddress); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
evm.StateDB.AddAddressToAccessList(addr)
|
||||
var overflow bool
|
||||
// We charge (cold-warm), since 'warm' is already charged as constantGas
|
||||
|
|
@ -148,6 +158,10 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem
|
|||
addr := common.Address(stack.peek().Bytes20())
|
||||
// Check slot presence in the access list
|
||||
if !evm.StateDB.AddressInAccessList(addr) {
|
||||
// EIP-8279: a cold account access adds the address to the BAL.
|
||||
if err := evm.extendFloor(params.BALBytesPerAddress); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
// If the caller cannot afford the cost, this change will be rolled back
|
||||
evm.StateDB.AddAddressToAccessList(addr)
|
||||
// The warm storage read cost is already charged as constantGas
|
||||
|
|
@ -165,6 +179,10 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
|
|||
// the cost to charge for cold access, if any, is Cold - Warm
|
||||
coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
|
||||
if !warmAccess {
|
||||
// EIP-8279: a cold account access adds the address to the BAL.
|
||||
if err := evm.extendFloor(params.BALBytesPerAddress); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
evm.StateDB.AddAddressToAccessList(addr)
|
||||
// Charge the remaining difference here already, to correctly calculate available
|
||||
// gas for call
|
||||
|
|
@ -286,6 +304,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
|
|||
// Perform EIP-2929 checks (stateless), checking address presence
|
||||
// in the accessList and charge the cold access accordingly.
|
||||
if !evm.StateDB.AddressInAccessList(addr) {
|
||||
// EIP-8279: a cold account access adds the address to the BAL.
|
||||
if err := evm.extendFloor(params.BALBytesPerAddress); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
evm.StateDB.AddAddressToAccessList(addr)
|
||||
|
||||
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form
|
||||
|
|
@ -321,6 +343,11 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
|
|||
if evm.StateDB.AddressInAccessList(target) {
|
||||
eip7702Cost = params.WarmStorageReadCostEIP2929
|
||||
} else {
|
||||
// EIP-8279: resolving a cold delegation target adds its address
|
||||
// to the BAL.
|
||||
if err := evm.extendFloor(params.BALBytesPerAddress); err != nil {
|
||||
return GasCosts{}, err
|
||||
}
|
||||
evm.StateDB.AddAddressToAccessList(target)
|
||||
eip7702Cost = params.ColdAccountAccessCostEIP2929
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,6 +291,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
|
|||
}
|
||||
options.Overrides = &overrides
|
||||
|
||||
options.BALExecutionMode = config.BALExecutionMode
|
||||
options.BlockingPrefetch = config.BlockingPrefetch
|
||||
options.PrefetchWorkers = int(config.PrefetchWorkers)
|
||||
|
||||
eth.blockchain, err = core.NewBlockChain(chainDb, config.Genesis, eth.engine, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -804,10 +804,12 @@ func (api *ConsensusAPI) NewPayloadV5(ctx context.Context, params engine.Executa
|
|||
return invalidStatus, paramsErr("nil beaconRoot post-cancun")
|
||||
case executionRequests == nil:
|
||||
return invalidStatus, paramsErr("nil executionRequests post-prague")
|
||||
case params.SlotNumber == nil:
|
||||
return invalidStatus, paramsErr("nil slotnumber post-amsterdam")
|
||||
case !api.checkFork(params.Timestamp, forks.Amsterdam):
|
||||
return invalidStatus, unsupportedForkErr("newPayloadV5 must only be called for amsterdam payloads")
|
||||
case params.SlotNumber == nil:
|
||||
return invalidStatus, paramsErr("nil slotnumber post-amsterdam")
|
||||
case params.BlockAccessList == nil:
|
||||
return invalidStatus, paramsErr("nil block access list post-amsterdam")
|
||||
}
|
||||
requests := convertRequests(executionRequests)
|
||||
if err := validateRequests(requests); err != nil {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package ethconfig
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
|
@ -221,6 +222,10 @@ type Config struct {
|
|||
|
||||
// RangeLimit restricts the maximum range (end - start) for range queries.
|
||||
RangeLimit uint64 `toml:",omitempty"`
|
||||
|
||||
BALExecutionMode bal.BALExecutionMode
|
||||
PrefetchWorkers uint
|
||||
BlockingPrefetch bool
|
||||
}
|
||||
|
||||
// CreateConsensusEngine creates a consensus engine for the given chain config.
|
||||
|
|
|
|||
|
|
@ -103,6 +103,28 @@ const (
|
|||
TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702
|
||||
TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037
|
||||
|
||||
// FloorCostPerAuth is the per-authorization tx-content floor contribution
|
||||
// defined by EIP-8131: an EIP-7702 authorization tuple is 101 bytes, charged
|
||||
// at the floor rate (101 * TxCostFloorPerToken7976 * TxTokenPerNonZeroByte).
|
||||
FloorCostPerAuth uint64 = 101 * TxCostFloorPerToken7976 * TxTokenPerNonZeroByte // 6464
|
||||
|
||||
// EIP-8279: Block Access List Byte Floor. Each byte an opcode adds to the
|
||||
// EIP-7928 Block Access List extends the transaction's floor accumulator by
|
||||
// FloorGasPerByte gas, charged at runtime before the BAL grows.
|
||||
FloorGasPerByte uint64 = TxCostFloorPerToken7976 * TxTokenPerNonZeroByte // 64: per-byte floor rate (EIP-7976)
|
||||
BALBytesPerAddress uint64 = 20 // BAL bytes for an account address
|
||||
BALBytesPerStorageKey uint64 = 32 // BAL bytes for a storage key
|
||||
BALBytesPerStorageValue uint64 = 32 // BAL bytes for a storage post-value
|
||||
BALBytesPerBalance uint64 = 32 // BAL bytes for a balance change
|
||||
BALBytesPerNonce uint64 = 8 // BAL bytes for a nonce change
|
||||
BALDelegationCodeBytes uint64 = 23 // EIP-7702 delegation marker length
|
||||
// BALBytesPerAuthorization is the worst-case BAL contribution an EIP-7702
|
||||
// authorization adds when it is applied: the authority address, the
|
||||
// delegation marker written to its code, and its nonce change. It is folded
|
||||
// into the static floor seed since set_delegation runs outside the EVM's
|
||||
// out-of-gas handler.
|
||||
BALBytesPerAuthorization uint64 = BALBytesPerAddress + BALDelegationCodeBytes + BALBytesPerNonce // 51
|
||||
|
||||
// These have been changed during the course of the chain
|
||||
CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction.
|
||||
CallGasEIP150 uint64 = 700 // Static portion of gas for CALL-derivates after EIP 150 (Tangerine)
|
||||
|
|
|
|||
|
|
@ -82,8 +82,17 @@ func TestBlockchain(t *testing.T) {
|
|||
|
||||
// TestExecutionSpecBlocktests runs the test fixtures from execution-spec-tests.
|
||||
func TestExecutionSpecBlocktests(t *testing.T) {
|
||||
if !common.FileExist(executionSpecBlockchainTestDir) {
|
||||
t.Skipf("directory %s does not exist", executionSpecBlockchainTestDir)
|
||||
testExecutionSpecBlocktests(t, executionSpecBlockchainTestDir)
|
||||
}
|
||||
|
||||
// TestExecutionSpecBlocktestsBAL runs the BAL release test fixtures from execution-spec-tests.
|
||||
func TestExecutionSpecBlocktestsBAL(t *testing.T) {
|
||||
testExecutionSpecBlocktests(t, executionSpecBALBlockchainTestDir)
|
||||
}
|
||||
|
||||
func testExecutionSpecBlocktests(t *testing.T, testDir string) {
|
||||
if !common.FileExist(testDir) {
|
||||
t.Skipf("directory %s does not exist", testDir)
|
||||
}
|
||||
bt := new(testMatcher)
|
||||
|
||||
|
|
@ -97,7 +106,7 @@ func TestExecutionSpecBlocktests(t *testing.T) {
|
|||
bt.skipLoad(`dynamicAccountOverwriteEmpty_Paris`)
|
||||
bt.skipLoad(`create2collisionStorageParis`)
|
||||
|
||||
bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) {
|
||||
bt.walk(t, testDir, func(t *testing.T, name string, test *BlockTest) {
|
||||
execBlockTest(t, bt, test)
|
||||
})
|
||||
}
|
||||
|
|
@ -118,7 +127,7 @@ func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) {
|
|||
}
|
||||
for _, snapshot := range snapshotConf {
|
||||
for _, dbscheme := range dbschemeConf {
|
||||
if err := bt.checkFailure(t, test.Run(snapshot, dbscheme, true, nil, nil)); err != nil {
|
||||
if err := bt.checkFailure(t, test.Run(snapshot, dbscheme, true, true, nil, nil)); err != nil {
|
||||
t.Errorf("test with config {snapshotter:%v, scheme:%v} failed: %v", snapshot, dbscheme, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,27 +113,20 @@ type btHeaderMarshaling struct {
|
|||
SlotNumber *math.HexOrDecimal64
|
||||
}
|
||||
|
||||
func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) {
|
||||
config, ok := Forks[t.json.Network]
|
||||
if !ok {
|
||||
return UnsupportedForkError{t.json.Network}
|
||||
}
|
||||
|
||||
func (t *BlockTest) createTestBlockChain(config *params.ChainConfig, snapshotter bool, scheme string, witness, createAndVerifyBAL bool, tracer *tracing.Hooks) (*core.BlockChain, error) {
|
||||
// import pre accounts & construct test genesis block & state root
|
||||
// Commit genesis state
|
||||
var (
|
||||
gspec = t.genesis(config)
|
||||
db = rawdb.NewMemoryDatabase()
|
||||
tconf = &triedb.Config{
|
||||
Preimages: true,
|
||||
IsUBT: gspec.Config.UBTTime != nil && *gspec.Config.UBTTime <= gspec.Timestamp,
|
||||
}
|
||||
)
|
||||
if scheme == rawdb.PathScheme || tconf.IsUBT {
|
||||
if scheme == rawdb.PathScheme {
|
||||
tconf.PathDB = pathdb.Defaults
|
||||
} else {
|
||||
tconf.HashDB = hashdb.Defaults
|
||||
}
|
||||
gspec := t.genesis(config)
|
||||
|
||||
// if ttd is not specified, set an arbitrary huge value
|
||||
if gspec.Config.TerminalTotalDifficulty == nil {
|
||||
|
|
@ -142,15 +135,15 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t
|
|||
triedb := triedb.NewDatabase(db, tconf)
|
||||
gblock, err := gspec.Commit(db, triedb, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
triedb.Close() // close the db to prevent memory leak
|
||||
|
||||
if gblock.Hash() != t.json.Genesis.Hash {
|
||||
return fmt.Errorf("genesis block hash doesn't match test: computed=%x, test=%x", gblock.Hash().Bytes()[:6], t.json.Genesis.Hash[:6])
|
||||
return nil, fmt.Errorf("genesis block hash doesn't match test: computed=%x, test=%x", gblock.Hash().Bytes()[:6], t.json.Genesis.Hash[:6])
|
||||
}
|
||||
if gblock.Root() != t.json.Genesis.StateRoot {
|
||||
return fmt.Errorf("genesis block state root does not match test: computed=%x, test=%x", gblock.Root().Bytes()[:6], t.json.Genesis.StateRoot[:6])
|
||||
return nil, fmt.Errorf("genesis block state root does not match test: computed=%x, test=%x", gblock.Root().Bytes()[:6], t.json.Genesis.StateRoot[:6])
|
||||
}
|
||||
// Wrap the original engine within the beacon-engine
|
||||
engine := beacon.New(ethash.NewFaker())
|
||||
|
|
@ -164,12 +157,28 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t
|
|||
Tracer: tracer,
|
||||
},
|
||||
StatelessSelfValidation: witness,
|
||||
NoPrefetch: true,
|
||||
BlockingPrefetch: true,
|
||||
PrefetchWorkers: 100, // note: this is totally unrelated to NoPrefetch, just for BAL execution
|
||||
}
|
||||
if snapshotter {
|
||||
options.SnapshotLimit = 1
|
||||
options.SnapshotWait = true
|
||||
}
|
||||
chain, err := core.NewBlockChain(db, gspec, engine, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return chain, nil
|
||||
}
|
||||
|
||||
func (t *BlockTest) Run(snapshotter bool, scheme string, witness, createAndVerifyBAL bool, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) {
|
||||
config, ok := Forks[t.json.Network]
|
||||
if !ok {
|
||||
return UnsupportedForkError{t.json.Network}
|
||||
}
|
||||
|
||||
chain, err := t.createTestBlockChain(config, snapshotter, scheme, witness, createAndVerifyBAL, tracer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -203,7 +212,50 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t
|
|||
}
|
||||
}
|
||||
}
|
||||
return t.validateImportedHeaders(chain, validBlocks)
|
||||
err = t.validateImportedHeaders(chain, validBlocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if createAndVerifyBAL {
|
||||
newChain, _ := t.createTestBlockChain(config, snapshotter, scheme, witness, createAndVerifyBAL, tracer)
|
||||
defer newChain.Stop()
|
||||
|
||||
var blocksWithBAL types.Blocks
|
||||
for i := uint64(1); i <= chain.CurrentBlock().Number.Uint64(); i++ {
|
||||
block := chain.GetBlockByNumber(i)
|
||||
if chain.Config().IsAmsterdam(block.Number(), block.Time()) && block.AccessList() == nil {
|
||||
return fmt.Errorf("block %d missing BAL", block.NumberU64())
|
||||
}
|
||||
blocksWithBAL = append(blocksWithBAL, block)
|
||||
}
|
||||
|
||||
amt, err := newChain.InsertChain(blocksWithBAL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = amt
|
||||
newDB, err := newChain.State()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = t.validatePostState(newDB); err != nil {
|
||||
return fmt.Errorf("post state validation failed: %v", err)
|
||||
}
|
||||
// Cross-check the snapshot-to-hash against the trie hash
|
||||
if snapshotter {
|
||||
if newChain.Snapshots() != nil {
|
||||
if err := chain.Snapshots().Verify(chain.CurrentBlock().Root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
err = t.validateImportedHeaders(newChain, validBlocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Network returns the network/fork name for this test.
|
||||
|
|
|
|||
|
|
@ -41,9 +41,10 @@ var (
|
|||
transactionTestDir = filepath.Join(baseDir, "TransactionTests")
|
||||
rlpTestDir = filepath.Join(baseDir, "RLPTests")
|
||||
difficultyTestDir = filepath.Join(baseDir, "BasicTests")
|
||||
executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests")
|
||||
executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests")
|
||||
executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests")
|
||||
executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests")
|
||||
executionSpecBALBlockchainTestDir = filepath.Join(".", "spec-tests-bal", "fixtures", "blockchain_tests")
|
||||
executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests")
|
||||
executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests")
|
||||
benchmarksDir = filepath.Join(".", "evm-benchmarks", "benchmarks")
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func (tt *TransactionTest) Run() error {
|
|||
|
||||
if rules.IsPrague {
|
||||
var floorDataGas uint64
|
||||
floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList())
|
||||
floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations())))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -451,3 +451,11 @@ func (t *BinaryTrie) PrefetchStorage(addr common.Address, keys [][]byte) error {
|
|||
func (t *BinaryTrie) Witness() map[string][]byte {
|
||||
return t.tracer.Values()
|
||||
}
|
||||
|
||||
func (t *BinaryTrie) UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (t *BinaryTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, _ []int) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,6 +210,29 @@ func (t *StateTrie) UpdateStorage(_ common.Address, key, value []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateStorageBatch attempts to update a list storages in the batch manner.
|
||||
func (t *StateTrie) UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error {
|
||||
var (
|
||||
hkeys = make([][]byte, 0, len(keys))
|
||||
evals = make([][]byte, 0, len(values))
|
||||
)
|
||||
for _, key := range keys {
|
||||
hk := crypto.Keccak256(key)
|
||||
if t.preimages != nil {
|
||||
t.secKeyCache[common.Hash(hk)] = key
|
||||
}
|
||||
hkeys = append(hkeys, hk)
|
||||
}
|
||||
for _, val := range values {
|
||||
data, err := rlp.EncodeToBytes(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
evals = append(evals, data)
|
||||
}
|
||||
return t.trie.UpdateBatch(hkeys, evals)
|
||||
}
|
||||
|
||||
// UpdateAccount will abstract the write of an account to the secure trie.
|
||||
func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccount, _ int) error {
|
||||
hk := crypto.Keccak256(address.Bytes())
|
||||
|
|
@ -226,6 +249,29 @@ func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccoun
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateAccountBatch attempts to update a list accounts in the batch manner.
|
||||
func (t *StateTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, _ []int) error {
|
||||
var (
|
||||
hkeys = make([][]byte, 0, len(addresses))
|
||||
values = make([][]byte, 0, len(accounts))
|
||||
)
|
||||
for _, addr := range addresses {
|
||||
hk := crypto.Keccak256(addr.Bytes())
|
||||
if t.preimages != nil {
|
||||
t.secKeyCache[common.Hash(hk)] = addr.Bytes()
|
||||
}
|
||||
hkeys = append(hkeys, hk)
|
||||
}
|
||||
for _, acc := range accounts {
|
||||
data, err := rlp.EncodeToBytes(acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values = append(values, data)
|
||||
}
|
||||
return t.trie.UpdateBatch(hkeys, values)
|
||||
}
|
||||
|
||||
func (t *StateTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,12 +33,10 @@ import (
|
|||
// while the latter is inserted/deleted in order to follow the rule of trie.
|
||||
// This tool can track all of them no matter the node is embedded in its
|
||||
// parent or not, but valueNode is never tracked.
|
||||
//
|
||||
// Note opTracer is not thread-safe, callers should be responsible for handling
|
||||
// the concurrency issues by themselves.
|
||||
type opTracer struct {
|
||||
inserts map[string]struct{}
|
||||
deletes map[string]struct{}
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// newOpTracer initializes the tracer for capturing trie changes.
|
||||
|
|
@ -53,6 +51,9 @@ func newOpTracer() *opTracer {
|
|||
// in the deletion set (resurrected node), then just wipe it from
|
||||
// the deletion set as it's "untouched".
|
||||
func (t *opTracer) onInsert(path []byte) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
if _, present := t.deletes[string(path)]; present {
|
||||
delete(t.deletes, string(path))
|
||||
return
|
||||
|
|
@ -64,6 +65,9 @@ func (t *opTracer) onInsert(path []byte) {
|
|||
// in the addition set, then just wipe it from the addition set
|
||||
// as it's untouched.
|
||||
func (t *opTracer) onDelete(path []byte) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
if _, present := t.inserts[string(path)]; present {
|
||||
delete(t.inserts, string(path))
|
||||
return
|
||||
|
|
@ -73,12 +77,18 @@ func (t *opTracer) onDelete(path []byte) {
|
|||
|
||||
// reset clears the content tracked by tracer.
|
||||
func (t *opTracer) reset() {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
clear(t.inserts)
|
||||
clear(t.deletes)
|
||||
}
|
||||
|
||||
// copy returns a deep copied tracer instance.
|
||||
func (t *opTracer) copy() *opTracer {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
return &opTracer{
|
||||
inserts: maps.Clone(t.inserts),
|
||||
deletes: maps.Clone(t.deletes),
|
||||
|
|
@ -87,6 +97,9 @@ func (t *opTracer) copy() *opTracer {
|
|||
|
||||
// deletedList returns a list of node paths which are deleted from the trie.
|
||||
func (t *opTracer) deletedList() [][]byte {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
paths := make([][]byte, 0, len(t.deletes))
|
||||
for path := range t.deletes {
|
||||
paths = append(paths, []byte(path))
|
||||
|
|
|
|||
|
|
@ -144,6 +144,19 @@ func (t *TransitionTrie) UpdateStorage(address common.Address, key []byte, value
|
|||
return t.overlay.UpdateStorage(address, key, v)
|
||||
}
|
||||
|
||||
// UpdateStorageBatch attempts to update a list storages in the batch manner.
|
||||
func (t *TransitionTrie) UpdateStorageBatch(address common.Address, keys [][]byte, values [][]byte) error {
|
||||
if len(keys) != len(values) {
|
||||
return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values))
|
||||
}
|
||||
for i, key := range keys {
|
||||
if err := t.UpdateStorage(address, key, values[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAccount abstract an account write to the trie.
|
||||
func (t *TransitionTrie) UpdateAccount(addr common.Address, account *types.StateAccount, codeLen int) error {
|
||||
// NOTE: before the rebase, this was saving the state root, so that OpenStorageTrie
|
||||
|
|
@ -152,6 +165,22 @@ func (t *TransitionTrie) UpdateAccount(addr common.Address, account *types.State
|
|||
return t.overlay.UpdateAccount(addr, account, codeLen)
|
||||
}
|
||||
|
||||
// UpdateAccountBatch attempts to update a list accounts in the batch manner.
|
||||
func (t *TransitionTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLens []int) error {
|
||||
if len(addresses) != len(accounts) {
|
||||
return fmt.Errorf("address and accounts length mismatch: %d != %d", len(addresses), len(accounts))
|
||||
}
|
||||
if len(addresses) != len(codeLens) {
|
||||
return fmt.Errorf("address and code length mismatch: %d != %d", len(addresses), len(codeLens))
|
||||
}
|
||||
for i, addr := range addresses {
|
||||
if err := t.UpdateAccount(addr, accounts[i], codeLens[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteStorage removes any existing value for key from the trie. If a node was not
|
||||
// found in the database, a trie.MissingNodeError is returned.
|
||||
func (t *TransitionTrie) DeleteStorage(addr common.Address, key []byte) error {
|
||||
|
|
|
|||
63
trie/trie.go
63
trie/trie.go
|
|
@ -480,6 +480,69 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
|
|||
}
|
||||
}
|
||||
|
||||
// UpdateBatch updates a batch of entries concurrently.
|
||||
func (t *Trie) UpdateBatch(keys [][]byte, values [][]byte) error {
|
||||
// Short circuit if the trie is already committed and unusable.
|
||||
if t.committed {
|
||||
return ErrCommitted
|
||||
}
|
||||
if len(keys) != len(values) {
|
||||
return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values))
|
||||
}
|
||||
// Insert the entries sequentially if there are not too many
|
||||
// trie nodes in the trie.
|
||||
fn, ok := t.root.(*fullNode)
|
||||
if !ok || len(keys) < 4 { // TODO(rjl493456442) the parallelism threshold should be twisted
|
||||
for i, key := range keys {
|
||||
err := t.Update(key, values[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
ikeys = make(map[byte][][]byte)
|
||||
ivals = make(map[byte][][]byte)
|
||||
eg errgroup.Group
|
||||
)
|
||||
for i, key := range keys {
|
||||
hkey := keybytesToHex(key)
|
||||
ikeys[hkey[0]] = append(ikeys[hkey[0]], hkey)
|
||||
ivals[hkey[0]] = append(ivals[hkey[0]], values[i])
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
fn.flags = t.newFlag()
|
||||
}
|
||||
for pos, ks := range ikeys {
|
||||
eg.Go(func() error {
|
||||
vs := ivals[pos]
|
||||
for i, k := range ks {
|
||||
if len(vs[i]) != 0 {
|
||||
_, n, err := t.insert(fn.Children[pos], []byte{pos}, k[1:], valueNode(vs[i]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn.Children[pos] = n
|
||||
} else {
|
||||
_, n, err := t.delete(fn.Children[pos], []byte{pos}, k[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn.Children[pos] = n
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
t.unhashed += len(keys)
|
||||
t.uncommitted += len(keys)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustDelete is a wrapper of Delete and will omit any encountered error but
|
||||
// just print out an error message.
|
||||
func (t *Trie) MustDelete(key []byte) {
|
||||
|
|
|
|||
|
|
@ -1580,3 +1580,57 @@ func BenchmarkTrieSeqPrefetch(b *testing.B) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateBatch(t *testing.T) {
|
||||
testUpdateBatch(t, []kv{
|
||||
{k: []byte("do"), v: []byte("verb")},
|
||||
{k: []byte("ether"), v: []byte("wookiedoo")},
|
||||
{k: []byte("horse"), v: []byte("stallion")},
|
||||
{k: []byte("shaman"), v: []byte("horse")},
|
||||
{k: []byte("doge"), v: []byte("coin")},
|
||||
{k: []byte("dog"), v: []byte("puppy")},
|
||||
})
|
||||
|
||||
var entries []kv
|
||||
for i := 0; i < 256; i++ {
|
||||
entries = append(entries, kv{k: testrand.Bytes(32), v: testrand.Bytes(32)})
|
||||
}
|
||||
testUpdateBatch(t, entries)
|
||||
}
|
||||
|
||||
func testUpdateBatch(t *testing.T, entries []kv) {
|
||||
var (
|
||||
base = NewEmpty(nil)
|
||||
keys [][]byte
|
||||
vals [][]byte
|
||||
)
|
||||
for _, entry := range entries {
|
||||
base.Update(entry.k, entry.v)
|
||||
keys = append(keys, entry.k)
|
||||
vals = append(vals, entry.v)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
k, v := testrand.Bytes(32), testrand.Bytes(32)
|
||||
base.Update(k, v)
|
||||
keys = append(keys, k)
|
||||
vals = append(vals, v)
|
||||
}
|
||||
|
||||
cmp := NewEmpty(nil)
|
||||
if err := cmp.UpdateBatch(keys, vals); err != nil {
|
||||
t.Fatalf("Failed to update batch, %v", err)
|
||||
}
|
||||
|
||||
// Traverse the original tree, the changes made on the copy one shouldn't
|
||||
// affect the old one
|
||||
for _, key := range keys {
|
||||
v1, _ := base.Get(key)
|
||||
v2, _ := cmp.Get(key)
|
||||
if !bytes.Equal(v1, v2) {
|
||||
t.Errorf("Unexpected data, key: %v, want: %v, got: %v", key, v1, v2)
|
||||
}
|
||||
}
|
||||
if base.Hash() != cmp.Hash() {
|
||||
t.Errorf("Hash mismatch: want %x, got %x", base.Hash(), cmp.Hash())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue