diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ca370680..f9da035342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - (ante) [#59](https://github.com/EscanBE/evermint/pull/59) Prevent panic when building error message of fee which overflow int64 - (swagger) [#66](https://github.com/EscanBE/evermint/pull/66) Correct script gen swagger after switched to use vanilla Cosmos-SDK - (rename-chain) [#80](https://github.com/EscanBE/evermint/pull/80) Handle new cases of rename-chain with recent changes +- (rpc) [#85](https://github.com/EscanBE/evermint/pull/85) Compute and return correct `transactionsRoot` and `receiptsRoot` hashes ### Client Breaking diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index f4b6372c0b..b17f06aa39 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -103,7 +103,7 @@ type EVMBackend interface { GetTxByEthHash(txHash common.Hash) (*evertypes.TxResult, error) GetTxByTxIndex(height int64, txIndex uint) (*evertypes.TxResult, error) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) - GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetTransactionReceipt(hash common.Hash) (*rpctypes.RPCReceipt, error) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) diff --git a/rpc/backend/backend_suite_test.go b/rpc/backend/backend_suite_test.go index c3e28a1b0b..53ff4c51cd 100644 --- a/rpc/backend/backend_suite_test.go +++ b/rpc/backend/backend_suite_test.go @@ -87,6 +87,7 @@ func (suite *BackendTestSuite) SetupTest() { suite.backend.clientCtx.Client = mocks.NewClient(suite.T()) suite.backend.queryClient.FeeMarket = mocks.NewFeeMarketQueryClient(suite.T()) suite.backend.ctx = rpctypes.ContextWithHeight(1) + suite.backend.indexer = mocks.NewEVMTxIndexer(suite.T()) // Add codec encCfg := encoding.MakeConfig(app.ModuleBasics) @@ -130,40 +131,55 @@ func (suite *BackendTestSuite) buildFormattedBlock( gasLimit := int64(^uint32(0)) // for `MaxGas = -1` (DefaultConsensusParams) gasUsed := new(big.Int).SetUint64(uint64(blockRes.TxsResults[0].GasUsed)) - root := common.Hash{}.Bytes() - receipt := ethtypes.NewReceipt(root, false, gasUsed.Uint64()) - bloom := ethtypes.CreateBloom(ethtypes.Receipts{receipt}) - - ethRPCTxs := []interface{}{} + var transactions ethtypes.Transactions + var receipts ethtypes.Receipts if tx != nil { - if fullTx { - rpcTx, err := rpctypes.NewRPCTransaction( - tx.AsTransaction(), - common.BytesToHash(header.Hash()), - uint64(header.Height), - uint64(0), - baseFee, - suite.backend.chainID, - ) - suite.Require().NoError(err) - ethRPCTxs = []interface{}{rpcTx} - } else { - ethRPCTxs = []interface{}{common.HexToHash(tx.Hash)} - } + transactions = append(transactions, tx.AsTransaction()) + receipt := createTestReceipt(nil, resBlock, tx, false, mockGasUsed) + receipts = append(receipts, receipt) } + bloom := ethtypes.CreateBloom(receipts) + return rpctypes.FormatBlock( header, + suite.backend.chainID, resBlock.Block.Size(), - gasLimit, - gasUsed, - ethRPCTxs, + gasLimit, gasUsed, baseFee, + transactions, fullTx, + receipts, bloom, common.BytesToAddress(validator.Bytes()), - baseFee, + suite.backend.logger, ) } +func createTestReceipt(root []byte, resBlock *tmrpctypes.ResultBlock, tx *evmtypes.MsgEthereumTx, failed bool, gasUsed uint64) *ethtypes.Receipt { + var status uint64 + if failed { + status = ethtypes.ReceiptStatusFailed + } else { + status = ethtypes.ReceiptStatusSuccessful + } + + transaction := tx.AsTransaction() + + return ðtypes.Receipt{ + Type: transaction.Type(), + PostState: root, + Status: status, + CumulativeGasUsed: gasUsed, + Bloom: ethtypes.BytesToBloom(ethtypes.LogsBloom([]*ethtypes.Log{})), + Logs: []*ethtypes.Log{}, + TxHash: transaction.Hash(), + ContractAddress: common.Address{}, + GasUsed: gasUsed, + BlockHash: common.HexToHash(resBlock.Block.Header.Hash().String()), + BlockNumber: big.NewInt(resBlock.Block.Height), + TransactionIndex: 0, + } +} + func (suite *BackendTestSuite) generateTestKeyring(clientDir string) (keyring.Keyring, error) { buf := bufio.NewReader(os.Stdin) encCfg := encoding.MakeConfig(app.ModuleBasics) @@ -191,3 +207,22 @@ func (suite *BackendTestSuite) signAndEncodeEthTx(msgEthereumTx *evmtypes.MsgEth return txBz } + +func (suite *BackendTestSuite) signMsgEthTx(msgEthereumTx *evmtypes.MsgEthereumTx) (*evmtypes.MsgEthereumTx, []byte) { + queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsWithoutHeader(queryClient, 1) + + ethSigner := ethtypes.LatestSigner(suite.backend.ChainConfig()) + msgEthereumTx.From = suite.from.String() + err := msgEthereumTx.Sign(ethSigner, suite.signer) + suite.Require().NoError(err) + + tx, err := msgEthereumTx.BuildTx(suite.backend.clientCtx.TxConfig.NewTxBuilder(), constants.BaseDenom) + suite.Require().NoError(err) + + txEncoder := suite.backend.clientCtx.TxConfig.TxEncoder() + txBz, err := txEncoder(tx) + suite.Require().NoError(err) + + return msgEthereumTx, txBz +} diff --git a/rpc/backend/blocks.go b/rpc/backend/blocks.go index 4823e2e900..ae8fe45977 100644 --- a/rpc/backend/blocks.go +++ b/rpc/backend/blocks.go @@ -370,45 +370,9 @@ func (b *Backend) RPCBlockFromTendermintBlock( blockRes *tmrpctypes.ResultBlockResults, fullTx bool, ) (map[string]interface{}, error) { - ethRPCTxs := []interface{}{} - block := resBlock.Block - - baseFee, err := b.BaseFee(blockRes) - if err != nil { - // handle the error for pruned node. - b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", block.Height, "error", err) - } - - msgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes) - for txIndex, ethMsg := range msgs { - if !fullTx { - hash := common.HexToHash(ethMsg.Hash) - ethRPCTxs = append(ethRPCTxs, hash) - continue - } - - tx := ethMsg.AsTransaction() - height := uint64(block.Height) //#nosec G701 -- checked for int overflow already - index := uint64(txIndex) //#nosec G701 -- checked for int overflow already - rpcTx, err := rpctypes.NewRPCTransaction( - tx, - common.BytesToHash(block.Hash()), - height, - index, - baseFee, - b.chainID, - ) - if err != nil { - b.logger.Debug("NewTransactionFromData for receipt failed", "hash", tx.Hash().Hex(), "error", err.Error()) - continue - } - ethRPCTxs = append(ethRPCTxs, rpcTx) - } + // prepare block information - bloom, err := b.BlockBloom(blockRes) - if err != nil { - b.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error()) - } + block := resBlock.Block req := &evmtypes.QueryValidatorAccountRequest{ ConsAddress: sdk.ConsAddress(block.Header.ProposerAddress).String(), @@ -419,6 +383,7 @@ func (b *Backend) RPCBlockFromTendermintBlock( ctx := rpctypes.ContextWithHeight(block.Height) res, err := b.queryClient.ValidatorAccount(ctx, req) if err != nil { + // TODO ES return error b.logger.Debug( "failed to query validator operator address", "height", block.Height, @@ -426,6 +391,7 @@ func (b *Backend) RPCBlockFromTendermintBlock( "error", err.Error(), ) // use zero address as the validator operator address + //goland:noinspection GoRedundantConversion validatorAccAddr = sdk.AccAddress(common.Address{}.Bytes()) } else { validatorAccAddr, err = sdk.AccAddressFromBech32(res.AccountAddress) @@ -436,27 +402,114 @@ func (b *Backend) RPCBlockFromTendermintBlock( validatorAddr := common.BytesToAddress(validatorAccAddr) + chainID, err := b.ChainID() + if err != nil { + return nil, err + } + + // prepare gas & fee information + gasLimit, err := rpctypes.BlockMaxGasFromConsensusParams(ctx, b.clientCtx, block.Height) if err != nil { + // TODO ES return error b.logger.Error("failed to query consensus params", "error", err.Error()) } - gasUsed := uint64(0) + var gasUsed uint64 + var gasUsedByTxs []uint64 + for _, txResult := range blockRes.TxsResults { + gasUsedByTx := uint64(txResult.GetGasUsed()) // #nosec G701 -- checked for int overflow already - for _, txsResult := range blockRes.TxsResults { // workaround for cosmos-sdk bug. https://github.com/cosmos/cosmos-sdk/issues/10832 - if ShouldIgnoreGasUsed(txsResult) { + if ShouldIgnoreGasUsed(txResult) { // block gas limit has exceeded, other txs must have failed with same reason. - break + gasUsedByTx = 0 } - gasUsed += uint64(txsResult.GetGasUsed()) // #nosec G701 -- checked for int overflow already + + gasUsed += gasUsedByTx + gasUsedByTxs = append(gasUsedByTxs, gasUsedByTx) } + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // TODO ES return error + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from pruned block. Check node pruning configuration", "height", block.Height, "error", err) + } + + // prepare txs information + + ethMsgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes) + + var transactions ethtypes.Transactions + var receipts ethtypes.Receipts + for transactionIndex, ethMsg := range ethMsgs { + transaction := ethMsg.AsTransaction() + + transactions = append(transactions, transaction) + + indexedTxByHash, err := b.GetTxByEthHash(transaction.Hash()) + if err != nil { + return nil, err + } + + var cumulativeGasUsed uint64 + for _, gasUsedByTx := range gasUsedByTxs[0:indexedTxByHash.TxIndex] { // previous txs + cumulativeGasUsed += gasUsedByTx + } + cumulativeGasUsed += indexedTxByHash.CumulativeGasUsed + + logs, err := TxLogsFromEvents(blockRes.TxsResults[indexedTxByHash.TxIndex].Events, int(indexedTxByHash.MsgIndex)) + if err != nil { + // TODO ES return error + b.logger.Debug("failed to parse logs", "hash", transaction.Hash().Hex(), "error", err.Error()) + } + + txData, err := evmtypes.UnpackTxData(ethMsg.Data) + if err != nil { + return nil, errors.Wrap(err, "failed to unpack tx data") + } + + receipt, err := rpctypes.NewRPCReceipt( + ethMsg, + hexutil.Uint64(transactionIndex), + !indexedTxByHash.Failed, + hexutil.Uint64(b.GetGasUsed(indexedTxByHash, txData.GetGasPrice(), txData.GetGas())), + hexutil.Uint64(cumulativeGasUsed), + baseFee, + logs, + common.BytesToHash(resBlock.BlockID.Hash.Bytes()), + hexutil.Uint64(indexedTxByHash.Height), + chainID.ToInt(), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create transaction receipt") + } + receipts = append(receipts, receipt.AsEthReceipt()) + } + + // prepare block-bloom information + + bloom, err := b.BlockBloom(blockRes) + if err != nil { + // TODO ES return error + b.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error()) + } + + // finalize + formattedBlock := rpctypes.FormatBlock( - block.Header, block.Size(), - gasLimit, new(big.Int).SetUint64(gasUsed), - ethRPCTxs, bloom, validatorAddr, baseFee, + block.Header, + b.chainID, + block.Size(), + gasLimit, new(big.Int).SetUint64(gasUsed), baseFee, + transactions, fullTx, + receipts, + bloom, + validatorAddr, + b.logger, ) + return formattedBlock, nil } diff --git a/rpc/backend/blocks_test.go b/rpc/backend/blocks_test.go index 9f499f8d94..4cfbbe83d2 100644 --- a/rpc/backend/blocks_test.go +++ b/rpc/backend/blocks_test.go @@ -2,6 +2,7 @@ package backend import ( "fmt" + "github.com/cometbft/cometbft/libs/log" "math/big" "cosmossdk.io/math" @@ -85,7 +86,8 @@ func (suite *BackendTestSuite) TestGetBlockByNumber() { blockRes *tmrpctypes.ResultBlockResults resBlock *tmrpctypes.ResultBlock ) - msgEthereumTx, bz := suite.buildEthereumTx() + msgEthereumTx, _ := suite.buildEthereumTx() + msgEthereumTx, bz := suite.signMsgEthTx(msgEthereumTx) testCases := []struct { name string @@ -166,6 +168,10 @@ func (suite *BackendTestSuite) TestGetBlockByNumber() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) RegisterValidatorAccount(queryClient, validator) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) }, false, true, @@ -188,6 +194,13 @@ func (suite *BackendTestSuite) TestGetBlockByNumber() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) RegisterValidatorAccount(queryClient, validator) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByTxHash(indexer, msgEthereumTx.AsTransaction().Hash(), height) }, false, true, @@ -228,7 +241,8 @@ func (suite *BackendTestSuite) TestGetBlockByHash() { blockRes *tmrpctypes.ResultBlockResults resBlock *tmrpctypes.ResultBlock ) - msgEthereumTx, bz := suite.buildEthereumTx() + msgEthereumTx, _ := suite.buildEthereumTx() + msgEthereumTx, bz := suite.signMsgEthTx(msgEthereumTx) block := tmtypes.MakeBlock(1, []tmtypes.Tx{bz}, nil, nil) @@ -311,6 +325,10 @@ func (suite *BackendTestSuite) TestGetBlockByHash() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) RegisterValidatorAccount(queryClient, validator) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) }, false, true, @@ -334,6 +352,13 @@ func (suite *BackendTestSuite) TestGetBlockByHash() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) RegisterValidatorAccount(queryClient, validator) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByTxHash(indexer, msgEthereumTx.AsTransaction().Hash(), height) }, false, true, @@ -887,7 +912,8 @@ func (suite *BackendTestSuite) TestBlockBloom() { } func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { - msgEthereumTx, bz := suite.buildEthereumTx() + msgEthereumTx, _ := suite.buildEthereumTx() + msgEthereumTx, bz := suite.signMsgEthTx(msgEthereumTx) emptyBlock := tmtypes.MakeBlock(1, []tmtypes.Tx{}, nil, nil) testCases := []struct { @@ -920,6 +946,10 @@ func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { client := suite.backend.clientCtx.Client.(*mocks.Client) RegisterConsensusParams(client, height) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) }, false, true, @@ -944,6 +974,13 @@ func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { client := suite.backend.clientCtx.Client.(*mocks.Client) RegisterConsensusParams(client, height) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByTxHash(indexer, msgEthereumTx.AsTransaction().Hash(), height) }, true, true, @@ -968,6 +1005,13 @@ func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { client := suite.backend.clientCtx.Client.(*mocks.Client) RegisterConsensusParams(client, height) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByTxHash(indexer, msgEthereumTx.AsTransaction().Hash(), height) }, true, true, @@ -992,6 +1036,13 @@ func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { client := suite.backend.clientCtx.Client.(*mocks.Client) RegisterConsensusParamsError(client, height) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByTxHash(indexer, msgEthereumTx.AsTransaction().Hash(), height) }, true, true, @@ -1022,6 +1073,10 @@ func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { client := suite.backend.clientCtx.Client.(*mocks.Client) RegisterConsensusParams(client, height) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) }, false, true, @@ -1046,6 +1101,13 @@ func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { client := suite.backend.clientCtx.Client.(*mocks.Client) RegisterConsensusParams(client, height) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByTxHash(indexer, msgEthereumTx.AsTransaction().Hash(), height) }, true, true, @@ -1070,6 +1132,13 @@ func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { client := suite.backend.clientCtx.Client.(*mocks.Client) RegisterConsensusParams(client, height) + + var header metadata.MD + RegisterParams(queryClient, &header, height) + RegisterParamsWithoutHeader(queryClient, height) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByTxHash(indexer, msgEthereumTx.AsTransaction().Hash(), height) }, true, true, @@ -1087,43 +1156,32 @@ func (suite *BackendTestSuite) TestGetEthBlockFromTendermint() { gasLimit := int64(^uint32(0)) // for `MaxGas = -1` (DefaultConsensusParams) gasUsed := new(big.Int).SetUint64(uint64(tc.blockRes.TxsResults[0].GasUsed)) - root := common.Hash{}.Bytes() - receipt := ethtypes.NewReceipt(root, false, gasUsed.Uint64()) - bloom := ethtypes.CreateBloom(ethtypes.Receipts{receipt}) - - ethRPCTxs := []interface{}{} + var transactions ethtypes.Transactions + var receipts ethtypes.Receipts if tc.expTxs { - if tc.fullTx { - rpcTx, err := ethrpc.NewRPCTransaction( - msgEthereumTx.AsTransaction(), - common.BytesToHash(header.Hash()), - uint64(header.Height), - uint64(0), - tc.baseFee, - suite.backend.chainID, - ) - suite.Require().NoError(err) - ethRPCTxs = []interface{}{rpcTx} - } else { - ethRPCTxs = []interface{}{common.HexToHash(msgEthereumTx.Hash)} - } + transactions = append(transactions, msgEthereumTx.AsTransaction()) + receipt := createTestReceipt(nil, tc.resBlock, msgEthereumTx, false, mockGasUsed) + receipts = append(receipts, receipt) } + bloom := ethtypes.CreateBloom(receipts) + expBlock = ethrpc.FormatBlock( header, + suite.backend.chainID, tc.resBlock.Block.Size(), - gasLimit, - gasUsed, - ethRPCTxs, + gasLimit, gasUsed, tc.baseFee, + transactions, tc.fullTx, + receipts, bloom, common.BytesToAddress(tc.validator.Bytes()), - tc.baseFee, + log.NewNopLogger(), ) if tc.expPass { - suite.Require().Equal(expBlock, block) suite.Require().NoError(err) + suite.Equal(expBlock, block) } else { suite.Require().Error(err) } diff --git a/rpc/backend/chain_info_test.go b/rpc/backend/chain_info_test.go index 70485ebd28..0664c8da30 100644 --- a/rpc/backend/chain_info_test.go +++ b/rpc/backend/chain_info_test.go @@ -390,17 +390,20 @@ func (suite *BackendTestSuite) TestFeeHistory() { { "fail - Invalid base fee", func(validator sdk.AccAddress) { - // baseFee := sdk.NewInt(1) queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) client := suite.backend.clientCtx.Client.(*mocks.Client) suite.backend.cfg.JSONRPC.FeeHistoryCap = 2 - _, err := RegisterBlock(client, ethrpc.BlockNumber(1).Int64(), nil) + _, err := RegisterBlock(client, 1, nil) suite.Require().NoError(err) _, err = RegisterBlockResults(client, 1) suite.Require().NoError(err) RegisterBaseFeeError(queryClient) RegisterValidatorAccount(queryClient, validator) RegisterConsensusParams(client, 1) + + var header metadata.MD + RegisterParams(queryClient, &header, 1) + RegisterParamsWithoutHeader(queryClient, 1) }, 1, 1, diff --git a/rpc/backend/indexer_test.go b/rpc/backend/indexer_test.go new file mode 100644 index 0000000000..48865638a7 --- /dev/null +++ b/rpc/backend/indexer_test.go @@ -0,0 +1,48 @@ +package backend + +import ( + "github.com/EscanBE/evermint/v12/rpc/backend/mocks" + "github.com/EscanBE/evermint/v12/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" +) + +// QueryClient defines a mocked object that implements the ethermint GRPC +// QueryClient interface. It allows for performing QueryClient queries without having +// to run a ethermint GRPC server. +// +// To use a mock method it has to be registered in a given test. +var _ types.EVMTxIndexer = &mocks.EVMTxIndexer{} + +const mockGasUsed = 100 + +func RegisterIndexerGetByBlockAndIndex(queryClient *mocks.EVMTxIndexer, height int64, index int32) { + queryClient.On("GetByBlockAndIndex", height, index). + Return(&types.TxResult{ + Height: height, + TxIndex: uint32(index), + MsgIndex: 0, + EthTxIndex: index, + Failed: false, + GasUsed: mockGasUsed, + CumulativeGasUsed: mockGasUsed, + }, nil) +} + +func RegisterIndexerGetByBlockAndIndexError(queryClient *mocks.EVMTxIndexer, height int64, index int32) { + queryClient.On("GetByBlockAndIndex", height, index). + Return(nil, sdkerrors.ErrInvalidRequest) +} + +func RegisterIndexerGetByTxHash(queryClient *mocks.EVMTxIndexer, hash common.Hash, height int64) { + queryClient.On("GetByTxHash", hash). + Return(&types.TxResult{ + Height: height, + TxIndex: 0, + MsgIndex: 0, + EthTxIndex: 0, + Failed: false, + GasUsed: mockGasUsed, + CumulativeGasUsed: mockGasUsed, + }, nil) +} diff --git a/rpc/backend/mocks/indexer.go b/rpc/backend/mocks/indexer.go new file mode 100644 index 0000000000..4f9873e2fe --- /dev/null +++ b/rpc/backend/mocks/indexer.go @@ -0,0 +1,141 @@ +// Code generated by mockery v2.40.1. DO NOT EDIT. + +package mocks + +import ( + abcitypes "github.com/cometbft/cometbft/abci/types" + cometbfttypes "github.com/cometbft/cometbft/types" + + common "github.com/ethereum/go-ethereum/common" + + mock "github.com/stretchr/testify/mock" + + types "github.com/EscanBE/evermint/v12/types" +) + +// EVMTxIndexer is an autogenerated mock type for the EVMTxIndexer type +type EVMTxIndexer struct { + mock.Mock +} + +// GetByBlockAndIndex provides a mock function with given fields: _a0, _a1 +func (_m *EVMTxIndexer) GetByBlockAndIndex(_a0 int64, _a1 int32) (*types.TxResult, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for GetByBlockAndIndex") + } + + var r0 *types.TxResult + var r1 error + if rf, ok := ret.Get(0).(func(int64, int32) (*types.TxResult, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(int64, int32) *types.TxResult); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.TxResult) + } + } + + if rf, ok := ret.Get(1).(func(int64, int32) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByTxHash provides a mock function with given fields: _a0 +func (_m *EVMTxIndexer) GetByTxHash(_a0 common.Hash) (*types.TxResult, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetByTxHash") + } + + var r0 *types.TxResult + var r1 error + if rf, ok := ret.Get(0).(func(common.Hash) (*types.TxResult, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(common.Hash) *types.TxResult); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.TxResult) + } + } + + if rf, ok := ret.Get(1).(func(common.Hash) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IndexBlock provides a mock function with given fields: _a0, _a1 +func (_m *EVMTxIndexer) IndexBlock(_a0 *cometbfttypes.Block, _a1 []*abcitypes.ResponseDeliverTx) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for IndexBlock") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*cometbfttypes.Block, []*abcitypes.ResponseDeliverTx) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// LastIndexedBlock provides a mock function with given fields: +func (_m *EVMTxIndexer) LastIndexedBlock() (int64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for LastIndexedBlock") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func() (int64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewEVMTxIndexer interface { + mock.TestingT + Cleanup(func()) +} + +// NewEVMTxIndexer creates a new instance of EVMTxIndexer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEVMTxIndexer(t mockConstructorTestingTNewEVMTxIndexer) *EVMTxIndexer { + mock := &EVMTxIndexer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index a57857aac0..819cb43a5f 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -16,7 +16,6 @@ import ( "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/crypto" "github.com/pkg/errors" ) @@ -127,17 +126,21 @@ func (b *Backend) getTransactionByHashPending(txHash common.Hash) (*rpctypes.RPC // GetGasUsed returns gasUsed from transaction func (b *Backend) GetGasUsed(res *types.TxResult, price *big.Int, gas uint64) uint64 { + return b.getGasUsed(!res.Failed, res.Height, price, gas, res.GasUsed) +} + +func (b *Backend) getGasUsed(success bool, height int64, price *big.Int, gas, recordedGasUsed uint64) uint64 { // patch gasUsed if tx is reverted and happened before height on which fixed was introduced // to return real gas charged // more info at https://github.com/evmos/ethermint/pull/1557 - if res.Failed && res.Height < b.cfg.JSONRPC.FixRevertGasRefundHeight { + if !success && height < b.cfg.JSONRPC.FixRevertGasRefundHeight { return new(big.Int).Mul(price, new(big.Int).SetUint64(gas)).Uint64() } - return res.GasUsed + return recordedGasUsed } // GetTransactionReceipt returns the transaction receipt identified by hash. -func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { +func (b *Backend) GetTransactionReceipt(hash common.Hash) (*rpctypes.RPCReceipt, error) { hexTx := hash.Hex() b.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) @@ -146,16 +149,19 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) return nil, nil } + resBlock, err := b.TendermintBlockByNumber(rpctypes.BlockNumber(res.Height)) if err != nil { b.logger.Debug("block not found", "height", res.Height, "error", err.Error()) return nil, nil } + tx, err := b.clientCtx.TxConfig.TxDecoder()(resBlock.Block.Txs[res.TxIndex]) if err != nil { b.logger.Debug("decoding failed", "error", err.Error()) return nil, fmt.Errorf("failed to decode tx: %w", err) } + ethMsg := tx.GetMsgs()[res.MsgIndex].(*evmtypes.MsgEthereumTx) txData, err := evmtypes.UnpackTxData(ethMsg.Data) @@ -175,26 +181,16 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ } cumulativeGasUsed += res.CumulativeGasUsed - var status hexutil.Uint - if res.Failed { - status = hexutil.Uint(ethtypes.ReceiptStatusFailed) - } else { - status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) - } chainID, err := b.ChainID() if err != nil { return nil, err } - from, err := ethMsg.GetSender(chainID.ToInt()) - if err != nil { - return nil, err - } - // parse tx logs from events msgIndex := int(res.MsgIndex) // #nosec G701 -- checked for int overflow already logs, err := TxLogsFromEvents(blockRes.TxsResults[res.TxIndex].Events, msgIndex) if err != nil { + // TODO ES return error b.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error()) } @@ -213,51 +209,29 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ return nil, errors.New("can't find index of ethereum tx") } - receipt := map[string]interface{}{ - // Consensus fields: These fields are defined by the Yellow Paper - "status": status, - "cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed), - "logsBloom": ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)), - "logs": logs, - - // Implementation fields: These fields are added by geth when processing a transaction. - // They are stored in the chain database. - "transactionHash": hash, - "contractAddress": nil, - "gasUsed": hexutil.Uint64(b.GetGasUsed(res, txData.GetGasPrice(), txData.GetGas())), - - // Inclusion information: These fields provide information about the inclusion of the - // transaction corresponding to this receipt. - "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), - "blockNumber": hexutil.Uint64(res.Height), - "transactionIndex": hexutil.Uint64(res.EthTxIndex), - - // sender and receiver (contract or EOA) addreses - "from": from, - "to": txData.GetTo(), - "type": hexutil.Uint(ethMsg.AsTransaction().Type()), - } - - if logs == nil { - receipt["logs"] = [][]*ethtypes.Log{} - } - - // If the ContractAddress is 20 0x0 bytes, assume it is not a contract creation - if txData.GetTo() == nil { - receipt["contractAddress"] = crypto.CreateAddress(from, txData.GetNonce()) - } - - if dynamicTx, ok := txData.(*evmtypes.DynamicFeeTx); ok { - baseFee, err := b.BaseFee(blockRes) + var baseFee *big.Int + if ethMsg.AsTransaction().Type() == uint8(ethtypes.DynamicFeeTxType) { + baseFee, err = b.BaseFee(blockRes) if err != nil { + // TODO ES return error + // TODO ES in NewRPCReceipt, remove condition check base fee is nil // tolerate the error for pruned node. b.logger.Error("fetch basefee failed, node is pruned?", "height", res.Height, "error", err) - } else { - receipt["effectiveGasPrice"] = hexutil.Big(*dynamicTx.EffectiveGasPrice(baseFee)) } } - return receipt, nil + return rpctypes.NewRPCReceipt( + ethMsg, + hexutil.Uint64(res.EthTxIndex), + !res.Failed, + hexutil.Uint64(b.GetGasUsed(res, txData.GetGasPrice(), txData.GetGas())), + hexutil.Uint64(cumulativeGasUsed), + baseFee, + logs, + common.BytesToHash(resBlock.BlockID.Hash.Bytes()), + hexutil.Uint64(res.Height), + chainID.ToInt(), + ) } // GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. diff --git a/rpc/backend/tx_info_test.go b/rpc/backend/tx_info_test.go index a084ff00ed..a29ffee866 100644 --- a/rpc/backend/tx_info_test.go +++ b/rpc/backend/tx_info_test.go @@ -281,7 +281,8 @@ func (suite *BackendTestSuite) TestGetTransactionByBlockHashAndIndex() { } func (suite *BackendTestSuite) TestGetTransactionByBlockAndIndex() { - msgEthTx, bz := suite.buildEthereumTx() + msgEthTx, _ := suite.buildEthereumTx() + msgEthTx, bz := suite.signMsgEthTx(msgEthTx) defaultBlock := types.MakeBlock(1, []types.Tx{bz}, nil, nil) defaultResponseDeliverTx := []*abci.ResponseDeliverTx{ @@ -322,6 +323,9 @@ func (suite *BackendTestSuite) TestGetTransactionByBlockAndIndex() { client := suite.backend.clientCtx.Client.(*mocks.Client) _, err := RegisterBlockResults(client, 1) suite.Require().NoError(err) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByBlockAndIndexError(indexer, 1, 1) }, &tmrpctypes.ResultBlock{Block: types.MakeBlock(1, []types.Tx{bz}, nil, nil)}, 1, @@ -336,6 +340,9 @@ func (suite *BackendTestSuite) TestGetTransactionByBlockAndIndex() { _, err := RegisterBlockResults(client, 1) suite.Require().NoError(err) RegisterBaseFeeError(queryClient) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByBlockAndIndex(indexer, 1, 0) }, &tmrpctypes.ResultBlock{Block: defaultBlock}, 0, @@ -370,6 +377,9 @@ func (suite *BackendTestSuite) TestGetTransactionByBlockAndIndex() { _, err := RegisterBlockResults(client, 1) suite.Require().NoError(err) RegisterBaseFee(queryClient, sdk.NewInt(1)) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByBlockAndIndex(indexer, 1, 0) }, &tmrpctypes.ResultBlock{Block: defaultBlock}, 0, @@ -435,6 +445,9 @@ func (suite *BackendTestSuite) TestGetTransactionByBlockNumberAndIndex() { _, err = RegisterBlockResults(client, 1) suite.Require().NoError(err) RegisterBaseFee(queryClient, sdk.NewInt(1)) + + indexer := suite.backend.indexer.(*mocks.EVMTxIndexer) + RegisterIndexerGetByBlockAndIndex(indexer, 1, 0) }, 0, 0, @@ -554,7 +567,7 @@ func (suite *BackendTestSuite) TestGetTransactionReceipt() { tx *evmtypes.MsgEthereumTx block *types.Block blockResult []*abci.ResponseDeliverTx - expTxReceipt map[string]interface{} + expTxReceipt *rpctypes.RPCReceipt expPass bool }{ { @@ -587,7 +600,7 @@ func (suite *BackendTestSuite) TestGetTransactionReceipt() { }, }, }, - map[string]interface{}(nil), + (*rpctypes.RPCReceipt)(nil), false, }, } @@ -605,9 +618,9 @@ func (suite *BackendTestSuite) TestGetTransactionReceipt() { txReceipt, err := suite.backend.GetTransactionReceipt(common.HexToHash(tc.tx.Hash)) if tc.expPass { suite.Require().NoError(err) - suite.Require().Equal(txReceipt, tc.expTxReceipt) + suite.Equal(tc.expTxReceipt, txReceipt) } else { - suite.Require().NotEqual(txReceipt, tc.expTxReceipt) + suite.NotEqual(tc.expTxReceipt, txReceipt) } }) } diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index f610d6abd1..068e43c0d0 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -42,7 +42,7 @@ type EthereumAPI interface { // it is a user or a smart contract. GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransaction, error) GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) - GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetTransactionReceipt(hash common.Hash) (*rpctypes.RPCReceipt, error) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) // eth_getBlockReceipts @@ -175,7 +175,7 @@ func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rp } // GetTransactionReceipt returns the transaction receipt identified by hash. -func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { +func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (*rpctypes.RPCReceipt, error) { hexTx := hash.Hex() e.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) return e.backend.GetTransactionReceipt(hash) diff --git a/rpc/namespaces/ethereum/eth/eth_rpc_it_suite/eth_api_block_test.go b/rpc/namespaces/ethereum/eth/eth_rpc_it_suite/eth_api_block_test.go index 09c4d3693c..c5e0bb53c7 100644 --- a/rpc/namespaces/ethereum/eth/eth_rpc_it_suite/eth_api_block_test.go +++ b/rpc/namespaces/ethereum/eth/eth_rpc_it_suite/eth_api_block_test.go @@ -339,7 +339,7 @@ func (suite *EthRpcTestSuite) Test_GetBlockByNumberAndHash() { suite.Equal("0x0000000000000000", textResultStruct.Nonce, "nonce must be zero since PoS chain does not have this") suite.Equal(fmt.Sprintf("0x%x", testBlockHeight), textResultStruct.Number) suite.Equal("0x"+hex.EncodeToString(previousBlockResult.Block.Hash()), textResultStruct.ParentHash, "parentHash must be previous Tendermint block hash") - suite.Equal(func() string { // TODO ES fix the RPC to return correct receipt root + suite.Equal(func() string { var receipts ethtypes.Receipts for _, tx := range textResultStruct.Transactions { var transaction *ethtypes.Transaction @@ -365,7 +365,7 @@ func (suite *EthRpcTestSuite) Test_GetBlockByNumberAndHash() { suite.Equal(fmt.Sprintf("0x%x", blockResult.Block.Time.UTC().Unix()), textResultStruct.Timestamp, "timestamp must be block UTC epoch seconds") suite.Equal("0x0", textResultStruct.TotalDifficulty, "total difficulty must be zero since PoS chain does not have this") suite.Len(textResultStruct.Transactions, evmTxsCount, "transaction list must be same as sent EVM txs") - suite.Equal(func() string { // TODO ES fix the RPC to return correct transaction root + suite.Equal(func() string { var transactions ethtypes.Transactions for _, tx := range textResultStruct.Transactions { var transaction *ethtypes.Transaction diff --git a/rpc/namespaces/ethereum/eth/eth_rpc_it_suite/eth_api_transaction_test.go b/rpc/namespaces/ethereum/eth/eth_rpc_it_suite/eth_api_transaction_test.go index 8db272a680..c4167bccff 100644 --- a/rpc/namespaces/ethereum/eth/eth_rpc_it_suite/eth_api_transaction_test.go +++ b/rpc/namespaces/ethereum/eth/eth_rpc_it_suite/eth_api_transaction_test.go @@ -329,6 +329,27 @@ func (suite *EthRpcTestSuite) Test_GetTransactionCount() { } func (suite *EthRpcTestSuite) Test_GetTransactionReceipt() { + assertReceiptFields := func(receipt *rpctypes.RPCReceipt) { + if receipt == nil { + return + } + + bz, err := json.Marshal(receipt) + suite.Require().NoError(err) + + var mapper map[string]interface{} + err = json.Unmarshal(bz, &mapper) + suite.Require().NoError(err) + + logs, found := mapper["logs"] + if suite.True(found, "expected logs always available regardless empty or not") { + arr, ok := logs.([]interface{}) + if suite.True(ok) { + suite.Equal(len(receipt.Logs), len(arr)) + } + } + } + suite.Run("basic", func() { sender := suite.CITS.WalletAccounts.Number(1) receiver := suite.CITS.WalletAccounts.Number(2) @@ -350,6 +371,7 @@ func (suite *EthRpcTestSuite) Test_GetTransactionReceipt() { gotReceipt, err := suite.GetEthPublicAPI().GetTransactionReceipt(sentTxHash) suite.Require().NoError(err) suite.Require().NotNil(gotReceipt) + assertReceiptFields(gotReceipt) bzReceipt, err := json.Marshal(gotReceipt) suite.Require().NoError(err) @@ -365,16 +387,16 @@ func (suite *EthRpcTestSuite) Test_GetTransactionReceipt() { } suite.Empty(receipt.Logs) suite.Equal(sentTxHash, receipt.TxHash) - suite.Nil(gotReceipt["contractAddress"]) + suite.Nil(gotReceipt.ContractAddress) suite.Greater(receipt.GasUsed, uint64(0)) suite.Equal(*gotTx.BlockHash, receipt.BlockHash) suite.Equal(gotTx.BlockNumber.ToInt().Int64(), receipt.BlockNumber.Int64()) suite.Equal(uint(*gotTx.TransactionIndex), receipt.TransactionIndex) - if suite.NotNil(gotReceipt["from"]) { - suite.Equal(sender.GetEthAddress(), gotReceipt["from"].(common.Address)) + if suite.NotNil(gotReceipt.From) { + suite.Equal(sender.GetEthAddress(), gotReceipt.From) } - if suite.NotNil(gotReceipt["to"]) { - suite.Equal(receiver.GetEthAddress(), *(gotReceipt["to"].(*common.Address))) + if suite.NotNil(gotReceipt.To) { + suite.Equal(receiver.GetEthAddress(), *(gotReceipt.To)) } suite.Equal(sentEvmTx.AsTransaction().Type(), receipt.Type) }) @@ -431,6 +453,7 @@ func (suite *EthRpcTestSuite) Test_GetTransactionReceipt() { gotReceipt, err := suite.GetEthPublicAPI().GetTransactionReceipt(sentTxHash) suite.Require().NoError(err) suite.Require().NotNil(gotReceipt) + assertReceiptFields(gotReceipt) bzReceipt, err := json.Marshal(gotReceipt) suite.Require().NoError(err) @@ -459,6 +482,7 @@ func (suite *EthRpcTestSuite) Test_GetTransactionReceipt() { bzReceipt, err := json.Marshal(gotReceipt) suite.Require().NoError(err) + assertReceiptFields(gotReceipt) var receipt ethtypes.Receipt err = json.Unmarshal(bzReceipt, &receipt) @@ -480,6 +504,7 @@ func (suite *EthRpcTestSuite) Test_GetTransactionReceipt() { gotReceipt, err := suite.GetEthPublicAPI().GetTransactionReceipt(sentTxHash) suite.Require().NoError(err) suite.Require().NotNil(gotReceipt) + assertReceiptFields(gotReceipt) bzReceipt, err := json.Marshal(gotReceipt) suite.Require().NoError(err) diff --git a/rpc/types/receipt.go b/rpc/types/receipt.go new file mode 100644 index 0000000000..888ae006cd --- /dev/null +++ b/rpc/types/receipt.go @@ -0,0 +1,29 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "math/big" +) + +func (r *RPCReceipt) AsEthReceipt() *ethtypes.Receipt { + var contractAddress common.Address + if r.ContractAddress != nil { + contractAddress = *r.ContractAddress + } + + return ðtypes.Receipt{ + Type: uint8(r.Type), + PostState: nil, + Status: uint64(r.Status), + CumulativeGasUsed: uint64(r.CumulativeGasUsed), + Bloom: r.Bloom, + Logs: r.Logs, + TxHash: r.TransactionHash, + ContractAddress: contractAddress, + GasUsed: uint64(r.GasUsed), + BlockHash: r.BlockHash, + BlockNumber: big.NewInt(int64(r.BlockNumber)), + TransactionIndex: uint(r.TransactionIndex), + } +} diff --git a/rpc/types/types.go b/rpc/types/types.go index df44e50d62..4b01b4b3f8 100644 --- a/rpc/types/types.go +++ b/rpc/types/types.go @@ -52,6 +52,35 @@ type RPCTransaction struct { S *hexutil.Big `json:"s"` } +// RPCReceipt represents a receipt (of a transaction) that will serialize to the RPC representation of the receipt +type RPCReceipt struct { + // Consensus fields: These fields are defined by the Yellow Paper + Status hexutil.Uint `json:"status"` + CumulativeGasUsed hexutil.Uint64 `json:"cumulativeGasUsed"` + Bloom ethtypes.Bloom `json:"logsBloom"` + Logs []*ethtypes.Log `json:"logs"` + + // Implementation fields: These fields are added by geth when processing a transaction. + // They are stored in the chain database. + TransactionHash common.Hash `json:"transactionHash"` + ContractAddress *common.Address `json:"contractAddress,omitempty"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + + // Inclusion information: These fields provide information about the inclusion of the + // transaction corresponding to this receipt. + BlockHash common.Hash `json:"blockHash"` + BlockNumber hexutil.Uint64 `json:"blockNumber"` + TransactionIndex hexutil.Uint64 `json:"transactionIndex"` + Type hexutil.Uint `json:"type"` + + // sender and receiver (contract or EOA) addresses + From common.Address `json:"from"` + To *common.Address `json:"to"` + + // others + EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice,omitempty"` +} + // StateOverride is the collection of overridden accounts. type StateOverride map[common.Address]OverrideAccount diff --git a/rpc/types/utils.go b/rpc/types/utils.go index 5abe0376de..4441adec39 100644 --- a/rpc/types/utils.go +++ b/rpc/types/utils.go @@ -3,7 +3,11 @@ package types import ( "context" "fmt" + "github.com/cometbft/cometbft/libs/log" tmrpcclient "github.com/cometbft/cometbft/rpc/client" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/trie" + "github.com/pkg/errors" "math/big" "strings" @@ -102,15 +106,55 @@ func BlockMaxGasFromConsensusParams(goCtx context.Context, clientCtx client.Cont // FormatBlock creates an ethereum block from a tendermint header and ethereum-formatted // transactions. func FormatBlock( - header tmtypes.Header, size int, gasLimit int64, - gasUsed *big.Int, transactions []interface{}, bloom ethtypes.Bloom, - validatorAddr common.Address, baseFee *big.Int, + header tmtypes.Header, + chainID *big.Int, + size int, + gasLimit int64, gasUsed *big.Int, baseFee *big.Int, + transactions ethtypes.Transactions, fullTx bool, + receipts ethtypes.Receipts, + bloom ethtypes.Bloom, + validatorAddr common.Address, + logger log.Logger, ) map[string]interface{} { var transactionsRoot common.Hash if len(transactions) == 0 { transactionsRoot = ethtypes.EmptyRootHash } else { - transactionsRoot = common.BytesToHash(header.DataHash) + transactionsRoot = ethtypes.DeriveSha(transactions, trie.NewStackTrie(nil)) + } + + var receiptsRoot common.Hash + if len(receipts) == 0 { + receiptsRoot = ethtypes.EmptyRootHash + } else { + receiptsRoot = ethtypes.DeriveSha(receipts, trie.NewStackTrie(nil)) + } + + var txsList []interface{} + + for txIndex, tx := range transactions { + if !fullTx { + txsList = append(txsList, tx.Hash()) + continue + } + + height := uint64(header.Height) //#nosec G701 -- checked for int overflow already + index := uint64(txIndex) //#nosec G701 -- checked for int overflow already + + rpcTx, err := NewRPCTransaction( + tx, + common.BytesToHash(header.Hash()), + height, + index, + baseFee, + chainID, + ) + if err != nil { + logger.Error("NewRPCTransaction failed", "hash", tx.Hash().Hex(), "error", err.Error()) + continue + } + + txsList = append(txsList, rpcTx) } result := map[string]interface{}{ @@ -130,10 +174,10 @@ func FormatBlock( "gasUsed": (*hexutil.Big)(gasUsed), "timestamp": hexutil.Uint64(header.Time.Unix()), "transactionsRoot": transactionsRoot, - "receiptsRoot": ethtypes.EmptyRootHash, + "receiptsRoot": receiptsRoot, "uncles": []common.Hash{}, - "transactions": transactions, + "transactions": txsList, "totalDifficulty": (*hexutil.Big)(big.NewInt(0)), } @@ -218,6 +262,72 @@ func NewRPCTransaction( return result, nil } +func NewRPCReceipt( + ethMsg *evmtypes.MsgEthereumTx, + transactionIndex hexutil.Uint64, + success bool, + gasUsed hexutil.Uint64, + cumulativeGasUsed hexutil.Uint64, + baseFee *big.Int, + logs []*ethtypes.Log, + blockHash common.Hash, + blockNumber hexutil.Uint64, + chainID *big.Int, +) (receipt *RPCReceipt, err error) { + var status hexutil.Uint + + if success { + status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) + } else { + status = hexutil.Uint(ethtypes.ReceiptStatusFailed) + } + + if logs == nil { + logs = []*ethtypes.Log{} + } + + from, err := ethMsg.GetSender(chainID) + if err != nil { + return nil, errors.Wrap(err, "failed to get sender") + } + + txData, err := evmtypes.UnpackTxData(ethMsg.Data) + if err != nil { + return nil, errors.Wrap(err, "failed to unpack tx data") + } + + rpcReceipt := RPCReceipt{ + Status: status, + CumulativeGasUsed: cumulativeGasUsed, + Bloom: ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)), + Logs: logs, + TransactionHash: ethMsg.AsTransaction().Hash(), + ContractAddress: nil, + GasUsed: gasUsed, + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: transactionIndex, + Type: hexutil.Uint(ethMsg.AsTransaction().Type()), + From: from, + To: txData.GetTo(), + EffectiveGasPrice: nil, + } + + if rpcReceipt.To == nil { + newContractAddress := crypto.CreateAddress(from, txData.GetNonce()) + rpcReceipt.ContractAddress = &newContractAddress + } + + if baseFee != nil { + if dynamicTx, ok := txData.(*evmtypes.DynamicFeeTx); ok { + effectiveGasPrice := hexutil.Big(*dynamicTx.EffectiveGasPrice(baseFee)) + rpcReceipt.EffectiveGasPrice = &effectiveGasPrice + } + } + + return &rpcReceipt, nil +} + // BaseFeeFromEvents parses the feemarket basefee from cosmos events func BaseFeeFromEvents(events []abci.Event) *big.Int { for _, event := range events { diff --git a/types/indexer.go b/types/indexer.go index 132ad7cc6b..f881b5cb6e 100644 --- a/types/indexer.go +++ b/types/indexer.go @@ -1,3 +1,5 @@ +//go:generate mockery --name EVMTxIndexer + package types import (