diff --git a/miner/builder.go b/miner/builder.go index 578e2a09e5..ab9080be5f 100644 --- a/miner/builder.go +++ b/miner/builder.go @@ -22,6 +22,13 @@ import ( "github.com/holiman/uint256" ) +var ( + ErrInvalidInclusionRange = errors.New("invalid inclusion range") + ErrInvalidBlockNumber = errors.New("invalid block number") + ErrExceedsMaxBlock = errors.New("block number exceeds max block") + ErrEmptyTxs = errors.New("empty transactions") +) + type BuilderConfig struct { ChainConfig *params.ChainConfig Engine consensus.Engine @@ -77,26 +84,92 @@ func NewBuilder(config *BuilderConfig, args *BuilderArgs) (*Builder, error) { return b, nil } -type SBundle struct { - BlockNumber *big.Int `json:"blockNumber,omitempty"` // if BlockNumber is set it must match DecryptionCondition! - MaxBlock *big.Int `json:"maxBlock,omitempty"` - Txs types.Transactions `json:"txs"` - RevertingHashes []common.Hash `json:"revertingHashes,omitempty"` - RefundPercent *int `json:"percent,omitempty"` -} - -func (b *Builder) AddTransaction(txn *types.Transaction) (*suavextypes.SimulateTransactionResult, error) { +func (b *Builder) addTransaction(txn *types.Transaction, env *environment) (*suavextypes.SimulateTransactionResult, error) { // If the context is not set, the logs will not be recorded b.env.state.SetTxContext(txn.Hash(), b.env.tcount) - logs, err := b.wrk.commitTransaction(b.env, txn) + prevGas := env.header.GasUsed + logs, err := b.wrk.commitTransaction(env, txn) if err != nil { return &suavextypes.SimulateTransactionResult{ Error: err.Error(), Success: false, - }, nil + }, err } - return receiptToSimResult(&types.Receipt{Logs: logs}), nil + egp := env.header.GasUsed - prevGas + return receiptToSimResult(&types.Receipt{Logs: logs}, egp), nil +} + +func (b *Builder) AddTransaction(txn *types.Transaction) (*suavextypes.SimulateTransactionResult, error) { + res, _ := b.addTransaction(txn, b.env) + return res, nil +} + +func (b *Builder) AddTransactions(txns types.Transactions) ([]*suavextypes.SimulateTransactionResult, error) { + results := make([]*suavextypes.SimulateTransactionResult, 0) + snap := b.env.copy() + + for _, txn := range txns { + res, err := b.addTransaction(txn, snap) + results = append(results, res) + if err != nil { + return results, nil + } + } + b.env = snap + return results, nil +} + +func (b *Builder) addBundle(bundle *suavextypes.Bundle, env *environment) (*suavextypes.SimulateBundleResult, error) { + if err := checkBundleParams(b.env.header.Number, bundle); err != nil { + return &suavextypes.SimulateBundleResult{ + Error: err.Error(), + Success: false, + }, err + } + + revertingHashes := bundle.RevertingHashesMap() + egp := uint64(0) + + var results []*suavextypes.SimulateTransactionResult + for _, txn := range bundle.Txs { + result, err := b.addTransaction(txn, env) + results = append(results, result) + if err != nil { + if _, ok := revertingHashes[txn.Hash()]; ok { + // continue if the transaction is in the reverting hashes + continue + } + return &suavextypes.SimulateBundleResult{ + Error: err.Error(), + SimulateTransactionResults: results, + Success: false, + }, err + } + egp += result.Egp + } + + return &suavextypes.SimulateBundleResult{ + Egp: egp, + SimulateTransactionResults: results, + Success: true, + }, nil +} + +func (b *Builder) AddBundles(bundles []*suavextypes.Bundle) ([]*suavextypes.SimulateBundleResult, error) { + var results []*suavextypes.SimulateBundleResult + snap := b.env.copy() + + for _, bundle := range bundles { + result, err := b.addBundle(bundle, snap) + results = append(results, result) + if err != nil { + return results, nil + } + } + + b.env = snap + return results, nil } func (b *Builder) FillPending() error { @@ -139,8 +212,8 @@ func (b *Builder) Bid(builderPubKey phase0.BLSPubKey) (*suavextypes.SubmitBlockR blockBidMsg := builderV1.BidTrace{ Slot: b.args.Slot, - ParentHash: phase0.Hash32(payload.ParentHash), - BlockHash: phase0.Hash32(payload.BlockHash), + ParentHash: payload.ParentHash, + BlockHash: payload.BlockHash, BuilderPubkey: builderPubKey, ProposerPubkey: phase0.BLSPubKey(proposerPubkey), ProposerFeeRecipient: bellatrix.ExecutionAddress(b.args.FeeRecipient), @@ -169,8 +242,9 @@ func (b *Builder) Bid(builderPubKey phase0.BLSPubKey) (*suavextypes.SubmitBlockR return &bidRequest, nil } -func receiptToSimResult(receipt *types.Receipt) *suavextypes.SimulateTransactionResult { +func receiptToSimResult(receipt *types.Receipt, egp uint64) *suavextypes.SimulateTransactionResult { result := &suavextypes.SimulateTransactionResult{ + Egp: egp, Success: true, Logs: []*suavextypes.SimulatedLog{}, } @@ -223,3 +297,33 @@ func executableDataToDenebExecutionPayload(data *engine.ExecutableData) (*deneb. Withdrawals: withdrawalData, }, nil } + +func checkBundleParams(currentBlockNumber *big.Int, bundle *suavextypes.Bundle) error { + if bundle.BlockNumber != nil && bundle.MaxBlock != nil && bundle.BlockNumber.Cmp(bundle.MaxBlock) > 0 { + return ErrInvalidInclusionRange + } + + // check inclusion target if BlockNumber is set + if bundle.BlockNumber != nil { + if bundle.MaxBlock == nil && currentBlockNumber.Cmp(bundle.BlockNumber) != 0 { + return ErrInvalidBlockNumber + } + + if bundle.MaxBlock != nil { + if currentBlockNumber.Cmp(bundle.MaxBlock) > 0 { + return ErrExceedsMaxBlock + } + + if currentBlockNumber.Cmp(bundle.BlockNumber) < 0 { + return ErrInvalidBlockNumber + } + } + } + + // check if the bundle has transactions + if bundle.Txs == nil || bundle.Txs.Len() == 0 { + return ErrEmptyTxs + } + + return nil +} diff --git a/miner/builder_test.go b/miner/builder_test.go index d0e89ff3d4..423094cc3b 100644 --- a/miner/builder_test.go +++ b/miner/builder_test.go @@ -12,9 +12,12 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/clique" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + suavextypes "github.com/ethereum/go-ethereum/suave/builder/api" "github.com/stretchr/testify/require" ) @@ -25,12 +28,13 @@ func TestBuilder_AddTxn_Simple(t *testing.T) { builder, err := NewBuilder(config, &BuilderArgs{}) require.NoError(t, err) - tx1 := backend.newRandomTx(true) + tx1 := backend.newRandomTx(false) res, err := builder.AddTransaction(tx1) require.NoError(t, err) require.True(t, res.Success) require.Len(t, builder.env.receipts, 1) + require.Equal(t, big.NewInt(1000), builder.env.state.GetBalance(testUserAddress)) // we cannot add the same transaction again. Note that by design the // function does not error but returns the SimulateTransactionResult.success = false @@ -38,6 +42,150 @@ func TestBuilder_AddTxn_Simple(t *testing.T) { require.NoError(t, err) require.False(t, res.Success) require.Len(t, builder.env.receipts, 1) + require.Equal(t, big.NewInt(1000), builder.env.state.GetBalance(testUserAddress)) +} + +func TestBuilder_AddTxns_Simple(t *testing.T) { + t.Parallel() + config, backend := newMockBuilderConfig(t) + builder, err := NewBuilder(config, &BuilderArgs{}) + require.NoError(t, err) + + tx1 := backend.newRandomTx(false) + tx2 := backend.newRandomTxWithNonce(1) + + res, err := builder.AddTransactions([]*types.Transaction{tx1, tx2}) + require.NoError(t, err) + require.Len(t, res, 2) + for _, r := range res { + require.True(t, r.Success) + } + require.Equal(t, big.NewInt(2000), builder.env.state.GetBalance(testUserAddress)) + + tx3 := backend.newRandomTxWithNonce(2) + tx4 := backend.newRandomTxWithNonce(1000) // fails with nonce too high + + res, err = builder.AddTransactions([]*types.Transaction{tx3, tx4}) + require.NoError(t, err) + require.Len(t, res, 2) + require.True(t, res[0].Success) + require.False(t, res[1].Success) + require.Len(t, builder.env.txs, 2) + require.Equal(t, big.NewInt(2000), builder.env.state.GetBalance(testUserAddress)) +} + +func TestBuilder_AddBundles_Simple(t *testing.T) { + t.Parallel() + config, backend := newMockBuilderConfig(t) + builder, err := NewBuilder(config, &BuilderArgs{}) + require.NoError(t, err) + + tx1 := backend.newRandomTx(false) + tx2 := backend.newRandomTxWithNonce(1) + + bundle1 := &suavextypes.Bundle{ + Txs: []*types.Transaction{tx1, tx2}, + } + + tx3 := backend.newRandomTxWithNonce(2) + tx4 := backend.newRandomTxWithNonce(3) + + bundle2 := &suavextypes.Bundle{ + Txs: []*types.Transaction{tx3, tx4}, + } + + res, err := builder.AddBundles([]*suavextypes.Bundle{bundle1, bundle2}) + require.NoError(t, err) + require.Len(t, res, 2) + require.True(t, res[0].Success) + require.True(t, res[1].Success) + require.Equal(t, big.NewInt(4000), builder.env.state.GetBalance(testUserAddress)) +} + +func TestBuilder_AddBundles_RevertHashes(t *testing.T) { + t.Parallel() + config, backend := newMockBuilderConfig(t) + builder, err := NewBuilder(config, &BuilderArgs{}) + require.NoError(t, err) + + tx1 := backend.newRandomTx(false) + tx2 := backend.newRandomTxWithNonce(3) // fails with nonce too high + + bundle := &suavextypes.Bundle{ + Txs: []*types.Transaction{tx1, tx2}, + } + + res, err := builder.AddBundles([]*suavextypes.Bundle{bundle}) + require.NoError(t, err) + require.Len(t, res, 1) + require.False(t, res[0].Success) + require.Len(t, res[0].SimulateTransactionResults, 2) + require.True(t, res[0].SimulateTransactionResults[0].Success) + require.False(t, res[0].SimulateTransactionResults[1].Success) + require.Equal(t, big.NewInt(0), builder.env.state.GetBalance(testUserAddress)) + + bundle.RevertingHashes = []common.Hash{tx2.Hash()} + + res, err = builder.AddBundles([]*suavextypes.Bundle{bundle}) + require.NoError(t, err) + require.Len(t, res, 1) + require.True(t, res[0].Success) + require.Len(t, res[0].SimulateTransactionResults, 2) + require.True(t, res[0].SimulateTransactionResults[0].Success) + require.False(t, res[0].SimulateTransactionResults[1].Success) + require.Equal(t, big.NewInt(1000), builder.env.state.GetBalance(testUserAddress)) +} + +func TestBuilder_AddBundles_InvalidParams(t *testing.T) { + t.Parallel() + config, backend := newMockBuilderConfig(t) + // set builder target block number to 10 + backend.insertRandomBlocks(9) + + builder, err := NewBuilder(config, &BuilderArgs{}) + require.Equal(t, uint64(10), builder.env.header.Number.Uint64()) + require.NoError(t, err) + + tx1 := backend.newRandomTx(false) + tx2 := backend.newRandomTx(false) + + bundle := &suavextypes.Bundle{ + Txs: []*types.Transaction{tx1, tx2}, + BlockNumber: big.NewInt(20), + } + + res, err := builder.AddBundles([]*suavextypes.Bundle{bundle}) + require.NoError(t, err) + require.Len(t, res, 1) + require.False(t, res[0].Success) + require.Equal(t, ErrInvalidBlockNumber.Error(), res[0].Error) + require.Len(t, res[0].SimulateTransactionResults, 0) + require.Equal(t, big.NewInt(0), builder.env.state.GetBalance(testUserAddress)) + + bundle = &suavextypes.Bundle{ + Txs: []*types.Transaction{tx1, tx2}, + BlockNumber: big.NewInt(5), + MaxBlock: big.NewInt(6), + } + + res, err = builder.AddBundles([]*suavextypes.Bundle{bundle}) + require.NoError(t, err) + require.Len(t, res, 1) + require.False(t, res[0].Success) + require.Equal(t, ErrExceedsMaxBlock.Error(), res[0].Error) + require.Len(t, res[0].SimulateTransactionResults, 0) + require.Equal(t, big.NewInt(0), builder.env.state.GetBalance(testUserAddress)) + + bundle = &suavextypes.Bundle{ + Txs: []*types.Transaction{}, + } + + res, err = builder.AddBundles([]*suavextypes.Bundle{bundle}) + require.NoError(t, err) + require.False(t, res[0].Success) + require.Equal(t, ErrEmptyTxs.Error(), res[0].Error) + require.Len(t, res[0].SimulateTransactionResults, 0) + require.Equal(t, big.NewInt(0), builder.env.state.GetBalance(testUserAddress)) } func TestBuilder_FillTransactions(t *testing.T) { @@ -118,10 +266,8 @@ func TestBuilder_Bid(t *testing.T) { _, err = builder.BuildBlock() require.NoError(t, err) - req, err := builder.Bid([48]byte{}) + _, err = builder.Bid([48]byte{}) require.NoError(t, err) - - fmt.Println("-- req --", req) } func newMockBuilderConfig(t *testing.T) (*BuilderConfig, *testWorkerBackend) { @@ -145,6 +291,49 @@ func newMockBuilderConfig(t *testing.T) (*BuilderConfig, *testWorkerBackend) { return bConfig, backend } +func (b *testWorkerBackend) newRandomTxWithNonce(nonce uint64) *types.Transaction { + gasPrice := big.NewInt(10 * params.InitialBaseFee) + tx, _ := types.SignTx(types.NewTransaction(nonce, testUserAddress, big.NewInt(1000), params.TxGas, gasPrice, nil), types.HomesteadSigner{}, testBankKey) + return tx +} + +func (b *testWorkerBackend) insertRandomBlocks(n int) []*types.Block { + extraVanity := 32 + extraSeal := crypto.SignatureLength + diffInTurn := big.NewInt(2) + signer := new(types.HomesteadSigner) + _, blocks, _ := core.GenerateChainWithGenesis(b.genesis, b.chain.Engine(), n, func(i int, block *core.BlockGen) { + block.SetDifficulty(big.NewInt(2)) // diffInTurn + + if i != 1 { + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testBankAddress), common.Address{0x00}, new(big.Int), params.TxGas, block.BaseFee(), nil), signer, testBankKey) + if err != nil { + panic(err) + } + block.AddTxWithChain(b.chain, tx) + } + }) + + for i, block := range blocks { + header := block.Header() + if i > 0 { + header.ParentHash = blocks[i-1].Hash() + } + header.Extra = make([]byte, extraVanity+extraSeal) + header.Difficulty = diffInTurn + + sig, _ := crypto.Sign(clique.SealHash(header).Bytes(), testBankKey) + copy(header.Extra[len(header.Extra)-extraSeal:], sig) + blocks[i] = block.WithSeal(header) + } + + if _, err := b.chain.InsertChain(blocks); err != nil { + panic(fmt.Sprintf("failed to insert initial blocks: %v", err)) + } + + return blocks +} + func (b *testWorkerBackend) newCall(to common.Address, data []byte) *types.Transaction { gasPrice := big.NewInt(10 * params.InitialBaseFee) tx, _ := types.SignTx(types.NewTransaction(b.txPool.Nonce(testBankAddress), to, big.NewInt(0), 1000000, gasPrice, data), types.HomesteadSigner{}, testBankKey) diff --git a/suave/builder/api/api.go b/suave/builder/api/api.go index 41376fede6..34da1e2e8a 100644 --- a/suave/builder/api/api.go +++ b/suave/builder/api/api.go @@ -24,6 +24,14 @@ type Bundle struct { RefundPercent *int `json:"percent,omitempty"` } +func (bundle *Bundle) RevertingHashesMap() map[common.Hash]struct{} { + m := make(map[common.Hash]struct{}) + for _, hash := range bundle.RevertingHashes { + m[hash] = struct{}{} + } + return m +} + type BuildBlockArgs struct { Slot uint64 `json:"slot"` ProposerPubkey []byte `json:"proposerPubkey"` @@ -52,6 +60,13 @@ type SimulateTransactionResult struct { Error string `json:"error"` } +type SimulateBundleResult struct { + Egp uint64 `json:"egp"` + SimulateTransactionResults []*SimulateTransactionResult `json:"simulateTransactionResults"` + Success bool `json:"success"` + Error string `json:"error"` +} + // field type overrides for gencodec type simulateTransactionResultMarshaling struct { Egp hexutil.Uint64 @@ -77,6 +92,8 @@ type SubmitBlockRequest struct { type API interface { NewSession(ctx context.Context, args *BuildBlockArgs) (string, error) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*SimulateTransactionResult, error) + AddTransactions(ctx context.Context, sessionId string, txs types.Transactions) ([]*SimulateTransactionResult, error) + AddBundles(ctx context.Context, sessionId string, bundles []*Bundle) ([]*SimulateBundleResult, error) BuildBlock(ctx context.Context, sessionId string) error Bid(ctx context.Context, sessioId string, blsPubKey phase0.BLSPubKey) (*SubmitBlockRequest, error) } diff --git a/suave/builder/api/api_client.go b/suave/builder/api/api_client.go index 9f75c04a62..49e49ad8d8 100644 --- a/suave/builder/api/api_client.go +++ b/suave/builder/api/api_client.go @@ -42,6 +42,18 @@ func (a *APIClient) AddTransaction(ctx context.Context, sessionId string, tx *ty return receipt, err } +func (a *APIClient) AddTransactions(ctx context.Context, sessionId string, txs types.Transactions) ([]*SimulateTransactionResult, error) { + var receipt []*SimulateTransactionResult + err := a.rpc.CallContext(ctx, &receipt, "suavex_addTransactions", sessionId, txs) + return receipt, err +} + +func (a *APIClient) AddBundles(ctx context.Context, sessionId string, bundles []*Bundle) ([]*SimulateBundleResult, error) { + var receipt []*SimulateBundleResult + err := a.rpc.CallContext(ctx, &receipt, "suavex_addBundles", sessionId, bundles) + return receipt, err +} + 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 233240bb3d..9a2204f46d 100644 --- a/suave/builder/api/api_server.go +++ b/suave/builder/api/api_server.go @@ -13,6 +13,8 @@ var _ API = (*Server)(nil) type SessionManager interface { NewSession(context.Context, *BuildBlockArgs) (string, error) AddTransaction(sessionId string, tx *types.Transaction) (*SimulateTransactionResult, error) + AddTransactions(sessionId string, txs types.Transactions) ([]*SimulateTransactionResult, error) + AddBundles(sessionId string, bundles []*Bundle) ([]*SimulateBundleResult, error) BuildBlock(sessionId string) error Bid(sessionId string, blsPubKey phase0.BLSPubKey) (*SubmitBlockRequest, error) } @@ -36,6 +38,14 @@ func (s *Server) AddTransaction(ctx context.Context, sessionId string, tx *types return s.sessionMngr.AddTransaction(sessionId, tx) } +func (s *Server) AddTransactions(ctx context.Context, sessionId string, txs types.Transactions) ([]*SimulateTransactionResult, error) { + return s.sessionMngr.AddTransactions(sessionId, txs) +} + +func (s *Server) AddBundles(ctx context.Context, sessionId string, bundles []*Bundle) ([]*SimulateBundleResult, error) { + return s.sessionMngr.AddBundles(sessionId, bundles) +} + func (s *Server) BuildBlock(ctx context.Context, sessionId string) error { return s.sessionMngr.BuildBlock(sessionId) } diff --git a/suave/builder/api/api_test.go b/suave/builder/api/api_test.go index 0c29477ac1..4c4d263e8e 100644 --- a/suave/builder/api/api_test.go +++ b/suave/builder/api/api_test.go @@ -27,6 +27,14 @@ func TestAPI(t *testing.T) { txn := types.NewTransaction(0, common.Address{}, big.NewInt(1), 1, big.NewInt(1), []byte{}) _, err = c.AddTransaction(context.Background(), "1", txn) require.NoError(t, err) + _, err = c.AddTransactions(context.Background(), "1", []*types.Transaction{txn}) + require.NoError(t, err) + + bundle := &Bundle{ + Txs: []*types.Transaction{txn}, + } + _, err = c.AddBundles(context.Background(), "1", []*Bundle{bundle}) + require.NoError(t, err) } type nullSessionManager struct{} @@ -39,8 +47,12 @@ func (nullSessionManager) AddTransaction(sessionId string, tx *types.Transaction return &SimulateTransactionResult{Logs: []*SimulatedLog{}}, nil } -func (nullSessionManager) AddBundle(sessionId string, bundle Bundle) error { - return nil +func (nullSessionManager) AddTransactions(sessionId string, txs types.Transactions) ([]*SimulateTransactionResult, error) { + return nil, nil +} + +func (nullSessionManager) AddBundles(sessionId string, bundles []*Bundle) ([]*SimulateBundleResult, error) { + return nil, nil } func (nullSessionManager) BuildBlock(sessionId string) error { diff --git a/suave/builder/session_manager.go b/suave/builder/session_manager.go index cef7126591..b0495e00ff 100644 --- a/suave/builder/session_manager.go +++ b/suave/builder/session_manager.go @@ -150,6 +150,22 @@ func (s *SessionManager) AddTransaction(sessionId string, tx *types.Transaction) return builder.AddTransaction(tx) } +func (s *SessionManager) AddTransactions(sessionId string, txs types.Transactions) ([]*api.SimulateTransactionResult, error) { + builder, err := s.getSession(sessionId) + if err != nil { + return nil, err + } + return builder.AddTransactions(txs) +} + +func (s *SessionManager) AddBundles(sessionId string, bundles []*api.Bundle) ([]*api.SimulateBundleResult, error) { + builder, err := s.getSession(sessionId) + if err != nil { + return nil, err + } + return builder.AddBundles(bundles) +} + func (s *SessionManager) BuildBlock(sessionId string) error { builder, err := s.getSession(sessionId) if err != nil {