diff --git a/app/abci.go b/app/abci.go index a7ee43fc09..80d5b30b65 100644 --- a/app/abci.go +++ b/app/abci.go @@ -188,6 +188,11 @@ func (app *App) EvmBalance(evmAddr common.Address, seiAddrBz []byte) uint256.Int return bigIntToUint256(mempoolBalanceFloor(balance)) } +// EvmChainID returns the EVM chain ID configured for this network. +func (app *App) EvmChainID() uint64 { + return app.EvmKeeper.ChainID(app.GetCheckCtx()).Uint64() +} + func bigIntToUint256(x *big.Int) uint256.Int { if x == nil { return uint256.Int{} diff --git a/giga/evmonly/rpc/info.go b/giga/evmonly/rpc/info.go new file mode 100644 index 0000000000..94031f032f --- /dev/null +++ b/giga/evmonly/rpc/info.go @@ -0,0 +1,24 @@ +package rpc + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +type infoAPI struct { + backend Backend +} + +// BlockNumber returns the height of the most recently committed block. +func (api *infoAPI) BlockNumber(_ context.Context) hexutil.Uint64 { + return hexutil.Uint64(api.backend.EvmBlockNumber()) +} + +// ChainId returns the EVM chain ID this Autobahn shard is configured for. +// +//nolint:revive // matches the go-ethereum RPC method name eth_chainId. +func (api *infoAPI) ChainId(_ context.Context) *hexutil.Big { + return (*hexutil.Big)(new(big.Int).SetUint64(api.backend.EvmChainID())) +} diff --git a/giga/evmonly/rpc/info_test.go b/giga/evmonly/rpc/info_test.go new file mode 100644 index 0000000000..c02706b91f --- /dev/null +++ b/giga/evmonly/rpc/info_test.go @@ -0,0 +1,57 @@ +package rpc + +import ( + "math/big" + "net/http/httptest" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/giga/evmonly" +) + +func TestBlockNumber(t *testing.T) { + backend := &testBackend{blockNumber: func() uint64 { return 42 }} + api := &infoAPI{backend: backend} + require.Equal(t, hexutil.Uint64(42), api.BlockNumber(t.Context())) +} + +func TestBlockNumberEndToEnd(t *testing.T) { + backend := &testBackend{blockNumber: func() uint64 { return 42 }} + handler, err := newHandler(backend, evmonly.NewMemoryReceiptStore()) + 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 hexutil.Uint64 + require.NoError(t, client.CallContext(t.Context(), &got, "eth_blockNumber")) + require.Equal(t, hexutil.Uint64(42), got) +} + +func TestChainId(t *testing.T) { + backend := &testBackend{chainID: func() uint64 { return 713715 }} + api := &infoAPI{backend: backend} + require.Equal(t, (*hexutil.Big)(big.NewInt(713715)), api.ChainId(t.Context())) +} + +func TestChainIdEndToEnd(t *testing.T) { + backend := &testBackend{chainID: func() uint64 { return 713715 }} + handler, err := newHandler(backend, evmonly.NewMemoryReceiptStore()) + 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 hexutil.Big + require.NoError(t, client.CallContext(t.Context(), &got, "eth_chainId")) + require.Equal(t, *big.NewInt(713715), big.Int(got)) +} diff --git a/giga/evmonly/rpc/send.go b/giga/evmonly/rpc/send.go new file mode 100644 index 0000000000..9723900c3f --- /dev/null +++ b/giga/evmonly/rpc/send.go @@ -0,0 +1,56 @@ +package rpc + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" +) + +type sendAPI struct { + backend Backend +} + +// SendRawTransaction submits a signed raw Ethereum transaction to Autobahn and +// returns its Ethereum transaction hash. +func (api *sendAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) (common.Hash, error) { + tx := new(ethtypes.Transaction) + if err := tx.UnmarshalBinary(input); err != nil { + return common.Hash{}, err + } + hash := tx.Hash() + + if sender, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(tx.ChainId()), tx); err == nil { + if client, ok := api.backend.EvmProxy(sender).Get(); ok { + if err := client.CallContext(ctx, &hash, "eth_sendRawTransaction", input); err != nil { + return hash, err + } + return hash, nil + } + } + + result, err := api.backend.BroadcastTx(ctx, &coretypes.RequestBroadcastTx{ + Tx: append(tmtypes.Tx(nil), input...), + }) + if err != nil { + return hash, err + } + if result == nil { + return hash, errors.New("missing broadcast response") + } + if result.Code != abci.CodeTypeOK { + message := result.Log + if message == "" { + message = fmt.Sprintf("transaction rejected with code %d", result.Code) + } + return hash, errors.New(message) + } + return hash, nil +} diff --git a/giga/evmonly/rpc/server_test.go b/giga/evmonly/rpc/send_test.go similarity index 83% rename from giga/evmonly/rpc/server_test.go rename to giga/evmonly/rpc/send_test.go index 192f37692d..bce1cae435 100644 --- a/giga/evmonly/rpc/server_test.go +++ b/giga/evmonly/rpc/send_test.go @@ -18,24 +18,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" ) -type testBackend struct { - broadcast func(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) - block func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) - proxy utils.Option[*ethrpc.Client] -} - -func (b *testBackend) BroadcastTx(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { - return b.broadcast(ctx, req) -} - -func (b *testBackend) Block(ctx context.Context, req *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { - return b.block(ctx, req) -} - -func (b *testBackend) EvmProxy(common.Address) utils.Option[*ethrpc.Client] { - return b.proxy -} - func TestSendRawTransaction(t *testing.T) { tx, raw := testSignedTransaction(t) var broadcastRaw []byte @@ -60,9 +42,9 @@ func TestSendRawTransaction(t *testing.T) { require.Equal(t, tx.Hash(), got) require.Equal(t, raw, broadcastRaw) - var chainID hexutil.Big - err = client.CallContext(t.Context(), &chainID, "eth_chainId") - require.ErrorContains(t, err, "method eth_chainId does not exist") + var callResult hexutil.Bytes + err = client.CallContext(t.Context(), &callResult, "eth_call") + require.ErrorContains(t, err, "method eth_call does not exist") err = client.CallContext(t.Context(), nil, "status") require.ErrorContains(t, err, "method status does not exist") } diff --git a/giga/evmonly/rpc/server.go b/giga/evmonly/rpc/server.go index e19863a414..73b54cea87 100644 --- a/giga/evmonly/rpc/server.go +++ b/giga/evmonly/rpc/server.go @@ -10,15 +10,12 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - ethtypes "github.com/ethereum/go-ethereum/core/types" ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/holiman/uint256" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" - abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" - tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" "github.com/sei-protocol/seilog" ) @@ -29,53 +26,16 @@ const ( var logger = seilog.NewLogger("giga", "evmonly", "rpc") -// Backend submits transactions, reads finalized blocks, and returns the RPC -// client for an Autobahn shard owner. +// Backend submits transactions, reads committed EVM state and finalized +// blocks, and returns the RPC client for an Autobahn shard owner. type Backend interface { - BroadcastTx(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) Block(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) + BroadcastTx(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) + EvmBalance(common.Address) uint256.Int + EvmBlockNumber() uint64 + EvmChainID() uint64 EvmProxy(common.Address) utils.Option[*ethrpc.Client] -} - -type sendAPI struct { - backend Backend -} - -// SendRawTransaction submits a signed raw Ethereum transaction to Autobahn and -// returns its Ethereum transaction hash. -func (api *sendAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) (common.Hash, error) { - tx := new(ethtypes.Transaction) - if err := tx.UnmarshalBinary(input); err != nil { - return common.Hash{}, err - } - hash := tx.Hash() - - if sender, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(tx.ChainId()), tx); err == nil { - if client, ok := api.backend.EvmProxy(sender).Get(); ok { - if err := client.CallContext(ctx, &hash, "eth_sendRawTransaction", input); err != nil { - return hash, err - } - return hash, nil - } - } - - result, err := api.backend.BroadcastTx(ctx, &coretypes.RequestBroadcastTx{ - Tx: append(tmtypes.Tx(nil), input...), - }) - if err != nil { - return hash, err - } - if result == nil { - return hash, errors.New("missing broadcast response") - } - if result.Code != abci.CodeTypeOK { - message := result.Log - if message == "" { - message = fmt.Sprintf("transaction rejected with code %d", result.Code) - } - return hash, errors.New(message) - } - return hash, nil + EvmTransactionCount(common.Address) uint64 } // Server serves the EVM-only JSON-RPC API on port 8545. @@ -112,10 +72,16 @@ func newHandler(backend Backend, receiptStore receipt.ReceiptStore) (*ethrpc.Ser } rpcServer := ethrpc.NewServer() if err := rpcServer.RegisterName("eth", &sendAPI{backend: backend}); err != nil { - return nil, fmt.Errorf("register EVM-only RPC: %w", err) + return nil, fmt.Errorf("register EVM-only send RPC: %w", err) + } + if err := rpcServer.RegisterName("eth", &txAPI{backend: backend, store: receiptStore}); err != nil { + return nil, fmt.Errorf("register EVM-only transaction RPC: %w", err) + } + if err := rpcServer.RegisterName("eth", &stateAPI{backend: backend}); err != nil { + return nil, fmt.Errorf("register EVM-only state RPC: %w", err) } - if err := rpcServer.RegisterName("eth", &receiptAPI{backend: backend, store: receiptStore}); err != nil { - return nil, fmt.Errorf("register EVM-only receipt RPC: %w", err) + if err := rpcServer.RegisterName("eth", &infoAPI{backend: backend}); err != nil { + return nil, fmt.Errorf("register EVM-only info RPC: %w", err) } return rpcServer, nil } diff --git a/giga/evmonly/rpc/setup_test.go b/giga/evmonly/rpc/setup_test.go new file mode 100644 index 0000000000..82facca2d4 --- /dev/null +++ b/giga/evmonly/rpc/setup_test.go @@ -0,0 +1,50 @@ +package rpc + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/holiman/uint256" + + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" + "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" +) + +type testBackend struct { + broadcast func(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) + block func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) + balance func(common.Address) uint256.Int + proxy utils.Option[*ethrpc.Client] + transactionCount func(common.Address) uint64 + blockNumber func() uint64 + chainID func() uint64 +} + +func (b *testBackend) BroadcastTx(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { + return b.broadcast(ctx, req) +} + +func (b *testBackend) Block(ctx context.Context, req *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { + return b.block(ctx, req) +} + +func (b *testBackend) EvmBalance(address common.Address) uint256.Int { + return b.balance(address) +} + +func (b *testBackend) EvmProxy(common.Address) utils.Option[*ethrpc.Client] { + return b.proxy +} + +func (b *testBackend) EvmTransactionCount(address common.Address) uint64 { + return b.transactionCount(address) +} + +func (b *testBackend) EvmBlockNumber() uint64 { + return b.blockNumber() +} + +func (b *testBackend) EvmChainID() uint64 { + return b.chainID() +} diff --git a/giga/evmonly/rpc/state.go b/giga/evmonly/rpc/state.go new file mode 100644 index 0000000000..c4dc114c8c --- /dev/null +++ b/giga/evmonly/rpc/state.go @@ -0,0 +1,38 @@ +package rpc + +import ( + "context" + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethrpc "github.com/ethereum/go-ethereum/rpc" +) + +var errHistoricalStateUnsupported = errors.New("historical state is not supported by EVM-only RPC") + +type stateAPI struct { + backend Backend +} + +// GetBalance returns the address balance from the current committed EVM state. +func (api *stateAPI) GetBalance(_ context.Context, address common.Address, block ethrpc.BlockNumberOrHash) (*hexutil.Big, error) { + if err := requireCurrentState(block); err != nil { + return nil, err + } + balance := api.backend.EvmBalance(address) + return (*hexutil.Big)(balance.ToBig()), nil +} + +func requireCurrentState(block ethrpc.BlockNumberOrHash) error { + number, ok := block.Number() + if !ok { + return errHistoricalStateUnsupported + } + switch number { + case ethrpc.LatestBlockNumber, ethrpc.SafeBlockNumber, ethrpc.FinalizedBlockNumber, ethrpc.PendingBlockNumber: + return nil + default: + return errHistoricalStateUnsupported + } +} diff --git a/giga/evmonly/rpc/state_test.go b/giga/evmonly/rpc/state_test.go new file mode 100644 index 0000000000..def2ee8f9d --- /dev/null +++ b/giga/evmonly/rpc/state_test.go @@ -0,0 +1,80 @@ +package rpc + +import ( + "net/http/httptest" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/giga/evmonly" +) + +func TestGetBalance(t *testing.T) { + address := common.HexToAddress("0x1000000000000000000000000000000000000001") + want := uint256.NewInt(123456789) + backend := &testBackend{ + balance: func(got common.Address) uint256.Int { + require.Equal(t, address, got) + return *want + }, + } + api := &stateAPI{backend: backend} + + for _, tag := range []ethrpc.BlockNumber{ + ethrpc.LatestBlockNumber, + ethrpc.SafeBlockNumber, + ethrpc.FinalizedBlockNumber, + ethrpc.PendingBlockNumber, + } { + got, err := api.GetBalance(t.Context(), address, ethrpc.BlockNumberOrHashWithNumber(tag)) + require.NoError(t, err) + require.Equal(t, want.ToBig(), got.ToInt()) + } +} + +func TestGetBalanceRejectsHistoricalState(t *testing.T) { + backend := &testBackend{ + balance: func(common.Address) uint256.Int { + t.Fatal("historical request read the current balance") + return uint256.Int{} + }, + } + api := &stateAPI{backend: backend} + address := common.Address{1} + + for _, block := range []ethrpc.BlockNumberOrHash{ + ethrpc.BlockNumberOrHashWithNumber(ethrpc.EarliestBlockNumber), + ethrpc.BlockNumberOrHashWithNumber(7), + ethrpc.BlockNumberOrHashWithHash(common.Hash{2}, true), + {}, + } { + got, err := api.GetBalance(t.Context(), address, block) + require.ErrorIs(t, err, errHistoricalStateUnsupported) + require.Nil(t, got) + } +} + +func TestHandlerServesGetBalance(t *testing.T) { + address := common.HexToAddress("0x2000000000000000000000000000000000000002") + backend := &testBackend{ + balance: func(common.Address) uint256.Int { + return *uint256.NewInt(42) + }, + } + handler, err := newHandler(backend, evmonly.NewMemoryReceiptStore()) + 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 hexutil.Big + require.NoError(t, client.CallContext(t.Context(), &got, "eth_getBalance", address, "latest")) + require.Equal(t, "0x2a", got.String()) +} diff --git a/giga/evmonly/rpc/receipt.go b/giga/evmonly/rpc/tx.go similarity index 85% rename from giga/evmonly/rpc/receipt.go rename to giga/evmonly/rpc/tx.go index b543b924aa..df060f5012 100644 --- a/giga/evmonly/rpc/receipt.go +++ b/giga/evmonly/rpc/tx.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" + 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" @@ -17,13 +18,22 @@ import ( evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) -type receiptAPI struct { +type txAPI struct { backend Backend store receiptpkg.ReceiptStore } +// GetTransactionCount returns the address nonce from the current committed EVM state. +func (api *txAPI) GetTransactionCount(_ context.Context, address common.Address, block ethrpc.BlockNumberOrHash) (*hexutil.Uint64, error) { + if err := requireCurrentState(block); err != nil { + return nil, err + } + nonce := hexutil.Uint64(api.backend.EvmTransactionCount(address)) + return &nonce, nil +} + // GetTransactionReceipt returns the finalized Ethereum receipt for hash. -func (api *receiptAPI) GetTransactionReceipt(ctx context.Context, hash common.Hash) (map[string]any, error) { +func (api *txAPI) GetTransactionReceipt(ctx context.Context, hash common.Hash) (map[string]any, error) { stored, err := api.store.GetReceipt(receiptContext(ctx), hash) if errors.Is(err, receiptpkg.ErrNotFound) { return nil, nil diff --git a/giga/evmonly/rpc/receipt_test.go b/giga/evmonly/rpc/tx_test.go similarity index 72% rename from giga/evmonly/rpc/receipt_test.go rename to giga/evmonly/rpc/tx_test.go index 9090a3cf6f..edc24f962b 100644 --- a/giga/evmonly/rpc/receipt_test.go +++ b/giga/evmonly/rpc/tx_test.go @@ -22,6 +22,59 @@ import ( evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) +func TestGetTransactionCountCurrentState(t *testing.T) { + address := common.HexToAddress("0x1000000000000000000000000000000000000001") + backend := &testBackend{transactionCount: func(common.Address) uint64 { return 7 }} + api := &txAPI{backend: backend} + + for _, tag := range []ethrpc.BlockNumberOrHash{ + ethrpc.BlockNumberOrHashWithNumber(ethrpc.LatestBlockNumber), + ethrpc.BlockNumberOrHashWithNumber(ethrpc.SafeBlockNumber), + ethrpc.BlockNumberOrHashWithNumber(ethrpc.FinalizedBlockNumber), + ethrpc.BlockNumberOrHashWithNumber(ethrpc.PendingBlockNumber), + } { + got, err := api.GetTransactionCount(t.Context(), address, tag) + require.NoError(t, err) + require.Equal(t, hexutil.Uint64(7), *got) + } +} + +func TestGetTransactionCountRejectsHistoricalState(t *testing.T) { + address := common.HexToAddress("0x1000000000000000000000000000000000000001") + backend := &testBackend{transactionCount: func(common.Address) uint64 { + t.Fatal("historical lookup reached the backend") + return 0 + }} + api := &txAPI{backend: backend} + + for _, tag := range []ethrpc.BlockNumberOrHash{ + ethrpc.BlockNumberOrHashWithNumber(ethrpc.EarliestBlockNumber), + ethrpc.BlockNumberOrHashWithNumber(8), + ethrpc.BlockNumberOrHashWithHash(common.Hash{0x01}, false), + {}, + } { + _, err := api.GetTransactionCount(t.Context(), address, tag) + require.ErrorIs(t, err, errHistoricalStateUnsupported) + } +} + +func TestGetTransactionCountEndToEnd(t *testing.T) { + address := common.HexToAddress("0x1000000000000000000000000000000000000001") + backend := &testBackend{transactionCount: func(common.Address) uint64 { return 3 }} + handler, err := newHandler(backend, evmonly.NewMemoryReceiptStore()) + 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 hexutil.Uint64 + require.NoError(t, client.CallContext(t.Context(), &got, "eth_getTransactionCount", address, "latest")) + require.Equal(t, hexutil.Uint64(3), got) +} + func TestGetTransactionReceipt(t *testing.T) { txHash := common.HexToHash("0x1234") blockHash := common.HexToHash("0xabcd") @@ -136,7 +189,7 @@ func TestGetTransactionReceiptReturnsNullBeforeBlockCommit(t *testing.T) { }, } - got, err := (&receiptAPI{backend: backend, store: store}).GetTransactionReceipt(t.Context(), txHash) + got, err := (&txAPI{backend: backend, store: store}).GetTransactionReceipt(t.Context(), txHash) require.NoError(t, err) require.Nil(t, got) diff --git a/integration_test/autobahn/README.md b/integration_test/autobahn/README.md index 0106ab1a23..71e8cb6bbb 100644 --- a/integration_test/autobahn/README.md +++ b/integration_test/autobahn/README.md @@ -371,7 +371,11 @@ tail -f build/generated/logs/seid-0.log The public EVM JSON-RPC surface intentionally contains only: - `eth_sendRawTransaction`, used by `sei-load` and `cast publish`; -- `eth_getTransactionReceipt`, for finalized receipts. +- `eth_getTransactionReceipt`, for finalized receipts; +- `eth_getBalance`, for the current committed EVM balance; +- `eth_getTransactionCount`, for the current committed nonce; +- `eth_blockNumber`, for the current committed block height; +- `eth_chainId`, for the configured EVM chain ID. All other `eth_*` methods currently return JSON-RPC method-not-found. A lookup for a pending or unknown hash returns `null`. @@ -387,12 +391,9 @@ cast receipt \ 0xYOUR_TRANSACTION_HASH ``` -Without `--async`, `cast receipt` polls until the receipt exists. While it is -waiting, current Foundry versions may also poll `eth_blockNumber` and print a -method-not-found error, although the command still returns the receipt after -finalization. Add `--async` for a one-shot lookup that fails immediately when -the hash is not found. Confirmation counting is not available without -`eth_blockNumber`; the endpoint itself only returns finalized receipts. +Without `--async`, `cast receipt` polls until the receipt exists, now also +polling `eth_blockNumber` for confirmation counting. Add `--async` for a +one-shot lookup that fails immediately when the hash is not found. For a repeatable end-to-end check, create a new throwaway key, sign completely offline, publish the raw transaction, and fetch its receipt: @@ -421,13 +422,38 @@ 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 the nonce, block height, and chain ID with `cast` + +`cast nonce`, `cast block-number`, and `cast chain-id` all work against the +EVM-only RPC: + +```sh +cast nonce --rpc-url http://127.0.0.1:8545 0xYOUR_ADDRESS +cast block-number --rpc-url http://127.0.0.1:8545 +cast chain-id --rpc-url http://127.0.0.1:8545 +``` + +`eth_getTransactionCount` accepts the `latest`, `safe`, `finalized`, and +`pending` block tags, but all four resolve to the current committed nonce. +`pending` is accepted so standard tooling that requests it (`cast send`, +ethers, viem) keeps working, not because instant finality makes committed and +pending equivalent: instant finality removes reorg risk, not the +broadcast-to-commit window `pending` exists to cover. Two transactions sent +back-to-back from the same key before the first commits are therefore +assigned the same nonce, and the second is rejected; callers issuing rapid +sequential sends must track the next nonce themselves rather than relying on +`pending`. An explicit height, an explicit hash, or `earliest` returns an +error: historical state is not available from this RPC. `eth_blockNumber` and +`eth_chainId` take no block selector and always return the current height and +the network's configured EVM chain ID. + 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 -chain ID, balance, nonce, fee-estimation, gas-estimation, call, log, or -WebSocket subscription methods. Commands that depend on those queries cannot -operate normally; raw transactions must provide chain ID, nonce, gas limit, -and gas price offline as in the example above. +fee-estimation, gas-estimation, call, 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/integration_test/autobahn/autobahn_test.go b/integration_test/autobahn/autobahn_test.go index 2d990db080..5ad454b165 100644 --- a/integration_test/autobahn/autobahn_test.go +++ b/integration_test/autobahn/autobahn_test.go @@ -714,12 +714,89 @@ func testEVMOnlyLoad(t *testing.T) { lastHeight, included := waitForEVMOnlyTxs(t, ctx, listRunningNodes(t), len(block.Txs)) assertEVMOnlyReceipts(t, ctx, clients, block.Txs) + assertEVMOnlyBalances(t, ctx, clients, block.Txs) + assertEVMOnlyTransactionCount(t, ctx, clients, block.Txs) + assertEVMOnlyChainID(t, ctx, clients) + assertEVMOnlyBlockNumber(t, ctx, clients, lastHeight) elapsed := time.Since(started) t.Logf("Autobahn finalized %d raw EVM transfers through %d validators in %s (%.0f tx/s)", included, clusterSize, elapsed.Round(time.Millisecond), float64(included)/elapsed.Seconds()) t.Logf("all validators executed through at least height %d", lastHeight) } +func assertEVMOnlyBalances(t *testing.T, ctx context.Context, clients []*ethrpc.Client, txs [][]byte) { + t.Helper() + want := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(1), 200), big.NewInt(1)) + for nodeIndex, client := range clients { + tx := new(ethtypes.Transaction) + if err := tx.UnmarshalBinary(txs[nodeIndex]); err != nil { + t.Fatalf("decode EVM-only transaction %d: %v", nodeIndex, err) + } + var got hexutil.Big + if err := client.CallContext(ctx, &got, "eth_getBalance", tx.To(), "latest"); err != nil { + t.Fatalf("read EVM-only balance %s from node %d: %v", tx.To(), nodeIndex, err) + } + if got.ToInt().Cmp(want) != 0 { + t.Fatalf("node %d returned balance %s for %s, want %s", nodeIndex, got.ToInt(), tx.To(), want) + } + } +} + +// assertEVMOnlyTransactionCount checks eth_getTransactionCount against every +// node: each transfer's sender starts at nonce 0 and sends exactly one +// transaction, so its committed nonce should now be 1. +func assertEVMOnlyTransactionCount(t *testing.T, ctx context.Context, clients []*ethrpc.Client, txs [][]byte) { + t.Helper() + signer := ethtypes.LatestSignerForChainID(new(big.Int).SetUint64(tmconfig.AutobahnEVMOnlyChainID)) + for nodeIndex, client := range clients { + tx := new(ethtypes.Transaction) + if err := tx.UnmarshalBinary(txs[nodeIndex]); err != nil { + t.Fatalf("decode EVM-only transaction %d: %v", nodeIndex, err) + } + sender, err := ethtypes.Sender(signer, tx) + if err != nil { + t.Fatalf("recover EVM-only sender for transaction %d: %v", nodeIndex, err) + } + var got hexutil.Uint64 + if err := client.CallContext(ctx, &got, "eth_getTransactionCount", sender, "latest"); err != nil { + t.Fatalf("read EVM-only transaction count %s from node %d: %v", sender, nodeIndex, err) + } + if got != 1 { + t.Fatalf("node %d returned transaction count %d for %s, want 1", nodeIndex, got, sender) + } + } +} + +// assertEVMOnlyChainID checks eth_chainId against every node. +func assertEVMOnlyChainID(t *testing.T, ctx context.Context, clients []*ethrpc.Client) { + t.Helper() + wantChainID := new(big.Int).SetUint64(tmconfig.AutobahnEVMOnlyChainID) + for nodeIndex, client := range clients { + var chainID hexutil.Big + if err := client.CallContext(ctx, &chainID, "eth_chainId"); err != nil { + t.Fatalf("read EVM-only chain ID from node %d: %v", nodeIndex, err) + } + if (*big.Int)(&chainID).Cmp(wantChainID) != 0 { + t.Fatalf("node %d returned chain ID %s, want %s", nodeIndex, (*big.Int)(&chainID), wantChainID) + } + } +} + +// assertEVMOnlyBlockNumber checks eth_blockNumber against every node once the +// load run has finalized through minHeight. +func assertEVMOnlyBlockNumber(t *testing.T, ctx context.Context, clients []*ethrpc.Client, minHeight int64) { + t.Helper() + for nodeIndex, client := range clients { + var height hexutil.Uint64 + if err := client.CallContext(ctx, &height, "eth_blockNumber"); err != nil { + t.Fatalf("read EVM-only block number from node %d: %v", nodeIndex, err) + } + if int64(height) < minHeight { + t.Fatalf("node %d returned block number %d, want at least %d", nodeIndex, height, minHeight) + } + } +} + func assertEVMOnlyReceipts(t *testing.T, ctx context.Context, clients []*ethrpc.Client, txs [][]byte) { t.Helper() for nodeIndex, client := range clients { diff --git a/sei-cosmos/baseapp/baseapp.go b/sei-cosmos/baseapp/baseapp.go index 6b06025ec4..449b3fee11 100644 --- a/sei-cosmos/baseapp/baseapp.go +++ b/sei-cosmos/baseapp/baseapp.go @@ -88,6 +88,10 @@ func (app *BaseApp) EvmBalance(_ common.Address, _ []byte) uint256.Int { return uint256.Int{} } +func (app *BaseApp) EvmChainID() uint64 { + return 0 +} + // BaseApp reflects the ABCI application implementation. type BaseApp struct { // initialized on creation diff --git a/sei-cosmos/server/rollback_test.go b/sei-cosmos/server/rollback_test.go index 1f5f4bbf8d..571c78414c 100644 --- a/sei-cosmos/server/rollback_test.go +++ b/sei-cosmos/server/rollback_test.go @@ -90,6 +90,10 @@ func (m *mockApplication) EvmBalance(common.Address, []byte) uint256.Int { return uint256.Int{} } +func (m *mockApplication) EvmChainID() uint64 { + return 0 +} + func (m *mockApplication) BeginBlock(ctx context.Context, req *abci.RequestBeginBlock) (*abci.ResponseBeginBlock, error) { return &abci.ResponseBeginBlock{}, nil } diff --git a/sei-tendermint/abci/types/application.go b/sei-tendermint/abci/types/application.go index f16dd68c02..906c21b6f4 100644 --- a/sei-tendermint/abci/types/application.go +++ b/sei-tendermint/abci/types/application.go @@ -26,8 +26,9 @@ type Application interface { // Mempool Connection CheckTx(context.Context, *RequestCheckTxV2) *ResponseCheckTxV2 // Validate a tx for the mempool GetTxPriorityHint(context.Context, *RequestGetTxPriorityHintV2) (*ResponseGetTxPriorityHint, error) // Get tx priority before checkTx - EvmNonce(common.Address) uint64 EvmBalance(common.Address, []byte) uint256.Int + EvmChainID() uint64 + EvmNonce(common.Address) uint64 // Consensus Connection InitChain(*RequestInitChain) (*ResponseInitChain, error) // Initialize blockchain w validators/other info from TendermintCore @@ -108,6 +109,10 @@ func (BaseApplication) EvmBalance(common.Address, []byte) uint256.Int { return uint256.Int{} } +func (BaseApplication) EvmChainID() uint64 { + return 0 +} + func (BaseApplication) FinalizeBlock(_ context.Context, req *RequestFinalizeBlock) (*ResponseFinalizeBlock, error) { txs := make([]*ExecTxResult, len(req.Txs)) for i := range req.Txs { diff --git a/sei-tendermint/abci/types/mocks/application.go b/sei-tendermint/abci/types/mocks/application.go index e36908bfad..72259b612b 100644 --- a/sei-tendermint/abci/types/mocks/application.go +++ b/sei-tendermint/abci/types/mocks/application.go @@ -121,6 +121,24 @@ func (_m *Application) EvmBalance(_a0 common.Address, _a1 []byte) uint256.Int { return r0 } +// EvmChainID provides a mock function with no fields +func (_m *Application) EvmChainID() uint64 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for EvmChainID") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + // EvmNonce provides a mock function with given fields: _a0 func (_m *Application) EvmNonce(_a0 common.Address) uint64 { ret := _m.Called(_a0) diff --git a/sei-tendermint/internal/evmonlyapp/app.go b/sei-tendermint/internal/evmonlyapp/app.go index 20c6d466fc..6f66c99cab 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -238,6 +238,10 @@ func (a *evmOnlyApplication) EvmBalance(address common.Address, _ []byte) uint25 return *new(uint256.Int).SetBytes(balance[:]) } +func (a *evmOnlyApplication) EvmChainID() uint64 { + return a.chainID.Uint64() +} + func (a *evmOnlyApplication) FinalizeBlock(ctx context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { height := req.Header.Height if height <= 0 { diff --git a/sei-tendermint/internal/proxy/proxy.go b/sei-tendermint/internal/proxy/proxy.go index 8f435fc26f..30cd912419 100644 --- a/sei-tendermint/internal/proxy/proxy.go +++ b/sei-tendermint/internal/proxy/proxy.go @@ -54,6 +54,11 @@ func (app *Proxy) EvmBalance(addr common.Address, seiAddr []byte) uint256.Int { return app.app.EvmBalance(addr, seiAddr) } +func (app *Proxy) EvmChainID() uint64 { + defer addTimeSample(Global.MethodTimingAt("evm_chain_id", "sync"))() + return app.app.EvmChainID() +} + 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/rpc/core/mempool.go b/sei-tendermint/internal/rpc/core/mempool.go index a11a802ef1..51e7c11d46 100644 --- a/sei-tendermint/internal/rpc/core/mempool.go +++ b/sei-tendermint/internal/rpc/core/mempool.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/holiman/uint256" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/state/indexer" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" @@ -26,6 +27,11 @@ func (env *Environment) EvmProxy(sender common.Address) utils.Option[*ethrpc.Cli return utils.None[*ethrpc.Client]() } +// EvmBalance returns the address balance from the current committed EVM state. +func (env *Environment) EvmBalance(address common.Address) uint256.Int { + return env.App.EvmBalance(address, nil) +} + func (env *Environment) EvmTxByHash(hash common.Hash) (types.Tx, bool) { if giga, ok := env.gigaRouter().Get(); ok { if v, ok := giga.Mempool().Get(); ok { @@ -295,6 +301,22 @@ func (env *Environment) NumUnconfirmedTxs(ctx context.Context) (*coretypes.Resul }, nil } +// EvmTransactionCount returns the address transaction count (nonce) from the +// current committed EVM state. +func (env *Environment) EvmTransactionCount(address common.Address) uint64 { + return env.App.EvmNonce(address) +} + +// EvmBlockNumber returns the height of the most recently committed block. +func (env *Environment) EvmBlockNumber() uint64 { + return utils.Clamp[uint64](env.App.LastBlockHeight()) +} + +// EvmChainID returns the EVM chain ID this node is configured for. +func (env *Environment) EvmChainID() uint64 { + return env.App.EvmChainID() +} + // 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 diff --git a/sei-tendermint/node/mock_app.go b/sei-tendermint/node/mock_app.go index a4d0cf1582..2784800abf 100644 --- a/sei-tendermint/node/mock_app.go +++ b/sei-tendermint/node/mock_app.go @@ -146,6 +146,10 @@ func (app *MockApp) EvmNonce(addr common.Address) uint64 { func (app *MockApp) EvmBalance(common.Address, []byte) uint256.Int { return baseBalance } +func (app *MockApp) EvmChainID() uint64 { + return app.app.EvmChainID() +} + func (app *MockApp) ProcessProposal(_ context.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { return nil, errMockAppProcessProposal }