diff --git a/CHANGELOG.md b/CHANGELOG.md index 1909ac31..bf7eb3cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,3 +58,9 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Update uninstallFilter and getFilterChanges accordingly * uninstallFilter stops the polling goroutine * getFilterChanges returns the filter's internal list of block hashes and resets it + +* (rpc) [\#54](https://github.com/ChainSafe/ethermint/issues/54) [\#55](https://github.com/ChainSafe/ethermint/issues/55) + Implement eth_getFilterLogs and eth_getLogs + * for a given filter, look through each block for transactions. If there are transactions in the block, get the logs from it, and filter using the filterLogs method + * eth_getLogs and eth_getFilterChanges for log filters use the same underlying method as eth_getFilterLogs + * update HandleMsgEthereumTx to store logs using the ethereum hash \ No newline at end of file diff --git a/rpc/backend.go b/rpc/backend.go index 18a909f2..5bd9ae55 100644 --- a/rpc/backend.go +++ b/rpc/backend.go @@ -20,6 +20,7 @@ type Backend interface { // Used by block filter; also used for polling BlockNumber() (hexutil.Uint64, error) GetBlockByNumber(blockNum BlockNumber, fullTx bool) (map[string]interface{}, error) + GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) getEthBlockByNumber(height int64, fullTx bool) (map[string]interface{}, error) getGasLimit() (int64, error) @@ -62,6 +63,21 @@ func (e *EthermintBackend) GetBlockByNumber(blockNum BlockNumber, fullTx bool) ( return e.getEthBlockByNumber(value, fullTx) } +// GetBlockByHash returns the block identified by hash. +func (e *EthermintBackend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", types.ModuleName, evm.QueryHashToHeight, hash.Hex())) + if err != nil { + return nil, err + } + + var out types.QueryResBlockNumber + if err := e.cliCtx.Codec.UnmarshalJSON(res, &out); err != nil { + return nil, err + } + + return e.getEthBlockByNumber(out.Number, fullTx) +} + func (e *EthermintBackend) getEthBlockByNumber(height int64, fullTx bool) (map[string]interface{}, error) { // Remove this check when 0 query is fixed ref: (https://github.com/tendermint/tendermint/issues/4014) var blkNumPtr *int64 @@ -82,7 +98,7 @@ func (e *EthermintBackend) getEthBlockByNumber(height int64, fullTx bool) (map[s var ( gasUsed *big.Int - transactions []interface{} + transactions []common.Hash ) if fullTx { @@ -96,7 +112,7 @@ func (e *EthermintBackend) getEthBlockByNumber(height int64, fullTx bool) (map[s } else { // TODO: Gas used not saved and cannot be calculated by hashes // Return slice of transaction hashes - transactions = make([]interface{}, len(block.Block.Txs)) + transactions = make([]common.Hash, len(block.Block.Txs)) for i, tx := range block.Block.Txs { transactions[i] = common.BytesToHash(tx.Hash()) } @@ -142,13 +158,17 @@ func (e *EthermintBackend) getGasLimit() (int64, error) { func (e *EthermintBackend) GetTxLogs(txHash common.Hash) ([]*ethtypes.Log, error) { // do we need to use the block height somewhere? ctx := e.cliCtx + res, _, err := ctx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", types.ModuleName, evm.QueryTxLogs, txHash.Hex()), nil) if err != nil { return nil, err } - var out types.QueryETHLogs - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) + out := new(types.QueryETHLogs) + if err := e.cliCtx.Codec.UnmarshalJSON(res, &out); err != nil { + return nil, err + } + return out.Logs, nil } diff --git a/rpc/eth_api.go b/rpc/eth_api.go index 27ae05ff..a4c9b511 100644 --- a/rpc/eth_api.go +++ b/rpc/eth_api.go @@ -293,8 +293,6 @@ func (e *PublicEthAPI) SendTransaction(args params.SendTxArgs) (common.Hash, err // Broadcast transaction res, err := e.cliCtx.BroadcastTx(txBytes) // If error is encountered on the node, the broadcast will not return an error - // TODO: Remove res log - fmt.Println(res.RawLog) if err != nil { return common.Hash{}, err } @@ -471,14 +469,7 @@ func (e *PublicEthAPI) EstimateGas(args CallArgs) (hexutil.Uint64, error) { // GetBlockByHash returns the block identified by hash. func (e *PublicEthAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { - res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", types.ModuleName, evm.QueryHashToHeight, hash.Hex())) - if err != nil { - return nil, err - } - - var out types.QueryResBlockNumber - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) - return e.backend.getEthBlockByNumber(out.Number, fullTx) + return e.backend.GetBlockByHash(hash, fullTx) } // GetBlockByNumber returns the block identified by number. @@ -488,7 +479,7 @@ func (e *PublicEthAPI) GetBlockByNumber(blockNum BlockNumber, fullTx bool) (map[ func formatBlock( header tmtypes.Header, size int, gasLimit int64, - gasUsed *big.Int, transactions []interface{}, bloom ethtypes.Bloom, + gasUsed *big.Int, transactions interface{}, bloom ethtypes.Bloom, ) map[string]interface{} { return map[string]interface{}{ "number": hexutil.Uint64(header.Height), @@ -507,13 +498,13 @@ func formatBlock( "gasLimit": hexutil.Uint64(gasLimit), // Static gas limit "gasUsed": (*hexutil.Big)(gasUsed), "timestamp": hexutil.Uint64(header.Time.Unix()), - "transactions": transactions, + "transactions": transactions.([]common.Hash), "uncles": nil, } } -func convertTransactionsToRPC(cliCtx context.CLIContext, txs []tmtypes.Tx, blockHash common.Hash, height uint64) ([]interface{}, *big.Int, error) { - transactions := make([]interface{}, len(txs)) +func convertTransactionsToRPC(cliCtx context.CLIContext, txs []tmtypes.Tx, blockHash common.Hash, height uint64) ([]common.Hash, *big.Int, error) { + transactions := make([]common.Hash, len(txs)) gasUsed := big.NewInt(0) for i, tx := range txs { @@ -523,10 +514,11 @@ func convertTransactionsToRPC(cliCtx context.CLIContext, txs []tmtypes.Tx, block } // TODO: Remove gas usage calculation if saving gasUsed per block gasUsed.Add(gasUsed, ethTx.Fee()) - transactions[i], err = newRPCTransaction(*ethTx, blockHash, &height, uint64(i)) + tx, err := newRPCTransaction(*ethTx, blockHash, &height, uint64(i)) if err != nil { return nil, nil, err } + transactions[i] = tx.Hash } return transactions, gasUsed, nil diff --git a/rpc/filter_api.go b/rpc/filter_api.go index 724fd040..8a771146 100644 --- a/rpc/filter_api.go +++ b/rpc/filter_api.go @@ -1,12 +1,9 @@ package rpc import ( - "encoding/json" - "fmt" - "math/big" + "errors" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/ethermint/x/evm/types" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" @@ -62,11 +59,14 @@ func (e *PublicFilterAPI) UninstallFilter(id rpc.ID) bool { // If the filter is a block filter, it returns an array of block hashes. // If the filter is a pending transaction filter, it returns an array of transaction hashes. func (e *PublicFilterAPI) GetFilterChanges(id rpc.ID) (interface{}, error) { + if e.filters[id] == nil { + return nil, errors.New("invalid filter ID") + } return e.filters[id].getFilterChanges() } // GetFilterLogs returns an array of all logs matching filter with given id. -func (e *PublicFilterAPI) GetFilterLogs(id rpc.ID) []*ethtypes.Log { +func (e *PublicFilterAPI) GetFilterLogs(id rpc.ID) ([]*ethtypes.Log, error) { return e.filters[id].getFilterLogs() } @@ -74,50 +74,6 @@ func (e *PublicFilterAPI) GetFilterLogs(id rpc.ID) []*ethtypes.Log { // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs func (e *PublicFilterAPI) GetLogs(criteria filters.FilterCriteria) ([]*ethtypes.Log, error) { - var filter *Filter - if criteria.BlockHash != nil { - /* - Still need to add blockhash in prepare function for log entry - */ - filter = NewFilterWithBlockHash(e.backend, &criteria) - results := e.getLogs() - logs := filterLogs(results, nil, nil, filter.addresses, filter.topics) - return logs, nil - } - // Convert the RPC block numbers into internal representations - begin := rpc.LatestBlockNumber.Int64() - if criteria.FromBlock != nil { - begin = criteria.FromBlock.Int64() - } - from := big.NewInt(begin) - end := rpc.LatestBlockNumber.Int64() - if criteria.ToBlock != nil { - end = criteria.ToBlock.Int64() - } - to := big.NewInt(end) - results := e.getLogs() - logs := filterLogs(results, from, to, criteria.Addresses, criteria.Topics) - - return returnLogs(logs), nil -} - -func (e *PublicFilterAPI) getLogs() (results []*ethtypes.Log) { - l, _, err := e.cliCtx.QueryWithData(fmt.Sprintf("custom/%s/logs", types.ModuleName), nil) - if err != nil { - fmt.Printf("error from querier %e ", err) - } - - if err := json.Unmarshal(l, &results); err != nil { - panic(err) - } - return results -} - -// returnLogs is a helper that will return an empty log array in case the given logs array is nil, -// otherwise the given logs array is returned. -func returnLogs(logs []*ethtypes.Log) []*ethtypes.Log { - if logs == nil { - return []*ethtypes.Log{} - } - return logs + filter := NewFilter(e.backend, &criteria) + return filter.getFilterLogs() } diff --git a/rpc/filters.go b/rpc/filters.go index 3b565bba..936a6917 100644 --- a/rpc/filters.go +++ b/rpc/filters.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/log" ) /* @@ -37,7 +38,7 @@ type Filter struct { // NewFilter returns a new Filter func NewFilter(backend Backend, criteria *filters.FilterCriteria) *Filter { - return &Filter{ + filter := &Filter{ backend: backend, fromBlock: criteria.FromBlock, toBlock: criteria.ToBlock, @@ -46,6 +47,8 @@ func NewFilter(backend Backend, criteria *filters.FilterCriteria) *Filter { typ: logFilter, stopped: false, } + + return filter } // NewFilterWithBlockHash returns a new Filter with a blockHash. @@ -139,28 +142,107 @@ func (f *Filter) getFilterChanges() (interface{}, error) { case pendingTxFilter: // TODO case logFilter: - // TODO + return f.getFilterLogs() } - return nil, nil + return nil, errors.New("unsupported filter") } -func (f *Filter) getFilterLogs() []*ethtypes.Log { - // TODO - return nil -} +func (f *Filter) getFilterLogs() ([]*ethtypes.Log, error) { + ret := []*ethtypes.Log{} -func includes(addresses []common.Address, a common.Address) bool { - for _, addr := range addresses { - if addr == a { - return true + // filter specific block only + if f.blockHash != nil { + block, err := f.backend.GetBlockByHash(*f.blockHash, true) + if err != nil { + return nil, err + } + + // if the logsBloom == 0, there are no logs in that block + if txs, ok := block["transactions"].([]common.Hash); !ok { + return ret, nil + } else if len(txs) != 0 { + return f.checkMatches(block) } } - return false + // filter range of blocks + num, err := f.backend.BlockNumber() + if err != nil { + return nil, err + } + + // if f.fromBlock is set to 0, set it to the latest block number + if f.fromBlock == nil || f.fromBlock.Cmp(big.NewInt(0)) == 0 { + f.fromBlock = big.NewInt(int64(num)) + } + + // if f.toBlock is set to 0, set it to the latest block number + if f.toBlock == nil || f.toBlock.Cmp(big.NewInt(0)) == 0 { + f.toBlock = big.NewInt(int64(num)) + } + + log.Debug("[ethAPI] Retrieving filter logs", "fromBlock", f.fromBlock, "toBlock", f.toBlock, + "topics", f.topics, "addresses", f.addresses) + + from := f.fromBlock.Int64() + to := f.toBlock.Int64() + + for i := from; i <= to; i++ { + block, err := f.backend.GetBlockByNumber(NewBlockNumber(big.NewInt(i)), true) + if err != nil { + f.err = err + log.Debug("[ethAPI] Cannot get block", "block", block["number"], "error", err) + break + } + + log.Debug("[ethAPI] filtering", "block", block) + + // TODO: block logsBloom is often set in the wrong block + // if the logsBloom == 0, there are no logs in that block + + if txs, ok := block["transactions"].([]common.Hash); !ok { + continue + } else if len(txs) != 0 { + logs, err := f.checkMatches(block) + if err != nil { + f.err = err + break + } + + ret = append(ret, logs...) + } + } + + return ret, nil +} + +func (f *Filter) checkMatches(block map[string]interface{}) ([]*ethtypes.Log, error) { + transactions, ok := block["transactions"].([]common.Hash) + if !ok { + return nil, errors.New("invalid block transactions") + } + + unfiltered := []*ethtypes.Log{} + + for _, tx := range transactions { + logs, err := f.backend.GetTxLogs(common.BytesToHash(tx[:])) + if err != nil { + return nil, err + } + + unfiltered = append(unfiltered, logs...) + } + + return filterLogs(unfiltered, f.fromBlock, f.toBlock, f.addresses, f.topics), nil } // filterLogs creates a slice of logs matching the given criteria. +// [] -> anything +// [A] -> A in first position of log topics, anything after +// [null, B] -> anything in first position, B in second position +// [A, B] -> A in first position and B in second position +// [[A, B], [A, B]] -> A or B in first position, A or B in second position func filterLogs(logs []*ethtypes.Log, fromBlock, toBlock *big.Int, addresses []common.Address, topics [][]common.Hash) []*ethtypes.Log { var ret []*ethtypes.Log Logs: @@ -194,3 +276,13 @@ Logs: } return ret } + +func includes(addresses []common.Address, a common.Address) bool { + for _, addr := range addresses { + if addr == a { + return true + } + } + + return false +} diff --git a/rpc/tester/tester_test.go b/rpc/tester/tester_test.go index 81755fb7..1b71d595 100644 --- a/rpc/tester/tester_test.go +++ b/rpc/tester/tester_test.go @@ -15,9 +15,11 @@ import ( "math/big" "net/http" "testing" + "time" "github.com/cosmos/ethermint/version" "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" ) @@ -29,6 +31,7 @@ const ( ) var addr = fmt.Sprintf("http://%s:%d", host, port) +var zeroString = "0x0" type Request struct { Version string `json:"jsonrpc"` @@ -78,7 +81,7 @@ func call(t *testing.T, method string, params interface{}) (*Response, error) { } if rpcRes.Error != nil { - t.Fatal(errors.New(rpcRes.Error.Message)) + return nil, errors.New(rpcRes.Error.Message) } err = res.Body.Close() @@ -115,7 +118,7 @@ func TestEth_blockNumber(t *testing.T) { } func TestEth_GetBalance(t *testing.T) { - rpcRes, err := call(t, "eth_getBalance", []string{addrA, "0x0"}) + rpcRes, err := call(t, "eth_getBalance", []string{addrA, zeroString}) require.NoError(t, err) var res hexutil.Big @@ -132,7 +135,7 @@ func TestEth_GetBalance(t *testing.T) { func TestEth_GetStorageAt(t *testing.T) { expectedRes := hexutil.Bytes{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - rpcRes, err := call(t, "eth_getStorageAt", []string{addrA, string(addrAStoreKey), "0x0"}) + rpcRes, err := call(t, "eth_getStorageAt", []string{addrA, string(addrAStoreKey), zeroString}) require.NoError(t, err) var storage hexutil.Bytes @@ -146,7 +149,7 @@ func TestEth_GetStorageAt(t *testing.T) { func TestEth_GetCode(t *testing.T) { expectedRes := hexutil.Bytes{} - rpcRes, err := call(t, "eth_getCode", []string{addrA, "0x0"}) + rpcRes, err := call(t, "eth_getCode", []string{addrA, zeroString}) require.NoError(t, err) var code hexutil.Bytes @@ -158,6 +161,33 @@ func TestEth_GetCode(t *testing.T) { require.True(t, bytes.Equal(expectedRes, code), "expected: %X got: %X", expectedRes, code) } +func getAddress(t *testing.T) []byte { + rpcRes, err := call(t, "eth_accounts", []string{}) + require.NoError(t, err) + + var res []hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &res) + require.NoError(t, err) + + return res[0] +} + +func TestEth_SendTransaction(t *testing.T) { + from := getAddress(t) + + param := make([]map[string]string, 1) + param[0] = make(map[string]string) + param[0]["from"] = "0x" + fmt.Sprintf("%x", from) + param[0]["data"] = "0x6080604052348015600f57600080fd5b5060117f775a94827b8fd9b519d36cd827093c664f93347070a554f65e4a6f56cd73889860405160405180910390a2603580604b6000396000f3fe6080604052600080fdfea165627a7a723058206cab665f0f557620554bb45adf266708d2bd349b8a4314bdff205ee8440e3c240029" + + rpcRes, err := call(t, "eth_sendTransaction", param) + require.NoError(t, err) + + var hash hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &hash) + require.NoError(t, err) +} + func TestEth_NewFilter(t *testing.T) { param := make([]map[string][]string, 1) param[0] = make(map[string][]string) @@ -165,7 +195,299 @@ func TestEth_NewFilter(t *testing.T) { rpcRes, err := call(t, "eth_newFilter", param) require.NoError(t, err) - var code hexutil.Bytes - err = code.UnmarshalJSON(rpcRes.Result) + var ID hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &ID) require.NoError(t, err) } + +func TestEth_NewBlockFilter(t *testing.T) { + rpcRes, err := call(t, "eth_newBlockFilter", []string{}) + require.NoError(t, err) + + var ID hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &ID) + require.NoError(t, err) +} + +func TestEth_GetFilterChanges_NoLogs(t *testing.T) { + param := make([]map[string][]string, 1) + param[0] = make(map[string][]string) + param[0]["topics"] = []string{} + rpcRes, err := call(t, "eth_newFilter", param) + require.NoError(t, err) + + var ID hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &ID) + require.NoError(t, err) + + changesRes, err := call(t, "eth_getFilterChanges", []string{ID.String()}) + require.NoError(t, err) + + var logs []*ethtypes.Log + err = json.Unmarshal(changesRes.Result, &logs) + require.NoError(t, err) +} + +func TestEth_GetFilterChanges_WrongID(t *testing.T) { + _, err := call(t, "eth_getFilterChanges", []string{"0x1122334400000077"}) + require.NotNil(t, err) +} + +// deployTestContract deploys a contract that emits an event in the constructor +func deployTestContract(t *testing.T) hexutil.Bytes { + from := getAddress(t) + + param := make([]map[string]string, 1) + param[0] = make(map[string]string) + param[0]["from"] = "0x" + fmt.Sprintf("%x", from) + param[0]["data"] = "0x6080604052348015600f57600080fd5b5060117f775a94827b8fd9b519d36cd827093c664f93347070a554f65e4a6f56cd73889860405160405180910390a2603580604b6000396000f3fe6080604052600080fdfea165627a7a723058206cab665f0f557620554bb45adf266708d2bd349b8a4314bdff205ee8440e3c240029" + + rpcRes, err := call(t, "eth_sendTransaction", param) + require.NoError(t, err) + + var hash hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &hash) + require.NoError(t, err) + + return hash +} + +func TestEth_GetTransactionReceipt(t *testing.T) { + hash := deployTestContract(t) + + time.Sleep(time.Second * 2) + + param := []string{hash.String()} + rpcRes, err := call(t, "eth_getTransactionReceipt", param) + require.NoError(t, err) + + t.Log(rpcRes.Result) + // TODO: why does this not return a receipt? +} + +func TestEth_GetTxLogs(t *testing.T) { + // currently fails due to eth_sendTransaction returning the tendermint hash, + // while the logs are stored in the db using the ethereum hash + t.Skip() + hash := deployTestContract(t) + + time.Sleep(time.Second * 5) + + param := []string{hash.String()} + rpcRes, err := call(t, "eth_getTxLogs", param) + require.NoError(t, err) + + logs := new([]*ethtypes.Log) + err = json.Unmarshal(rpcRes.Result, logs) + require.NoError(t, err) + + require.Equal(t, 1, len(*logs)) + t.Log((*logs)[0]) + time.Sleep(time.Second) +} + +func TestEth_GetFilterChanges_NoTopics(t *testing.T) { + rpcRes, err := call(t, "eth_blockNumber", []string{}) + require.NoError(t, err) + + var res hexutil.Uint64 + err = res.UnmarshalJSON(rpcRes.Result) + require.NoError(t, err) + + param := make([]map[string]interface{}, 1) + param[0] = make(map[string]interface{}) + param[0]["topics"] = []string{} + param[0]["fromBlock"] = res.String() + param[0]["toBlock"] = zeroString // latest + + // deploy contract, emitting some event + deployTestContract(t) + + rpcRes, err = call(t, "eth_newFilter", param) + require.NoError(t, err) + + var ID hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &ID) + require.NoError(t, err) + + time.Sleep(time.Second) + + // get filter changes + changesRes, err := call(t, "eth_getFilterChanges", []string{ID.String()}) + require.NoError(t, err) + + var logs []*ethtypes.Log + err = json.Unmarshal(changesRes.Result, &logs) + require.NoError(t, err) + + require.Equal(t, 1, len(logs)) + time.Sleep(time.Second) + + //t.Log(logs[0]) + // TODO: why is the tx hash in the log not the same as the tx hash of the transaction? + //require.Equal(t, logs[0].TxHash, common.BytesToHash(hash)) +} + +func TestEth_GetFilterChanges_Addresses(t *testing.T) { + // TODO: need transaction receipts to determine contract deployment address +} + +func TestEth_GetFilterChanges_BlockHash(t *testing.T) { + // TODO: need transaction receipts to determine tx block +} + +// hash of Hello event +var helloTopic = "0x775a94827b8fd9b519d36cd827093c664f93347070a554f65e4a6f56cd738898" + +// world parameter in Hello event +var worldTopic = "0x0000000000000000000000000000000000000000000000000000000000000011" + +func deployTestContractWithFunction(t *testing.T) hexutil.Bytes { + // pragma solidity ^0.5.1; + + // contract Test { + // event Hello(uint256 indexed world); + // event Test(uint256 indexed a, uint256 indexed b); + + // constructor() public { + // emit Hello(17); + // } + + // function test(uint256 a, uint256 b) public { + // emit Test(a, b); + // } + // } + + bytecode := "0x608060405234801561001057600080fd5b5060117f775a94827b8fd9b519d36cd827093c664f93347070a554f65e4a6f56cd73889860405160405180910390a260c98061004d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063eb8ac92114602d575b600080fd5b606060048036036040811015604157600080fd5b8101908080359060200190929190803590602001909291905050506062565b005b80827f91916a5e2c96453ddf6b585497262675140eb9f7a774095fb003d93e6dc6921660405160405180910390a3505056fea265627a7a72315820ef746422e676b3ed22147cd771a6f689e7c33ef17bf5cd91921793b5dd01e3e064736f6c63430005110032" + + from := getAddress(t) + + param := make([]map[string]string, 1) + param[0] = make(map[string]string) + param[0]["from"] = "0x" + fmt.Sprintf("%x", from) + param[0]["data"] = bytecode + + rpcRes, err := call(t, "eth_sendTransaction", param) + require.NoError(t, err) + + var hash hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &hash) + require.NoError(t, err) + + return hash +} + +// Tests topics case where there are topics in first two positions +func TestEth_GetFilterChanges_Topics_AB(t *testing.T) { + time.Sleep(time.Second) + + rpcRes, err := call(t, "eth_blockNumber", []string{}) + require.NoError(t, err) + + var res hexutil.Uint64 + err = res.UnmarshalJSON(rpcRes.Result) + require.NoError(t, err) + + param := make([]map[string]interface{}, 1) + param[0] = make(map[string]interface{}) + param[0]["topics"] = []string{helloTopic, worldTopic} + param[0]["fromBlock"] = res.String() + param[0]["toBlock"] = zeroString // latest + + deployTestContractWithFunction(t) + + rpcRes, err = call(t, "eth_newFilter", param) + require.NoError(t, err) + + var ID hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &ID) + require.NoError(t, err) + + time.Sleep(time.Second * 2) + + // get filter changes + changesRes, err := call(t, "eth_getFilterChanges", []string{ID.String()}) + require.NoError(t, err) + + var logs []*ethtypes.Log + err = json.Unmarshal(changesRes.Result, &logs) + require.NoError(t, err) + + require.Equal(t, 1, len(logs)) + time.Sleep(time.Second * 2) +} + +func TestEth_GetFilterChanges_Topics_XB(t *testing.T) { + rpcRes, err := call(t, "eth_blockNumber", []string{}) + require.NoError(t, err) + + var res hexutil.Uint64 + err = res.UnmarshalJSON(rpcRes.Result) + require.NoError(t, err) + + param := make([]map[string]interface{}, 1) + param[0] = make(map[string]interface{}) + param[0]["topics"] = []interface{}{nil, worldTopic} + param[0]["fromBlock"] = res.String() + param[0]["toBlock"] = "0x0" // latest + + deployTestContractWithFunction(t) + + rpcRes, err = call(t, "eth_newFilter", param) + require.NoError(t, err) + + var ID hexutil.Bytes + err = json.Unmarshal(rpcRes.Result, &ID) + require.NoError(t, err) + + time.Sleep(time.Second * 2) + + // get filter changes + changesRes, err := call(t, "eth_getFilterChanges", []string{ID.String()}) + require.NoError(t, err) + + var logs []*ethtypes.Log + err = json.Unmarshal(changesRes.Result, &logs) + require.NoError(t, err) + + require.Equal(t, 1, len(logs)) + time.Sleep(time.Second) +} + +func TestEth_GetFilterChanges_Topics_XXC(t *testing.T) { + // TODO: call test function, need tx receipts to determine contract address +} + +func TestEth_GetLogs_NoLogs(t *testing.T) { + param := make([]map[string][]string, 1) + param[0] = make(map[string][]string) + param[0]["topics"] = []string{} + _, err := call(t, "eth_getLogs", param) + require.NoError(t, err) +} + +func TestEth_GetLogs_Topics_AB(t *testing.T) { + rpcRes, err := call(t, "eth_blockNumber", []string{}) + require.NoError(t, err) + + var res hexutil.Uint64 + err = res.UnmarshalJSON(rpcRes.Result) + require.NoError(t, err) + + param := make([]map[string]interface{}, 1) + param[0] = make(map[string]interface{}) + param[0]["topics"] = []string{helloTopic, worldTopic} + param[0]["fromBlock"] = res.String() + param[0]["toBlock"] = zeroString // latest + + deployTestContractWithFunction(t) + + rpcRes, err = call(t, "eth_getLogs", param) + require.NoError(t, err) + + var logs []*ethtypes.Log + err = json.Unmarshal(rpcRes.Result, &logs) + require.NoError(t, err) + + require.Equal(t, 1, len(logs)) +} diff --git a/rpc/types.go b/rpc/types.go index ab776141..8eeb0018 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -3,6 +3,7 @@ package rpc import ( "fmt" "math" + "math/big" "strings" "github.com/ethereum/go-ethereum/common/hexutil" @@ -19,6 +20,10 @@ const ( EarliestBlockNumber = BlockNumber(1) ) +func NewBlockNumber(n *big.Int) BlockNumber { + return BlockNumber(n.Int64()) +} + // UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports: // - "latest", "earliest" or "pending" as string arguments // - the block number diff --git a/x/evm/handler.go b/x/evm/handler.go index 09c7930e..06680517 100644 --- a/x/evm/handler.go +++ b/x/evm/handler.go @@ -43,8 +43,7 @@ func HandleMsgEthereumTx(ctx sdk.Context, k Keeper, msg types.MsgEthereumTx) sdk return sdk.ResultFromError(err) } - txHash := tmtypes.Tx(ctx.TxBytes()).Hash() - ethHash := common.BytesToHash(txHash) + txHash := msg.Hash() st := types.StateTransition{ Sender: sender, @@ -56,11 +55,12 @@ func HandleMsgEthereumTx(ctx sdk.Context, k Keeper, msg types.MsgEthereumTx) sdk Payload: msg.Data.Payload, Csdb: k.CommitStateDB.WithContext(ctx), ChainID: intChainID, - THash: ðHash, + THash: &txHash, Simulate: ctx.IsCheckTx(), } // Prepare db for logs - k.CommitStateDB.Prepare(ethHash, common.Hash{}, k.TxCount) + // TODO: block hash + k.CommitStateDB.Prepare(txHash, txHash, k.TxCount) k.TxCount++ // TODO: move to keeper @@ -73,7 +73,7 @@ func HandleMsgEthereumTx(ctx sdk.Context, k Keeper, msg types.MsgEthereumTx) sdk k.Bloom.Or(k.Bloom, returnData.Bloom) // update transaction logs in KVStore - err = k.SetTransactionLogs(ctx, returnData.Logs, txHash) + err = k.SetTransactionLogs(ctx, returnData.Logs, txHash[:]) if err != nil { return sdk.ResultFromError(err) } diff --git a/x/evm/types/state_transition.go b/x/evm/types/state_transition.go index 5846e66d..575d2a0e 100644 --- a/x/evm/types/state_transition.go +++ b/x/evm/types/state_transition.go @@ -40,7 +40,6 @@ type ReturnData struct { // TODO: update godoc, it doesn't explain what it does in depth. func (st StateTransition) TransitionCSDB(ctx sdk.Context) (*ReturnData, error) { returnData := new(ReturnData) - contractCreation := st.Recipient == nil cost, err := core.IntrinsicGas(st.Payload, contractCreation, true) @@ -123,11 +122,13 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (*ReturnData, error) { bloomInt := big.NewInt(0) var bloomFilter ethtypes.Bloom var logs []*ethtypes.Log + if st.THash != nil && !st.Simulate { logs, err = csdb.GetLogs(*st.THash) if err != nil { return nil, err } + bloomInt = ethtypes.LogsBloom(logs) bloomFilter = ethtypes.BytesToBloom(bloomInt.Bytes()) } @@ -171,6 +172,11 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (*ReturnData, error) { // Out of gas check does not need to be done here since it is done within the EVM execution ctx.WithGasMeter(currentGasMeter).GasMeter().ConsumeGas(gasConsumed, "EVM execution consumption") + err = st.Csdb.SetLogs(*st.THash, logs) + if err != nil { + return nil, err + } + returnData.Logs = logs returnData.Bloom = bloomInt returnData.Result = &sdk.Result{Data: resultData, GasUsed: gasConsumed} diff --git a/x/evm/types/statedb.go b/x/evm/types/statedb.go index b4896c81..7d5f657c 100644 --- a/x/evm/types/statedb.go +++ b/x/evm/types/statedb.go @@ -157,6 +157,18 @@ func (csdb *CommitStateDB) SetCode(addr ethcmn.Address, code []byte) { } } +// SetLogs sets the logs for a transaction in the KVStore. +func (csdb *CommitStateDB) SetLogs(hash ethcmn.Hash, logs []*ethtypes.Log) error { + store := csdb.ctx.KVStore(csdb.storeKey) + enc, err := EncodeLogs(logs) + if err != nil { + return err + } + + store.Set(LogsKey(hash[:]), enc) + return nil +} + // AddLog adds a new log to the state and sets the log metadata from the state. func (csdb *CommitStateDB) AddLog(log *ethtypes.Log) { csdb.journal.append(addLogChange{txhash: csdb.thash}) @@ -288,7 +300,7 @@ func (csdb *CommitStateDB) GetCommittedState(addr ethcmn.Address, hash ethcmn.Ha return ethcmn.Hash{} } -// GetLogs returns the current logs for a given hash in the state. +// GetLogs returns the current logs for a given transaction hash from the KVStore. func (csdb *CommitStateDB) GetLogs(hash ethcmn.Hash) ([]*ethtypes.Log, error) { if csdb.logs[hash] != nil { return csdb.logs[hash], nil