diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e66927..f8913a50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ the Tracer type used to collect execution traces from the EVM transaction execut * (rpc) [#313](https://github.com/tharsis/ethermint/pull/313) Implement internal debug namespace (Not including logger functions nor traces). * (rpc) [#349](https://github.com/tharsis/ethermint/pull/349) Implement configurable JSON-RPC APIs to manage enabled namespaces. * (rpc) [#377](https://github.com/tharsis/ethermint/pull/377) Implement `miner_` namespace. `miner_setEtherbase` and `miner_setGasPrice` are working as intended. All the other calls are not applicable and return `unsupported`. +* (eth) [tharsis#460](https://github.com/tharsis/ethermint/issues/460) Add support for EIP-1898. ### Bug Fixes diff --git a/docs/api/json-rpc/endpoints.md b/docs/api/json-rpc/endpoints.md index f8c3655b..a3811cd2 100644 --- a/docs/api/json-rpc/endpoints.md +++ b/docs/api/json-rpc/endpoints.md @@ -63,6 +63,7 @@ Check the JSON-RPC methods supported on Ethermint. {synopsis} | `eth_getUncleCountByBlockNumber` | Eth | N/A | Not relevant to Ethermint | | `eth_getUncleByBlockHashAndIndex` | Eth | N/A | Not relevant to Ethermint | | `eth_getUncleByBlockNumberAndIndex` | Eth | N/A | Not relevant to Ethermint | +| [`eth_getProof`](#eth-getProof) | Eth | ✔ | | | [`eth_subscribe`](#eth-subscribe) | Websocket | ✔ | | | [`eth_unsubscribe`](#eth-unsubscribe) | Websocket | ✔ | | | [`personal_importRawKey`](#personal-importrawkey) | Personal | ✔ | | @@ -299,7 +300,7 @@ Returns the account balance for a given account address and Block Number. - Account Address -- Block Number +- Block Number or Block Hash [(eip-1898)](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1898.md) ```json // Request @@ -319,7 +320,7 @@ Returns the storage address for a given account address. - Integer of the position in the storage -- Block Number +- Block Number or Block Hash [(eip-1898)](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1898.md) ```json // Request @@ -337,7 +338,7 @@ Returns the total transaction for a given account address and Block Number. - Account Address -- Block Number +- Block Number or Block Hash [(eip-1898)](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1898.md) ```json // Request @@ -387,7 +388,7 @@ Returns the code for a given account address and Block Number. - Account Address -- Block Number +- Block Number or Block Hash [(eip-1898)](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1898.md) ```json // Request @@ -489,7 +490,7 @@ Executes a new message call immediately without creating a transaction on the bl data: DATA - (optional) Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI in the Solidity documentation -- Block number +- Block number or Block Hash [(eip-1898)](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1898.md) ```json // Request @@ -778,6 +779,26 @@ curl -X POST --data '{"jsonrpc":"2.0","method":"eth_coinbase","params":[],"id":1 {"jsonrpc":"2.0","id":1,"result":"0x7cB61D4117AE31a12E393a1Cfa3BaC666481D02E"} ``` +### eth_getProof + +Returns the account- and storage-values of the specified account including the Merkle-proof. + +#### Parameters + +- Address of account or contract + +- Integer of the position in the storage + +- Block Number or Block Hash [(eip-1898)](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1898.md) + +```json +// Request +curl -X POST --data '{"jsonrpc":"2.0","method":"eth_getProof","params":["0x1234567890123456789012345678901234567890",["0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000001"],"latest"],"id":1}' -H "Content-type:application/json" http://localhost:8545 + +// Result +{"jsonrpc": "2.0", "id": 1, "result": {"address": "0x1234567890123456789012345678901234567890", "accountProof": ["0xf90211a090dcaf88c40c7bbc95a912cbdde67c175767b31173df9ee4b0d733bfdd511c43a0babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bda0473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021a0bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0a04e44caecff45c9891f74f6a2156735886eedf6f1a733628ebc802ec79d844648a0a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7a0e823850f50bf72baae9d1733a36a444ab65d0a6faaba404f0583ce0ca4dad92da0f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721a07117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681a069eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472a0203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8ea09287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208a06fc2d754e304c48ce6a517753c62b1a9c1d5925b89707486d7fc08919e0a94eca07b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475a051f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0a089d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb80", "0xf90211a0395d87a95873cd98c21cf1df9421af03f7247880a2554e20738eec2c7507a494a0bcf6546339a1e7e14eb8fb572a968d217d2a0d1f3bc4257b22ef5333e9e4433ca012ae12498af8b2752c99efce07f3feef8ec910493be749acd63822c3558e6671a0dbf51303afdc36fc0c2d68a9bb05dab4f4917e7531e4a37ab0a153472d1b86e2a0ae90b50f067d9a2244e3d975233c0a0558c39ee152969f6678790abf773a9621a01d65cd682cc1be7c5e38d8da5c942e0a73eeaef10f387340a40a106699d494c3a06163b53d956c55544390c13634ea9aa75309f4fd866f312586942daf0f60fb37a058a52c1e858b1382a8893eb9c1f111f266eb9e21e6137aff0dddea243a567000a037b4b100761e02de63ea5f1fcfcf43e81a372dafb4419d126342136d329b7a7ba032472415864b08f808ba4374092003c8d7c40a9f7f9fe9cc8291f62538e1cc14a074e238ff5ec96b810364515551344100138916594d6af966170ff326a092fab0a0d31ac4eef14a79845200a496662e92186ca8b55e29ed0f9f59dbc6b521b116fea090607784fe738458b63c1942bba7c0321ae77e18df4961b2bc66727ea996464ea078f757653c1b63f72aff3dcc3f2a2e4c8cb4a9d36d1117c742833c84e20de994a0f78407de07f4b4cb4f899dfb95eedeb4049aeb5fc1635d65cf2f2f4dfd25d1d7a0862037513ba9d45354dd3e36264aceb2b862ac79d2050f14c95657e43a51b85c80", "0xf90171a04ad705ea7bf04339fa36b124fa221379bd5a38ffe9a6112cb2d94be3a437b879a08e45b5f72e8149c01efcb71429841d6a8879d4bbe27335604a5bff8dfdf85dcea00313d9b2f7c03733d6549ea3b810e5262ed844ea12f70993d87d3e0f04e3979ea0b59e3cdd6750fa8b15164612a5cb6567cdfb386d4e0137fccee5f35ab55d0efda0fe6db56e42f2057a071c980a778d9a0b61038f269dd74a0e90155b3f40f14364a08538587f2378a0849f9608942cf481da4120c360f8391bbcc225d811823c6432a026eac94e755534e16f9552e73025d6d9c30d1d7682a4cb5bd7741ddabfd48c50a041557da9a74ca68da793e743e81e2029b2835e1cc16e9e25bd0c1e89d4ccad6980a041dda0a40a21ade3a20fcd1a4abb2a42b74e9a32b02424ff8db4ea708a5e0fb9a09aaf8326a51f613607a8685f57458329b41e938bb761131a5747e066b81a0a16808080a022e6cef138e16d2272ef58434ddf49260dc1de1f8ad6dfca3da5d2a92aaaadc58080", "0xf851808080a009833150c367df138f1538689984b8a84fc55692d3d41fe4d1e5720ff5483a6980808080808080808080a0a319c1c415b271afc0adcb664e67738d103ac168e0bc0b7bd2da7966165cb9518080"], "balance": "0x0", "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", "nonce": "0x0", "storageHash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "storageProof": [{"key": "0x0000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "proof": []}, {"key": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "proof": []}]}} +``` + ## WebSocket Methods Read about websockets in [events](./../quickstart/events.md) {hide} diff --git a/ethereum/rpc/backend/backend.go b/ethereum/rpc/backend/backend.go index b7131638..88b64ef3 100644 --- a/ethereum/rpc/backend/backend.go +++ b/ethereum/rpc/backend/backend.go @@ -318,6 +318,10 @@ func (e *EVMBackend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, erro return nil, err } + if resBlock.Block == nil { + return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) + } + req := &evmtypes.QueryBlockBloomRequest{Height: resBlock.Block.Height} blockBloomResp, err := e.queryClient.BlockBloom(types.ContextWithHeight(resBlock.Block.Height), req) diff --git a/ethereum/rpc/namespaces/eth/api.go b/ethereum/rpc/namespaces/eth/api.go index e43bd53a..5935e7f5 100644 --- a/ethereum/rpc/namespaces/eth/api.go +++ b/ethereum/rpc/namespaces/eth/api.go @@ -197,8 +197,13 @@ func (e *PublicAPI) BlockNumber() (hexutil.Uint64, error) { } // GetBalance returns the provided account's balance up to the provided block number. -func (e *PublicAPI) GetBalance(address common.Address, blockNum rpctypes.BlockNumber) (*hexutil.Big, error) { // nolint: interfacer - e.logger.Debug("eth_getBalance", "address", address.String(), "block number", blockNum) +func (e *PublicAPI) GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) { // nolint: interfacer + e.logger.Debug("eth_getBalance", "address", address.String(), "block number or hash", blockNrOrHash) + + blockNum, err := e.getBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } req := &evmtypes.QueryBalanceRequest{ Address: address.String(), @@ -218,8 +223,13 @@ func (e *PublicAPI) GetBalance(address common.Address, blockNum rpctypes.BlockNu } // GetStorageAt returns the contract storage at the given address, block number, and key. -func (e *PublicAPI) GetStorageAt(address common.Address, key string, blockNum rpctypes.BlockNumber) (hexutil.Bytes, error) { // nolint: interfacer - e.logger.Debug("eth_getStorageAt", "address", address.Hex(), "key", key, "block number", blockNum) +func (e *PublicAPI) GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { // nolint: interfacer + e.logger.Debug("eth_getStorageAt", "address", address.Hex(), "key", key, "block number or hash", blockNrOrHash) + + blockNum, err := e.getBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } req := &evmtypes.QueryStorageRequest{ Address: address.String(), @@ -236,8 +246,12 @@ func (e *PublicAPI) GetStorageAt(address common.Address, key string, blockNum rp } // GetTransactionCount returns the number of transactions at the given address up to the given block number. -func (e *PublicAPI) GetTransactionCount(address common.Address, blockNum rpctypes.BlockNumber) (*hexutil.Uint64, error) { - e.logger.Debug("eth_getTransactionCount", "address", address.Hex(), "block number", blockNum) +func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) { + e.logger.Debug("eth_getTransactionCount", "address", address.Hex(), "block number or hash", blockNrOrHash) + blockNum, err := e.getBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } return e.backend.GetTransactionCount(address, blockNum) } @@ -289,14 +303,19 @@ func (e *PublicAPI) GetUncleCountByBlockNumber(blockNum rpctypes.BlockNumber) he } // GetCode returns the contract code at the given address and block number. -func (e *PublicAPI) GetCode(address common.Address, blockNumber rpctypes.BlockNumber) (hexutil.Bytes, error) { // nolint: interfacer - e.logger.Debug("eth_getCode", "address", address.Hex(), "block number", blockNumber) +func (e *PublicAPI) GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { // nolint: interfacer + e.logger.Debug("eth_getCode", "address", address.Hex(), "block number or hash", blockNrOrHash) + + blockNum, err := e.getBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } req := &evmtypes.QueryCodeRequest{ Address: address.String(), } - res, err := e.queryClient.Code(rpctypes.ContextWithHeight(blockNumber.Int64()), req) + res, err := e.queryClient.Code(rpctypes.ContextWithHeight(blockNum.Int64()), req) if err != nil { return nil, err } @@ -418,9 +437,14 @@ func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) } // Call performs a raw contract call. -func (e *PublicAPI) Call(args evmtypes.CallArgs, blockNr rpctypes.BlockNumber, _ *rpctypes.StateOverride) (hexutil.Bytes, error) { - e.logger.Debug("eth_call", "args", args.String(), "block number", blockNr) - data, err := e.doCall(args, blockNr) +func (e *PublicAPI) Call(args evmtypes.CallArgs, blockNrOrHash rpctypes.BlockNumberOrHash, _ *rpctypes.StateOverride) (hexutil.Bytes, error) { + e.logger.Debug("eth_call", "args", args.String(), "block number or hash", blockNrOrHash) + + blockNum, err := e.getBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + data, err := e.doCall(args, blockNum) if err != nil { return []byte{}, err } @@ -791,9 +815,14 @@ func (e *PublicAPI) GetUncleByBlockNumberAndIndex(number hexutil.Uint, idx hexut } // GetProof returns an account object with proof and any storage proofs -func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, blockNumber rpctypes.BlockNumber) (*rpctypes.AccountResult, error) { - height := blockNumber.Int64() - e.logger.Debug("eth_getProof", "address", address.Hex(), "keys", storageKeys, "number", height) +func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) { + e.logger.Debug("eth_getProof", "address", address.Hex(), "keys", storageKeys, "block number or hash", blockNrOrHash) + + blockNum, err := e.getBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + height := blockNum.Int64() ctx := rpctypes.ContextWithHeight(height) clientCtx := e.clientCtx.WithHeight(height) @@ -859,3 +888,21 @@ func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, block StorageProof: storageProofs, }, nil } + +// getBlockNumber returns the BlockNumber from BlockNumberOrHash +func (e *PublicAPI) getBlockNumber(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) { + switch { + case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil: + return rpctypes.EthEarliestBlockNumber, fmt.Errorf("BlockHash and BlockNumber cannot be both nil") + case blockNrOrHash.BlockHash != nil: + blockHeader, err := e.backend.HeaderByHash(*blockNrOrHash.BlockHash) + if err != nil { + return rpctypes.EthEarliestBlockNumber, err + } + return rpctypes.NewBlockNumber(blockHeader.Number), nil + case blockNrOrHash.BlockNumber != nil: + return *blockNrOrHash.BlockNumber, nil + default: + return rpctypes.EthEarliestBlockNumber, nil + } +} diff --git a/ethereum/rpc/types/block.go b/ethereum/rpc/types/block.go index 6986305a..0485eef4 100644 --- a/ethereum/rpc/types/block.go +++ b/ethereum/rpc/types/block.go @@ -2,11 +2,14 @@ package types import ( "context" + "encoding/json" "fmt" "math" "math/big" "strings" + "github.com/ethereum/go-ethereum/common" + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" "github.com/spf13/cast" "google.golang.org/grpc/metadata" @@ -23,6 +26,12 @@ const ( EthEarliestBlockNumber = BlockNumber(0) ) +const ( + BlockParamEarliest = "earliest" + BlockParamLatest = "latest" + BlockParamPending = "pending" +) + // NewBlockNumber creates a new BlockNumber instance. func NewBlockNumber(n *big.Int) BlockNumber { return BlockNumber(n.Int64()) @@ -52,13 +61,13 @@ func (bn *BlockNumber) UnmarshalJSON(data []byte) error { } switch input { - case "earliest": + case BlockParamEarliest: *bn = EthEarliestBlockNumber return nil - case "latest": + case BlockParamLatest: *bn = EthLatestBlockNumber return nil - case "pending": + case BlockParamPending: *bn = EthPendingBlockNumber return nil } @@ -103,3 +112,74 @@ func (bn BlockNumber) TmHeight() *int64 { height := bn.Int64() return &height } + +// BlockNumberOrHash represents a block number or a block hash. +type BlockNumberOrHash struct { + BlockNumber *BlockNumber `json:"blockNumber,omitempty"` + BlockHash *common.Hash `json:"blockHash,omitempty"` +} + +func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error { + type erased BlockNumberOrHash + e := erased{} + err := json.Unmarshal(data, &e) + if err == nil { + return bnh.checkUnmarshal(BlockNumberOrHash(e)) + } + var input string + err = json.Unmarshal(data, &input) + if err != nil { + return err + } + err = bnh.decodeFromString(input) + if err != nil { + return err + } + + return nil +} + +func (bnh *BlockNumberOrHash) checkUnmarshal(e BlockNumberOrHash) error { + if e.BlockNumber != nil && e.BlockHash != nil { + return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other") + } + bnh.BlockNumber = e.BlockNumber + bnh.BlockHash = e.BlockHash + return nil +} + +func (bnh *BlockNumberOrHash) decodeFromString(input string) error { + switch input { + case BlockParamEarliest: + bn := EthEarliestBlockNumber + bnh.BlockNumber = &bn + case BlockParamLatest: + bn := EthLatestBlockNumber + bnh.BlockNumber = &bn + case BlockParamPending: + bn := EthPendingBlockNumber + bnh.BlockNumber = &bn + default: + // check if the input is a block hash + if len(input) == 66 { + hash := common.Hash{} + err := hash.UnmarshalText([]byte(input)) + if err != nil { + return err + } + bnh.BlockHash = &hash + break + } + // otherwise take the hex string has int64 value + blockNumber, err := hexutil.DecodeUint64(input) + if err != nil { + return err + } + if blockNumber > math.MaxInt64 { + return fmt.Errorf("blocknumber %d is too high", blockNumber) + } + bn := BlockNumber(blockNumber) + bnh.BlockNumber = &bn + } + return nil +} diff --git a/ethereum/rpc/types/block_test.go b/ethereum/rpc/types/block_test.go new file mode 100644 index 00000000..1e287f05 --- /dev/null +++ b/ethereum/rpc/types/block_test.go @@ -0,0 +1,102 @@ +package types + +import ( + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "testing" +) + +func TestUnmarshalBlockNumberOrHash(t *testing.T) { + bnh := new(BlockNumberOrHash) + + testCases := []struct { + msg string + input []byte + malleate func() + expPass bool + }{ + { + "JSON input with block hash", + []byte("{\"blockHash\": \"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\"}"), + func() { + require.Equal(t, *bnh.BlockHash, common.HexToHash("0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739")) + require.Nil(t, bnh.BlockNumber) + }, + true, + }, + { + "JSON input with block number", + []byte("{\"blockNumber\": \"0x35\"}"), + func() { + require.Equal(t, *bnh.BlockNumber, BlockNumber(0x35)) + require.Nil(t, bnh.BlockHash) + }, + true, + }, + { + "JSON input with block number latest", + []byte("{\"blockNumber\": \"latest\"}"), + func() { + require.Equal(t, *bnh.BlockNumber, EthLatestBlockNumber) + require.Nil(t, bnh.BlockHash) + }, + true, + }, + { + "JSON input with both block hash and block number", + []byte("{\"blockHash\": \"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\", \"blockNumber\": \"0x35\"}"), + func() { + }, + false, + }, + { + "String input with block hash", + []byte("\"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\""), + func() { + require.Equal(t, *bnh.BlockHash, common.HexToHash("0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739")) + require.Nil(t, bnh.BlockNumber) + }, + true, + }, + { + "String input with block number", + []byte("\"0x35\""), + func() { + require.Equal(t, *bnh.BlockNumber, BlockNumber(0x35)) + require.Nil(t, bnh.BlockHash) + }, + true, + }, + { + "String input with block number latest", + []byte("\"latest\""), + func() { + require.Equal(t, *bnh.BlockNumber, EthLatestBlockNumber) + require.Nil(t, bnh.BlockHash) + }, + true, + }, + { + "String input with block number overflow", + []byte("\"0xffffffffffffffffffffffffffffffffffffff\""), + func() { + }, + false, + }, + } + + for _, tc := range testCases { + fmt.Sprintf("Case %s", tc.msg) + // reset input + bnh = new(BlockNumberOrHash) + err := bnh.UnmarshalJSON(tc.input) + tc.malleate() + if tc.expPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + } + +}