From 36772f44653be57a63e0e3477033d59672d33a77 Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Tue, 11 Apr 2023 10:21:10 +0200 Subject: [PATCH] Add eth_call override (#1337) * Add eth_call override * Pass lint * Apply feedback --------- Co-authored-by: Victor Castell --- consensus/polybft/contractsapi/init.go | 6 ++ .../test-contracts/TestSimple.json | 24 +++++++ .../test-contracts/TestSimple.sol | 10 +++ e2e-polybft/e2e/jsonrpc_test.go | 55 ++++++++++++++++ go.mod | 2 +- go.sum | 2 + jsonrpc/eth_blockchain_test.go | 6 +- jsonrpc/eth_endpoint.go | 53 ++++++++++++++-- jsonrpc/eth_state_test.go | 2 +- server/server.go | 7 +++ state/executor.go | 30 +++++++++ state/executor_test.go | 62 +++++++++++++++++++ state/state.go | 5 ++ state/txn.go | 16 +++++ state/txn_test.go | 6 +- types/types.go | 11 ++++ 16 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 consensus/polybft/contractsapi/test-contracts/TestSimple.json create mode 100644 consensus/polybft/contractsapi/test-contracts/TestSimple.sol create mode 100644 e2e-polybft/e2e/jsonrpc_test.go create mode 100644 state/executor_test.go diff --git a/consensus/polybft/contractsapi/init.go b/consensus/polybft/contractsapi/init.go index 218eb8b5ed..7f04bf0046 100644 --- a/consensus/polybft/contractsapi/init.go +++ b/consensus/polybft/contractsapi/init.go @@ -41,6 +41,7 @@ var ( testContracts embed.FS TestWriteBlockMetadata *artifact.Artifact RootERC20 *artifact.Artifact + TestSimple *artifact.Artifact RootERC721 *artifact.Artifact RootERC1155 *artifact.Artifact ) @@ -172,6 +173,11 @@ func init() { if err != nil { log.Fatal(err) } + + TestSimple, err = artifact.DecodeArtifact(readTestContractContent("TestSimple.json")) + if err != nil { + log.Fatal(err) + } } func readTestContractContent(contractFileName string) []byte { diff --git a/consensus/polybft/contractsapi/test-contracts/TestSimple.json b/consensus/polybft/contractsapi/test-contracts/TestSimple.json new file mode 100644 index 0000000000..3e99f69810 --- /dev/null +++ b/consensus/polybft/contractsapi/test-contracts/TestSimple.json @@ -0,0 +1,24 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestSimple", + "sourceName": "contracts/TestSimple.sol", + "abi": [ + { + "inputs": [], + "name": "getValue", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80632096525514602d575b600080fd5b60336047565b604051603e91906067565b60405180910390f35b60008054905090565b6000819050919050565b6061816050565b82525050565b6000602082019050607a6000830184605a565b9291505056fea26469706673582212203adde2f97bef70ed52dd170d2965805f1ed0cd3eaa229edc27180041091175ac64736f6c63430008110033", + "deployedBytecode": "0x6080604052348015600f57600080fd5b506004361060285760003560e01c80632096525514602d575b600080fd5b60336047565b604051603e91906067565b60405180910390f35b60008054905090565b6000819050919050565b6061816050565b82525050565b6000602082019050607a6000830184605a565b9291505056fea26469706673582212203adde2f97bef70ed52dd170d2965805f1ed0cd3eaa229edc27180041091175ac64736f6c63430008110033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/consensus/polybft/contractsapi/test-contracts/TestSimple.sol b/consensus/polybft/contractsapi/test-contracts/TestSimple.sol new file mode 100644 index 0000000000..6d62fdf7f8 --- /dev/null +++ b/consensus/polybft/contractsapi/test-contracts/TestSimple.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +contract TestSimple { + uint256 val; + + function getValue() public view returns (uint256) { + return val; + } +} diff --git a/e2e-polybft/e2e/jsonrpc_test.go b/e2e-polybft/e2e/jsonrpc_test.go new file mode 100644 index 0000000000..2e9a93620b --- /dev/null +++ b/e2e-polybft/e2e/jsonrpc_test.go @@ -0,0 +1,55 @@ +package e2e + +import ( + "testing" + + "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" + "github.com/0xPolygon/polygon-edge/e2e-polybft/framework" + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/require" + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/abi" + "github.com/umbracle/ethgo/wallet" +) + +func TestE2E_JsonRPC(t *testing.T) { + acct, _ := wallet.GenerateKey() + + cluster := framework.NewTestCluster(t, 3, + framework.WithPremine(types.Address(acct.Address())), + ) + defer cluster.Stop() + + cluster.WaitForReady(t) + + client := cluster.Servers[0].JSONRPC().Eth() + + // Test eth_call with override in state diff + t.Run("eth_call state override", func(t *testing.T) { + deployTxn := cluster.Deploy(t, acct, contractsapi.TestSimple.Bytecode) + require.NoError(t, deployTxn.Wait()) + require.True(t, deployTxn.Succeed()) + + target := deployTxn.Receipt().ContractAddress + + input := abi.MustNewMethod("function getValue() public returns (uint256)").ID() + + resp, err := client.Call(ðgo.CallMsg{To: &target, Data: input}, ethgo.Latest) + require.NoError(t, err) + require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", resp) + + override := ðgo.StateOverride{ + target: ethgo.OverrideAccount{ + StateDiff: &map[ethgo.Hash]ethgo.Hash{ + // storage slot 0 stores the 'val' uint256 value + {0x0}: {0x3}, + }, + }, + } + + resp, err = client.Call(ðgo.CallMsg{To: &target, Data: input}, ethgo.Latest, override) + require.NoError(t, err) + + require.Equal(t, "0x0300000000000000000000000000000000000000000000000000000000000000", resp) + }) +} diff --git a/go.mod b/go.mod index 9c2f1191e8..2e7fa91693 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/klauspost/compress v1.15.5 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mitchellh/mapstructure v1.5.0 - github.com/umbracle/ethgo v0.1.4-0.20230126112511-6a4d02533af6 + github.com/umbracle/ethgo v0.1.4-0.20230326234627-15b1df435098 github.com/valyala/fastjson v1.6.3 // indirect go.uber.org/zap v1.22.0 // indirect golang.org/x/sys v0.4.0 // indirect diff --git a/go.sum b/go.sum index ced22307e0..b5ba8d3958 100644 --- a/go.sum +++ b/go.sum @@ -744,6 +744,8 @@ github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2n github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/umbracle/ethgo v0.1.4-0.20230126112511-6a4d02533af6 h1:WqlyYNdrBECgDwDIEMxa4mLUSH/FfPdAuOnniUqNpJs= github.com/umbracle/ethgo v0.1.4-0.20230126112511-6a4d02533af6/go.mod h1:8QIHEG/YfGnW4I5AND2Znl9W0LU3tXR9IGqgmSieiGo= +github.com/umbracle/ethgo v0.1.4-0.20230326234627-15b1df435098 h1:OXYAHR6AKpV2A/BjQNECXod9rR8r5XOT2QCakD2r0YQ= +github.com/umbracle/ethgo v0.1.4-0.20230326234627-15b1df435098/go.mod h1:bjxSp984qsxCStKoKjqz5fgugi4uWj6+/tFkSEpjk3A= github.com/umbracle/fastrlp v0.0.0-20220527094140-59d5dd30e722 h1:10Nbw6cACsnQm7r34zlpJky+IzxVLRk6MKTS2d3Vp0E= github.com/umbracle/fastrlp v0.0.0-20220527094140-59d5dd30e722/go.mod h1:c8J0h9aULj2i3umrfyestM6jCq0LK0U6ly6bWy96nd4= github.com/umbracle/go-eth-bn256 v0.0.0-20230125114011-47cb310d9b0b h1:5/xofhZiOG0I9DQXqDSPxqYObk6QI7mBGMJI+ngyIgc= diff --git a/jsonrpc/eth_blockchain_test.go b/jsonrpc/eth_blockchain_test.go index f29b689780..b49ac71840 100644 --- a/jsonrpc/eth_blockchain_test.go +++ b/jsonrpc/eth_blockchain_test.go @@ -304,7 +304,7 @@ func TestEth_Call(t *testing.T) { Nonce: argUintPtr(0), } - res, err := eth.Call(contractCall, BlockNumberOrHash{}) + res, err := eth.Call(contractCall, BlockNumberOrHash{}, nil) assert.Error(t, err) assert.Contains(t, err.Error(), store.ethCallError.Error()) @@ -328,7 +328,7 @@ func TestEth_Call(t *testing.T) { Nonce: argUintPtr(0), } - res, err := eth.Call(contractCall, BlockNumberOrHash{}) + res, err := eth.Call(contractCall, BlockNumberOrHash{}, nil) assert.NoError(t, err) assert.NotNil(t, res) @@ -517,7 +517,7 @@ func (m *mockBlockStore) GetAvgGasPrice() *big.Int { return big.NewInt(m.averageGasPrice) } -func (m *mockBlockStore) ApplyTxn(header *types.Header, txn *types.Transaction) (*runtime.ExecutionResult, error) { +func (m *mockBlockStore) ApplyTxn(header *types.Header, txn *types.Transaction, overrides types.StateOverride) (*runtime.ExecutionResult, error) { return &runtime.ExecutionResult{Err: m.ethCallError}, nil } diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index ba06b25a65..504ed682dd 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -62,7 +62,7 @@ type ethBlockchainStore interface { GetAvgGasPrice() *big.Int // ApplyTxn applies a transaction object to the blockchain - ApplyTxn(header *types.Header, txn *types.Transaction) (*runtime.ExecutionResult, error) + ApplyTxn(header *types.Header, txn *types.Transaction, override types.StateOverride) (*runtime.ExecutionResult, error) // GetSyncProgression retrieves the current sync progression, if any GetSyncProgression() *progress.Progression @@ -388,8 +388,45 @@ func (e *Eth) GasPrice() (interface{}, error) { return argUint64(common.Max(e.priceLimit, avgGasPrice)), nil } +type overrideAccount struct { + Nonce *argUint64 `json:"nonce"` + Code *argBytes `json:"code"` + Balance *argUint64 `json:"balance"` + State *map[types.Hash]types.Hash `json:"state"` + StateDiff *map[types.Hash]types.Hash `json:"stateDiff"` +} + +func (o *overrideAccount) ToType() types.OverrideAccount { + res := types.OverrideAccount{} + + if o.Nonce != nil { + res.Nonce = (*uint64)(o.Nonce) + } + + if o.Code != nil { + res.Code = *o.Code + } + + if o.Balance != nil { + res.Balance = new(big.Int).SetUint64(*(*uint64)(o.Balance)) + } + + if o.State != nil { + res.State = *o.State + } + + if o.StateDiff != nil { + res.StateDiff = *o.StateDiff + } + + return res +} + +// StateOverride is the collection of overridden accounts. +type stateOverride map[types.Address]overrideAccount + // Call executes a smart contract call using the transaction object data -func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash) (interface{}, error) { +func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash, apiOverride *stateOverride) (interface{}, error) { header, err := GetHeaderFromBlockNumberOrHash(filter, e.store) if err != nil { return nil, err @@ -404,8 +441,16 @@ func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash) (interface{}, error) transaction.Gas = header.GasLimit } + var override types.StateOverride + if apiOverride != nil { + override = types.StateOverride{} + for addr, o := range *apiOverride { + override[addr] = o.ToType() + } + } + // The return value of the execution is saved in the transition (returnValue field) - result, err := e.store.ApplyTxn(header, transaction) + result, err := e.store.ApplyTxn(header, transaction, override) if err != nil { return nil, err } @@ -540,7 +585,7 @@ func (e *Eth) EstimateGas(arg *txnArgs, rawNum *BlockNumber) (interface{}, error txn := transaction.Copy() txn.Gas = gas - result, applyErr := e.store.ApplyTxn(header, txn) + result, applyErr := e.store.ApplyTxn(header, txn, nil) if applyErr != nil { // Check the application error. diff --git a/jsonrpc/eth_state_test.go b/jsonrpc/eth_state_test.go index af3ff5cbd5..5f18b7ede1 100644 --- a/jsonrpc/eth_state_test.go +++ b/jsonrpc/eth_state_test.go @@ -840,7 +840,7 @@ func (m *mockSpecialStore) GetForksInTime(blockNumber uint64) chain.ForksInTime return chain.ForksInTime{} } -func (m *mockSpecialStore) ApplyTxn(header *types.Header, txn *types.Transaction) (*runtime.ExecutionResult, error) { +func (m *mockSpecialStore) ApplyTxn(header *types.Header, txn *types.Transaction, overrides types.StateOverride) (*runtime.ExecutionResult, error) { if m.applyTxnHook != nil { return m.applyTxnHook(header, txn) } diff --git a/server/server.go b/server/server.go index 27e646e709..dbafcbc4db 100644 --- a/server/server.go +++ b/server/server.go @@ -677,6 +677,7 @@ func (j *jsonRPCHub) GetCode(root types.Hash, addr types.Address) ([]byte, error func (j *jsonRPCHub) ApplyTxn( header *types.Header, txn *types.Transaction, + override types.StateOverride, ) (result *runtime.ExecutionResult, err error) { blockCreator, err := j.GetConsensus().GetBlockCreator(header) if err != nil { @@ -688,6 +689,12 @@ func (j *jsonRPCHub) ApplyTxn( return } + if override != nil { + if err = transition.WithStateOverride(override); err != nil { + return + } + } + result, err = transition.Apply(txn) return diff --git a/state/executor.go b/state/executor.go index 34f9b5892f..2f9771f2c0 100644 --- a/state/executor.go +++ b/state/executor.go @@ -259,6 +259,36 @@ func NewTransition(config chain.ForksInTime, snap Snapshot, radix *Txn) *Transit } } +func (t *Transition) WithStateOverride(override types.StateOverride) error { + for addr, o := range override { + if o.State != nil && o.StateDiff != nil { + return fmt.Errorf("cannot override both state and state diff") + } + + if o.Nonce != nil { + t.state.SetNonce(addr, *o.Nonce) + } + + if o.Balance != nil { + t.state.SetBalance(addr, o.Balance) + } + + if o.Code != nil { + t.state.SetCode(addr, o.Code) + } + + if o.State != nil { + t.state.SetFullStorage(addr, o.State) + } + + for k, v := range o.StateDiff { + t.state.SetState(addr, k, v) + } + } + + return nil +} + func (t *Transition) TotalGas() uint64 { return t.totalGas } diff --git a/state/executor_test.go b/state/executor_test.go new file mode 100644 index 0000000000..640f6761c7 --- /dev/null +++ b/state/executor_test.go @@ -0,0 +1,62 @@ +package state + +import ( + "math/big" + "testing" + + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/require" +) + +func TestOverride(t *testing.T) { + t.Parallel() + state := newStateWithPreState(map[types.Address]*PreState{ + {0x0}: { + Nonce: 1, + Balance: 1, + State: map[types.Hash]types.Hash{ + types.ZeroHash: {0x1}, + }, + }, + {0x1}: { + State: map[types.Hash]types.Hash{ + types.ZeroHash: {0x1}, + }, + }, + }) + + nonce := uint64(2) + balance := big.NewInt(2) + code := []byte{0x1} + + tt := NewTransition(chain.ForksInTime{}, state, newTxn(state)) + + require.Empty(t, tt.state.GetCode(types.ZeroAddress)) + + err := tt.WithStateOverride(types.StateOverride{ + {0x0}: types.OverrideAccount{ + Nonce: &nonce, + Balance: balance, + Code: code, + StateDiff: map[types.Hash]types.Hash{ + types.ZeroHash: {0x2}, + }, + }, + {0x1}: types.OverrideAccount{ + State: map[types.Hash]types.Hash{ + {0x1}: {0x1}, + }, + }, + }) + require.NoError(t, err) + + require.Equal(t, nonce, tt.state.GetNonce(types.ZeroAddress)) + require.Equal(t, balance, tt.state.GetBalance(types.ZeroAddress)) + require.Equal(t, code, tt.state.GetCode(types.ZeroAddress)) + require.Equal(t, types.Hash{0x2}, tt.state.GetState(types.ZeroAddress, types.ZeroHash)) + + // state is fully replaced + require.Equal(t, types.Hash{0x0}, tt.state.GetState(types.Address{0x1}, types.ZeroHash)) + require.Equal(t, types.Hash{0x1}, tt.state.GetState(types.Address{0x1}, types.Hash{0x1})) +} diff --git a/state/state.go b/state/state.go index 0a2089b298..d6152f3997 100644 --- a/state/state.go +++ b/state/state.go @@ -112,6 +112,10 @@ type StateObject struct { Deleted bool DirtyCode bool Txn *iradix.Txn + + // withFakeStorage signals whether the state object + // is using the override full state + withFakeStorage bool } func (s *StateObject) Empty() bool { @@ -129,6 +133,7 @@ func (s *StateObject) Copy() *StateObject { ss.Deleted = s.Deleted ss.DirtyCode = s.DirtyCode ss.Code = s.Code + ss.withFakeStorage = s.withFakeStorage if s.Txn != nil { ss.Txn = s.Txn.CommitOnly().Txn() diff --git a/state/txn.go b/state/txn.go index 00e11364b3..7cfb715ac9 100644 --- a/state/txn.go +++ b/state/txn.go @@ -339,6 +339,10 @@ func (txn *Txn) GetState(addr types.Address, key types.Hash) types.Hash { } } + if object.withFakeStorage { + return types.Hash{} + } + return txn.snapshot.GetStorage(addr, object.Account.Root, key) } @@ -484,6 +488,18 @@ func (txn *Txn) GetCommittedState(addr types.Address, key types.Hash) types.Hash return txn.snapshot.GetStorage(addr, obj.Account.Root, key) } +// SetFullStorage is used to replace the full state of the address. +// Only used for debugging on the override jsonrpc endpoint. +func (txn *Txn) SetFullStorage(addr types.Address, state map[types.Hash]types.Hash) { + for k, v := range state { + txn.SetState(addr, k, v) + } + + txn.upsertAccount(addr, true, func(object *StateObject) { + object.withFakeStorage = true + }) +} + func (txn *Txn) TouchAccount(addr types.Address) { txn.upsertAccount(addr, true, func(obj *StateObject) { diff --git a/state/txn_test.go b/state/txn_test.go index d1ff74b519..0f1dde2acd 100644 --- a/state/txn_test.go +++ b/state/txn_test.go @@ -45,7 +45,11 @@ func (m *mockSnapshot) GetCode(hash types.Hash) ([]byte, bool) { return nil, false } -func newStateWithPreState(preState map[types.Address]*PreState) readSnapshot { +func (m *mockSnapshot) Commit(objs []*Object) (Snapshot, []byte) { + return nil, nil +} + +func newStateWithPreState(preState map[types.Address]*PreState) Snapshot { return &mockSnapshot{state: preState} } diff --git a/types/types.go b/types/types.go index 5c4b58cd1a..4cf4bd1948 100644 --- a/types/types.go +++ b/types/types.go @@ -2,6 +2,7 @@ package types import ( "fmt" + "math/big" "strings" "unicode" @@ -160,3 +161,13 @@ type Proof struct { Data []Hash // the proof himself Metadata map[string]interface{} } + +type OverrideAccount struct { + Nonce *uint64 + Code []byte + Balance *big.Int + State map[Hash]Hash + StateDiff map[Hash]Hash +} + +type StateOverride map[Address]OverrideAccount