diff --git a/builder/builder.go b/builder/builder.go index 3e5cb4cea1..a8802298f3 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -183,13 +183,15 @@ func (b *Builder) Stop() error { return nil } -func (b *Builder) onSealedBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, commitedBundles, allBundles []types.SimulatedBundle, proposerPubkey boostTypes.PublicKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { +func (b *Builder) onSealedBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + proposerPubkey boostTypes.PublicKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { if b.eth.Config().IsShanghai(block.Time()) { - if err := b.submitCapellaBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, proposerPubkey, vd, attrs); err != nil { + if err := b.submitCapellaBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, proposerPubkey, vd, attrs); err != nil { return err } } else { - if err := b.submitBellatrixBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, proposerPubkey, vd, attrs); err != nil { + if err := b.submitBellatrixBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, proposerPubkey, vd, attrs); err != nil { return err } } @@ -200,7 +202,9 @@ func (b *Builder) onSealedBlock(block *types.Block, blockValue *big.Int, ordersC return nil } -func (b *Builder) submitBellatrixBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, commitedBundles, allBundles []types.SimulatedBundle, proposerPubkey boostTypes.PublicKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { +func (b *Builder) submitBellatrixBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + proposerPubkey boostTypes.PublicKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { executableData := engine.BlockToExecutableData(block, blockValue) payload, err := executableDataToExecutionPayload(executableData.ExecutionPayload) if err != nil { @@ -245,7 +249,7 @@ func (b *Builder) submitBellatrixBlock(block *types.Block, blockValue *big.Int, log.Error("could not validate bellatrix block", "err", err) } } else { - go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, &blockBidMsg) + go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &blockBidMsg) err = b.relay.SubmitBlock(&blockSubmitReq, vd) if err != nil { log.Error("could not submit bellatrix block", "err", err, "#commitedBundles", len(commitedBundles)) @@ -258,7 +262,9 @@ func (b *Builder) submitBellatrixBlock(block *types.Block, blockValue *big.Int, return nil } -func (b *Builder) submitCapellaBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, commitedBundles, allBundles []types.SimulatedBundle, proposerPubkey boostTypes.PublicKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { +func (b *Builder) submitCapellaBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + proposerPubkey boostTypes.PublicKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { executableData := engine.BlockToExecutableData(block, blockValue) payload, err := executableDataToCapellaExecutionPayload(executableData.ExecutionPayload) if err != nil { @@ -308,7 +314,7 @@ func (b *Builder) submitCapellaBlock(block *types.Block, blockValue *big.Int, or log.Error("could not validate block for capella", "err", err) } } else { - go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, &boostBidTrace) + go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &boostBidTrace) err = b.relay.SubmitBlockCapella(&blockSubmitReq, vd) if err != nil { log.Error("could not submit capella block", "err", err, "#commitedBundles", len(commitedBundles)) @@ -375,6 +381,7 @@ type blockQueueEntry struct { sealedAt time.Time commitedBundles []types.SimulatedBundle allBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle } func (b *Builder) runBuildingJob(slotCtx context.Context, proposerPubkey boostTypes.PublicKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) { @@ -401,7 +408,8 @@ func (b *Builder) runBuildingJob(slotCtx context.Context, proposerPubkey boostTy submitBestBlock := func() { queueMu.Lock() if queueBestEntry.block.Hash() != queueLastSubmittedHash { - err := b.onSealedBlock(queueBestEntry.block, queueBestEntry.blockValue, queueBestEntry.ordersCloseTime, queueBestEntry.sealedAt, queueBestEntry.commitedBundles, queueBestEntry.allBundles, proposerPubkey, vd, attrs) + err := b.onSealedBlock(queueBestEntry.block, queueBestEntry.blockValue, queueBestEntry.ordersCloseTime, queueBestEntry.sealedAt, + queueBestEntry.commitedBundles, queueBestEntry.allBundles, queueBestEntry.usedSbundles, proposerPubkey, vd, attrs) if err != nil { log.Error("could not run sealed block hook", "err", err) @@ -422,7 +430,7 @@ func (b *Builder) runBuildingJob(slotCtx context.Context, proposerPubkey boostTy // Populates queue with submissions that increase block profit blockHook := func(block *types.Block, blockValue *big.Int, ordersCloseTime time.Time, - committedBundles, allBundles []types.SimulatedBundle, + committedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, ) { if ctx.Err() != nil { return @@ -440,6 +448,7 @@ func (b *Builder) runBuildingJob(slotCtx context.Context, proposerPubkey boostTy sealedAt: sealedAt, commitedBundles: committedBundles, allBundles: allBundles, + usedSbundles: usedSbundles, } select { diff --git a/builder/eth_service.go b/builder/eth_service.go index eec1e03497..26e3cf9379 100644 --- a/builder/eth_service.go +++ b/builder/eth_service.go @@ -28,10 +28,11 @@ type testEthereumService struct { testBlockValue *big.Int testBundlesMerged []types.SimulatedBundle testAllBundles []types.SimulatedBundle + testUsedSbundles []types.UsedSBundle } func (t *testEthereumService) BuildBlock(attrs *types.BuilderPayloadAttributes, sealedBlockCallback miner.BlockHookFn) error { - sealedBlockCallback(t.testBlock, t.testBlockValue, time.Now(), t.testBundlesMerged, t.testAllBundles) + sealedBlockCallback(t.testBlock, t.testBlockValue, time.Now(), t.testBundlesMerged, t.testAllBundles, t.testUsedSbundles) return nil } diff --git a/builder/eth_service_test.go b/builder/eth_service_test.go index ac71b65f72..b1f3dff65c 100644 --- a/builder/eth_service_test.go +++ b/builder/eth_service_test.go @@ -94,7 +94,7 @@ func TestBuildBlock(t *testing.T) { service := NewEthereumService(ethservice) service.eth.APIBackend.Miner().SetEtherbase(common.Address{0x05, 0x11}) - err := service.BuildBlock(testPayloadAttributes, func(block *types.Block, blockValue *big.Int, _ time.Time, _, _ []types.SimulatedBundle) { + err := service.BuildBlock(testPayloadAttributes, func(block *types.Block, blockValue *big.Int, _ time.Time, _, _ []types.SimulatedBundle, _ []types.UsedSBundle) { executableData := engine.BlockToExecutableData(block, blockValue) require.Equal(t, common.Address{0x05, 0x11}, executableData.ExecutionPayload.FeeRecipient) require.Equal(t, common.Hash{0x05, 0x10}, executableData.ExecutionPayload.Random) diff --git a/common/big.go b/common/big.go index 65d4377bf7..713d133b77 100644 --- a/common/big.go +++ b/common/big.go @@ -25,6 +25,12 @@ var ( Big3 = big.NewInt(3) Big0 = big.NewInt(0) Big32 = big.NewInt(32) + Big100 = big.NewInt(100) Big256 = big.NewInt(256) Big257 = big.NewInt(257) ) + +func PercentOf(val *big.Int, percent int) *big.Int { + res := new(big.Int).Mul(val, big.NewInt(int64(percent))) + return new(big.Int).Div(res, Big100) +} diff --git a/core/sbundle_sim.go b/core/sbundle_sim.go new file mode 100644 index 0000000000..3dd5d2b688 --- /dev/null +++ b/core/sbundle_sim.go @@ -0,0 +1,140 @@ +package core + +import ( + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "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/params" +) + +var ( + ErrInvalidInclusion = errors.New("invalid inclusion") + + ErrTxFailed = errors.New("tx failed") + ErrNegativeProfit = errors.New("negative profit") + ErrInvalidBundle = errors.New("invalid bundle") + + SbundlePayoutMaxCostInt uint64 = 30_000 + SbundlePayoutMaxCost = big.NewInt(30_000) +) + +type SimBundleResult struct { + TotalProfit *big.Int + RefundableValue *big.Int + GasUsed uint64 + MevGasPrice *big.Int + BodyLogs []SimBundleBodyLogs +} + +type SimBundleBodyLogs struct { + TxLogs []*types.Log `json:"txLogs,omitempty"` + BundleLogs []SimBundleBodyLogs `json:"bundleLogs,omitempty"` +} + +func NewSimBundleResult() SimBundleResult { + return SimBundleResult{ + TotalProfit: big.NewInt(0), + RefundableValue: big.NewInt(0), + GasUsed: 0, + MevGasPrice: big.NewInt(0), + BodyLogs: nil, + } +} + +func SimBundle(chainConfig *params.ChainConfig, chain *BlockChain, gp *GasPool, statedb *state.StateDB, header *types.Header, b *types.SBundle, logs bool) (SimBundleResult, error) { + res := NewSimBundleResult() + + currBlock := header.Number.Uint64() + if currBlock < b.Inclusion.BlockNumber || currBlock > b.Inclusion.MaxBlockNumber { + return res, ErrInvalidInclusion + } + + // extract constraints into convenient format + refundIdx := make([]bool, len(b.Body)) + refundPercents := make([]int, len(b.Body)) + for _, el := range b.Validity.Refund { + refundIdx[el.BodyIdx] = true + refundPercents[el.BodyIdx] = el.Percent + } + + var ( + coinbaseDelta = new(big.Int) + coinbaseBefore *big.Int + ) + for i, el := range b.Body { + coinbaseDelta.Set(common.Big0) + coinbaseBefore = statedb.GetBalance(header.Coinbase) + + if el.Tx != nil { + vmconfig := vm.Config{} + receipt, err := ApplyTransaction(chainConfig, chain, &header.Coinbase, gp, statedb, header, el.Tx, &header.GasUsed, vmconfig, nil) + if err != nil { + return res, err + } + if receipt.Status != types.ReceiptStatusSuccessful && !el.CanRevert { + return res, ErrTxFailed + } + res.GasUsed += receipt.GasUsed + if logs { + res.BodyLogs = append(res.BodyLogs, SimBundleBodyLogs{TxLogs: receipt.Logs}) + } + } else if el.Bundle != nil { + innerRes, err := SimBundle(chainConfig, chain, gp, statedb, header, el.Bundle, logs) + if err != nil { + return res, err + } + res.GasUsed += innerRes.GasUsed + if logs { + res.BodyLogs = append(res.BodyLogs, SimBundleBodyLogs{BundleLogs: innerRes.BodyLogs}) + } + } else { + return res, ErrInvalidBundle + } + + coinbaseDelta.Set(statedb.GetBalance(header.Coinbase)) + coinbaseDelta.Sub(coinbaseDelta, coinbaseBefore) + + res.TotalProfit.Add(res.TotalProfit, coinbaseDelta) + if !refundIdx[i] { + res.RefundableValue.Add(res.RefundableValue, coinbaseDelta) + } + } + + // estimate payout value and subtract from total profit + signer := types.MakeSigner(chainConfig, header.Number) + for i, el := range refundPercents { + if !refundIdx[i] { + continue + } + // we pay tx cost out of the refundable value + + // cost + refundConfig, err := types.GetRefundConfig(&b.Body[i], signer) + if err != nil { + return res, err + } + payoutTxFee := new(big.Int).Mul(header.BaseFee, SbundlePayoutMaxCost) + payoutTxFee.Mul(payoutTxFee, new(big.Int).SetInt64(int64(len(refundConfig)))) + res.GasUsed += SbundlePayoutMaxCost.Uint64() * uint64(len(refundConfig)) + + // allocated refundable value + payoutValue := common.PercentOf(res.RefundableValue, el) + + if payoutTxFee.Cmp(payoutValue) > 0 { + return res, ErrNegativeProfit + } + + res.TotalProfit.Sub(res.TotalProfit, payoutValue) + } + + if res.TotalProfit.Sign() < 0 { + res.TotalProfit.Set(common.Big0) + return res, ErrNegativeProfit + } + res.MevGasPrice.Div(res.TotalProfit, new(big.Int).SetUint64(res.GasUsed)) + return res, nil +} diff --git a/core/txpool/sbundle_pool.go b/core/txpool/sbundle_pool.go new file mode 100644 index 0000000000..b947d16410 --- /dev/null +++ b/core/txpool/sbundle_pool.go @@ -0,0 +1,196 @@ +package txpool + +// TODO: cancel sbundles, fetch them from the db + +import ( + "errors" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +const ( + maxSBundleRange = 30 + maxSBundleNesting = 1 +) + +var ( + ErrInvalidInclusion = errors.New("invalid inclusion") + ErrBundleTooDeep = errors.New("bundle too deep") + ErrInvalidBody = errors.New("invalid body") + ErrInvalidConstraints = errors.New("invalid constraints") +) + +type SBundlePool struct { + mu sync.Mutex + + bundles map[common.Hash]*types.SBundle + byBlock map[uint64][]*types.SBundle + + signer types.Signer + + // data from tx_pool that is constantly updated + istanbul bool + eip2718 bool + eip1559 bool + shanghai bool + currentMaxGas uint64 +} + +func NewSBundlePool(signer types.Signer) *SBundlePool { + return &SBundlePool{ + bundles: make(map[common.Hash]*types.SBundle), + byBlock: make(map[uint64][]*types.SBundle), + signer: signer, + } +} + +func (p *SBundlePool) ResetPoolData(pool *TxPool) { + p.mu.Lock() + defer p.mu.Unlock() + + p.istanbul = pool.istanbul + p.eip2718 = pool.eip2718 + p.eip1559 = pool.eip1559 + p.shanghai = pool.shanghai + p.currentMaxGas = pool.currentMaxGas +} + +func (p *SBundlePool) Add(bundle *types.SBundle) error { + p.mu.Lock() + defer p.mu.Unlock() + + if _, ok := p.bundles[bundle.Hash()]; ok { + return nil + } + + if err := p.validateSBundle(0, bundle); err != nil { + return err + } + + p.bundles[bundle.Hash()] = bundle + for b := bundle.Inclusion.BlockNumber; b <= bundle.Inclusion.MaxBlockNumber; b++ { + p.byBlock[b] = append(p.byBlock[b], bundle) + } + return nil +} + +func (p *SBundlePool) GetSBundles(nextBlock uint64) []*types.SBundle { + p.mu.Lock() + defer p.mu.Unlock() + + // remove old blocks + for b, el := range p.byBlock { + if b < nextBlock { + for _, bundle := range el { + if bundle.Inclusion.MaxBlockNumber < nextBlock { + delete(p.bundles, bundle.Hash()) + } + delete(p.bundles, bundle.Hash()) + } + delete(p.byBlock, b) + } + } + return p.byBlock[nextBlock] +} + +func (p *SBundlePool) validateSBundle(level int, b *types.SBundle) error { + if level > maxSBundleNesting { + return ErrBundleTooDeep + } + // inclusion + if b.Inclusion.BlockNumber > b.Inclusion.MaxBlockNumber { + return ErrInvalidInclusion + } + if b.Inclusion.MaxBlockNumber-b.Inclusion.BlockNumber > maxSBundleRange { + return ErrInvalidInclusion + } + + // body + for _, el := range b.Body { + if el.Tx != nil { + if err := p.validateTx(el.Tx); err != nil { + return err + } + } else if el.Bundle != nil { + if err := p.validateSBundle(level+1, el.Bundle); err != nil { + return err + } + } else { + return ErrInvalidBody + } + } + + // constraints + if len(b.Validity.Refund) > len(b.Body) { + return ErrInvalidConstraints + } + + usedConstraints := make([]bool, len(b.Body)) + totalRefundPercent := 0 + for _, el := range b.Validity.Refund { + if el.BodyIdx >= len(b.Body) { + return ErrInvalidConstraints + } + if usedConstraints[el.BodyIdx] { + return ErrInvalidConstraints + } + usedConstraints[el.BodyIdx] = true + totalRefundPercent += el.Percent + } + if totalRefundPercent > 100 { + return ErrInvalidConstraints + } + + return nil +} + +// same as core/tx_pool.go but we don't check for gas price and nonce +func (p *SBundlePool) validateTx(tx *types.Transaction) error { + // Accept only legacy transactions until EIP-2718/2930 activates. + if !p.eip2718 && tx.Type() != types.LegacyTxType { + return core.ErrTxTypeNotSupported + } + // Reject dynamic fee transactions until EIP-1559 activates. + if !p.eip1559 && tx.Type() == types.DynamicFeeTxType { + return core.ErrTxTypeNotSupported + } + // Reject transactions over defined size to prevent DOS attacks + if tx.Size() > txMaxSize { + return ErrOversizedData + } + // Check whether the init code size has been exceeded. + if p.shanghai && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize { + return fmt.Errorf("%w: code size %v limit %v", core.ErrMaxInitCodeSizeExceeded, len(tx.Data()), params.MaxInitCodeSize) + } + // Transactions can't be negative. This may never happen using RLP decoded + // transactions but may occur if you create a transaction using the RPC. + if tx.Value().Sign() < 0 { + return core.ErrNegativeValue + } + // Ensure the transaction doesn't exceed the current block limit gas. + if p.currentMaxGas < tx.Gas() { + return ErrGasLimit + } + // Sanity check for extremely large numbers + if tx.GasFeeCap().BitLen() > 256 { + return core.ErrFeeCapVeryHigh + } + if tx.GasTipCap().BitLen() > 256 { + return core.ErrTipVeryHigh + } + // Ensure gasFeeCap is greater than or equal to gasTipCap. + if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 { + return core.ErrTipAboveFeeCap + } + // Make sure the transaction is signed properly. + _, err := types.Sender(p.signer, tx) + if err != nil { + return ErrInvalidSender + } + return nil +} diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index d5eb1686ad..04a5b4cd1e 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -291,6 +291,7 @@ type TxPool struct { privateTxs *timestampedTxHashSet mevBundles []types.MevBundle bundleFetcher IFetcher + sbundles *SBundlePool } type txpoolResetRequest struct { @@ -322,6 +323,7 @@ func NewTxPool(config Config, chainconfig *params.ChainConfig, chain blockChain) initDoneCh: make(chan struct{}), gasPrice: new(big.Int).SetUint64(config.PriceLimit), privateTxs: newExpiringTxHashSet(config.PrivateTxLifetime), + sbundles: NewSBundlePool(types.LatestSigner(chainconfig)), } pool.locals = newAccountSet(pool.signer) @@ -750,6 +752,14 @@ func (pool *TxPool) AddMevBundle(txs types.Transactions, blockNumber *big.Int, r return nil } +func (pool *TxPool) AddSBundle(bundle *types.SBundle) error { + return pool.sbundles.Add(bundle) +} + +func (pool *TxPool) GetSBundles(block *big.Int) []*types.SBundle { + return pool.sbundles.GetSBundles(block.Uint64()) +} + // Locals retrieves the accounts currently considered local by the pool. func (pool *TxPool) Locals() []common.Address { pool.mu.Lock() @@ -1576,6 +1586,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { pool.eip2718 = pool.chainconfig.IsBerlin(next) pool.eip1559 = pool.chainconfig.IsLondon(next) pool.shanghai = pool.chainconfig.IsShanghai(uint64(time.Now().Unix())) + pool.sbundles.ResetPoolData(pool) } // promoteExecutables moves transactions that have become processable from the diff --git a/core/types/sbundle.go b/core/types/sbundle.go new file mode 100644 index 0000000000..f6fe50f824 --- /dev/null +++ b/core/types/sbundle.go @@ -0,0 +1,113 @@ +package types + +import ( + "errors" + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "golang.org/x/crypto/sha3" +) + +var ( + ErrIncorrectRefundConfig = errors.New("incorrect refund config") +) + +type SBundle struct { + Inclusion BundleInclusion + Body []BundleBody + Validity BundleValidity + + hash atomic.Value +} + +type BundleInclusion struct { + BlockNumber uint64 + MaxBlockNumber uint64 +} + +type BundleBody struct { + Tx *Transaction + Bundle *SBundle + CanRevert bool +} + +type BundleValidity struct { + Refund []RefundConstraint `json:"refund,omitempty"` + RefundConfig []RefundConfig `json:"refundConfig,omitempty"` +} + +type RefundConstraint struct { + BodyIdx int `json:"bodyIdx"` + Percent int `json:"percent"` +} + +type RefundConfig struct { + Address common.Address `json:"address"` + Percent int `json:"percent"` +} + +type BundlePrivacy struct { + RefundAddress common.Address +} + +func (b *SBundle) Hash() common.Hash { + if hash := b.hash.Load(); hash != nil { + return hash.(common.Hash) + } + + bodyHashes := make([]common.Hash, len(b.Body)) + for i, body := range b.Body { + if body.Tx != nil { + bodyHashes[i] = body.Tx.Hash() + } else if body.Bundle != nil { + bodyHashes[i] = body.Bundle.Hash() + } + } + + var h common.Hash + if len(bodyHashes) == 1 { + h = bodyHashes[0] + } else { + hasher := sha3.NewLegacyKeccak256() + for _, h := range bodyHashes { + hasher.Write(h[:]) + } + h = common.BytesToHash(hasher.Sum(nil)) + } + b.hash.Store(h) + return h +} + +type SimSBundle struct { + Bundle *SBundle + MevGasPrice *big.Int + Profit *big.Int +} + +func GetRefundConfig(body *BundleBody, signer Signer) ([]RefundConfig, error) { + if body.Tx != nil { + address, err := signer.Sender(body.Tx) + if err != nil { + return nil, err + } + return []RefundConfig{{Address: address, Percent: 100}}, nil + } + if bundle := body.Bundle; bundle != nil { + if len(bundle.Validity.RefundConfig) > 0 { + return bundle.Validity.RefundConfig, nil + } else { + if len(bundle.Body) == 0 { + return nil, ErrIncorrectRefundConfig + } + return GetRefundConfig(&bundle.Body[0], signer) + } + } + return nil, ErrIncorrectRefundConfig +} + +// UsedSBundle is a bundle that was used in the block building +type UsedSBundle struct { + Bundle *SBundle + Success bool +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 24b601acdb..319a34431d 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -476,6 +476,7 @@ func (s TxByNonce) Swap(i, j int) { s[i], s[j] = s[j], s[i] } type _Order interface { AsTx() *Transaction AsBundle() *SimulatedBundle + AsSBundle() *SimSBundle } type _TxOrder struct { @@ -484,6 +485,7 @@ type _TxOrder struct { func (o _TxOrder) AsTx() *Transaction { return o.tx } func (o _TxOrder) AsBundle() *SimulatedBundle { return nil } +func (o _TxOrder) AsSBundle() *SimSBundle { return nil } type _BundleOrder struct { bundle *SimulatedBundle @@ -491,6 +493,15 @@ type _BundleOrder struct { func (o _BundleOrder) AsTx() *Transaction { return nil } func (o _BundleOrder) AsBundle() *SimulatedBundle { return o.bundle } +func (o _BundleOrder) AsSBundle() *SimSBundle { return nil } + +type _SBundleOrder struct { + sbundle *SimSBundle +} + +func (o _SBundleOrder) AsTx() *Transaction { return nil } +func (o _SBundleOrder) AsBundle() *SimulatedBundle { return nil } +func (o _SBundleOrder) AsSBundle() *SimSBundle { return o.sbundle } // TxWithMinerFee wraps a transaction with its gas price or effective miner gasTipCap type TxWithMinerFee struct { @@ -506,6 +517,10 @@ func (t *TxWithMinerFee) Bundle() *SimulatedBundle { return t.order.AsBundle() } +func (t *TxWithMinerFee) SBundle() *SimSBundle { + return t.order.AsSBundle() +} + // NewTxWithMinerFee creates a wrapped transaction, calculating the effective // miner gasTipCap if a base fee is provided. // Returns error in case of a negative effective miner gasTipCap. @@ -529,6 +544,15 @@ func NewBundleWithMinerFee(bundle *SimulatedBundle, baseFee *big.Int) (*TxWithMi }, nil } +// NewSBundleWithMinerFee creates a wrapped bundle. +func NewSBundleWithMinerFee(sbundle *SimSBundle, baseFee *big.Int) (*TxWithMinerFee, error) { + minerFee := sbundle.MevGasPrice + return &TxWithMinerFee{ + order: _SBundleOrder{sbundle}, + minerFee: minerFee, + }, nil +} + // TxByPriceAndTime implements both the sort and the heap interface, making it useful // for all at once sorting as well as individually adding and removing elements. type TxByPriceAndTime []*TxWithMinerFee @@ -581,9 +605,18 @@ type TransactionsByPriceAndNonce struct { // // Note, the input map is reowned so the caller should not interact any more with // if after providing it to the constructor. -func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, bundles []SimulatedBundle, baseFee *big.Int) *TransactionsByPriceAndNonce { +func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, bundles []SimulatedBundle, sbundles []*SimSBundle, baseFee *big.Int) *TransactionsByPriceAndNonce { // Initialize a price and received time based heap with the head transactions - heads := make(TxByPriceAndTime, 0, len(txs)+len(bundles)) + heads := make(TxByPriceAndTime, 0, len(txs)+len(bundles)+len(sbundles)) + + for i := range sbundles { + wrapped, err := NewSBundleWithMinerFee(sbundles[i], baseFee) + if err != nil { + continue + } + heads = append(heads, wrapped) + } + for i := range bundles { wrapped, err := NewBundleWithMinerFee(&bundles[i], baseFee) if err != nil { diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go index 42c335343c..a4f18ea651 100644 --- a/core/types/transaction_test.go +++ b/core/types/transaction_test.go @@ -321,7 +321,7 @@ func testTransactionPriceNonceSort(t *testing.T, baseFee *big.Int) { expectedCount += count } // Sort the transactions and cross check the nonce ordering - txset := NewTransactionsByPriceAndNonce(signer, groups, nil, baseFee) + txset := NewTransactionsByPriceAndNonce(signer, groups, nil, nil, baseFee) txs := Transactions{} for tx := txset.Peek(); tx != nil; tx = txset.Peek() { @@ -378,7 +378,7 @@ func TestTransactionTimeSort(t *testing.T) { groups[addr] = append(groups[addr], tx) } // Sort the transactions and cross check the nonce ordering - txset := NewTransactionsByPriceAndNonce(signer, groups, nil, nil) + txset := NewTransactionsByPriceAndNonce(signer, groups, nil, nil, nil) txs := Transactions{} for tx := txset.Peek(); tx != nil; tx = txset.Peek() { diff --git a/eth/api_backend.go b/eth/api_backend.go index 5d61b11825..5e939beaba 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -286,6 +286,10 @@ func (b *EthAPIBackend) SendBundle(ctx context.Context, txs types.Transactions, return b.eth.txPool.AddMevBundle(txs, big.NewInt(blockNumber.Int64()), uuid, signingAddress, minTimestamp, maxTimestamp, revertingTxHashes) } +func (b *EthAPIBackend) SendSBundle(ctx context.Context, sbundle *types.SBundle) error { + return b.eth.txPool.AddSBundle(sbundle) +} + func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) { pending := b.eth.txPool.Pending(false) var txs types.Transactions diff --git a/flashbotsextra/database.go b/flashbotsextra/database.go index fa3904d01c..026ac0189d 100644 --- a/flashbotsextra/database.go +++ b/flashbotsextra/database.go @@ -20,14 +20,17 @@ const ( ) type IDatabaseService interface { - ConsumeBuiltBlock(block *types.Block, blockValue *big.Int, OrdersClosedAt time.Time, sealedAt time.Time, commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, bidTrace *boostTypes.BidTrace) + ConsumeBuiltBlock(block *types.Block, blockValue *big.Int, OrdersClosedAt time.Time, sealedAt time.Time, + commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, + usedSbundles []types.UsedSBundle, + bidTrace *boostTypes.BidTrace) GetPriorityBundles(ctx context.Context, blockNum int64, isHighPrio bool) ([]DbBundle, error) GetLatestUuidBundles(ctx context.Context, blockNum int64) ([]types.LatestUuidBundle, error) } type NilDbService struct{} -func (NilDbService) ConsumeBuiltBlock(block *types.Block, _ *big.Int, _ time.Time, _ time.Time, _ []types.SimulatedBundle, _ []types.SimulatedBundle, _ *boostTypes.BidTrace) { +func (NilDbService) ConsumeBuiltBlock(block *types.Block, _ *big.Int, _ time.Time, _ time.Time, _ []types.SimulatedBundle, _ []types.SimulatedBundle, _ []types.UsedSBundle, _ *boostTypes.BidTrace) { } func (NilDbService) GetPriorityBundles(ctx context.Context, blockNum int64, isHighPrio bool) ([]DbBundle, error) { @@ -224,7 +227,27 @@ func (ds *DatabaseService) insertAllBlockBundleIds(tx *sqlx.Tx, ctx context.Cont return err } -func (ds *DatabaseService) ConsumeBuiltBlock(block *types.Block, blockValue *big.Int, ordersClosedAt time.Time, sealedAt time.Time, commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, bidTrace *boostTypes.BidTrace) { +func (ds *DatabaseService) insertUsedSBundleIds(tx *sqlx.Tx, ctx context.Context, blockId uint64, usedSbundles []types.UsedSBundle) error { + if len(usedSbundles) == 0 { + return nil + } + + toInsert := make([]DbUsedSBundle, len(usedSbundles)) + for i, u := range usedSbundles { + toInsert[i] = DbUsedSBundle{ + BlockId: blockId, + Hash: u.Bundle.Hash().Bytes(), + Inserted: u.Success, + } + } + _, err := tx.NamedExecContext(ctx, insertUsedSbundleQuery, toInsert) + return err +} + +func (ds *DatabaseService) ConsumeBuiltBlock(block *types.Block, blockValue *big.Int, ordersClosedAt time.Time, sealedAt time.Time, + commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, + usedSbundles []types.UsedSBundle, + bidTrace *boostTypes.BidTrace) { ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() @@ -267,6 +290,13 @@ func (ds *DatabaseService) ConsumeBuiltBlock(block *types.Block, blockValue *big return } + err = ds.insertUsedSBundleIds(tx, ctx, blockId, usedSbundles) + if err != nil { + tx.Rollback() + log.Error("could not insert used sbundles", "err", err) + return + } + err = tx.Commit() if err != nil { log.Error("could not commit DB trasnaction", "err", err) diff --git a/flashbotsextra/database_test.go b/flashbotsextra/database_test.go index 8c7071b54d..5d0fe37e6c 100644 --- a/flashbotsextra/database_test.go +++ b/flashbotsextra/database_test.go @@ -15,7 +15,7 @@ import ( func TestDatabaseBlockInsertion(t *testing.T) { dsn := os.Getenv("FLASHBOTS_TEST_POSTGRES_DSN") if dsn == "" { - return + t.Skip() } ds, err := NewDatabaseService(dsn) @@ -106,11 +106,27 @@ func TestDatabaseBlockInsertion(t *testing.T) { var bundle4Id uint64 ds.db.Get(&bundle4Id, "insert into bundles (bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase) values (:bundle_hash, :param_signed_txs, :param_block_number, :param_timestamp, :received_timestamp, :param_reverting_tx_hashes, :coinbase_diff, :total_gas_used, :state_block_number, :gas_fees, :eth_sent_to_coinbase) on conflict (bundle_hash, param_block_number) do nothing returning id", SimulatedBundleToDbBundle(&simBundle4)) + usedSbundle := types.UsedSBundle{ + Bundle: &types.SBundle{ + Inclusion: types.BundleInclusion{ + BlockNumber: 5, + MaxBlockNumber: 6, + }, + Body: []types.BundleBody{ + { + Tx: types.NewTransaction(uint64(53), common.Address{0x63}, big.NewInt(111), uint64(169), big.NewInt(435), []byte{})}, + }, + }, + Success: true, + } + bidTrace := &boostTypes.BidTrace{} ocAt := time.Now().Add(-time.Hour).UTC() sealedAt := time.Now().Add(-30 * time.Minute).UTC() - ds.ConsumeBuiltBlock(block, blockProfit, ocAt, sealedAt, []types.SimulatedBundle{simBundle1, simBundle2}, []types.SimulatedBundle{simBundle1, simBundle2, simBundle3, simBundle4}, bidTrace) + ds.ConsumeBuiltBlock(block, blockProfit, ocAt, sealedAt, + []types.SimulatedBundle{simBundle1, simBundle2}, []types.SimulatedBundle{simBundle1, simBundle2, simBundle3, simBundle4}, + []types.UsedSBundle{usedSbundle}, bidTrace) var dbBlock BuiltBlock require.NoError(t, ds.db.Get(&dbBlock, "select block_id, block_number, profit, slot, hash, gas_limit, gas_used, base_fee, parent_hash, timestamp, timestamp_datetime, orders_closed_at, sealed_at from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab'")) @@ -148,4 +164,12 @@ func TestDatabaseBlockInsertion(t *testing.T) { require.NoError(t, ds.db.Select(&allBundles, "select b.bundle_hash as bundle_hash from built_blocks_all_bundles bbb inner join bundles b on b.id = bbb.bundle_id where bbb.block_id = $1 order by b.param_timestamp", dbBlock.BlockId)) require.Len(t, allBundles, 4) require.Equal(t, []string{simBundle1.OriginalBundle.Hash.String(), simBundle2.OriginalBundle.Hash.String(), simBundle3.OriginalBundle.Hash.String(), simBundle4.OriginalBundle.Hash.String()}, allBundles) + + var usedSbundles []DbUsedSBundle + require.NoError(t, ds.db.Select(&usedSbundles, "select hash, inserted from sbundle_builder_used where block_id = $1", dbBlock.BlockId)) + require.Len(t, usedSbundles, 1) + require.Equal(t, DbUsedSBundle{ + Hash: usedSbundle.Bundle.Hash().Bytes(), + Inserted: usedSbundle.Success, + }, usedSbundles[0]) } diff --git a/flashbotsextra/database_types.go b/flashbotsextra/database_types.go index bf4ebfe391..8bff4d8cd2 100644 --- a/flashbotsextra/database_types.go +++ b/flashbotsextra/database_types.go @@ -65,6 +65,17 @@ type blockAndBundleId struct { BundleId uint64 `db:"bundle_id"` } +type DbUsedSBundle struct { + BlockId uint64 `db:"block_id"` + Hash []byte `db:"hash"` + Inserted bool `db:"inserted"` +} + +var insertUsedSbundleQuery = ` +INSERT INTO sbundle_builder_used (block_id, hash, inserted) +VALUES (:block_id, :hash, :inserted) +ON CONFLICT (block_id, hash) DO NOTHING` + func SimulatedBundleToDbBundle(bundle *types.SimulatedBundle) DbBundle { revertingTxHashes := make([]string, len(bundle.OriginalBundle.RevertingTxHashes)) for i, rTxHash := range bundle.OriginalBundle.RevertingTxHashes { diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index f0123a4775..7c0bbb6df7 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -77,6 +77,7 @@ type Backend interface { // Transaction pool API SendTx(ctx context.Context, signedTx *types.Transaction, private bool) error SendBundle(ctx context.Context, txs types.Transactions, blockNumber rpc.BlockNumber, uuid uuid.UUID, signingAddress common.Address, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash) error + SendSBundle(ctx context.Context, sbundle *types.SBundle) error GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) GetPoolTransactions() (types.Transactions, error) GetPoolTransaction(txHash common.Hash) *types.Transaction @@ -131,6 +132,9 @@ func GetAPIs(apiBackend Backend, chain *core.BlockChain) []rpc.API { }, { Namespace: "eth", Service: NewBundleAPI(apiBackend, chain), + }, { + Namespace: "mev", + Service: NewMevAPI(apiBackend, chain), }, } } diff --git a/internal/ethapi/sbundle_api.go b/internal/ethapi/sbundle_api.go new file mode 100644 index 0000000000..07b2b595cf --- /dev/null +++ b/internal/ethapi/sbundle_api.go @@ -0,0 +1,212 @@ +package ethapi + +import ( + "context" + "errors" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +const maxDepth = 1 +const maxBodySize = 50 +const simTimeout = time.Second * 5 + +var ( + ErrMaxDepth = errors.New("max depth reached") + ErrUnmatchedBundle = errors.New("unmatched bundle") + ErrBundleTooLarge = errors.New("bundle too large") + ErrInvalidValidity = errors.New("invalid validity") + ErrInvalidInclusion = errors.New("invalid inclusion") +) + +type MevAPI struct { + b Backend + chain *core.BlockChain +} + +func NewMevAPI(b Backend, chain *core.BlockChain) *MevAPI { + return &MevAPI{b, chain} +} + +type SendMevBundleArgs struct { + Version string `json:"version"` + Inclusion MevBundleInclusion `json:"inclusion"` + Body []MevBundleBody `json:"body"` + Validity types.BundleValidity `json:"validity"` +} + +type MevBundleInclusion struct { + BlockNumber hexutil.Uint64 `json:"block"` + MaxBlock hexutil.Uint64 `json:"maxBlock"` +} + +type MevBundleBody struct { + Hash *common.Hash `json:"hash,omitempty"` + Tx *hexutil.Bytes `json:"tx,omitempty"` + Bundle *SendMevBundleArgs `json:"bundle,omitempty"` + CanRevert bool `json:"canRevert,omitempty"` +} + +func parseBundleInner(level int, args *SendMevBundleArgs) (bundle types.SBundle, err error) { + if level > maxDepth { + return bundle, ErrMaxDepth + } + + bundle.Inclusion.BlockNumber = uint64(args.Inclusion.BlockNumber) + if args.Inclusion.MaxBlock > 0 { + bundle.Inclusion.MaxBlockNumber = uint64(args.Inclusion.MaxBlock) + } else { + bundle.Inclusion.MaxBlockNumber = uint64(args.Inclusion.BlockNumber) + } + if bundle.Inclusion.MaxBlockNumber < bundle.Inclusion.BlockNumber { + return bundle, ErrInvalidInclusion + } + if bundle.Inclusion.BlockNumber == 0 { + return bundle, ErrInvalidInclusion + } + + if len(bundle.Body) > maxBodySize { + return bundle, ErrBundleTooLarge + } + + bundle.Body = make([]types.BundleBody, len(args.Body)) + for i, el := range args.Body { + if el.Hash != nil { + return bundle, ErrUnmatchedBundle + } else if el.Tx != nil { + var tx types.Transaction + if err := tx.UnmarshalBinary(*el.Tx); err != nil { + return bundle, err + } + bundle.Body[i].Tx = &tx + if el.CanRevert { + bundle.Body[i].CanRevert = true + } + } else if el.Bundle != nil { + innerBundle, err := parseBundleInner(level+1, el.Bundle) + if err != nil { + return bundle, err + } + bundle.Body[i].Bundle = &innerBundle + } + } + + maxIdx := len(bundle.Body) - 1 + totalPercent := 0 + for _, el := range args.Validity.Refund { + if el.BodyIdx < 0 || el.BodyIdx > maxIdx { + return bundle, ErrInvalidValidity + } + if el.Percent < 0 || el.Percent > 100 { + return bundle, ErrInvalidValidity + } + totalPercent += el.Percent + } + if totalPercent > 100 { + return bundle, ErrInvalidValidity + } + totalPercent = 0 + for _, el := range args.Validity.RefundConfig { + percent := el.Percent + if percent < 0 || percent > 100 { + return bundle, ErrInvalidValidity + } + totalPercent += percent + } + if totalPercent > 100 { + return bundle, ErrInvalidValidity + } + bundle.Validity = args.Validity + + return bundle, nil +} + +func (api *MevAPI) SendBundle(ctx context.Context, args SendMevBundleArgs) error { + bundle, err := parseBundleInner(0, &args) + if err != nil { + return err + } + go api.b.SendSBundle(ctx, &bundle) + return nil +} + +type SimMevBundleResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + StateBlock hexutil.Uint64 `json:"stateBlock"` + EffectiveGasPrice hexutil.Big `json:"effectiveGasPrice"` + Profit hexutil.Big `json:"profit"` + RefundableValue hexutil.Big `json:"refundableValue"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + BodyLogs []core.SimBundleBodyLogs `json:"logs,omitempty"` +} + +func (api *MevAPI) SimBundle(ctx context.Context, args SendMevBundleArgs) (*SimMevBundleResponse, error) { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, simTimeout) + defer cancel() + + bundle, err := parseBundleInner(0, &args) + if err != nil { + return nil, err + } + + currHeader := api.b.CurrentHeader() + if currHeader == nil { + return nil, errors.New("no current header") + } + + stateBlock := currHeader.Number.Uint64() + + nextBlock := stateBlock + 1 + minBlock := bundle.Inclusion.BlockNumber + maxBlock := bundle.Inclusion.MaxBlockNumber + if minBlock > nextBlock { + return nil, errors.New("min stateBlock is in the future") + } + if maxBlock < nextBlock { + // select past stateBlock + stateBlock = maxBlock - 1 + nextBlock = maxBlock + } + + state, parent, err := api.b.StateAndHeaderByNumber(ctx, rpc.BlockNumber(stateBlock)) + if err != nil { + return nil, err + } + header := types.Header{ + ParentHash: parent.Hash(), + Number: new(big.Int).SetUint64(nextBlock), + GasLimit: parent.GasLimit, + Time: parent.Time + 12, + Difficulty: new(big.Int).Set(parent.Difficulty), + Coinbase: parent.Coinbase, + BaseFee: misc.CalcBaseFee(api.b.ChainConfig(), parent), + } + + gp := new(core.GasPool).AddGas(header.GasLimit) + + result := &SimMevBundleResponse{} + bundleRes, err := core.SimBundle(api.b.ChainConfig(), api.chain, gp, state, &header, &bundle, true) + if err != nil { + result.Success = false + result.Error = err.Error() + } else { + result.Success = true + result.BodyLogs = bundleRes.BodyLogs + } + result.StateBlock = hexutil.Uint64(stateBlock) + result.EffectiveGasPrice = hexutil.Big(*bundleRes.MevGasPrice) + result.Profit = hexutil.Big(*bundleRes.TotalProfit) + result.RefundableValue = hexutil.Big(*bundleRes.RefundableValue) + result.GasUsed = hexutil.Uint64(bundleRes.GasUsed) + + return result, nil +} diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 71ed8c7640..209577b1b3 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -221,6 +221,10 @@ func (b *backendMock) SendBundle(ctx context.Context, txs types.Transactions, bl return nil } +func (b *backendMock) SendSBundle(ctx context.Context, sbundle *types.SBundle) error { + return nil +} + func newBackendMock() *backendMock { config := ¶ms.ChainConfig{ ChainID: big.NewInt(42), diff --git a/les/api_backend.go b/les/api_backend.go index 884a1c0b7a..82f8d11a2c 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -206,6 +206,10 @@ func (b *LesApiBackend) SendBundle(ctx context.Context, txs types.Transactions, return b.eth.txPool.AddMevBundle(txs, big.NewInt(blockNumber.Int64()), uuid, signingAddress, minTimestamp, maxTimestamp, revertingTxHashes) } +func (b *LesApiBackend) SendSBundle(ctx context.Context, sbundle *types.SBundle) error { + return nil +} + func (b *LesApiBackend) SendMegabundle(ctx context.Context, txs types.Transactions, blockNumber rpc.BlockNumber, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash, relayAddr common.Address) error { return nil } diff --git a/local-builder/devnet/.gitignore b/local-builder/devnet/.gitignore new file mode 100644 index 0000000000..249cda967c --- /dev/null +++ b/local-builder/devnet/.gitignore @@ -0,0 +1 @@ +/data \ No newline at end of file diff --git a/local-builder/devnet/README.md b/local-builder/devnet/README.md new file mode 100644 index 0000000000..592d55cecc --- /dev/null +++ b/local-builder/devnet/README.md @@ -0,0 +1,13 @@ +Run local builder + +Use `./devnet run` to start local node (or `./devnet clean` to reset chain) + +### Prefunded accounts +`panic keen way shuffle post attract clever country juice point pulp february` - mnemonic + +| name | address | private key | path | +| --- | --- |--------------------------------------------------------------------|--------------------| +| validator | `0x8691735873b058e9c9959cd1ae11e0df941bb063` | `dc5a15972116d30544019f651d900a33cf5e28f9dc81300e480de96bd28cb055` | | +| user | `0x8ec1237b1e80a6adf191f40d4b7d095e21cdb18f` | `c01dd3e4426ef5739c3cb2f08d0287c83172e33625e9d3f21b73e32144fa62eb` | `m/44'/60'/0'/0/0` | +| searcher | `0x26487f7B4beA745224abF1a1bDe6aEc85031E043` | `3c89341a994f27526fd675b2fe9a9ba775887b1d3ea23fad823cd026896854cb` | `m/44'/60'/0'/0/1` | +| builder | `0x679f39C9f78315665becDf5d24Bc902F72851695` | `3fc0ec3f3f7b0f0b4729e5f761ca93f0f73bef97162db46f6a36d06e61399ff1` | | diff --git a/local-builder/devnet/devnet b/local-builder/devnet/devnet new file mode 100755 index 0000000000..4bd454fbd0 --- /dev/null +++ b/local-builder/devnet/devnet @@ -0,0 +1,48 @@ +#!/bin/bash +# devnet.sh run - runs a node +# devnet.sh clean - cleans data dir + +set -e + +cd "$(dirname "$0")" + +if [ "$1" == "run" ]; then + export GETH=${GETH:-../../build/bin/geth} + export BUILDER_TX_SIGNING_KEY=${BUILDER_TX_SIGNING_KEY:-0x3fc0ec3f3f7b0f0b4729e5f761ca93f0f73bef97162db46f6a36d06e61399ff1} + + if [ ! -d "data" ] + then + echo Creating genesis block + mkdir data + cp -r keystore data/ + touch data/password + $GETH init --datadir data genesis.json + fi + + + $GETH --datadir ./data \ + --networkid 11155111 \ + --unlock 8691735873B058E9C9959Cd1AE11E0Df941BB063 --password=./data/password --allow-insecure-unlock \ + --mine \ + --miner.algotype greedy \ + --miner.maxmergedbundles 3 \ + --miner.recommit 1s \ + --miner.etherbase 0x8691735873B058E9C9959Cd1AE11E0Df941BB063 \ + --nodiscover \ + --port 30305 \ + --http.api=eth,web3,net,debug,txpool,mev \ + --http \ + --http.port=8545 \ + --metrics \ + --metrics.addr 127.0.0.1 \ + --metrics.expensive \ + --vmodule="miner=5" + +elif [ "$1" == "clean" ]; then + echo "Cleaning data dir" + rm -rf ./data +else + echo "Usage: devnet.sh run|clean" +fi + + diff --git a/local-builder/devnet/genesis.json b/local-builder/devnet/genesis.json new file mode 100644 index 0000000000..9d5ee24cc4 --- /dev/null +++ b/local-builder/devnet/genesis.json @@ -0,0 +1,28 @@ +{ + "config": { + "chainId": 11155111, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "clique": { + "period": 5, + "epoch": 30000 + } + }, + "difficulty": "1", + "gasLimit": "30000000", + "extradata": "0x00000000000000000000000000000000000000000000000000000000000000008691735873b058e9c9959cd1ae11e0df941bb0630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "alloc": { + "8691735873b058e9c9959cd1ae11e0df941bb063": { "balance": "100000000000000000000" }, + "8ec1237b1e80a6adf191f40d4b7d095e21cdb18f": { "balance": "100000000000000000000" }, + "0x26487f7B4beA745224abF1a1bDe6aEc85031E043": { "balance": "100000000000000000000" }, + "0x679f39C9f78315665becDf5d24Bc902F72851695": { "balance": "100000000000000000000" } + } +} diff --git a/local-builder/devnet/keystore/UTC--2022-11-01T10-09-46.713396318Z--8691735873b058e9c9959cd1ae11e0df941bb063 b/local-builder/devnet/keystore/UTC--2022-11-01T10-09-46.713396318Z--8691735873b058e9c9959cd1ae11e0df941bb063 new file mode 100644 index 0000000000..2dff81f9b9 --- /dev/null +++ b/local-builder/devnet/keystore/UTC--2022-11-01T10-09-46.713396318Z--8691735873b058e9c9959cd1ae11e0df941bb063 @@ -0,0 +1 @@ +{"address":"8691735873b058e9c9959cd1ae11e0df941bb063","crypto":{"cipher":"aes-128-ctr","ciphertext":"7d761350dd8a34737ecd41ad0ebcc45a92b817966b6ea3f4de24a09dda1d0ba4","cipherparams":{"iv":"ee205c87e626df6cda1daa09ce7e8427"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"43a7f82c46c08ba17978edcab31e9a45c5c6f29ecd646966b9cf96c6c88af3ab"},"mac":"1a3aa91883e9bbd9d65403409478f037281a9fbb937c25e4eca90d1813ce5f40"},"id":"958bb7fc-f034-45cd-af6c-2ee57bb4b619","version":3} \ No newline at end of file diff --git a/miner/algo_common.go b/miner/algo_common.go index 45b572b2be..5628756740 100644 --- a/miner/algo_common.go +++ b/miner/algo_common.go @@ -392,3 +392,144 @@ func (envDiff *environmentDiff) commitPayoutTx(amount *big.Int, sender, receiver return receipt, nil } + +func (envDiff *environmentDiff) commitSBundle(b *types.SimSBundle, chData chainData, interrupt *int32, key *ecdsa.PrivateKey) error { + if key == nil { + return errors.New("no private key provided") + } + + tmpEnvDiff := envDiff.copy() + + coinbaseBefore := tmpEnvDiff.state.GetBalance(tmpEnvDiff.header.Coinbase) + gasBefore := tmpEnvDiff.gasPool.Gas() + + if err := tmpEnvDiff.commitSBundleInner(b.Bundle, chData, interrupt, key); err != nil { + return err + } + + coinbaseAfter := tmpEnvDiff.state.GetBalance(tmpEnvDiff.header.Coinbase) + gasAfter := tmpEnvDiff.gasPool.Gas() + + coinbaseDelta := new(big.Int).Sub(coinbaseAfter, coinbaseBefore) + gasDelta := new(big.Int).SetUint64(gasBefore - gasAfter) + + if coinbaseDelta.Cmp(common.Big0) < 0 { + return errors.New("coinbase balance decreased") + } + + gotEGP := new(big.Int).Div(coinbaseDelta, gasDelta) + simEGP := new(big.Int).Set(b.MevGasPrice) + + // allow > 1% difference + gotEGP = gotEGP.Mul(gotEGP, big.NewInt(101)) + simEGP = simEGP.Mul(simEGP, common.Big100) + + if gotEGP.Cmp(simEGP) < 0 { + return fmt.Errorf("incorrect EGP: got %d, expected %d", gotEGP, simEGP) + } + + *envDiff = *tmpEnvDiff + return nil +} + +func (envDiff *environmentDiff) commitSBundleInner(b *types.SBundle, chData chainData, interrupt *int32, key *ecdsa.PrivateKey) error { + // check inclusion + minBlock := b.Inclusion.BlockNumber + maxBlock := b.Inclusion.MaxBlockNumber + if current := envDiff.header.Number.Uint64(); current < minBlock || current > maxBlock { + return fmt.Errorf("bundle inclusion block number out of range: %d <= %d <= %d", minBlock, current, maxBlock) + } + + // extract constraints into convenient format + refundIdx := make([]bool, len(b.Body)) + refundPercents := make([]int, len(b.Body)) + for _, el := range b.Validity.Refund { + refundIdx[el.BodyIdx] = true + refundPercents[el.BodyIdx] = el.Percent + } + + var ( + totalProfit *big.Int = new(big.Int) + refundableProfit *big.Int = new(big.Int) + ) + + var ( + coinbaseDelta = new(big.Int) + coinbaseBefore *big.Int + ) + // insert body and check it + for i, el := range b.Body { + coinbaseDelta.Set(common.Big0) + coinbaseBefore = envDiff.state.GetBalance(envDiff.header.Coinbase) + + if el.Tx != nil { + receipt, _, err := envDiff.commitTx(el.Tx, chData) + if err != nil { + return err + } + if receipt.Status != types.ReceiptStatusSuccessful && !el.CanRevert { + return errors.New("tx failed") + } + } else if el.Bundle != nil { + err := envDiff.commitSBundleInner(el.Bundle, chData, interrupt, key) + if err != nil { + return err + } + } else { + return errors.New("invalid body element") + } + + coinbaseDelta.Set(envDiff.state.GetBalance(envDiff.header.Coinbase)) + coinbaseDelta.Sub(coinbaseDelta, coinbaseBefore) + + totalProfit.Add(totalProfit, coinbaseDelta) + if !refundIdx[i] { + refundableProfit.Add(refundableProfit, coinbaseDelta) + } + } + + // enforce constraints + coinbaseDelta.Set(common.Big0) + coinbaseBefore = envDiff.state.GetBalance(envDiff.header.Coinbase) + for i, el := range refundPercents { + if !refundIdx[i] { + continue + } + refundConfig, err := types.GetRefundConfig(&b.Body[i], envDiff.baseEnvironment.signer) + if err != nil { + return err + } + + maxPayoutCost := new(big.Int).Set(core.SbundlePayoutMaxCost) + maxPayoutCost.Mul(maxPayoutCost, big.NewInt(int64(len(refundConfig)))) + maxPayoutCost.Mul(maxPayoutCost, envDiff.header.BaseFee) + + allocatedValue := common.PercentOf(refundableProfit, el) + allocatedValue.Sub(allocatedValue, maxPayoutCost) + + if allocatedValue.Cmp(common.Big0) < 0 { + return fmt.Errorf("negative payout") + } + + for _, refund := range refundConfig { + refundValue := common.PercentOf(allocatedValue, refund.Percent) + refundReceiver := refund.Address + rec, err := envDiff.commitPayoutTx(refundValue, envDiff.header.Coinbase, refundReceiver, core.SbundlePayoutMaxCostInt, key, chData) + if err != nil { + return err + } + if rec.Status != types.ReceiptStatusSuccessful { + return fmt.Errorf("refund tx failed") + } + log.Trace("Committed kickback", "payout", ethIntToFloat(allocatedValue), "receiver", refundReceiver) + } + } + coinbaseDelta.Set(envDiff.state.GetBalance(envDiff.header.Coinbase)) + coinbaseDelta.Sub(coinbaseDelta, coinbaseBefore) + totalProfit.Add(totalProfit, coinbaseDelta) + + if totalProfit.Cmp(common.Big0) < 0 { + return fmt.Errorf("negative profit") + } + return nil +} diff --git a/miner/algo_greedy.go b/miner/algo_greedy.go index 396df03e70..0b36e89c45 100644 --- a/miner/algo_greedy.go +++ b/miner/algo_greedy.go @@ -1,6 +1,8 @@ package miner import ( + "crypto/ecdsa" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" @@ -16,19 +18,22 @@ import ( type greedyBuilder struct { inputEnvironment *environment chainData chainData + builderKey *ecdsa.PrivateKey interrupt *int32 } -func newGreedyBuilder(chain *core.BlockChain, chainConfig *params.ChainConfig, blacklist map[common.Address]struct{}, env *environment, interrupt *int32) *greedyBuilder { +func newGreedyBuilder(chain *core.BlockChain, chainConfig *params.ChainConfig, blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *int32) *greedyBuilder { return &greedyBuilder{ inputEnvironment: env, chainData: chainData{chainConfig, chain, blacklist}, + builderKey: key, interrupt: interrupt, } } -func (b *greedyBuilder) mergeOrdersIntoEnvDiff(envDiff *environmentDiff, orders *types.TransactionsByPriceAndNonce) []types.SimulatedBundle { +func (b *greedyBuilder) mergeOrdersIntoEnvDiff(envDiff *environmentDiff, orders *types.TransactionsByPriceAndNonce) ([]types.SimulatedBundle, []types.UsedSBundle) { usedBundles := []types.SimulatedBundle{} + usedSbundles := []types.UsedSBundle{} for { order := orders.Peek() @@ -64,16 +69,32 @@ func (b *greedyBuilder) mergeOrdersIntoEnvDiff(envDiff *environmentDiff, orders log.Trace("Included bundle", "bundleEGP", bundle.MevGasPrice.String(), "gasUsed", bundle.TotalGasUsed, "ethToCoinbase", ethIntToFloat(bundle.TotalEth)) usedBundles = append(usedBundles, *bundle) + } else if sbundle := order.SBundle(); sbundle != nil { + usedEntry := types.UsedSBundle{ + Bundle: sbundle.Bundle, + } + err := envDiff.commitSBundle(sbundle, b.chainData, b.interrupt, b.builderKey) + orders.Pop() + if err != nil { + log.Trace("Could not apply sbundle", "bundle", sbundle.Bundle.Hash(), "err", err) + usedEntry.Success = false + usedSbundles = append(usedSbundles, usedEntry) + continue + } + + log.Trace("Included sbundle", "bundleEGP", sbundle.MevGasPrice.String(), "ethToCoinbase", ethIntToFloat(sbundle.Profit)) + usedEntry.Success = true + usedSbundles = append(usedSbundles, usedEntry) } } - return usedBundles + return usedBundles, usedSbundles } -func (b *greedyBuilder) buildBlock(simBundles []types.SimulatedBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle) { - orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, b.inputEnvironment.header.BaseFee) +func (b *greedyBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, b.inputEnvironment.header.BaseFee) envDiff := newEnvironmentDiff(b.inputEnvironment.copy()) - usedBundles := b.mergeOrdersIntoEnvDiff(envDiff, orders) + usedBundles, usedSbundles := b.mergeOrdersIntoEnvDiff(envDiff, orders) envDiff.applyToBaseEnv() - return envDiff.baseEnvironment, usedBundles + return envDiff.baseEnvironment, usedBundles, usedSbundles } diff --git a/miner/algo_greedy_test.go b/miner/algo_greedy_test.go index de866dc3c8..fde54c1a4b 100644 --- a/miner/algo_greedy_test.go +++ b/miner/algo_greedy_test.go @@ -24,9 +24,9 @@ func TestBuildBlockGasLimit(t *testing.T) { signers.signTx(2, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{}), } - builder := newGreedyBuilder(chData.chain, chData.chainConfig, nil, env, nil) + builder := newGreedyBuilder(chData.chain, chData.chainConfig, nil, env, nil, nil) - result, _ := builder.buildBlock([]types.SimulatedBundle{}, txs) + result, _, _ := builder.buildBlock([]types.SimulatedBundle{}, nil, txs) log.Info("block built", "txs", len(result.txs), "gasPool", result.gasPool.Gas()) if result.tcount != 1 { t.Fatal("Incorrect tx count") @@ -50,7 +50,7 @@ func TestTxWithMinerFeeHeap(t *testing.T) { bundle1 := types.SimulatedBundle{MevGasPrice: big.NewInt(3), OriginalBundle: types.MevBundle{Hash: common.HexToHash("0xb1")}} bundle2 := types.SimulatedBundle{MevGasPrice: big.NewInt(2), OriginalBundle: types.MevBundle{Hash: common.HexToHash("0xb2")}} - orders := types.NewTransactionsByPriceAndNonce(env.signer, txs, []types.SimulatedBundle{bundle2, bundle1}, env.header.BaseFee) + orders := types.NewTransactionsByPriceAndNonce(env.signer, txs, []types.SimulatedBundle{bundle2, bundle1}, nil, env.header.BaseFee) for { order := orders.Peek() diff --git a/miner/algo_test.go b/miner/algo_test.go index 013aecd0f0..cf35ab90a6 100644 --- a/miner/algo_test.go +++ b/miner/algo_test.go @@ -212,11 +212,11 @@ func runAlgoTest(config *params.ChainConfig, alloc core.GenesisAlloc, txPool map var ( statedb, chData = genTestSetupWithAlloc(config, alloc) env = newEnvironment(chData, statedb, header.Coinbase, header.GasLimit*uint64(scale), header.BaseFee) - builder = newGreedyBuilder(chData.chain, chData.chainConfig, nil, env, nil) + builder = newGreedyBuilder(chData.chain, chData.chainConfig, nil, env, nil, nil) ) // build block - resultEnv, _ := builder.buildBlock(bundles, txPool) + resultEnv, _, _ := builder.buildBlock(bundles, nil, txPool) return resultEnv.profit, nil } diff --git a/miner/bundle_cache.go b/miner/bundle_cache.go index 2d6bf18537..d1ff789b72 100644 --- a/miner/bundle_cache.go +++ b/miner/bundle_cache.go @@ -39,17 +39,21 @@ func (b *BundleCache) GetBundleCache(header common.Hash) *BundleCacheEntry { } type BundleCacheEntry struct { - mu sync.Mutex - headerHash common.Hash - successfulBundles map[common.Hash]*simulatedBundle - failedBundles map[common.Hash]struct{} + mu sync.Mutex + headerHash common.Hash + successfulBundles map[common.Hash]*simulatedBundle + failedBundles map[common.Hash]struct{} + successfulSBundles map[common.Hash]*types.SimSBundle + failedSBundles map[common.Hash]struct{} } func newCacheEntry(header common.Hash) *BundleCacheEntry { return &BundleCacheEntry{ - headerHash: header, - successfulBundles: make(map[common.Hash]*simulatedBundle), - failedBundles: make(map[common.Hash]struct{}), + headerHash: header, + successfulBundles: make(map[common.Hash]*simulatedBundle), + failedBundles: make(map[common.Hash]struct{}), + successfulSBundles: make(map[common.Hash]*types.SimSBundle), + failedSBundles: make(map[common.Hash]struct{}), } } @@ -81,3 +85,32 @@ func (c *BundleCacheEntry) UpdateSimulatedBundles(result []*types.SimulatedBundl } } } + +func (c *BundleCacheEntry) GetSimSBundle(bundle common.Hash) (*types.SimSBundle, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + if simmed, ok := c.successfulSBundles[bundle]; ok { + return simmed, true + } + + if _, ok := c.failedSBundles[bundle]; ok { + return nil, true + } + + return nil, false +} + +func (c *BundleCacheEntry) UpdateSimSBundle(result []*types.SimSBundle, bundles []*types.SBundle) { + c.mu.Lock() + defer c.mu.Unlock() + + for i, simBundle := range result { + bundleHash := bundles[i].Hash() + if simBundle != nil { + c.successfulSBundles[bundleHash] = simBundle + } else { + c.failedSBundles[bundleHash] = struct{}{} + } + } +} diff --git a/miner/miner.go b/miner/miner.go index a65b26f1ca..3535673116 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -287,7 +287,7 @@ func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscript } // Accepts the block, time at which orders were taken, bundles which were used to build the block and all bundles that were considered for the block -type BlockHookFn = func(*types.Block, *big.Int, time.Time, []types.SimulatedBundle, []types.SimulatedBundle) +type BlockHookFn = func(*types.Block, *big.Int, time.Time, []types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle) // BuildPayload builds the payload according to the provided parameters. func (miner *Miner) BuildPayload(args *BuildPayloadArgs) (*Payload, error) { diff --git a/miner/worker.go b/miner/worker.go index aa29bf3909..99df0aed87 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -713,7 +713,7 @@ func (w *worker) mainLoop() { acc, _ := types.Sender(w.current.signer, tx) txs[acc] = append(txs[acc], tx) } - txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, nil, w.current.header.BaseFee) + txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, nil, nil, w.current.header.BaseFee) tcount := w.current.tcount w.commitTransactions(w.current, txset, nil) @@ -1271,6 +1271,8 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { if err != nil { return nil, err } + // uncomment to enable dirty fix for clique coinbase for local builder + //header.Coinbase = genParams.coinbase // Could potentially happen if starting to mine in an odd state. // Note genParams.coinbase can be different with header.Coinbase @@ -1301,21 +1303,22 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { return env, nil } -func (w *worker) fillTransactionsSelectAlgo(interrupt *int32, env *environment) (error, []types.SimulatedBundle, []types.SimulatedBundle) { +func (w *worker) fillTransactionsSelectAlgo(interrupt *int32, env *environment) (error, []types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle) { var ( blockBundles []types.SimulatedBundle allBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle err error ) switch w.flashbots.algoType { case ALGO_GREEDY: - err, blockBundles, allBundles = w.fillTransactionsAlgoWorker(interrupt, env) + err, blockBundles, allBundles, usedSbundles = w.fillTransactionsAlgoWorker(interrupt, env) case ALGO_MEV_GETH: err, blockBundles, allBundles = w.fillTransactions(interrupt, env) default: err, blockBundles, allBundles = w.fillTransactions(interrupt, env) } - return err, blockBundles, allBundles + return err, blockBundles, allBundles, usedSbundles } // fillTransactions retrieves the pending transactions from the txpool and fills them @@ -1363,13 +1366,13 @@ func (w *worker) fillTransactions(interrupt *int32, env *environment) (error, [] } if len(localTxs) > 0 { - txs := types.NewTransactionsByPriceAndNonce(env.signer, localTxs, nil, env.header.BaseFee) + txs := types.NewTransactionsByPriceAndNonce(env.signer, localTxs, nil, nil, env.header.BaseFee) if err := w.commitTransactions(env, txs, interrupt); err != nil { return err, nil, nil } } if len(remoteTxs) > 0 { - txs := types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, nil, env.header.BaseFee) + txs := types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, nil, nil, env.header.BaseFee) if err := w.commitTransactions(env, txs, interrupt); err != nil { return err, nil, nil } @@ -1381,52 +1384,53 @@ func (w *worker) fillTransactions(interrupt *int32, env *environment) (error, [] // fillTransactionsAlgoWorker retrieves the pending transactions and bundles from the txpool and fills them // into the given sealing block. // Returns error if any, otherwise the bundles that made it into the block and all bundles that passed simulation -func (w *worker) fillTransactionsAlgoWorker(interrupt *int32, env *environment) (error, []types.SimulatedBundle, []types.SimulatedBundle) { +func (w *worker) fillTransactionsAlgoWorker(interrupt *int32, env *environment) (error, []types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle) { // Split the pending transactions into locals and remotes // Fill the block with all available pending transactions. pending := w.eth.TxPool().Pending(true) - bundlesToConsider, err := w.getSimulatedBundles(env) + bundlesToConsider, sbundlesToConsider, err := w.getSimulatedBundles(env) if err != nil { - return err, nil, nil + return err, nil, nil, nil } - builder := newGreedyBuilder(w.chain, w.chainConfig, w.blockList, env, interrupt) + builder := newGreedyBuilder(w.chain, w.chainConfig, w.blockList, env, w.config.BuilderTxSigningKey, interrupt) start := time.Now() - newEnv, blockBundles := builder.buildBlock(bundlesToConsider, pending) + newEnv, blockBundles, usedSbundle := builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) if metrics.EnabledBuilder { mergeAlgoTimer.Update(time.Since(start)) } *env = *newEnv - return nil, blockBundles, bundlesToConsider + return nil, blockBundles, bundlesToConsider, usedSbundle } -func (w *worker) getSimulatedBundles(env *environment) ([]types.SimulatedBundle, error) { +func (w *worker) getSimulatedBundles(env *environment) ([]types.SimulatedBundle, []*types.SimSBundle, error) { if !w.flashbots.isFlashbots { - return nil, nil + return nil, nil, nil } bundles, ccBundlesCh := w.eth.TxPool().MevBundles(env.header.Number, env.header.Time) + sbundles := w.eth.TxPool().GetSBundles(env.header.Number) // TODO: consider interrupt - simBundles, err := w.simulateBundles(env, bundles, nil) /* do not consider gas impact of mempool txs as bundles are treated as transactions wrt ordering */ + simBundles, simSBundles, err := w.simulateBundles(env, bundles, sbundles, nil) /* do not consider gas impact of mempool txs as bundles are treated as transactions wrt ordering */ if err != nil { log.Error("Failed to simulate bundles", "err", err) - return nil, err + return nil, nil, err } ccBundles := <-ccBundlesCh if ccBundles == nil { - return simBundles, nil + return simBundles, simSBundles, nil } - simCcBundles, err := w.simulateBundles(env, ccBundles, nil) /* do not consider gas impact of mempool txs as bundles are treated as transactions wrt ordering */ + simCcBundles, _, err := w.simulateBundles(env, ccBundles, nil, nil) /* do not consider gas impact of mempool txs as bundles are treated as transactions wrt ordering */ if err != nil { log.Error("Failed to simulate cc bundles", "err", err) - return simBundles, nil + return simBundles, simSBundles, nil } - return append(simBundles, simCcBundles...), nil + return append(simBundles, simCcBundles...), simSBundles, nil } // generateWork generates a sealing block based on the given parameters. @@ -1442,14 +1446,25 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e } defer work.discard() - finalizeFn := func(env *environment, orderCloseTime time.Time, blockBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, noTxs bool) (*types.Block, *big.Int, error) { + finalizeFn := func(env *environment, orderCloseTime time.Time, + blockBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, noTxs bool) (*types.Block, *big.Int, error) { block, profit, err := w.finalizeBlock(env, params.withdrawals, validatorCoinbase, noTxs) if err != nil { log.Error("could not finalize block", "err", err) return nil, nil, err } - log.Info("Block finalized and assembled", "blockProfit", ethIntToFloat(profit), "txs", len(env.txs), "bundles", len(blockBundles), "gasUsed", block.GasUsed(), "time", time.Since(start)) + var okSbundles, totalSbundles int + for _, sb := range usedSbundles { + if sb.Success { + okSbundles++ + } + totalSbundles++ + } + + log.Info("Block finalized and assembled", "blockProfit", ethIntToFloat(profit), + "txs", len(env.txs), "bundles", len(blockBundles), "okSbundles", okSbundles, "totalSbundles", totalSbundles, + "gasUsed", block.GasUsed(), "time", time.Since(start)) if metrics.EnabledBuilder { buildBlockTimer.Update(time.Since(start)) blockProfitHistogram.Update(profit.Int64()) @@ -1459,14 +1474,14 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e transactionNumGauge.Update(int64(len(env.txs))) } if params.onBlock != nil { - go params.onBlock(block, profit, orderCloseTime, blockBundles, allBundles) + go params.onBlock(block, profit, orderCloseTime, blockBundles, allBundles, usedSbundles) } return block, profit, nil } if params.noTxs { - return finalizeFn(work, time.Now(), nil, nil, true) + return finalizeFn(work, time.Now(), nil, nil, nil, true) } paymentTxReserve, err := w.proposerTxPrepare(work, &validatorCoinbase) @@ -1476,7 +1491,7 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e orderCloseTime := time.Now() - err, blockBundles, allBundles := w.fillTransactionsSelectAlgo(nil, work) + err, blockBundles, allBundles, usedSbundles := w.fillTransactionsSelectAlgo(nil, work) if err != nil { return nil, nil, err @@ -1484,7 +1499,7 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e // no bundles or tx from mempool if len(work.txs) == 0 { - return finalizeFn(work, orderCloseTime, blockBundles, allBundles, true) + return finalizeFn(work, orderCloseTime, blockBundles, allBundles, usedSbundles, true) } err = w.proposerTxCommit(work, &validatorCoinbase, paymentTxReserve) @@ -1492,7 +1507,7 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e return nil, nil, err } - return finalizeFn(work, orderCloseTime, blockBundles, allBundles, false) + return finalizeFn(work, orderCloseTime, blockBundles, allBundles, usedSbundles, false) } func (w *worker) finalizeBlock(work *environment, withdrawals types.Withdrawals, validatorCoinbase common.Address, noTxs bool) (*types.Block, *big.Int, error) { @@ -1567,7 +1582,7 @@ func (w *worker) commitWork(interrupt *int32, noempty bool, timestamp int64) { } // Fill pending transactions from the txpool - err, _, _ = w.fillTransactionsSelectAlgo(interrupt, work) + err, _, _, _ = w.fillTransactionsSelectAlgo(interrupt, work) switch { case err == nil: // The entire block is filled, decrease resubmit interval in case @@ -1682,7 +1697,7 @@ func (w *worker) isTTDReached(header *types.Header) bool { type simulatedBundle = types.SimulatedBundle func (w *worker) generateFlashbotsBundle(env *environment, bundles []types.MevBundle, pendingTxs map[common.Address]types.Transactions) (types.Transactions, simulatedBundle, []types.SimulatedBundle, int, []types.SimulatedBundle, error) { - simulatedBundles, err := w.simulateBundles(env, bundles, pendingTxs) + simulatedBundles, _, err := w.simulateBundles(env, bundles, nil, pendingTxs) if err != nil { return nil, simulatedBundle{}, nil, 0, nil, err } @@ -1751,12 +1766,13 @@ func (w *worker) mergeBundles(env *environment, bundles []simulatedBundle, pendi }, mergedBundles, count, nil } -func (w *worker) simulateBundles(env *environment, bundles []types.MevBundle, pendingTxs map[common.Address]types.Transactions) ([]simulatedBundle, error) { +func (w *worker) simulateBundles(env *environment, bundles []types.MevBundle, sbundles []*types.SBundle, pendingTxs map[common.Address]types.Transactions) ([]simulatedBundle, []*types.SimSBundle, error) { start := time.Now() headerHash := env.header.Hash() simCache := w.flashbots.bundleCache.GetBundleCache(headerHash) simResult := make([]*simulatedBundle, len(bundles)) + sbSimResult := make([]*types.SimSBundle, len(sbundles)) var wg sync.WaitGroup for i, bundle := range bundles { @@ -1802,10 +1818,52 @@ func (w *worker) simulateBundles(env *environment, bundles []types.MevBundle, pe }(i, bundle, env.state.Copy()) } + for i, sbundle := range sbundles { + if simmed, ok := simCache.GetSimSBundle(sbundle.Hash()); ok { + sbSimResult[i] = simmed + continue + } + + wg.Add(1) + go func(idx int, sbundle *types.SBundle, state *state.StateDB) { + defer wg.Done() + + start := time.Now() + if metrics.EnabledBuilder { + bundleTxNumHistogram.Update(int64(len(sbundle.Body))) + } + + gp := new(core.GasPool).AddGas(env.header.GasLimit) + + simRes, err := core.SimBundle(w.chainConfig, w.chain, gp, state, env.header, sbundle, false) + if metrics.EnabledBuilder { + simulationMeter.Mark(1) + } + if err != nil { + if metrics.EnabledBuilder { + simulationRevertedMeter.Mark(1) + failedBundleSimulationTimer.UpdateSince(start) + } + return + } + + result := &types.SimSBundle{ + Bundle: sbundle, + MevGasPrice: simRes.MevGasPrice, + Profit: simRes.TotalProfit, + } + sbSimResult[idx] = result + + if metrics.EnabledBuilder { + simulationCommittedMeter.Mark(1) + successfulBundleSimulationTimer.UpdateSince(start) + } + }(i, sbundle, env.state.Copy()) + } + wg.Wait() simCache.UpdateSimulatedBundles(simResult, bundles) - simulatedBundles := make([]simulatedBundle, 0, len(bundles)) for _, bundle := range simResult { if bundle != nil { @@ -1813,11 +1871,20 @@ func (w *worker) simulateBundles(env *environment, bundles []types.MevBundle, pe } } - log.Debug("Simulated bundles", "block", env.header.Number, "allBundles", len(bundles), "okBundles", len(simulatedBundles), "time", time.Since(start)) + simCache.UpdateSimSBundle(sbSimResult, sbundles) + simulatedSbundle := make([]*types.SimSBundle, 0, len(sbundles)) + for _, sbundle := range sbSimResult { + if sbundle != nil { + simulatedSbundle = append(simulatedSbundle, sbundle) + } + } + + log.Debug("Simulated bundles", "block", env.header.Number, "allBundles", len(bundles), "okBundles", len(simulatedBundles), + "allSbundles", len(sbundles), "okSbundles", len(simulatedSbundle), "time", time.Since(start)) if metrics.EnabledBuilder { blockBundleSimulationTimer.Update(time.Since(start)) } - return simulatedBundles, nil + return simulatedBundles, simulatedSbundle, nil } func containsHash(arr []common.Hash, match common.Hash) bool { diff --git a/miner/worker_test.go b/miner/worker_test.go index 384075bf2e..bc57593f38 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -710,7 +710,7 @@ func TestSimulateBundles(t *testing.T) { bundle2 := types.MevBundle{Txs: types.Transactions{signTx(1)}, Hash: common.HexToHash("0x02")} bundle3 := types.MevBundle{Txs: types.Transactions{signTx(0)}, Hash: common.HexToHash("0x03")} - simBundles, err := w.simulateBundles(env, []types.MevBundle{bundle1, bundle2, bundle3}, nil) + simBundles, _, err := w.simulateBundles(env, []types.MevBundle{bundle1, bundle2, bundle3}, nil, nil) require.NoError(t, err) if len(simBundles) != 2 { @@ -724,7 +724,7 @@ func TestSimulateBundles(t *testing.T) { } // simulate 2 times to check cache - simBundles, err = w.simulateBundles(env, []types.MevBundle{bundle1, bundle2, bundle3}, nil) + simBundles, _, err = w.simulateBundles(env, []types.MevBundle{bundle1, bundle2, bundle3}, nil, nil) require.NoError(t, err) if len(simBundles) != 2 {