Skip to content
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

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from

Conversation

rafaelcr
Copy link

@rafaelcr rafaelcr commented Dec 7, 2024

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:

  1. Since it does not consider transaction nonces, non-mineable transactions with high fees but old (or very future) nonces go to the top of the list and get visited every time, thus wasting block construction time budget.
  2. Transactions with null fee rates are sent to the bottom of the list even if their nonces should be evaluated next, forcing the algorithm to query them separately with some probability of inclusion.

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.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

1 similar comment
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@rafaelcr rafaelcr changed the title (draft) fix: update miner mempool transaction iterator query to consider fee rate and nonce fix: update miner mempool transaction iterator query to consider fee rate and nonce Dec 15, 2024
@rafaelcr rafaelcr marked this pull request as ready for review December 15, 2024 00:09
@rafaelcr rafaelcr requested a review from a team as a code owner December 15, 2024 00:09
@rafaelcr rafaelcr changed the title fix: update miner mempool transaction iterator query to consider fee rate and nonce fix: update miner mempool iterator query to consider both nonces and fee rates Dec 15, 2024
Comment on lines 5256 to 5269
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),
],
Copy link
Contributor

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.

Copy link
Author

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great test!

Copy link
Author

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

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Author

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

@obycode obycode added this to the 3.1.0.0.3 milestone Dec 16, 2024
@obycode obycode self-requested a review December 16, 2024 20:32
Comment on lines +1700 to +1701
LEFT JOIN nonces ON mempool.origin_address = nonces.address
WHERE nonces.address IS NULL OR mempool.origin_nonce >= nonces.nonce
Copy link
Contributor

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";
Copy link
Member

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);
Copy link
Member

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
Copy link
Member

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 (
Copy link
Member

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.

Copy link
Member

@jcnelson jcnelson left a 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?

@rafaelcr
Copy link
Author

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 {
Copy link
Member

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.

Copy link
Author

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
Copy link
Member

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?

Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Status: In Review
Development

Successfully merging this pull request may close these issues.

4 participants