diff --git builder/miner/worker.go bolt-builder/miner/worker.go
index 09d46ed99f9f600550d979c31b582201ab4eef0a..2a69317c437a82e49e2db32d61e4b464a7c28289 100644
--- builder/miner/worker.go
+++ bolt-builder/miner/worker.go
@@ -25,6 +25,7 @@ "sync"
"sync/atomic"
"time"
+ "github.com/chainbound/shardmap"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
@@ -644,7 +645,7 @@ plainTxs := newTransactionsByPriceAndNonce(w.current.signer, txs, nil, nil, w.current.header.BaseFee) // Mixed bag of everrything, yolo
blobTxs := newTransactionsByPriceAndNonce(w.current.signer, nil, nil, nil, w.current.header.BaseFee) // Empty bag, don't bother optimising
tcount := w.current.tcount
- w.commitTransactions(w.current, plainTxs, blobTxs, nil)
+ w.commitTransactions(w.current, plainTxs, blobTxs, nil, nil)
// Only update the snapshot if any new transactions were added
// to the pending block
@@ -1017,14 +1018,39 @@
return nil
}
-func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error {
+// commitTransactions applies sorted transactions to the current environment, updating the state
+// and creating the resulting block
+//
+// Assumptions:
+// - there are no nonce-conflicting transactions between `plainTxs`, `blobTxs` and the constraints
+// - all transaction are correctly signed
+func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, constraints types.HashToConstraintDecoded, interrupt *atomic.Int32) error {
gasLimit := env.header.GasLimit
if env.gasPool == nil {
env.gasPool = new(core.GasPool).AddGas(gasLimit)
}
var coalescedLogs []*types.Log
+ // Here we initialize and track the constraints left to be executed along
+ // with their gas requirements
+ constraintsOrderedByNonceAndHashDesc,
+ constraintsTotalGasLeft,
+ constraintsTotalBlobGasLeft := types.ParseConstraintsDecoded(constraints)
+
+ constraintsRecoveredOrderedByNonceAndHashDesc := make([]*types.TransactionEcRecovered, 0, len(constraintsOrderedByNonceAndHashDesc))
+ for _, tx := range constraintsOrderedByNonceAndHashDesc {
+ // Error may be ignored here, see assumption
+ from, _ := types.Sender(env.signer, tx)
+ constraintsRecoveredOrderedByNonceAndHashDesc = append(constraintsRecoveredOrderedByNonceAndHashDesc, &types.TransactionEcRecovered{
+ Transaction: tx,
+ Sender: from,
+ })
+ }
+
for {
+ // `env.tcount` starts from 0 so it's correct to use it as the current index
+ currentTxIndex := uint64(env.tcount)
+
// Check interruption signal and abort building if it's fired.
if interrupt != nil {
if signal := interrupt.Load(); signal != commitInterruptNone {
@@ -1036,102 +1062,176 @@ if env.gasPool.Gas() < params.TxGas {
log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas)
break
}
+
+ blobGasLeft := uint64(params.MaxBlobGasPerBlock - env.blobs*params.BlobTxBlobGasPerBlob)
+
// If we don't have enough blob space for any further blob transactions,
// skip that list altogether
- if !blobTxs.Empty() && env.blobs*params.BlobTxBlobGasPerBlob >= params.MaxBlobGasPerBlock {
+ if !blobTxs.Empty() && blobGasLeft <= 0 {
log.Trace("Not enough blob space for further blob transactions")
blobTxs.Clear()
// Fall though to pick up any plain txs
}
// Retrieve the next transaction and abort if all done.
var (
- ltx *txpool.LazyTransaction
- txs *transactionsByPriceAndNonce
- pltx *txpool.LazyTransaction
- ptip *uint256.Int
- bltx *txpool.LazyTransaction
- btip *uint256.Int
+ lazyTx *txpool.LazyTransaction
+ txs *transactionsByPriceAndNonce
+ plainLazyTx *txpool.LazyTransaction
+ plainTxTip *uint256.Int
+ blobLazyTx *txpool.LazyTransaction
+ blobTxTip *uint256.Int
)
- pTxWithMinerFee := plainTxs.Peek()
- if pTxWithMinerFee != nil {
- pltx = pTxWithMinerFee.Tx()
- ptip = pTxWithMinerFee.fees
+ if pTxWithMinerFee := plainTxs.Peek(); pTxWithMinerFee != nil {
+ plainLazyTx = pTxWithMinerFee.Tx()
+ plainTxTip = pTxWithMinerFee.fees
}
- bTxWithMinerFee := blobTxs.Peek()
- if bTxWithMinerFee != nil {
- bltx = bTxWithMinerFee.Tx()
- btip = bTxWithMinerFee.fees
+ if bTxWithMinerFee := blobTxs.Peek(); bTxWithMinerFee != nil {
+ blobLazyTx = bTxWithMinerFee.Tx()
+ blobTxTip = bTxWithMinerFee.fees
}
switch {
- case pltx == nil:
- txs, ltx = blobTxs, bltx
- case bltx == nil:
- txs, ltx = plainTxs, pltx
+ case plainLazyTx == nil:
+ txs, lazyTx = blobTxs, blobLazyTx
+ case blobLazyTx == nil:
+ txs, lazyTx = plainTxs, plainLazyTx
default:
- if ptip.Lt(btip) {
- txs, ltx = blobTxs, bltx
+ if plainTxTip.Lt(blobTxTip) {
+ txs, lazyTx = blobTxs, blobLazyTx
} else {
- txs, ltx = plainTxs, pltx
+ txs, lazyTx = plainTxs, plainLazyTx
}
}
- if ltx == nil {
- break
- }
-
- // If we don't have enough space for the next transaction, skip the account.
- if env.gasPool.Gas() < ltx.Gas {
- log.Trace("Not enough gas left for transaction", "hash", ltx.Hash, "left", env.gasPool.Gas(), "needed", ltx.Gas)
- txs.Pop()
- continue
+ type candidateTx struct {
+ tx *types.Transaction
+ isConstraint bool
}
- if left := uint64(params.MaxBlobGasPerBlock - env.blobs*params.BlobTxBlobGasPerBlob); left < ltx.BlobGas {
- log.Trace("Not enough blob gas left for transaction", "hash", ltx.Hash, "left", left, "needed", ltx.BlobGas)
- txs.Pop()
- continue
+ // candidate is the transaction we should execute in this cycle of the loop
+ var candidate struct {
+ tx *types.Transaction
+ isConstraint bool
}
- // Transaction seems to fit, pull it up from the pool
- tx := ltx.Resolve()
- if tx == nil {
- log.Trace("Ignoring evicted transaction", "hash", ltx.Hash)
- txs.Pop()
- continue
+
+ isSomePoolTxLeft := lazyTx != nil
+ var from common.Address
+
+ if isSomePoolTxLeft {
+ // Check if there enough gas left for this tx
+ if constraintsTotalGasLeft+lazyTx.Gas > env.gasPool.Gas() || constraintsTotalBlobGasLeft+lazyTx.BlobGas > blobGasLeft {
+ // Skip this tx and try to fit one with less gas.
+ // Drop all consecutive transactions from the same sender because of `nonce-too-high` clause.
+ log.Debug("Could not find transactions gas with the remaining constraints, account skipped", "hash", lazyTx.Hash)
+ txs.Pop()
+ // Edge case:
+ //
+ // Assumption: suppose sender A sends tx T_1 with nonce 1, and T_2 with nonce 2, and T_2 is a constraint.
+ //
+ //
+ // When running the block building algorithm I first have to make sure to reserve enough gas for the constraints.
+ // This implies that when a pooled tx comes I have to check if there is enough gas for it while taking into account
+ // the rest of the remaining constraint gas to allocate.
+ // Suppose there is no gas for the pooled tx T_1, then I have to drop it and consequently drop every tx from the same
+ // sender with higher nonce due to "nonce-too-high" issues, including T_2.
+ // But then, I have dropped a constraint which means my bid is invalid.
+ //
+ // NOTE: this actually cannot happen because the sidecar accept constraints considering the previous block
+ // state and not pending transactions. So this setting would be rejected by the sidecar with `NonceTooHigh`
+ // error. A scenario like T_1 is a constraint and T_2 is not is possible instead and correctly handled (see below).
+
+ // Repeat the loop to try to find another pool transaction
+ continue
+ }
+
+ // We can safely consider the pool tx as the candidate,
+ // since by assumption it is not nonce-conflicting.
+ tx := lazyTx.Resolve()
+ if tx == nil {
+ log.Trace("Ignoring evicted transaction", "hash", candidate.tx.Hash())
+ txs.Pop()
+ continue
+ }
+
+ // Error may be ignored here, see assumption
+ from, _ = types.Sender(env.signer, tx)
+
+ // We cannot choose this pooled tx yet, we need to make sure that there is not a constraint with lower nonce.
+ // That is, a scenario where T_1 is a constraint and T_2 is pooled.
+ constraintsBySender := append(constraintsRecoveredOrderedByNonceAndHashDesc, []*types.TransactionEcRecovered{}...)
+ common.Filter(&constraintsBySender, func(txRecovered *types.TransactionEcRecovered) bool {
+ return txRecovered.Sender == from
+ })
+
+ // The slice might be empty so the last value might be nil!
+ lowestNonceConstraintBySender := common.Last(constraintsBySender)
+ if lowestNonceConstraintBySender != nil && lowestNonceConstraintBySender.Transaction.Nonce() < tx.Nonce() {
+ // This means that the constraint with the lowest nonce from this sender
+ // has lower nonce than the pooled tx, so we cannot execute the pooled tx yet.
+ // We need to execute the constraint first.
+ candidate = candidateTx{tx: lowestNonceConstraintBySender.Transaction, isConstraint: true}
+ } else {
+ candidate = candidateTx{tx: tx, isConstraint: false}
+ }
+ } else {
+ // No more pool tx left, we can add the unindexed ones if available
+ if len(constraintsRecoveredOrderedByNonceAndHashDesc) == 0 {
+ // To recap, this means:
+ // 1. there are no more pool tx left
+ // 2. there are no more constraints
+ // As such, we can safely exist
+ break
+ }
+ from = common.Last(constraintsRecoveredOrderedByNonceAndHashDesc).Sender
+ candidate = candidateTx{tx: common.Pop(&constraintsRecoveredOrderedByNonceAndHashDesc).Transaction, isConstraint: true}
}
- // Error may be ignored here. The error has already been checked
- // during transaction acceptance is the transaction pool.
- from, _ := types.Sender(env.signer, tx)
// Check whether the tx is replay protected. If we're not in the EIP155 hf
// phase, start ignoring the sender until we do.
- if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) {
- log.Trace("Ignoring replay protected transaction", "hash", ltx.Hash, "eip155", w.chainConfig.EIP155Block)
+ if candidate.tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) {
+ log.Trace("Ignoring replay protected transaction", "hash", candidate.tx.Hash(), "eip155", w.chainConfig.EIP155Block)
txs.Pop()
continue
}
// Start executing the transaction
- env.state.SetTxContext(tx.Hash(), env.tcount)
+ env.state.SetTxContext(candidate.tx.Hash(), env.tcount)
- logs, err := w.commitTransaction(env, tx)
+ logs, err := w.commitTransaction(env, candidate.tx)
switch {
case errors.Is(err, core.ErrNonceTooLow):
// New head notification data race between the transaction pool and miner, shift
- log.Trace("Skipping transaction with low nonce", "hash", ltx.Hash, "sender", from, "nonce", tx.Nonce())
- txs.Shift()
+ log.Trace("Skipping transaction with low nonce", "hash", candidate.tx.Hash(), "sender", from, "nonce", candidate.tx.Nonce())
+ if candidate.isConstraint {
+ log.Warn(fmt.Sprintf("Skipping constraint with low nonce, hash %s, sender %s, nonce %d", candidate.tx.Hash(), from, candidate.tx.Nonce()))
+ } else {
+ txs.Shift()
+ }
case errors.Is(err, nil):
// Everything ok, collect the logs and shift in the next transaction from the same account
coalescedLogs = append(coalescedLogs, logs...)
env.tcount++
- txs.Shift()
+ if candidate.isConstraint {
+ // Update the amount of gas left for the constraints
+ constraintsTotalGasLeft -= candidate.tx.Gas()
+ constraintsTotalBlobGasLeft -= candidate.tx.BlobGas()
+
+ constraintTip, _ := candidate.tx.EffectiveGasTip(env.header.BaseFee)
+ log.Info(fmt.Sprintf("Executed constraint %s at index %d with effective gas tip %d", candidate.tx.Hash().String(), currentTxIndex, constraintTip))
+ } else {
+ txs.Shift()
+ }
default:
// Transaction is regarded as invalid, drop all consecutive transactions from
// the same sender because of `nonce-too-high` clause.
- log.Debug("Transaction failed, account skipped", "hash", ltx.Hash, "err", err)
- txs.Pop()
+ log.Debug("Transaction failed, account skipped", "hash", candidate.tx.Hash(), "err", err)
+ if candidate.isConstraint {
+ log.Warn("Constraint failed, account skipped", "hash", candidate.tx.Hash(), "err", err)
+ } else {
+ txs.Pop()
+ }
}
}
if !w.isRunning() && len(coalescedLogs) > 0 {
@@ -1154,16 +1254,18 @@ }
// generateParams wraps various of settings for generating sealing task.
type generateParams struct {
- timestamp uint64 // The timestamp for sealing task
- forceTime bool // Flag whether the given timestamp is immutable or not
- parentHash common.Hash // Parent block hash, empty means the latest chain head
- coinbase common.Address // The fee recipient address for including transaction
- gasLimit uint64 // The validator's requested gas limit target
- random common.Hash // The randomness generated by beacon chain, empty before the merge
- withdrawals types.Withdrawals // List of withdrawals to include in block.
- beaconRoot *common.Hash // The beacon root (cancun field).
- noTxs bool // Flag whether an empty block without any transaction is expected
- onBlock BlockHookFn // Callback to call for each produced block
+ timestamp uint64 // The timestamp for sealing task
+ forceTime bool // Flag whether the given timestamp is immutable or not
+ parentHash common.Hash // Parent block hash, empty means the latest chain head
+ coinbase common.Address // The fee recipient address for including transaction
+ gasLimit uint64 // The validator's requested gas limit target
+ random common.Hash // The randomness generated by beacon chain, empty before the merge
+ withdrawals types.Withdrawals // List of withdrawals to include in block.
+ beaconRoot *common.Hash // The beacon root (cancun field).
+ noTxs bool // Flag whether an empty block without any transaction is expected
+ onBlock BlockHookFn // Callback to call for each produced block
+ slot uint64 // The slot in which the block is being produced
+ constraintsCache *shardmap.FIFOMap[uint64, types.HashToConstraintDecoded] // The constraints to include in the block
}
func doPrepareHeader(genParams *generateParams, chain *core.BlockChain, config *Config, chainConfig *params.ChainConfig, extra []byte, engine consensus.Engine) (*types.Header, *types.Header, error) {
@@ -1266,7 +1368,7 @@ }
return env, nil
}
-func (w *worker) fillTransactionsSelectAlgo(interrupt *atomic.Int32, env *environment) ([]types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle, map[common.Hash]struct{}, error) {
+func (w *worker) fillTransactionsSelectAlgo(interrupt *atomic.Int32, env *environment, constraints types.HashToConstraintDecoded) ([]types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle, map[common.Hash]struct{}, error) {
var (
blockBundles []types.SimulatedBundle
allBundles []types.SimulatedBundle
@@ -1274,21 +1376,35 @@ usedSbundles []types.UsedSBundle
mempoolTxHashes map[common.Hash]struct{}
err error
)
- switch w.flashbots.algoType {
- case ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP:
+
+ // switch w.flashbots.algoType {
+ //
+ // case ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP:
+ //
+ // blockBundles, allBundles, usedSbundles, mempoolTxHashes, err = w.fillTransactionsAlgoWorker(interrupt, env)
+ // blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(interrupt, env, constraints)
+ // case ALGO_MEV_GETH:
+ // blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(interrupt, env, constraints)
+ // default:
+ // blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(interrupt, env, constraints)
+ // }
+
+ // // FIXME: (BOLT) the greedy algorithms do not support the constraints interface at the moment.
+ // // As such for this PoC we will be always using the MEV GETH algorithm regardless of the worker configuration.
+ if len(constraints) > 0 {
+ blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(interrupt, env, constraints)
+ } else {
blockBundles, allBundles, usedSbundles, mempoolTxHashes, err = w.fillTransactionsAlgoWorker(interrupt, env)
- case ALGO_MEV_GETH:
- blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(interrupt, env)
- default:
- blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(interrupt, env)
}
+
return blockBundles, allBundles, usedSbundles, mempoolTxHashes, err
}
// fillTransactions retrieves the pending transactions from the txpool and fills them
// into the given sealing block. The transaction selection and ordering strategy can
// be customized with the plugin in the future.
-func (w *worker) fillTransactions(interrupt *atomic.Int32, env *environment) ([]types.SimulatedBundle, []types.SimulatedBundle, map[common.Hash]struct{}, error) {
+func (w *worker) fillTransactions(interrupt *atomic.Int32, env *environment, constraints types.HashToConstraintDecoded) ([]types.SimulatedBundle, []types.SimulatedBundle, map[common.Hash]struct{}, error) {
+ log.Info(fmt.Sprintf("Filling transactions with %d constraints:", len(constraints)))
w.mu.RLock()
tip := w.tip
w.mu.RUnlock()
@@ -1304,6 +1420,12 @@ mempoolTxHashes[tx.Hash] = struct{}{}
}
}
+ // NOTE: as done with builder txs, we need to fill mempoolTxHashes with the constraints hashes
+ // in order to pass block validation
+ for hash := range constraints {
+ mempoolTxHashes[hash] = struct{}{}
+ }
+
if env.header.BaseFee != nil {
filter.BaseFee = uint256.MustFromBig(env.header.BaseFee)
}
@@ -1316,6 +1438,45 @@
filter.OnlyPlainTxs, filter.OnlyBlobTxs = false, true
pendingBlobTxs := w.eth.TxPool().Pending(filter)
+ // Drop all transactions that conflict with the constraints (sender, nonce)
+ signerAndNonceOfConstraints := make(map[common.Address]uint64)
+
+ for _, tx := range constraints {
+ from, err := types.Sender(env.signer, tx)
+ log.Info(fmt.Sprintf("Inside fillTransactions, constraint %s from %s", tx.Hash().String(), from.String()))
+ if err != nil {
+ // NOTE: is this the right behaviour? If this happens the builder is not able to
+ // produce a valid bid
+ log.Error("Failed to recover sender from constraint. Skipping constraint", "err", err)
+ continue
+ }
+
+ signerAndNonceOfConstraints[from] = tx.Nonce()
+ }
+ for sender, lazyTxs := range pendingPlainTxs {
+ common.Filter(&lazyTxs, func(lazyTx *txpool.LazyTransaction) bool {
+ if nonce, ok := signerAndNonceOfConstraints[sender]; ok {
+ if lazyTx.Tx.Nonce() == nonce {
+ return false
+ }
+ }
+
+ return true
+ })
+ }
+
+ for sender, lazyTxs := range pendingBlobTxs {
+ common.Filter(&lazyTxs, func(lazyTx *txpool.LazyTransaction) bool {
+ if nonce, ok := signerAndNonceOfConstraints[sender]; ok {
+ if lazyTx.Tx.Nonce() == nonce {
+ return false
+ }
+ }
+
+ return true
+ })
+ }
+
// Split the pending transactions into locals and remotes.
localPlainTxs, remotePlainTxs := make(map[common.Address][]*txpool.LazyTransaction), pendingPlainTxs
localBlobTxs, remoteBlobTxs := make(map[common.Address][]*txpool.LazyTransaction), pendingBlobTxs
@@ -1333,48 +1494,49 @@ }
var blockBundles []types.SimulatedBundle
var allBundles []types.SimulatedBundle
- if w.flashbots.isFlashbots {
- bundles, ccBundleCh := w.eth.TxPool().MevBundles(env.header.Number, env.header.Time)
- bundles = append(bundles, <-ccBundleCh...)
-
- var (
- bundleTxs []*types.Transaction
- resultingBundle simulatedBundle
- mergedBundles []types.SimulatedBundle
- numBundles int
- err error
- )
- // Sets allBundles in outer scope
- bundleTxs, resultingBundle, mergedBundles, numBundles, allBundles, err = w.generateFlashbotsBundle(env, bundles, pending)
- if err != nil {
- log.Error("Failed to generate flashbots bundle", "err", err)
- return nil, nil, nil, err
- }
- log.Info("Flashbots bundle", "ethToCoinbase", ethIntToFloat(resultingBundle.TotalEth), "gasUsed", resultingBundle.TotalGasUsed, "bundleScore", resultingBundle.MevGasPrice, "bundleLength", len(bundleTxs), "numBundles", numBundles, "worker", w.flashbots.maxMergedBundles)
- if len(bundleTxs) == 0 {
- return nil, nil, nil, errors.New("no bundles to apply")
- }
- if err := w.commitBundle(env, bundleTxs, interrupt); err != nil {
- return nil, nil, nil, err
- }
- blockBundles = mergedBundles
- env.profit.Add(env.profit, resultingBundle.EthSentToCoinbase)
- }
+ // if w.flashbots.isFlashbots {
+ // bundles, ccBundleCh := w.eth.TxPool().MevBundles(env.header.Number, env.header.Time)
+ // bundles = append(bundles, <-ccBundleCh...)
+ //
+ // var (
+ // bundleTxs []*types.Transaction
+ // resultingBundle simulatedBundle
+ // mergedBundles []types.SimulatedBundle
+ // numBundles int
+ // err error
+ // )
+ // // Sets allBundles in outer scope
+ // bundleTxs, resultingBundle, mergedBundles, numBundles, allBundles, err = w.generateFlashbotsBundle(env, bundles, pending)
+ // if err != nil {
+ // log.Error("Failed to generate flashbots bundle", "err", err)
+ // return nil, nil, nil, err
+ // }
+ // log.Info("Flashbots bundle", "ethToCoinbase", ethIntToFloat(resultingBundle.TotalEth), "gasUsed", resultingBundle.TotalGasUsed, "bundleScore", resultingBundle.MevGasPrice, "bundleLength", len(bundleTxs), "numBundles", numBundles, "worker", w.flashbots.maxMergedBundles)
+ // if len(bundleTxs) == 0 {
+ // log.Info("No bundles to apply")
+ // return nil, nil, nil, errors.New("no bundles to apply")
+ // }
+ // if err := w.commitBundle(env, bundleTxs, interrupt); err != nil {
+ // return nil, nil, nil, err
+ // }
+ // blockBundles = mergedBundles
+ // env.profit.Add(env.profit, resultingBundle.EthSentToCoinbase)
+ // }
// Fill the block with all available pending transactions.
- if len(localPlainTxs) > 0 || len(localBlobTxs) > 0 {
+ if len(localPlainTxs) > 0 || len(localBlobTxs) > 0 || len(constraints) > 0 {
plainTxs := newTransactionsByPriceAndNonce(env.signer, localPlainTxs, nil, nil, env.header.BaseFee)
blobTxs := newTransactionsByPriceAndNonce(env.signer, localBlobTxs, nil, nil, env.header.BaseFee)
- if err := w.commitTransactions(env, plainTxs, blobTxs, interrupt); err != nil {
+ if err := w.commitTransactions(env, plainTxs, blobTxs, constraints, interrupt); err != nil {
return nil, nil, nil, err
}
}
- if len(remotePlainTxs) > 0 || len(remoteBlobTxs) > 0 {
+ if len(remotePlainTxs) > 0 || len(remoteBlobTxs) > 0 || len(constraints) > 0 {
plainTxs := newTransactionsByPriceAndNonce(env.signer, remotePlainTxs, nil, nil, env.header.BaseFee)
blobTxs := newTransactionsByPriceAndNonce(env.signer, remoteBlobTxs, nil, nil, env.header.BaseFee)
- if err := w.commitTransactions(env, plainTxs, blobTxs, interrupt); err != nil {
+ if err := w.commitTransactions(env, plainTxs, blobTxs, constraints, interrupt); err != nil {
return nil, nil, nil, err
}
}
@@ -1400,6 +1562,7 @@ }
// Split the pending transactions into locals and remotes
// Fill the block with all available pending transactions.
pending := w.eth.TxPool().Pending(filter)
+
mempoolTxHashes := make(map[common.Hash]struct{}, len(pending))
for _, txs := range pending {
for _, tx := range txs {
@@ -1587,11 +1750,25 @@ }
orderCloseTime := time.Now()
- blockBundles, allBundles, usedSbundles, mempoolTxHashes, err := w.fillTransactionsSelectAlgo(nil, work)
+ var constraints types.HashToConstraintDecoded
+
+ if params.constraintsCache != nil {
+ constraints, _ = params.constraintsCache.Get(params.slot)
+ log.Info(fmt.Sprintf("[BOLT]: found %d constraints for slot %d ", len(constraints), params.slot))
+ }
+
+ blockBundles, allBundles, usedSbundles, mempoolTxHashes, err := w.fillTransactionsSelectAlgo(nil, work, constraints)
if err != nil {
return &newPayloadResult{err: err}
}
+ // NOTE: as done with builder txs, we need to fill mempoolTxHashes with the constraints hashes
+ // in order to pass block validation. Otherwise the constraints will be rejected as unknown
+ // because they not part of the mempool and not part of the known bundles
+ for hash := range constraints {
+ mempoolTxHashes[hash] = struct{}{}
+ }
+
// We mark transactions created by the builder as mempool transactions so code validating bundles will not fail
// for transactions created by the builder such as mev share refunds.
for _, tx := range work.txs {
@@ -1645,6 +1822,8 @@
return block, blockProfit, nil
}
+// checkProposerPayment checks that the last transaction in the block is targeting the
+// validator coinbase and returns the block profit equal to the value of the last transaction.
func (w *worker) checkProposerPayment(work *environment, validatorCoinbase common.Address) (*big.Int, error) {
if len(work.txs) == 0 {
return nil, errors.New("no proposer payment tx")
@@ -1694,7 +1873,7 @@ return
}
// Fill pending transactions from the txpool
- _, _, _, _, err = w.fillTransactionsSelectAlgo(interrupt, work)
+ _, _, _, _, err = w.fillTransactionsSelectAlgo(interrupt, work, nil)
switch {
case err == nil:
// The entire block is filled, decrease resubmit interval in case
@@ -2198,6 +2377,8 @@ w.mu.Lock()
sender := w.coinbase
w.mu.Unlock()
builderBalance := env.state.GetBalance(sender).ToBig()
+
+ log.Info(fmt.Sprintf("[BOLT]: builderBalance %v, reserve.builderBalance %v", builderBalance, reserve.builderBalance))
availableFunds := new(big.Int).Sub(builderBalance, reserve.builderBalance)
if availableFunds.Sign() <= 0 {
diff --git builder/miner/worker_test.go bolt-builder/miner/worker_test.go
index d65ad578de31558b667c7934cb7581751853fa8f..bf5344876ae3ce499b380856edc2dad72edc010f 100644
--- builder/miner/worker_test.go
+++ bolt-builder/miner/worker_test.go
@@ -24,6 +24,7 @@ "sync/atomic"
"testing"
"time"
+ "github.com/chainbound/shardmap"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
@@ -77,6 +78,9 @@ // Test transactions
pendingTxs []*types.Transaction
newTxs []*types.Transaction
+ // Test testConstraintsCache
+ testConstraintsCache = new(shardmap.FIFOMap[uint64, types.HashToConstraintDecoded])
+
testConfig = &Config{
Recommit: time.Second,
GasCeil: params.GenesisGasLimit,
@@ -84,6 +88,8 @@ }
defaultGenesisAlloc = types.GenesisAlloc{testBankAddress: {Balance: testBankFunds}}
)
+
+const pendingTxsLen = 50
func init() {
testTxPoolConfig = legacypool.DefaultConfig
@@ -98,15 +104,32 @@ Epoch: 30000,
}
signer := types.LatestSigner(params.TestChainConfig)
- tx1 := types.MustSignNewTx(testBankKey, signer, &types.AccessListTx{
- ChainID: params.TestChainConfig.ChainID,
- Nonce: 0,
- To: &testUserAddress,
- Value: big.NewInt(1000),
- Gas: params.TxGas,
- GasPrice: big.NewInt(params.InitialBaseFee),
- })
- pendingTxs = append(pendingTxs, tx1)
+ for i := 0; i < pendingTxsLen; i++ {
+ tx1 := types.MustSignNewTx(testBankKey, signer, &types.AccessListTx{
+ ChainID: params.TestChainConfig.ChainID,
+ Nonce: uint64(i),
+ To: &testUserAddress,
+ Value: big.NewInt(1000),
+ Gas: params.TxGas,
+ GasPrice: big.NewInt(params.InitialBaseFee),
+ })
+
+ // Add some constraints every 3 txs, and every 6 add an index
+ if i%3 == 0 {
+ idx := new(uint64)
+ if i%2 == 0 {
+ *idx = uint64(i)
+ } else {
+ idx = nil
+ }
+ constraints := make(map[common.Hash]*types.Transaction)
+ constraints[tx1.Hash()] = tx1
+ // FIXME: slot 0 is probably not correct for these tests
+ testConstraintsCache.Put(0, constraints)
+ }
+
+ pendingTxs = append(pendingTxs, tx1)
+ }
tx2 := types.MustSignNewTx(testBankKey, signer, &types.LegacyTx{
Nonce: 1,
@@ -130,7 +153,7 @@ func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, alloc types.GenesisAlloc, n int, gasLimit uint64) *testWorkerBackend {
if alloc == nil {
alloc = defaultGenesisAlloc
}
- var gspec = &core.Genesis{
+ gspec := &core.Genesis{
Config: chainConfig,
GasLimit: gasLimit,
Alloc: alloc,
@@ -251,10 +274,10 @@
w, _ := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), nil, 0)
defer w.close()
- taskCh := make(chan struct{}, 2)
+ taskCh := make(chan struct{}, pendingTxsLen*2)
checkEqual := func(t *testing.T, task *task) {
// The work should contain 1 tx
- receiptLen, balance := 1, uint256.NewInt(1000)
+ receiptLen, balance := pendingTxsLen, uint256.NewInt(50_000)
if len(task.receipts) != receiptLen {
t.Fatalf("receipt number mismatch: have %d, want %d", len(task.receipts), receiptLen)
}
@@ -378,12 +401,12 @@ }
func TestGetSealingWorkEthash(t *testing.T) {
t.Parallel()
- testGetSealingWork(t, ethashChainConfig, ethash.NewFaker())
+ testGetSealingWork(t, ethashChainConfig, ethash.NewFaker(), nil)
}
func TestGetSealingWorkClique(t *testing.T) {
t.Parallel()
- testGetSealingWork(t, cliqueChainConfig, clique.New(cliqueChainConfig.Clique, rawdb.NewMemoryDatabase()))
+ testGetSealingWork(t, cliqueChainConfig, clique.New(cliqueChainConfig.Clique, rawdb.NewMemoryDatabase()), nil)
}
func TestGetSealingWorkPostMerge(t *testing.T) {
@@ -391,10 +414,25 @@ t.Parallel()
local := new(params.ChainConfig)
*local = *ethashChainConfig
local.TerminalTotalDifficulty = big.NewInt(0)
- testGetSealingWork(t, local, ethash.NewFaker())
+ testGetSealingWork(t, local, ethash.NewFaker(), nil)
}
-func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine) {
+// TestGetSealingWorkWithConstraints tests the getSealingWork function with constraints.
+// This is the main test for the modified block building algorithm. Unfortunately
+// is not easy to make an end to end test where the constraints are pulled from the relay.
+//
+// A suggestion is to walk through the executing code with a debugger to further inspect the algorithm.
+//
+// However, if you want to check that functionality see `builder_test.go`
+func TestGetSealingWorkWithConstraints(t *testing.T) {
+ // t.Parallel()
+ local := new(params.ChainConfig)
+ *local = *ethashChainConfig
+ local.TerminalTotalDifficulty = big.NewInt(0)
+ testGetSealingWork(t, local, ethash.NewFaker(), testConstraintsCache)
+}
+
+func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, constraintsCache *shardmap.FIFOMap[uint64, types.HashToConstraintDecoded]) {
defer engine.Close()
w, b := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), nil, 0)
defer w.close()
@@ -486,15 +524,16 @@
// This API should work even when the automatic sealing is not enabled
for _, c := range cases {
r := w.getSealingBlock(&generateParams{
- parentHash: c.parent,
- timestamp: timestamp,
- coinbase: c.coinbase,
- random: c.random,
- withdrawals: nil,
- beaconRoot: nil,
- noTxs: false,
- forceTime: true,
- onBlock: nil,
+ parentHash: c.parent,
+ timestamp: timestamp,
+ coinbase: c.coinbase,
+ random: c.random,
+ withdrawals: nil,
+ beaconRoot: nil,
+ noTxs: false,
+ forceTime: true,
+ onBlock: nil,
+ constraintsCache: constraintsCache,
})
if c.expectErr {
if r.err == nil {