From 63ae0b3adcd782d915611fab206ff2da4e21dfe9 Mon Sep 17 00:00:00 2001 From: masih Date: Tue, 15 Sep 2026 13:45:21 +0000 Subject: [PATCH 1/4] Recover each EVM tx sender once on the ingesting validator --- giga/evmonly/executor.go | 2 +- giga/evmonly/parser.go | 43 +++-- giga/evmonly/parser_test.go | 152 ++++++++++++++++++ giga/evmonly/rpc/server.go | 29 +++- giga/evmonly/rpc/server_test.go | 26 ++- giga/evmonly/types.go | 10 +- sei-tendermint/internal/evmonlyapp/app.go | 41 ++++- .../internal/evmonlyapp/app_test.go | 28 ++++ sei-tendermint/internal/p2p/giga_router.go | 2 + .../internal/p2p/giga_router_fullnode.go | 2 + .../internal/p2p/giga_router_validator.go | 7 +- sei-tendermint/internal/rpc/core/mempool.go | 8 + 12 files changed, 325 insertions(+), 25 deletions(-) create mode 100644 giga/evmonly/parser_test.go diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 8d46de2500..68664cba66 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -101,7 +101,7 @@ 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) + parsed, err := parseBlockTxs(ctx, req.Txs, signer, req.KnownSender, e.cfg.ParseWorkers) if err != nil { return PreparedBlock{}, err } diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go index 2955efbc66..7c69d54b08 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -9,7 +9,11 @@ import ( "golang.org/x/sync/errgroup" ) -func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, workers int) ([]PreparedTx, error) { +// knownSenderFunc returns the already verified sender of the transaction with +// the given hash. A nil function knows no senders. +type knownSenderFunc func(common.Hash) (common.Address, bool) + +func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, known knownSenderFunc, workers int) ([]PreparedTx, error) { parsed := make([]PreparedTx, len(txs)) if len(txs) == 0 { return parsed, nil @@ -19,7 +23,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, known) if err != nil { return nil, fmt.Errorf("parse tx %d: %w", i, err) } @@ -45,7 +49,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, known) if err != nil { return fmt.Errorf("parse tx %d: %w", i, err) } @@ -60,25 +64,46 @@ 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 it has an entry for the decoded transaction's hash, which binds the +// remembered sender to exactly these bytes; otherwise it is recovered from the +// signature. +func parsePreparedTx(raw []byte, signer ethtypes.Signer, known knownSenderFunc) (PreparedTx, error) { + tx, err := decodeRawTx(raw) if err != nil { return PreparedTx{}, err } if err := validateSupportedTx(tx); err != nil { return PreparedTx{}, err } + if known != nil { + if sender, ok := known(tx.Hash()); ok { + 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 - if err := tx.UnmarshalBinary(raw); err != nil { + tx, err := decodeRawTx(raw) + if err != nil { return nil, common.Address{}, err } - sender, err := ethtypes.Sender(signer, &tx) + sender, err := ethtypes.Sender(signer, tx) if err != nil { return nil, common.Address{}, err } - return &tx, sender, nil + return tx, sender, nil +} + +func decodeRawTx(raw []byte) (*ethtypes.Transaction, error) { + tx := new(ethtypes.Transaction) + if err := tx.UnmarshalBinary(raw); err != nil { + return nil, err + } + return tx, nil } diff --git a/giga/evmonly/parser_test.go b/giga/evmonly/parser_test.go new file mode 100644 index 0000000000..6fdec44b54 --- /dev/null +++ b/giga/evmonly/parser_test.go @@ -0,0 +1,152 @@ +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" +) + +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) { + lookups := 0 + known := func(hash common.Hash) (common.Address, bool) { + lookups++ + require.Equal(t, tx.Hash(), hash) + return claimed, true + } + prepared, err := parsePreparedTx(rawTx, signer, known) + require.NoError(t, err) + require.Equal(t, 1, lookups) + require.Equal(t, claimed, prepared.Sender) + require.Equal(t, tx.Hash(), prepared.Tx.Hash()) + }) + + t.Run("unknown hash falls back to recovery", func(t *testing.T) { + known := func(common.Hash) (common.Address, bool) { return claimed, false } + prepared, err := parsePreparedTx(rawTx, signer, known) + require.NoError(t, err) + require.Equal(t, sender, prepared.Sender) + }) + + t.Run("nil lookup recovers", func(t *testing.T) { + prepared, err := parsePreparedTx(rawTx, signer, nil) + require.NoError(t, err) + require.Equal(t, sender, prepared.Sender) + }) + + t.Run("sender remembered for other bytes is not applied", func(t *testing.T) { + otherRaw := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(1), nil) + other := decodeTx(t, otherRaw) + known := func(hash common.Hash) (common.Address, bool) { + if hash == other.Hash() { + return claimed, true + } + return common.Address{}, false + } + prepared, err := parsePreparedTx(rawTx, signer, known) + require.NoError(t, err) + require.Equal(t, sender, prepared.Sender) + }) +} + +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 := map[common.Hash]common.Address{} + 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[decodeTx(t, raws[i]).Hash()] = senders[i] + } + } + lookup := func(hash common.Hash) (common.Address, bool) { + sender, ok := known[hash] + return sender, ok + } + for _, workers := range []int{1, 4} { + parsed, err := parseBlockTxs(t.Context(), raws, signer, lookup, 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) + tx := decodeTx(t, rawTx) + // A deliberately wrong sender proves the lookup, not recovery, decided. + claimed := testAddress(0xc5) + + executor := NewExecutor(Config{}, withTestState(NewMemoryState())) + prepared, err := executor.PrepareBlock(t.Context(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + KnownSender: func(hash common.Hash) (common.Address, bool) { + return claimed, hash == tx.Hash() + }, + }) + require.NoError(t, err) + require.Len(t, prepared.Txs, 1) + require.Equal(t, claimed, prepared.Txs[0].Sender) + + 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 := func(common.Hash) (common.Address, bool) { return sender, true } + + b.Run("recover", func(b *testing.B) { + for b.Loop() { + if _, err := parsePreparedTx(rawTx, signer, nil); 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/server.go b/giga/evmonly/rpc/server.go index e19863a414..15f0c44f41 100644 --- a/giga/evmonly/rpc/server.go +++ b/giga/evmonly/rpc/server.go @@ -30,11 +30,14 @@ const ( var logger = seilog.NewLogger("giga", "evmonly", "rpc") // Backend submits transactions, reads finalized blocks, and returns the RPC -// client for an Autobahn shard owner. +// 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 { BroadcastTx(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) Block(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) EvmProxy(common.Address) utils.Option[*ethrpc.Client] + EvmProxyEnabled() bool } type sendAPI struct { @@ -50,13 +53,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{ @@ -78,6 +79,20 @@ 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) +} + // Server serves the EVM-only JSON-RPC API on port 8545. type Server struct { listener net.Listener diff --git a/giga/evmonly/rpc/server_test.go b/giga/evmonly/rpc/server_test.go index 192f37692d..7b5d428160 100644 --- a/giga/evmonly/rpc/server_test.go +++ b/giga/evmonly/rpc/server_test.go @@ -19,9 +19,10 @@ import ( ) 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] + broadcast func(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) + block func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) + proxy utils.Option[*ethrpc.Client] + proxyCalls int } func (b *testBackend) BroadcastTx(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { @@ -33,9 +34,14 @@ func (b *testBackend) Block(ctx context.Context, req *coretypes.RequestBlockInfo } 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 TestSendRawTransaction(t *testing.T) { tx, raw := testSignedTransaction(t) var broadcastRaw []byte @@ -67,6 +73,20 @@ func TestSendRawTransaction(t *testing.T) { require.ErrorContains(t, err, "method status does not exist") } +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) +} + func TestRejectsInvalidTransaction(t *testing.T) { backend := &testBackend{ broadcast: func(context.Context, *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 99e7be07f8..27e3578b6f 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -36,10 +36,14 @@ 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. KnownSender, when non-nil, +// returns the sender of a transaction whose signature this process has already +// verified, keyed by transaction hash; PrepareBlock uses it instead of +// recovering the sender. Transactions it does not know are recovered as usual. type BlockRequest struct { - Context BlockContext - Txs [][]byte + Context BlockContext + Txs [][]byte + KnownSender func(common.Hash) (common.Address, bool) } // 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 20c6d466fc..1e226abed9 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -30,6 +30,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 @@ -39,6 +44,10 @@ type evmOnlyApplication struct { changeSetEncoder evmonly.NamedChangeSetEncoder validators []abci.ValidatorUpdate state utils.Mutex[*evmOnlyState] + // 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] } type evmOnlyState struct { @@ -76,6 +85,7 @@ func NewEVMOnlyApplication( changeSetEncoder: changeSetEncoder, validators: slices.Clone(validators), state: utils.NewMutex(&evmOnlyState{}), + checkedSenders: utils.NewMutex(map[common.Hash]common.Address{}), } } @@ -174,6 +184,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, @@ -188,6 +199,32 @@ 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 + } +} + +// knownSender returns the sender CheckTx recovered for hash, if any. +func (a *evmOnlyApplication) knownSender(hash common.Hash) (common.Address, bool) { + for senders := range a.checkedSenders.Lock() { + sender, ok := senders[hash] + return sender, ok + } + panic("unreachable") +} + +func (a *evmOnlyApplication) forgetSenders(result *evmonly.BlockResult) { + for senders := range a.checkedSenders.Lock() { + for _, tx := range result.Txs { + delete(senders, tx.Hash) + } + } +} + func (a *evmOnlyApplication) parseTx(raw []byte) (*ethtypes.Transaction, common.Address, error) { tx := new(ethtypes.Transaction) if err := tx.UnmarshalBinary(raw); err != nil { @@ -275,12 +312,14 @@ 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, + KnownSender: a.knownSender, }) if err != nil { return nil, err } defer result.Release() + a.forgetSenders(result) appHash, err := hashEVMOnlyResult(state.appHash, number, blockHash, result) if err != nil { return nil, err diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index 1c2cf21ab9..53303457ec 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -192,6 +192,34 @@ 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 := newInitializedEVMOnlyTestApp(t) + unchecked := newInitializedEVMOnlyTestApp(t) + + check := checked.CheckTx(t.Context(), &abci.RequestCheckTxV2{Tx: raw}) + require.True(t, check.IsOK()) + require.Equal(t, sender, check.EVMSenderAddress) + checkedResponse, err := checked.FinalizeBlock(t.Context(), request) + require.NoError(t, err) + 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)) +} + func TestEVMOnlyApplicationRequiresInitChain(t *testing.T) { app := newEVMOnlyTestApp(t, nil) 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..9418a8f37c 100644 --- a/sei-tendermint/internal/p2p/giga_router_fullnode.go +++ b/sei-tendermint/internal/p2p/giga_router_fullnode.go @@ -69,6 +69,8 @@ func (r *gigaFullnodeRouter) Run(ctx context.Context) error { // 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) EvmProxyEnabled() bool { return true } + func (r *gigaFullnodeRouter) EvmProxy(sender common.Address) utils.Option[*ethrpc.Client] { return r.evmProxy(r.nextCommitEpoch.Load().Committee().EvmShard(sender)) } 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 a11a802ef1..e86ffa3b6a 100644 --- a/sei-tendermint/internal/rpc/core/mempool.go +++ b/sei-tendermint/internal/rpc/core/mempool.go @@ -26,6 +26,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 +} + func (env *Environment) EvmTxByHash(hash common.Hash) (types.Tx, bool) { if giga, ok := env.gigaRouter().Get(); ok { if v, ok := giga.Mempool().Get(); ok { From 1f4069bbcd6b082f11626ada24437ff87574a49b Mon Sep 17 00:00:00 2001 From: masih Date: Tue, 15 Sep 2026 21:23:04 +0000 Subject: [PATCH 2/4] Guard known senders by chain ID and cover the sender cache lifecycle --- giga/evmonly/executor_test.go | 3 ++- giga/evmonly/parser.go | 18 +++--------------- giga/evmonly/parser_test.go | 11 +++++++++++ giga/evmonly/types.go | 5 +++-- sei-tendermint/internal/evmonlyapp/app_test.go | 17 ++++++++++++++++- .../internal/p2p/giga_router_fullnode.go | 6 ++++-- 6 files changed, 39 insertions(+), 21 deletions(-) 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 7c69d54b08..b71912765a 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -65,8 +65,8 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, kn } // parsePreparedTx decodes raw and resolves its sender. The sender is taken from -// known when it has an entry for the decoded transaction's hash, which binds the -// remembered sender to exactly these bytes; otherwise it is recovered from the +// known when it has an entry for the decoded transaction's hash and the +// transaction is bound to signer's chain; otherwise it is recovered from the // signature. func parsePreparedTx(raw []byte, signer ethtypes.Signer, known knownSenderFunc) (PreparedTx, error) { tx, err := decodeRawTx(raw) @@ -76,7 +76,7 @@ func parsePreparedTx(raw []byte, signer ethtypes.Signer, known knownSenderFunc) if err := validateSupportedTx(tx); err != nil { return PreparedTx{}, err } - if known != nil { + if known != nil && tx.Protected() && tx.ChainId().Cmp(signer.ChainID()) == 0 { if sender, ok := known(tx.Hash()); ok { return PreparedTx{Tx: tx, Sender: sender}, nil } @@ -88,18 +88,6 @@ func parsePreparedTx(raw []byte, signer ethtypes.Signer, known knownSenderFunc) return PreparedTx{Tx: tx, Sender: sender}, nil } -func parseTx(raw []byte, signer ethtypes.Signer) (*ethtypes.Transaction, common.Address, error) { - tx, err := decodeRawTx(raw) - if err != nil { - return nil, common.Address{}, err - } - sender, err := ethtypes.Sender(signer, tx) - if err != nil { - return nil, common.Address{}, err - } - return tx, sender, nil -} - func decodeRawTx(raw []byte) (*ethtypes.Transaction, error) { tx := new(ethtypes.Transaction) if err := tx.UnmarshalBinary(raw); err != nil { diff --git a/giga/evmonly/parser_test.go b/giga/evmonly/parser_test.go index 6fdec44b54..beb8162a8d 100644 --- a/giga/evmonly/parser_test.go +++ b/giga/evmonly/parser_test.go @@ -61,6 +61,17 @@ func TestParsePreparedTxUsesKnownSender(t *testing.T) { 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) + known := func(common.Hash) (common.Address, bool) { + t.Fatal("known sender consulted for a tx from another chain") + return claimed, true + } + _, err := parsePreparedTx(otherRaw, signer, known) + require.ErrorIs(t, err, ethtypes.ErrInvalidChainId) + }) } func TestParseBlockTxsMixesKnownAndRecoveredSenders(t *testing.T) { diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 27e3578b6f..b6fa9d3f9f 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -38,8 +38,9 @@ type ResultSink interface { // BlockRequest contains all consensus/runtime inputs needed to execute a block. // Txs must be raw Ethereum transaction RLP bytes. KnownSender, when non-nil, // returns the sender of a transaction whose signature this process has already -// verified, keyed by transaction hash; PrepareBlock uses it instead of -// recovering the sender. Transactions it does not know are recovered as usual. +// verified against the executor's chain ID, keyed by transaction hash; +// PrepareBlock uses it instead of recovering the sender for transactions bound +// to that chain. Transactions it does not know are recovered as usual. type BlockRequest struct { Context BlockContext Txs [][]byte diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index 53303457ec..29173afa77 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -19,6 +19,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() @@ -202,14 +209,22 @@ func TestEVMOnlyApplicationExecutesCheckedTxLikeUncheckedTx(t *testing.T) { Time: time.Unix(1_700_000_001, 0), }, } - checked := newInitializedEVMOnlyTestApp(t) + 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) + _, ok = checked.knownSender(common.Hash{}) + require.False(t, ok) + known, ok := checked.knownSender(decodeEVMOnlyTestTx(t, raw).Hash()) + require.True(t, ok) + require.Equal(t, sender, known) checkedResponse, err := checked.FinalizeBlock(t.Context(), request) require.NoError(t, err) + _, ok = checked.knownSender(decodeEVMOnlyTestTx(t, raw).Hash()) + require.False(t, ok) uncheckedResponse, err := unchecked.FinalizeBlock(t.Context(), request) require.NoError(t, err) diff --git a/sei-tendermint/internal/p2p/giga_router_fullnode.go b/sei-tendermint/internal/p2p/giga_router_fullnode.go index 9418a8f37c..1ed05fea2a 100644 --- a/sei-tendermint/internal/p2p/giga_router_fullnode.go +++ b/sei-tendermint/internal/p2p/giga_router_fullnode.go @@ -67,10 +67,12 @@ func (r *gigaFullnodeRouter) Run(ctx context.Context) error { }) } -// 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. +// 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] { return r.evmProxy(r.nextCommitEpoch.Load().Committee().EvmShard(sender)) } From c2ee7f42e38f20d16dcf8f859a79adb314245bd9 Mon Sep 17 00:00:00 2001 From: masih Date: Tue, 15 Sep 2026 21:41:42 +0000 Subject: [PATCH 3/4] Restore upstream formatting in rpc/core mempool.go --- sei-tendermint/internal/rpc/core/mempool.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sei-tendermint/internal/rpc/core/mempool.go b/sei-tendermint/internal/rpc/core/mempool.go index afa0ad1200..5e2fb8fc27 100644 --- a/sei-tendermint/internal/rpc/core/mempool.go +++ b/sei-tendermint/internal/rpc/core/mempool.go @@ -192,10 +192,10 @@ func (env *Environment) broadcastTxCommitFromCheckTx(ctx context.Context, req *c "duration", time.Since(startAt), "err", ctx.Err()) return &coretypes.ResultBroadcastTxCommit{ - CheckTx: *r, - Hash: req.Tx.Hash().Bytes(), - }, fmt.Errorf("timeout waiting for commit of tx %s (%s)", - req.Tx.Hash(), time.Since(startAt)) + CheckTx: *r, + Hash: req.Tx.Hash().Bytes(), + }, fmt.Errorf("timeout waiting for commit of tx %s (%s)", + req.Tx.Hash(), time.Since(startAt)) case <-timer.C: txres, err := env.Tx(ctx, &coretypes.RequestTx{ Hash: req.Tx.Hash().Bytes(), From 0b30507e3841cdb0d6a91eda83994da0ca071103 Mon Sep 17 00:00:00 2001 From: masih Date: Wed, 16 Sep 2026 11:54:49 +0000 Subject: [PATCH 4/4] Pass known senders to the executor as a slice aligned with the block's txs --- giga/evmonly/executor.go | 5 +- giga/evmonly/parser.go | 32 +++++---- giga/evmonly/parser_test.go | 70 ++++++------------- giga/evmonly/types.go | 18 ++--- sei-tendermint/internal/evmonlyapp/app.go | 30 ++++---- .../internal/evmonlyapp/app_test.go | 13 ++-- 6 files changed, 73 insertions(+), 95 deletions(-) diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 5176ce97c6..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, req.KnownSender, 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/parser.go b/giga/evmonly/parser.go index b71912765a..8b131813e3 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -7,13 +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" ) -// knownSenderFunc returns the already verified sender of the transaction with -// the given hash. A nil function knows no senders. -type knownSenderFunc func(common.Hash) (common.Address, bool) +// 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, known knownSenderFunc, workers int) ([]PreparedTx, error) { +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 @@ -23,7 +30,7 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, kn if err := ctx.Err(); err != nil { return nil, err } - prepared, err := parsePreparedTx(raw, signer, known) + prepared, err := parsePreparedTx(raw, signer, senderAt(senders, i)) if err != nil { return nil, fmt.Errorf("parse tx %d: %w", i, err) } @@ -49,7 +56,7 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, kn for range workers { g.Go(func() error { for i := range jobs { - prepared, err := parsePreparedTx(txs[i], signer, known) + prepared, err := parsePreparedTx(txs[i], signer, senderAt(senders, i)) if err != nil { return fmt.Errorf("parse tx %d: %w", i, err) } @@ -65,10 +72,9 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer, kn } // parsePreparedTx decodes raw and resolves its sender. The sender is taken from -// known when it has an entry for the decoded transaction's hash and the -// transaction is bound to signer's chain; otherwise it is recovered from the -// signature. -func parsePreparedTx(raw []byte, signer ethtypes.Signer, known knownSenderFunc) (PreparedTx, error) { +// 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 @@ -76,10 +82,8 @@ func parsePreparedTx(raw []byte, signer ethtypes.Signer, known knownSenderFunc) if err := validateSupportedTx(tx); err != nil { return PreparedTx{}, err } - if known != nil && tx.Protected() && tx.ChainId().Cmp(signer.ChainID()) == 0 { - if sender, ok := known(tx.Hash()); ok { - return PreparedTx{Tx: tx, Sender: sender}, nil - } + 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 { diff --git a/giga/evmonly/parser_test.go b/giga/evmonly/parser_test.go index beb8162a8d..b71b2b6f20 100644 --- a/giga/evmonly/parser_test.go +++ b/giga/evmonly/parser_test.go @@ -8,6 +8,8 @@ import ( 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) { @@ -22,42 +24,14 @@ func TestParsePreparedTxUsesKnownSender(t *testing.T) { claimed := testAddress(0xc2) t.Run("known sender is used without recovery", func(t *testing.T) { - lookups := 0 - known := func(hash common.Hash) (common.Address, bool) { - lookups++ - require.Equal(t, tx.Hash(), hash) - return claimed, true - } - prepared, err := parsePreparedTx(rawTx, signer, known) + prepared, err := parsePreparedTx(rawTx, signer, utils.Some(claimed)) require.NoError(t, err) - require.Equal(t, 1, lookups) require.Equal(t, claimed, prepared.Sender) require.Equal(t, tx.Hash(), prepared.Tx.Hash()) }) - t.Run("unknown hash falls back to recovery", func(t *testing.T) { - known := func(common.Hash) (common.Address, bool) { return claimed, false } - prepared, err := parsePreparedTx(rawTx, signer, known) - require.NoError(t, err) - require.Equal(t, sender, prepared.Sender) - }) - - t.Run("nil lookup recovers", func(t *testing.T) { - prepared, err := parsePreparedTx(rawTx, signer, nil) - require.NoError(t, err) - require.Equal(t, sender, prepared.Sender) - }) - - t.Run("sender remembered for other bytes is not applied", func(t *testing.T) { - otherRaw := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(1), nil) - other := decodeTx(t, otherRaw) - known := func(hash common.Hash) (common.Address, bool) { - if hash == other.Hash() { - return claimed, true - } - return common.Address{}, false - } - prepared, err := parsePreparedTx(rawTx, signer, known) + 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) }) @@ -65,11 +39,7 @@ func TestParsePreparedTxUsesKnownSender(t *testing.T) { 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) - known := func(common.Hash) (common.Address, bool) { - t.Fatal("known sender consulted for a tx from another chain") - return claimed, true - } - _, err := parsePreparedTx(otherRaw, signer, known) + _, err := parsePreparedTx(otherRaw, signer, utils.Some(claimed)) require.ErrorIs(t, err, ethtypes.ErrInvalidChainId) }) } @@ -81,22 +51,18 @@ func TestParseBlockTxsMixesKnownAndRecoveredSenders(t *testing.T) { const n = 8 raws := make([][]byte, n) senders := make([]common.Address, n) - known := map[common.Hash]common.Address{} + 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[decodeTx(t, raws[i]).Hash()] = senders[i] + known[i] = utils.Some(senders[i]) } } - lookup := func(hash common.Hash) (common.Address, bool) { - sender, ok := known[hash] - return sender, ok - } for _, workers := range []int{1, 4} { - parsed, err := parseBlockTxs(t.Context(), raws, signer, lookup, workers) + parsed, err := parseBlockTxs(t.Context(), raws, signer, known, workers) require.NoError(t, err) require.Len(t, parsed, n) for i, prepared := range parsed { @@ -112,22 +78,26 @@ func TestExecutorPrepareBlockUsesKnownSender(t *testing.T) { sender := crypto.PubkeyToAddress(key.PublicKey) recipient := testAddress(0xc4) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) - tx := decodeTx(t, rawTx) - // A deliberately wrong sender proves the lookup, not recovery, decided. + // 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}, - KnownSender: func(hash common.Hash) (common.Address, bool) { - return claimed, hash == tx.Hash() - }, + 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}, @@ -144,11 +114,11 @@ func BenchmarkParsePreparedTx(b *testing.B) { sender := crypto.PubkeyToAddress(key.PublicKey) recipient := testAddress(0xc6) rawTx := signLegacyTx(b, key, chainID, 0, &recipient, big.NewInt(1), nil) - known := func(common.Hash) (common.Address, bool) { return sender, true } + known := utils.Some(sender) b.Run("recover", func(b *testing.B) { for b.Loop() { - if _, err := parsePreparedTx(rawTx, signer, nil); err != nil { + if _, err := parsePreparedTx(rawTx, signer, utils.None[common.Address]()); err != nil { b.Fatal(err) } } diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index b6fa9d3f9f..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,15 +38,15 @@ type ResultSink interface { } // BlockRequest contains all consensus/runtime inputs needed to execute a block. -// Txs must be raw Ethereum transaction RLP bytes. KnownSender, when non-nil, -// returns the sender of a transaction whose signature this process has already -// verified against the executor's chain ID, keyed by transaction hash; -// PrepareBlock uses it instead of recovering the sender for transactions bound -// to that chain. Transactions it does not know are recovered as usual. +// 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 - KnownSender func(common.Hash) (common.Address, bool) + 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 2893770793..4dba48e2d8 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -258,21 +258,22 @@ func (a *evmOnlyApplication) rememberSender(hash common.Hash, sender common.Addr } } -// knownSender returns the sender CheckTx recovered for hash, if any. -func (a *evmOnlyApplication) knownSender(hash common.Hash) (common.Address, bool) { +// 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() { - sender, ok := senders[hash] - return sender, ok - } - panic("unreachable") -} - -func (a *evmOnlyApplication) forgetSenders(result *evmonly.BlockResult) { - for senders := range a.checkedSenders.Lock() { - for _, tx := range result.Txs { - delete(senders, tx.Hash) + 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) { @@ -364,14 +365,13 @@ func (a *evmOnlyApplication) FinalizeBlock(ctx context.Context, req *abci.Reques BlockHash: blockHash, PrevRandao: crypto.Keccak256Hash(binary.BigEndian.AppendUint64(nil, timestamp)), }, - Txs: req.Txs, - KnownSender: a.knownSender, + Txs: req.Txs, + Senders: a.takeSenders(req.Txs), }) if err != nil { return nil, errors.Join(err, a.abandonPending(height)) } defer result.Release() - a.forgetSenders(result) pending, err := a.pendingCursor(height) if err != nil { return nil, err diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index 57c9683f3a..5f9cddd05c 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -276,15 +276,14 @@ func TestEVMOnlyApplicationExecutesCheckedTxLikeUncheckedTx(t *testing.T) { check := checked.CheckTx(t.Context(), &abci.RequestCheckTxV2{Tx: raw}) require.True(t, check.IsOK()) require.Equal(t, sender, check.EVMSenderAddress) - _, ok = checked.knownSender(common.Hash{}) - require.False(t, ok) - known, ok := checked.knownSender(decodeEVMOnlyTestTx(t, raw).Hash()) - require.True(t, ok) - require.Equal(t, sender, known) + 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) - _, ok = checked.knownSender(decodeEVMOnlyTestTx(t, raw).Hash()) - require.False(t, ok) + for senders := range checked.checkedSenders.Lock() { + require.Empty(t, senders) + } uncheckedResponse, err := unchecked.FinalizeBlock(t.Context(), request) require.NoError(t, err)