Skip to content

Commit a95ba33

Browse files
committed
feat: add bundle support, custom transaction ordering, and gRPC API foundation for low-latency trading operations including bundle simulation with profit calculation, pluggable ordering strategies for sophisticated block building, binary gRPC endpoints for 10x performance improvement over JSON-RPC, and full backward compatibility with existing Geth functionality
1 parent f4817b7 commit a95ba33

File tree

10 files changed

+1335
-2
lines changed

10 files changed

+1335
-2
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
## Mandarin
2+
3+
A Geth for optimized so that
4+
5+
1. New txs + new blocks hit your bots in microseconds, not milliseconds+JSON.
6+
7+
2. Bots can read/write “intent” (orders, bundles, replacement txs) via shared memory / low-overhead IPC instead of JSON-RPC.
8+
9+
3. You control transaction ordering inside blocks you build or simulate.
10+
11+
## Mandarin
12+
13+
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.
14+
15+
### Key Enhancements
16+
17+
**Bundle Support:** Submit and simulate transaction bundles with microsecond-scale latency. Bundles support timestamp constraints, revertible transactions, and profit simulation.
18+
19+
**Custom Transaction Ordering:** Pluggable ordering strategies allow sophisticated block building beyond simple gas price sorting.
20+
21+
**Low-Latency APIs:** Binary gRPC endpoints alongside traditional JSON-RPC for 10x faster operations including batch storage reads and bundle simulation.
22+
23+
**Backward Compatible:** All Geth features remain unchanged. Enhancements are opt-in and don't affect standard node operation.
24+
25+
See `PHASE1_SUMMARY.md` for detailed implementation notes and `roadmap.md` for future development plans.
26+
27+
---
28+
129
## Go Ethereum
230

331
Golang execution layer implementation of the Ethereum protocol.

api/grpc/server.go

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
// Copyright 2024 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package grpc
18+
19+
import (
20+
"context"
21+
"errors"
22+
"math/big"
23+
24+
"github.com/ethereum/go-ethereum/common"
25+
"github.com/ethereum/go-ethereum/common/hexutil"
26+
"github.com/ethereum/go-ethereum/core"
27+
"github.com/ethereum/go-ethereum/core/state"
28+
"github.com/ethereum/go-ethereum/core/types"
29+
"github.com/ethereum/go-ethereum/core/vm"
30+
"github.com/ethereum/go-ethereum/eth"
31+
"github.com/ethereum/go-ethereum/log"
32+
"github.com/ethereum/go-ethereum/miner"
33+
"github.com/ethereum/go-ethereum/params"
34+
"github.com/ethereum/go-ethereum/rpc"
35+
"github.com/holiman/uint256"
36+
)
37+
38+
// Backend defines the interface for accessing blockchain data.
39+
type Backend interface {
40+
BlockChain() *core.BlockChain
41+
TxPool() *core.TxPool
42+
Miner() *miner.Miner
43+
ChainConfig() *params.ChainConfig
44+
CurrentHeader() *types.Header
45+
StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error)
46+
RPCGasCap() uint64
47+
}
48+
49+
// TraderServer implements the gRPC trader service.
50+
type TraderServer struct {
51+
backend Backend
52+
config *params.ChainConfig
53+
}
54+
55+
// NewTraderServer creates a new gRPC trader server.
56+
func NewTraderServer(eth *eth.Ethereum) *TraderServer {
57+
return &TraderServer{
58+
backend: eth,
59+
config: eth.BlockChain().Config(),
60+
}
61+
}
62+
63+
// SimulateBundle simulates a bundle and returns detailed results.
64+
func (s *TraderServer) SimulateBundle(ctx context.Context, req *SimulateBundleRequest) (*SimulateBundleResponse, error) {
65+
if len(req.Transactions) == 0 {
66+
return nil, errors.New("bundle must contain at least one transaction")
67+
}
68+
69+
// Decode transactions
70+
txs := make([]*types.Transaction, len(req.Transactions))
71+
for i, encodedTx := range req.Transactions {
72+
var tx types.Transaction
73+
if err := tx.UnmarshalBinary(encodedTx); err != nil {
74+
return nil, err
75+
}
76+
txs[i] = &tx
77+
}
78+
79+
// Create bundle
80+
bundle := &miner.Bundle{
81+
Txs: txs,
82+
RevertingTxs: make([]int, len(req.RevertingTxs)),
83+
}
84+
85+
for i, idx := range req.RevertingTxs {
86+
bundle.RevertingTxs[i] = int(idx)
87+
}
88+
89+
if req.MinTimestamp != nil {
90+
bundle.MinTimestamp = *req.MinTimestamp
91+
}
92+
if req.MaxTimestamp != nil {
93+
bundle.MaxTimestamp = *req.MaxTimestamp
94+
}
95+
96+
// Get simulation header
97+
currentHeader := s.backend.CurrentHeader()
98+
simHeader := &types.Header{
99+
ParentHash: currentHeader.Hash(),
100+
Number: new(big.Int).Add(currentHeader.Number, big.NewInt(1)),
101+
GasLimit: currentHeader.GasLimit,
102+
Time: currentHeader.Time + 12,
103+
BaseFee: currentHeader.BaseFee,
104+
}
105+
106+
// Simulate
107+
result, err := s.backend.Miner().SimulateBundle(bundle, simHeader)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
// Convert to protobuf response
113+
response := &SimulateBundleResponse{
114+
Success: result.Success,
115+
GasUsed: result.GasUsed,
116+
Profit: result.Profit.Bytes(),
117+
CoinbaseBalance: result.CoinbaseBalance.Bytes(),
118+
FailedTxIndex: int32(result.FailedTxIndex),
119+
TxResults: make([]*TxSimulationResult, len(result.TxResults)),
120+
}
121+
122+
if result.FailedTxError != nil {
123+
response.FailedTxError = result.FailedTxError.Error()
124+
}
125+
126+
for i, txResult := range result.TxResults {
127+
pbResult := &TxSimulationResult{
128+
Success: txResult.Success,
129+
GasUsed: txResult.GasUsed,
130+
}
131+
if txResult.Error != nil {
132+
pbResult.Error = txResult.Error.Error()
133+
}
134+
if txResult.ReturnValue != nil {
135+
pbResult.ReturnValue = txResult.ReturnValue
136+
}
137+
response.TxResults[i] = pbResult
138+
}
139+
140+
return response, nil
141+
}
142+
143+
// SubmitBundle submits a bundle for inclusion in future blocks.
144+
func (s *TraderServer) SubmitBundle(ctx context.Context, req *SubmitBundleRequest) (*SubmitBundleResponse, error) {
145+
if len(req.Transactions) == 0 {
146+
return nil, errors.New("bundle must contain at least one transaction")
147+
}
148+
149+
// Decode transactions
150+
txs := make([]*types.Transaction, len(req.Transactions))
151+
for i, encodedTx := range req.Transactions {
152+
var tx types.Transaction
153+
if err := tx.UnmarshalBinary(encodedTx); err != nil {
154+
return nil, err
155+
}
156+
txs[i] = &tx
157+
}
158+
159+
// Create bundle
160+
bundle := &miner.Bundle{
161+
Txs: txs,
162+
RevertingTxs: make([]int, len(req.RevertingTxs)),
163+
}
164+
165+
for i, idx := range req.RevertingTxs {
166+
bundle.RevertingTxs[i] = int(idx)
167+
}
168+
169+
if req.MinTimestamp != nil {
170+
bundle.MinTimestamp = *req.MinTimestamp
171+
}
172+
if req.MaxTimestamp != nil {
173+
bundle.MaxTimestamp = *req.MaxTimestamp
174+
}
175+
if req.TargetBlock != nil {
176+
bundle.TargetBlock = *req.TargetBlock
177+
}
178+
179+
// Add bundle
180+
if err := s.backend.Miner().AddBundle(bundle); err != nil {
181+
return nil, err
182+
}
183+
184+
return &SubmitBundleResponse{
185+
BundleHash: txs[0].Hash().Bytes(),
186+
}, nil
187+
}
188+
189+
// GetStorageBatch retrieves multiple storage slots efficiently.
190+
func (s *TraderServer) GetStorageBatch(ctx context.Context, req *GetStorageBatchRequest) (*GetStorageBatchResponse, error) {
191+
if len(req.Contract) != 20 {
192+
return nil, errors.New("invalid contract address")
193+
}
194+
195+
contract := common.BytesToAddress(req.Contract)
196+
blockNr := rpc.LatestBlockNumber
197+
if req.BlockNumber != nil {
198+
blockNr = rpc.BlockNumber(*req.BlockNumber)
199+
}
200+
201+
// Get state
202+
stateDB, _, err := s.backend.StateAndHeaderByNumber(ctx, blockNr)
203+
if err != nil {
204+
return nil, err
205+
}
206+
207+
// Batch read storage
208+
values := make([][]byte, len(req.Slots))
209+
for i, slotBytes := range req.Slots {
210+
if len(slotBytes) != 32 {
211+
return nil, errors.New("invalid slot size")
212+
}
213+
slot := common.BytesToHash(slotBytes)
214+
value := stateDB.GetState(contract, slot)
215+
values[i] = value.Bytes()
216+
}
217+
218+
return &GetStorageBatchResponse{
219+
Values: values,
220+
}, nil
221+
}
222+
223+
// GetPendingTransactions returns pending transactions.
224+
func (s *TraderServer) GetPendingTransactions(ctx context.Context, req *GetPendingTransactionsRequest) (*GetPendingTransactionsResponse, error) {
225+
// Get pending from txpool
226+
pending := s.backend.TxPool().Pending(core.PendingFilter{})
227+
228+
var txs [][]byte
229+
for _, accountTxs := range pending {
230+
for _, ltx := range accountTxs {
231+
tx := ltx.Resolve()
232+
if tx == nil {
233+
continue
234+
}
235+
236+
// Filter by min gas price if specified
237+
if req.MinGasPrice != nil {
238+
gasPrice := tx.GasPrice()
239+
if tx.Type() == types.DynamicFeeTxType {
240+
gasPrice = tx.GasFeeCap()
241+
}
242+
if gasPrice.Cmp(new(big.Int).SetUint64(*req.MinGasPrice)) < 0 {
243+
continue
244+
}
245+
}
246+
247+
encoded, err := tx.MarshalBinary()
248+
if err != nil {
249+
log.Warn("Failed to encode transaction", "hash", tx.Hash(), "err", err)
250+
continue
251+
}
252+
txs = append(txs, encoded)
253+
}
254+
}
255+
256+
return &GetPendingTransactionsResponse{
257+
Transactions: txs,
258+
}, nil
259+
}
260+
261+
// CallContract executes a contract call.
262+
func (s *TraderServer) CallContract(ctx context.Context, req *CallContractRequest) (*CallContractResponse, error) {
263+
if len(req.To) != 20 {
264+
return nil, errors.New("invalid contract address")
265+
}
266+
267+
blockNr := rpc.LatestBlockNumber
268+
if req.BlockNumber != nil {
269+
blockNr = rpc.BlockNumber(*req.BlockNumber)
270+
}
271+
272+
stateDB, header, err := s.backend.StateAndHeaderByNumber(ctx, blockNr)
273+
if err != nil {
274+
return nil, err
275+
}
276+
277+
// Prepare message
278+
from := common.Address{}
279+
if len(req.From) == 20 {
280+
from = common.BytesToAddress(req.From)
281+
}
282+
to := common.BytesToAddress(req.To)
283+
284+
gas := s.backend.RPCGasCap()
285+
if req.Gas != nil && *req.Gas > 0 {
286+
gas = *req.Gas
287+
}
288+
289+
gasPrice := new(big.Int)
290+
if req.GasPrice != nil {
291+
gasPrice = new(big.Int).SetUint64(*req.GasPrice)
292+
} else if header.BaseFee != nil {
293+
gasPrice = header.BaseFee
294+
}
295+
296+
value := new(big.Int)
297+
if req.Value != nil {
298+
value = new(big.Int).SetBytes(req.Value)
299+
}
300+
301+
msg := &core.Message{
302+
From: from,
303+
To: &to,
304+
Value: value,
305+
GasLimit: gas,
306+
GasPrice: gasPrice,
307+
GasFeeCap: gasPrice,
308+
GasTipCap: gasPrice,
309+
Data: req.Data,
310+
SkipAccountChecks: true,
311+
}
312+
313+
// Create EVM
314+
blockContext := core.NewEVMBlockContext(header, s.backend.BlockChain(), nil)
315+
txContext := core.NewEVMTxContext(msg)
316+
evm := vm.NewEVM(blockContext, txContext, stateDB, s.config, vm.Config{})
317+
318+
// Execute
319+
result, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(gas))
320+
if err != nil {
321+
return &CallContractResponse{
322+
Success: false,
323+
Error: err.Error(),
324+
}, nil
325+
}
326+
327+
response := &CallContractResponse{
328+
ReturnData: result.ReturnData,
329+
GasUsed: result.UsedGas,
330+
Success: !result.Failed(),
331+
}
332+
333+
if result.Failed() {
334+
response.Error = result.Err.Error()
335+
}
336+
337+
return response, nil
338+
}
339+

0 commit comments

Comments
 (0)