Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions giga/evmonly/rpc/balance.go
Original file line number Diff line number Diff line change
@@ -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 balanceAPI struct {
backend Backend
}

// GetBalance returns the address balance from the current committed EVM state.
func (api *balanceAPI) 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are explicitly blocking the case where they guess the current block number and slip it in the very narrow window it's true?

number, ok := block.Number()
if !ok {
return errHistoricalStateUnsupported
}
switch number {
case ethrpc.LatestBlockNumber, ethrpc.SafeBlockNumber, ethrpc.FinalizedBlockNumber, ethrpc.PendingBlockNumber:
return nil
default:
return errHistoricalStateUnsupported
}
}
80 changes: 80 additions & 0 deletions giga/evmonly/rpc/balance_test.go
Original file line number Diff line number Diff line change
@@ -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 := &balanceAPI{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 := &balanceAPI{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())
}
9 changes: 7 additions & 2 deletions giga/evmonly/rpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"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"
Expand All @@ -29,11 +30,12 @@ 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)
EvmBalance(common.Address) uint256.Int
EvmProxy(common.Address) utils.Option[*ethrpc.Client]
}

Expand Down Expand Up @@ -117,6 +119,9 @@ func newHandler(backend Backend, receiptStore receipt.ReceiptStore) (*ethrpc.Ser
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", &balanceAPI{backend: backend}); err != nil {
return nil, fmt.Errorf("register EVM-only balance RPC: %w", err)
}
return rpcServer, nil
}

Expand Down
6 changes: 6 additions & 0 deletions giga/evmonly/rpc/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
ethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"

"github.com/sei-protocol/sei-chain/giga/evmonly"
Expand All @@ -21,6 +22,7 @@ import (
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]
}

Expand All @@ -32,6 +34,10 @@ func (b *testBackend) Block(ctx context.Context, req *coretypes.RequestBlockInfo
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
}
Expand Down
30 changes: 23 additions & 7 deletions integration_test/autobahn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ passed to `deploy`, pass the same name to `list`, `forward`, and `teardown`.
Both targets require Go 1.25.6 and `make`. Local deployment also requires a
running Docker engine with Docker Compose v2. AWS deployment requires the AWS
CLI, `git`, and `ssh`, plus credentials allowed to manage EC2 instances,
security groups, and key pairs. The inspection and receipt examples also use
`jq` and Foundry's `cast`.
security groups, and key pairs. The inspection, balance, and receipt examples
also use `jq` and Foundry's `cast`.

Build the manager once:

Expand Down Expand Up @@ -297,11 +297,27 @@ 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.

All other `eth_*` methods currently return JSON-RPC method-not-found. A lookup
for a pending or unknown hash returns `null`.

### Fetch balances with `cast`

`eth_getBalance` accepts `latest`, `safe`, `finalized`, and `pending`; all four
read the current committed state because Sei has instant finality. Explicit
block numbers and hashes return an error because historical EVM-only state is
not wired yet.

@shemnon shemnon Sep 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not wired yet? Will we be supporting archive nodes in the future that do support this?


Every previously unseen address starts with the test-only `2^200` wei balance:

```sh
cast balance \
--rpc-url http://127.0.0.1:8545 \
0x000000000000000000000000000000000000dEaD
```

### Fetch receipts with `cast`

`cast receipt` works for a known finalized transaction hash. Contract
Expand Down Expand Up @@ -350,10 +366,10 @@ correct.
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.
chain ID, 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.

## Tear down

Expand Down
19 changes: 19 additions & 0 deletions integration_test/autobahn/autobahn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,12 +677,31 @@ 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)
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)
}
}
}

func assertEVMOnlyReceipts(t *testing.T, ctx context.Context, clients []*ethrpc.Client, txs [][]byte) {
t.Helper()
for nodeIndex, client := range clients {
Expand Down
6 changes: 6 additions & 0 deletions sei-tendermint/internal/rpc/core/mempool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down
Loading