Skip to content

feat: support state overrides in eth_call #337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
503 changes: 290 additions & 213 deletions api/cosmos/evm/vm/v1/query.pulsar.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions proto/cosmos/evm/vm/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ message EthCallRequest {
"github.com/cosmos/cosmos-sdk/types.ConsAddress" ];
// chain_id is the eip155 chain id parsed from the requested block header
int64 chain_id = 4;
// state overrides encoded as json
bytes overrides = 5;
}

// EstimateGasResponse defines EstimateGas response
Expand Down
3 changes: 2 additions & 1 deletion rpc/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backend

import (
"context"
"encoding/json"
"fmt"
"math/big"
"time"
Expand Down Expand Up @@ -112,7 +113,7 @@ type EVMBackend interface {
SendRawTransaction(data hexutil.Bytes) (common.Hash, error)
SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error)
EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error)
DoCall(args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber) (*evmtypes.MsgEthereumTxResponse, error)
DoCall(args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber, overrides *json.RawMessage) (*evmtypes.MsgEthereumTxResponse, error)
GasPrice() (*hexutil.Big, error)

// Filter API
Expand Down
13 changes: 11 additions & 2 deletions rpc/backend/call_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,10 @@ func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.Transac
}

// EstimateGas returns an estimate of gas usage for the given smart contract call.
func (b *Backend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) {
func (b *Backend) EstimateGas(
args evmtypes.TransactionArgs,
blockNrOptional *rpctypes.BlockNumber,
) (hexutil.Uint64, error) {
blockNr := rpctypes.EthPendingBlockNumber
if blockNrOptional != nil {
blockNr = *blockNrOptional
Expand Down Expand Up @@ -327,7 +330,7 @@ func (b *Backend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rp
// DoCall performs a simulated call operation through the evmtypes. It returns the
// estimated gas used on the operation or an error if fails.
func (b *Backend) DoCall(
args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber,
args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber, overrides *json.RawMessage,
) (*evmtypes.MsgEthereumTxResponse, error) {
Comment on lines 332 to 334
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: formatting of the arguments - can we make them vertical?

bz, err := json.Marshal(&args)
if err != nil {
Expand All @@ -339,11 +342,17 @@ func (b *Backend) DoCall(
return nil, errors.New("header not found")
}

var bzOverrides []byte
if overrides != nil {
bzOverrides = *overrides
}

req := evmtypes.EthCallRequest{
Args: bz,
GasCap: b.RPCGasCap(),
ProposerAddress: sdk.ConsAddress(header.Header.ProposerAddress),
ChainId: b.EvmChainID.Int64(),
Overrides: bzOverrides,
}

// From ContextWithHeight: if the provided height is 0,
Expand Down
16 changes: 6 additions & 10 deletions rpc/namespaces/ethereum/eth/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package eth

import (
"context"
"fmt"
"encoding/json"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
Expand Down Expand Up @@ -68,7 +68,7 @@ type EthereumAPI interface {
//
// Allows developers to read data from the blockchain which includes executing
// smart contracts. However, no data is published to the Ethereum network.
Call(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, override *rpctypes.StateOverride) (hexutil.Bytes, error)
Call(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, overrides *json.RawMessage) (hexutil.Bytes, error)

// Chain Information
//
Expand Down Expand Up @@ -266,22 +266,18 @@ func (e *PublicAPI) GetProof(address common.Address,
///////////////////////////////////////////////////////////////////////////////

// Call performs a raw contract call.
func (e *PublicAPI) Call(args evmtypes.TransactionArgs,
func (e *PublicAPI) Call(
args evmtypes.TransactionArgs,
blockNrOrHash rpctypes.BlockNumberOrHash,
override *rpctypes.StateOverride,
overrides *json.RawMessage,
) (hexutil.Bytes, error) {
e.logger.Debug("eth_call", "args", args.String(), "block number or hash", blockNrOrHash)

if override != nil {
e.logger.Debug("eth_call", "error", "overrides are unsupported in call queries")
return nil, fmt.Errorf("overrides are unsupported in call queries")
}

blockNum, err := e.backend.BlockNumberFromTendermint(blockNrOrHash)
if err != nil {
return nil, err
}
data, err := e.backend.DoCall(args, blockNum)
data, err := e.backend.DoCall(args, blockNum, overrides)
if err != nil {
return []byte{}, err
}
Expand Down
41 changes: 41 additions & 0 deletions rpc/types/types.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package types

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/tracing"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/holiman/uint256"

"github.com/cosmos/evm/x/vm/statedb"
)

// Copied the Account and StorageResult types since they are registered under an
Expand Down Expand Up @@ -55,6 +60,42 @@ type RPCTransaction struct {
// StateOverride is the collection of overridden accounts.
type StateOverride map[common.Address]OverrideAccount

// Apply overrides the fields of specified accounts into the given state.
func (diff *StateOverride) Apply(db *statedb.StateDB) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of go-ethereum v1.15.10, the StateOverride.Apply method includes logic for handling precompile addresses.

From a compatibility standpoint, it may be best to either align the logic to match exactly, or alternatively, directly import the override package from Geth and use its Apply method. In the latter case, the StateDB.Finalise method would likely need to be implemented as a noop function.

if db == nil || diff == nil {
return nil
}
for addr, account := range *diff {
// Override account nonce.
if account.Nonce != nil {
db.SetNonce(addr, uint64(*account.Nonce), tracing.NonceChangeUnspecified)
}
// Override account(contract) code.
if account.Code != nil {
db.SetCode(addr, *account.Code)
}
// Override account balance.
if account.Balance != nil && *account.Balance != nil {
u256Balance, _ := uint256.FromBig((*big.Int)(*account.Balance))
db.SetBalance(addr, u256Balance)
}
if account.State != nil && account.StateDiff != nil {
return fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
}
// Replace entire state if caller requires.
if account.State != nil {
db.SetStorage(addr, *account.State)
}
// Apply state diff into specified accounts.
if account.StateDiff != nil {
for key, value := range *account.StateDiff {
db.SetState(addr, key, value)
}
Comment on lines +91 to +93

Check warning

Code scanning / CodeQL

Iteration over map Warning

Iteration over map may be a possible source of non-determinism
}
}
return nil
}

// OverrideAccount indicates the overriding fields of account during the execution of
// a message call.
// Note, state and stateDiff can't be specified at the same time. If state is
Expand Down
78 changes: 77 additions & 1 deletion tests/integration/rpc/backend/test_call_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,11 +425,24 @@ func (s *TestSuite) TestDoCall() {
argsBz, err := json.Marshal(callArgs)
s.Require().NoError(err)

overrides := json.RawMessage(`{
"` + toAddr.Hex() + `": {
"balance": "0x1000000000000000000",
"nonce": "0x1",
"code": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063c6888fa11461003b578063c8e7ca2e14610057575b600080fd5b610055600480360381019061005091906100a3565b610075565b005b61005f61007f565b60405161006c91906100e1565b60405180910390f35b8060008190555050565b60008054905090565b600080fd5b6000819050919050565b61009d8161008a565b81146100a857600080fd5b50565b6000813590506100ba81610094565b92915050565b6000602082840312156100d6576100d5610085565b5b60006100e4848285016100ab565b91505092915050565b6100f68161008a565b82525050565b600060208201905061011160008301846100ed565b9291505056fea2646970667358221220c7d2d7c0b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b264736f6c634300080a0033",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0x123"
}
}
}`)
invalidOverrides := json.RawMessage(`{"invalid": json}`)
emptyOverrides := json.RawMessage(`{}`)
testCases := []struct {
name string
registerMock func()
blockNum rpctypes.BlockNumber
callArgs evmtypes.TransactionArgs
overrides *json.RawMessage
expEthTx *evmtypes.MsgEthereumTxResponse
expPass bool
}{
Expand All @@ -444,6 +457,7 @@ func (s *TestSuite) TestDoCall() {
},
rpctypes.BlockNumber(1),
callArgs,
nil,
&evmtypes.MsgEthereumTxResponse{},
false,
},
Expand All @@ -458,6 +472,67 @@ func (s *TestSuite) TestDoCall() {
},
rpctypes.BlockNumber(1),
callArgs,
nil,
&evmtypes.MsgEthereumTxResponse{},
true,
},
{
"pass - With state overrides",
func() {
client := s.backend.ClientCtx.Client.(*mocks.Client)
QueryClient := s.backend.QueryClient.QueryClient.(*mocks.EVMQueryClient)
_, err := RegisterBlock(client, 1, bz)
s.Require().NoError(err)
expected := &evmtypes.EthCallRequest{
Args: argsBz,
ChainId: s.backend.EvmChainID.Int64(),
Overrides: overrides,
}
RegisterEthCall(QueryClient, expected)
},
rpctypes.BlockNumber(1),
callArgs,
&overrides,
&evmtypes.MsgEthereumTxResponse{},
true,
},
{
"fail - Invalid state overrides JSON",
func() {
client := s.backend.ClientCtx.Client.(*mocks.Client)
QueryClient := s.backend.QueryClient.QueryClient.(*mocks.EVMQueryClient)
_, err := RegisterBlock(client, 1, bz)
s.Require().NoError(err)
expected := &evmtypes.EthCallRequest{
Args: argsBz,
ChainId: s.backend.EvmChainID.Int64(),
Overrides: invalidOverrides,
}
RegisterEthCallError(QueryClient, expected)
},
rpctypes.BlockNumber(1),
callArgs,
&invalidOverrides,
&evmtypes.MsgEthereumTxResponse{},
false,
},
{
"pass - Empty state overrides",
func() {
client := s.backend.ClientCtx.Client.(*mocks.Client)
QueryClient := s.backend.QueryClient.QueryClient.(*mocks.EVMQueryClient)
_, err := RegisterBlock(client, 1, bz)
s.Require().NoError(err)
expected := &evmtypes.EthCallRequest{
Args: argsBz,
ChainId: s.backend.EvmChainID.Int64(),
Overrides: emptyOverrides,
}
RegisterEthCall(QueryClient, expected)
},
rpctypes.BlockNumber(1),
callArgs,
&emptyOverrides,
&evmtypes.MsgEthereumTxResponse{},
true,
},
Expand All @@ -468,9 +543,10 @@ func (s *TestSuite) TestDoCall() {
s.SetupTest() // reset test and queries
tc.registerMock()

msgEthTx, err := s.backend.DoCall(tc.callArgs, tc.blockNum)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to add a simple test case to check whether the expected result is returned when the state overrides input is set.

msgEthTx, err := s.backend.DoCall(tc.callArgs, tc.blockNum, tc.overrides)

if tc.expPass {
s.Require().NoError(err)
s.Require().Equal(tc.expEthTx, msgEthTx)
} else {
s.Require().Error(err)
Expand Down
Loading