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
5 changes: 4 additions & 1 deletion giga/evmonly/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion giga/evmonly/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 30 additions & 13 deletions giga/evmonly/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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
}
133 changes: 133 additions & 0 deletions giga/evmonly/parser_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
26 changes: 20 additions & 6 deletions giga/evmonly/rpc/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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{
Expand All @@ -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)
}
14 changes: 14 additions & 0 deletions giga/evmonly/rpc/send_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions giga/evmonly/rpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ 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)
EvmBalance(common.Address) uint256.Int
EvmBlockNumber() uint64
EvmChainID() uint64
EvmProxy(common.Address) utils.Option[*ethrpc.Client]
EvmProxyEnabled() bool
EvmTransactionCount(common.Address) uint64
}

Expand Down
6 changes: 6 additions & 0 deletions giga/evmonly/rpc/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
9 changes: 8 additions & 1 deletion giga/evmonly/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading