diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go index e677e2bb21..7d7109f27e 100644 --- a/ethclient/gethclient/gethclient.go +++ b/ethclient/gethclient/gethclient.go @@ -171,6 +171,31 @@ func (ec *Client) CallContractWithBlockOverrides(ctx context.Context, msg ethere return hex, err } +// CallContractWithTransientOverrides executes a message call transaction, which is directly executed +// in the VM of the node, but never mined into the blockchain. +// +// blockNumber selects the block height at which the call runs. It can be nil, in which +// case the code is taken from the latest known block. Note that state from very old +// blocks might not be available. +// +// overrides specifies a map of contract states that should be overwritten before executing +// the message call. +// +// blockOverrides specifies block fields exposed to the EVM that can be overridden for the call. +// +// transientOverrides specifies transient storage slots that should be overwritten before executing +// the message call. Transient storage is reset at the beginning of each transaction. +// +// Please use ethclient.CallContract instead if you don't need the override functionality. +func (ec *Client) CallContractWithTransientOverrides(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int, overrides *map[common.Address]OverrideAccount, blockOverrides *BlockOverrides, transientOverrides *TransientOverrides) ([]byte, error) { + var hex hexutil.Bytes + err := ec.c.CallContext( + ctx, &hex, "eth_call", toCallArg(msg), + toBlockNumArg(blockNumber), overrides, blockOverrides, transientOverrides, + ) + return hex, err +} + // GCStats retrieves the current garbage collection stats from a geth node. func (ec *Client) GCStats(ctx context.Context) (*debug.GCStats, error) { var result debug.GCStats @@ -407,3 +432,7 @@ type OverrideAccount = ethereum.OverrideAccount // BlockOverrides is an alias for ethereum.BlockOverrides. type BlockOverrides = ethereum.BlockOverrides + +// TransientOverrides specifies transient storage slots to override for eth_call. +// The map key is the contract address, and the value is another map of slot to value. +type TransientOverrides map[common.Address]map[common.Hash]common.Hash diff --git a/graphql/graphql.go b/graphql/graphql.go index f25bfd127a..d6285fc132 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -1207,7 +1207,7 @@ func (c *CallResult) Status() hexutil.Uint64 { func (b *Block) Call(ctx context.Context, args struct { Data ethapi.TransactionArgs }) (*CallResult, error) { - result, err := ethapi.DoCall(ctx, b.r.backend, args.Data, *b.numberOrHash, nil, nil, b.r.backend.RPCEVMTimeout(), b.r.backend.RPCGasCap()) + result, err := ethapi.DoCall(ctx, b.r.backend, args.Data, *b.numberOrHash, nil, nil, nil, b.r.backend.RPCEVMTimeout(), b.r.backend.RPCGasCap()) if err != nil { return nil, err } @@ -1270,7 +1270,7 @@ func (p *Pending) Call(ctx context.Context, args struct { Data ethapi.TransactionArgs }) (*CallResult, error) { pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - result, err := ethapi.DoCall(ctx, p.r.backend, args.Data, pendingBlockNr, nil, nil, p.r.backend.RPCEVMTimeout(), p.r.backend.RPCGasCap()) + result, err := ethapi.DoCall(ctx, p.r.backend, args.Data, pendingBlockNr, nil, nil, nil, p.r.backend.RPCEVMTimeout(), p.r.backend.RPCGasCap()) if err != nil { return nil, err } diff --git a/interfaces.go b/interfaces.go index 345f59b094..21d42c6d34 100644 --- a/interfaces.go +++ b/interfaces.go @@ -317,26 +317,21 @@ type OverrideAccount struct { // StateDiff allows overriding individual storage slots. StateDiff map[common.Hash]common.Hash - - // TransientStorage allows overriding transient storage slots. - TransientStorage map[common.Hash]common.Hash } func (a OverrideAccount) MarshalJSON() ([]byte, error) { type acc struct { - Nonce hexutil.Uint64 `json:"nonce,omitempty"` - Code string `json:"code,omitempty"` - Balance *hexutil.Big `json:"balance,omitempty"` - State interface{} `json:"state,omitempty"` - StateDiff map[common.Hash]common.Hash `json:"stateDiff,omitempty"` - TransientStorage map[common.Hash]common.Hash `json:"transientStorage,omitempty"` + Nonce hexutil.Uint64 `json:"nonce,omitempty"` + Code string `json:"code,omitempty"` + Balance *hexutil.Big `json:"balance,omitempty"` + State interface{} `json:"state,omitempty"` + StateDiff map[common.Hash]common.Hash `json:"stateDiff,omitempty"` } output := acc{ - Nonce: hexutil.Uint64(a.Nonce), - Balance: (*hexutil.Big)(a.Balance), - StateDiff: a.StateDiff, - TransientStorage: a.TransientStorage, + Nonce: hexutil.Uint64(a.Nonce), + Balance: (*hexutil.Big)(a.Balance), + StateDiff: a.StateDiff, } if a.Code != nil { output.Code = hexutil.Encode(a.Code) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 4f217d0578..24f61e9ec5 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -727,7 +727,7 @@ func (context *ChainContext) GetHeaderByHash(hash common.Hash) *types.Header { return header } -func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, overrides *override.StateOverride, blockOverrides *override.BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { +func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, overrides *override.StateOverride, blockOverrides *override.BlockOverrides, transientOverrides *override.TransientStorageOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil) if blockOverrides != nil { if err := blockOverrides.Apply(&blockCtx); err != nil { @@ -740,6 +740,9 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S return nil, err } + // Apply transient storage overrides. These will be applied after Prepare() is called. + transientOverrides.Apply(state) + // Setup context so it may be cancelled the call has completed // or, in case of unmetered gas, setup a context with a timeout. var cancel context.CancelFunc @@ -807,14 +810,14 @@ func applyMessageWithEVM(ctx context.Context, evm *vm.EVM, msg *core.Message, ti return result, nil } -func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *override.StateOverride, blockOverrides *override.BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { +func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *override.StateOverride, blockOverrides *override.BlockOverrides, transientOverrides *override.TransientStorageOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return nil, err } - return doCall(ctx, b, args, state, header, overrides, blockOverrides, timeout, globalGasCap) + return doCall(ctx, b, args, state, header, overrides, blockOverrides, transientOverrides, timeout, globalGasCap) } // Call executes the given transaction on the state for the given block number. @@ -823,12 +826,12 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash // // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. -func (api *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *override.StateOverride, blockOverrides *override.BlockOverrides) (hexutil.Bytes, error) { +func (api *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *override.StateOverride, blockOverrides *override.BlockOverrides, transientOverrides *override.TransientStorageOverride) (hexutil.Bytes, error) { if blockNrOrHash == nil { latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) blockNrOrHash = &latest } - result, err := DoCall(ctx, api.b, args, *blockNrOrHash, overrides, blockOverrides, api.b.RPCEVMTimeout(), api.b.RPCGasCap()) + result, err := DoCall(ctx, api.b, args, *blockNrOrHash, overrides, blockOverrides, transientOverrides, api.b.RPCEVMTimeout(), api.b.RPCGasCap()) if err != nil { return nil, err } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 1f39d4047b..a9dcc9a3ec 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -1032,13 +1032,14 @@ func TestCall(t *testing.T) { })) randomAccounts := newAccounts(3) var testSuite = []struct { - name string - blockNumber rpc.BlockNumber - overrides override.StateOverride - call TransactionArgs - blockOverrides override.BlockOverrides - expectErr error - want string + name string + blockNumber rpc.BlockNumber + overrides override.StateOverride + call TransactionArgs + blockOverrides override.BlockOverrides + transientOverrides override.TransientStorageOverride + expectErr error + want string }{ // transfer on genesis { @@ -1303,16 +1304,18 @@ func TestCall(t *testing.T) { randomAccounts[2].addr: override.OverrideAccount{ // PUSH1 0x00 TLOAD PUSH1 0x00 MSTORE PUSH1 0x20 PUSH1 0x00 RETURN Code: hex2Bytes("0x60005c60005260206000f3"), - TransientStorage: map[common.Hash]common.Hash{ - common.Hash{}: common.HexToHash("0xabcd"), - }, + }, + }, + transientOverrides: override.TransientStorageOverride{ + randomAccounts[2].addr: map[common.Hash]common.Hash{ + common.Hash{}: common.HexToHash("0xabcd"), }, }, want: "0x000000000000000000000000000000000000000000000000000000000000abcd", }, } for _, tc := range testSuite { - result, err := api.Call(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides, &tc.blockOverrides) + result, err := api.Call(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides, &tc.blockOverrides, &tc.transientOverrides) if tc.expectErr != nil { if err == nil { t.Errorf("test %s: want error %v, have nothing", tc.name, tc.expectErr) diff --git a/internal/ethapi/override/override.go b/internal/ethapi/override/override.go index 21f30f540e..2f288bc244 100644 --- a/internal/ethapi/override/override.go +++ b/internal/ethapi/override/override.go @@ -44,13 +44,15 @@ type OverrideAccount struct { Balance *hexutil.Big `json:"balance"` State map[common.Hash]common.Hash `json:"state"` StateDiff map[common.Hash]common.Hash `json:"stateDiff"` - TransientStorage map[common.Hash]common.Hash `json:"transientStorage"` MovePrecompileTo *common.Address `json:"movePrecompileToAddress"` } // StateOverride is the collection of overridden accounts. type StateOverride map[common.Address]OverrideAccount +// TransientStorageOverride is the collection of transient storage overrides. +type TransientStorageOverride map[common.Address]map[common.Hash]common.Hash + func (diff *StateOverride) has(address common.Address) bool { _, ok := (*diff)[address] return ok @@ -61,22 +63,10 @@ func (diff *StateOverride) Apply(statedb *state.StateDB, precompiles vm.Precompi if diff == nil { return nil } - // Get transient storage overrides to apply after Prepare() - transientOverrides := make(map[common.Address]map[common.Hash]common.Hash) - for addr, account := range *diff { - if account.TransientStorage != nil && len(account.TransientStorage) > 0 { - transientOverrides[addr] = account.TransientStorage - } - } - - // Store transient storage overrides - if len(transientOverrides) > 0 { - statedb.SetPendingTransientOverrides(transientOverrides) - } - // Iterate in deterministic order so error messages and behavior are stable (e.g. for tests). addrs := slices.SortedFunc(maps.Keys(*diff), common.Address.Cmp) + // Tracks destinations of precompiles that were moved. dirtyAddrs := make(map[common.Address]struct{}) for _, addr := range addrs { @@ -139,6 +129,14 @@ func (diff *StateOverride) Apply(statedb *state.StateDB, precompiles vm.Precompi return nil } +// Apply stores transient storage overrides to be applied after Prepare(). +func (diff *TransientStorageOverride) Apply(statedb *state.StateDB) { + if diff == nil || len(*diff) == 0 { + return + } + statedb.SetPendingTransientOverrides(*diff) +} + // BlockOverrides is a set of header fields to override. type BlockOverrides struct { Number *hexutil.Big diff --git a/internal/ethapi/override/override_test.go b/internal/ethapi/override/override_test.go index 7bfe179713..9d1ed898dc 100644 --- a/internal/ethapi/override/override_test.go +++ b/internal/ethapi/override/override_test.go @@ -151,19 +151,15 @@ func TestStateOverrideTransientStorage(t *testing.T) { t.Fatalf("expected initial transient state to be empty, got %s", got.Hex()) } - // Apply override with transient storage - override := StateOverride{ - addr: OverrideAccount{ - TransientStorage: map[common.Hash]common.Hash{ - key1: value1, - key2: value2, - }, + // Apply transient storage override + transientOverride := TransientStorageOverride{ + addr: map[common.Hash]common.Hash{ + key1: value1, + key2: value2, }, } - if err := override.Apply(statedb, nil); err != nil { - t.Fatalf("failed to apply override: %v", err) - } + transientOverride.Apply(statedb) statedb.Prepare(params.Rules{}, common.Address{}, common.Address{}, nil, nil, nil)