diff --git a/rpc/eth_api.go b/rpc/eth_api.go index 94e29c59..d7c2de5b 100644 --- a/rpc/eth_api.go +++ b/rpc/eth_api.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "math/big" + "strconv" emintcrypto "github.com/cosmos/ethermint/crypto" emintkeys "github.com/cosmos/ethermint/keys" @@ -18,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/cosmos/cosmos-sdk/client/context" @@ -393,12 +395,20 @@ func (e *PublicEthAPI) getEthBlockByNumber(value int64, fullTx bool) (map[string } } - return formatBlock(header, block.Block.Size(), gasLimit, gasUsed, transactions), nil + res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", types.ModuleName, evm.QueryLogsBloom, strconv.FormatInt(block.Block.Height, 10))) + if err != nil { + return nil, err + } + + var out types.QueryBloomFilter + e.cliCtx.Codec.MustUnmarshalJSON(res, &out) + + return formatBlock(header, block.Block.Size(), gasLimit, gasUsed, transactions, out.Bloom), nil } func formatBlock( header tmtypes.Header, size int, gasLimit int64, - gasUsed *big.Int, transactions []interface{}, + gasUsed *big.Int, transactions []interface{}, bloom ethtypes.Bloom, ) map[string]interface{} { return map[string]interface{}{ "number": hexutil.Uint64(header.Height), @@ -406,7 +416,7 @@ func formatBlock( "parentHash": hexutil.Bytes(header.LastBlockID.Hash), "nonce": nil, // PoW specific "sha3Uncles": nil, // No uncles in Tendermint - "logsBloom": "", // TODO: Complete with #55 + "logsBloom": bloom, "transactionsRoot": hexutil.Bytes(header.DataHash), "stateRoot": hexutil.Bytes(header.AppHash), "miner": hexutil.Bytes(header.ValidatorsHash), @@ -584,6 +594,17 @@ func (e *PublicEthAPI) GetTransactionReceipt(hash common.Hash) (map[string]inter status = hexutil.Uint(0) } + res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", types.ModuleName, evm.QueryTxLogs, hash.Hex())) + if err != nil { + return nil, err + } + + var logs types.QueryTxLogs + e.cliCtx.Codec.MustUnmarshalJSON(res, &logs) + + // TODO: change hard coded indexing of bytes + bloomFilter := ethtypes.BytesToBloom(tx.TxResult.GetData()[20:]) + fields := map[string]interface{}{ "blockHash": blockHash, "blockNumber": hexutil.Uint64(tx.Height), @@ -594,13 +615,15 @@ func (e *PublicEthAPI) GetTransactionReceipt(hash common.Hash) (map[string]inter "gasUsed": hexutil.Uint64(tx.TxResult.GasUsed), "cumulativeGasUsed": nil, // ignore until needed "contractAddress": nil, - "logs": nil, // TODO: Do with #55 (eth_getLogs output) - "logsBloom": nil, + "logs": logs.Logs, + "logsBloom": bloomFilter, "status": status, } - if common.BytesToAddress(tx.TxResult.GetData()) != (common.Address{}) { - fields["contractAddress"] = hexutil.Bytes(tx.TxResult.GetData()) + contractAddress := common.BytesToAddress(tx.TxResult.GetData()[:20]) + if contractAddress != (common.Address{}) { + // TODO: change hard coded indexing of first 20 bytes + fields["contractAddress"] = contractAddress } return fields, nil diff --git a/x/evm/handler.go b/x/evm/handler.go index 5782afcb..fb30d02f 100644 --- a/x/evm/handler.go +++ b/x/evm/handler.go @@ -47,6 +47,15 @@ func handleETHTxMsg(ctx sdk.Context, keeper Keeper, msg types.EthereumTxMsg) sdk return emint.ErrInvalidSender(err.Error()).Result() } + // Encode transaction by default Tx encoder + txEncoder := authutils.GetTxEncoder(types.ModuleCdc) + txBytes, err := txEncoder(msg) + if err != nil { + return sdk.ErrInternal(err.Error()).Result() + } + txHash := tm.Tx(txBytes).Hash() + ethHash := common.BytesToHash(txHash) + st := types.StateTransition{ Sender: sender, AccountNonce: msg.Data.AccountNonce, @@ -57,21 +66,15 @@ func handleETHTxMsg(ctx sdk.Context, keeper Keeper, msg types.EthereumTxMsg) sdk Payload: msg.Data.Payload, Csdb: keeper.csdb, ChainID: intChainID, + THash: ðHash, } - - // Encode transaction by default Tx encoder - txEncoder := authutils.GetTxEncoder(types.ModuleCdc) - txBytes, err := txEncoder(msg) - if err != nil { - return sdk.ErrInternal(err.Error()).Result() - } - txHash := tm.Tx(txBytes).Hash() - // Prepare db for logs - keeper.csdb.Prepare(common.BytesToHash(txHash), common.Hash{}, keeper.txCount.get()) + keeper.csdb.Prepare(ethHash, common.Hash{}, keeper.txCount.get()) keeper.txCount.increment() - return st.TransitionCSDB(ctx) + res, bloom := st.TransitionCSDB(ctx) + keeper.bloom.Or(keeper.bloom, bloom) + return res } func handleEmintMsg(ctx sdk.Context, keeper Keeper, msg types.EmintMsg) sdk.Result { @@ -105,5 +108,6 @@ func handleEmintMsg(ctx sdk.Context, keeper Keeper, msg types.EmintMsg) sdk.Resu keeper.csdb.Prepare(common.Hash{}, common.Hash{}, keeper.txCount.get()) // Cannot provide tx hash keeper.txCount.increment() - return st.TransitionCSDB(ctx) + res, _ := st.TransitionCSDB(ctx) + return res } diff --git a/x/evm/keeper.go b/x/evm/keeper.go index fe31c1e5..290c720b 100644 --- a/x/evm/keeper.go +++ b/x/evm/keeper.go @@ -24,6 +24,7 @@ type Keeper struct { cdc *codec.Codec blockKey sdk.StoreKey txCount *count + bloom *big.Int } type count int @@ -48,6 +49,7 @@ func NewKeeper(ak auth.AccountKeeper, storageKey, codeKey sdk.StoreKey, cdc: cdc, blockKey: blockKey, txCount: new(count), + bloom: big.NewInt(0), } } @@ -75,6 +77,31 @@ func (k *Keeper) GetBlockHashMapping(ctx sdk.Context, hash []byte) (height int64 return } +// ---------------------------------------------------------------------------- +// Block bloom bits mapping functions +// May be removed when using only as module (only required by rpc api) +// ---------------------------------------------------------------------------- + +// SetBlockBloomMapping sets the mapping from block height to bloom bits +func (k *Keeper) SetBlockBloomMapping(ctx sdk.Context, bloom ethtypes.Bloom, height int64) { + store := ctx.KVStore(k.blockKey) + heightHash := k.cdc.MustMarshalBinaryLengthPrefixed(height) + if !bytes.Equal(heightHash, []byte{}) { + store.Set(heightHash, bloom.Bytes()) + } +} + +// GetBlockBloomMapping gets bloombits from block height +func (k *Keeper) GetBlockBloomMapping(ctx sdk.Context, height int64) ethtypes.Bloom { + store := ctx.KVStore(k.blockKey) + heightHash := k.cdc.MustMarshalBinaryLengthPrefixed(height) + bloom := store.Get(heightHash) + if bytes.Equal(heightHash, []byte{}) { + panic(fmt.Errorf("block with bloombits %s not found", bloom)) + } + return ethtypes.BytesToBloom(bloom) +} + // ---------------------------------------------------------------------------- // Genesis // ---------------------------------------------------------------------------- diff --git a/x/evm/keeper_test.go b/x/evm/keeper_test.go index 9a2d304c..2b1ecd40 100644 --- a/x/evm/keeper_test.go +++ b/x/evm/keeper_test.go @@ -14,6 +14,7 @@ import ( evmtypes "github.com/cosmos/ethermint/x/evm/types" ethcmn "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" @@ -84,6 +85,10 @@ func TestDBStorage(t *testing.T) { ek.SetBlockHashMapping(ctx, ethcmn.FromHex("0x0d87a3a5f73140f46aac1bf419263e4e94e87c292f25007700ab7f2060e2af68"), 7) ek.SetBlockHashMapping(ctx, []byte{0x43, 0x32}, 8) + // Test block height mapping functionality + testBloom := ethtypes.BytesToBloom([]byte{0x1, 0x3}) + ek.SetBlockBloomMapping(ctx, testBloom, 4) + // Get those state transitions require.Equal(t, ek.GetBalance(ctx, address).Cmp(big.NewInt(5)), 0) require.Equal(t, ek.GetNonce(ctx, address), uint64(4)) @@ -93,6 +98,8 @@ func TestDBStorage(t *testing.T) { require.Equal(t, ek.GetBlockHashMapping(ctx, ethcmn.FromHex("0x0d87a3a5f73140f46aac1bf419263e4e94e87c292f25007700ab7f2060e2af68")), int64(7)) require.Equal(t, ek.GetBlockHashMapping(ctx, []byte{0x43, 0x32}), int64(8)) + require.Equal(t, ek.GetBlockBloomMapping(ctx, 4), testBloom) + // commit stateDB _, err = ek.Commit(ctx, false) require.NoError(t, err, "failed to commit StateDB") diff --git a/x/evm/module.go b/x/evm/module.go index 7d303357..df6f754d 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -2,6 +2,7 @@ package evm import ( "encoding/json" + "math/big" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" @@ -9,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/ethermint/x/evm/client/cli" "github.com/cosmos/ethermint/x/evm/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/gorilla/mux" "github.com/spf13/cobra" abci "github.com/tendermint/tendermint/abci/types" @@ -106,7 +108,10 @@ func (am AppModule) NewQuerierHandler() sdk.Querier { // BeginBlock function for module at start of each block func (am AppModule) BeginBlock(ctx sdk.Context, bl abci.RequestBeginBlock) { // Consider removing this when using evm as module without web3 API + bloom := ethtypes.BytesToBloom(am.keeper.bloom.Bytes()) + am.keeper.SetBlockBloomMapping(ctx, bloom, bl.Header.GetHeight()-1) am.keeper.SetBlockHashMapping(ctx, bl.Header.LastBlockId.GetHash(), bl.Header.GetHeight()-1) + am.keeper.bloom = big.NewInt(0) am.keeper.txCount.reset() } diff --git a/x/evm/querier.go b/x/evm/querier.go index 96159e86..e10aa9ab 100644 --- a/x/evm/querier.go +++ b/x/evm/querier.go @@ -1,6 +1,8 @@ package evm import ( + "strconv" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/ethermint/utils" @@ -20,6 +22,8 @@ const ( QueryCode = "code" QueryNonce = "nonce" QueryHashToHeight = "hashToHeight" + QueryTxLogs = "txLogs" + QueryLogsBloom = "logsBloom" ) // NewQuerier is the module level router for state queries @@ -40,6 +44,10 @@ func NewQuerier(keeper Keeper) sdk.Querier { return queryNonce(ctx, path, keeper) case QueryHashToHeight: return queryHashToHeight(ctx, path, keeper) + case QueryTxLogs: + return queryTxLogs(ctx, path, keeper) + case QueryLogsBloom: + return queryBlockLogsBloom(ctx, path, keeper) default: return nil, sdk.ErrUnknownRequest("unknown query endpoint") } @@ -129,3 +137,33 @@ func queryHashToHeight(ctx sdk.Context, path []string, keeper Keeper) ([]byte, s return res, nil } + +func queryBlockLogsBloom(ctx sdk.Context, path []string, keeper Keeper) ([]byte, sdk.Error) { + num, err := strconv.ParseInt(path[1], 10, 64) + if err != nil { + panic("could not unmarshall block number: " + err.Error()) + } + + bloom := keeper.GetBlockBloomMapping(ctx, num) + + bRes := types.QueryBloomFilter{Bloom: bloom} + res, err := codec.MarshalJSONIndent(keeper.cdc, bRes) + if err != nil { + panic("could not marshal result to JSON: " + err.Error()) + } + + return res, nil +} + +func queryTxLogs(ctx sdk.Context, path []string, keeper Keeper) ([]byte, sdk.Error) { + txHash := ethcmn.HexToHash(path[1]) + logs := keeper.GetLogs(ctx, txHash) + + bRes := types.QueryTxLogs{Logs: logs} + res, err := codec.MarshalJSONIndent(keeper.cdc, bRes) + if err != nil { + panic("could not marshal result to JSON: " + err.Error()) + } + + return res, nil +} diff --git a/x/evm/types/msg_test.go b/x/evm/types/msg_test.go index 4577f114..0bbb53f2 100644 --- a/x/evm/types/msg_test.go +++ b/x/evm/types/msg_test.go @@ -6,9 +6,12 @@ import ( "math/big" "testing" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/ethermint/crypto" "github.com/cosmos/ethermint/utils" + "github.com/ethereum/go-ethereum/common" ethcmn "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/require" ) @@ -174,3 +177,38 @@ func TestMarshalAndUnmarshalData(t *testing.T) { require.NoError(t, err) require.Equal(t, e, *e2) } + +func TestMarshalAndUnmarshalLogs(t *testing.T) { + var cdc = codec.New() + + logs := []*ethtypes.Log{ + { + Address: common.BytesToAddress([]byte{0x11}), + TxHash: common.HexToHash("0x01"), + // May need to find workaround since Topics is required to unmarshal from JSON + Topics: []common.Hash{}, + Removed: true, + }, + {Address: common.BytesToAddress([]byte{0x01, 0x11}), Topics: []common.Hash{}}, + } + + raw, err := codec.MarshalJSONIndent(cdc, logs) + require.NoError(t, err) + + var logs2 []*ethtypes.Log + err = cdc.UnmarshalJSON(raw, &logs2) + require.NoError(t, err) + + require.Len(t, logs2, 2) + require.Equal(t, logs[0].Address, logs2[0].Address) + require.Equal(t, logs[0].TxHash, logs2[0].TxHash) + require.True(t, logs[0].Removed) + + emptyLogs := []*ethtypes.Log{} + + raw, err = codec.MarshalJSONIndent(cdc, emptyLogs) + require.NoError(t, err) + + err = cdc.UnmarshalJSON(raw, &logs2) + require.NoError(t, err) +} diff --git a/x/evm/types/querier.go b/x/evm/types/querier.go index 9a8972bb..a8d48ed9 100644 --- a/x/evm/types/querier.go +++ b/x/evm/types/querier.go @@ -1,5 +1,11 @@ package types +import ( + "fmt" + + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + // QueryResProtocolVersion is response type for protocol version query type QueryResProtocolVersion struct { Version string `json:"version"` @@ -53,3 +59,21 @@ type QueryResNonce struct { func (q QueryResNonce) String() string { return string(q.Nonce) } + +// QueryTxLogs is response type for tx logs query +type QueryTxLogs struct { + Logs []*ethtypes.Log `json:"logs"` +} + +func (q QueryTxLogs) String() string { + return string(fmt.Sprintf("%+v", q.Logs)) +} + +// QueryBloomFilter is response type for tx logs query +type QueryBloomFilter struct { + Bloom ethtypes.Bloom `json:"bloom"` +} + +func (q QueryBloomFilter) String() string { + return string(q.Bloom.Bytes()) +} diff --git a/x/evm/types/state_transition.go b/x/evm/types/state_transition.go index 4565f9eb..17c2024a 100644 --- a/x/evm/types/state_transition.go +++ b/x/evm/types/state_transition.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" sdk "github.com/cosmos/cosmos-sdk/types" @@ -23,10 +24,11 @@ type StateTransition struct { Payload []byte Csdb *CommitStateDB ChainID *big.Int + THash *common.Hash } // TransitionCSDB performs an evm state transition from a transaction -func (st StateTransition) TransitionCSDB(ctx sdk.Context) sdk.Result { +func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) { contractCreation := st.Recipient == nil // Create context for evm @@ -61,7 +63,7 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) sdk.Result { // handle errors if vmerr != nil { - return emint.ErrVMExecution(vmerr.Error()).Result() + return emint.ErrVMExecution(vmerr.Error()).Result(), nil } // Refund remaining gas from tx (Check these values and ensure gas is being consumed correctly) @@ -74,7 +76,19 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) sdk.Result { // TODO: Consume gas from sender - return sdk.Result{Data: addr.Bytes(), GasUsed: st.GasLimit - leftOverGas} + // Generate bloom filter to be saved in tx receipt data + bloomInt := big.NewInt(0) + var bloomFilter ethtypes.Bloom + if st.THash != nil { + logs := st.Csdb.GetLogs(*st.THash) + bloomInt = ethtypes.LogsBloom(logs) + bloomFilter = ethtypes.BytesToBloom(bloomInt.Bytes()) + } + + // TODO: coniditionally add either/ both of these to return data + returnData := append(addr.Bytes(), bloomFilter.Bytes()...) + + return sdk.Result{Data: returnData, GasUsed: st.GasLimit - leftOverGas}, bloomInt } func refundGas(