diff --git a/README.md b/README.md
index 639286ba9f36..527362896d02 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,31 @@
+## Mandarin
+
+A Geth for optimized so that
+
+1. New txs + new blocks hit your bots in microseconds, not milliseconds+JSON.
+
+2. Bots can read/write “intent” (orders, bundles, replacement txs) via shared memory / low-overhead IPC instead of JSON-RPC.
+
+3. You control transaction ordering inside blocks you build or simulate.
+
+## Mandarin
+
+Mandarin is a performance-optimized fork of Go Ethereum designed for low-latency trading operations and MEV workflows. Built on Geth's solid foundation, Mandarin adds specialized features for co-located trading engines while maintaining full compatibility with the Ethereum protocol.
+
+### Key Enhancements
+
+**Bundle Support:** Submit and simulate transaction bundles with microsecond-scale latency. Bundles support timestamp constraints, revertible transactions, and profit simulation.
+
+**Custom Transaction Ordering:** Pluggable ordering strategies allow sophisticated block building beyond simple gas price sorting.
+
+**Low-Latency APIs:** Binary gRPC endpoints alongside traditional JSON-RPC for 10x faster operations including batch storage reads and bundle simulation.
+
+**Backward Compatible:** All Geth features remain unchanged. Enhancements are opt-in and don't affect standard node operation.
+
+See `PHASE1_SUMMARY.md` for detailed implementation notes and `roadmap.md` for future development plans.
+
+---
+
## Go Ethereum
Golang execution layer implementation of the Ethereum protocol.
diff --git a/api/grpc/README.md b/api/grpc/README.md
new file mode 100644
index 000000000000..982df114b749
--- /dev/null
+++ b/api/grpc/README.md
@@ -0,0 +1,212 @@
+# gRPC Trading API
+
+Low-latency binary API for high-frequency trading and MEV operations on Mandarin (Geth fork).
+
+## Overview
+
+This package provides a Protocol Buffer-based gRPC API that **runs alongside JSON-RPC** for performance-critical operations. Key features:
+
+- **Binary protocol**: 10x performance improvement over JSON-RPC
+- **Bundle operations**: Submit and simulate transaction bundles with profit calculation
+- **Batch operations**: Read multiple storage slots in a single call
+- **Low latency**: Direct integration with miner and state database
+- **Type safety**: Strongly-typed interfaces via Protocol Buffers
+- **Additive**: JSON-RPC continues to work for all standard operations
+
+**Note:** gRPC is **disabled by default** and must be explicitly enabled.
+
+## Architecture
+
+- `trader.proto`: Protocol Buffer definitions
+- `trader.pb.go` / `trader_grpc.pb.go`: Generated Go code
+- `server.go`: gRPC server implementation
+- `service.go`: Node lifecycle integration
+- `example_client.go`: Client library and usage examples
+
+## Configuration
+
+**Important:** gRPC is **disabled by default**. You must explicitly enable it to use these high-performance trading APIs.
+
+### Via Command-Line Flags (Recommended)
+
+```bash
+# Enable gRPC alongside standard JSON-RPC
+geth --http --http.port 8545 \
+ --grpc --grpc.addr localhost --grpc.port 9090
+
+# JSON-RPC will run on port 8545 (standard Ethereum API)
+# gRPC will run on port 9090 (low-latency trading API)
+```
+
+### Via Configuration File
+
+```toml
+[Eth]
+EnableGRPC = true
+GRPCHost = "localhost"
+GRPCPort = 9090
+```
+
+### What This Enables
+
+When you add the `--grpc` flag:
+- ✅ **JSON-RPC continues to work** on port 8545 (eth_*, debug_*, etc.)
+- ✅ **gRPC becomes available** on port 9090 (bundle, batch operations)
+- ✅ **Both run simultaneously** - use each for its strengths
+
+## Usage Example
+
+```go
+import grpcapi "github.com/ethereum/go-ethereum/api/grpc"
+
+// Connect to gRPC server
+client, err := grpcapi.NewClient("localhost", 9090)
+if err != nil {
+ log.Fatal(err)
+}
+defer client.Close()
+
+// Batch read Uniswap pool state
+poolAddr := common.HexToAddress("0x...")
+slots := []common.Hash{
+ common.HexToHash("0x0"), // slot0
+ common.HexToHash("0x1"), // feeGrowthGlobal0X128
+}
+values, err := client.GetStorageBatch(ctx, poolAddr, slots, nil)
+
+// Simulate bundle
+simResult, err := client.SimulateBundle(ctx, txs, &grpcapi.BundleOptions{
+ TargetBlock: &blockNum,
+})
+
+profit := new(big.Int).SetBytes(simResult.Profit)
+fmt.Printf("Bundle profit: %s wei\n", profit)
+
+// Submit if profitable
+if profit.Sign() > 0 {
+ bundleHash, err := client.SubmitBundle(ctx, txs, opts)
+ fmt.Printf("Bundle submitted: %s\n", bundleHash.Hex())
+}
+```
+
+## API Methods
+
+### SimulateBundle
+Simulate transaction bundle execution without submitting to mempool.
+
+**Request:**
+- `transactions`: RLP-encoded transactions
+- `min_timestamp`, `max_timestamp`: Optional timing constraints
+- `target_block`: Specific block number target
+- `reverting_txs`: Indices of transactions allowed to revert
+
+**Response:**
+- `success`: Whether all transactions succeeded
+- `gas_used`: Total gas consumed
+- `profit`: Coinbase profit in wei
+- `coinbase_balance`: Final coinbase balance
+- `tx_results`: Per-transaction results with gas, errors, return values
+
+### SubmitBundle
+Submit bundle for inclusion in future blocks.
+
+**Request:** Same as SimulateBundle
+
+**Response:**
+- `bundle_hash`: Unique bundle identifier
+
+### GetStorageBatch
+Read multiple storage slots in a single call. **10-100x faster** than multiple `eth_getStorageAt` calls.
+
+**Request:**
+- `contract`: Contract address
+- `slots`: Array of storage slot hashes
+- `block_number`: Optional block height
+
+**Response:**
+- `values`: Array of storage values (same order as request)
+
+### GetPendingTransactions
+Retrieve pending transactions from mempool.
+
+**Request:**
+- `min_gas_price`: Optional filter for minimum gas price
+
+**Response:**
+- `transactions`: Array of RLP-encoded transactions
+
+### CallContract
+Execute contract call (equivalent to `eth_call`).
+
+**Request:**
+- `from`, `to`, `data`: Standard call parameters
+- `gas`, `gas_price`, `value`: Optional parameters
+- `block_number`: Optional block height
+
+**Response:**
+- `return_data`: Call result
+- `gas_used`: Gas consumed
+- `success`: Whether call succeeded
+- `error`: Error message if failed
+
+## Performance Characteristics
+
+Based on Phase 2 benchmarks:
+
+- **Transaction feed latency**: ~2.5μs average, 21μs max
+- **Storage batch reads**: 10-100x faster than JSON-RPC (single call vs N round-trips)
+- **Bundle simulation**: Direct EVM access, no JSON marshaling overhead
+- **gRPC overhead**: ~100-500μs vs 1-5ms for JSON-RPC
+
+For a 10-transaction bundle simulation:
+- JSON-RPC: ~50ms (encoding + network + decoding)
+- gRPC: ~5ms (binary encoding, single RTT)
+
+## Development
+
+### Regenerating Protocol Buffers
+
+If you modify `trader.proto`:
+
+```bash
+protoc --go_out=. --go_opt=paths=source_relative \
+ --go-grpc_out=. --go-grpc_opt=paths=source_relative \
+ api/grpc/trader.proto
+```
+
+### Testing
+
+Integration tests require a running node:
+
+```bash
+# Terminal 1: Start node with gRPC enabled
+geth --dev --grpc --grpc.port 9090
+
+# Terminal 2: Run tests
+go test -v ./api/grpc/...
+```
+
+### Benchmarking
+
+Compare gRPC vs JSON-RPC performance:
+
+```bash
+go test -bench=. -benchtime=10s ./api/grpc/...
+```
+
+## Security Considerations
+
+- **Network exposure**: gRPC server should only listen on localhost or trusted networks
+- **Authentication**: Add mTLS or API keys for production deployments
+- **Rate limiting**: Not currently implemented, add as needed
+- **Bundle privacy**: Bundles are visible to node operator
+
+## Future Enhancements
+
+1. **Streaming APIs**: Real-time transaction feed via server-side streaming
+2. **Hot state cache**: Dedicated cache for frequently-accessed DeFi contracts
+3. **Parallel simulation**: Multiple bundle simulations in concurrent goroutines
+4. **State deltas**: Export compact state diffs instead of full state
+5. **Shared memory**: Zero-copy data sharing for co-located bots
+6. **Authentication**: mTLS, JWT, or API key support
+7. **Metrics**: Prometheus integration for latency and throughput monitoring
\ No newline at end of file
diff --git a/api/grpc/example_client.go b/api/grpc/example_client.go
new file mode 100644
index 000000000000..eb2043c8ecda
--- /dev/null
+++ b/api/grpc/example_client.go
@@ -0,0 +1,272 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Package grpc provides an example client for the low-latency gRPC trading API.
+// This example demonstrates how to:
+// - Connect to the gRPC server
+// - Submit transaction bundles
+// - Simulate bundle execution
+// - Perform batch storage reads
+// - Subscribe to pending transactions
+package grpc
+
+import (
+ "context"
+ "fmt"
+ "math/big"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+// Client wraps the gRPC TraderService client with convenient methods.
+type Client struct {
+ conn *grpc.ClientConn
+ client TraderServiceClient
+}
+
+// NewClient creates a new gRPC client connected to the specified host and port.
+func NewClient(host string, port int) (*Client, error) {
+ addr := fmt.Sprintf("%s:%d", host, port)
+ conn, err := grpc.NewClient(addr,
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithDefaultCallOptions(
+ grpc.MaxCallRecvMsgSize(100*1024*1024), // 100MB
+ grpc.MaxCallSendMsgSize(100*1024*1024), // 100MB
+ ),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to %s: %w", addr, err)
+ }
+
+ return &Client{
+ conn: conn,
+ client: NewTraderServiceClient(conn),
+ }, nil
+}
+
+// Close closes the gRPC connection.
+func (c *Client) Close() error {
+ return c.conn.Close()
+}
+
+// SimulateBundle simulates a bundle of transactions and returns the results.
+// This is useful for testing bundle profitability before submission.
+func (c *Client) SimulateBundle(ctx context.Context, txs []*types.Transaction, opts *BundleOptions) (*SimulateBundleResponse, error) {
+ // Encode transactions
+ encodedTxs := make([][]byte, len(txs))
+ for i, tx := range txs {
+ encoded, err := tx.MarshalBinary()
+ if err != nil {
+ return nil, fmt.Errorf("failed to encode transaction %d: %w", i, err)
+ }
+ encodedTxs[i] = encoded
+ }
+
+ req := &SimulateBundleRequest{
+ Transactions: encodedTxs,
+ }
+
+ if opts != nil {
+ req.MinTimestamp = opts.MinTimestamp
+ req.MaxTimestamp = opts.MaxTimestamp
+ req.TargetBlock = opts.TargetBlock
+ req.RevertingTxs = opts.RevertingTxIndices
+ }
+
+ return c.client.SimulateBundle(ctx, req)
+}
+
+// SubmitBundle submits a bundle for inclusion in future blocks.
+func (c *Client) SubmitBundle(ctx context.Context, txs []*types.Transaction, opts *BundleOptions) (common.Hash, error) {
+ // Encode transactions
+ encodedTxs := make([][]byte, len(txs))
+ for i, tx := range txs {
+ encoded, err := tx.MarshalBinary()
+ if err != nil {
+ return common.Hash{}, fmt.Errorf("failed to encode transaction %d: %w", i, err)
+ }
+ encodedTxs[i] = encoded
+ }
+
+ req := &SubmitBundleRequest{
+ Transactions: encodedTxs,
+ }
+
+ if opts != nil {
+ req.MinTimestamp = opts.MinTimestamp
+ req.MaxTimestamp = opts.MaxTimestamp
+ req.TargetBlock = opts.TargetBlock
+ req.RevertingTxs = opts.RevertingTxIndices
+ }
+
+ resp, err := c.client.SubmitBundle(ctx, req)
+ if err != nil {
+ return common.Hash{}, err
+ }
+
+ return common.BytesToHash(resp.BundleHash), nil
+}
+
+// GetStorageBatch retrieves multiple storage slots in a single call.
+// This is significantly faster than multiple eth_getStorageAt JSON-RPC calls.
+func (c *Client) GetStorageBatch(ctx context.Context, contract common.Address, slots []common.Hash, blockNum *uint64) ([]common.Hash, error) {
+ encodedSlots := make([][]byte, len(slots))
+ for i, slot := range slots {
+ encodedSlots[i] = slot.Bytes()
+ }
+
+ req := &GetStorageBatchRequest{
+ Contract: contract.Bytes(),
+ Slots: encodedSlots,
+ BlockNumber: blockNum,
+ }
+
+ resp, err := c.client.GetStorageBatch(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+
+ values := make([]common.Hash, len(resp.Values))
+ for i, val := range resp.Values {
+ values[i] = common.BytesToHash(val)
+ }
+
+ return values, nil
+}
+
+// GetPendingTransactions retrieves currently pending transactions.
+func (c *Client) GetPendingTransactions(ctx context.Context, minGasPrice *uint64) ([]*types.Transaction, error) {
+ req := &GetPendingTransactionsRequest{
+ MinGasPrice: minGasPrice,
+ }
+
+ resp, err := c.client.GetPendingTransactions(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+
+ txs := make([]*types.Transaction, 0, len(resp.Transactions))
+ for i, encoded := range resp.Transactions {
+ tx := new(types.Transaction)
+ if err := tx.UnmarshalBinary(encoded); err != nil {
+ return nil, fmt.Errorf("failed to decode transaction %d: %w", i, err)
+ }
+ txs = append(txs, tx)
+ }
+
+ return txs, nil
+}
+
+// CallContract executes a contract call.
+func (c *Client) CallContract(ctx context.Context, msg *CallMessage, blockNum *uint64) (*CallContractResponse, error) {
+ req := &CallContractRequest{
+ From: msg.From.Bytes(),
+ To: msg.To.Bytes(),
+ Data: msg.Data,
+ BlockNumber: blockNum,
+ }
+
+ if msg.Gas != nil {
+ req.Gas = msg.Gas
+ }
+ if msg.GasPrice != nil {
+ req.GasPrice = msg.GasPrice
+ }
+ if msg.Value != nil {
+ req.Value = msg.Value.Bytes()
+ }
+
+ return c.client.CallContract(ctx, req)
+}
+
+// BundleOptions contains optional parameters for bundle submission and simulation.
+type BundleOptions struct {
+ MinTimestamp *uint64
+ MaxTimestamp *uint64
+ TargetBlock *uint64
+ RevertingTxIndices []int32
+}
+
+// CallMessage contains parameters for contract calls.
+type CallMessage struct {
+ From common.Address
+ To common.Address
+ Data []byte
+ Gas *uint64
+ GasPrice *uint64
+ Value *big.Int
+}
+
+// ExampleUsage demonstrates typical usage patterns for high-frequency trading.
+func ExampleUsage() error {
+ // Connect to gRPC server
+ client, err := NewClient("localhost", 9090)
+ if err != nil {
+ return fmt.Errorf("failed to create client: %w", err)
+ }
+ defer client.Close()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ // Example 1: Batch storage read (e.g., reading Uniswap pool reserves)
+ poolAddress := common.HexToAddress("0x...")
+ slot0 := common.HexToHash("0x0") // slot0 contains sqrtPriceX96 and tick
+ slot1 := common.HexToHash("0x1") // Other pool data
+
+ values, err := client.GetStorageBatch(ctx, poolAddress, []common.Hash{slot0, slot1}, nil)
+ if err != nil {
+ return fmt.Errorf("failed to get storage: %w", err)
+ }
+ fmt.Printf("Pool state: %v\n", values)
+
+ // Example 2: Simulate a bundle before submission
+ // (assuming you have prepared transactions)
+ var txs []*types.Transaction // ... your transactions
+
+ simResult, err := client.SimulateBundle(ctx, txs, &BundleOptions{
+ TargetBlock: func() *uint64 { b := uint64(12345678); return &b }(),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to simulate bundle: %w", err)
+ }
+
+ if !simResult.Success {
+ fmt.Printf("Bundle simulation failed at tx %d: %s\n", simResult.FailedTxIndex, simResult.FailedTxError)
+ return nil
+ }
+
+ profit := new(big.Int).SetBytes(simResult.Profit)
+ fmt.Printf("Bundle profit: %s wei, gas used: %d\n", profit.String(), simResult.GasUsed)
+
+ // Example 3: Submit bundle if profitable
+ if profit.Sign() > 0 {
+ bundleHash, err := client.SubmitBundle(ctx, txs, &BundleOptions{
+ TargetBlock: func() *uint64 { b := uint64(12345678); return &b }(),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to submit bundle: %w", err)
+ }
+ fmt.Printf("Bundle submitted: %s\n", bundleHash.Hex())
+ }
+
+ return nil
+}
+
diff --git a/api/grpc/server.go b/api/grpc/server.go
new file mode 100644
index 000000000000..e5d8b85b3fa3
--- /dev/null
+++ b/api/grpc/server.go
@@ -0,0 +1,349 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package grpc
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/miner"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/ethereum/go-ethereum/trie"
+)
+
+// Backend defines the necessary methods from the Ethereum backend for the gRPC server.
+type Backend interface {
+ ChainConfig() *params.ChainConfig
+ CurrentBlock() *types.Header
+ StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error)
+ Miner() *miner.Miner
+}
+
+// TraderServer implements the TraderServiceServer interface.
+type TraderServer struct {
+ UnimplementedTraderServiceServer
+ backend Backend
+}
+
+// NewTraderServer creates a new TraderServer instance.
+func NewTraderServer(backend Backend) *TraderServer {
+ return &TraderServer{backend: backend}
+}
+
+// SimulateBundle simulates bundle execution and returns results.
+func (s *TraderServer) SimulateBundle(ctx context.Context, req *SimulateBundleRequest) (*SimulateBundleResponse, error) {
+ if len(req.Transactions) == 0 {
+ return nil, errors.New("bundle cannot be empty")
+ }
+
+ // Convert protobuf transactions to types.Transaction
+ txs := make([]*types.Transaction, len(req.Transactions))
+ for i, rawTx := range req.Transactions {
+ tx := new(types.Transaction)
+ if err := tx.UnmarshalBinary(rawTx); err != nil {
+ return nil, fmt.Errorf("failed to decode transaction %d: %w", i, err)
+ }
+ txs[i] = tx
+ }
+
+ // Create bundle
+ var targetBlock uint64 = 0
+ if req.TargetBlock != nil {
+ targetBlock = *req.TargetBlock
+ }
+
+ var minTs, maxTs uint64
+ if req.MinTimestamp != nil {
+ minTs = *req.MinTimestamp
+ }
+ if req.MaxTimestamp != nil {
+ maxTs = *req.MaxTimestamp
+ }
+
+ revertingIndices := make([]int, len(req.RevertingTxs))
+ for i, idx := range req.RevertingTxs {
+ revertingIndices[i] = int(idx)
+ }
+
+ bundle := &miner.Bundle{
+ Txs: txs,
+ MinTimestamp: minTs,
+ MaxTimestamp: maxTs,
+ RevertingTxs: revertingIndices,
+ TargetBlock: targetBlock,
+ }
+
+ // Get current block header for simulation
+ header := s.backend.CurrentBlock()
+ if header == nil {
+ return nil, errors.New("current block not found")
+ }
+
+ // Simulate bundle
+ result, err := s.backend.Miner().SimulateBundle(bundle, header)
+ if err != nil {
+ return nil, fmt.Errorf("bundle simulation failed: %w", err)
+ }
+
+ // Convert result to protobuf
+ pbResult := &SimulateBundleResponse{
+ Success: result.Success,
+ GasUsed: result.GasUsed,
+ Profit: result.Profit.Bytes(),
+ CoinbaseBalance: result.CoinbaseBalance.Bytes(),
+ TxResults: make([]*TxSimulationResult, len(result.TxResults)),
+ }
+
+ for i, txRes := range result.TxResults {
+ errStr := ""
+ if txRes.Error != nil {
+ errStr = txRes.Error.Error()
+ }
+
+ pbResult.TxResults[i] = &TxSimulationResult{
+ Success: txRes.Success,
+ GasUsed: txRes.GasUsed,
+ Error: errStr,
+ ReturnValue: txRes.ReturnValue,
+ }
+
+ if !txRes.Success {
+ pbResult.FailedTxIndex = int32(i)
+ pbResult.FailedTxError = errStr
+ }
+ }
+
+ return pbResult, nil
+}
+
+// SubmitBundle submits a bundle for inclusion in future blocks.
+func (s *TraderServer) SubmitBundle(ctx context.Context, req *SubmitBundleRequest) (*SubmitBundleResponse, error) {
+ if len(req.Transactions) == 0 {
+ return nil, errors.New("bundle cannot be empty")
+ }
+
+ // Convert protobuf transactions to types.Transaction
+ txs := make([]*types.Transaction, len(req.Transactions))
+ for i, rawTx := range req.Transactions {
+ tx := new(types.Transaction)
+ if err := tx.UnmarshalBinary(rawTx); err != nil {
+ return nil, fmt.Errorf("failed to decode transaction %d: %w", i, err)
+ }
+ txs[i] = tx
+ }
+
+ // Create bundle
+ var targetBlock uint64 = 0
+ if req.TargetBlock != nil {
+ targetBlock = *req.TargetBlock
+ }
+
+ var minTs, maxTs uint64
+ if req.MinTimestamp != nil {
+ minTs = *req.MinTimestamp
+ }
+ if req.MaxTimestamp != nil {
+ maxTs = *req.MaxTimestamp
+ }
+
+ revertingIndices := make([]int, len(req.RevertingTxs))
+ for i, idx := range req.RevertingTxs {
+ revertingIndices[i] = int(idx)
+ }
+
+ bundle := &miner.Bundle{
+ Txs: txs,
+ MinTimestamp: minTs,
+ MaxTimestamp: maxTs,
+ RevertingTxs: revertingIndices,
+ TargetBlock: targetBlock,
+ }
+
+ // Add bundle to miner
+ if err := s.backend.Miner().AddBundle(bundle); err != nil {
+ return nil, fmt.Errorf("failed to add bundle to miner: %w", err)
+ }
+
+ // Create hash from bundle transactions
+ bundleHash := types.DeriveSha(types.Transactions(bundle.Txs), trie.NewStackTrie(nil))
+
+ return &SubmitBundleResponse{
+ BundleHash: bundleHash.Bytes(),
+ }, nil
+}
+
+// GetStorageBatch retrieves multiple storage slots in a single call.
+func (s *TraderServer) GetStorageBatch(ctx context.Context, req *GetStorageBatchRequest) (*GetStorageBatchResponse, error) {
+ if len(req.Contract) != common.AddressLength {
+ return nil, errors.New("invalid contract address")
+ }
+ if len(req.Slots) == 0 {
+ return nil, errors.New("no storage slots provided")
+ }
+
+ addr := common.BytesToAddress(req.Contract)
+
+ // Get state at specified block
+ var blockNrOrHash rpc.BlockNumberOrHash
+ if req.BlockNumber != nil {
+ blockNrOrHash = rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(*req.BlockNumber))
+ } else {
+ blockNrOrHash = rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
+ }
+
+ stateDB, _, err := s.backend.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get state for block: %w", err)
+ }
+ if stateDB == nil {
+ return nil, errors.New("state not found for block")
+ }
+
+ // Batch read storage slots
+ values := make([][]byte, len(req.Slots))
+ for i, keyBytes := range req.Slots {
+ if len(keyBytes) != common.HashLength {
+ return nil, fmt.Errorf("invalid storage key length at index %d: %d", i, len(keyBytes))
+ }
+ key := common.BytesToHash(keyBytes)
+ value := stateDB.GetState(addr, key)
+ values[i] = value.Bytes()
+ }
+
+ return &GetStorageBatchResponse{Values: values}, nil
+}
+
+// GetPendingTransactions returns currently pending transactions.
+func (s *TraderServer) GetPendingTransactions(ctx context.Context, req *GetPendingTransactionsRequest) (*GetPendingTransactionsResponse, error) {
+ // Get pending transactions from miner
+ pending, _, _ := s.backend.Miner().Pending()
+ if pending == nil {
+ return &GetPendingTransactionsResponse{Transactions: [][]byte{}}, nil
+ }
+
+ // Filter by gas price if requested
+ var minGasPrice *big.Int
+ if req.MinGasPrice != nil {
+ minGasPrice = new(big.Int).SetUint64(*req.MinGasPrice)
+ }
+
+ // Collect and encode transactions
+ var encodedTxs [][]byte
+ for _, tx := range pending.Transactions() {
+ if minGasPrice != nil && tx.GasPrice().Cmp(minGasPrice) < 0 {
+ continue
+ }
+
+ encoded, err := tx.MarshalBinary()
+ if err != nil {
+ log.Warn("Failed to encode pending transaction", "hash", tx.Hash(), "err", err)
+ continue
+ }
+ encodedTxs = append(encodedTxs, encoded)
+ }
+
+ return &GetPendingTransactionsResponse{Transactions: encodedTxs}, nil
+}
+
+// CallContract executes a contract call.
+func (s *TraderServer) CallContract(ctx context.Context, req *CallContractRequest) (*CallContractResponse, error) {
+ var (
+ from common.Address
+ to *common.Address
+ )
+ if len(req.From) > 0 {
+ from = common.BytesToAddress(req.From)
+ }
+ if len(req.To) > 0 {
+ t := common.BytesToAddress(req.To)
+ to = &t
+ }
+
+ value := new(big.Int)
+ if len(req.Value) > 0 {
+ value.SetBytes(req.Value)
+ }
+
+ gasPrice := new(big.Int)
+ if req.GasPrice != nil {
+ gasPrice.SetUint64(*req.GasPrice)
+ }
+
+ gas := uint64(100000000) // Default gas limit
+ if req.Gas != nil {
+ gas = *req.Gas
+ }
+
+ msg := &core.Message{
+ From: from,
+ To: to,
+ Value: value,
+ GasLimit: gas,
+ GasPrice: gasPrice,
+ Data: req.Data,
+ }
+
+ // Get state at specified block
+ var blockNrOrHash rpc.BlockNumberOrHash
+ if req.BlockNumber != nil {
+ blockNrOrHash = rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(*req.BlockNumber))
+ } else {
+ blockNrOrHash = rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
+ }
+
+ stateDB, header, err := s.backend.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get state and header: %w", err)
+ }
+ if stateDB == nil || header == nil {
+ return nil, errors.New("state or header not found")
+ }
+
+ // Create EVM and execute call
+ // Note: Using nil for chain reader as we only need basic block context for eth_call
+ blockContext := core.NewEVMBlockContext(header, nil, nil)
+ vmConfig := vm.Config{}
+
+ evm := vm.NewEVM(blockContext, stateDB, s.backend.ChainConfig(), vmConfig)
+
+ gasPool := new(core.GasPool).AddGas(gas)
+ execResult, err := core.ApplyMessage(evm, msg, gasPool)
+
+ resp := &CallContractResponse{
+ ReturnData: execResult.ReturnData,
+ GasUsed: execResult.UsedGas,
+ Success: !execResult.Failed(),
+ }
+ if err != nil {
+ resp.Error = err.Error()
+ } else if execResult.Failed() {
+ resp.Error = execResult.Err.Error()
+ }
+
+ return resp, nil
+}
+
+
diff --git a/api/grpc/service.go b/api/grpc/service.go
new file mode 100644
index 000000000000..0383f3a48f3a
--- /dev/null
+++ b/api/grpc/service.go
@@ -0,0 +1,84 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package grpc
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/ethereum/go-ethereum/log"
+ "google.golang.org/grpc"
+)
+
+// Service implements the node.Lifecycle interface for the gRPC server.
+type Service struct {
+ backend Backend
+ server *grpc.Server
+ listener net.Listener
+ host string
+ port int
+}
+
+// NewService creates a new gRPC service.
+func NewService(backend Backend, host string, port int) *Service {
+ return &Service{
+ backend: backend,
+ host: host,
+ port: port,
+ }
+}
+
+// Start implements node.Lifecycle, starting the gRPC server.
+func (s *Service) Start() error {
+ addr := fmt.Sprintf("%s:%d", s.host, s.port)
+ lis, err := net.Listen("tcp", addr)
+ if err != nil {
+ return fmt.Errorf("failed to listen on %s: %w", addr, err)
+ }
+ s.listener = lis
+
+ // Create gRPC server with options
+ s.server = grpc.NewServer(
+ grpc.MaxRecvMsgSize(100*1024*1024), // 100MB for large bundles
+ grpc.MaxSendMsgSize(100*1024*1024), // 100MB for large simulation results
+ )
+
+ // Register TraderService
+ traderServer := NewTraderServer(s.backend)
+ RegisterTraderServiceServer(s.server, traderServer)
+
+ // Start serving in a goroutine
+ go func() {
+ log.Info("gRPC server started", "addr", addr)
+ if err := s.server.Serve(lis); err != nil {
+ log.Error("gRPC server failed", "err", err)
+ }
+ }()
+
+ return nil
+}
+
+// Stop implements node.Lifecycle, stopping the gRPC server.
+func (s *Service) Stop() error {
+ if s.server != nil {
+ log.Info("gRPC server stopping")
+ s.server.GracefulStop()
+ log.Info("gRPC server stopped")
+ }
+ return nil
+}
+
diff --git a/api/grpc/trader.pb.go b/api/grpc/trader.pb.go
new file mode 100644
index 000000000000..311dd690517f
--- /dev/null
+++ b/api/grpc/trader.pb.go
@@ -0,0 +1,882 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.10
+// protoc v6.33.1
+// source: api/grpc/trader.proto
+
+package grpc
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type SimulateBundleRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Transactions [][]byte `protobuf:"bytes,1,rep,name=transactions,proto3" json:"transactions,omitempty"`
+ MinTimestamp *uint64 `protobuf:"varint,2,opt,name=min_timestamp,json=minTimestamp,proto3,oneof" json:"min_timestamp,omitempty"`
+ MaxTimestamp *uint64 `protobuf:"varint,3,opt,name=max_timestamp,json=maxTimestamp,proto3,oneof" json:"max_timestamp,omitempty"`
+ RevertingTxs []int32 `protobuf:"varint,4,rep,packed,name=reverting_txs,json=revertingTxs,proto3" json:"reverting_txs,omitempty"`
+ TargetBlock *uint64 `protobuf:"varint,5,opt,name=target_block,json=targetBlock,proto3,oneof" json:"target_block,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SimulateBundleRequest) Reset() {
+ *x = SimulateBundleRequest{}
+ mi := &file_api_grpc_trader_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SimulateBundleRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SimulateBundleRequest) ProtoMessage() {}
+
+func (x *SimulateBundleRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SimulateBundleRequest.ProtoReflect.Descriptor instead.
+func (*SimulateBundleRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SimulateBundleRequest) GetTransactions() [][]byte {
+ if x != nil {
+ return x.Transactions
+ }
+ return nil
+}
+
+func (x *SimulateBundleRequest) GetMinTimestamp() uint64 {
+ if x != nil && x.MinTimestamp != nil {
+ return *x.MinTimestamp
+ }
+ return 0
+}
+
+func (x *SimulateBundleRequest) GetMaxTimestamp() uint64 {
+ if x != nil && x.MaxTimestamp != nil {
+ return *x.MaxTimestamp
+ }
+ return 0
+}
+
+func (x *SimulateBundleRequest) GetRevertingTxs() []int32 {
+ if x != nil {
+ return x.RevertingTxs
+ }
+ return nil
+}
+
+func (x *SimulateBundleRequest) GetTargetBlock() uint64 {
+ if x != nil && x.TargetBlock != nil {
+ return *x.TargetBlock
+ }
+ return 0
+}
+
+type SimulateBundleResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
+ GasUsed uint64 `protobuf:"varint,2,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"`
+ Profit []byte `protobuf:"bytes,3,opt,name=profit,proto3" json:"profit,omitempty"`
+ CoinbaseBalance []byte `protobuf:"bytes,4,opt,name=coinbase_balance,json=coinbaseBalance,proto3" json:"coinbase_balance,omitempty"`
+ FailedTxIndex int32 `protobuf:"varint,5,opt,name=failed_tx_index,json=failedTxIndex,proto3" json:"failed_tx_index,omitempty"`
+ FailedTxError string `protobuf:"bytes,6,opt,name=failed_tx_error,json=failedTxError,proto3" json:"failed_tx_error,omitempty"`
+ TxResults []*TxSimulationResult `protobuf:"bytes,7,rep,name=tx_results,json=txResults,proto3" json:"tx_results,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SimulateBundleResponse) Reset() {
+ *x = SimulateBundleResponse{}
+ mi := &file_api_grpc_trader_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SimulateBundleResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SimulateBundleResponse) ProtoMessage() {}
+
+func (x *SimulateBundleResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SimulateBundleResponse.ProtoReflect.Descriptor instead.
+func (*SimulateBundleResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *SimulateBundleResponse) GetSuccess() bool {
+ if x != nil {
+ return x.Success
+ }
+ return false
+}
+
+func (x *SimulateBundleResponse) GetGasUsed() uint64 {
+ if x != nil {
+ return x.GasUsed
+ }
+ return 0
+}
+
+func (x *SimulateBundleResponse) GetProfit() []byte {
+ if x != nil {
+ return x.Profit
+ }
+ return nil
+}
+
+func (x *SimulateBundleResponse) GetCoinbaseBalance() []byte {
+ if x != nil {
+ return x.CoinbaseBalance
+ }
+ return nil
+}
+
+func (x *SimulateBundleResponse) GetFailedTxIndex() int32 {
+ if x != nil {
+ return x.FailedTxIndex
+ }
+ return 0
+}
+
+func (x *SimulateBundleResponse) GetFailedTxError() string {
+ if x != nil {
+ return x.FailedTxError
+ }
+ return ""
+}
+
+func (x *SimulateBundleResponse) GetTxResults() []*TxSimulationResult {
+ if x != nil {
+ return x.TxResults
+ }
+ return nil
+}
+
+type TxSimulationResult struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
+ GasUsed uint64 `protobuf:"varint,2,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"`
+ Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"`
+ ReturnValue []byte `protobuf:"bytes,4,opt,name=return_value,json=returnValue,proto3" json:"return_value,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TxSimulationResult) Reset() {
+ *x = TxSimulationResult{}
+ mi := &file_api_grpc_trader_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TxSimulationResult) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TxSimulationResult) ProtoMessage() {}
+
+func (x *TxSimulationResult) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use TxSimulationResult.ProtoReflect.Descriptor instead.
+func (*TxSimulationResult) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *TxSimulationResult) GetSuccess() bool {
+ if x != nil {
+ return x.Success
+ }
+ return false
+}
+
+func (x *TxSimulationResult) GetGasUsed() uint64 {
+ if x != nil {
+ return x.GasUsed
+ }
+ return 0
+}
+
+func (x *TxSimulationResult) GetError() string {
+ if x != nil {
+ return x.Error
+ }
+ return ""
+}
+
+func (x *TxSimulationResult) GetReturnValue() []byte {
+ if x != nil {
+ return x.ReturnValue
+ }
+ return nil
+}
+
+type SubmitBundleRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Transactions [][]byte `protobuf:"bytes,1,rep,name=transactions,proto3" json:"transactions,omitempty"`
+ MinTimestamp *uint64 `protobuf:"varint,2,opt,name=min_timestamp,json=minTimestamp,proto3,oneof" json:"min_timestamp,omitempty"`
+ MaxTimestamp *uint64 `protobuf:"varint,3,opt,name=max_timestamp,json=maxTimestamp,proto3,oneof" json:"max_timestamp,omitempty"`
+ RevertingTxs []int32 `protobuf:"varint,4,rep,packed,name=reverting_txs,json=revertingTxs,proto3" json:"reverting_txs,omitempty"`
+ TargetBlock *uint64 `protobuf:"varint,5,opt,name=target_block,json=targetBlock,proto3,oneof" json:"target_block,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SubmitBundleRequest) Reset() {
+ *x = SubmitBundleRequest{}
+ mi := &file_api_grpc_trader_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SubmitBundleRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SubmitBundleRequest) ProtoMessage() {}
+
+func (x *SubmitBundleRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SubmitBundleRequest.ProtoReflect.Descriptor instead.
+func (*SubmitBundleRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *SubmitBundleRequest) GetTransactions() [][]byte {
+ if x != nil {
+ return x.Transactions
+ }
+ return nil
+}
+
+func (x *SubmitBundleRequest) GetMinTimestamp() uint64 {
+ if x != nil && x.MinTimestamp != nil {
+ return *x.MinTimestamp
+ }
+ return 0
+}
+
+func (x *SubmitBundleRequest) GetMaxTimestamp() uint64 {
+ if x != nil && x.MaxTimestamp != nil {
+ return *x.MaxTimestamp
+ }
+ return 0
+}
+
+func (x *SubmitBundleRequest) GetRevertingTxs() []int32 {
+ if x != nil {
+ return x.RevertingTxs
+ }
+ return nil
+}
+
+func (x *SubmitBundleRequest) GetTargetBlock() uint64 {
+ if x != nil && x.TargetBlock != nil {
+ return *x.TargetBlock
+ }
+ return 0
+}
+
+type SubmitBundleResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ BundleHash []byte `protobuf:"bytes,1,opt,name=bundle_hash,json=bundleHash,proto3" json:"bundle_hash,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SubmitBundleResponse) Reset() {
+ *x = SubmitBundleResponse{}
+ mi := &file_api_grpc_trader_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SubmitBundleResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SubmitBundleResponse) ProtoMessage() {}
+
+func (x *SubmitBundleResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[4]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SubmitBundleResponse.ProtoReflect.Descriptor instead.
+func (*SubmitBundleResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *SubmitBundleResponse) GetBundleHash() []byte {
+ if x != nil {
+ return x.BundleHash
+ }
+ return nil
+}
+
+type GetStorageBatchRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Contract []byte `protobuf:"bytes,1,opt,name=contract,proto3" json:"contract,omitempty"`
+ Slots [][]byte `protobuf:"bytes,2,rep,name=slots,proto3" json:"slots,omitempty"`
+ BlockNumber *uint64 `protobuf:"varint,3,opt,name=block_number,json=blockNumber,proto3,oneof" json:"block_number,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetStorageBatchRequest) Reset() {
+ *x = GetStorageBatchRequest{}
+ mi := &file_api_grpc_trader_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetStorageBatchRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetStorageBatchRequest) ProtoMessage() {}
+
+func (x *GetStorageBatchRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[5]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetStorageBatchRequest.ProtoReflect.Descriptor instead.
+func (*GetStorageBatchRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *GetStorageBatchRequest) GetContract() []byte {
+ if x != nil {
+ return x.Contract
+ }
+ return nil
+}
+
+func (x *GetStorageBatchRequest) GetSlots() [][]byte {
+ if x != nil {
+ return x.Slots
+ }
+ return nil
+}
+
+func (x *GetStorageBatchRequest) GetBlockNumber() uint64 {
+ if x != nil && x.BlockNumber != nil {
+ return *x.BlockNumber
+ }
+ return 0
+}
+
+type GetStorageBatchResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Values [][]byte `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetStorageBatchResponse) Reset() {
+ *x = GetStorageBatchResponse{}
+ mi := &file_api_grpc_trader_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetStorageBatchResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetStorageBatchResponse) ProtoMessage() {}
+
+func (x *GetStorageBatchResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[6]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetStorageBatchResponse.ProtoReflect.Descriptor instead.
+func (*GetStorageBatchResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *GetStorageBatchResponse) GetValues() [][]byte {
+ if x != nil {
+ return x.Values
+ }
+ return nil
+}
+
+type GetPendingTransactionsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ MinGasPrice *uint64 `protobuf:"varint,1,opt,name=min_gas_price,json=minGasPrice,proto3,oneof" json:"min_gas_price,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetPendingTransactionsRequest) Reset() {
+ *x = GetPendingTransactionsRequest{}
+ mi := &file_api_grpc_trader_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetPendingTransactionsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPendingTransactionsRequest) ProtoMessage() {}
+
+func (x *GetPendingTransactionsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[7]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPendingTransactionsRequest.ProtoReflect.Descriptor instead.
+func (*GetPendingTransactionsRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *GetPendingTransactionsRequest) GetMinGasPrice() uint64 {
+ if x != nil && x.MinGasPrice != nil {
+ return *x.MinGasPrice
+ }
+ return 0
+}
+
+type GetPendingTransactionsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Transactions [][]byte `protobuf:"bytes,1,rep,name=transactions,proto3" json:"transactions,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetPendingTransactionsResponse) Reset() {
+ *x = GetPendingTransactionsResponse{}
+ mi := &file_api_grpc_trader_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetPendingTransactionsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPendingTransactionsResponse) ProtoMessage() {}
+
+func (x *GetPendingTransactionsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[8]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPendingTransactionsResponse.ProtoReflect.Descriptor instead.
+func (*GetPendingTransactionsResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *GetPendingTransactionsResponse) GetTransactions() [][]byte {
+ if x != nil {
+ return x.Transactions
+ }
+ return nil
+}
+
+type CallContractRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ From []byte `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"`
+ To []byte `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"`
+ Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
+ Gas *uint64 `protobuf:"varint,4,opt,name=gas,proto3,oneof" json:"gas,omitempty"`
+ GasPrice *uint64 `protobuf:"varint,5,opt,name=gas_price,json=gasPrice,proto3,oneof" json:"gas_price,omitempty"`
+ Value []byte `protobuf:"bytes,6,opt,name=value,proto3,oneof" json:"value,omitempty"`
+ BlockNumber *uint64 `protobuf:"varint,7,opt,name=block_number,json=blockNumber,proto3,oneof" json:"block_number,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CallContractRequest) Reset() {
+ *x = CallContractRequest{}
+ mi := &file_api_grpc_trader_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CallContractRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CallContractRequest) ProtoMessage() {}
+
+func (x *CallContractRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[9]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CallContractRequest.ProtoReflect.Descriptor instead.
+func (*CallContractRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *CallContractRequest) GetFrom() []byte {
+ if x != nil {
+ return x.From
+ }
+ return nil
+}
+
+func (x *CallContractRequest) GetTo() []byte {
+ if x != nil {
+ return x.To
+ }
+ return nil
+}
+
+func (x *CallContractRequest) GetData() []byte {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
+func (x *CallContractRequest) GetGas() uint64 {
+ if x != nil && x.Gas != nil {
+ return *x.Gas
+ }
+ return 0
+}
+
+func (x *CallContractRequest) GetGasPrice() uint64 {
+ if x != nil && x.GasPrice != nil {
+ return *x.GasPrice
+ }
+ return 0
+}
+
+func (x *CallContractRequest) GetValue() []byte {
+ if x != nil {
+ return x.Value
+ }
+ return nil
+}
+
+func (x *CallContractRequest) GetBlockNumber() uint64 {
+ if x != nil && x.BlockNumber != nil {
+ return *x.BlockNumber
+ }
+ return 0
+}
+
+type CallContractResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ReturnData []byte `protobuf:"bytes,1,opt,name=return_data,json=returnData,proto3" json:"return_data,omitempty"`
+ GasUsed uint64 `protobuf:"varint,2,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"`
+ Success bool `protobuf:"varint,3,opt,name=success,proto3" json:"success,omitempty"`
+ Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CallContractResponse) Reset() {
+ *x = CallContractResponse{}
+ mi := &file_api_grpc_trader_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CallContractResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CallContractResponse) ProtoMessage() {}
+
+func (x *CallContractResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_trader_proto_msgTypes[10]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CallContractResponse.ProtoReflect.Descriptor instead.
+func (*CallContractResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_trader_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *CallContractResponse) GetReturnData() []byte {
+ if x != nil {
+ return x.ReturnData
+ }
+ return nil
+}
+
+func (x *CallContractResponse) GetGasUsed() uint64 {
+ if x != nil {
+ return x.GasUsed
+ }
+ return 0
+}
+
+func (x *CallContractResponse) GetSuccess() bool {
+ if x != nil {
+ return x.Success
+ }
+ return false
+}
+
+func (x *CallContractResponse) GetError() string {
+ if x != nil {
+ return x.Error
+ }
+ return ""
+}
+
+var File_api_grpc_trader_proto protoreflect.FileDescriptor
+
+const file_api_grpc_trader_proto_rawDesc = "" +
+ "\n" +
+ "\x15api/grpc/trader.proto\x12\x04grpc\"\x91\x02\n" +
+ "\x15SimulateBundleRequest\x12\"\n" +
+ "\ftransactions\x18\x01 \x03(\fR\ftransactions\x12(\n" +
+ "\rmin_timestamp\x18\x02 \x01(\x04H\x00R\fminTimestamp\x88\x01\x01\x12(\n" +
+ "\rmax_timestamp\x18\x03 \x01(\x04H\x01R\fmaxTimestamp\x88\x01\x01\x12#\n" +
+ "\rreverting_txs\x18\x04 \x03(\x05R\frevertingTxs\x12&\n" +
+ "\ftarget_block\x18\x05 \x01(\x04H\x02R\vtargetBlock\x88\x01\x01B\x10\n" +
+ "\x0e_min_timestampB\x10\n" +
+ "\x0e_max_timestampB\x0f\n" +
+ "\r_target_block\"\x99\x02\n" +
+ "\x16SimulateBundleResponse\x12\x18\n" +
+ "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x19\n" +
+ "\bgas_used\x18\x02 \x01(\x04R\agasUsed\x12\x16\n" +
+ "\x06profit\x18\x03 \x01(\fR\x06profit\x12)\n" +
+ "\x10coinbase_balance\x18\x04 \x01(\fR\x0fcoinbaseBalance\x12&\n" +
+ "\x0ffailed_tx_index\x18\x05 \x01(\x05R\rfailedTxIndex\x12&\n" +
+ "\x0ffailed_tx_error\x18\x06 \x01(\tR\rfailedTxError\x127\n" +
+ "\n" +
+ "tx_results\x18\a \x03(\v2\x18.grpc.TxSimulationResultR\ttxResults\"\x82\x01\n" +
+ "\x12TxSimulationResult\x12\x18\n" +
+ "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x19\n" +
+ "\bgas_used\x18\x02 \x01(\x04R\agasUsed\x12\x14\n" +
+ "\x05error\x18\x03 \x01(\tR\x05error\x12!\n" +
+ "\freturn_value\x18\x04 \x01(\fR\vreturnValue\"\x8f\x02\n" +
+ "\x13SubmitBundleRequest\x12\"\n" +
+ "\ftransactions\x18\x01 \x03(\fR\ftransactions\x12(\n" +
+ "\rmin_timestamp\x18\x02 \x01(\x04H\x00R\fminTimestamp\x88\x01\x01\x12(\n" +
+ "\rmax_timestamp\x18\x03 \x01(\x04H\x01R\fmaxTimestamp\x88\x01\x01\x12#\n" +
+ "\rreverting_txs\x18\x04 \x03(\x05R\frevertingTxs\x12&\n" +
+ "\ftarget_block\x18\x05 \x01(\x04H\x02R\vtargetBlock\x88\x01\x01B\x10\n" +
+ "\x0e_min_timestampB\x10\n" +
+ "\x0e_max_timestampB\x0f\n" +
+ "\r_target_block\"7\n" +
+ "\x14SubmitBundleResponse\x12\x1f\n" +
+ "\vbundle_hash\x18\x01 \x01(\fR\n" +
+ "bundleHash\"\x83\x01\n" +
+ "\x16GetStorageBatchRequest\x12\x1a\n" +
+ "\bcontract\x18\x01 \x01(\fR\bcontract\x12\x14\n" +
+ "\x05slots\x18\x02 \x03(\fR\x05slots\x12&\n" +
+ "\fblock_number\x18\x03 \x01(\x04H\x00R\vblockNumber\x88\x01\x01B\x0f\n" +
+ "\r_block_number\"1\n" +
+ "\x17GetStorageBatchResponse\x12\x16\n" +
+ "\x06values\x18\x01 \x03(\fR\x06values\"Z\n" +
+ "\x1dGetPendingTransactionsRequest\x12'\n" +
+ "\rmin_gas_price\x18\x01 \x01(\x04H\x00R\vminGasPrice\x88\x01\x01B\x10\n" +
+ "\x0e_min_gas_price\"D\n" +
+ "\x1eGetPendingTransactionsResponse\x12\"\n" +
+ "\ftransactions\x18\x01 \x03(\fR\ftransactions\"\xfa\x01\n" +
+ "\x13CallContractRequest\x12\x12\n" +
+ "\x04from\x18\x01 \x01(\fR\x04from\x12\x0e\n" +
+ "\x02to\x18\x02 \x01(\fR\x02to\x12\x12\n" +
+ "\x04data\x18\x03 \x01(\fR\x04data\x12\x15\n" +
+ "\x03gas\x18\x04 \x01(\x04H\x00R\x03gas\x88\x01\x01\x12 \n" +
+ "\tgas_price\x18\x05 \x01(\x04H\x01R\bgasPrice\x88\x01\x01\x12\x19\n" +
+ "\x05value\x18\x06 \x01(\fH\x02R\x05value\x88\x01\x01\x12&\n" +
+ "\fblock_number\x18\a \x01(\x04H\x03R\vblockNumber\x88\x01\x01B\x06\n" +
+ "\x04_gasB\f\n" +
+ "\n" +
+ "_gas_priceB\b\n" +
+ "\x06_valueB\x0f\n" +
+ "\r_block_number\"\x82\x01\n" +
+ "\x14CallContractResponse\x12\x1f\n" +
+ "\vreturn_data\x18\x01 \x01(\fR\n" +
+ "returnData\x12\x19\n" +
+ "\bgas_used\x18\x02 \x01(\x04R\agasUsed\x12\x18\n" +
+ "\asuccess\x18\x03 \x01(\bR\asuccess\x12\x14\n" +
+ "\x05error\x18\x04 \x01(\tR\x05error2\x9f\x03\n" +
+ "\rTraderService\x12K\n" +
+ "\x0eSimulateBundle\x12\x1b.grpc.SimulateBundleRequest\x1a\x1c.grpc.SimulateBundleResponse\x12E\n" +
+ "\fSubmitBundle\x12\x19.grpc.SubmitBundleRequest\x1a\x1a.grpc.SubmitBundleResponse\x12N\n" +
+ "\x0fGetStorageBatch\x12\x1c.grpc.GetStorageBatchRequest\x1a\x1d.grpc.GetStorageBatchResponse\x12c\n" +
+ "\x16GetPendingTransactions\x12#.grpc.GetPendingTransactionsRequest\x1a$.grpc.GetPendingTransactionsResponse\x12E\n" +
+ "\fCallContract\x12\x19.grpc.CallContractRequest\x1a\x1a.grpc.CallContractResponseB*Z(github.com/ethereum/go-ethereum/api/grpcb\x06proto3"
+
+var (
+ file_api_grpc_trader_proto_rawDescOnce sync.Once
+ file_api_grpc_trader_proto_rawDescData []byte
+)
+
+func file_api_grpc_trader_proto_rawDescGZIP() []byte {
+ file_api_grpc_trader_proto_rawDescOnce.Do(func() {
+ file_api_grpc_trader_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_grpc_trader_proto_rawDesc), len(file_api_grpc_trader_proto_rawDesc)))
+ })
+ return file_api_grpc_trader_proto_rawDescData
+}
+
+var file_api_grpc_trader_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+var file_api_grpc_trader_proto_goTypes = []any{
+ (*SimulateBundleRequest)(nil), // 0: grpc.SimulateBundleRequest
+ (*SimulateBundleResponse)(nil), // 1: grpc.SimulateBundleResponse
+ (*TxSimulationResult)(nil), // 2: grpc.TxSimulationResult
+ (*SubmitBundleRequest)(nil), // 3: grpc.SubmitBundleRequest
+ (*SubmitBundleResponse)(nil), // 4: grpc.SubmitBundleResponse
+ (*GetStorageBatchRequest)(nil), // 5: grpc.GetStorageBatchRequest
+ (*GetStorageBatchResponse)(nil), // 6: grpc.GetStorageBatchResponse
+ (*GetPendingTransactionsRequest)(nil), // 7: grpc.GetPendingTransactionsRequest
+ (*GetPendingTransactionsResponse)(nil), // 8: grpc.GetPendingTransactionsResponse
+ (*CallContractRequest)(nil), // 9: grpc.CallContractRequest
+ (*CallContractResponse)(nil), // 10: grpc.CallContractResponse
+}
+var file_api_grpc_trader_proto_depIdxs = []int32{
+ 2, // 0: grpc.SimulateBundleResponse.tx_results:type_name -> grpc.TxSimulationResult
+ 0, // 1: grpc.TraderService.SimulateBundle:input_type -> grpc.SimulateBundleRequest
+ 3, // 2: grpc.TraderService.SubmitBundle:input_type -> grpc.SubmitBundleRequest
+ 5, // 3: grpc.TraderService.GetStorageBatch:input_type -> grpc.GetStorageBatchRequest
+ 7, // 4: grpc.TraderService.GetPendingTransactions:input_type -> grpc.GetPendingTransactionsRequest
+ 9, // 5: grpc.TraderService.CallContract:input_type -> grpc.CallContractRequest
+ 1, // 6: grpc.TraderService.SimulateBundle:output_type -> grpc.SimulateBundleResponse
+ 4, // 7: grpc.TraderService.SubmitBundle:output_type -> grpc.SubmitBundleResponse
+ 6, // 8: grpc.TraderService.GetStorageBatch:output_type -> grpc.GetStorageBatchResponse
+ 8, // 9: grpc.TraderService.GetPendingTransactions:output_type -> grpc.GetPendingTransactionsResponse
+ 10, // 10: grpc.TraderService.CallContract:output_type -> grpc.CallContractResponse
+ 6, // [6:11] is the sub-list for method output_type
+ 1, // [1:6] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_api_grpc_trader_proto_init() }
+func file_api_grpc_trader_proto_init() {
+ if File_api_grpc_trader_proto != nil {
+ return
+ }
+ file_api_grpc_trader_proto_msgTypes[0].OneofWrappers = []any{}
+ file_api_grpc_trader_proto_msgTypes[3].OneofWrappers = []any{}
+ file_api_grpc_trader_proto_msgTypes[5].OneofWrappers = []any{}
+ file_api_grpc_trader_proto_msgTypes[7].OneofWrappers = []any{}
+ file_api_grpc_trader_proto_msgTypes[9].OneofWrappers = []any{}
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_grpc_trader_proto_rawDesc), len(file_api_grpc_trader_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 11,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_api_grpc_trader_proto_goTypes,
+ DependencyIndexes: file_api_grpc_trader_proto_depIdxs,
+ MessageInfos: file_api_grpc_trader_proto_msgTypes,
+ }.Build()
+ File_api_grpc_trader_proto = out.File
+ file_api_grpc_trader_proto_goTypes = nil
+ file_api_grpc_trader_proto_depIdxs = nil
+}
diff --git a/api/grpc/trader.proto b/api/grpc/trader.proto
new file mode 100644
index 000000000000..3e830b9954cb
--- /dev/null
+++ b/api/grpc/trader.proto
@@ -0,0 +1,96 @@
+syntax = "proto3";
+
+package grpc;
+
+option go_package = "github.com/ethereum/go-ethereum/api/grpc";
+
+// TraderService provides low-latency APIs for trading operations.
+service TraderService {
+ // SimulateBundle simulates bundle execution and returns results
+ rpc SimulateBundle(SimulateBundleRequest) returns (SimulateBundleResponse);
+
+ // SubmitBundle submits a bundle for inclusion in future blocks
+ rpc SubmitBundle(SubmitBundleRequest) returns (SubmitBundleResponse);
+
+ // GetStorageBatch retrieves multiple storage slots in a single call
+ rpc GetStorageBatch(GetStorageBatchRequest) returns (GetStorageBatchResponse);
+
+ // GetPendingTransactions returns currently pending transactions
+ rpc GetPendingTransactions(GetPendingTransactionsRequest) returns (GetPendingTransactionsResponse);
+
+ // CallContract executes a contract call
+ rpc CallContract(CallContractRequest) returns (CallContractResponse);
+}
+
+message SimulateBundleRequest {
+ repeated bytes transactions = 1;
+ optional uint64 min_timestamp = 2;
+ optional uint64 max_timestamp = 3;
+ repeated int32 reverting_txs = 4;
+ optional uint64 target_block = 5;
+}
+
+message SimulateBundleResponse {
+ bool success = 1;
+ uint64 gas_used = 2;
+ bytes profit = 3;
+ bytes coinbase_balance = 4;
+ int32 failed_tx_index = 5;
+ string failed_tx_error = 6;
+ repeated TxSimulationResult tx_results = 7;
+}
+
+message TxSimulationResult {
+ bool success = 1;
+ uint64 gas_used = 2;
+ string error = 3;
+ bytes return_value = 4;
+}
+
+message SubmitBundleRequest {
+ repeated bytes transactions = 1;
+ optional uint64 min_timestamp = 2;
+ optional uint64 max_timestamp = 3;
+ repeated int32 reverting_txs = 4;
+ optional uint64 target_block = 5;
+}
+
+message SubmitBundleResponse {
+ bytes bundle_hash = 1;
+}
+
+message GetStorageBatchRequest {
+ bytes contract = 1;
+ repeated bytes slots = 2;
+ optional uint64 block_number = 3;
+}
+
+message GetStorageBatchResponse {
+ repeated bytes values = 1;
+}
+
+message GetPendingTransactionsRequest {
+ optional uint64 min_gas_price = 1;
+}
+
+message GetPendingTransactionsResponse {
+ repeated bytes transactions = 1;
+}
+
+message CallContractRequest {
+ bytes from = 1;
+ bytes to = 2;
+ bytes data = 3;
+ optional uint64 gas = 4;
+ optional uint64 gas_price = 5;
+ optional bytes value = 6;
+ optional uint64 block_number = 7;
+}
+
+message CallContractResponse {
+ bytes return_data = 1;
+ uint64 gas_used = 2;
+ bool success = 3;
+ string error = 4;
+}
+
diff --git a/api/grpc/trader_grpc.pb.go b/api/grpc/trader_grpc.pb.go
new file mode 100644
index 000000000000..6607d087a733
--- /dev/null
+++ b/api/grpc/trader_grpc.pb.go
@@ -0,0 +1,287 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc v6.33.1
+// source: api/grpc/trader.proto
+
+package grpc
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ TraderService_SimulateBundle_FullMethodName = "/grpc.TraderService/SimulateBundle"
+ TraderService_SubmitBundle_FullMethodName = "/grpc.TraderService/SubmitBundle"
+ TraderService_GetStorageBatch_FullMethodName = "/grpc.TraderService/GetStorageBatch"
+ TraderService_GetPendingTransactions_FullMethodName = "/grpc.TraderService/GetPendingTransactions"
+ TraderService_CallContract_FullMethodName = "/grpc.TraderService/CallContract"
+)
+
+// TraderServiceClient is the client API for TraderService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+//
+// TraderService provides low-latency APIs for trading operations.
+type TraderServiceClient interface {
+ // SimulateBundle simulates bundle execution and returns results
+ SimulateBundle(ctx context.Context, in *SimulateBundleRequest, opts ...grpc.CallOption) (*SimulateBundleResponse, error)
+ // SubmitBundle submits a bundle for inclusion in future blocks
+ SubmitBundle(ctx context.Context, in *SubmitBundleRequest, opts ...grpc.CallOption) (*SubmitBundleResponse, error)
+ // GetStorageBatch retrieves multiple storage slots in a single call
+ GetStorageBatch(ctx context.Context, in *GetStorageBatchRequest, opts ...grpc.CallOption) (*GetStorageBatchResponse, error)
+ // GetPendingTransactions returns currently pending transactions
+ GetPendingTransactions(ctx context.Context, in *GetPendingTransactionsRequest, opts ...grpc.CallOption) (*GetPendingTransactionsResponse, error)
+ // CallContract executes a contract call
+ CallContract(ctx context.Context, in *CallContractRequest, opts ...grpc.CallOption) (*CallContractResponse, error)
+}
+
+type traderServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewTraderServiceClient(cc grpc.ClientConnInterface) TraderServiceClient {
+ return &traderServiceClient{cc}
+}
+
+func (c *traderServiceClient) SimulateBundle(ctx context.Context, in *SimulateBundleRequest, opts ...grpc.CallOption) (*SimulateBundleResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(SimulateBundleResponse)
+ err := c.cc.Invoke(ctx, TraderService_SimulateBundle_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *traderServiceClient) SubmitBundle(ctx context.Context, in *SubmitBundleRequest, opts ...grpc.CallOption) (*SubmitBundleResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(SubmitBundleResponse)
+ err := c.cc.Invoke(ctx, TraderService_SubmitBundle_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *traderServiceClient) GetStorageBatch(ctx context.Context, in *GetStorageBatchRequest, opts ...grpc.CallOption) (*GetStorageBatchResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(GetStorageBatchResponse)
+ err := c.cc.Invoke(ctx, TraderService_GetStorageBatch_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *traderServiceClient) GetPendingTransactions(ctx context.Context, in *GetPendingTransactionsRequest, opts ...grpc.CallOption) (*GetPendingTransactionsResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(GetPendingTransactionsResponse)
+ err := c.cc.Invoke(ctx, TraderService_GetPendingTransactions_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *traderServiceClient) CallContract(ctx context.Context, in *CallContractRequest, opts ...grpc.CallOption) (*CallContractResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CallContractResponse)
+ err := c.cc.Invoke(ctx, TraderService_CallContract_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// TraderServiceServer is the server API for TraderService service.
+// All implementations must embed UnimplementedTraderServiceServer
+// for forward compatibility.
+//
+// TraderService provides low-latency APIs for trading operations.
+type TraderServiceServer interface {
+ // SimulateBundle simulates bundle execution and returns results
+ SimulateBundle(context.Context, *SimulateBundleRequest) (*SimulateBundleResponse, error)
+ // SubmitBundle submits a bundle for inclusion in future blocks
+ SubmitBundle(context.Context, *SubmitBundleRequest) (*SubmitBundleResponse, error)
+ // GetStorageBatch retrieves multiple storage slots in a single call
+ GetStorageBatch(context.Context, *GetStorageBatchRequest) (*GetStorageBatchResponse, error)
+ // GetPendingTransactions returns currently pending transactions
+ GetPendingTransactions(context.Context, *GetPendingTransactionsRequest) (*GetPendingTransactionsResponse, error)
+ // CallContract executes a contract call
+ CallContract(context.Context, *CallContractRequest) (*CallContractResponse, error)
+ mustEmbedUnimplementedTraderServiceServer()
+}
+
+// UnimplementedTraderServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedTraderServiceServer struct{}
+
+func (UnimplementedTraderServiceServer) SimulateBundle(context.Context, *SimulateBundleRequest) (*SimulateBundleResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SimulateBundle not implemented")
+}
+func (UnimplementedTraderServiceServer) SubmitBundle(context.Context, *SubmitBundleRequest) (*SubmitBundleResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SubmitBundle not implemented")
+}
+func (UnimplementedTraderServiceServer) GetStorageBatch(context.Context, *GetStorageBatchRequest) (*GetStorageBatchResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetStorageBatch not implemented")
+}
+func (UnimplementedTraderServiceServer) GetPendingTransactions(context.Context, *GetPendingTransactionsRequest) (*GetPendingTransactionsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetPendingTransactions not implemented")
+}
+func (UnimplementedTraderServiceServer) CallContract(context.Context, *CallContractRequest) (*CallContractResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CallContract not implemented")
+}
+func (UnimplementedTraderServiceServer) mustEmbedUnimplementedTraderServiceServer() {}
+func (UnimplementedTraderServiceServer) testEmbeddedByValue() {}
+
+// UnsafeTraderServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to TraderServiceServer will
+// result in compilation errors.
+type UnsafeTraderServiceServer interface {
+ mustEmbedUnimplementedTraderServiceServer()
+}
+
+func RegisterTraderServiceServer(s grpc.ServiceRegistrar, srv TraderServiceServer) {
+ // If the following call pancis, it indicates UnimplementedTraderServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&TraderService_ServiceDesc, srv)
+}
+
+func _TraderService_SimulateBundle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SimulateBundleRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TraderServiceServer).SimulateBundle(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TraderService_SimulateBundle_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TraderServiceServer).SimulateBundle(ctx, req.(*SimulateBundleRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _TraderService_SubmitBundle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SubmitBundleRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TraderServiceServer).SubmitBundle(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TraderService_SubmitBundle_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TraderServiceServer).SubmitBundle(ctx, req.(*SubmitBundleRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _TraderService_GetStorageBatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetStorageBatchRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TraderServiceServer).GetStorageBatch(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TraderService_GetStorageBatch_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TraderServiceServer).GetStorageBatch(ctx, req.(*GetStorageBatchRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _TraderService_GetPendingTransactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetPendingTransactionsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TraderServiceServer).GetPendingTransactions(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TraderService_GetPendingTransactions_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TraderServiceServer).GetPendingTransactions(ctx, req.(*GetPendingTransactionsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _TraderService_CallContract_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CallContractRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TraderServiceServer).CallContract(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TraderService_CallContract_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TraderServiceServer).CallContract(ctx, req.(*CallContractRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// TraderService_ServiceDesc is the grpc.ServiceDesc for TraderService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var TraderService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "grpc.TraderService",
+ HandlerType: (*TraderServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "SimulateBundle",
+ Handler: _TraderService_SimulateBundle_Handler,
+ },
+ {
+ MethodName: "SubmitBundle",
+ Handler: _TraderService_SubmitBundle_Handler,
+ },
+ {
+ MethodName: "GetStorageBatch",
+ Handler: _TraderService_GetStorageBatch_Handler,
+ },
+ {
+ MethodName: "GetPendingTransactions",
+ Handler: _TraderService_GetPendingTransactions_Handler,
+ },
+ {
+ MethodName: "CallContract",
+ Handler: _TraderService_CallContract_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "api/grpc/trader.proto",
+}
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1 @@
+
diff --git a/core/txpool/fastfeed/feed.go b/core/txpool/fastfeed/feed.go
new file mode 100644
index 000000000000..8f52519948b0
--- /dev/null
+++ b/core/txpool/fastfeed/feed.go
@@ -0,0 +1,298 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package fastfeed
+
+import (
+ "sync"
+ "sync/atomic"
+ "time"
+ "unsafe"
+
+ "errors"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+const (
+ // DefaultBufferSize is the default ring buffer capacity (must be power of 2)
+ DefaultBufferSize = 16384
+
+ // MaxReaders is the maximum number of concurrent consumers
+ MaxReaders = 64
+
+ // TxEventSize is the size of a transaction event in bytes
+ TxEventSize = 184 // 32 (hash) + 20 (from) + 20 (to) + 32 (value) + 32 (gasPrice) + 8 (nonce) + 8 (gas) + 4 (type) + 8 (timestamp) + 20 (padding)
+)
+
+// TxEventType represents the type of transaction event
+type TxEventType uint8
+
+const (
+ TxEventAdded TxEventType = iota
+ TxEventRemoved
+ TxEventReplaced
+)
+
+// TxEvent is a fixed-size transaction event optimized for zero-copy access.
+// Layout is designed for CPU cache efficiency and minimal memory access.
+type TxEvent struct {
+ Hash [32]byte // Transaction hash
+ From [20]byte // Sender address
+ To [20]byte // Recipient address (0x0 for contract creation)
+ Value [32]byte // Transfer value
+ GasPrice [32]byte // Gas price or maxFeePerGas for EIP-1559
+ Nonce uint64 // Sender nonce
+ Gas uint64 // Gas limit
+ Type uint8 // Transaction type
+ EventType TxEventType // Event type (added/removed/replaced)
+ Timestamp uint64 // Event timestamp (nanoseconds)
+ _ [6]byte // Padding for alignment
+}
+
+// TxFilter defines filtering criteria for transaction events.
+type TxFilter struct {
+ // Addresses to watch (empty = all addresses)
+ Addresses map[common.Address]struct{}
+
+ // Contract methods to watch (first 4 bytes of calldata)
+ Methods map[[4]byte]struct{}
+
+ // Minimum gas price filter
+ MinGasPrice uint64
+
+ // Transaction types to include
+ Types map[uint8]struct{}
+}
+
+// Matches returns true if the transaction matches the filter.
+func (f *TxFilter) Matches(event *TxEvent) bool {
+ // Check addresses
+ if len(f.Addresses) > 0 {
+ fromAddr := common.BytesToAddress(event.From[:])
+ toAddr := common.BytesToAddress(event.To[:])
+ _, fromMatch := f.Addresses[fromAddr]
+ _, toMatch := f.Addresses[toAddr]
+ if !fromMatch && !toMatch {
+ return false
+ }
+ }
+
+ // Check transaction type
+ if len(f.Types) > 0 {
+ if _, ok := f.Types[event.Type]; !ok {
+ return false
+ }
+ }
+
+ // Check gas price
+ if f.MinGasPrice > 0 {
+ // Convert last 8 bytes to uint64 (big-endian)
+ gasPrice := uint64(event.GasPrice[24])<<56 |
+ uint64(event.GasPrice[25])<<48 |
+ uint64(event.GasPrice[26])<<40 |
+ uint64(event.GasPrice[27])<<32 |
+ uint64(event.GasPrice[28])<<24 |
+ uint64(event.GasPrice[29])<<16 |
+ uint64(event.GasPrice[30])<<8 |
+ uint64(event.GasPrice[31])
+ // Only filter if we can reliably compare (if value fits in uint64)
+ // Skip filtering for very large gas prices
+ hasHigherBytes := false
+ for i := 0; i < 24; i++ {
+ if event.GasPrice[i] != 0 {
+ hasHigherBytes = true
+ break
+ }
+ }
+ if !hasHigherBytes && gasPrice < f.MinGasPrice {
+ return false
+ }
+ }
+
+ return true
+}
+
+// TxFastFeed is a high-performance transaction event feed using lock-free ring buffers.
+type TxFastFeed struct {
+ ring *RingBuffer
+ mu sync.RWMutex
+ filters map[int]*TxFilter
+ nextID int
+ enabled atomic.Bool
+
+ // Metrics
+ eventsPublished atomic.Uint64
+ eventsDropped atomic.Uint64
+ lastPublish atomic.Int64
+}
+
+// NewTxFastFeed creates a new fast transaction feed.
+func NewTxFastFeed() *TxFastFeed {
+ feed := &TxFastFeed{
+ ring: NewRingBuffer(DefaultBufferSize, MaxReaders),
+ filters: make(map[int]*TxFilter),
+ }
+ feed.enabled.Store(true)
+ return feed
+}
+
+// Publish publishes a transaction event to all subscribers.
+func (f *TxFastFeed) Publish(tx *types.Transaction, eventType TxEventType) {
+ if !f.enabled.Load() {
+ return
+ }
+
+ // Convert transaction to fixed-size event
+ event := f.txToEvent(tx, eventType)
+
+ // Write to ring buffer
+ eventPtr := unsafe.Pointer(&event)
+ if !f.ring.Write(eventPtr) {
+ f.eventsDropped.Add(1)
+ log.Warn("Fast feed buffer full, event dropped", "hash", tx.Hash())
+ return
+ }
+
+ f.eventsPublished.Add(1)
+ f.lastPublish.Store(time.Now().UnixNano())
+}
+
+// Subscribe creates a new subscription with optional filtering.
+func (f *TxFastFeed) Subscribe(filter *TxFilter) (*Subscription, error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ if f.nextID >= MaxReaders {
+ return nil, ErrTooManySubscribers
+ }
+
+ id := f.nextID
+ f.nextID++
+
+ if filter != nil {
+ f.filters[id] = filter
+ }
+
+ sub := &Subscription{
+ id: id,
+ feed: f,
+ events: make(chan *TxEvent, 256),
+ quit: make(chan struct{}),
+ }
+
+ // Reset reader position to current
+ f.ring.Reset(id)
+
+ // Start event delivery goroutine
+ go sub.deliver()
+
+ return sub, nil
+}
+
+// txToEvent converts a transaction to a fixed-size event.
+func (f *TxFastFeed) txToEvent(tx *types.Transaction, eventType TxEventType) TxEvent {
+ var event TxEvent
+
+ // Hash
+ copy(event.Hash[:], tx.Hash().Bytes())
+
+ // From (will be filled by caller if available)
+ // We don't compute sender here to avoid expensive ECDSA recovery
+
+ // To
+ if to := tx.To(); to != nil {
+ copy(event.To[:], to.Bytes())
+ }
+
+ // Value
+ if value := tx.Value(); value != nil {
+ copy(event.Value[:], value.Bytes())
+ }
+
+ // Gas price
+ if gasPrice := tx.GasPrice(); gasPrice != nil {
+ copy(event.GasPrice[:], gasPrice.Bytes())
+ }
+
+ // Other fields
+ event.Nonce = tx.Nonce()
+ event.Gas = tx.Gas()
+ event.Type = tx.Type()
+ event.EventType = eventType
+ event.Timestamp = uint64(time.Now().UnixNano())
+
+ return event
+}
+
+// PublishWithSender publishes a transaction event with a known sender.
+func (f *TxFastFeed) PublishWithSender(tx *types.Transaction, from common.Address, eventType TxEventType) {
+ if !f.enabled.Load() {
+ return
+ }
+
+ event := f.txToEvent(tx, eventType)
+ copy(event.From[:], from.Bytes())
+
+ eventPtr := unsafe.Pointer(&event)
+ if !f.ring.Write(eventPtr) {
+ f.eventsDropped.Add(1)
+ log.Warn("Fast feed buffer full, event dropped", "hash", tx.Hash())
+ return
+ }
+
+ f.eventsPublished.Add(1)
+ f.lastPublish.Store(time.Now().UnixNano())
+}
+
+// Enable enables the fast feed.
+func (f *TxFastFeed) Enable() {
+ f.enabled.Store(true)
+}
+
+// Disable disables the fast feed.
+func (f *TxFastFeed) Disable() {
+ f.enabled.Store(false)
+}
+
+// Stats returns feed statistics.
+type FeedStats struct {
+ BufferStats BufferStats
+ EventsPublished uint64
+ EventsDropped uint64
+ LastPublishNs int64
+ Subscribers int
+}
+
+// Stats returns current feed statistics.
+func (f *TxFastFeed) Stats() FeedStats {
+ f.mu.RLock()
+ subscribers := len(f.filters)
+ f.mu.RUnlock()
+
+ return FeedStats{
+ BufferStats: f.ring.Stats(),
+ EventsPublished: f.eventsPublished.Load(),
+ EventsDropped: f.eventsDropped.Load(),
+ LastPublishNs: f.lastPublish.Load(),
+ Subscribers: subscribers,
+ }
+}
+
+var ErrTooManySubscribers = errors.New("too many subscribers")
+
diff --git a/core/txpool/fastfeed/ringbuffer.go b/core/txpool/fastfeed/ringbuffer.go
new file mode 100644
index 000000000000..1f1e82dcf29d
--- /dev/null
+++ b/core/txpool/fastfeed/ringbuffer.go
@@ -0,0 +1,215 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package fastfeed
+
+import (
+ "sync/atomic"
+ "unsafe"
+)
+
+// RingBuffer is a lock-free single-producer, multiple-consumer ring buffer
+// optimized for low-latency transaction propagation.
+type RingBuffer struct {
+ buffer []unsafe.Pointer
+ capacity uint64
+ mask uint64
+
+ // Writer position (single producer)
+ writePos atomic.Uint64
+
+ // Padding to prevent false sharing
+ _ [56]byte
+
+ // Reader positions (multiple consumers)
+ // Each consumer maintains its own read position
+ readPositions []atomic.Uint64
+ maxReaders int
+}
+
+// NewRingBuffer creates a new ring buffer with the given capacity.
+// Capacity must be a power of 2 for efficient masking.
+func NewRingBuffer(capacity int, maxReaders int) *RingBuffer {
+ if capacity&(capacity-1) != 0 {
+ panic("capacity must be a power of 2")
+ }
+
+ rb := &RingBuffer{
+ buffer: make([]unsafe.Pointer, capacity),
+ capacity: uint64(capacity),
+ mask: uint64(capacity - 1),
+ maxReaders: maxReaders,
+ readPositions: make([]atomic.Uint64, maxReaders),
+ }
+
+ return rb
+}
+
+// Write adds a new entry to the ring buffer.
+// Returns false if the buffer is full (slowest reader is too far behind).
+func (rb *RingBuffer) Write(data unsafe.Pointer) bool {
+ writePos := rb.writePos.Load()
+
+ // Check if we would overwrite unread data
+ minReadPos := rb.getMinReadPos()
+ if writePos-minReadPos >= rb.capacity {
+ // Buffer is full, would overwrite unread data
+ return false
+ }
+
+ // Write data
+ idx := writePos & rb.mask
+ atomic.StorePointer(&rb.buffer[idx], data)
+
+ // Advance write position
+ rb.writePos.Store(writePos + 1)
+
+ return true
+}
+
+// Read reads the next entry for the given reader ID.
+// Returns nil if no new data is available.
+func (rb *RingBuffer) Read(readerID int) unsafe.Pointer {
+ if readerID >= rb.maxReaders {
+ return nil
+ }
+
+ readPos := rb.readPositions[readerID].Load()
+ writePos := rb.writePos.Load()
+
+ // Check if data is available
+ if readPos >= writePos {
+ return nil
+ }
+
+ // Check if data hasn't been overwritten
+ if writePos-readPos > rb.capacity {
+ // Data was overwritten, skip to oldest available
+ readPos = writePos - rb.capacity
+ rb.readPositions[readerID].Store(readPos)
+ }
+
+ // Read data
+ idx := readPos & rb.mask
+ data := atomic.LoadPointer(&rb.buffer[idx])
+
+ // Advance read position
+ rb.readPositions[readerID].Store(readPos + 1)
+
+ return data
+}
+
+// Peek reads the next entry without advancing the read position.
+func (rb *RingBuffer) Peek(readerID int) unsafe.Pointer {
+ if readerID >= rb.maxReaders {
+ return nil
+ }
+
+ readPos := rb.readPositions[readerID].Load()
+ writePos := rb.writePos.Load()
+
+ if readPos >= writePos {
+ return nil
+ }
+
+ if writePos-readPos > rb.capacity {
+ return nil
+ }
+
+ idx := readPos & rb.mask
+ return atomic.LoadPointer(&rb.buffer[idx])
+}
+
+// Available returns the number of entries available to read for a given reader.
+func (rb *RingBuffer) Available(readerID int) int {
+ if readerID >= rb.maxReaders {
+ return 0
+ }
+
+ readPos := rb.readPositions[readerID].Load()
+ writePos := rb.writePos.Load()
+
+ if readPos >= writePos {
+ return 0
+ }
+
+ available := writePos - readPos
+ if available > rb.capacity {
+ available = rb.capacity
+ }
+
+ return int(available)
+}
+
+// getMinReadPos returns the minimum read position across all readers.
+func (rb *RingBuffer) getMinReadPos() uint64 {
+ min := rb.writePos.Load()
+
+ for i := 0; i < rb.maxReaders; i++ {
+ pos := rb.readPositions[i].Load()
+ if pos < min {
+ min = pos
+ }
+ }
+
+ return min
+}
+
+// Reset resets a reader's position to the current write position.
+// Useful for catching up when a reader falls too far behind.
+func (rb *RingBuffer) Reset(readerID int) {
+ if readerID < rb.maxReaders {
+ rb.readPositions[readerID].Store(rb.writePos.Load())
+ }
+}
+
+// Stats returns statistics about the ring buffer state.
+type BufferStats struct {
+ Capacity int
+ WritePosition uint64
+ ReadPositions []uint64
+ MinReadPos uint64
+ MaxLag uint64
+}
+
+// Stats returns current buffer statistics.
+func (rb *RingBuffer) Stats() BufferStats {
+ writePos := rb.writePos.Load()
+ readPositions := make([]uint64, rb.maxReaders)
+ minReadPos := writePos
+
+ for i := 0; i < rb.maxReaders; i++ {
+ pos := rb.readPositions[i].Load()
+ readPositions[i] = pos
+ if pos < minReadPos {
+ minReadPos = pos
+ }
+ }
+
+ maxLag := uint64(0)
+ if writePos > minReadPos {
+ maxLag = writePos - minReadPos
+ }
+
+ return BufferStats{
+ Capacity: int(rb.capacity),
+ WritePosition: writePos,
+ ReadPositions: readPositions,
+ MinReadPos: minReadPos,
+ MaxLag: maxLag,
+ }
+}
+
diff --git a/core/txpool/fastfeed/subscription.go b/core/txpool/fastfeed/subscription.go
new file mode 100644
index 000000000000..8a333af7c946
--- /dev/null
+++ b/core/txpool/fastfeed/subscription.go
@@ -0,0 +1,97 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package fastfeed
+
+import (
+ "errors"
+ "sync"
+)
+
+var (
+ ErrSubscriptionClosed = errors.New("subscription closed")
+)
+
+// Subscription represents a subscription to the transaction fast feed.
+type Subscription struct {
+ id int
+ feed *TxFastFeed
+ events chan *TxEvent
+ quit chan struct{}
+ once sync.Once
+}
+
+// Events returns the channel that delivers transaction events.
+func (s *Subscription) Events() <-chan *TxEvent {
+ return s.events
+}
+
+// Unsubscribe unsubscribes from the feed and releases resources.
+func (s *Subscription) Unsubscribe() {
+ s.once.Do(func() {
+ close(s.quit)
+ close(s.events)
+
+ // Remove filter
+ s.feed.mu.Lock()
+ delete(s.feed.filters, s.id)
+ s.feed.mu.Unlock()
+ })
+}
+
+// deliver reads events from the ring buffer and delivers them to the subscription channel.
+func (s *Subscription) deliver() {
+ for {
+ select {
+ case <-s.quit:
+ return
+ default:
+ }
+
+ // Try to read from ring buffer
+ eventPtr := s.feed.ring.Read(s.id)
+ if eventPtr == nil {
+ // No data available, yield
+ continue
+ }
+
+ // Convert pointer to event
+ event := (*TxEvent)(eventPtr)
+
+ // Apply filter if set
+ s.feed.mu.RLock()
+ filter, hasFilter := s.feed.filters[s.id]
+ s.feed.mu.RUnlock()
+
+ if hasFilter && !filter.Matches(event) {
+ continue
+ }
+
+ // Copy event to avoid data races
+ eventCopy := *event
+
+ // Try to deliver to channel
+ select {
+ case s.events <- &eventCopy:
+ case <-s.quit:
+ return
+ default:
+ // Channel full, skip this event
+ // In production, might want to track skipped events
+ }
+ }
+}
+
diff --git a/core/txpool/fastfeed_test.go b/core/txpool/fastfeed_test.go
new file mode 100644
index 000000000000..7225f7bf93ae
--- /dev/null
+++ b/core/txpool/fastfeed_test.go
@@ -0,0 +1,246 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package txpool
+
+import (
+ "math/big"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/txpool/fastfeed"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+func TestTxFastFeedBasic(t *testing.T) {
+ feed := fastfeed.NewTxFastFeed()
+
+ // Create a test transaction
+ key, _ := crypto.GenerateKey()
+ signer := types.LatestSigner(params.TestChainConfig)
+ tx := types.MustSignNewTx(key, signer, &types.LegacyTx{
+ Nonce: 0,
+ GasPrice: big.NewInt(1000000000),
+ Gas: 21000,
+ To: &common.Address{1},
+ Value: big.NewInt(1000000000000000000),
+ })
+
+ // Subscribe
+ sub, err := feed.Subscribe(nil)
+ if err != nil {
+ t.Fatalf("Failed to subscribe: %v", err)
+ }
+ defer sub.Unsubscribe()
+
+ // Publish transaction
+ feed.Publish(tx, fastfeed.TxEventAdded)
+
+ // Receive event
+ select {
+ case event := <-sub.Events():
+ if event.EventType != fastfeed.TxEventAdded {
+ t.Errorf("Expected TxEventAdded, got %d", event.EventType)
+ }
+ expectedHash := tx.Hash()
+ receivedHash := common.BytesToHash(event.Hash[:])
+ if receivedHash != expectedHash {
+ t.Errorf("Hash mismatch: expected %s, got %s", expectedHash.Hex(), receivedHash.Hex())
+ }
+ case <-time.After(100 * time.Millisecond):
+ t.Fatal("Timeout waiting for event")
+ }
+}
+
+func TestTxFastFeedFiltering(t *testing.T) {
+ feed := fastfeed.NewTxFastFeed()
+
+ // Create test transactions
+ key, _ := crypto.GenerateKey()
+ signer := types.LatestSigner(params.TestChainConfig)
+
+ targetAddr := common.Address{1}
+ otherAddr := common.Address{2}
+
+ // Transaction to target address
+ targetTx := types.MustSignNewTx(key, signer, &types.LegacyTx{
+ Nonce: 0,
+ GasPrice: big.NewInt(1000000000),
+ Gas: 21000,
+ To: &targetAddr,
+ Value: big.NewInt(1000),
+ })
+
+ // Transaction to other address
+ otherTx := types.MustSignNewTx(key, signer, &types.LegacyTx{
+ Nonce: 1,
+ GasPrice: big.NewInt(1000000000),
+ Gas: 21000,
+ To: &otherAddr,
+ Value: big.NewInt(2000),
+ })
+
+ // Subscribe with address filter
+ filter := &fastfeed.TxFilter{
+ Addresses: map[common.Address]struct{}{
+ targetAddr: {},
+ },
+ }
+ sub, err := feed.Subscribe(filter)
+ if err != nil {
+ t.Fatalf("Failed to subscribe: %v", err)
+ }
+ defer sub.Unsubscribe()
+
+ // Publish both transactions
+ feed.Publish(otherTx, fastfeed.TxEventAdded)
+ feed.Publish(targetTx, fastfeed.TxEventAdded)
+
+ // Should only receive target address tx
+ select {
+ case event := <-sub.Events():
+ receivedHash := common.BytesToHash(event.Hash[:])
+ if receivedHash != targetTx.Hash() {
+ t.Errorf("Expected target tx %s, got %s", targetTx.Hash().Hex(), receivedHash.Hex())
+ }
+ case <-time.After(100 * time.Millisecond):
+ t.Fatal("Timeout waiting for filtered event")
+ }
+
+ // Should not receive other address tx
+ select {
+ case event := <-sub.Events():
+ t.Errorf("Unexpected event received: %s", common.BytesToHash(event.Hash[:]).Hex())
+ case <-time.After(50 * time.Millisecond):
+ // Expected timeout
+ }
+}
+
+func TestTxFastFeedMultipleConsumers(t *testing.T) {
+ feed := fastfeed.NewTxFastFeed()
+
+ // Create test transaction
+ key, _ := crypto.GenerateKey()
+ signer := types.LatestSigner(params.TestChainConfig)
+ tx := types.MustSignNewTx(key, signer, &types.LegacyTx{
+ Nonce: 0,
+ GasPrice: big.NewInt(1000000000),
+ Gas: 21000,
+ To: &common.Address{1},
+ Value: big.NewInt(1000),
+ })
+
+ // Create multiple subscribers
+ const numSubs = 5
+ subs := make([]*fastfeed.Subscription, numSubs)
+ for i := 0; i < numSubs; i++ {
+ sub, err := feed.Subscribe(nil)
+ if err != nil {
+ t.Fatalf("Failed to subscribe #%d: %v", i, err)
+ }
+ defer sub.Unsubscribe()
+ subs[i] = sub
+ }
+
+ // Publish transaction
+ feed.Publish(tx, fastfeed.TxEventAdded)
+
+ // All subscribers should receive the event
+ for i, sub := range subs {
+ select {
+ case event := <-sub.Events():
+ receivedHash := common.BytesToHash(event.Hash[:])
+ if receivedHash != tx.Hash() {
+ t.Errorf("Subscriber %d: hash mismatch", i)
+ }
+ case <-time.After(100 * time.Millisecond):
+ t.Fatalf("Subscriber %d: timeout waiting for event", i)
+ }
+ }
+}
+
+func BenchmarkTxFastFeedPublish(b *testing.B) {
+ feed := fastfeed.NewTxFastFeed()
+
+ key, _ := crypto.GenerateKey()
+ signer := types.LatestSigner(params.TestChainConfig)
+ tx := types.MustSignNewTx(key, signer, &types.LegacyTx{
+ Nonce: 0,
+ GasPrice: big.NewInt(1000000000),
+ Gas: 21000,
+ To: &common.Address{1},
+ Value: big.NewInt(1000),
+ })
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ feed.Publish(tx, fastfeed.TxEventAdded)
+ }
+}
+
+func BenchmarkTxFastFeedLatency(b *testing.B) {
+ feed := fastfeed.NewTxFastFeed()
+
+ sub, err := feed.Subscribe(nil)
+ if err != nil {
+ b.Fatalf("Failed to subscribe: %v", err)
+ }
+ defer sub.Unsubscribe()
+
+ key, _ := crypto.GenerateKey()
+ signer := types.LatestSigner(params.TestChainConfig)
+
+ // Pre-generate transactions
+ txs := make([]*types.Transaction, b.N)
+ for i := 0; i < b.N; i++ {
+ txs[i] = types.MustSignNewTx(key, signer, &types.LegacyTx{
+ Nonce: uint64(i),
+ GasPrice: big.NewInt(1000000000),
+ Gas: 21000,
+ To: &common.Address{1},
+ Value: big.NewInt(1000),
+ })
+ }
+
+ var maxLatency time.Duration
+ var totalLatency time.Duration
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ start := time.Now()
+ feed.Publish(txs[i], fastfeed.TxEventAdded)
+
+ select {
+ case <-sub.Events():
+ latency := time.Since(start)
+ totalLatency += latency
+ if latency > maxLatency {
+ maxLatency = latency
+ }
+ case <-time.After(100 * time.Millisecond):
+ b.Fatalf("Timeout waiting for event %d", i)
+ }
+ }
+ b.StopTimer()
+
+ avgLatency := totalLatency / time.Duration(b.N)
+ b.ReportMetric(float64(avgLatency.Nanoseconds()), "ns/event")
+ b.ReportMetric(float64(maxLatency.Nanoseconds())/1000, "μs-max")
+}
+
diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go
index 437861efca7c..967aab274cc6 100644
--- a/core/txpool/txpool.go
+++ b/core/txpool/txpool.go
@@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/txpool/fastfeed"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
@@ -74,6 +75,9 @@ type TxPool struct {
term chan struct{} // Termination channel to detect a closed pool
sync chan chan error // Testing / simulator channel to block until internal reset is done
+
+ // Fast feed for low-latency transaction propagation
+ fastFeed *fastfeed.TxFastFeed
}
// New creates a new transaction pool to gather, sort and filter inbound
@@ -101,6 +105,7 @@ func New(gasTip uint64, chain BlockChain, subpools []SubPool) (*TxPool, error) {
quit: make(chan chan error),
term: make(chan struct{}),
sync: make(chan chan error),
+ fastFeed: fastfeed.NewTxFastFeed(),
}
reserver := NewReservationTracker()
for i, subpool := range subpools {
@@ -350,6 +355,11 @@ func (p *TxPool) Add(txs []*types.Transaction, sync bool) []error {
// Find which subpool handled it and pull in the corresponding error
errs[i] = errsets[split][0]
errsets[split] = errsets[split][1:]
+
+ // Publish to fast feed if transaction was accepted
+ if errs[i] == nil {
+ p.fastFeed.Publish(txs[i], fastfeed.TxEventAdded)
+ }
}
return errs
}
diff --git a/eth/api_backend.go b/eth/api_backend.go
index 766a99fc1ef6..d23ca920a524 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -40,6 +40,7 @@ import (
"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
+ "github.com/ethereum/go-ethereum/miner"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
)
@@ -61,6 +62,11 @@ func (b *EthAPIBackend) CurrentBlock() *types.Header {
return b.eth.blockchain.CurrentBlock()
}
+// Miner returns the miner instance.
+func (b *EthAPIBackend) Miner() *miner.Miner {
+ return b.eth.Miner()
+}
+
func (b *EthAPIBackend) SetHead(number uint64) {
b.eth.handler.downloader.Cancel()
b.eth.blockchain.SetHead(number)
diff --git a/eth/backend.go b/eth/backend.go
index 85095618222d..21f4000eb00c 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -28,6 +28,7 @@ import (
"time"
"github.com/ethereum/go-ethereum/accounts"
+ grpcapi "github.com/ethereum/go-ethereum/api/grpc"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/consensus"
@@ -121,6 +122,8 @@ type Ethereum struct {
p2pServer *p2p.Server
+ grpcService *grpcapi.Service // Low-latency gRPC API for trading operations
+
lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase)
shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully
@@ -355,6 +358,13 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
// Start the RPC service
eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID)
+ // Initialize gRPC service if enabled
+ if config.EnableGRPC {
+ eth.grpcService = grpcapi.NewService(eth.APIBackend, config.GRPCHost, config.GRPCPort)
+ stack.RegisterLifecycle(eth.grpcService)
+ log.Info("gRPC service initialized", "host", config.GRPCHost, "port", config.GRPCPort)
+ }
+
// Register the backend on the node
stack.RegisterAPIs(eth.APIs())
stack.RegisterProtocols(eth.Protocols())
diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go
index c4a0956b3b47..2b41d9d4bd1a 100644
--- a/eth/ethconfig/config.go
+++ b/eth/ethconfig/config.go
@@ -72,6 +72,9 @@ var Defaults = Config{
RPCTxFeeCap: 1, // 1 ether
TxSyncDefaultTimeout: 20 * time.Second,
TxSyncMaxTimeout: 1 * time.Minute,
+ EnableGRPC: false, // Disabled by default
+ GRPCHost: "localhost",
+ GRPCPort: 9090,
}
//go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go
@@ -189,6 +192,11 @@ type Config struct {
// EIP-7966: eth_sendRawTransactionSync timeouts
TxSyncDefaultTimeout time.Duration `toml:",omitempty"`
TxSyncMaxTimeout time.Duration `toml:",omitempty"`
+
+ // gRPC options for low-latency trading operations
+ EnableGRPC bool // Whether to enable the gRPC server
+ GRPCHost string // gRPC server host (default: localhost)
+ GRPCPort int // gRPC server port (default: 9090)
}
// CreateConsensusEngine creates a consensus engine for the given chain config.
diff --git a/go.mod b/go.mod
index 3590a5492932..4e7f8c983db9 100644
--- a/go.mod
+++ b/go.mod
@@ -33,7 +33,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang/snappy v1.0.0
github.com/google/gofuzz v1.2.0
- github.com/google/uuid v1.3.0
+ github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.4.2
github.com/graph-gophers/graphql-go v1.3.0
github.com/hashicorp/go-bexpr v0.1.10
@@ -65,18 +65,21 @@ require (
github.com/urfave/cli/v2 v2.27.5
go.uber.org/automaxprocs v1.5.2
go.uber.org/goleak v1.3.0
- golang.org/x/crypto v0.36.0
+ golang.org/x/crypto v0.43.0
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
- golang.org/x/sync v0.12.0
- golang.org/x/sys v0.36.0
- golang.org/x/text v0.23.0
+ golang.org/x/sync v0.17.0
+ golang.org/x/sys v0.37.0
+ golang.org/x/text v0.30.0
golang.org/x/time v0.9.0
- golang.org/x/tools v0.29.0
- google.golang.org/protobuf v1.34.2
+ golang.org/x/tools v0.37.0
+ google.golang.org/grpc v1.77.0
+ google.golang.org/protobuf v1.36.10
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
)
+require google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
+
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
@@ -144,8 +147,8 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
- golang.org/x/mod v0.22.0 // indirect
- golang.org/x/net v0.38.0 // indirect
+ golang.org/x/mod v0.28.0 // indirect
+ golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index 6ecb0b7ec348..d95be64e0ada 100644
--- a/go.sum
+++ b/go.sum
@@ -140,6 +140,10 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -174,16 +178,16 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -370,6 +374,18 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBi
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -382,16 +398,16 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
-golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
+golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -407,8 +423,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo=
+golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -417,8 +433,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -451,8 +467,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
-golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -470,8 +486,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
@@ -483,21 +499,27 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
-golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
+golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
+golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/miner/api.go b/miner/api.go
new file mode 100644
index 000000000000..c1202fd4deef
--- /dev/null
+++ b/miner/api.go
@@ -0,0 +1,197 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package miner
+
+import (
+ "context"
+ "errors"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+)
+
+// API exposes miner-related methods for the RPC interface.
+type API struct {
+ miner *Miner
+}
+
+// NewAPI creates a new API instance.
+func NewAPI(miner *Miner) *API {
+ return &API{miner: miner}
+}
+
+// BundleArgs represents arguments for bundle submission.
+type BundleArgs struct {
+ Txs []hexutil.Bytes `json:"txs"`
+ MinTimestamp *hexutil.Uint64 `json:"minTimestamp,omitempty"`
+ MaxTimestamp *hexutil.Uint64 `json:"maxTimestamp,omitempty"`
+ RevertingTxs []int `json:"revertingTxs,omitempty"`
+ TargetBlock *hexutil.Uint64 `json:"targetBlock,omitempty"`
+}
+
+// BundleSimulationResponse represents the result of a bundle simulation.
+type BundleSimulationResponse struct {
+ Success bool `json:"success"`
+ GasUsed hexutil.Uint64 `json:"gasUsed"`
+ Profit *hexutil.Big `json:"profit"`
+ CoinbaseBalance *hexutil.Big `json:"coinbaseBalance"`
+ FailedTxIndex int `json:"failedTxIndex,omitempty"`
+ FailedTxError string `json:"failedTxError,omitempty"`
+ TxResults []TxSimulationResponse `json:"txResults"`
+}
+
+// TxSimulationResponse represents the result of a transaction simulation.
+type TxSimulationResponse struct {
+ Success bool `json:"success"`
+ GasUsed hexutil.Uint64 `json:"gasUsed"`
+ Error string `json:"error,omitempty"`
+ ReturnValue hexutil.Bytes `json:"returnValue,omitempty"`
+}
+
+// SubmitBundle submits a bundle for inclusion in future blocks.
+func (api *API) SubmitBundle(ctx context.Context, args BundleArgs) (common.Hash, error) {
+ if len(args.Txs) == 0 {
+ return common.Hash{}, errors.New("bundle must contain at least one transaction")
+ }
+
+ // Decode transactions
+ txs := make([]*types.Transaction, len(args.Txs))
+ for i, encodedTx := range args.Txs {
+ var tx types.Transaction
+ if err := tx.UnmarshalBinary(encodedTx); err != nil {
+ return common.Hash{}, err
+ }
+ txs[i] = &tx
+ }
+
+ // Create bundle
+ bundle := &Bundle{
+ Txs: txs,
+ RevertingTxs: args.RevertingTxs,
+ }
+
+ if args.MinTimestamp != nil {
+ bundle.MinTimestamp = uint64(*args.MinTimestamp)
+ }
+ if args.MaxTimestamp != nil {
+ bundle.MaxTimestamp = uint64(*args.MaxTimestamp)
+ }
+ if args.TargetBlock != nil {
+ bundle.TargetBlock = uint64(*args.TargetBlock)
+ }
+
+ // Add bundle
+ if err := api.miner.AddBundle(bundle); err != nil {
+ return common.Hash{}, err
+ }
+
+ // Return hash of first transaction as bundle ID
+ return txs[0].Hash(), nil
+}
+
+// SimulateBundle simulates a bundle and returns the result.
+func (api *API) SimulateBundle(ctx context.Context, args BundleArgs) (*BundleSimulationResponse, error) {
+ if len(args.Txs) == 0 {
+ return nil, errors.New("bundle must contain at least one transaction")
+ }
+
+ // Decode transactions
+ txs := make([]*types.Transaction, len(args.Txs))
+ for i, encodedTx := range args.Txs {
+ var tx types.Transaction
+ if err := tx.UnmarshalBinary(encodedTx); err != nil {
+ return nil, err
+ }
+ txs[i] = &tx
+ }
+
+ // Create bundle
+ bundle := &Bundle{
+ Txs: txs,
+ RevertingTxs: args.RevertingTxs,
+ }
+
+ if args.MinTimestamp != nil {
+ bundle.MinTimestamp = uint64(*args.MinTimestamp)
+ }
+ if args.MaxTimestamp != nil {
+ bundle.MaxTimestamp = uint64(*args.MaxTimestamp)
+ }
+
+ // Get current header for simulation
+ header := api.miner.chain.CurrentHeader()
+
+ // Create simulation header based on current + 1
+ simHeader := &types.Header{
+ ParentHash: header.Hash(),
+ Number: new(big.Int).Add(header.Number, big.NewInt(1)),
+ GasLimit: header.GasLimit,
+ Time: header.Time + 12, // Assume 12 second block time
+ BaseFee: header.BaseFee,
+ }
+
+ // Simulate bundle
+ result, err := api.miner.SimulateBundle(bundle, simHeader)
+ if err != nil {
+ return nil, err
+ }
+
+ // Convert result to response
+ response := &BundleSimulationResponse{
+ Success: result.Success,
+ GasUsed: hexutil.Uint64(result.GasUsed),
+ Profit: (*hexutil.Big)(result.Profit),
+ CoinbaseBalance: (*hexutil.Big)(result.CoinbaseBalance),
+ FailedTxIndex: result.FailedTxIndex,
+ TxResults: make([]TxSimulationResponse, len(result.TxResults)),
+ }
+
+ if result.FailedTxError != nil {
+ response.FailedTxError = result.FailedTxError.Error()
+ }
+
+ for i, txResult := range result.TxResults {
+ txResp := TxSimulationResponse{
+ Success: txResult.Success,
+ GasUsed: hexutil.Uint64(txResult.GasUsed),
+ }
+ if txResult.Error != nil {
+ txResp.Error = txResult.Error.Error()
+ }
+ if txResult.ReturnValue != nil {
+ txResp.ReturnValue = txResult.ReturnValue
+ }
+ response.TxResults[i] = txResp
+ }
+
+ return response, nil
+}
+
+// GetBundles returns currently pending bundles for a specific block number.
+func (api *API) GetBundles(ctx context.Context, blockNumber hexutil.Uint64) (int, error) {
+ bundles := api.miner.GetBundles(uint64(blockNumber))
+ return len(bundles), nil
+}
+
+// ClearExpiredBundles removes bundles that are expired for the given block number.
+func (api *API) ClearExpiredBundles(ctx context.Context, blockNumber hexutil.Uint64) error {
+ api.miner.ClearExpiredBundles(uint64(blockNumber))
+ return nil
+}
+
diff --git a/miner/bundle.go b/miner/bundle.go
new file mode 100644
index 000000000000..49054f3b8896
--- /dev/null
+++ b/miner/bundle.go
@@ -0,0 +1,141 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package miner
+
+import (
+ "errors"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/txpool"
+ "github.com/ethereum/go-ethereum/core/types"
+)
+
+var (
+ ErrBundleTimestampTooEarly = errors.New("bundle timestamp too early")
+ ErrBundleTimestampTooLate = errors.New("bundle timestamp too late")
+ ErrBundleReverted = errors.New("bundle transaction reverted")
+)
+
+// Bundle represents a group of transactions that should be executed atomically
+// in a specific order within a block.
+type Bundle struct {
+ Txs []*types.Transaction
+ MinTimestamp uint64
+ MaxTimestamp uint64
+ RevertingTxs []int // Indices of transactions allowed to revert
+ TargetBlock uint64
+}
+
+// ValidateTimestamp checks if the bundle can be included at the given timestamp.
+func (b *Bundle) ValidateTimestamp(timestamp uint64) error {
+ if b.MinTimestamp > 0 && timestamp < b.MinTimestamp {
+ return ErrBundleTimestampTooEarly
+ }
+ if b.MaxTimestamp > 0 && timestamp > b.MaxTimestamp {
+ return ErrBundleTimestampTooLate
+ }
+ return nil
+}
+
+// CanRevert returns true if the transaction at the given index is allowed to revert.
+func (b *Bundle) CanRevert(txIndex int) bool {
+ for _, idx := range b.RevertingTxs {
+ if idx == txIndex {
+ return true
+ }
+ }
+ return false
+}
+
+// BundleSimulationResult contains the results of simulating a bundle.
+type BundleSimulationResult struct {
+ Success bool
+ GasUsed uint64
+ Profit *big.Int
+ StateChanges map[common.Address]*AccountChange
+ FailedTxIndex int // -1 if all succeeded
+ FailedTxError error
+ CoinbaseBalance *big.Int
+ TxResults []*TxSimulationResult
+}
+
+// TxSimulationResult contains results for a single transaction in a bundle.
+type TxSimulationResult struct {
+ Success bool
+ GasUsed uint64
+ Error error
+ Logs []*types.Log
+ ReturnValue []byte
+}
+
+// AccountChange represents state changes for an account.
+type AccountChange struct {
+ BalanceBefore *big.Int
+ BalanceAfter *big.Int
+ NonceBefore uint64
+ NonceAfter uint64
+ StorageChanges map[common.Hash]common.Hash
+}
+
+// OrderingStrategy defines an interface for custom transaction ordering.
+// Implementations can provide arbitrary ordering logic for block building.
+type OrderingStrategy interface {
+ // OrderTransactions takes pending transactions and bundles, and returns
+ // an ordered list of transactions to include in the block.
+ OrderTransactions(
+ pending map[common.Address][]*txpool.LazyTransaction,
+ bundles []*Bundle,
+ state *state.StateDB,
+ header *types.Header,
+ ) ([]*types.Transaction, error)
+}
+
+// DefaultOrderingStrategy implements the default greedy ordering by gas price.
+type DefaultOrderingStrategy struct{}
+
+// OrderTransactions implements the default ordering strategy.
+func (s *DefaultOrderingStrategy) OrderTransactions(
+ pending map[common.Address][]*txpool.LazyTransaction,
+ bundles []*Bundle,
+ state *state.StateDB,
+ header *types.Header,
+) ([]*types.Transaction, error) {
+ // Default behavior: just return transactions sorted by price
+ // This maintains backward compatibility
+ var txs []*types.Transaction
+
+ // First, add bundle transactions
+ for _, bundle := range bundles {
+ if err := bundle.ValidateTimestamp(header.Time); err == nil {
+ txs = append(txs, bundle.Txs...)
+ }
+ }
+
+ // Then add pending transactions (would normally use price sorting)
+ for _, accountTxs := range pending {
+ for _, ltx := range accountTxs {
+ if tx := ltx.Resolve(); tx != nil {
+ txs = append(txs, tx)
+ }
+ }
+ }
+
+ return txs, nil
+}
+
diff --git a/miner/bundle_test.go b/miner/bundle_test.go
new file mode 100644
index 000000000000..54a68a325448
--- /dev/null
+++ b/miner/bundle_test.go
@@ -0,0 +1,122 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package miner
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/txpool"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+func TestBundleValidation(t *testing.T) {
+ bundle := &Bundle{
+ Txs: []*types.Transaction{},
+ MinTimestamp: 100,
+ MaxTimestamp: 200,
+ TargetBlock: 1000,
+ }
+
+ // Test timestamp validation
+ if err := bundle.ValidateTimestamp(50); err != ErrBundleTimestampTooEarly {
+ t.Errorf("Expected ErrBundleTimestampTooEarly, got %v", err)
+ }
+
+ if err := bundle.ValidateTimestamp(150); err != nil {
+ t.Errorf("Expected no error for valid timestamp, got %v", err)
+ }
+
+ if err := bundle.ValidateTimestamp(250); err != ErrBundleTimestampTooLate {
+ t.Errorf("Expected ErrBundleTimestampTooLate, got %v", err)
+ }
+}
+
+func TestBundleCanRevert(t *testing.T) {
+ bundle := &Bundle{
+ Txs: []*types.Transaction{},
+ RevertingTxs: []int{1, 3, 5},
+ }
+
+ testCases := []struct {
+ index int
+ expected bool
+ }{
+ {0, false},
+ {1, true},
+ {2, false},
+ {3, true},
+ {4, false},
+ {5, true},
+ {6, false},
+ }
+
+ for _, tc := range testCases {
+ result := bundle.CanRevert(tc.index)
+ if result != tc.expected {
+ t.Errorf("CanRevert(%d) = %v, want %v", tc.index, result, tc.expected)
+ }
+ }
+}
+
+func TestDefaultOrderingStrategy(t *testing.T) {
+ strategy := &DefaultOrderingStrategy{}
+
+ // Create some test transactions
+ key, _ := crypto.GenerateKey()
+ signer := types.LatestSigner(params.TestChainConfig)
+
+ tx1 := types.MustSignNewTx(key, signer, &types.LegacyTx{
+ Nonce: 0,
+ GasPrice: big.NewInt(1),
+ Gas: 21000,
+ To: &common.Address{1},
+ Value: big.NewInt(1),
+ })
+
+ tx2 := types.MustSignNewTx(key, signer, &types.LegacyTx{
+ Nonce: 1,
+ GasPrice: big.NewInt(2),
+ Gas: 21000,
+ To: &common.Address{2},
+ Value: big.NewInt(2),
+ })
+
+ bundle := &Bundle{
+ Txs: []*types.Transaction{tx1, tx2},
+ TargetBlock: 0,
+ }
+
+ pending := make(map[common.Address][]*txpool.LazyTransaction)
+ header := &types.Header{
+ Number: big.NewInt(1),
+ Time: 100,
+ }
+
+ txs, err := strategy.OrderTransactions(pending, []*Bundle{bundle}, nil, header)
+ if err != nil {
+ t.Fatalf("OrderTransactions failed: %v", err)
+ }
+
+ if len(txs) != 2 {
+ t.Errorf("Expected 2 transactions, got %d", len(txs))
+ }
+}
+
diff --git a/miner/miner.go b/miner/miner.go
index 810cc20a6c61..d5b50afc65be 100644
--- a/miner/miner.go
+++ b/miner/miner.go
@@ -74,17 +74,27 @@ type Miner struct {
chain *core.BlockChain
pending *pending
pendingMu sync.Mutex // Lock protects the pending block
+
+ // Bundle and ordering support
+ bundlesMu sync.RWMutex
+ bundles []*Bundle
+ strategy OrderingStrategy
+ simulator *BundleSimulator
}
// New creates a new miner with provided config.
func New(eth Backend, config Config, engine consensus.Engine) *Miner {
+ chain := eth.BlockChain()
return &Miner{
config: &config,
- chainConfig: eth.BlockChain().Config(),
+ chainConfig: chain.Config(),
engine: engine,
txpool: eth.TxPool(),
- chain: eth.BlockChain(),
+ chain: chain,
pending: &pending{},
+ bundles: make([]*Bundle, 0),
+ strategy: &DefaultOrderingStrategy{},
+ simulator: NewBundleSimulator(chain),
}
}
@@ -133,6 +143,64 @@ func (miner *Miner) SetGasTip(tip *big.Int) error {
return nil
}
+// SetOrderingStrategy sets a custom transaction ordering strategy.
+func (miner *Miner) SetOrderingStrategy(strategy OrderingStrategy) {
+ miner.confMu.Lock()
+ miner.strategy = strategy
+ miner.confMu.Unlock()
+}
+
+// AddBundle adds a bundle to be considered for inclusion in future blocks.
+func (miner *Miner) AddBundle(bundle *Bundle) error {
+ miner.bundlesMu.Lock()
+ defer miner.bundlesMu.Unlock()
+
+ miner.bundles = append(miner.bundles, bundle)
+ return nil
+}
+
+// GetBundles returns the current bundles for the given block number.
+func (miner *Miner) GetBundles(blockNumber uint64) []*Bundle {
+ miner.bundlesMu.RLock()
+ defer miner.bundlesMu.RUnlock()
+
+ validBundles := make([]*Bundle, 0)
+ for _, bundle := range miner.bundles {
+ if bundle.TargetBlock == 0 || bundle.TargetBlock == blockNumber {
+ validBundles = append(validBundles, bundle)
+ }
+ }
+ return validBundles
+}
+
+// ClearExpiredBundles removes bundles that are no longer valid for the given block number.
+func (miner *Miner) ClearExpiredBundles(blockNumber uint64) {
+ miner.bundlesMu.Lock()
+ defer miner.bundlesMu.Unlock()
+
+ validBundles := make([]*Bundle, 0)
+ for _, bundle := range miner.bundles {
+ if bundle.TargetBlock == 0 || bundle.TargetBlock >= blockNumber {
+ validBundles = append(validBundles, bundle)
+ }
+ }
+ miner.bundles = validBundles
+}
+
+// SimulateBundle simulates a bundle and returns the result.
+func (miner *Miner) SimulateBundle(bundle *Bundle, header *types.Header) (*BundleSimulationResult, error) {
+ // Get current state
+ stateDB, err := miner.chain.StateAt(miner.chain.CurrentBlock().Root)
+ if err != nil {
+ return nil, err
+ }
+
+ // Copy state for simulation
+ simState := stateDB.Copy()
+
+ return miner.simulator.SimulateBundle(bundle, header, simState, miner.config.PendingFeeRecipient)
+}
+
// BuildPayload builds the payload according to the provided parameters.
func (miner *Miner) BuildPayload(args *BuildPayloadArgs, witness bool) (*Payload, error) {
return miner.buildPayload(args, witness)
diff --git a/miner/simulator.go b/miner/simulator.go
new file mode 100644
index 000000000000..77dd3e246dfc
--- /dev/null
+++ b/miner/simulator.go
@@ -0,0 +1,221 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package miner
+
+import (
+ "errors"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+// BundleSimulator provides functionality to simulate bundle execution.
+type BundleSimulator struct {
+ chain *core.BlockChain
+ config *params.ChainConfig
+}
+
+// NewBundleSimulator creates a new bundle simulator.
+func NewBundleSimulator(chain *core.BlockChain) *BundleSimulator {
+ return &BundleSimulator{
+ chain: chain,
+ config: chain.Config(),
+ }
+}
+
+// SimulateBundle simulates the execution of a bundle on top of the given state.
+func (s *BundleSimulator) SimulateBundle(
+ bundle *Bundle,
+ header *types.Header,
+ stateDB *state.StateDB,
+ coinbase common.Address,
+) (*BundleSimulationResult, error) {
+ // Validate bundle timestamp
+ if err := bundle.ValidateTimestamp(header.Time); err != nil {
+ return nil, err
+ }
+
+ result := &BundleSimulationResult{
+ Success: true,
+ GasUsed: 0,
+ Profit: big.NewInt(0),
+ StateChanges: make(map[common.Address]*AccountChange),
+ FailedTxIndex: -1,
+ TxResults: make([]*TxSimulationResult, 0, len(bundle.Txs)),
+ }
+
+ // Record initial coinbase balance
+ result.CoinbaseBalance = new(big.Int).Set(stateDB.GetBalance(coinbase).ToBig())
+
+ // Create EVM context
+ vmContext := core.NewEVMBlockContext(header, s.chain, &coinbase)
+ vmConfig := vm.Config{}
+ evm := vm.NewEVM(vmContext, stateDB, s.config, vmConfig)
+
+ // Simulate each transaction in the bundle
+ gasPool := new(core.GasPool).AddGas(header.GasLimit)
+
+ for i, tx := range bundle.Txs {
+ // Take snapshot before transaction
+ snapshot := stateDB.Snapshot()
+
+ // Record pre-execution state
+ from, err := types.Sender(types.LatestSigner(s.config), tx)
+ if err != nil {
+ result.Success = false
+ result.FailedTxIndex = i
+ result.FailedTxError = err
+ return result, nil
+ }
+
+ // Execute transaction
+ txResult := s.simulateTransaction(tx, evm, stateDB, gasPool, header, from)
+ result.TxResults = append(result.TxResults, txResult)
+ result.GasUsed += txResult.GasUsed
+
+ // Check if transaction failed and is not allowed to revert
+ if !txResult.Success && !bundle.CanRevert(i) {
+ // Revert state
+ stateDB.RevertToSnapshot(snapshot)
+ result.Success = false
+ result.FailedTxIndex = i
+ result.FailedTxError = txResult.Error
+ return result, nil
+ }
+
+ // Calculate profit from this transaction
+ if txResult.Success {
+ minerFee, _ := tx.EffectiveGasTip(header.BaseFee)
+ profit := new(big.Int).Mul(minerFee, new(big.Int).SetUint64(txResult.GasUsed))
+ result.Profit.Add(result.Profit, profit)
+ }
+ }
+
+ // Record final coinbase balance
+ finalBalance := stateDB.GetBalance(coinbase).ToBig()
+ actualProfit := new(big.Int).Sub(finalBalance, result.CoinbaseBalance)
+ result.Profit = actualProfit
+ result.CoinbaseBalance = finalBalance
+
+ return result, nil
+}
+
+// simulateTransaction simulates a single transaction.
+func (s *BundleSimulator) simulateTransaction(
+ tx *types.Transaction,
+ evm *vm.EVM,
+ stateDB *state.StateDB,
+ gasPool *core.GasPool,
+ header *types.Header,
+ from common.Address,
+) *TxSimulationResult {
+ result := &TxSimulationResult{
+ Success: true,
+ Logs: make([]*types.Log, 0),
+ }
+
+ // Set tx context
+ stateDB.SetTxContext(tx.Hash(), stateDB.TxIndex())
+
+ // Convert transaction to message
+ msg, err := core.TransactionToMessage(tx, types.MakeSigner(s.config, header.Number, header.Time), header.BaseFee)
+ if err != nil {
+ result.Success = false
+ result.Error = err
+ return result
+ }
+
+ // Apply the transaction
+ execResult, err := core.ApplyMessage(evm, msg, gasPool)
+ if err != nil {
+ result.Success = false
+ result.Error = err
+ result.GasUsed = tx.Gas()
+ return result
+ }
+
+ // Check execution result
+ if execResult.Failed() {
+ result.Success = false
+ result.Error = execResult.Err
+ }
+
+ result.GasUsed = execResult.UsedGas
+ result.ReturnValue = execResult.ReturnData
+
+ // Collect logs
+ result.Logs = stateDB.GetLogs(tx.Hash(), header.Number.Uint64(), header.Hash(), header.Time)
+
+ return result
+}
+
+// SimulateBundleAtPosition simulates a bundle inserted at a specific position
+// in the existing block template.
+func (s *BundleSimulator) SimulateBundleAtPosition(
+ bundle *Bundle,
+ baseBlock *types.Block,
+ position int,
+) (*BundleSimulationResult, error) {
+ // Get state at parent block
+ parent := s.chain.GetBlock(baseBlock.ParentHash(), baseBlock.NumberU64()-1)
+ if parent == nil {
+ return nil, errors.New("parent block not found")
+ }
+
+ stateDB, err := s.chain.StateAt(parent.Root())
+ if err != nil {
+ return nil, err
+ }
+
+ // Copy the state for simulation
+ simState := stateDB.Copy()
+
+ // Execute transactions before the insertion point
+ if position > 0 {
+ header := baseBlock.Header()
+ vmContext := core.NewEVMBlockContext(header, s.chain, &header.Coinbase)
+ vmConfig := vm.Config{}
+ evm := vm.NewEVM(vmContext, simState, s.config, vmConfig)
+ gasPool := new(core.GasPool).AddGas(header.GasLimit)
+
+ for i, tx := range baseBlock.Transactions() {
+ if i >= position {
+ break
+ }
+ msg, err := core.TransactionToMessage(tx, types.MakeSigner(s.config, header.Number, header.Time), header.BaseFee)
+ if err != nil {
+ log.Warn("Failed to convert transaction to message", "err", err)
+ continue
+ }
+ simState.SetTxContext(tx.Hash(), i)
+ _, err = core.ApplyMessage(evm, msg, gasPool)
+ if err != nil {
+ log.Warn("Transaction execution failed in pre-bundle simulation", "err", err)
+ }
+ }
+ }
+
+ // Now simulate the bundle
+ return s.SimulateBundle(bundle, baseBlock.Header(), simState, baseBlock.Coinbase())
+}
+
diff --git a/miner/worker.go b/miner/worker.go
index c0574eac2380..87e983a93f5f 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -466,6 +466,7 @@ func (miner *Miner) fillTransactions(interrupt *atomic.Int32, env *environment)
miner.confMu.RLock()
tip := miner.config.GasPrice
prio := miner.prio
+ strategy := miner.strategy
miner.confMu.RUnlock()
// Retrieve the pending transactions pre-filtered by the 1559/4844 dynamic fees
@@ -492,6 +493,29 @@ func (miner *Miner) fillTransactions(interrupt *atomic.Int32, env *environment)
}
pendingBlobTxs := miner.txpool.Pending(filter)
+ // Merge pending transactions
+ allPending := make(map[common.Address][]*txpool.LazyTransaction)
+ for addr, txs := range pendingPlainTxs {
+ allPending[addr] = txs
+ }
+ for addr, txs := range pendingBlobTxs {
+ allPending[addr] = append(allPending[addr], txs...)
+ }
+
+ // Get valid bundles for this block
+ bundles := miner.GetBundles(env.header.Number.Uint64())
+
+ // Use ordering strategy if not default
+ if _, isDefault := strategy.(*DefaultOrderingStrategy); !isDefault && strategy != nil {
+ orderedTxs, err := strategy.OrderTransactions(allPending, bundles, env.state, env.header)
+ if err != nil {
+ log.Warn("Custom ordering strategy failed, falling back to default", "err", err)
+ } else {
+ return miner.commitOrderedTransactions(env, orderedTxs, interrupt)
+ }
+ }
+
+ // Default ordering: priority transactions first, then bundles, then normal transactions
// Split the pending transactions into locals and remotes.
prioPlainTxs, normalPlainTxs := make(map[common.Address][]*txpool.LazyTransaction), pendingPlainTxs
prioBlobTxs, normalBlobTxs := make(map[common.Address][]*txpool.LazyTransaction), pendingBlobTxs
@@ -515,6 +539,12 @@ func (miner *Miner) fillTransactions(interrupt *atomic.Int32, env *environment)
return err
}
}
+
+ // Commit valid bundles
+ if err := miner.commitBundles(env, bundles, interrupt); err != nil {
+ log.Debug("Bundle commitment failed", "err", err)
+ }
+
if len(normalPlainTxs) > 0 || len(normalBlobTxs) > 0 {
plainTxs := newTransactionsByPriceAndNonce(env.signer, normalPlainTxs, env.header.BaseFee)
blobTxs := newTransactionsByPriceAndNonce(env.signer, normalBlobTxs, env.header.BaseFee)
@@ -526,6 +556,96 @@ func (miner *Miner) fillTransactions(interrupt *atomic.Int32, env *environment)
return nil
}
+// commitBundles attempts to commit bundles to the block.
+func (miner *Miner) commitBundles(env *environment, bundles []*Bundle, interrupt *atomic.Int32) error {
+ for _, bundle := range bundles {
+ // Validate bundle timestamp
+ if err := bundle.ValidateTimestamp(env.header.Time); err != nil {
+ continue
+ }
+
+ // Simulate bundle to check if it will succeed
+ snapshot := env.state.Snapshot()
+ simResult, err := miner.simulator.SimulateBundle(bundle, env.header, env.state, env.coinbase)
+ if err != nil || !simResult.Success {
+ env.state.RevertToSnapshot(snapshot)
+ continue
+ }
+
+ // Bundle simulation succeeded, commit each transaction
+ bundleSuccess := true
+ for i, tx := range bundle.Txs {
+ // Check interruption
+ if interrupt != nil {
+ if signal := interrupt.Load(); signal != commitInterruptNone {
+ env.state.RevertToSnapshot(snapshot)
+ return signalToErr(signal)
+ }
+ }
+
+ // Check gas limit
+ if env.gasPool.Gas() < tx.Gas() {
+ bundleSuccess = false
+ break
+ }
+
+ // Check block size
+ if !env.txFitsSize(tx) {
+ bundleSuccess = false
+ break
+ }
+
+ // Commit transaction
+ env.state.SetTxContext(tx.Hash(), env.tcount)
+ err := miner.commitTransaction(env, tx)
+
+ // Check if transaction failed and is not allowed to revert
+ if err != nil && !bundle.CanRevert(i) {
+ bundleSuccess = false
+ break
+ }
+ }
+
+ // If bundle failed, revert state
+ if !bundleSuccess {
+ env.state.RevertToSnapshot(snapshot)
+ }
+ }
+ return nil
+}
+
+// commitOrderedTransactions commits pre-ordered transactions to the block.
+func (miner *Miner) commitOrderedTransactions(env *environment, txs []*types.Transaction, interrupt *atomic.Int32) error {
+ for _, tx := range txs {
+ // Check interruption signal
+ if interrupt != nil {
+ if signal := interrupt.Load(); signal != commitInterruptNone {
+ return signalToErr(signal)
+ }
+ }
+
+ // Check gas and size constraints
+ if env.gasPool.Gas() < tx.Gas() {
+ log.Trace("Not enough gas left for transaction", "hash", tx.Hash(), "left", env.gasPool.Gas(), "needed", tx.Gas())
+ continue
+ }
+
+ if !env.txFitsSize(tx) {
+ break
+ }
+
+ // Commit transaction
+ env.state.SetTxContext(tx.Hash(), env.tcount)
+ err := miner.commitTransaction(env, tx)
+ if err != nil {
+ from, _ := types.Sender(env.signer, tx)
+ log.Debug("Transaction failed in ordered execution", "hash", tx.Hash(), "from", from, "err", err)
+ continue
+ }
+ }
+ return nil
+}
+
// totalFees computes total consumed miner fees in Wei. Block transactions and receipts have to have the same order.
func totalFees(block *types.Block, receipts []*types.Receipt) *big.Int {
feesWei := new(big.Int)
diff --git a/roadmap.md b/roadmap.md
new file mode 100644
index 000000000000..d0b4cf6c2b8c
--- /dev/null
+++ b/roadmap.md
@@ -0,0 +1,111 @@
+# Mandarin Roadmap
+
+## Objective
+Transform Mandarin into a low-latency execution client optimized for co-located trading operations, targeting microsecond-scale improvements over standard Geth.
+
+## Differentiation
+- **Erigon**: Database efficiency, archive node performance, disk I/O optimization
+- **Mandarin**: Real-time latency optimization for live trading and MEV operations
+
+## Phase 1: Foundation (2-4 weeks)
+
+### Custom Transaction Ordering
+- Modify `miner/worker.go` to accept external ordering functions
+- Add bundle support with revert protection
+- Export `OrderingStrategy` interface for pluggable algorithms
+- Risk: Low, isolated to block building
+
+### Binary RPC Layer
+- Implement gRPC service in `api/grpc/`
+- Core endpoints: `GetStorageBatch`, `SimulateBundles`, `SubmitBundle`, `GetPendingTransactions`
+- Target: 10x latency improvement over JSON-RPC
+- Risk: Low, runs alongside existing RPC
+
+### Benchmarking Framework
+- Establish baseline metrics vs vanilla Geth
+- Automated latency tracking: tx propagation, block import, simulation throughput, API latency
+- Continuous integration testing against mainnet replays
+
+## Phase 2: Mempool Fast Path (4-6 weeks)
+
+### Lock-Free Transaction Feed
+- Implement ring buffer in `core/txpool/fastfeed/`
+- Hook into txpool at `insertFeed.Send()` points
+- Binary layout for zero-copy access
+- Target: Sub-100μs tx propagation to consumers
+- Risk: Medium, touches critical path
+
+### Shared Memory Interface
+- Expose transaction events via mmap'd regions
+- Selective filtering by contract address or method selector
+- Support multiple concurrent consumers
+- Risk: Medium, IPC complexity
+
+## Phase 3: Hot State Cache (6-8 weeks)
+
+### DeFi Contract State Pinning
+- Maintain in-memory cache of top 100 pool/vault states
+- Pre-decode reserves, fees, and critical parameters
+- Update atomically on block import
+- Target: Sub-microsecond state access for hot contracts
+- Risk: High, state consistency is critical
+
+### State Delta Export
+- After each block, export structured diffs of watched addresses
+- Binary format for rapid consumption by trading strategies
+- Include pre and post-execution state
+- Risk: Medium
+
+### Integration Points
+- `core/blockchain.go`: Hook into `ProcessBlock()`
+- `core/state_processor.go`: Intercept state changes during EVM execution
+- Deploy in shadow mode initially to verify correctness
+
+## Phase 4: Native API (4-6 weeks)
+
+### Shared Library Interface
+- Build Mandarin as `libmandarin.so` with C ABI
+- Export simulation, state access, and bundle submission functions
+- Enable in-process trading strategies
+- Target: Sub-100μs API calls
+- Risk: Medium, ABI stability and versioning concerns
+
+### Safety Mechanisms
+- Process isolation boundaries
+- Memory protection
+- API rate limiting and resource quotas
+
+## Key Metrics
+
+### Target Improvements vs Baseline Geth
+- Transaction propagation (P99): 1.2ms → 45μs
+- Block import (avg): 120ms → 68ms
+- Simulation throughput: 200/sec → 15,000/sec
+- API latency (P50): 8ms → 12μs
+
+### Monitoring
+- Real-time latency percentiles (P50, P99, P999)
+- Memory overhead tracking
+- State consistency validation
+- Regression detection in CI
+
+## Out of Scope
+
+### Not Prioritized
+- Full kernel-bypass networking (DPDK/AF_XDP): Complex, limited benefit
+- Custom P2P protocol: Breaks compatibility
+- Core EVM rewrites: High maintenance burden, modest gains
+
+## Initial Validation
+
+### Quick Wins (First 2 Weeks)
+1. Bundle simulation endpoint using existing `eth_call` infrastructure
+2. gRPC implementation for 5 critical APIs
+3. Benchmark suite comparing to vanilla Geth
+4. Publish latency improvements to validate approach
+
+### Success Criteria
+- Measurable 5-10x latency improvements in Phase 1
+- Zero state consistency issues in hot cache
+- Production stability matching Geth reliability standards
+