diff --git a/suave/builder/api/api.go b/suave/builder/api/api.go index 773d7086af..8e08280c2b 100644 --- a/suave/builder/api/api.go +++ b/suave/builder/api/api.go @@ -33,4 +33,5 @@ type API interface { NewSession(ctx context.Context, args *BuildBlockArgs) (string, error) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) AddBundle(ctx context.Context, sessionId string, bundle Bundle) error + BuildBlock(ctx context.Context, sessionId string) error } diff --git a/suave/builder/api/api_client.go b/suave/builder/api/api_client.go index 62631df8b4..d781c642d5 100644 --- a/suave/builder/api/api_client.go +++ b/suave/builder/api/api_client.go @@ -44,3 +44,7 @@ func (a *APIClient) AddTransaction(ctx context.Context, sessionId string, tx *ty func (a *APIClient) AddBundle(ctx context.Context, sessionId string, bundle Bundle) error { return a.rpc.CallContext(ctx, nil, "suavex_addBundle", sessionId, bundle) } + +func (a *APIClient) BuildBlock(ctx context.Context, sessionId string) error { + return a.rpc.CallContext(ctx, nil, "suavex_buildBlock", sessionId) +} diff --git a/suave/builder/api/api_server.go b/suave/builder/api/api_server.go index eb598270d9..f6eeb80127 100644 --- a/suave/builder/api/api_server.go +++ b/suave/builder/api/api_server.go @@ -11,6 +11,7 @@ type SessionManager interface { NewSession(context.Context, *BuildBlockArgs) (string, error) AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) AddBundle(sessionId string, bundle Bundle) error + BuildBlock(sessionId string) error } func NewServer(s SessionManager) *Server { @@ -36,6 +37,10 @@ func (s *Server) AddBundle(ctx context.Context, sessionId string, bundle Bundle) return s.sessionMngr.AddBundle(sessionId, bundle) } +func (s *Server) BuildBlock(ctx context.Context, sessionId string) error { + return s.sessionMngr.BuildBlock(sessionId) +} + type MockServer struct { } @@ -50,3 +55,7 @@ func (s *MockServer) AddTransaction(ctx context.Context, sessionId string, tx *t func (s *MockServer) AddBundle(ctx context.Context, sessionId string, bundle Bundle) error { return nil } + +func (s *MockServer) BuildBlock(ctx context.Context) error { + return nil +} diff --git a/suave/builder/api/api_test.go b/suave/builder/api/api_test.go index 356ff0d083..66ab9ec8f7 100644 --- a/suave/builder/api/api_test.go +++ b/suave/builder/api/api_test.go @@ -41,3 +41,7 @@ func (nullSessionManager) AddTransaction(sessionId string, tx *types.Transaction func (nullSessionManager) AddBundle(sessionId string, bundle Bundle) error { return nil } + +func (nullSessionManager) BuildBlock(sessionId string) error { + return nil +} diff --git a/suave/builder/builder.go b/suave/builder/builder.go index b471f7a949..83c695f87a 100644 --- a/suave/builder/builder.go +++ b/suave/builder/builder.go @@ -1,9 +1,13 @@ package builder import ( + "fmt" "math/big" + "sync/atomic" + "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" @@ -14,20 +18,31 @@ import ( ) type builder struct { - config *builderConfig - txns []*types.Transaction - receipts []*types.Receipt - state *state.StateDB - gasPool *core.GasPool - gasUsed *uint64 - signer types.Signer + config *builderConfig + txns []*types.Transaction + receipts []*types.Receipt + state *state.StateDB + gasPool *core.GasPool + gasUsed *uint64 + signer types.Signer + args api.BuildBlockArgs + coinbasePreBalance *big.Int + engine consensus.Engine } type builderConfig struct { - preState *state.StateDB - header *types.Header - config *params.ChainConfig - context core.ChainContext + preState *state.StateDB + header *types.Header + config *params.ChainConfig + context core.ChainContext + chainReader consensus.ChainHeaderReader + + // newpayloadTimeout is the maximum timeout allowance for creating payload. + // The default value is 2 seconds but node operator can set it to arbitrary + // large value. A large timeout allowance may cause Geth to fail creating + // a non-empty payload within the specified time and eventually miss the slot + // in case there are some computation expensive transactions in txpool. + newpayloadTimeout time.Duration } func newBuilder(config *builderConfig) *builder { @@ -35,11 +50,12 @@ func newBuilder(config *builderConfig) *builder { var gasUsed uint64 return &builder{ - config: config, - state: config.preState.Copy(), - gasPool: &gp, - gasUsed: &gasUsed, - signer: types.MakeSigner(config.config, config.header.Number, config.header.Time), + config: config, + state: config.preState.Copy(), + gasPool: &gp, + gasUsed: &gasUsed, + signer: types.MakeSigner(config.config, config.header.Number, config.header.Time), + coinbasePreBalance: config.preState.GetBalance(config.header.Coinbase), } } @@ -171,3 +187,89 @@ func (b *builder) AddTransaction(txn *types.Transaction) (*types.SimulateTransac return result, nil } + +func (b *builder) commitPendingTxs() error { + interrupt := new(atomic.Int32) + timer := time.AfterFunc(b.config.newpayloadTimeout, func() { + interrupt.Store(commitInterruptTimeout) + }) + defer timer.Stop() + if err := b.fillTransactions(); err != nil { + return err + } + return nil +} + +func (b *builder) fillTransactions() error { + // Split the pending transactions into locals and remotes + // Fill the block with all available pending transactions. + pending := w.eth.TxPool().Pending(true) + localTxs, remoteTxs := make(map[common.Address]types.Transactions), pending + for _, account := range w.eth.TxPool().Locals() { + if txs := remoteTxs[account]; len(txs) > 0 { + delete(remoteTxs, account) + localTxs[account] = txs + } + } + if len(localTxs) > 0 { + txs := types.NewTransactionsByPriceAndNonce(env.signer, localTxs, env.header.BaseFee) + if err := b.commitTransactions(env, txs, interrupt); err != nil { + return err + } + } + if len(remoteTxs) > 0 { + txs := types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, env.header.BaseFee) + if err := w.commitTransactions(env, txs, interrupt); err != nil { + return err + } + } + return nil +} + +func (b *builder) BuildBlock() error { + if b.args.FillPending { + if err := b.commitPendingTxs(); err != nil { + return err + } + } + + // create ephemeral addr and private key for payment txn + ephemeralPrivKey, err := crypto.GenerateKey() + if err != nil { + return err + } + ephemeralAddr := crypto.PubkeyToAddress(ephemeralPrivKey.PublicKey) + + // Assume static 28000 gas transfers for both mev-share and proposer payments + refundTransferCost := new(big.Int).Mul(big.NewInt(28000), b.config.header.BaseFee) + + profitPost := b.state.GetBalance(b.config.header.Coinbase) + proposerProfit := new(big.Int).Set(profitPost) // = post-pre-transfer_cost + proposerProfit = proposerProfit.Sub(profitPost, b.coinbasePreBalance) + proposerProfit = proposerProfit.Sub(proposerProfit, refundTransferCost) + + currNonce := b.state.GetNonce(ephemeralAddr) + paymentTx, err := types.SignTx(types.NewTx(&types.LegacyTx{ + Nonce: currNonce, + To: &b.args.FeeRecipient, + Value: proposerProfit, + Gas: 28000, + GasPrice: b.config.header.BaseFee, + }), b.signer, ephemeralPrivKey) + if err != nil { + return fmt.Errorf("could not sign proposer payment: %w", err) + } + + // commit payment txn + if _, err := b.AddTransaction(paymentTx); err != nil { + return err + } + + block, err := b.engine.FinalizeAndAssemble(b.config.chainReader, b.config.header, b.state, b.txns, []*types.Header{}, b.receipts, b.args.Withdrawals) + if err != nil { + return err + } + + fmt.Println("-- block --", block) + return nil +} diff --git a/suave/builder/session_manager.go b/suave/builder/session_manager.go index 194bd9067d..6ed89f288e 100644 --- a/suave/builder/session_manager.go +++ b/suave/builder/session_manager.go @@ -171,6 +171,14 @@ func (s *SessionManager) AddBundle(sessionId string, bundle api.Bundle) error { return builder.AddBundle(bundle) } +func (s *SessionManager) BuildBlock(sessionId string) error { + builder, err := s.getSession(sessionId) + if err != nil { + return err + } + return builder.BuildBlock() +} + // CalcBaseFee calculates the basefee of the header. func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { // If the current block is the first EIP-1559 block, return the InitialBaseFee.