Skip to content
This repository was archived by the owner on Oct 25, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
571a2a3
Add initial implementation for builder bucketized merging algorithm
Wazzymandias Jun 13, 2023
75de38c
Simplify logic and update buckets to initialize from top of heap rath…
Wazzymandias Jun 13, 2023
96af7eb
Add logic to commit transactions when heap is empty
Wazzymandias Jun 13, 2023
44cce7a
Fix erroneous integer division
Wazzymandias Jun 13, 2023
631a995
Refactor function signatures
Wazzymandias Jun 13, 2023
d3a6c14
Revert algo type since lots of tight coupling to it
Wazzymandias Jun 13, 2023
9f3eb5a
Update unit tests, pass in builder algorithm type to greedy builder
Wazzymandias Jun 14, 2023
a54912e
Fix linter error
Wazzymandias Jun 14, 2023
5aec42b
Add comment
Wazzymandias Jun 14, 2023
a489dc7
Move profit function to TxWithMinerFee pointer receiver, refactor sor…
Wazzymandias Jun 15, 2023
32a38d8
Add logic for enforcing profit on bundles and sbundles
Wazzymandias Jun 15, 2023
d184abc
Fix unit tests
Wazzymandias Jun 15, 2023
9c67c26
Split greedy buckets builder from greedy builder
Wazzymandias Jun 15, 2023
b226b8c
Add greedy bucket worker
Wazzymandias Jun 15, 2023
e422c3b
Update tests to support separate greedy buckets builder, add retry logic
Wazzymandias Jun 16, 2023
76206d7
Rename function for retry and push
Wazzymandias Jun 16, 2023
b71bc56
Fix README, update comments
Wazzymandias Jun 16, 2023
13be2e9
Make new multi worker explicit in supported algorithm types, update S…
Wazzymandias Jun 17, 2023
359517d
Address PR feedback
Wazzymandias Jun 21, 2023
1c5300a
Fix unit test
Wazzymandias Jun 21, 2023
02e1ebe
Reduce retry count to 1, update signature formatting
Wazzymandias Jun 23, 2023
56880a3
Add else statement with panic clause for unsupported order type in al…
Wazzymandias Jun 23, 2023
5f311af
Update function signature
Wazzymandias Jun 23, 2023
e20aab1
Update unit test
Wazzymandias Jun 23, 2023
9a463fb
Update greedy buckets algorithm to use gas used for transaction on re…
Wazzymandias Jun 24, 2023
d2a2a86
Address PR feedback
Wazzymandias Jun 26, 2023
5cea23a
Merge remote-tracking branch 'origin/main' into build-300/improve-blo…
Wazzymandias Jun 26, 2023
59ee5cd
Remove print statements used for debugging
Wazzymandias Jun 26, 2023
c9cad3d
Add support for test builder algorithm for parallelized algo testing,…
Wazzymandias Jun 28, 2023
19feb4a
Remove tx profit validation for the scope of this PR due to performan…
Wazzymandias Jun 28, 2023
647309f
Update unit test
Wazzymandias Jun 28, 2023
0df6b29
Update method signatures to algoConf
Wazzymandias Jun 28, 2023
4b9e2c3
Update references of validation conf to algo conf
Wazzymandias Jun 28, 2023
8eccf03
Merge remote-tracking branch 'origin/main' into build-300/improve-blo…
Wazzymandias Jun 28, 2023
76aec25
Remove test only algorithm from PR
Wazzymandias Jun 28, 2023
adaaf14
Move closures to outside function, add low profit error and update gr…
Wazzymandias Jun 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ Miner is responsible for block creation. Request from the `builder` is routed to
`proposerTxCommit`. We do it in a way so all fees received by the block builder are sent to the fee recipient.
* Transaction insertion is done in `fillTransactionsAlgoWorker` \ `fillTransactions`. Depending on the algorithm selected.
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.
(see `--miner.algo`)
(see `--miner.algotype`)
* Worker is also responsible for simulating bundles. Bundles are simulated in parallel and results are cached for the particular parent block.
* `algo_greedy.go` implements logic of the block building. Bundles and transactions are sorted in the order of effective gas price then
we try to insert everything into to block until gas limit is reached. Failing bundles are reverted during the insertion but txs are not.
Expand Down
70 changes: 68 additions & 2 deletions core/types/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,44 @@ func (t *TxWithMinerFee) SBundle() *SimSBundle {
return t.order.AsSBundle()
}

func (t *TxWithMinerFee) Price() *big.Int {
return new(big.Int).Set(t.minerFee)
}

func (t *TxWithMinerFee) Profit(baseFee *big.Int, gasUsed uint64) *big.Int {
if tx := t.Tx(); tx != nil {
profit := new(big.Int).Sub(tx.GasPrice(), baseFee)
if gasUsed != 0 {
profit.Mul(profit, new(big.Int).SetUint64(gasUsed))
} else {
profit.Mul(profit, new(big.Int).SetUint64(tx.Gas()))
}
return profit
} else if bundle := t.Bundle(); bundle != nil {
return bundle.TotalEth
} else if sbundle := t.SBundle(); sbundle != nil {
return sbundle.Profit
} else {
panic("profit called on unsupported order type")
}
}

// SetPrice sets the miner fee of the wrapped transaction.
func (t *TxWithMinerFee) SetPrice(price *big.Int) {
t.minerFee.Set(price)
}

// SetProfit sets the profit of the wrapped transaction.
func (t *TxWithMinerFee) SetProfit(profit *big.Int) {
if bundle := t.Bundle(); bundle != nil {
bundle.TotalEth.Set(profit)
} else if sbundle := t.SBundle(); sbundle != nil {
sbundle.Profit.Set(profit)
} else {
panic("SetProfit called on unsupported order type")
}
}

// 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.
Expand All @@ -536,7 +574,7 @@ func NewTxWithMinerFee(tx *Transaction, baseFee *big.Int) (*TxWithMinerFee, erro
}

// NewBundleWithMinerFee creates a wrapped bundle.
func NewBundleWithMinerFee(bundle *SimulatedBundle, baseFee *big.Int) (*TxWithMinerFee, error) {
func NewBundleWithMinerFee(bundle *SimulatedBundle, _ *big.Int) (*TxWithMinerFee, error) {
minerFee := bundle.MevGasPrice
return &TxWithMinerFee{
order: _BundleOrder{bundle},
Expand All @@ -545,7 +583,7 @@ func NewBundleWithMinerFee(bundle *SimulatedBundle, baseFee *big.Int) (*TxWithMi
}

// NewSBundleWithMinerFee creates a wrapped bundle.
func NewSBundleWithMinerFee(sbundle *SimSBundle, baseFee *big.Int) (*TxWithMinerFee, error) {
func NewSBundleWithMinerFee(sbundle *SimSBundle, _ *big.Int) (*TxWithMinerFee, error) {
minerFee := sbundle.MevGasPrice
return &TxWithMinerFee{
order: _SBundleOrder{sbundle},
Expand Down Expand Up @@ -683,6 +721,34 @@ func (t *TransactionsByPriceAndNonce) Shift() {
heap.Pop(&t.heads)
}

// ShiftAndPushByAccountForTx attempts to update the transaction list associated with a given account address
// based on the input transaction account. If the associated account exists and has additional transactions,
// the top of the transaction list is popped and pushed to the heap.
// Note that this operation should only be performed when the head transaction on the heap is different from the
// input transaction. This operation is useful in scenarios where the current best head transaction for an account
// was already popped from the heap and we want to process the next one from the same account.
func (t *TransactionsByPriceAndNonce) ShiftAndPushByAccountForTx(tx *Transaction) {
if tx == nil {
return
}

acc, _ := Sender(t.signer, tx)
if txs, exists := t.txs[acc]; exists && len(txs) > 0 {
if wrapped, err := NewTxWithMinerFee(txs[0], t.baseFee); err == nil {
t.txs[acc] = txs[1:]
heap.Push(&t.heads, wrapped)
}
}
}

func (t *TransactionsByPriceAndNonce) Push(tx *TxWithMinerFee) {
if tx == nil {
return
}

heap.Push(&t.heads, tx)
}

// Pop removes the best transaction, *not* replacing it with the next one from
// the same account. This should be used when a transaction cannot be executed
// and hence all subsequent ones should be discarded from the same account.
Expand Down
117 changes: 101 additions & 16 deletions miner/algo_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,49 @@ const (
popTx = 2
)

// defaultProfitPercentMinimum is to ensure committed transactions, bundles, sbundles don't fall below this threshold
// when profit is enforced
const defaultProfitPercentMinimum = 70

var (
defaultProfitThreshold = big.NewInt(defaultProfitPercentMinimum)
defaultAlgorithmConfig = algorithmConfig{
EnforceProfit: false,
ExpectedProfit: common.Big0,
ProfitThresholdPercent: defaultProfitThreshold,
}
)

var emptyCodeHash = common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")

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

// lowProfitError is returned when an order is not committed due to low profit or low effective gas price
type lowProfitError struct {
ExpectedProfit *big.Int
ActualProfit *big.Int

ExpectedEffectiveGasPrice *big.Int
ActualEffectiveGasPrice *big.Int
}

func (e *lowProfitError) Error() string {
return fmt.Sprintf(
"low profit: expected %v, actual %v, expected effective gas price %v, actual effective gas price %v",
e.ExpectedProfit, e.ActualProfit, e.ExpectedEffectiveGasPrice, e.ActualEffectiveGasPrice,
)
}

type algorithmConfig struct {
// EnforceProfit is true if we want to enforce a minimum profit threshold
// for committing a transaction based on ProfitThresholdPercent
EnforceProfit bool
// ExpectedProfit should be set on a per transaction basis when profit is enforced
ExpectedProfit *big.Int
// ProfitThresholdPercent is the minimum profit threshold for committing a transaction
ProfitThresholdPercent *big.Int
}

type chainData struct {
chainConfig *params.ChainConfig
chain *core.BlockChain
Expand Down Expand Up @@ -156,49 +195,51 @@ func (envDiff *environmentDiff) commitTx(tx *types.Transaction, chData chainData

receipt, newState, err := applyTransactionWithBlacklist(signer, chData.chainConfig, chData.chain, coinbase,
envDiff.gasPool, envDiff.state, header, tx, &header.GasUsed, *chData.chain.GetVMConfig(), chData.blacklist)

envDiff.state = newState
if err != nil {
switch {
case errors.Is(err, core.ErrGasLimitReached):
// Pop the current out-of-gas transaction without shifting in the next from the account
from, _ := types.Sender(signer, tx)
log.Trace("Gas limit exceeded for current block", "sender", from)
return nil, popTx, err
return receipt, popTx, err

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

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

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

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

envDiff.newProfit = envDiff.newProfit.Add(envDiff.newProfit, gasPrice.Mul(gasPrice, big.NewInt(int64(receipt.GasUsed))))
envDiff.newTxs = append(envDiff.newTxs, tx)
envDiff.newReceipts = append(envDiff.newReceipts, receipt)

return receipt, shiftTx, nil
}

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

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

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

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

if bundleSimEffGP.Cmp(bundleActualEffGP) == 1 {
if simulatedEGP.Cmp(actualEGP) > 0 {
log.Trace("Bundle underpays after inclusion", "bundle", bundle.OriginalBundle.Hash)
return errors.New("bundle underpays")
return &lowProfitError{
ExpectedEffectiveGasPrice: bundleSimEffGP,
ActualEffectiveGasPrice: bundleActualEffGP,
}
}

if algoConf.EnforceProfit {
// if profit is enforced between simulation and actual commit, only allow ProfitThresholdPercent divergence
simulatedBundleProfit := new(big.Int).Set(bundle.TotalEth)
actualBundleProfit := new(big.Int).Mul(bundleActualEffGP, big.NewInt(int64(gasUsed)))

// We want to make simulated profit smaller to allow for some leeway in cases where the actual profit is
// lower due to transaction ordering
simulatedProfitMultiple := new(big.Int).Mul(simulatedBundleProfit, algoConf.ProfitThresholdPercent)
actualProfitMultiple := new(big.Int).Mul(actualBundleProfit, common.Big100)

if simulatedProfitMultiple.Cmp(actualProfitMultiple) > 0 {
log.Trace("Lower bundle profit found after inclusion", "bundle", bundle.OriginalBundle.Hash)
return &lowProfitError{
ExpectedProfit: simulatedBundleProfit,
ActualProfit: actualBundleProfit,
}
}
}

*envDiff = *tmpEnvDiff
Expand Down Expand Up @@ -395,7 +458,7 @@ 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 {
func (envDiff *environmentDiff) commitSBundle(b *types.SimSBundle, chData chainData, interrupt *int32, key *ecdsa.PrivateKey, algoConf algorithmConfig) error {
if key == nil {
return errors.New("no private key provided")
}
Expand Down Expand Up @@ -423,11 +486,33 @@ func (envDiff *environmentDiff) commitSBundle(b *types.SimSBundle, chData chainD
simEGP := new(big.Int).Set(b.MevGasPrice)

// allow > 1% difference
gotEGP = gotEGP.Mul(gotEGP, big.NewInt(101))
simEGP = simEGP.Mul(simEGP, common.Big100)
actualEGP := new(big.Int).Mul(gotEGP, big.NewInt(101))
simulatedEGP := new(big.Int).Mul(simEGP, common.Big100)

if gotEGP.Cmp(simEGP) < 0 {
return fmt.Errorf("incorrect EGP: got %d, expected %d", gotEGP, simEGP)
if simulatedEGP.Cmp(actualEGP) > 0 {
return &lowProfitError{
ExpectedEffectiveGasPrice: simEGP,
ActualEffectiveGasPrice: gotEGP,
}
}

if algoConf.EnforceProfit {
// if profit is enforced between simulation and actual commit, only allow >-1% divergence
simulatedSbundleProfit := new(big.Int).Set(b.Profit)
actualSbundleProfit := new(big.Int).Set(coinbaseDelta)

// We want to make simulated profit smaller to allow for some leeway in cases where the actual profit is
// lower due to transaction ordering
simulatedProfitMultiple := new(big.Int).Mul(simulatedSbundleProfit, algoConf.ProfitThresholdPercent)
actualProfitMultiple := new(big.Int).Mul(actualSbundleProfit, common.Big100)

if simulatedProfitMultiple.Cmp(actualProfitMultiple) > 0 {
log.Trace("Lower sbundle profit found after inclusion", "sbundle", b.Bundle.Hash())
return &lowProfitError{
ExpectedProfit: simulatedSbundleProfit,
ActualProfit: actualSbundleProfit,
}
}
}

*envDiff = *tmpEnvDiff
Expand Down
8 changes: 4 additions & 4 deletions miner/algo_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func TestBundleCommit(t *testing.T) {
t.Fatal("Failed to simulate bundle", err)
}

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

err = envDiff.commitBundle(&simBundle, chData, nil)
err = envDiff.commitBundle(&simBundle, chData, nil, defaultAlgorithmConfig)
if err == nil {
t.Fatal("Committed failed bundle", err)
}
Expand Down Expand Up @@ -523,7 +523,7 @@ func TestGetSealingWorkAlgos(t *testing.T) {
testConfig.AlgoType = ALGO_MEV_GETH
})

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

for _, algoType := range []AlgoType{ALGO_GREEDY} {
for _, algoType := range []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS} {
var err error
testConfig.BuilderTxSigningKey, err = crypto.GenerateKey()
require.NoError(t, err)
Expand Down
Loading