diff --git a/giga/evmonly/rpc/call.go b/giga/evmonly/rpc/call.go index 53ad6c720d..d02cae03bf 100644 --- a/giga/evmonly/rpc/call.go +++ b/giga/evmonly/rpc/call.go @@ -29,8 +29,10 @@ func (api *callAPI) Call(ctx context.Context, args export.TransactionArgs, block if err := requireCurrentState(block); err != nil { return nil, err } - // Must match the base fee EvmCall executes under (evmOnlyBaseFee). - baseFee := new(big.Int) + baseFee, err := api.backend.EvmBaseFee() + if err != nil { + return nil, err + } chainID := new(big.Int).SetUint64(api.backend.EvmChainID()) if err := args.CallDefaults(defaultCallGasCap, baseFee, chainID); err != nil { return nil, err diff --git a/giga/evmonly/rpc/call_test.go b/giga/evmonly/rpc/call_test.go index 8ab1dd67ea..7ec32b6b46 100644 --- a/giga/evmonly/rpc/call_test.go +++ b/giga/evmonly/rpc/call_test.go @@ -3,6 +3,7 @@ package rpc import ( "context" "errors" + "math/big" "net/http/httptest" "testing" @@ -40,6 +41,7 @@ func TestCallHappyPath(t *testing.T) { var gotMsg *core.Message backend := &testBackend{ chainID: func() uint64 { return 713715 }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, call: func(_ context.Context, msg *core.Message) (*core.ExecutionResult, error) { gotMsg = msg return &core.ExecutionResult{ReturnData: []byte{0x2a}}, nil @@ -65,6 +67,7 @@ func TestCallCapsExplicitGasAboveDefault(t *testing.T) { var gotMsg *core.Message backend := &testBackend{ chainID: func() uint64 { return 713715 }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, call: func(_ context.Context, msg *core.Message) (*core.ExecutionResult, error) { gotMsg = msg return &core.ExecutionResult{}, nil @@ -85,6 +88,7 @@ func TestCallPreservesExplicitGasBelowDefault(t *testing.T) { var gotMsg *core.Message backend := &testBackend{ chainID: func() uint64 { return 713715 }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, call: func(_ context.Context, msg *core.Message) (*core.ExecutionResult, error) { gotMsg = msg return &core.ExecutionResult{}, nil @@ -105,6 +109,7 @@ func TestCallSurfacesRevertReason(t *testing.T) { revert := revertData("insufficient balance") backend := &testBackend{ chainID: func() uint64 { return 713715 }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, call: func(context.Context, *core.Message) (*core.ExecutionResult, error) { return &core.ExecutionResult{Err: vm.ErrExecutionReverted, ReturnData: revert}, nil }, @@ -127,6 +132,7 @@ func TestCallPassesThroughNonRevertExecutionError(t *testing.T) { wantErr := errors.New("out of gas") backend := &testBackend{ chainID: func() uint64 { return 713715 }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, call: func(context.Context, *core.Message) (*core.ExecutionResult, error) { return &core.ExecutionResult{Err: wantErr}, nil }, @@ -164,6 +170,7 @@ func TestHandlerServesCall(t *testing.T) { to := common.HexToAddress("0x1000000000000000000000000000000000000001") backend := &testBackend{ chainID: func() uint64 { return 713715 }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, call: func(context.Context, *core.Message) (*core.ExecutionResult, error) { return &core.ExecutionResult{ReturnData: []byte{0x01, 0x02}}, nil }, diff --git a/giga/evmonly/rpc/server.go b/giga/evmonly/rpc/server.go index 1a39dbcd4e..d75c9eb6e4 100644 --- a/giga/evmonly/rpc/server.go +++ b/giga/evmonly/rpc/server.go @@ -5,12 +5,14 @@ import ( "context" "errors" "fmt" + "math/big" "net" "net/http" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" ethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/holiman/uint256" @@ -35,8 +37,10 @@ type Backend interface { Block(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) BroadcastTx(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) EvmBalance(common.Address) uint256.Int + EvmBaseFee() (*big.Int, error) EvmBlockNumber() uint64 EvmCall(context.Context, *core.Message) (*core.ExecutionResult, error) + EvmChainConfig() (*params.ChainConfig, error) EvmChainID() uint64 EvmProxy(common.Address) utils.Option[*ethrpc.Client] EvmProxyEnabled() bool diff --git a/giga/evmonly/rpc/setup_test.go b/giga/evmonly/rpc/setup_test.go index 63bfdb5a09..3cf2958ea6 100644 --- a/giga/evmonly/rpc/setup_test.go +++ b/giga/evmonly/rpc/setup_test.go @@ -2,9 +2,11 @@ package rpc import ( "context" + "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" ethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/holiman/uint256" @@ -14,10 +16,12 @@ import ( type testBackend struct { balance func(common.Address) uint256.Int + baseFee func() (*big.Int, error) block func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) blockNumber func() uint64 broadcast func(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) call func(context.Context, *core.Message) (*core.ExecutionResult, error) + chainConfig func() (*params.ChainConfig, error) chainID func() uint64 proxy utils.Option[*ethrpc.Client] proxyCalls int @@ -41,6 +45,10 @@ func (b *testBackend) EvmBalance(address common.Address) uint256.Int { return b.balance(address) } +func (b *testBackend) EvmBaseFee() (*big.Int, error) { + return b.baseFee() +} + func (b *testBackend) EvmProxyEnabled() bool { return b.proxy.IsPresent() } @@ -53,6 +61,10 @@ func (b *testBackend) EvmCall(ctx context.Context, msg *core.Message) (*core.Exe return b.call(ctx, msg) } +func (b *testBackend) EvmChainConfig() (*params.ChainConfig, error) { + return b.chainConfig() +} + func (b *testBackend) EvmChainID() uint64 { return b.chainID() } diff --git a/giga/evmonly/rpc/tx.go b/giga/evmonly/rpc/tx.go index df060f5012..0440be6715 100644 --- a/giga/evmonly/rpc/tx.go +++ b/giga/evmonly/rpc/tx.go @@ -10,10 +10,12 @@ 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/export" ethrpc "github.com/ethereum/go-ethereum/rpc" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" receiptpkg "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) @@ -34,32 +36,90 @@ func (api *txAPI) GetTransactionCount(_ context.Context, address common.Address, // GetTransactionReceipt returns the finalized Ethereum receipt for hash. func (api *txAPI) GetTransactionReceipt(ctx context.Context, hash common.Hash) (map[string]any, error) { + stored, block, err := api.lookupFinalizedTx(ctx, hash) + if err != nil || stored == nil { + return nil, err + } + return encodeReceipt(hash, stored, common.BytesToHash(block.BlockID.Hash)), nil +} + +// GetTransactionByHash returns hash's transaction as committed in a finalized +// block, decoded from the block's raw transaction bytes, or nil if hash is +// unknown or its block is not yet finalized. This server tracks no local +// mempool, so unlike a full Ethereum node it never returns a pending result. +func (api *txAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (*export.RPCTransaction, error) { + stored, block, err := api.lookupFinalizedTx(ctx, hash) + if err != nil || stored == nil { + return nil, err + } + if int(stored.TransactionIndex) >= len(block.Block.Txs) { + return nil, fmt.Errorf("receipt transaction index %d exceeds block %d transaction count %d", + stored.TransactionIndex, stored.BlockNumber, len(block.Block.Txs)) + } + ethtx := new(ethtypes.Transaction) + if err := ethtx.UnmarshalBinary(block.Block.Txs[stored.TransactionIndex]); err != nil { + return nil, fmt.Errorf("decode transaction at block %d index %d: %w", stored.BlockNumber, stored.TransactionIndex, err) + } + chainConfig, err := api.backend.EvmChainConfig() + if err != nil { + return nil, err + } + blockUnix, ok := utils.SafeCast[uint64](block.Block.Time.Unix()) + if !ok { + return nil, fmt.Errorf("block %d time is negative: %s", stored.BlockNumber, block.Block.Time) + } + baseFee, err := api.backend.EvmBaseFee() + if err != nil { + return nil, err + } + // TODO: If the EVM-only base fee becomes dynamic, read the fee for + // stored.BlockNumber here or persist it with the receipt. Using the current + // fee would misreport a historical transaction's effective gas price. + result := export.NewRPCTransaction(ethtx, common.BytesToHash(block.BlockID.Hash), stored.BlockNumber, blockUnix, + uint64(stored.TransactionIndex), baseFee, chainConfig) + replaceFrom(result, stored) + return result, nil +} + +// lookupFinalizedTx resolves hash's stored receipt and finalized block. It +// returns a nil receipt with a nil error when hash is unknown or its block is +// not yet finalized. +func (api *txAPI) lookupFinalizedTx(ctx context.Context, hash common.Hash) (*evmtypes.Receipt, *coretypes.ResultBlock, error) { stored, err := api.store.GetReceipt(receiptContext(ctx), hash) if errors.Is(err, receiptpkg.ErrNotFound) { - return nil, nil + return nil, nil, nil } if err != nil { - return nil, fmt.Errorf("read transaction receipt: %w", err) + return nil, nil, fmt.Errorf("read transaction receipt: %w", err) } if stored == nil { - return nil, errors.New("receipt store returned a nil receipt") + return nil, nil, errors.New("receipt store returned a nil receipt") } if stored.BlockNumber > math.MaxInt64 { - return nil, fmt.Errorf("receipt block number %d exceeds int64", stored.BlockNumber) + return nil, nil, fmt.Errorf("receipt block number %d exceeds int64", stored.BlockNumber) } height := coretypes.Int64(stored.BlockNumber) block, err := api.backend.Block(ctx, &coretypes.RequestBlockInfo{Height: &height}) if errors.Is(err, coretypes.ErrHeightExceedsChainHead) { - return nil, nil + return nil, nil, nil } if err != nil { - return nil, fmt.Errorf("read receipt block %d: %w", stored.BlockNumber, err) + return nil, nil, fmt.Errorf("read receipt block %d: %w", stored.BlockNumber, err) } if block == nil || block.Block == nil { - return nil, nil + return nil, nil, nil + } + return stored, block, nil +} + +// replaceFrom patches a decoded transaction's From field from its stored +// receipt when the tx's own signature did not resolve a sender, an edge case +// for some legacy transaction shapes. +func replaceFrom(tx *export.RPCTransaction, stored *evmtypes.Receipt) { + if tx.From == (common.Address{}) { + tx.From = common.HexToAddress(stored.From) } - return encodeReceipt(hash, stored, common.BytesToHash(block.BlockID.Hash)), nil } func encodeReceipt(hash common.Hash, stored *evmtypes.Receipt, blockHash common.Hash) map[string]any { diff --git a/giga/evmonly/rpc/tx_test.go b/giga/evmonly/rpc/tx_test.go index edc24f962b..8bdc03bc79 100644 --- a/giga/evmonly/rpc/tx_test.go +++ b/giga/evmonly/rpc/tx_test.go @@ -6,10 +6,14 @@ import ( "math/big" "net/http/httptest" "testing" + "time" "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/ethereum/go-ethereum/export" + "github.com/ethereum/go-ethereum/params" ethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/require" @@ -195,6 +199,227 @@ func TestGetTransactionReceiptReturnsNullBeforeBlockCommit(t *testing.T) { require.Nil(t, got) } +// testChainConfig returns the chain configuration evmOnlyApplication builds: +// every fork active from genesis, chain ID set to chainID. +func testChainConfig(chainID *big.Int) *params.ChainConfig { + cfg := *params.AllDevChainProtocolChanges + cfg.ChainID = chainID + return &cfg +} + +func TestGetTransactionByHash(t *testing.T) { + key, err := crypto.HexToECDSA("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + to := common.HexToAddress("0x2000000000000000000000000000000000000002") + chainID := big.NewInt(713715) + unsigned := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: 5, + GasTipCap: big.NewInt(1_500_000_000), + GasFeeCap: big.NewInt(2_000_000_000), + Gas: 50_000, + To: &to, + Value: big.NewInt(7), + Data: []byte{0xde, 0xad, 0xbe, 0xef}, + }) + tx, err := ethtypes.SignTx(unsigned, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := tx.MarshalBinary() + require.NoError(t, err) + + blockHash := common.HexToHash("0xabcd") + blockTime := time.Unix(1_700_000_000, 0) + store := evmonly.NewMemoryReceiptStore() + require.NoError(t, store.SetReceipts(sdk.Context{}.WithContext(t.Context()), []receipt.ReceiptRecord{{ + TxHash: tx.Hash(), + Receipt: &evmtypes.Receipt{ + TxHashHex: tx.Hash().Hex(), + BlockNumber: 9, + TransactionIndex: 1, + From: sender.Hex(), + }, + }})) + + chainConfig := testChainConfig(chainID) + backend := &testBackend{ + block: func(_ context.Context, req *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { + require.NotNil(t, req.Height) + require.Equal(t, coretypes.Int64(9), *req.Height) + return &coretypes.ResultBlock{ + BlockID: tmtypes.BlockID{Hash: blockHash.Bytes()}, + Block: &tmtypes.Block{ + Header: tmtypes.Header{Time: blockTime}, + Data: tmtypes.Data{Txs: tmtypes.Txs{[]byte("some other transaction"), raw}}, + }, + }, nil + }, + chainConfig: func() (*params.ChainConfig, error) { return chainConfig, nil }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, + } + + got, err := (&txAPI{backend: backend, store: store}).GetTransactionByHash(t.Context(), tx.Hash()) + + require.NoError(t, err) + require.NotNil(t, got) + require.Equal(t, tx.Hash(), got.Hash) + require.Equal(t, hexutil.Uint64(5), got.Nonce) + require.Equal(t, hexutil.Uint64(50_000), got.Gas) + require.Equal(t, sender, got.From) + require.Equal(t, &to, got.To) + require.Equal(t, big.NewInt(7), got.Value.ToInt()) + require.Equal(t, hexutil.Bytes{0xde, 0xad, 0xbe, 0xef}, got.Input) + require.Equal(t, big.NewInt(2_000_000_000), got.GasFeeCap.ToInt()) + require.Equal(t, big.NewInt(1_500_000_000), got.GasTipCap.ToInt()) + require.NotNil(t, got.BlockHash) + require.Equal(t, blockHash, *got.BlockHash) + require.NotNil(t, got.BlockNumber) + require.Equal(t, big.NewInt(9), got.BlockNumber.ToInt()) + require.NotNil(t, got.TransactionIndex) + require.Equal(t, hexutil.Uint64(1), *got.TransactionIndex) +} + +func TestGetTransactionByHashPatchesFromWhenSenderDoesNotRecover(t *testing.T) { + sender := common.HexToAddress("0x1000000000000000000000000000000000000001") + to := common.HexToAddress("0x2000000000000000000000000000000000000002") + // A legacy transaction protected (EIP-155) for a different chain ID than + // the backend's chain config fails sender recovery against that config's + // signer, exercising the same edge case evmrpc's transaction API patches + // from the receipt. + unsigned := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: 1, + GasPrice: big.NewInt(1_000_000_000), + Gas: 21_000, + To: &to, + Value: big.NewInt(1), + }) + key, err := crypto.HexToECDSA("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + require.NoError(t, err) + tx, err := ethtypes.SignTx(unsigned, ethtypes.NewEIP155Signer(big.NewInt(999)), key) + require.NoError(t, err) + raw, err := tx.MarshalBinary() + require.NoError(t, err) + + store := evmonly.NewMemoryReceiptStore() + require.NoError(t, store.SetReceipts(sdk.Context{}.WithContext(t.Context()), []receipt.ReceiptRecord{{ + TxHash: tx.Hash(), + Receipt: &evmtypes.Receipt{ + TxHashHex: tx.Hash().Hex(), + BlockNumber: 3, + TransactionIndex: 0, + From: sender.Hex(), + }, + }})) + chainConfig := testChainConfig(big.NewInt(713715)) + backend := &testBackend{ + block: func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { + return &coretypes.ResultBlock{ + BlockID: tmtypes.BlockID{Hash: common.HexToHash("0xabcd").Bytes()}, + Block: &tmtypes.Block{ + Header: tmtypes.Header{Time: time.Unix(1_700_000_000, 0)}, + Data: tmtypes.Data{Txs: tmtypes.Txs{raw}}, + }, + }, nil + }, + chainConfig: func() (*params.ChainConfig, error) { return chainConfig, nil }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, + } + + got, err := (&txAPI{backend: backend, store: store}).GetTransactionByHash(t.Context(), tx.Hash()) + + require.NoError(t, err) + require.NotNil(t, got) + require.Equal(t, sender, got.From) +} + +func TestGetTransactionByHashReturnsNullForUnknownHash(t *testing.T) { + backend := &testBackend{ + block: func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { + t.Fatal("block lookup reached for an unknown hash") + return nil, nil + }, + } + got, err := (&txAPI{backend: backend, store: evmonly.NewMemoryReceiptStore()}).GetTransactionByHash(t.Context(), common.Hash{9}) + require.NoError(t, err) + require.Nil(t, got) +} + +func TestGetTransactionByHashReturnsNullBeforeBlockCommit(t *testing.T) { + txHash := common.Hash{1} + store := evmonly.NewMemoryReceiptStore() + require.NoError(t, store.SetReceipts(sdk.Context{}.WithContext(t.Context()), []receipt.ReceiptRecord{{ + TxHash: txHash, + Receipt: &evmtypes.Receipt{ + TxHashHex: txHash.Hex(), + BlockNumber: 7, + }, + }})) + backend := &testBackend{ + block: func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { + return nil, fmt.Errorf("%w: 7", coretypes.ErrHeightExceedsChainHead) + }, + } + + got, err := (&txAPI{backend: backend, store: store}).GetTransactionByHash(t.Context(), txHash) + + require.NoError(t, err) + require.Nil(t, got) +} + +func TestGetTransactionByHashEndToEnd(t *testing.T) { + tx, raw := testSignedTransaction(t) + sender, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(big.NewInt(713715)), tx) + require.NoError(t, err) + blockHash := common.HexToHash("0xabcd") + store := evmonly.NewMemoryReceiptStore() + require.NoError(t, store.SetReceipts(sdk.Context{}.WithContext(t.Context()), []receipt.ReceiptRecord{{ + TxHash: tx.Hash(), + Receipt: &evmtypes.Receipt{ + TxHashHex: tx.Hash().Hex(), + BlockNumber: 4, + TransactionIndex: 0, + From: sender.Hex(), + }, + }})) + chainConfig := testChainConfig(big.NewInt(713715)) + backend := &testBackend{ + block: func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { + return &coretypes.ResultBlock{ + BlockID: tmtypes.BlockID{Hash: blockHash.Bytes()}, + Block: &tmtypes.Block{ + Header: tmtypes.Header{Time: time.Unix(1_700_000_000, 0)}, + Data: tmtypes.Data{Txs: tmtypes.Txs{raw}}, + }, + }, nil + }, + chainConfig: func() (*params.ChainConfig, error) { return chainConfig, nil }, + baseFee: func() (*big.Int, error) { return new(big.Int), nil }, + } + handler, err := newHandler(backend, store) + require.NoError(t, err) + t.Cleanup(handler.Stop) + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + client, err := ethrpc.DialHTTP(server.URL) + require.NoError(t, err) + t.Cleanup(client.Close) + + var got export.RPCTransaction + require.NoError(t, client.CallContext(t.Context(), &got, "eth_getTransactionByHash", tx.Hash())) + require.Equal(t, tx.Hash(), got.Hash) + require.Equal(t, sender, got.From) + require.Equal(t, hexutil.Uint64(0), got.Nonce) + require.Equal(t, hexutil.Uint64(21_000), got.Gas) + require.Equal(t, big.NewInt(1_000_000_000), got.GasPrice.ToInt()) + require.Equal(t, big.NewInt(1), got.Value.ToInt()) + require.NotNil(t, got.BlockHash) + require.Equal(t, blockHash, *got.BlockHash) + + var missing *export.RPCTransaction + require.NoError(t, client.CallContext(t.Context(), &missing, "eth_getTransactionByHash", common.Hash{9})) + require.Nil(t, missing) +} + func TestHandlerRequiresReceiptStore(t *testing.T) { _, err := newHandler(&testBackend{}, nil) require.EqualError(t, err, "EVM-only RPC requires a receipt store") diff --git a/integration_test/autobahn/README.md b/integration_test/autobahn/README.md index 89ed979dea..2a6f47441f 100644 --- a/integration_test/autobahn/README.md +++ b/integration_test/autobahn/README.md @@ -372,6 +372,7 @@ The public EVM JSON-RPC surface intentionally contains only: - `eth_sendRawTransaction`, used by `sei-load` and `cast publish`; - `eth_getTransactionReceipt`, for finalized receipts; +- `eth_getTransactionByHash`, for a finalized transaction's decoded fields; - `eth_getBalance`, for the current committed EVM balance; - `eth_getTransactionCount`, for the current committed nonce; - `eth_blockNumber`, for the current committed block height; @@ -423,6 +424,21 @@ This works because every new address receives the test-only initial balance and has nonce zero. Use a new key each time so the explicit nonce remains correct. +### Fetch a transaction with `cast tx` + +`cast tx` works for a known finalized transaction hash, decoding it the same +way `eth_sendRawTransaction` decoded it on the way in: + +```sh +cast tx \ + --rpc-url http://127.0.0.1:8545 \ + 0xYOUR_TRANSACTION_HASH +``` + +Like `eth_getTransactionReceipt`, a lookup for a pending or unknown hash +returns `null` rather than a pending-shaped result: this RPC tracks no local +mempool to resolve a pending transaction from. + ### Fetch the nonce, block height, and chain ID with `cast` `cast nonce`, `cast block-number`, and `cast chain-id` all work against the @@ -477,13 +493,14 @@ blocks) and `blockhash(current-1)` and further back are unavailable (only the current block's own hash is tracked outside of block execution). A view function that depends on either reads a placeholder rather than a real value. -The remaining `cast` gaps are RPC gaps, not receipt-decoding gaps. There is no -`eth_getTransactionByHash` or block API to discover a `sei-load` transfer hash, -and `sei-load` does not currently print every submitted hash. There are also no -fee-estimation, gas-estimation, log, or WebSocket subscription methods. -Commands that depend on those queries cannot operate normally; raw -transactions must provide gas limit and gas price offline as in the example -above. +The remaining `cast` gaps are RPC gaps, not receipt-decoding gaps. `sei-load` +does not currently print every submitted hash, and there is still no block API +(`eth_getBlockByNumber`/`eth_getBlockByHash`, or the by-block-and-index +transaction lookups) to discover a `sei-load` transfer hash independently. +There are also no fee-estimation, gas-estimation, log, or WebSocket +subscription methods. Commands that depend on those queries cannot operate +normally; raw transactions must provide gas limit and gas price offline as in +the example above. ## Tear down diff --git a/sei-tendermint/internal/evmonlyapp/app.go b/sei-tendermint/internal/evmonlyapp/app.go index c61297def1..d091a16af7 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -338,6 +338,16 @@ func (a *evmOnlyApplication) EvmChainID() uint64 { return a.chainID.Uint64() } +// EvmChainConfig returns the EVM chain configuration this node executes against. +func (a *evmOnlyApplication) EvmChainConfig() *params.ChainConfig { + return a.chainConfig +} + +// EvmBaseFee returns the base fee this application executes every block at. +func (a *evmOnlyApplication) EvmBaseFee() *big.Int { + return evmOnlyBaseFee() +} + // evmOnlyPrevRandao derives a deterministic PrevRandao from a block timestamp. func evmOnlyPrevRandao(timestamp uint64) common.Hash { return crypto.Keccak256Hash(binary.BigEndian.AppendUint64(nil, timestamp)) diff --git a/sei-tendermint/internal/proxy/proxy.go b/sei-tendermint/internal/proxy/proxy.go index ff68c27c28..68c9ff88bc 100644 --- a/sei-tendermint/internal/proxy/proxy.go +++ b/sei-tendermint/internal/proxy/proxy.go @@ -3,11 +3,13 @@ package proxy import ( "context" "fmt" + "math/big" "runtime/debug" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" "github.com/prometheus/client_golang/prometheus" @@ -78,6 +80,40 @@ func (app *Proxy) EvmCall(ctx context.Context, msg *core.Message) (*core.Executi return caller.EvmCall(ctx, msg) } +// evmChainConfigProvider is implemented by applications that expose the EVM +// chain configuration they execute against. +type evmChainConfigProvider interface { + EvmChainConfig() *params.ChainConfig +} + +// EvmChainConfig returns the wrapped application's EVM chain configuration. +// It errors if that application does not expose one. +func (app *Proxy) EvmChainConfig() (*params.ChainConfig, error) { + defer addTimeSample(Global.MethodTimingAt("evm_chain_config", "sync"))() + provider, ok := app.app.(evmChainConfigProvider) + if !ok { + return nil, fmt.Errorf("application does not expose an EVM chain configuration") + } + return provider.EvmChainConfig(), nil +} + +// evmBaseFeeProvider is implemented by applications that expose the base fee +// they execute every block at. +type evmBaseFeeProvider interface { + EvmBaseFee() *big.Int +} + +// EvmBaseFee returns the wrapped application's execution base fee. It errors +// if that application does not expose one. +func (app *Proxy) EvmBaseFee() (*big.Int, error) { + defer addTimeSample(Global.MethodTimingAt("evm_base_fee", "sync"))() + provider, ok := app.app.(evmBaseFeeProvider) + if !ok { + return nil, fmt.Errorf("application does not expose an EVM base fee") + } + return provider.EvmBaseFee(), nil +} + func (app *Proxy) Commit(ctx context.Context) (*types.ResponseCommit, error) { defer addTimeSample(Global.MethodTimingAt("commit", "sync"))() return app.app.Commit(ctx) diff --git a/sei-tendermint/internal/proxy/proxy_test.go b/sei-tendermint/internal/proxy/proxy_test.go index 1946f300d7..b2162de797 100644 --- a/sei-tendermint/internal/proxy/proxy_test.go +++ b/sei-tendermint/internal/proxy/proxy_test.go @@ -2,10 +2,12 @@ package proxy import ( "context" + "math/big" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -109,3 +111,57 @@ func TestEvmCallDelegatesToASupportingApplication(t *testing.T) { require.Same(t, want, got) require.Same(t, msg, gotMsg) } + +func TestEvmChainConfigErrorsWhenApplicationDoesNotSupportIt(t *testing.T) { + proxyApp := New(testApp{}) + + _, err := proxyApp.EvmChainConfig() + + require.Error(t, err) +} + +type testEvmChainConfigApp struct { + testApp + chainConfig *params.ChainConfig +} + +func (app testEvmChainConfigApp) EvmChainConfig() *params.ChainConfig { + return app.chainConfig +} + +func TestEvmChainConfigDelegatesToASupportingApplication(t *testing.T) { + want := ¶ms.ChainConfig{ChainID: big.NewInt(713715)} + proxyApp := New(testEvmChainConfigApp{chainConfig: want}) + + got, err := proxyApp.EvmChainConfig() + + require.NoError(t, err) + require.Same(t, want, got) +} + +func TestEvmBaseFeeErrorsWhenApplicationDoesNotSupportIt(t *testing.T) { + proxyApp := New(testApp{}) + + _, err := proxyApp.EvmBaseFee() + + require.Error(t, err) +} + +type testEvmBaseFeeApp struct { + testApp + baseFee *big.Int +} + +func (app testEvmBaseFeeApp) EvmBaseFee() *big.Int { + return app.baseFee +} + +func TestEvmBaseFeeDelegatesToASupportingApplication(t *testing.T) { + want := big.NewInt(7) + proxyApp := New(testEvmBaseFeeApp{baseFee: want}) + + got, err := proxyApp.EvmBaseFee() + + require.NoError(t, err) + require.Same(t, want, got) +} diff --git a/sei-tendermint/internal/rpc/core/mempool.go b/sei-tendermint/internal/rpc/core/mempool.go index ce61ec2702..000a295b35 100644 --- a/sei-tendermint/internal/rpc/core/mempool.go +++ b/sei-tendermint/internal/rpc/core/mempool.go @@ -4,11 +4,13 @@ import ( "context" "errors" "fmt" + "math/big" "math/rand" "time" "github.com/ethereum/go-ethereum/common" ethcore "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" ethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/holiman/uint256" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -327,12 +329,23 @@ func (env *Environment) EvmChainID() uint64 { return env.App.EvmChainID() } +// EvmChainConfig returns the EVM chain configuration of the wrapped application. +func (env *Environment) EvmChainConfig() (*params.ChainConfig, error) { + return env.App.EvmChainConfig() +} + // EvmCall executes msg as a read-only call against the current committed EVM // state, without creating a transaction or persisting any state change. func (env *Environment) EvmCall(ctx context.Context, msg *ethcore.Message) (*ethcore.ExecutionResult, error) { return env.App.EvmCall(ctx, msg) } +// EvmBaseFee returns the base fee the wrapped application executes every +// block at. +func (env *Environment) EvmBaseFee() (*big.Int, error) { + return env.App.EvmBaseFee() +} + // CheckTx checks the transaction without executing it. The transaction won't // be added to the mempool either. // More: https://docs.tendermint.com/master/rpc/#/Tx/check_tx