Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
ffac0a1
reduced runTx results
pompon0 Apr 30, 2026
4e7a82d
removed sender
pompon0 Apr 30, 2026
0f5af37
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding3
pompon0 Apr 30, 2026
ed35c51
out-of-process configs deprecated
pompon0 Apr 30, 2026
93cd15f
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding5
pompon0 May 1, 2026
3086a71
generic client context
pompon0 May 1, 2026
1141f9f
registrations for local node
pompon0 May 1, 2026
c7ce3a0
reordered defers in startInProcess
pompon0 May 1, 2026
956b0e5
compilation fix
pompon0 May 1, 2026
eb2c2c7
removed cast to NodeService
pompon0 May 1, 2026
b04b46f
LocalContext + test fix
pompon0 May 1, 2026
7bba698
depth 1
pompon0 May 4, 2026
9df91b7
fmt
pompon0 May 4, 2026
46faae0
test fix
pompon0 May 4, 2026
ad576ab
tx service is local
pompon0 May 4, 2026
149f9c8
test fix
pompon0 May 4, 2026
3ec92c0
simplified stuff
pompon0 May 4, 2026
a92bad2
simplified block access
pompon0 May 4, 2026
7744232
WIP
pompon0 May 4, 2026
caf7a9a
constrained servers
pompon0 May 4, 2026
a0d0a18
fully local evmrpc
pompon0 May 4, 2026
2c18b75
Re-run checks
pompon0 May 4, 2026
5034212
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding7
pompon0 May 4, 2026
d5a44f6
test fix
pompon0 May 4, 2026
24bded4
test fix
pompon0 May 4, 2026
4f45db2
reverted generic context
pompon0 May 5, 2026
ad1280d
compilation fix
pompon0 May 5, 2026
4d86336
reduced boiledplate
pompon0 May 5, 2026
f8d4d59
EvmNextPendingNonce
pompon0 May 5, 2026
f455ccf
duplicated evm nonce management
pompon0 May 5, 2026
7e86151
rewritten nonce management
pompon0 May 6, 2026
661dada
codex fixes
pompon0 May 6, 2026
2dd645f
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding7
pompon0 May 6, 2026
c342b5c
codex
pompon0 May 6, 2026
51981c0
codex
pompon0 May 6, 2026
c136a19
removed callbacks
pompon0 May 6, 2026
655d393
grouped evm data in WrappedTx, needs more work
pompon0 May 6, 2026
d60cf36
fixes
pompon0 May 7, 2026
8cac6bc
common.Address
pompon0 May 7, 2026
307c709
expiration index is out
pompon0 May 7, 2026
af313d9
removed unused args
pompon0 May 8, 2026
5dd0813
inline cleanupTx
pompon0 May 8, 2026
641c0da
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding8
pompon0 May 8, 2026
9beb38f
lint
pompon0 May 8, 2026
2b05133
EvmBalance fix
pompon0 May 8, 2026
0dbb903
snapshot
pompon0 May 8, 2026
eaa2ed9
balance handling fix
pompon0 May 8, 2026
b25b31c
test fix
pompon0 May 8, 2026
bbee25c
deflake port reservation
pompon0 May 8, 2026
e345b59
added a TODO
pompon0 May 8, 2026
2a07fc2
wip
pompon0 May 14, 2026
2138fe1
minor fix for 'removed' field access
pompon0 May 14, 2026
46e1720
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding8
pompon0 May 14, 2026
42cdb5a
added EvmSigner
pompon0 May 14, 2026
6b5daef
draft
pompon0 May 14, 2026
2767591
implemented the thing
pompon0 May 14, 2026
16d45b4
lookup
pompon0 May 14, 2026
8aec53a
fix
pompon0 May 14, 2026
5eb329d
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding8
pompon0 May 14, 2026
d4d7902
fix
pompon0 May 14, 2026
6a6edaf
Merge branch 'gprusak-rpc-sharding8' into gprusak-rpc-sharding9
pompon0 May 14, 2026
f99a6da
inline
pompon0 May 14, 2026
0feb1a1
improved test
pompon0 May 14, 2026
bf85d26
nonce and txs
pompon0 May 14, 2026
2259bf4
pool revamped
pompon0 May 14, 2026
904f8a0
no overrides
pompon0 May 14, 2026
646ecff
WIP
pompon0 May 14, 2026
b795714
reverted pool
pompon0 May 14, 2026
3fe7a82
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding8
pompon0 May 14, 2026
93dd1cb
Merge branch 'gprusak-rpc-sharding8' into gprusak-rpc-sharding9
pompon0 May 14, 2026
d7a97fd
reencoding test
pompon0 May 14, 2026
0796293
lint
pompon0 May 15, 2026
46b74e1
Merge remote-tracking branch 'origin/main' into gprusak-rpc-sharding9
pompon0 May 15, 2026
9c3516e
evmrpc address populated
pompon0 May 15, 2026
51c11db
metric
pompon0 May 15, 2026
0b5649d
added back AccessListTx support
pompon0 May 15, 2026
33794ff
EVMRPC_PORT variable
pompon0 May 15, 2026
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
2 changes: 2 additions & 0 deletions docker/localnode/scripts/step1_configure_init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ cp docker/localnode/config/config.toml "$TENDERMINT_CONFIG_FILE"
SEI_NODE_ID=$(seid tendermint show-node-id)
NODE_IP=$(hostname -i | awk '{print $1}')
P2P_PORT=26656 # Must match [p2p] laddr in config.toml
EVMRPC_PORT=8545 # Must match the EVM RPC HTTP port (evmrpc DefaultConfig HTTPPort).
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.

nit: would be nice if both ports come from a common config

echo "$SEI_NODE_ID@$NODE_IP:$P2P_PORT" >> build/generated/persistent_peers.txt

# Store autobahn-compatible pubkeys and address for config generation
cp ~/.sei/config/validator_pubkey.txt "$NODE_DIR/" || { echo "ERROR: failed to copy validator_pubkey.txt"; exit 1; }
cp ~/.sei/config/node_pubkey.txt "$NODE_DIR/" || { echo "ERROR: failed to copy node_pubkey.txt"; exit 1; }
echo "$NODE_IP:$P2P_PORT" > "$NODE_DIR/autobahn_address.txt"
echo "http://$NODE_IP:$EVMRPC_PORT" > "$NODE_DIR/evmrpc_url.txt"

# Create a new account
ACCOUNT_NAME="node_admin"
Expand Down
5 changes: 5 additions & 0 deletions evmrpc/block_txcount_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package evmrpc_test
import (
"context"
"fmt"
"net/url"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -37,6 +38,10 @@ func (*parityTxCountTMClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (*parityTxCountTMClient) EvmProxy(common.Address) (*url.URL, bool) {
return nil, false
}

func (c *parityTxCountTMClient) Block(_ context.Context, h *int64) (*coretypes.ResultBlock, error) {
if h != nil && *h == parityTestHeight {
return c.block, nil
Expand Down
5 changes: 5 additions & 0 deletions evmrpc/height_availability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package evmrpc
import (
"context"
"encoding/hex"
"net/url"
"testing"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -33,6 +34,10 @@ func (*heightTestClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (*heightTestClient) EvmProxy(common.Address) (*url.URL, bool) {
return nil, false
}

func newHeightTestClient(highHeight, earliest, latest int64) *heightTestClient {
return &heightTestClient{
Client: mock.Client{},
Expand Down
19 changes: 17 additions & 2 deletions evmrpc/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ var (
rpcTelemetryMeter = otel.Meter("evmrpc")

metrics = struct {
requestLatencySeconds metric.Float64Histogram
wsConnectionCount metric.Int64Counter
requestLatencySeconds metric.Float64Histogram
wsConnectionCount metric.Int64Counter
redirectedRequestCount metric.Int64Counter
}{
requestLatencySeconds: must(rpcTelemetryMeter.Float64Histogram(
"evmrpc_request_latency_seconds",
Expand All @@ -57,6 +58,11 @@ var (
metric.WithDescription("Number of new websocket connections"),
metric.WithUnit("{count}"),
)),
redirectedRequestCount: must(rpcTelemetryMeter.Int64Counter(
"evmrpc_redirected_requests_total",
metric.WithDescription("Number of EVM RPC requests redirected to another validator"),
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.

nit: can you clarify whether this is request you forwarded to someone else or it is request you received from redirection?

metric.WithUnit("{count}"),
)),
}
)

Expand Down Expand Up @@ -125,3 +131,12 @@ func recordRPCLatency(ctx context.Context, endpoint, connection string, success
func recordWebsocketConnect(ctx context.Context) {
metrics.wsConnectionCount.Add(ctx, 1)
}

func recordRedirectedRequest(ctx context.Context, endpoint, connection string) {
metrics.redirectedRequestCount.Add(ctx, 1,
metric.WithAttributes(
attribute.String(endpointKey, endpoint),
attribute.String(connectionKey, connection),
),
)
}
1 change: 1 addition & 0 deletions evmrpc/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestRecordRPCMetricsNoPanic(t *testing.T) {
endpoint := "eth_smoke_" + t.Name()
recordRPCLatency(ctx, endpoint, "http", true, nil, false, time.Now().Add(-2*time.Millisecond))
recordWebsocketConnect(ctx)
recordRedirectedRequest(ctx, endpoint, "http")
}

func TestClassifyRPCMetricError(t *testing.T) {
Expand Down
84 changes: 53 additions & 31 deletions evmrpc/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"math/big"
"sync"
"time"

Expand Down Expand Up @@ -77,25 +78,46 @@ func (s *SendAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) (
return
}
hash = tx.Hash()
var txData ethtx.TxData
txData, err = ethtx.NewTxDataFromTx(tx)
if err != nil {
return
// getSender fails for AccessListTx, in which case we are not able to proxy or simulate,
// but we still need to handle it.
sender, senderErr := getSender(tx, s.keeper.ChainID(s.ctxProvider(LatestCtxHeight)))
if senderErr == nil {
if url, ok := s.tmClient.EvmProxy(sender); ok {
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.

What's our plan for handling the case where one validator is down?

recordRedirectedRequest(ctx, "eth_sendRawTransaction", string(s.connectionType))
// HTTP transport pooling already happens globally underneath net/http, so
// creating a fresh RPC client per proxied request is fine here. If we
// start proxying over WebSocket, we'll need explicit custom pooling since
// the underlying TCP connection lifecycle is strictly bound to Dial -> Close calls.
client, err := rpc.DialContext(ctx, url.String())
if err != nil {
return hash, fmt.Errorf("rpc.DialContext(%q): %w", url.String(), err)
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.

Would we see one error log per tx when one validator is down? That would be too spammy

}
defer client.Close()

if err := client.CallContext(ctx, &hash, "eth_sendRawTransaction", input); err != nil {
return hash, fmt.Errorf("eth_sendRawTransaction(%q): %w", url.String(), err)
}
return hash, nil
}
}
var msg *types.MsgEVMTransaction
msg, err = types.NewMsgEVMTransaction(txData)

txData, err := ethtx.NewTxDataFromTx(tx)
if err != nil {
return
return hash, err
}
var gasUsedEstimate uint64
gasUsedEstimate, err = s.simulateTx(ctx, tx)
msg, err := types.NewMsgEVMTransaction(txData)
if err != nil {
tx, _ = msg.AsTransaction()
gasUsedEstimate = tx.Gas() // if issue simulating, fallback to gas limit
return hash, err
}
gasUsedEstimate := tx.Gas() // if issue simulating, fallback to gas limit
if senderErr == nil { // simulation requires sender.
if gas, err := s.simulateTx(ctx, sender, tx); err == nil {
gasUsedEstimate = gas
}
}
txBuilder := s.txConfigProvider(LatestCtxHeight).NewTxBuilder()
if err = txBuilder.SetMsgs(msg); err != nil {
return
if err := txBuilder.SetMsgs(msg); err != nil {
return hash, err
}
txBuilder.SetGasEstimate(gasUsedEstimate)
txbz, encodeErr := s.txConfigProvider(LatestCtxHeight).TxEncoder()(txBuilder.GetTx())
Expand Down Expand Up @@ -125,30 +147,30 @@ func (s *SendAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) (
return
}

func (s *SendAPI) simulateTx(ctx context.Context, tx *ethtypes.Transaction) (estimate uint64, err error) {
var from common.Address
if tx.Type() == ethtypes.DynamicFeeTxType {
signer := ethtypes.NewLondonSigner(s.keeper.ChainID(s.ctxProvider(LatestCtxHeight)))
from, err = signer.Sender(tx)
func getSender(tx *ethtypes.Transaction, chainID *big.Int) (common.Address, error) {
switch {
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.

Can we just use LatestSignerForChainID(chainID).Sender(tx) so we don't need to do the switch ourselves?

case tx.Type() == ethtypes.DynamicFeeTxType:
from, err := ethtypes.NewLondonSigner(chainID).Sender(tx)
if err != nil {
err = fmt.Errorf("failed to get sender for dynamic fee tx: %w", err)
return
return common.Address{}, fmt.Errorf("failed to get sender for dynamic fee tx: %w", err)
}
} else if tx.Protected() {
signer := ethtypes.NewEIP155Signer(s.keeper.ChainID(s.ctxProvider(LatestCtxHeight)))
from, err = signer.Sender(tx)
return from, nil
case tx.Protected():
from, err := ethtypes.NewEIP155Signer(chainID).Sender(tx)
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.

Would we always have NewEIP155Signer in tx.Protected()? Would ethtypes.LatestSignerForChainID(chainID)) be better? So we accept non-1559 transactions?

if err != nil {
err = fmt.Errorf("failed to get sender for protected tx: %w", err)
return
return common.Address{}, fmt.Errorf("failed to get sender for protected tx: %w", err)
}
} else {
signer := ethtypes.HomesteadSigner{}
from, err = signer.Sender(tx)
return from, nil
default:
from, err := ethtypes.HomesteadSigner{}.Sender(tx)
if err != nil {
err = fmt.Errorf("failed to get sender for homestead tx: %w", err)
return
return common.Address{}, fmt.Errorf("failed to get sender for homestead tx: %w", err)
}
return from, nil
}
}

func (s *SendAPI) simulateTx(ctx context.Context, sender common.Address, tx *ethtypes.Transaction) (estimate uint64, err error) {
input_ := (hexutil.Bytes)(tx.Data())
gas_ := hexutil.Uint64(tx.Gas())
nonce_ := hexutil.Uint64(tx.Nonce())
Expand All @@ -165,7 +187,7 @@ func (s *SendAPI) simulateTx(ctx context.Context, tx *ethtypes.Transaction) (est
gp = nil
}
txArgs := export.TransactionArgs{
From: &from,
From: &sender,
To: tx.To(),
Gas: &gas_,
GasPrice: (*hexutil.Big)(gp),
Expand Down
85 changes: 85 additions & 0 deletions evmrpc/send_test.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
package evmrpc_test

import (
"context"
"encoding/hex"
"encoding/json"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
legacyabci "github.com/sei-protocol/sei-chain/app/legacyabci"
"github.com/sei-protocol/sei-chain/evmrpc"
"github.com/sei-protocol/sei-chain/sei-cosmos/client"
"github.com/sei-protocol/sei-chain/sei-cosmos/crypto/hd"
sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
"github.com/sei-protocol/sei-chain/x/evm/types"
"github.com/stretchr/testify/require"
)

type sendProxyClient struct {
*MockClient
proxyURL *url.URL
}

func (c *sendProxyClient) EvmProxy(common.Address) (*url.URL, bool) {
if c.proxyURL == nil {
return nil, false
}
return c.proxyURL, true
}

func TestMnemonicToPrivateKey(t *testing.T) {
mnemonic := "mushroom lamp kingdom obscure sun advice puzzle ancient crystal service beef have zone true chimney act situate laundry guess vacuum razor virus wink enforce"
hdp := hd.CreateHDPath(sdk.GetConfig().GetCoinType(), 0, 0).String()
Expand Down Expand Up @@ -63,3 +84,67 @@ func TestSendRawTransaction(t *testing.T) {
errMap = resObj["error"].(map[string]interface{})
require.Equal(t, ": invalid sequence", errMap["message"].(string))
}

func TestSendRawTransactionUsesProxy(t *testing.T) {
to := common.HexToAddress("010203")
_, tx := buildTx(ethtypes.DynamicFeeTx{
Nonce: 1,
GasFeeCap: big.NewInt(10),
Gas: 1000,
To: &to,
Value: big.NewInt(1000),
Data: []byte("abc"),
ChainID: EVMKeeper.ChainID(Ctx),
})
ethTxBytes, err := tx.MarshalBinary()
require.NoError(t, err)

var gotMethod string
var gotPayload string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()

var req struct {
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
}
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
require.Len(t, req.Params, 1)
gotMethod = req.Method
require.NoError(t, json.Unmarshal(req.Params[0], &gotPayload))

w.Header().Set("Content-Type", "application/json")
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"id": 1,
"result": tx.Hash().Hex(),
}))
}))
defer server.Close()

proxyURL, err := url.Parse(server.URL)
require.NoError(t, err)

sendAPI := evmrpc.NewSendAPI(
&sendProxyClient{MockClient: &MockClient{}, proxyURL: proxyURL},
func(int64) client.TxConfig { return TxConfig },
&evmrpc.SendConfig{},
EVMKeeper,
legacyabci.BeginBlockKeepers{},
func(int64) sdk.Context { return Ctx },
"",
nil,
nil,
nil,
evmrpc.ConnectionTypeHTTP,
evmrpc.NewBlockCache(1),
nil,
nil,
)

hash, err := sendAPI.SendRawTransaction(context.Background(), hexutil.Bytes(ethTxBytes))
require.NoError(t, err)
require.Equal(t, tx.Hash(), hash)
require.Equal(t, "eth_sendRawTransaction", gotMethod)
require.Equal(t, hexutil.Encode(ethTxBytes), gotPayload)
}
5 changes: 5 additions & 0 deletions evmrpc/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"math/big"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -123,6 +124,10 @@ func (*MockClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (*MockClient) EvmProxy(common.Address) (*url.URL, bool) {
return nil, false
}

func NewMockClientWithLatest(latest int64) *MockClient {
return &MockClient{latestOverride: latest}
}
Expand Down
5 changes: 5 additions & 0 deletions evmrpc/simulate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"fmt"
"math/big"
"net/url"
"os"
"strings"
"sync"
Expand Down Expand Up @@ -871,6 +872,10 @@ func (c *fixedBlockClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (c *fixedBlockClient) EvmProxy(common.Address) (*url.URL, bool) {
return nil, false
}

func (c *fixedBlockClient) Block(_ context.Context, _ *int64) (*coretypes.ResultBlock, error) {
return c.block, nil
}
Expand Down
5 changes: 5 additions & 0 deletions evmrpc/tests/mock_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -40,6 +41,10 @@ func (c *MockClient) EvmNextPendingNonce(_ common.Address) uint64 {
return 0
}

func (c *MockClient) EvmProxy(common.Address) (*url.URL, bool) {
return nil, false
}

func (c *MockClient) Block(_ context.Context, h *int64) (*coretypes.ResultBlock, error) {
if c.mockedBlockResults != nil {
blockNum := int64(-1)
Expand Down
Loading
Loading