In the current fee market, which is a first-price auction mechanism, one strategy to optimize the miner's fee revenue is to put transactions into a block in a gas_price
order.
NOTE: Strictly speaking, although nonce also needs to be considered, it is not be considered in the following sections because it does not affect the computational complexity.
If a txpool is implemented as a vector, it must be sorted in gas_price
order before creating a block. The computational complexity of sorting is
If a txpool is implemented as a heap, there will be no need to sort. Adding a transaction takes
If a txpool is implemented as an SBST, there will be no need to sort as well as a heap implementation. Adding a transaction takes
Operation | Vector | Heap | SBST |
---|---|---|---|
Add a transaction | |||
Delete a transaction | |||
Pop the most profitable transaction | |||
Sort transactions | - | - |
The space complexity is
An EIP-1559 transaction has the parameters base_fee
and fee_cap
. The miner_bribe
is min(fee_cap - base_fee, max_miner_bribe)
, and it is optimal to put transactions into a block in the order of miner_bribe
.
However, miner_bribe
depends on base_fee
. A fast transaction pool is non-trivial.
According to https://hackmd.io/@adietrichs/1559-transaction-sorting , when base_fee
changes, there are two transaction sets: one in which miner_bribe
changes and one in which miner_bribe
does not change. Let us denote these sets as base_fee
change.
By using this property well, an efficient transaction pool can be constructed.
The txpool is consists of three SBSTs.
sbst_static
)
fee_cap - max_miner_bribe >= base_fee
(max_miner_bribe, transaction_hash)
transaction
sbst_dynamic
)
fee_cap - max_miner_bribe < base_fee
(fee_cap, transaction_hash)
transaction
sbst_decision
)
fee_cap - max_miner_bribe
transaction
NOTE: I have included the implementations below, but they are implemented in C++ because Python does not have SBST in the standard library. Other language implementations such as Python and Rust are WIP.
|Operation|Vector|SBST (Proposed)|
|-|-|-|-|
|Add a transaction|
|Delete a transaction|
|Pop the most profitable transaction|
|Sort transactions when basefee changes |
The space complexity is
The most profitable transaction is either of the following two.
max_miner_bribe
in sbst_static
is sorted in order of max_miner_bribe
because its key is (max_miner_bribe, transaction_hash)
. Hence, finding and deleting
fee_cap
in sbst_dynamic
is sorted in order of fee_cap
because its key is (fee_cap, transaction_hash)
. Hence, finding and deleting
By comparing the revenue of
Also, we have to delete the transaction from sbst_decision
(explained later) in
Therefore, the overall computational complexity is
Implementation
I left it out of the explanation, but I inserted a -fee_cap
between the first and second elements of the key. The reason is that when there are transactions with the same max_miner_bribe
, it is better to take the one with the smaller fee_cap
. When the next base_fee
is high, transactions with a small fee_cap
may have a smaller revenue.
Tx pop_most_profitable_tx() {
Tx tx;
assert(sbst_decision.size() > 0);
if(sbst_static.size() == 0) {
tx = sbst_dynamic.rbegin()->second;
sbst_dynamic.erase(prev(sbst_dynamic.end()));
} else if(sbst_dynamic.size() == 0) {
tx = sbst_static.rbegin()->second;
sbst_static.erase(prev(sbst_static.end()));
} else {
Tx tx_static = sbst_static.rbegin()->second;
Tx tx_dynamic = sbst_dynamic.rbegin()->second;
if(tx_static.miner_bribe(base_fee) >
tx_dynamic.miner_bribe(base_fee)) {
tx = tx_static;
sbst_static.erase(prev(sbst_static.end()));
} else {
tx = tx_dynamic;
sbst_dynamic.erase(prev(sbst_dynamic.end()));
}
}
sbst_decision.erase(sbst_decision.find(
Key(tx.fee_cap - tx.max_miner_bribe, -tx.fee_cap, tx.hash)));
return tx;
}
Suppose we select the
If fee_cap - base_fee >= max_miner_bribe
, we add the transaction to sbst_static
in
If fee_cap - base_fee < max_miner_bribe
, we add the transaction to sbst_dynamic
in
It is also added to sbst_decision
(explained later) in
Therefore, the overall computational complexity is
Implementation
void add_tx(Tx tx) {
if(tx.fee_cap - base_fee >= tx.max_miner_bribe) {
sbst_static.emplace(Key(tx.max_miner_bribe, -tx.fee_cap, tx.hash),
tx);
} else {
sbst_dynamic.emplace(Key(tx.fee_cap, -tx.fee_cap, tx.hash), tx);
}
sbst_decision.emplace(
Key(tx.fee_cap - tx.max_miner_bribe, -tx.fee_cap, tx.hash), tx);
}
When the base_fee changes, do the following.
Let us denote the previous basefee by prev_base_fee
.
prev_base_fee
< base_fee
We need to move the transaction that moves from sbst_static
to sbst_dynamic
.
The conditions of the transactions are as follows.
fee_cap - max_miner_bribe >= prev_base_fee
fee_cap - max_miner_bribe < base_fee
To check the conditions, sbst_decision
is used. The key of sbst_decision
is fee_cap - max_miner_bribe
. Thus, by performing a binary search by prev_base_fee
and then by base_fee
, the transaction set to be moved can be determined in
The move takes sbst_static
and add the transaction to sbst_dynamic
.
If the number of its moving transactions is k
is small if the change of basefee is small.
Implementation
auto left_tx =
sbst_decision.lower_bound(Key(prev_base_fee, -INT32_MAX, 0));
auto right_tx =
sbst_decision.lower_bound(Key(base_fee, -INT32_MAX, 0));
for(auto pointer = left_tx; pointer != right_tx; pointer++) {
Tx tx = pointer->second;
if(tx.fee_cap - tx.max_miner_bribe >= prev_base_fee) {
sbst_static.erase(sbst_static.find(
Key(tx.max_miner_bribe, -tx.fee_cap, tx.hash)));
add_tx(tx);
}
}
base_fee
< prev_base_fee
We need to move the transaction that moves from sbst_dynamic
to sbst_static
. The computational complexity is
Implementation
auto left_tx =
sbst_decision.lower_bound(Key(base_fee, -INT32_MAX, 0));
auto right_tx =
sbst_decision.lower_bound(Key(prev_base_fee, -INT32_MAX, 0));
for(auto pointer = left_tx; pointer != right_tx; pointer++) {
Tx tx = pointer->second;
if(tx.fee_cap - tx.max_miner_bribe < prev_base_fee) {
sbst_dynamic.erase(sbst_dynamic.find(
Key(tx.fee_cap, -tx.fee_cap, tx.hash)));
add_tx(tx);
}
}