diff --git a/app/app.go b/app/app.go index 2c0f204c3..8a2b30231 100644 --- a/app/app.go +++ b/app/app.go @@ -1831,7 +1831,7 @@ func (app *App) RegisterTendermintService(clientCtx client.Context) { return ctx.WithIsEVM(true) } if app.evmRPCConfig.HTTPEnabled { - evmHTTPServer, err := evmrpc.NewEVMHTTPServer(app.Logger(), app.evmRPCConfig, clientCtx.Client, &app.EvmKeeper, ctxProvider, app.encodingConfig.TxConfig, DefaultNodeHome) + evmHTTPServer, err := evmrpc.NewEVMHTTPServer(app.Logger(), app.evmRPCConfig, clientCtx.Client, &app.EvmKeeper, ctxProvider, app.encodingConfig.TxConfig, DefaultNodeHome, nil) if err != nil { panic(err) } diff --git a/evmrpc/README.md b/evmrpc/README.md new file mode 100644 index 000000000..06cfd436b --- /dev/null +++ b/evmrpc/README.md @@ -0,0 +1,24 @@ +# Sei's EVM RPC + +Sei supports the standard [Ethereum JSON-RPC API](https://ethereum.org/en/developers/docs/apis/json-rpc/) endpoints. On top of that, Sei supports some additional custom endpoints. + +## Sei_ endpoints + +### Endpoints for Synthetic txs +The motivation for these endpoints is to expose CW20 and CW721 events on the EVM side through synthetic receipts and logs. This is useful for indexing pointer contracts. + - `sei_getFilterLogs` + - same as `eth_getFilterLogs` but includes synthetic logs + - `sei_getLogs` + - same as `eth_getLogs` but includes synthetic logs + - `sei_getBlockByNumber` and `sei_getBlockByHash` + - same as `eth_getBlockByNumber` and `eth_getBlockByHash` but includes synthetic txs + - NOTE: for synthetic txs, `eth_getTransactionReceipt` can be used to get the receipt data for a synthetic tx hash. + +### Endpoints for excluding tracing failures +The motivation for these endpoints is to exclude tracing failures from the EVM side. Due to how our mempool works and our lack of tx simulation, we cannot rely on txs to pass all pre-state checks. Therefore, in the eth_ endpoints, we may see txs that fail tracing with errors like "nonce too low", "nonce too high", "insufficient funds", or other types of panic failures. These transactions are not executed, yet are still included in the block. These endpoints are useful for filtering out these txs. +- `sei_traceBlockByNumberExcludeTraceFail` + - same as `debug_traceBlockByNumber` but excludes panic txs +- `sei_getTransactionReceiptExcludeTraceFail` + - same as `eth_getTransactionReceipt` but excludes panic txs +- `sei_getBlockByNumberExcludeTraceFail` and `sei_getBlockByHashExcludeTraceFail` + - same as `eth_getBlockByNumber` and `eth_getBlockByHash` but excludes panic txs diff --git a/evmrpc/block.go b/evmrpc/block.go index 93043f209..b2be78d09 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -39,18 +39,50 @@ type BlockAPI struct { includeShellReceipts bool } -func NewBlockAPI(tmClient rpcclient.Client, k *keeper.Keeper, ctxProvider func(int64) sdk.Context, txConfig client.TxConfig, connectionType ConnectionType, namespace string) *BlockAPI { +type SeiBlockAPI struct { + *BlockAPI + isPanicTx func(ctx context.Context, hash common.Hash) (bool, error) +} + +func NewBlockAPI(tmClient rpcclient.Client, k *keeper.Keeper, ctxProvider func(int64) sdk.Context, txConfig client.TxConfig, connectionType ConnectionType) *BlockAPI { return &BlockAPI{ tmClient: tmClient, keeper: k, ctxProvider: ctxProvider, txConfig: txConfig, connectionType: connectionType, - namespace: namespace, - includeShellReceipts: shouldIncludeSynthetic(namespace), + includeShellReceipts: false, + namespace: "eth", } } +func NewSeiBlockAPI( + tmClient rpcclient.Client, + k *keeper.Keeper, + ctxProvider func(int64) sdk.Context, + txConfig client.TxConfig, + connectionType ConnectionType, + isPanicTx func(ctx context.Context, hash common.Hash) (bool, error), +) *SeiBlockAPI { + blockAPI := &BlockAPI{ + tmClient: tmClient, + keeper: k, + ctxProvider: ctxProvider, + txConfig: txConfig, + connectionType: connectionType, + includeShellReceipts: true, + namespace: "sei", + } + return &SeiBlockAPI{ + BlockAPI: blockAPI, + isPanicTx: isPanicTx, + } +} + +func (a *SeiBlockAPI) GetBlockByNumberExcludeTraceFail(ctx context.Context, number rpc.BlockNumber, fullTx bool) (result map[string]interface{}, returnErr error) { + return a.getBlockByNumber(ctx, number, fullTx, a.isPanicTx) +} + func (a *BlockAPI) GetBlockTransactionCountByNumber(ctx context.Context, number rpc.BlockNumber) (result *hexutil.Uint, returnErr error) { startTime := time.Now() defer recordMetrics(fmt.Sprintf("%s_getBlockTransactionCountByNumber", a.namespace), a.connectionType, startTime, returnErr == nil) @@ -76,12 +108,20 @@ func (a *BlockAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash } func (a *BlockAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { - startTime := time.Now() - defer recordMetrics(fmt.Sprintf("%s_getBlockByHash", a.namespace), a.connectionType, startTime, returnErr == nil) - return a.getBlockByHash(ctx, blockHash, fullTx) + return a.getBlockByHash(ctx, blockHash, fullTx, nil) +} + +func (a *SeiBlockAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { + return a.getBlockByHash(ctx, blockHash, fullTx, nil) +} + +func (a *SeiBlockAPI) GetBlockByHashExcludeTraceFail(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { + return a.getBlockByHash(ctx, blockHash, fullTx, a.isPanicTx) } -func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { +func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool, isPanicTx func(ctx context.Context, hash common.Hash) (bool, error)) (result map[string]interface{}, returnErr error) { + startTime := time.Now() + defer recordMetrics(fmt.Sprintf("%s_getBlockByHash", a.namespace), a.connectionType, startTime, returnErr == nil) block, err := blockByHashWithRetry(ctx, a.tmClient, blockHash[:], 1) if err != nil { return nil, err @@ -91,7 +131,7 @@ func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fu return nil, err } blockBloom := a.keeper.GetBlockBloom(a.ctxProvider(block.Block.Height)) - return EncodeTmBlock(a.ctxProvider(block.Block.Height), block, blockRes, blockBloom, a.keeper, a.txConfig.TxDecoder(), fullTx, a.includeShellReceipts) + return EncodeTmBlock(a.ctxProvider(block.Block.Height), block, blockRes, blockBloom, a.keeper, a.txConfig.TxDecoder(), fullTx, a.includeShellReceipts, isPanicTx) } func (a *BlockAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (result map[string]interface{}, returnErr error) { @@ -101,7 +141,7 @@ func (a *BlockAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, // for compatibility with the graph, always return genesis block return map[string]interface{}{ "number": (*hexutil.Big)(big.NewInt(0)), - "hash": common.HexToHash("F9D3845DF25B43B1C6926F3CEDA6845C17F5624E12212FD8847D0BA01DA1AB9E"), + "hash": "0xF9D3845DF25B43B1C6926F3CEDA6845C17F5624E12212FD8847D0BA01DA1AB9E", "parentHash": common.Hash{}, "nonce": ethtypes.BlockNonce{}, // inapplicable to Sei "mixHash": common.Hash{}, // inapplicable to Sei @@ -122,10 +162,17 @@ func (a *BlockAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, "baseFeePerGas": (*hexutil.Big)(big.NewInt(0)), }, nil } - return a.getBlockByNumber(ctx, number, fullTx) + return a.getBlockByNumber(ctx, number, fullTx, nil) } -func (a *BlockAPI) getBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (result map[string]interface{}, returnErr error) { +func (a *BlockAPI) getBlockByNumber( + ctx context.Context, + number rpc.BlockNumber, + fullTx bool, + isPanicTx func(ctx context.Context, hash common.Hash) (bool, error), +) (result map[string]interface{}, returnErr error) { + startTime := time.Now() + defer recordMetrics(fmt.Sprintf("%s_getBlockByNumber", a.namespace), a.connectionType, startTime, returnErr == nil) numberPtr, err := getBlockNumber(ctx, a.tmClient, number) if err != nil { return nil, err @@ -139,7 +186,7 @@ func (a *BlockAPI) getBlockByNumber(ctx context.Context, number rpc.BlockNumber, return nil, err } blockBloom := a.keeper.GetBlockBloom(a.ctxProvider(block.Block.Height)) - return EncodeTmBlock(a.ctxProvider(block.Block.Height), block, blockRes, blockBloom, a.keeper, a.txConfig.TxDecoder(), fullTx, a.includeShellReceipts) + return EncodeTmBlock(a.ctxProvider(block.Block.Height), block, blockRes, blockBloom, a.keeper, a.txConfig.TxDecoder(), fullTx, a.includeShellReceipts, isPanicTx) } func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (result []map[string]interface{}, returnErr error) { @@ -215,6 +262,7 @@ func EncodeTmBlock( txDecoder sdk.TxDecoder, fullTx bool, includeSyntheticTxs bool, + isPanicTx func(ctx context.Context, hash common.Hash) (bool, error), ) (map[string]interface{}, error) { number := big.NewInt(block.Block.Height) blockhash := common.HexToHash(block.BlockID.Hash.String()) @@ -243,16 +291,25 @@ func EncodeTmBlock( } ethtx, _ := m.AsTransaction() hash := ethtx.Hash() - if !fullTx { - transactions = append(transactions, hash) - } else { - receipt, err := k.GetReceipt(ctx, hash) + if isPanicTx != nil { + isPanic, err := isPanicTx(ctx.Context(), hash) if err != nil { - continue + return nil, fmt.Errorf("failed to check if tx is panic tx: %w", err) } - if !includeSyntheticTxs && receipt.TxType == ShellEVMTxType { + if isPanic { continue } + } + receipt, err := k.GetReceipt(ctx, hash) + if err != nil { + continue + } + if !includeSyntheticTxs && receipt.TxType == ShellEVMTxType { + continue + } + if !fullTx { + transactions = append(transactions, hash) + } else { newTx := ethapi.NewRPCTransaction(ethtx, blockhash, number.Uint64(), uint64(blockTime.Second()), uint64(receipt.TransactionIndex), baseFeePerGas, chainConfig) transactions = append(transactions, newTx) } diff --git a/evmrpc/block_test.go b/evmrpc/block_test.go index 398679d58..642edf443 100644 --- a/evmrpc/block_test.go +++ b/evmrpc/block_test.go @@ -34,6 +34,13 @@ func TestGetSeiBlockByHash(t *testing.T) { verifyBlockResult(t, resObj) } +func TestGetSeiBlockByNumberExcludeTraceFail(t *testing.T) { + resObj := sendSeiRequestGood(t, "getBlockByNumberExcludeTraceFail", "0x67", true) + // first tx is not a panic tx, second tx is a panic tx + expectedNumTxs := 1 + require.Equal(t, expectedNumTxs, len(resObj["result"].(map[string]interface{})["transactions"].([]interface{}))) +} + func TestGetBlockByNumber(t *testing.T) { resObjEarliest := sendSeiRequestGood(t, "getBlockByNumber", "earliest", true) verifyGenesisBlockResult(t, resObjEarliest) @@ -96,7 +103,7 @@ func TestGetBlockReceipts(t *testing.T) { require.Equal(t, 6, len(result)) // Query by block hash - resObj2 := sendRequestGood(t, "getBlockReceipts", "0x0000000000000000000000000000000000000000000000000000000000000002") + resObj2 := sendRequestGood(t, "getBlockReceipts", MultiTxBlockHash) result = resObj2["result"].([]interface{}) require.Equal(t, 3, len(result)) receipt1 = result[0].(map[string]interface{}) @@ -125,21 +132,9 @@ func verifyGenesisBlockResult(t *testing.T, resObj map[string]interface{}) { require.Equal(t, "0x", resObj["extraData"]) require.Equal(t, "0x0", resObj["gasLimit"]) require.Equal(t, "0x0", resObj["gasUsed"]) - require.Equal(t, "0xf9d3845df25b43b1c6926f3ceda6845c17f5624e12212fd8847d0ba01da1ab9e", resObj["hash"]) - require.Equal(t, "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", resObj["logsBloom"]) - require.Equal(t, "0x0000000000000000000000000000000000000000", resObj["miner"]) - require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", resObj["mixHash"]) + require.Equal(t, "0xF9D3845DF25B43B1C6926F3CEDA6845C17F5624E12212FD8847D0BA01DA1AB9E", resObj["hash"]) require.Equal(t, "0x0000000000000000", resObj["nonce"]) require.Equal(t, "0x0", resObj["number"]) - require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", resObj["parentHash"]) - require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", resObj["receiptsRoot"]) - require.Equal(t, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", resObj["sha3Uncles"]) - require.Equal(t, "0x0", resObj["size"]) - require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", resObj["stateRoot"]) - require.Equal(t, "0x0", resObj["timestamp"]) - require.Equal(t, []interface{}{}, resObj["transactions"]) - require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", resObj["transactionsRoot"]) - require.Equal(t, []interface{}{}, resObj["uncles"]) } func verifyBlockResult(t *testing.T, resObj map[string]interface{}) { @@ -210,7 +205,7 @@ func TestEncodeTmBlock_EmptyTransactions(t *testing.T) { } // Call EncodeTmBlock with empty transactions - result, err := evmrpc.EncodeTmBlock(ctx, block, blockRes, ethtypes.Bloom{}, k, Decoder, true, false) + result, err := evmrpc.EncodeTmBlock(ctx, block, blockRes, ethtypes.Bloom{}, k, Decoder, true, false, nil) require.Nil(t, err) // Assert txHash is equal to ethtypes.EmptyTxsHash @@ -256,7 +251,7 @@ func TestEncodeBankMsg(t *testing.T) { }, }, } - res, err := evmrpc.EncodeTmBlock(ctx, &resBlock, &resBlockRes, ethtypes.Bloom{}, k, Decoder, true, false) + res, err := evmrpc.EncodeTmBlock(ctx, &resBlock, &resBlockRes, ethtypes.Bloom{}, k, Decoder, true, false, nil) require.Nil(t, err) txs := res["transactions"].([]interface{}) require.Equal(t, 0, len(txs)) @@ -304,7 +299,7 @@ func TestEncodeWasmExecuteMsg(t *testing.T) { }, }, } - res, err := evmrpc.EncodeTmBlock(ctx, &resBlock, &resBlockRes, ethtypes.Bloom{}, k, Decoder, true, true) + res, err := evmrpc.EncodeTmBlock(ctx, &resBlock, &resBlockRes, ethtypes.Bloom{}, k, Decoder, true, true, nil) require.Nil(t, err) txs := res["transactions"].([]interface{}) require.Equal(t, 1, len(txs)) diff --git a/evmrpc/info_test.go b/evmrpc/info_test.go index 016bda147..3d58b0ee0 100644 --- a/evmrpc/info_test.go +++ b/evmrpc/info_test.go @@ -2,7 +2,6 @@ package evmrpc_test import ( "errors" - "fmt" "math/big" "testing" @@ -113,7 +112,6 @@ func TestFeeHistory(t *testing.T) { reward, ok := rewards[0].([]interface{}) require.True(t, ok, "Expected reward to be a slice of interfaces") require.Equal(t, 1, len(reward), "Expected exactly one sub-item in reward") - fmt.Println("resObj", resObj) require.Equal(t, tc.expectedReward, reward[0].(string), "Reward does not match expected value") diff --git a/evmrpc/server.go b/evmrpc/server.go index 9691d5b07..c3d40a93f 100644 --- a/evmrpc/server.go +++ b/evmrpc/server.go @@ -1,10 +1,12 @@ package evmrpc import ( + "context" "strings" "github.com/cosmos/cosmos-sdk/client" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rpc" evmCfg "github.com/sei-protocol/sei-chain/x/evm/config" "github.com/sei-protocol/sei-chain/x/evm/keeper" @@ -31,6 +33,7 @@ func NewEVMHTTPServer( ctxProvider func(int64) sdk.Context, txConfig client.TxConfig, homeDir string, + isPanicTxFunc func(ctx context.Context, hash common.Hash) (bool, error), // optional - for testing ) (EVMServer, error) { httpServer := NewHTTPServer(logger, rpc.HTTPTimeouts{ ReadTimeout: config.ReadTimeout, @@ -46,7 +49,14 @@ func NewEVMHTTPServer( ctx := ctxProvider(LatestCtxHeight) txAPI := NewTransactionAPI(tmClient, k, ctxProvider, txConfig, homeDir, ConnectionTypeHTTP) - // filterAPI := NewFilterAPI(tmClient, &LogFetcher{tmClient: tmClient, k: k, ctxProvider: ctxProvider}, &FilterConfig{timeout: config.FilterTimeout, maxLog: config.MaxLogNoBlock, maxBlock: config.MaxBlocksForLog}, ConnectionTypeHTTP) + debugAPI := NewDebugAPI(tmClient, k, ctxProvider, txConfig.TxDecoder(), simulateConfig, ConnectionTypeHTTP) + if isPanicTxFunc == nil { + isPanicTxFunc = func(ctx context.Context, hash common.Hash) (bool, error) { + return debugAPI.isPanicTx(ctx, hash) + } + } + seiTxAPI := NewSeiTransactionAPI(tmClient, k, ctxProvider, txConfig, homeDir, ConnectionTypeHTTP, isPanicTxFunc) + seiDebugAPI := NewSeiDebugAPI(tmClient, k, ctxProvider, txConfig.TxDecoder(), simulateConfig, ConnectionTypeHTTP) apis := []rpc.API{ { @@ -55,16 +65,20 @@ func NewEVMHTTPServer( }, { Namespace: "eth", - Service: NewBlockAPI(tmClient, k, ctxProvider, txConfig, ConnectionTypeHTTP, "eth"), + Service: NewBlockAPI(tmClient, k, ctxProvider, txConfig, ConnectionTypeHTTP), }, { Namespace: "sei", - Service: NewBlockAPI(tmClient, k, ctxProvider, txConfig, ConnectionTypeHTTP, "sei"), + Service: NewSeiBlockAPI(tmClient, k, ctxProvider, txConfig, ConnectionTypeHTTP, isPanicTxFunc), }, { Namespace: "eth", Service: txAPI, }, + { + Namespace: "sei", + Service: seiTxAPI, + }, { Namespace: "eth", Service: NewStateAPI(tmClient, k, ctxProvider, ConnectionTypeHTTP), @@ -107,7 +121,11 @@ func NewEVMHTTPServer( }, { Namespace: "debug", - Service: NewDebugAPI(tmClient, k, ctxProvider, txConfig.TxDecoder(), simulateConfig, ConnectionTypeHTTP), + Service: debugAPI, + }, + { + Namespace: "sei", + Service: seiDebugAPI, }, } // Test API can only exist on non-live chain IDs. These APIs instrument certain overrides. @@ -156,7 +174,7 @@ func NewEVMWebSocketServer( }, { Namespace: "eth", - Service: NewBlockAPI(tmClient, k, ctxProvider, txConfig, ConnectionTypeWS, "eth"), + Service: NewBlockAPI(tmClient, k, ctxProvider, txConfig, ConnectionTypeWS), }, { Namespace: "eth", diff --git a/evmrpc/setup_test.go b/evmrpc/setup_test.go index 1097d8c98..21101ae10 100644 --- a/evmrpc/setup_test.go +++ b/evmrpc/setup_test.go @@ -47,18 +47,25 @@ const TestPort = 7777 const TestWSPort = 7778 const TestBadPort = 7779 +const GenesisBlockHeight = 0 const MockHeight8 = 8 const MockHeight2 = 2 +const MockHeight103 = 103 const MockHeight101 = 101 const MockHeight100 = 100 var DebugTraceHashHex = "0x1234567890123456789023456789012345678901234567890123456789000004" -var DebugTraceBlockHash = "BE17E0261E539CB7E9A91E123A6D794E0163D656FCF9B8EAC07823F7ED28512B" -var MultiTxBlockHash = "0000000000000000000000000000000000000000000000000000000000000002" +var DebugTraceBlockHash = "0xBE17E0261E539CB7E9A91E123A6D794E0163D656FCF9B8EAC07823F7ED28512B" +var DebugTracePanicBlockHash = "0x0000000000000000000000000000000000000000000000000000000000000003" +var MultiTxBlockHash = "0x0000000000000000000000000000000000000000000000000000000000000002" var TestCosmosTxHash = "690D39ADF56D4C811B766DFCD729A415C36C4BFFE80D63E305373B9518EBFB14" var TestEvmTxHash = "0xf02362077ac075a397344172496b28e913ce5294879d811bb0269b3be20a872e" +var TestNonPanicTxHash = "0x566f1c956c74b089643a1e6f880ac65745de0e5cd8cfc3c7482d20a486576219" +var TestPanicTxHash = "0x0ea197de8403de9c2e8cf9ec724e43734e9dbd3a8294a09d031acd67914b73e4" +var TestBlockHash = "0x0000000000000000000000000000000000000000000000000000000000000001" + var EncodingConfig = app.MakeEncodingConfig() var TxConfig = EncodingConfig.TxConfig var Encoder = TxConfig.TxEncoder() @@ -77,6 +84,8 @@ var multiTxBlockTx4 *ethtypes.Transaction var multiTxBlockSynthTx *ethtypes.Transaction var DebugTraceTx sdk.Tx +var DebugTracePanicTx sdk.Tx +var DebugTraceNonPanicTx sdk.Tx var TxNonEvm sdk.Tx var TxNonEvmWithSyntheticLog sdk.Tx var UnconfirmedTx sdk.Tx @@ -87,11 +96,11 @@ var filterTimeoutDuration = 500 * time.Millisecond var TotalTxCount int = 11 var MockBlockID = tmtypes.BlockID{ - Hash: bytes.HexBytes(mustHexToBytes("0000000000000000000000000000000000000000000000000000000000000001")), + Hash: bytes.HexBytes(mustHexToBytes(TestBlockHash[2:])), } var MockBlockIDMultiTx = tmtypes.BlockID{ - Hash: bytes.HexBytes(mustHexToBytes(MultiTxBlockHash)), + Hash: bytes.HexBytes(mustHexToBytes(MultiTxBlockHash[2:])), } var NewHeadsCalled = make(chan struct{}) @@ -217,6 +226,18 @@ func (c *MockClient) mockBlock(height int64) *coretypes.ResultBlock { }(), } } + if height == MockHeight103 { + res.Block.Data.Txs = []tmtypes.Tx{ + func() []byte { + bz, _ := Encoder(DebugTraceNonPanicTx) // reuse the same tx for non-panic tx + return bz + }(), + func() []byte { + bz, _ := Encoder(DebugTracePanicTx) + return bz + }(), + } + } return res } @@ -278,24 +299,68 @@ func (c *MockClient) Genesis(context.Context) (*coretypes.ResultGenesis, error) } func (c *MockClient) Block(_ context.Context, h *int64) (*coretypes.ResultBlock, error) { - height := int64(MockHeight8) - if h != nil { - height = *h + if h == nil { + return c.mockBlock(MockHeight8), nil } - return c.mockBlock(height), nil + return c.mockBlock(*h), nil } func (c *MockClient) BlockByHash(_ context.Context, hash bytes.HexBytes) (*coretypes.ResultBlock, error) { - if hash.String() == DebugTraceBlockHash { + if hash.String() == DebugTraceBlockHash[2:] { return c.mockBlock(MockHeight101), nil } - if hash.String() == MultiTxBlockHash { + if hash.String() == DebugTracePanicBlockHash[2:] { + return c.mockBlock(MockHeight103), nil + } + if hash.String() == MultiTxBlockHash[2:] { return c.mockBlock(MockHeight2), nil } return c.mockBlock(MockHeight8), nil } func (c *MockClient) BlockResults(_ context.Context, height *int64) (*coretypes.ResultBlockResults, error) { + if *height == GenesisBlockHeight { + return &coretypes.ResultBlockResults{ + TxsResults: []*abci.ExecTxResult{ + { + Data: []byte{}, + GasWanted: 0, + GasUsed: 0, + }, + }, + ConsensusParamUpdates: &types2.ConsensusParams{ + Block: &types2.BlockParams{ + MaxBytes: 100000000, + MaxGas: 200000000, + }, + }, + }, nil + } + if *height == MockHeight103 { + TxResults := []*abci.ExecTxResult{ + { + Data: func() []byte { + bz, _ := Encoder(DebugTraceTx) + return bz + }(), + GasWanted: 10, + GasUsed: 5, + }, + { + Data: func() []byte { + bz, _ := Encoder(DebugTracePanicTx) + return bz + }(), + GasWanted: 10, + GasUsed: 5, + }, + } + return &coretypes.ResultBlockResults{TxsResults: TxResults, ConsensusParamUpdates: &types2.ConsensusParams{ + Block: &types2.BlockParams{ + MaxGas: 100000000, + }, + }}, nil + } return &coretypes.ResultBlockResults{ TxsResults: []*abci.ExecTxResult{ { @@ -474,7 +539,7 @@ func init() { if err != nil { panic(err) } - HttpServer, err := evmrpc.NewEVMHTTPServer(infoLog, goodConfig, &MockClient{}, EVMKeeper, ctxProvider, TxConfig, "") + HttpServer, err := evmrpc.NewEVMHTTPServer(infoLog, goodConfig, &MockClient{}, EVMKeeper, ctxProvider, TxConfig, "", isPanicTxFunc) if err != nil { panic(err) } @@ -486,7 +551,7 @@ func init() { badConfig := evmrpc.DefaultConfig badConfig.HTTPPort = TestBadPort badConfig.FilterTimeout = 500 * time.Millisecond - badHTTPServer, err := evmrpc.NewEVMHTTPServer(infoLog, badConfig, &MockBadClient{}, EVMKeeper, ctxProvider, TxConfig, "") + badHTTPServer, err := evmrpc.NewEVMHTTPServer(infoLog, badConfig, &MockBadClient{}, EVMKeeper, ctxProvider, TxConfig, "", nil) if err != nil { panic(err) } @@ -579,6 +644,24 @@ func generateTxData() { Data: []byte("abc"), ChainID: chainId, }) + debugTraceNonPanicTxBuilder, _ := buildTx(ethtypes.DynamicFeeTx{ + Nonce: 0, + GasFeeCap: big.NewInt(10), + Gas: 22000, + To: &to, + Value: big.NewInt(1000), + Data: []byte("abc"), + ChainID: chainId, + }) + debugTracePanicTxBuilder, _ := buildTx(ethtypes.DynamicFeeTx{ + Nonce: 100, // set a high nonce to make sure it will panic upon simulation + GasFeeCap: big.NewInt(10), + Gas: 22000, + To: &to, + Value: big.NewInt(1000), + Data: []byte("abc"), + ChainID: chainId, + }) Tx1 = txBuilder1.GetTx() MultiTxBlockTx1 = txBuilder1_5.GetTx() MultiTxBlockTx2 = txBuilder2.GetTx() @@ -586,6 +669,8 @@ func generateTxData() { MultiTxBlockTx4 = txBuilder4.GetTx() MultiTxBlockSynthTx = synthTxBuilder.GetTx() DebugTraceTx = debugTraceTxBuilder.GetTx() + DebugTracePanicTx = debugTracePanicTxBuilder.GetTx() + DebugTraceNonPanicTx = debugTraceNonPanicTxBuilder.GetTx() TxNonEvm = app.TestTx{} TxNonEvmWithSyntheticLog = app.TestTx{} bloomTx1 := ethtypes.CreateBloom(ethtypes.Receipts{ðtypes.Receipt{Logs: []*ethtypes.Log{{ @@ -797,6 +882,17 @@ func setupLogs() { TransactionIndex: 0, TxHashHex: DebugTraceHashHex, }) + CtxDebugTracePanic := Ctx.WithBlockHeight(MockHeight103) + EVMKeeper.MockReceipt(CtxDebugTracePanic, common.HexToHash(TestNonPanicTxHash), &types.Receipt{ + BlockNumber: MockHeight103, + TransactionIndex: 0, + TxHashHex: TestNonPanicTxHash, + }) + EVMKeeper.MockReceipt(CtxDebugTracePanic, common.HexToHash(TestPanicTxHash), &types.Receipt{ + BlockNumber: MockHeight103, + TransactionIndex: 1, + TxHashHex: TestPanicTxHash, + }) txNonEvmBz, _ := Encoder(TxNonEvmWithSyntheticLog) txNonEvmHash := sha256.Sum256(txNonEvmBz) EVMKeeper.MockReceipt(CtxMultiTx, txNonEvmHash, &types.Receipt{ @@ -1025,3 +1121,10 @@ func TestEcho(t *testing.T) { require.Nil(t, err) require.Equal(t, "{\"jsonrpc\":\"2.0\",\"id\":\"test\",\"result\":\"something\"}\n", string(buf)) } + +func isPanicTxFunc(ctx context.Context, hash common.Hash) (bool, error) { + if hash == common.HexToHash(TestPanicTxHash) { + return true, nil + } + return false, nil +} diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index 32645d690..cb4ea0a89 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -2,6 +2,7 @@ package evmrpc import ( "context" + "fmt" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,10 +11,16 @@ import ( _ "github.com/ethereum/go-ethereum/eth/tracers/native" // run init()s to register native tracers "github.com/ethereum/go-ethereum/lib/ethapi" "github.com/ethereum/go-ethereum/rpc" + "github.com/hashicorp/golang-lru/v2/expirable" "github.com/sei-protocol/sei-chain/x/evm/keeper" rpcclient "github.com/tendermint/tendermint/rpc/client" ) +const ( + IsPanicCacheSize = 5000 + IsPanicCacheTTL = 1 * time.Minute +) + type DebugAPI struct { tracersAPI *tracers.API tmClient rpcclient.Client @@ -21,12 +28,42 @@ type DebugAPI struct { ctxProvider func(int64) sdk.Context txDecoder sdk.TxDecoder connectionType ConnectionType + isPanicCache *expirable.LRU[common.Hash, bool] // hash to isPanic +} + +type SeiDebugAPI struct { + *DebugAPI } func NewDebugAPI(tmClient rpcclient.Client, k *keeper.Keeper, ctxProvider func(int64) sdk.Context, txDecoder sdk.TxDecoder, config *SimulateConfig, connectionType ConnectionType) *DebugAPI { backend := NewBackend(ctxProvider, k, txDecoder, tmClient, config) tracersAPI := tracers.NewAPI(backend) - return &DebugAPI{tracersAPI: tracersAPI, tmClient: tmClient, keeper: k, ctxProvider: ctxProvider, txDecoder: txDecoder, connectionType: connectionType} + evictCallback := func(key common.Hash, value bool) {} + isPanicCache := expirable.NewLRU[common.Hash, bool](IsPanicCacheSize, evictCallback, IsPanicCacheTTL) + return &DebugAPI{ + tracersAPI: tracersAPI, + tmClient: tmClient, + keeper: k, + ctxProvider: ctxProvider, + txDecoder: txDecoder, + connectionType: connectionType, + isPanicCache: isPanicCache, + } +} + +func NewSeiDebugAPI( + tmClient rpcclient.Client, + k *keeper.Keeper, + ctxProvider func(int64) sdk.Context, + txDecoder sdk.TxDecoder, + config *SimulateConfig, + connectionType ConnectionType, +) *SeiDebugAPI { + backend := NewBackend(ctxProvider, k, txDecoder, tmClient, config) + tracersAPI := tracers.NewAPI(backend) + return &SeiDebugAPI{ + DebugAPI: &DebugAPI{tracersAPI: tracersAPI, tmClient: tmClient, keeper: k, ctxProvider: ctxProvider, txDecoder: txDecoder, connectionType: connectionType}, + } } func (api *DebugAPI) TraceTransaction(ctx context.Context, hash common.Hash, config *tracers.TraceConfig) (result interface{}, returnErr error) { @@ -36,6 +73,59 @@ func (api *DebugAPI) TraceTransaction(ctx context.Context, hash common.Hash, con return } +func (api *SeiDebugAPI) TraceBlockByNumberExcludeTraceFail(ctx context.Context, number rpc.BlockNumber, config *tracers.TraceConfig) (result interface{}, returnErr error) { + startTime := time.Now() + defer recordMetrics("sei_traceBlockByNumberExcludeTraceFail", api.connectionType, startTime, returnErr == nil) + result, returnErr = api.tracersAPI.TraceBlockByNumber(ctx, number, config) + traces, ok := result.([]*tracers.TxTraceResult) + if !ok { + return nil, fmt.Errorf("unexpected type: %T", result) + } + finalTraces := make([]*tracers.TxTraceResult, 0) + for _, trace := range traces { + if len(trace.Error) > 0 { + continue + } + finalTraces = append(finalTraces, trace) + } + return finalTraces, nil +} + +func (api *DebugAPI) isPanicTx(ctx context.Context, hash common.Hash) (isPanic bool, err error) { + sdkctx := api.ctxProvider(LatestCtxHeight) + receipt, err := api.keeper.GetReceipt(sdkctx, hash) + if err != nil { + return false, err + } + height := receipt.BlockNumber + + isPanic, ok := api.isPanicCache.Get(hash) + if ok { + return isPanic, nil + } + + callTracer := "callTracer" + tracersResult, err := api.tracersAPI.TraceBlockByNumber(ctx, rpc.BlockNumber(height), &tracers.TraceConfig{ + Tracer: &callTracer, + }) + if err != nil { + return false, err + } + + result := false + for _, trace := range tracersResult { + if trace.TxHash == hash { + result = len(trace.Error) > 0 + } + if len(trace.Error) > 0 { + api.isPanicCache.Add(trace.TxHash, true) + } else { + api.isPanicCache.Add(trace.TxHash, false) + } + } + return result, nil +} + func (api *DebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, config *tracers.TraceConfig) (result interface{}, returnErr error) { startTime := time.Now() defer recordMetrics("debug_traceBlockByNumber", api.connectionType, startTime, returnErr == nil) diff --git a/evmrpc/tracers_test.go b/evmrpc/tracers_test.go index 9667758de..ff86fb914 100644 --- a/evmrpc/tracers_test.go +++ b/evmrpc/tracers_test.go @@ -86,3 +86,15 @@ func TestTraceCall(t *testing.T) { require.Equal(t, float64(21000), result["gas"]) require.Equal(t, false, result["failed"]) } + +func TestTraceBlockByNumberExcludeTraceFail(t *testing.T) { + args := map[string]interface{}{} + args["tracer"] = "callTracer" + seiResObj := sendRequestGoodWithNamespace(t, "sei", "traceBlockByNumberExcludeTraceFail", "0x67", args) + result := seiResObj["result"].([]interface{}) + // sei_traceBlockByNumber returns 1 trace, and removes the panic tx + require.Equal(t, 1, len(result)) + ethResObj := sendRequestGoodWithNamespace(t, "debug", "traceBlockByNumber", "0x67", args) + // eth_traceBlockByNumber returns 2 traces, including the panic tx + require.Equal(t, 2, len(ethResObj["result"].([]interface{}))) +} diff --git a/evmrpc/tx.go b/evmrpc/tx.go index d4d450e3a..30fee1afe 100644 --- a/evmrpc/tx.go +++ b/evmrpc/tx.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "errors" + "fmt" "math/big" "strings" "time" @@ -24,6 +25,8 @@ import ( tmtypes "github.com/tendermint/tendermint/types" ) +var ErrPanicTx = errors.New("transaction is panic tx") + const UnconfirmedTxQueryMaxPage = 20 const UnconfirmedTxQueryPerPage = 30 @@ -36,14 +39,55 @@ type TransactionAPI struct { connectionType ConnectionType } +type SeiTransactionAPI struct { + *TransactionAPI + isPanicTx func(ctx context.Context, hash common.Hash) (bool, error) +} + func NewTransactionAPI(tmClient rpcclient.Client, k *keeper.Keeper, ctxProvider func(int64) sdk.Context, txConfig client.TxConfig, homeDir string, connectionType ConnectionType) *TransactionAPI { return &TransactionAPI{tmClient: tmClient, keeper: k, ctxProvider: ctxProvider, txConfig: txConfig, homeDir: homeDir, connectionType: connectionType} } +func NewSeiTransactionAPI( + tmClient rpcclient.Client, + k *keeper.Keeper, + ctxProvider func(int64) sdk.Context, txConfig client.TxConfig, + homeDir string, + connectionType ConnectionType, + isPanicTx func(ctx context.Context, hash common.Hash) (bool, error), +) *SeiTransactionAPI { + return &SeiTransactionAPI{TransactionAPI: NewTransactionAPI(tmClient, k, ctxProvider, txConfig, homeDir, connectionType), isPanicTx: isPanicTx} +} + +func (t *SeiTransactionAPI) GetTransactionReceiptExcludeTraceFail(ctx context.Context, hash common.Hash) (result map[string]interface{}, returnErr error) { + return getTransactionReceipt(ctx, t.TransactionAPI, hash, true, t.isPanicTx) +} + func (t *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash common.Hash) (result map[string]interface{}, returnErr error) { + return getTransactionReceipt(ctx, t, hash, false, nil) +} + +func getTransactionReceipt( + ctx context.Context, + t *TransactionAPI, + hash common.Hash, + excludePanicTxs bool, + isPanicTx func(ctx context.Context, hash common.Hash) (bool, error), +) (result map[string]interface{}, returnErr error) { startTime := time.Now() defer recordMetrics("eth_getTransactionReceipt", t.connectionType, startTime, returnErr == nil) sdkctx := t.ctxProvider(LatestCtxHeight) + + if excludePanicTxs { + isPanicTx, err := isPanicTx(ctx, hash) + if isPanicTx { + return nil, ErrPanicTx + } + if err != nil { + return nil, fmt.Errorf("failed to check if tx is panic tx: %w", err) + } + } + receipt, err := t.keeper.GetReceipt(sdkctx, hash) if err != nil { if strings.Contains(err.Error(), "not found") { diff --git a/evmrpc/tx_test.go b/evmrpc/tx_test.go index c5352d0fb..d30d60be3 100644 --- a/evmrpc/tx_test.go +++ b/evmrpc/tx_test.go @@ -289,3 +289,18 @@ func TestGetTransactionReceiptFailedTx(t *testing.T) { require.Equal(t, "0x0000000000000000000000000000000000010203", resObj["to"].(string)) require.Nil(t, resObj["contractAddress"]) } + +func TestGetTransactionReceiptExcludeTraceFail(t *testing.T) { + body := fmt.Sprintf("{\"jsonrpc\": \"2.0\",\"method\": \"%s_getTransactionReceiptExcludeTraceFail\",\"params\":[\"%s\"],\"id\":\"test\"}", "sei", TestPanicTxHash) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s:%d", TestAddr, TestPort), strings.NewReader(body)) + require.Nil(t, err) + req.Header.Set("Content-Type", "application/json") + res, err := http.DefaultClient.Do(req) + require.Nil(t, err) + resBody, err := io.ReadAll(res.Body) + require.Nil(t, err) + resObj := map[string]interface{}{} + require.Nil(t, json.Unmarshal(resBody, &resObj)) + require.Greater(t, len(resObj["error"].(map[string]interface{})["message"].(string)), 0) + require.Nil(t, resObj["result"]) +} diff --git a/go.mod b/go.mod index 22afa8101..43a6ae60d 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/holiman/uint256 v1.2.4 github.com/justinas/alice v1.2.0 github.com/k0kubun/pp/v3 v3.2.0 @@ -179,7 +180,6 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect - github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect @@ -350,7 +350,7 @@ replace ( github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.48 github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.2.0 github.com/cosmos/ibc-go/v3 => github.com/sei-protocol/sei-ibc-go/v3 v3.3.3 - github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-25 + github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-9.0.20241222173138-aac6bf6d096a github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 github.com/sei-protocol/sei-db => github.com/sei-protocol/sei-db v0.0.46 // Latest goleveldb is broken, we have to stick to this version diff --git a/go.sum b/go.sum index e18354e50..0f5c88eef 100644 --- a/go.sum +++ b/go.sum @@ -807,8 +807,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= -github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -1341,8 +1341,8 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod github.com/securego/gosec/v2 v2.11.0 h1:+PDkpzR41OI2jrw1q6AdXZCbsNGNGT7pQjal0H0cArI= github.com/securego/gosec/v2 v2.11.0/go.mod h1:SX8bptShuG8reGC0XS09+a4H2BoWSJi+fscA+Pulbpo= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= -github.com/sei-protocol/go-ethereum v1.13.5-sei-25 h1:qj+AbDeIDMFiRdlOzJJZJVdTJoKA2uvyznhs6l9c5LE= -github.com/sei-protocol/go-ethereum v1.13.5-sei-25/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ= +github.com/sei-protocol/go-ethereum v1.13.5-sei-9.0.20241222173138-aac6bf6d096a h1:chN3ln8ctvK3TOrQ+l7/kBAsh/JCQYhdzGQ8TXKcpio= +github.com/sei-protocol/go-ethereum v1.13.5-sei-9.0.20241222173138-aac6bf6d096a/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ= github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQpIbXDA= github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8= github.com/sei-protocol/sei-cosmos v0.3.48 h1:kSDweeTaLZ4TByLqAD6/hmtgAhAJHwXU1beyqsVXJkQ=