-
Notifications
You must be signed in to change notification settings - Fork 677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: update miner mempool iterator query to consider both nonces and fee rates #5541
base: develop
Are you sure you want to change the base?
Conversation
|
1 similar comment
|
vec![ | ||
(address_0.clone(), 2), | ||
(address_0.clone(), 3), | ||
(address_0.clone(), 4), | ||
(address_0.clone(), 5), | ||
(address_0.clone(), 6), | ||
(address_1.clone(), 7), // Higher fee for address 1 | ||
(address_0.clone(), 7), | ||
(address_1.clone(), 8), | ||
(address_0.clone(), 8), | ||
(address_2.clone(), 9), // Higher fee for address 2 | ||
(address_1.clone(), 9), | ||
(address_0.clone(), 9), | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the ideal ordering would be:
vec![
(address_2.clone(), 9), // Higher fee for address 2
(address_1.clone(), 7), // Higher fee for address 1
(address_1.clone(), 8),
(address_1.clone(), 9),
(address_0.clone(), 2),
(address_0.clone(), 3),
(address_0.clone(), 4),
(address_0.clone(), 5),
(address_0.clone(), 6),
(address_0.clone(), 7),
(address_0.clone(), 8),
(address_0.clone(), 9),
],
We don't want to penalize ready transactions with higher fees just for having a higher nonce. We want to select all of the transactions with the ready nonce, then order that set by fee rate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch, will make that change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great test!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done @obycode , I had a bug in the WHERE condition for the LEFT JOIN
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. I see how this latest version is basically grouped in sets of nonces that are ready for each round:
- Round 1: (addr 2, nonce 9), (addr 1, nonce 7), (addr 0, nonce 2)
- Round 2: (addr 1, nonce 8), (addr 0, nonce 3)
- Round 3: (addr 1, nonce 9), addr 0, nonce 4)
- Round 4: (addr 0, nonce 5)
- ...
I think this is fine, but is there any good way to handle the chained nonces better? That could be part of a separate PR after this one if there is not a clear solution.
It would be great if we could get a good benchmark setup to compare how changes affect the transaction selection timing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we push that to a separate PR, can you add a comment in this test specifying the optimal ordering I noted earlier, so we can easily come back to this later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean, let me do some tests here and come back to you with an answer
LEFT JOIN nonces ON mempool.origin_address = nonces.address | ||
WHERE nonces.address IS NULL OR mempool.origin_nonce >= nonces.nonce |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also take the sponsor nonce into account similarly?
let address_1 = origin_addresses[1].to_string(); | ||
let address_2 = origin_addresses[2].to_string(); | ||
|
||
let test_name = "mempool_walk_test_nonce_filtered_and_ranked"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: you can use function_name!()
let address_2 = origin_addresses[2].to_string(); | ||
|
||
let test_name = "mempool_walk_test_nonce_filtered_and_ranked"; | ||
let mut peer_config = TestPeerConfig::new(test_name, 2002, 2003); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use TestPeerConfig::new(test_name, 0, 0)
, so the OS will allocate you ports.
// Iterate pending mempool transactions using a heuristic that maximizes miner fee profitability and minimizes CPU time | ||
// wasted on already-mined or not-yet-mineable transactions. This heuristic takes the following steps: | ||
// | ||
// 1. Filters out transactions that have nonces smaller than the origin address' next expected nonce as stated in the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @obycode has pointed out, you will also need to consider sponsor nonces.
LEFT JOIN nonces ON mempool.origin_address = nonces.address | ||
WHERE nonces.address IS NULL OR mempool.origin_nonce >= nonces.nonce | ||
), | ||
address_nonce_ranked AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're going to need two of these subqueries -- one for origin addresses and nonces, and one for sponsor addresses and nonces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking this on @rafaelcr!
This is something that I think is going to need somewhat extensive real-world testing before we make it the default behavior. Do you think you could add this new mempool walk logic as an opt-in alternative, which the node operator can opt to use? This would give miners a chance to compare/contrast the new behavior with the old behavior in an easy-to-rollback manner. A subsequent PR could make the new behavior the default.
What do you think?
Great idea @jcnelson and thanks for the feedback! I'll get on it. |
let mut tx_events = Vec::new(); | ||
|
||
// Submit nonces 0 through 9 for each of the 3 senders. | ||
for nonce in 0..10 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only testing unsponsored transactions. You are going to want to expand this to test sponsored transactions.
In particular, you'll want to test the case where a fixed number of sponsors send a wide variety of sponsored transactions (much like how e.g. a BNS registrar would), and where both the sponsors and sponsored accounts also send their own unsponsored transactions. In addition to testing that the transaction frontier is ordered from highest to lowest by fee, you'd test that the set of subsequence of sponsored transactions present in considered_txs
are exactly the ones with both the next nonces for its origin and sponsor accounts. In other words, a heterogenous mempool with unsponsored and sponsored transactions where origin and sponsor accounts can each have in-flight unsponsored and sponsored transactions still produces the desired priority list of mineable transactions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. Will do. That sounds like a great test.
WHERE fee_rate IS NOT NULL | ||
ORDER BY fee_rate DESC | ||
FROM address_nonce_ranked | ||
ORDER BY rank ASC, sort_fee_rate DESC |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before merging, can you manually confirm that this query does not do any table scans via EXPLAIN QUERY PLAN
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I've been monitoring that and will add any indexes necessary to avoid table scans.
Problem
The current query used by Stacks miners to iterate over pending mempool transactions sorts them only by
fee_rate
in descending order. This approach creates the following problems:Solution
This PR changes the transaction selection query to a new one that prioritizes those that can be confirmed as fast as possible (lowest valid nonces) with the highest fees as possible. This means that even if it doesn't select the transactions with the highest global fees first, it will select those that can be mined faster therefore optimizing block building time and allowing the miner to fit in more transactions in total within the allotted time.
The new query also mixes in transactions with null fee rates in results by simulating a fee rate for the purposes of ordering only.