diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 1bb049d493..2fcd2675e6 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -111,7 +111,10 @@ func (e *Executor) PrepareBlock(ctx context.Context, req BlockRequest) (Prepared return PreparedBlock{}, err } signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) - parsed, err := parseBlockTxs(ctx, req.Txs, signer, e.cfg.ParseWorkers) + if len(req.Senders) != 0 && len(req.Senders) != len(req.Txs) { + return PreparedBlock{}, fmt.Errorf("block request has %d senders for %d txs", len(req.Senders), len(req.Txs)) + } + parsed, err := parseBlockTxs(ctx, req.Txs, signer, req.Senders, e.cfg.ParseWorkers) if err != nil { return PreparedBlock{}, err } diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 0274673715..43c9f8388b 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -558,7 +558,8 @@ func TestExecutorRejectsBlobTxUntilBlockAccountingIsWired(t *testing.T) { require.Nil(t, result) require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) - tx, sender, err := parseTx(rawTx, ethtypes.LatestSignerForChainID(chainID)) + tx := decodeTx(t, rawTx) + sender, err = ethtypes.Sender(ethtypes.LatestSignerForChainID(chainID), tx) require.NoError(t, err) result, err = executor.ExecutePreparedBlock(t.Context(), PreparedBlock{ Context: ctx, diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go index 2955efbc66..8b131813e3 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -7,9 +7,20 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "golang.org/x/sync/errgroup" + + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) -func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, workers int) ([]PreparedTx, error) { +// senderAt returns the already verified sender of txs[i], if any. senders is +// either empty or aligned with txs. +func senderAt(senders []utils.Option[common.Address], i int) utils.Option[common.Address] { + if i < len(senders) { + return senders[i] + } + return utils.None[common.Address]() +} + +func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, senders []utils.Option[common.Address], workers int) ([]PreparedTx, error) { parsed := make([]PreparedTx, len(txs)) if len(txs) == 0 { return parsed, nil @@ -19,7 +30,7 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, wo if err := ctx.Err(); err != nil { return nil, err } - prepared, err := parsePreparedTx(raw, signer) + prepared, err := parsePreparedTx(raw, signer, senderAt(senders, i)) if err != nil { return nil, fmt.Errorf("parse tx %d: %w", i, err) } @@ -45,7 +56,7 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, wo for range workers { g.Go(func() error { for i := range jobs { - prepared, err := parsePreparedTx(txs[i], signer) + prepared, err := parsePreparedTx(txs[i], signer, senderAt(senders, i)) if err != nil { return fmt.Errorf("parse tx %d: %w", i, err) } @@ -60,25 +71,31 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, wo return parsed, nil } -func parsePreparedTx(raw []byte, signer ethtypes.Signer) (PreparedTx, error) { - tx, sender, err := parseTx(raw, signer) +// parsePreparedTx decodes raw and resolves its sender. The sender is taken from +// known when present and the transaction is bound to signer's chain; otherwise +// it is recovered from the signature. +func parsePreparedTx(raw []byte, signer ethtypes.Signer, known utils.Option[common.Address]) (PreparedTx, error) { + tx, err := decodeRawTx(raw) if err != nil { return PreparedTx{}, err } if err := validateSupportedTx(tx); err != nil { return PreparedTx{}, err } + if sender, ok := known.Get(); ok && tx.Protected() && tx.ChainId().Cmp(signer.ChainID()) == 0 { + return PreparedTx{Tx: tx, Sender: sender}, nil + } + sender, err := ethtypes.Sender(signer, tx) + if err != nil { + return PreparedTx{}, err + } return PreparedTx{Tx: tx, Sender: sender}, nil } -func parseTx(raw []byte, signer ethtypes.Signer) (*ethtypes.Transaction, common.Address, error) { - var tx ethtypes.Transaction +func decodeRawTx(raw []byte) (*ethtypes.Transaction, error) { + tx := new(ethtypes.Transaction) if err := tx.UnmarshalBinary(raw); err != nil { - return nil, common.Address{}, err - } - sender, err := ethtypes.Sender(signer, &tx) - if err != nil { - return nil, common.Address{}, err + return nil, err } - return &tx, sender, nil + return tx, nil } diff --git a/giga/evmonly/parser_test.go b/giga/evmonly/parser_test.go new file mode 100644 index 0000000000..b71b2b6f20 --- /dev/null +++ b/giga/evmonly/parser_test.go @@ -0,0 +1,133 @@ +package evmonly + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" +) + +func TestParsePreparedTxUsesKnownSender(t *testing.T) { + chainID := big.NewInt(testChainID) + signer := ethtypes.LatestSignerForChainID(chainID) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xc1) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + tx := decodeTx(t, rawTx) + claimed := testAddress(0xc2) + + t.Run("known sender is used without recovery", func(t *testing.T) { + prepared, err := parsePreparedTx(rawTx, signer, utils.Some(claimed)) + require.NoError(t, err) + require.Equal(t, claimed, prepared.Sender) + require.Equal(t, tx.Hash(), prepared.Tx.Hash()) + }) + + t.Run("no known sender recovers", func(t *testing.T) { + prepared, err := parsePreparedTx(rawTx, signer, utils.None[common.Address]()) + require.NoError(t, err) + require.Equal(t, sender, prepared.Sender) + }) + + t.Run("known sender is ignored for a tx from another chain", func(t *testing.T) { + otherChain := big.NewInt(testChainID + 1) + otherRaw := signLegacyTx(t, key, otherChain, 0, &recipient, big.NewInt(1), nil) + _, err := parsePreparedTx(otherRaw, signer, utils.Some(claimed)) + require.ErrorIs(t, err, ethtypes.ErrInvalidChainId) + }) +} + +func TestParseBlockTxsMixesKnownAndRecoveredSenders(t *testing.T) { + chainID := big.NewInt(testChainID) + signer := ethtypes.LatestSignerForChainID(chainID) + recipient := testAddress(0xc3) + const n = 8 + raws := make([][]byte, n) + senders := make([]common.Address, n) + known := make([]utils.Option[common.Address], n) + for i := range n { + key, err := crypto.GenerateKey() + require.NoError(t, err) + senders[i] = crypto.PubkeyToAddress(key.PublicKey) + raws[i] = signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + if i%2 == 0 { + known[i] = utils.Some(senders[i]) + } + } + for _, workers := range []int{1, 4} { + parsed, err := parseBlockTxs(t.Context(), raws, signer, known, workers) + require.NoError(t, err) + require.Len(t, parsed, n) + for i, prepared := range parsed { + require.Equal(t, senders[i], prepared.Sender) + } + } +} + +func TestExecutorPrepareBlockUsesKnownSender(t *testing.T) { + chainID := big.NewInt(testChainID) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xc4) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + // A deliberately wrong sender proves the slice, not recovery, decided. + claimed := testAddress(0xc5) + + executor := NewExecutor(Config{}, withTestState(NewMemoryState())) + prepared, err := executor.PrepareBlock(t.Context(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + Senders: []utils.Option[common.Address]{utils.Some(claimed)}, + }) + require.NoError(t, err) + require.Len(t, prepared.Txs, 1) + require.Equal(t, claimed, prepared.Txs[0].Sender) + + _, err = executor.PrepareBlock(t.Context(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx, rawTx}, + Senders: []utils.Option[common.Address]{utils.Some(claimed)}, + }) + require.Error(t, err) + + prepared, err = executor.PrepareBlock(t.Context(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + require.Equal(t, sender, prepared.Txs[0].Sender) +} + +func BenchmarkParsePreparedTx(b *testing.B) { + chainID := big.NewInt(testChainID) + signer := ethtypes.LatestSignerForChainID(chainID) + key, err := crypto.GenerateKey() + require.NoError(b, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xc6) + rawTx := signLegacyTx(b, key, chainID, 0, &recipient, big.NewInt(1), nil) + known := utils.Some(sender) + + b.Run("recover", func(b *testing.B) { + for b.Loop() { + if _, err := parsePreparedTx(rawTx, signer, utils.None[common.Address]()); err != nil { + b.Fatal(err) + } + } + }) + b.Run("known", func(b *testing.B) { + for b.Loop() { + if _, err := parsePreparedTx(rawTx, signer, known); err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/giga/evmonly/rpc/send.go b/giga/evmonly/rpc/send.go index 9723900c3f..ac36f0c833 100644 --- a/giga/evmonly/rpc/send.go +++ b/giga/evmonly/rpc/send.go @@ -8,8 +8,10 @@ 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" 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" ) @@ -27,13 +29,11 @@ func (api *sendAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) } 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 + if client, ok := api.shardProxy(tx).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{ @@ -54,3 +54,17 @@ func (api *sendAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) } return hash, nil } + +// shardProxy returns the RPC client of the validator owning tx's sender shard, +// or None when the transaction is handled locally. Sender recovery is skipped +// entirely when the backend has no proxies, since CheckTx recovers it anyway. +func (api *sendAPI) shardProxy(tx *ethtypes.Transaction) utils.Option[*ethrpc.Client] { + if !api.backend.EvmProxyEnabled() { + return utils.None[*ethrpc.Client]() + } + sender, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(tx.ChainId()), tx) + if err != nil { + return utils.None[*ethrpc.Client]() + } + return api.backend.EvmProxy(sender) +} diff --git a/giga/evmonly/rpc/send_test.go b/giga/evmonly/rpc/send_test.go index bce1cae435..1050b9f94e 100644 --- a/giga/evmonly/rpc/send_test.go +++ b/giga/evmonly/rpc/send_test.go @@ -129,3 +129,17 @@ func testSignedTransaction(t *testing.T) (*ethtypes.Transaction, []byte) { require.NoError(t, err) return tx, raw } + +func TestSkipsShardLookupWithoutProxies(t *testing.T) { + tx, raw := testSignedTransaction(t) + backend := &testBackend{ + broadcast: func(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { + return &coretypes.ResultBroadcastTx{}, nil + }, + proxy: utils.None[*ethrpc.Client](), + } + got, err := (&sendAPI{backend: backend}).SendRawTransaction(t.Context(), raw) + require.NoError(t, err) + require.Equal(t, tx.Hash(), got) + require.Zero(t, backend.proxyCalls) +} diff --git a/giga/evmonly/rpc/server.go b/giga/evmonly/rpc/server.go index 73b54cea87..7f623fa21a 100644 --- a/giga/evmonly/rpc/server.go +++ b/giga/evmonly/rpc/server.go @@ -28,6 +28,8 @@ var logger = seilog.NewLogger("giga", "evmonly", "rpc") // Backend submits transactions, reads committed EVM state and finalized // blocks, and returns the RPC client for an Autobahn shard owner. +// EvmProxyEnabled reports whether EvmProxy can ever return a client; when it +// is false every transaction is broadcast locally without recovering its sender. type Backend interface { Block(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) BroadcastTx(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) @@ -35,6 +37,7 @@ type Backend interface { EvmBlockNumber() uint64 EvmChainID() uint64 EvmProxy(common.Address) utils.Option[*ethrpc.Client] + EvmProxyEnabled() bool EvmTransactionCount(common.Address) uint64 } diff --git a/giga/evmonly/rpc/setup_test.go b/giga/evmonly/rpc/setup_test.go index 82facca2d4..7d984a6a5d 100644 --- a/giga/evmonly/rpc/setup_test.go +++ b/giga/evmonly/rpc/setup_test.go @@ -16,6 +16,7 @@ type testBackend struct { block func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) balance func(common.Address) uint256.Int proxy utils.Option[*ethrpc.Client] + proxyCalls int transactionCount func(common.Address) uint64 blockNumber func() uint64 chainID func() uint64 @@ -34,9 +35,14 @@ func (b *testBackend) EvmBalance(address common.Address) uint256.Int { } func (b *testBackend) EvmProxy(common.Address) utils.Option[*ethrpc.Client] { + b.proxyCalls++ return b.proxy } +func (b *testBackend) EvmProxyEnabled() bool { + return b.proxy.IsPresent() +} + func (b *testBackend) EvmTransactionCount(address common.Address) uint64 { return b.transactionCount(address) } diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 99e7be07f8..6420da9766 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -7,6 +7,8 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) // BlockExecutor is the Cosmos-free block execution boundary for the EVM-only path. @@ -36,10 +38,15 @@ type ResultSink interface { } // BlockRequest contains all consensus/runtime inputs needed to execute a block. -// Txs must be raw Ethereum transaction RLP bytes. +// Txs must be raw Ethereum transaction RLP bytes. Senders, when non-empty, is +// aligned with Txs and holds the sender of every transaction whose signature +// the caller has already verified against the executor's chain ID; PrepareBlock +// uses those instead of recovering them. Transactions whose slot is None are +// recovered as usual. type BlockRequest struct { Context BlockContext Txs [][]byte + Senders []utils.Option[common.Address] } // PreparedBlock contains decoded transactions with recovered senders. It is a diff --git a/sei-tendermint/internal/evmonlyapp/app.go b/sei-tendermint/internal/evmonlyapp/app.go index c48b51edf6..4dba48e2d8 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -32,6 +32,11 @@ func evmOnlyBaseFee() *big.Int { return new(big.Int) } var evmOnlyBaseBalance = new(big.Int).Lsh(big.NewInt(1), 200) +// checkedSendersCap bounds the senders remembered from CheckTx. Entries are +// dropped as their transactions execute; the cap only guards against admitted +// transactions that never reach a block. +const checkedSendersCap = 1 << 18 + type evmOnlyApplication struct { abci.BaseApplication @@ -44,6 +49,10 @@ type evmOnlyApplication struct { // Lock order: executor before cursor. FinalizeBlock holds executor while // the block's cursor encoder takes cursor. cursor utils.Mutex[*evmOnlyCursorState] + // checkedSenders maps the hash of every transaction this process admitted + // in CheckTx to the sender recovered there, so execution does not recover + // it again. + checkedSenders utils.Mutex[map[common.Hash]common.Address] } // evmOnlyCursorState is the execution position: the block whose state is @@ -76,6 +85,7 @@ func NewEVMOnlyApplication( validators: slices.Clone(validators), executor: utils.NewMutex(new(utils.Option[*evmonly.Executor])), cursor: utils.NewMutex(&evmOnlyCursorState{}), + checkedSenders: utils.NewMutex(map[common.Hash]common.Address{}), } cursor, err := loadEVMOnlyCursor(storage.SC()) if err != nil { @@ -224,6 +234,7 @@ func (a *evmOnlyApplication) CheckTx(_ context.Context, req *abci.RequestCheckTx if !ok { return &abci.ResponseCheckTxV2{ResponseCheckTx: &abci.ResponseCheckTx{Code: 1, Log: "transaction gas limit exceeds int64"}} } + a.rememberSender(tx.Hash(), sender) return &abci.ResponseCheckTxV2{ ResponseCheckTx: &abci.ResponseCheckTx{ Code: abci.CodeTypeOK, @@ -238,6 +249,33 @@ func (a *evmOnlyApplication) CheckTx(_ context.Context, req *abci.RequestCheckTx } } +func (a *evmOnlyApplication) rememberSender(hash common.Hash, sender common.Address) { + for senders := range a.checkedSenders.Lock() { + if len(senders) >= checkedSendersCap { + clear(senders) + } + senders[hash] = sender + } +} + +// takeSenders returns, aligned with txs, the sender CheckTx recovered for each +// transaction this process admitted, and forgets those entries. The hash of a +// raw transaction is the keccak of its bytes for every transaction type, so no +// decoding is needed. +func (a *evmOnlyApplication) takeSenders(txs [][]byte) []utils.Option[common.Address] { + out := make([]utils.Option[common.Address], len(txs)) + for senders := range a.checkedSenders.Lock() { + for i, raw := range txs { + hash := crypto.Keccak256Hash(raw) + if sender, ok := senders[hash]; ok { + out[i] = utils.Some(sender) + delete(senders, hash) + } + } + } + return out +} + func (a *evmOnlyApplication) parseTx(raw []byte) (*ethtypes.Transaction, common.Address, error) { tx := new(ethtypes.Transaction) if err := tx.UnmarshalBinary(raw); err != nil { @@ -327,7 +365,8 @@ func (a *evmOnlyApplication) FinalizeBlock(ctx context.Context, req *abci.Reques BlockHash: blockHash, PrevRandao: crypto.Keccak256Hash(binary.BigEndian.AppendUint64(nil, timestamp)), }, - Txs: req.Txs, + Txs: req.Txs, + Senders: a.takeSenders(req.Txs), }) if err != nil { return nil, errors.Join(err, a.abandonPending(height)) diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index a9b685d120..5f9cddd05c 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -21,6 +21,13 @@ import ( const evmOnlyTestChainID uint64 = 713715 +func decodeEVMOnlyTestTx(t *testing.T, raw []byte) *ethtypes.Transaction { + t.Helper() + tx := new(ethtypes.Transaction) + require.NoError(t, tx.UnmarshalBinary(raw)) + return tx +} + func signedEVMOnlyTestTx(t *testing.T, chainID uint64, nonce uint64) ([]byte, common.Address) { t.Helper() key, err := crypto.GenerateKey() @@ -252,6 +259,41 @@ func TestEVMOnlyApplicationProducesDeterministicRoot(t *testing.T) { require.Equal(t, firstResponse.AppHash, secondResponse.AppHash) } +func TestEVMOnlyApplicationExecutesCheckedTxLikeUncheckedTx(t *testing.T) { + raw, sender := signedEVMOnlyTestTx(t, evmOnlyTestChainID, 0) + request := &abci.RequestFinalizeBlock{ + Txs: [][]byte{raw}, + Hash: crypto.Keccak256([]byte("checked-block")), + Header: &tmproto.Header{ + Height: 1, + Time: time.Unix(1_700_000_001, 0), + }, + } + checked, ok := newInitializedEVMOnlyTestApp(t).(*evmOnlyApplication) + require.True(t, ok) + unchecked := newInitializedEVMOnlyTestApp(t) + + check := checked.CheckTx(t.Context(), &abci.RequestCheckTxV2{Tx: raw}) + require.True(t, check.IsOK()) + require.Equal(t, sender, check.EVMSenderAddress) + for senders := range checked.checkedSenders.Lock() { + require.Equal(t, map[common.Hash]common.Address{decodeEVMOnlyTestTx(t, raw).Hash(): sender}, senders) + } + checkedResponse, err := checked.FinalizeBlock(t.Context(), request) + require.NoError(t, err) + for senders := range checked.checkedSenders.Lock() { + require.Empty(t, senders) + } + uncheckedResponse, err := unchecked.FinalizeBlock(t.Context(), request) + require.NoError(t, err) + + require.Equal(t, uncheckedResponse.AppHash, checkedResponse.AppHash) + require.Equal(t, uncheckedResponse.TxResults[0].GasUsed, checkedResponse.TxResults[0].GasUsed) + _, err = checked.Commit(t.Context()) + require.NoError(t, err) + require.Equal(t, uint64(1), checked.EvmNonce(sender)) +} + // A restarted node must resume from the height and app hash its storage holds, // and continue executing without an InitChain. The reference app runs the same // blocks without restarting, so the resumed chain has to match it hash for hash. diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index a23498ef50..3c9341129c 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -75,6 +75,8 @@ type GigaRouter interface { BlockByNumber(ctx context.Context, n atypes.GlobalBlockNumber) (*coretypes.ResultBlock, error) BlockByHash(ctx context.Context, hash atypes.BlockHeaderHash) (*coretypes.ResultBlock, error) EvmProxy(sender common.Address) utils.Option[*rpc.Client] + // EvmProxyEnabled reports whether EvmProxy can return Some for any sender. + EvmProxyEnabled() bool Mempool() utils.Option[*producer.State] Validators(n atypes.GlobalBlockNumber) ([]*types.Validator, atypes.GlobalBlockNumber, error) fillInboundHandshake(spec handshakeSpec) (handshakeSpec, utils.Option[handshakeOffer]) diff --git a/sei-tendermint/internal/p2p/giga_router_fullnode.go b/sei-tendermint/internal/p2p/giga_router_fullnode.go index e84c0849ba..1ed05fea2a 100644 --- a/sei-tendermint/internal/p2p/giga_router_fullnode.go +++ b/sei-tendermint/internal/p2p/giga_router_fullnode.go @@ -67,6 +67,10 @@ func (r *gigaFullnodeRouter) Run(ctx context.Context) error { }) } +// EvmProxyEnabled is always true: fullnodes have no local mempool and proxy +// every transaction. +func (r *gigaFullnodeRouter) EvmProxyEnabled() bool { return true } + // EvmProxy on the fullnode always returns the shard owner's EVM RPC client. // EnableEvmProxy is a no-op here because fullnodes do not have a local mempool. func (r *gigaFullnodeRouter) EvmProxy(sender common.Address) utils.Option[*ethrpc.Client] { diff --git a/sei-tendermint/internal/p2p/giga_router_validator.go b/sei-tendermint/internal/p2p/giga_router_validator.go index 1abd66ddb8..a5f53ff989 100644 --- a/sei-tendermint/internal/p2p/giga_router_validator.go +++ b/sei-tendermint/internal/p2p/giga_router_validator.go @@ -125,12 +125,17 @@ func (r *gigaValidatorRouter) runCommitteePeer(ctx context.Context, validatorKey } } +// EvmProxyEnabled reports whether this validator proxies txs of remote shards. +func (r *gigaValidatorRouter) EvmProxyEnabled() bool { + return r.cfg.EnableEvmProxy +} + // EvmProxy on the validator returns None when the sender's shard owner is // us (handle locally via mempool). For remote // shards, we proxy only while the target validator is currently connected; // otherwise we keep the tx local as a best-effort availability heuristic. func (r *gigaValidatorRouter) EvmProxy(sender common.Address) utils.Option[*ethrpc.Client] { - if !r.cfg.EnableEvmProxy { + if !r.EvmProxyEnabled() { return utils.None[*ethrpc.Client]() } validator := r.nextCommitEpoch.Load().Committee().EvmShard(sender) diff --git a/sei-tendermint/internal/rpc/core/mempool.go b/sei-tendermint/internal/rpc/core/mempool.go index 51e7c11d46..5e2fb8fc27 100644 --- a/sei-tendermint/internal/rpc/core/mempool.go +++ b/sei-tendermint/internal/rpc/core/mempool.go @@ -27,6 +27,14 @@ func (env *Environment) EvmProxy(sender common.Address) utils.Option[*ethrpc.Cli return utils.None[*ethrpc.Client]() } +// EvmProxyEnabled reports whether EvmProxy can return a client for any sender. +func (env *Environment) EvmProxyEnabled() bool { + if r, ok := env.gigaRouter().Get(); ok { + return r.EvmProxyEnabled() + } + return false +} + // 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)