Skip to content
This repository was archived by the owner on Oct 25, 2024. It is now read-only.

Commit 44debde

Browse files
authored
Add support for additional block building algorithm (#76)
* Add initial implementation for builder bucketized merging algorithm * Simplify logic and update buckets to initialize from top of heap rather than static size from max element of heap * Add logic to commit transactions when heap is empty * Fix erroneous integer division * Move profit function to TxWithMinerFee pointer receiver, refactor sorting algorithm * Add logic for enforcing profit on bundles and sbundles * Split greedy buckets builder from greedy builder * Add greedy bucket worker * Update tests to support separate greedy buckets builder, add retry logic * Make new multi worker explicit in supported algorithm types, update ShiftAndPushByAccountForTx * Reduce retry count to 1, update signature formatting * Add else statement with panic clause for unsupported order type in algo greedy buckets * Update greedy buckets algorithm to use gas used for transaction on retries * Remove tx profit validation for the scope of this PR due to performance implications of rolling back state on low profit * Update method signatures to algoConf * Move closures to outside function, add low profit error and update greedy buckets algorithm to only reinsert when low profit error occurs. Update TxWithMinerFee to set price and set profit after low profit error to reinsert back into heap
1 parent 24deacd commit 44debde

File tree

12 files changed

+636
-142
lines changed

12 files changed

+636
-142
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ Miner is responsible for block creation. Request from the `builder` is routed to
206206
`proposerTxCommit`. We do it in a way so all fees received by the block builder are sent to the fee recipient.
207207
* Transaction insertion is done in `fillTransactionsAlgoWorker` \ `fillTransactions`. Depending on the algorithm selected.
208208
Algo worker (greedy) inserts bundles whenever they belong in the block by effective gas price but default method inserts bundles on top of the block.
209-
(see `--miner.algo`)
209+
(see `--miner.algotype`)
210210
* Worker is also responsible for simulating bundles. Bundles are simulated in parallel and results are cached for the particular parent block.
211211
* `algo_greedy.go` implements logic of the block building. Bundles and transactions are sorted in the order of effective gas price then
212212
we try to insert everything into to block until gas limit is reached. Failing bundles are reverted during the insertion but txs are not.

core/types/transaction.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,44 @@ func (t *TxWithMinerFee) SBundle() *SimSBundle {
521521
return t.order.AsSBundle()
522522
}
523523

524+
func (t *TxWithMinerFee) Price() *big.Int {
525+
return new(big.Int).Set(t.minerFee)
526+
}
527+
528+
func (t *TxWithMinerFee) Profit(baseFee *big.Int, gasUsed uint64) *big.Int {
529+
if tx := t.Tx(); tx != nil {
530+
profit := new(big.Int).Sub(tx.GasPrice(), baseFee)
531+
if gasUsed != 0 {
532+
profit.Mul(profit, new(big.Int).SetUint64(gasUsed))
533+
} else {
534+
profit.Mul(profit, new(big.Int).SetUint64(tx.Gas()))
535+
}
536+
return profit
537+
} else if bundle := t.Bundle(); bundle != nil {
538+
return bundle.TotalEth
539+
} else if sbundle := t.SBundle(); sbundle != nil {
540+
return sbundle.Profit
541+
} else {
542+
panic("profit called on unsupported order type")
543+
}
544+
}
545+
546+
// SetPrice sets the miner fee of the wrapped transaction.
547+
func (t *TxWithMinerFee) SetPrice(price *big.Int) {
548+
t.minerFee.Set(price)
549+
}
550+
551+
// SetProfit sets the profit of the wrapped transaction.
552+
func (t *TxWithMinerFee) SetProfit(profit *big.Int) {
553+
if bundle := t.Bundle(); bundle != nil {
554+
bundle.TotalEth.Set(profit)
555+
} else if sbundle := t.SBundle(); sbundle != nil {
556+
sbundle.Profit.Set(profit)
557+
} else {
558+
panic("SetProfit called on unsupported order type")
559+
}
560+
}
561+
524562
// NewTxWithMinerFee creates a wrapped transaction, calculating the effective
525563
// miner gasTipCap if a base fee is provided.
526564
// Returns error in case of a negative effective miner gasTipCap.
@@ -536,7 +574,7 @@ func NewTxWithMinerFee(tx *Transaction, baseFee *big.Int) (*TxWithMinerFee, erro
536574
}
537575

538576
// NewBundleWithMinerFee creates a wrapped bundle.
539-
func NewBundleWithMinerFee(bundle *SimulatedBundle, baseFee *big.Int) (*TxWithMinerFee, error) {
577+
func NewBundleWithMinerFee(bundle *SimulatedBundle, _ *big.Int) (*TxWithMinerFee, error) {
540578
minerFee := bundle.MevGasPrice
541579
return &TxWithMinerFee{
542580
order: _BundleOrder{bundle},
@@ -545,7 +583,7 @@ func NewBundleWithMinerFee(bundle *SimulatedBundle, baseFee *big.Int) (*TxWithMi
545583
}
546584

547585
// NewSBundleWithMinerFee creates a wrapped bundle.
548-
func NewSBundleWithMinerFee(sbundle *SimSBundle, baseFee *big.Int) (*TxWithMinerFee, error) {
586+
func NewSBundleWithMinerFee(sbundle *SimSBundle, _ *big.Int) (*TxWithMinerFee, error) {
549587
minerFee := sbundle.MevGasPrice
550588
return &TxWithMinerFee{
551589
order: _SBundleOrder{sbundle},
@@ -683,6 +721,34 @@ func (t *TransactionsByPriceAndNonce) Shift() {
683721
heap.Pop(&t.heads)
684722
}
685723

724+
// ShiftAndPushByAccountForTx attempts to update the transaction list associated with a given account address
725+
// based on the input transaction account. If the associated account exists and has additional transactions,
726+
// the top of the transaction list is popped and pushed to the heap.
727+
// Note that this operation should only be performed when the head transaction on the heap is different from the
728+
// input transaction. This operation is useful in scenarios where the current best head transaction for an account
729+
// was already popped from the heap and we want to process the next one from the same account.
730+
func (t *TransactionsByPriceAndNonce) ShiftAndPushByAccountForTx(tx *Transaction) {
731+
if tx == nil {
732+
return
733+
}
734+
735+
acc, _ := Sender(t.signer, tx)
736+
if txs, exists := t.txs[acc]; exists && len(txs) > 0 {
737+
if wrapped, err := NewTxWithMinerFee(txs[0], t.baseFee); err == nil {
738+
t.txs[acc] = txs[1:]
739+
heap.Push(&t.heads, wrapped)
740+
}
741+
}
742+
}
743+
744+
func (t *TransactionsByPriceAndNonce) Push(tx *TxWithMinerFee) {
745+
if tx == nil {
746+
return
747+
}
748+
749+
heap.Push(&t.heads, tx)
750+
}
751+
686752
// Pop removes the best transaction, *not* replacing it with the next one from
687753
// the same account. This should be used when a transaction cannot be executed
688754
// and hence all subsequent ones should be discarded from the same account.

miner/algo_common.go

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,49 @@ const (
2222
popTx = 2
2323
)
2424

25+
// defaultProfitPercentMinimum is to ensure committed transactions, bundles, sbundles don't fall below this threshold
26+
// when profit is enforced
27+
const defaultProfitPercentMinimum = 70
28+
29+
var (
30+
defaultProfitThreshold = big.NewInt(defaultProfitPercentMinimum)
31+
defaultAlgorithmConfig = algorithmConfig{
32+
EnforceProfit: false,
33+
ExpectedProfit: common.Big0,
34+
ProfitThresholdPercent: defaultProfitThreshold,
35+
}
36+
)
37+
2538
var emptyCodeHash = common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
2639

2740
var errInterrupt = errors.New("miner worker interrupted")
2841

42+
// lowProfitError is returned when an order is not committed due to low profit or low effective gas price
43+
type lowProfitError struct {
44+
ExpectedProfit *big.Int
45+
ActualProfit *big.Int
46+
47+
ExpectedEffectiveGasPrice *big.Int
48+
ActualEffectiveGasPrice *big.Int
49+
}
50+
51+
func (e *lowProfitError) Error() string {
52+
return fmt.Sprintf(
53+
"low profit: expected %v, actual %v, expected effective gas price %v, actual effective gas price %v",
54+
e.ExpectedProfit, e.ActualProfit, e.ExpectedEffectiveGasPrice, e.ActualEffectiveGasPrice,
55+
)
56+
}
57+
58+
type algorithmConfig struct {
59+
// EnforceProfit is true if we want to enforce a minimum profit threshold
60+
// for committing a transaction based on ProfitThresholdPercent
61+
EnforceProfit bool
62+
// ExpectedProfit should be set on a per transaction basis when profit is enforced
63+
ExpectedProfit *big.Int
64+
// ProfitThresholdPercent is the minimum profit threshold for committing a transaction
65+
ProfitThresholdPercent *big.Int
66+
}
67+
2968
type chainData struct {
3069
chainConfig *params.ChainConfig
3170
chain *core.BlockChain
@@ -156,49 +195,51 @@ func (envDiff *environmentDiff) commitTx(tx *types.Transaction, chData chainData
156195

157196
receipt, newState, err := applyTransactionWithBlacklist(signer, chData.chainConfig, chData.chain, coinbase,
158197
envDiff.gasPool, envDiff.state, header, tx, &header.GasUsed, *chData.chain.GetVMConfig(), chData.blacklist)
198+
159199
envDiff.state = newState
160200
if err != nil {
161201
switch {
162202
case errors.Is(err, core.ErrGasLimitReached):
163203
// Pop the current out-of-gas transaction without shifting in the next from the account
164204
from, _ := types.Sender(signer, tx)
165205
log.Trace("Gas limit exceeded for current block", "sender", from)
166-
return nil, popTx, err
206+
return receipt, popTx, err
167207

168208
case errors.Is(err, core.ErrNonceTooLow):
169209
// New head notification data race between the transaction pool and miner, shift
170210
from, _ := types.Sender(signer, tx)
171211
log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
172-
return nil, shiftTx, err
212+
return receipt, shiftTx, err
173213

174214
case errors.Is(err, core.ErrNonceTooHigh):
175215
// Reorg notification data race between the transaction pool and miner, skip account =
176216
from, _ := types.Sender(signer, tx)
177217
log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce())
178-
return nil, popTx, err
218+
return receipt, popTx, err
179219

180220
case errors.Is(err, core.ErrTxTypeNotSupported):
181221
// Pop the unsupported transaction without shifting in the next from the account
182222
from, _ := types.Sender(signer, tx)
183223
log.Trace("Skipping unsupported transaction type", "sender", from, "type", tx.Type())
184-
return nil, popTx, err
224+
return receipt, popTx, err
185225

186226
default:
187227
// Strange error, discard the transaction and get the next in line (note, the
188228
// nonce-too-high clause will prevent us from executing in vain).
189229
log.Trace("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
190-
return nil, shiftTx, err
230+
return receipt, shiftTx, err
191231
}
192232
}
193233

194234
envDiff.newProfit = envDiff.newProfit.Add(envDiff.newProfit, gasPrice.Mul(gasPrice, big.NewInt(int64(receipt.GasUsed))))
195235
envDiff.newTxs = append(envDiff.newTxs, tx)
196236
envDiff.newReceipts = append(envDiff.newReceipts, receipt)
237+
197238
return receipt, shiftTx, nil
198239
}
199240

200241
// Commit Bundle to env diff
201-
func (envDiff *environmentDiff) commitBundle(bundle *types.SimulatedBundle, chData chainData, interrupt *int32) error {
242+
func (envDiff *environmentDiff) commitBundle(bundle *types.SimulatedBundle, chData chainData, interrupt *int32, algoConf algorithmConfig) error {
202243
coinbase := envDiff.baseEnvironment.coinbase
203244
tmpEnvDiff := envDiff.copy()
204245

@@ -208,7 +249,7 @@ func (envDiff *environmentDiff) commitBundle(bundle *types.SimulatedBundle, chDa
208249
var gasUsed uint64
209250

210251
for _, tx := range bundle.OriginalBundle.Txs {
211-
if tmpEnvDiff.header.BaseFee != nil && tx.Type() == 2 {
252+
if tmpEnvDiff.header.BaseFee != nil && tx.Type() == types.DynamicFeeTxType {
212253
// Sanity check for extremely large numbers
213254
if tx.GasFeeCap().BitLen() > 256 {
214255
return core.ErrFeeCapVeryHigh
@@ -264,12 +305,34 @@ func (envDiff *environmentDiff) commitBundle(bundle *types.SimulatedBundle, chDa
264305
bundleSimEffGP := new(big.Int).Set(bundle.MevGasPrice)
265306

266307
// allow >-1% divergence
267-
bundleActualEffGP.Mul(bundleActualEffGP, big.NewInt(100))
268-
bundleSimEffGP.Mul(bundleSimEffGP, big.NewInt(99))
308+
actualEGP := new(big.Int).Mul(bundleActualEffGP, common.Big100) // bundle actual effective gas price * 100
309+
simulatedEGP := new(big.Int).Mul(bundleSimEffGP, big.NewInt(99)) // bundle simulated effective gas price * 99
269310

270-
if bundleSimEffGP.Cmp(bundleActualEffGP) == 1 {
311+
if simulatedEGP.Cmp(actualEGP) > 0 {
271312
log.Trace("Bundle underpays after inclusion", "bundle", bundle.OriginalBundle.Hash)
272-
return errors.New("bundle underpays")
313+
return &lowProfitError{
314+
ExpectedEffectiveGasPrice: bundleSimEffGP,
315+
ActualEffectiveGasPrice: bundleActualEffGP,
316+
}
317+
}
318+
319+
if algoConf.EnforceProfit {
320+
// if profit is enforced between simulation and actual commit, only allow ProfitThresholdPercent divergence
321+
simulatedBundleProfit := new(big.Int).Set(bundle.TotalEth)
322+
actualBundleProfit := new(big.Int).Mul(bundleActualEffGP, big.NewInt(int64(gasUsed)))
323+
324+
// We want to make simulated profit smaller to allow for some leeway in cases where the actual profit is
325+
// lower due to transaction ordering
326+
simulatedProfitMultiple := new(big.Int).Mul(simulatedBundleProfit, algoConf.ProfitThresholdPercent)
327+
actualProfitMultiple := new(big.Int).Mul(actualBundleProfit, common.Big100)
328+
329+
if simulatedProfitMultiple.Cmp(actualProfitMultiple) > 0 {
330+
log.Trace("Lower bundle profit found after inclusion", "bundle", bundle.OriginalBundle.Hash)
331+
return &lowProfitError{
332+
ExpectedProfit: simulatedBundleProfit,
333+
ActualProfit: actualBundleProfit,
334+
}
335+
}
273336
}
274337

275338
*envDiff = *tmpEnvDiff
@@ -395,7 +458,7 @@ func (envDiff *environmentDiff) commitPayoutTx(amount *big.Int, sender, receiver
395458
return receipt, nil
396459
}
397460

398-
func (envDiff *environmentDiff) commitSBundle(b *types.SimSBundle, chData chainData, interrupt *int32, key *ecdsa.PrivateKey) error {
461+
func (envDiff *environmentDiff) commitSBundle(b *types.SimSBundle, chData chainData, interrupt *int32, key *ecdsa.PrivateKey, algoConf algorithmConfig) error {
399462
if key == nil {
400463
return errors.New("no private key provided")
401464
}
@@ -423,11 +486,33 @@ func (envDiff *environmentDiff) commitSBundle(b *types.SimSBundle, chData chainD
423486
simEGP := new(big.Int).Set(b.MevGasPrice)
424487

425488
// allow > 1% difference
426-
gotEGP = gotEGP.Mul(gotEGP, big.NewInt(101))
427-
simEGP = simEGP.Mul(simEGP, common.Big100)
489+
actualEGP := new(big.Int).Mul(gotEGP, big.NewInt(101))
490+
simulatedEGP := new(big.Int).Mul(simEGP, common.Big100)
428491

429-
if gotEGP.Cmp(simEGP) < 0 {
430-
return fmt.Errorf("incorrect EGP: got %d, expected %d", gotEGP, simEGP)
492+
if simulatedEGP.Cmp(actualEGP) > 0 {
493+
return &lowProfitError{
494+
ExpectedEffectiveGasPrice: simEGP,
495+
ActualEffectiveGasPrice: gotEGP,
496+
}
497+
}
498+
499+
if algoConf.EnforceProfit {
500+
// if profit is enforced between simulation and actual commit, only allow >-1% divergence
501+
simulatedSbundleProfit := new(big.Int).Set(b.Profit)
502+
actualSbundleProfit := new(big.Int).Set(coinbaseDelta)
503+
504+
// We want to make simulated profit smaller to allow for some leeway in cases where the actual profit is
505+
// lower due to transaction ordering
506+
simulatedProfitMultiple := new(big.Int).Mul(simulatedSbundleProfit, algoConf.ProfitThresholdPercent)
507+
actualProfitMultiple := new(big.Int).Mul(actualSbundleProfit, common.Big100)
508+
509+
if simulatedProfitMultiple.Cmp(actualProfitMultiple) > 0 {
510+
log.Trace("Lower sbundle profit found after inclusion", "sbundle", b.Bundle.Hash())
511+
return &lowProfitError{
512+
ExpectedProfit: simulatedSbundleProfit,
513+
ActualProfit: actualSbundleProfit,
514+
}
515+
}
431516
}
432517

433518
*envDiff = *tmpEnvDiff

miner/algo_common_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func TestBundleCommit(t *testing.T) {
298298
t.Fatal("Failed to simulate bundle", err)
299299
}
300300

301-
err = envDiff.commitBundle(&simBundle, chData, nil)
301+
err = envDiff.commitBundle(&simBundle, chData, nil, defaultAlgorithmConfig)
302302
if err != nil {
303303
t.Fatal("Failed to commit bundle", err)
304304
}
@@ -408,7 +408,7 @@ func TestErrorBundleCommit(t *testing.T) {
408408
newProfitBefore := new(big.Int).Set(envDiff.newProfit)
409409
balanceBefore := envDiff.state.GetBalance(signers.addresses[2])
410410

411-
err = envDiff.commitBundle(&simBundle, chData, nil)
411+
err = envDiff.commitBundle(&simBundle, chData, nil, defaultAlgorithmConfig)
412412
if err == nil {
413413
t.Fatal("Committed failed bundle", err)
414414
}
@@ -523,7 +523,7 @@ func TestGetSealingWorkAlgos(t *testing.T) {
523523
testConfig.AlgoType = ALGO_MEV_GETH
524524
})
525525

526-
for _, algoType := range []AlgoType{ALGO_MEV_GETH, ALGO_GREEDY} {
526+
for _, algoType := range []AlgoType{ALGO_MEV_GETH, ALGO_GREEDY, ALGO_GREEDY_BUCKETS} {
527527
local := new(params.ChainConfig)
528528
*local = *ethashChainConfig
529529
local.TerminalTotalDifficulty = big.NewInt(0)
@@ -538,7 +538,7 @@ func TestGetSealingWorkAlgosWithProfit(t *testing.T) {
538538
testConfig.BuilderTxSigningKey = nil
539539
})
540540

541-
for _, algoType := range []AlgoType{ALGO_GREEDY} {
541+
for _, algoType := range []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS} {
542542
var err error
543543
testConfig.BuilderTxSigningKey, err = crypto.GenerateKey()
544544
require.NoError(t, err)

0 commit comments

Comments
 (0)