diff --git a/stackslib/src/chainstate/stacks/tests/block_construction.rs b/stackslib/src/chainstate/stacks/tests/block_construction.rs index 90fc7f1705..79491afa9e 100644 --- a/stackslib/src/chainstate/stacks/tests/block_construction.rs +++ b/stackslib/src/chainstate/stacks/tests/block_construction.rs @@ -30,6 +30,7 @@ use clarity::vm::costs::LimitedCostTracker; use clarity::vm::database::ClarityDatabase; use clarity::vm::test_util::TEST_BURN_STATE_DB; use clarity::vm::types::*; +use mempool::MemPoolWalkStrategy; use rand::seq::SliceRandom; use rand::{thread_rng, Rng}; use rusqlite::params; @@ -5087,3 +5088,247 @@ fn paramaterized_mempool_walk_test( }, ); } + +#[test] +/// Test that the mempool walk query ignores old nonces and prefers next possible nonces before higher global fees. +fn mempool_walk_test_next_nonce_with_highest_fee_rate_strategy() { + let key_address_pairs: Vec<(Secp256k1PrivateKey, StacksAddress)> = (0..7) + .map(|_user_index| { + let privk = StacksPrivateKey::new(); + let addr = StacksAddress::from_public_keys( + C32_ADDRESS_VERSION_TESTNET_SINGLESIG, + &AddressHashMode::SerializeP2PKH, + 1, + &vec![StacksPublicKey::from_private(&privk)], + ) + .unwrap(); + (privk, addr) + }) + .collect(); + let accounts: Vec = key_address_pairs + .iter() + .map(|(_, b)| b.to_string()) + .collect(); + let address_0 = accounts[0].to_string(); + let address_1 = accounts[1].to_string(); + let address_2 = accounts[2].to_string(); + let address_3 = accounts[3].to_string(); + let address_4 = accounts[4].to_string(); + let address_5 = accounts[5].to_string(); + let address_6 = accounts[6].to_string(); + + let test_name = function_name!(); + let mut peer_config = TestPeerConfig::new(&test_name, 0, 0); + peer_config.initial_balances = vec![]; + for (privk, addr) in &key_address_pairs { + peer_config + .initial_balances + .push((addr.to_account_principal(), 1000000000)); + } + + let recipient = + StacksAddress::from_string("ST1RFD5Q2QPK3E0F08HG9XDX7SSC7CNRS0QR0SGEV").unwrap(); + + let mut chainstate = + instantiate_chainstate_with_balances(false, 0x80000000, &test_name, vec![]); + let chainstate_path = chainstate_path(&test_name); + let mut mempool = MemPoolDB::open_test(false, 0x80000000, &chainstate_path).unwrap(); + let b_1 = make_block( + &mut chainstate, + ConsensusHash([0x1; 20]), + &( + FIRST_BURNCHAIN_CONSENSUS_HASH.clone(), + FIRST_STACKS_BLOCK_HASH.clone(), + ), + 1, + 1, + ); + let b_2 = make_block(&mut chainstate, ConsensusHash([0x2; 20]), &b_1, 2, 2); + + let mut tx_events = Vec::new(); + + // Simulate next possible nonces for **some** addresses. Leave some blank so we can test the case where the nonce cannot be + // found on the db table and has to be pulled from the MARF. + let mempool_tx = mempool.tx_begin().unwrap(); + mempool_tx + .execute( + "INSERT INTO nonces (address, nonce) VALUES (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)", + params![address_0, 2, address_1, 1, address_2, 6, address_4, 1, address_5, 0], + ) + .unwrap(); + mempool_tx.commit().unwrap(); + + // Test transactions with a wide variety of origin/sponsor configurations and fee rate values. Some transactions do not have a + // sponsor, some others do, and some others are sponsored by other sponsors. All will be in flight at the same time. + // + // tuple shape: (origin_address_index, origin_nonce, sponsor_address_index, sponsor_nonce, fee_rate) + let test_vectors = vec![ + (0, 0, 0, 0, 100.0), // Old origin nonce - ignored + (0, 1, 0, 1, 200.0), // Old origin nonce - ignored + (0, 2, 0, 2, 300.0), + (0, 3, 0, 3, 400.0), + (0, 4, 3, 0, 500.0), // Nonce 0 for address 3 is not in the table but will be valid on MARF + (1, 0, 1, 0, 400.0), // Old origin nonce - ignored + (1, 1, 3, 1, 600.0), + (1, 2, 3, 2, 700.0), + (1, 3, 3, 3, 800.0), + (1, 4, 1, 4, 1200.0), + (2, 3, 2, 3, 9000.0), // Old origin nonce - ignored + (2, 4, 2, 4, 9000.0), // Old origin nonce - ignored + (2, 5, 2, 5, 9000.0), // Old origin nonce - ignored + (2, 6, 4, 0, 900.0), // Old sponsor nonce - ignored + (2, 6, 4, 1, 1000.0), + (2, 7, 4, 2, 800.0), + (2, 8, 2, 8, 1000.0), + (2, 9, 3, 5, 1000.0), + (2, 10, 3, 6, 1500.0), + (3, 4, 3, 4, 100.0), + (4, 3, 5, 2, 500.0), + (5, 0, 5, 0, 500.0), + (5, 1, 5, 1, 500.0), + (5, 3, 4, 4, 2000.0), + (5, 4, 4, 5, 2000.0), + (6, 2, 6, 2, 1000.0), // Address has nonce 0 in MARF - ignored + ]; + for (origin_index, origin_nonce, sponsor_index, sponsor_nonce, fee_rate) in + test_vectors.into_iter() + { + // Create tx, either standard or sponsored + let mut tx = if origin_index != sponsor_index { + let payload = TransactionPayload::TokenTransfer( + recipient.to_account_principal(), + 1, + TokenTransferMemo([0; 34]), + ); + sign_sponsored_singlesig_tx( + payload.into(), + &key_address_pairs[origin_index].0, + &key_address_pairs[sponsor_index].0, + origin_nonce, + sponsor_nonce, + 200, + ) + } else { + make_user_stacks_transfer( + &key_address_pairs[origin_index].0, + origin_nonce, + 200, + &recipient.to_account_principal(), + 1, + ) + }; + + let mut mempool_tx = mempool.tx_begin().unwrap(); + + let origin_address = tx.origin_address(); + let sponsor_address = tx.sponsor_address().unwrap_or(origin_address); + tx.set_tx_fee(fee_rate as u64); + let txid = tx.txid(); + let tx_bytes = tx.serialize_to_vec(); + let tx_fee = tx.get_tx_fee(); + let height = 100; + MemPoolDB::try_add_tx( + &mut mempool_tx, + &mut chainstate, + &b_1.0, + &b_1.1, + true, + txid, + tx_bytes, + tx_fee, + height, + &origin_address, + origin_nonce, + &sponsor_address, + sponsor_nonce, + None, + ) + .unwrap(); + mempool_tx + .execute( + "UPDATE mempool SET fee_rate = ? WHERE txid = ?", + params![Some(fee_rate), &txid], + ) + .unwrap(); + + mempool_tx.commit().unwrap(); + } + + // Visit transactions using the `NextNonceWithHighestFeeRate` strategy. Keep a record of the order of visits so we can compare + // at the end. + let mut mempool_settings = MemPoolWalkSettings::default(); + mempool_settings.strategy = MemPoolWalkStrategy::NextNonceWithHighestFeeRate; + let mut considered_txs = vec![]; + let deadline = get_epoch_time_ms() + 30000; + chainstate.with_read_only_clarity_tx( + &TEST_BURN_STATE_DB, + &StacksBlockHeader::make_index_block_hash(&b_2.0, &b_2.1), + |clarity_conn| { + loop { + if mempool + .iterate_candidates::<_, ChainstateError, _>( + clarity_conn, + &mut tx_events, + mempool_settings.clone(), + |_, available_tx, _| { + considered_txs.push(( + available_tx.tx.metadata.origin_address.to_string(), + available_tx.tx.metadata.origin_nonce, + available_tx.tx.metadata.sponsor_address.to_string(), + available_tx.tx.metadata.sponsor_nonce, + available_tx.tx.metadata.tx_fee, + )); + Ok(Some( + // Generate any success result + TransactionResult::success( + &available_tx.tx.tx, + available_tx.tx.metadata.tx_fee, + StacksTransactionReceipt::from_stx_transfer( + available_tx.tx.tx.clone(), + vec![], + Value::okay(Value::Bool(true)).unwrap(), + ExecutionCost::ZERO, + ), + ) + .convert_to_event(), + )) + }, + ) + .unwrap() + .0 + == 0 + { + break; + } + assert!(get_epoch_time_ms() < deadline, "test timed out"); + } + + // Expected transaction consideration order, sorted by mineable first (next origin+sponsor nonces, highest fee). + // Ignores old and very future nonces. + let expected_tx_order = vec![ + (address_2.clone(), 6, address_4.clone(), 1, 1000), + (address_2.clone(), 7, address_4.clone(), 2, 800), + (address_2.clone(), 8, address_2.clone(), 8, 1000), + (address_5.clone(), 0, address_5.clone(), 0, 500), + (address_5.clone(), 1, address_5.clone(), 1, 500), + (address_4.clone(), 3, address_5.clone(), 2, 500), + (address_5.clone(), 3, address_4.clone(), 4, 2000), + (address_5.clone(), 4, address_4.clone(), 5, 2000), + (address_0.clone(), 2, address_0.clone(), 2, 300), + (address_0.clone(), 3, address_0.clone(), 3, 400), + (address_0.clone(), 4, address_3.clone(), 0, 500), + (address_1.clone(), 1, address_3.clone(), 1, 600), + (address_1.clone(), 2, address_3.clone(), 2, 700), + (address_1.clone(), 3, address_3.clone(), 3, 800), + (address_1.clone(), 4, address_1.clone(), 4, 1200), + (address_3.clone(), 4, address_3.clone(), 4, 100), + (address_2.clone(), 9, address_3.clone(), 5, 1000), + (address_2.clone(), 10, address_3.clone(), 6, 1500), + ]; + assert_eq!( + considered_txs, expected_tx_order, + "Mempool should visit transactions in the correct order while ignoring past nonces", + ); + }, + ); +} diff --git a/stackslib/src/chainstate/stacks/tests/mod.rs b/stackslib/src/chainstate/stacks/tests/mod.rs index 9a6a84507e..6e2ba7b448 100644 --- a/stackslib/src/chainstate/stacks/tests/mod.rs +++ b/stackslib/src/chainstate/stacks/tests/mod.rs @@ -1396,6 +1396,37 @@ pub fn sign_standard_singlesig_tx( tx_signer.get_tx().unwrap() } +pub fn sign_sponsored_singlesig_tx( + payload: TransactionPayload, + origin: &StacksPrivateKey, + sponsor: &StacksPrivateKey, + origin_nonce: u64, + sponsor_nonce: u64, + tx_fee: u64, +) -> StacksTransaction { + let mut origin_spending_condition = + TransactionSpendingCondition::new_singlesig_p2pkh(StacksPublicKey::from_private(origin)) + .expect("Failed to create p2pkh spending condition from public key."); + origin_spending_condition.set_nonce(origin_nonce); + origin_spending_condition.set_tx_fee(tx_fee); + let mut sponsored_spending_condition = + TransactionSpendingCondition::new_singlesig_p2pkh(StacksPublicKey::from_private(sponsor)) + .expect("Failed to create p2pkh spending condition from public key."); + sponsored_spending_condition.set_nonce(sponsor_nonce); + sponsored_spending_condition.set_tx_fee(tx_fee); + let auth = TransactionAuth::Sponsored(origin_spending_condition, sponsored_spending_condition); + let mut unsigned_tx = StacksTransaction::new(TransactionVersion::Testnet, auth, payload); + + unsigned_tx.chain_id = 0x80000000; + unsigned_tx.post_condition_mode = TransactionPostConditionMode::Allow; + + let mut tx_signer = StacksTransactionSigner::new(&unsigned_tx); + tx_signer.sign_origin(origin).unwrap(); + tx_signer.sign_sponsor(sponsor).unwrap(); + + tx_signer.get_tx().unwrap() +} + pub fn get_stacks_account(peer: &mut TestPeer, addr: &PrincipalData) -> StacksAccount { let account = peer .with_db_state(|ref mut sortdb, ref mut chainstate, _, _| { diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index 513c3ba3b6..48b5ddd457 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -46,7 +46,7 @@ use crate::chainstate::stacks::index::storage::TrieHashCalculationMode; use crate::chainstate::stacks::miner::{BlockBuilderSettings, MinerStatus}; use crate::chainstate::stacks::MAX_BLOCK_LEN; use crate::config::chain_data::MinerStats; -use crate::core::mempool::{MemPoolWalkSettings, MemPoolWalkTxTypes}; +use crate::core::mempool::{MemPoolWalkSettings, MemPoolWalkStrategy, MemPoolWalkTxTypes}; use crate::core::{ MemPoolDB, StacksEpoch, StacksEpochExtension, StacksEpochId, BITCOIN_TESTNET_FIRST_BLOCK_HEIGHT, BITCOIN_TESTNET_STACKS_25_BURN_HEIGHT, @@ -1064,6 +1064,7 @@ impl Config { BlockBuilderSettings { max_miner_time_ms: miner_config.nakamoto_attempt_time_ms, mempool_settings: MemPoolWalkSettings { + strategy: miner_config.mempool_walk_strategy, max_walk_time_ms: miner_config.nakamoto_attempt_time_ms, consider_no_estimate_tx_prob: miner_config.probability_pick_no_estimate_tx, nonce_cache_size: miner_config.nonce_cache_size, @@ -1107,6 +1108,7 @@ impl Config { // second or later attempt to mine a block -- give it some time miner_config.subsequent_attempt_time_ms }, + strategy: miner_config.mempool_walk_strategy, consider_no_estimate_tx_prob: miner_config.probability_pick_no_estimate_tx, nonce_cache_size: miner_config.nonce_cache_size, candidate_retry_cache_size: miner_config.candidate_retry_cache_size, @@ -2102,6 +2104,8 @@ pub struct MinerConfig { pub microblock_attempt_time_ms: u64, /// Max time to assemble Nakamoto block pub nakamoto_attempt_time_ms: u64, + /// Strategy to follow when picking next mempool transactions to consider. + pub mempool_walk_strategy: MemPoolWalkStrategy, pub probability_pick_no_estimate_tx: u8, pub block_reward_recipient: Option, /// If possible, mine with a p2wpkh address @@ -2182,6 +2186,7 @@ impl Default for MinerConfig { activated_vrf_key_path: None, fast_rampup: false, underperform_stop_threshold: None, + mempool_walk_strategy: MemPoolWalkStrategy::GlobalFeeRate, txs_to_consider: MemPoolWalkTxTypes::all(), filter_origins: HashSet::new(), max_reorg_depth: 3, @@ -2560,6 +2565,7 @@ pub struct MinerConfigFile { pub subsequent_attempt_time_ms: Option, pub microblock_attempt_time_ms: Option, pub nakamoto_attempt_time_ms: Option, + pub mempool_walk_strategy: Option, pub probability_pick_no_estimate_tx: Option, pub block_reward_recipient: Option, pub segwit: Option, @@ -2677,6 +2683,9 @@ impl MinerConfigFile { activated_vrf_key_path: self.activated_vrf_key_path.clone(), fast_rampup: self.fast_rampup.unwrap_or(miner_default_config.fast_rampup), underperform_stop_threshold: self.underperform_stop_threshold, + mempool_walk_strategy: self.mempool_walk_strategy + .map(|s| str::parse(&s).unwrap_or_else(|e| panic!("Could not parse '{s}': {e}"))) + .unwrap_or(MemPoolWalkStrategy::GlobalFeeRate), txs_to_consider: { if let Some(txs_to_consider) = &self.txs_to_consider { txs_to_consider diff --git a/stackslib/src/core/mempool.rs b/stackslib/src/core/mempool.rs index 46ff54924b..066c8ba2ac 100644 --- a/stackslib/src/core/mempool.rs +++ b/stackslib/src/core/mempool.rs @@ -29,7 +29,8 @@ use rand::distributions::Uniform; use rand::prelude::Distribution; use rusqlite::types::ToSql; use rusqlite::{ - params, Connection, Error as SqliteError, OpenFlags, OptionalExtension, Row, Rows, Transaction, + params, Connection, Error as SqliteError, OpenFlags, OptionalExtension, Row, Rows, Statement, + Transaction, }; use siphasher::sip::SipHasher; // this is SipHash-2-4 use stacks_common::codec::{ @@ -519,14 +520,40 @@ impl MemPoolWalkTxTypes { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MemPoolWalkStrategy { + /// Select transactions with the highest global fee rate. + GlobalFeeRate, + /// Select transactions with the next expected nonce for origin and sponsor addresses, + NextNonceWithHighestFeeRate, +} + +impl FromStr for MemPoolWalkStrategy { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + "GlobalFeeRate" => { + return Ok(Self::GlobalFeeRate); + } + "NextNonceWithHighestFeeRate" => { + return Ok(Self::NextNonceWithHighestFeeRate); + } + _ => { + return Err("Unknown mempool walk strategy"); + } + } + } +} + #[derive(Debug, Clone)] pub struct MemPoolWalkSettings { + /// Strategy to use when selecting the next transactions to consider in the `mempool` table. + pub strategy: MemPoolWalkStrategy, /// Maximum amount of time a miner will spend walking through mempool transactions, in /// milliseconds. This is a soft deadline. pub max_walk_time_ms: u64, /// Probability percentage to consider a transaction which has not received a cost estimate. - /// That is, with x%, when picking the next transaction to include a block, select one that - /// either failed to get a cost estimate or has not been estimated yet. + /// Only used when walk strategy is `GlobalFeeRate`. pub consider_no_estimate_tx_prob: u8, /// Size of the nonce cache. This avoids MARF look-ups. pub nonce_cache_size: u64, @@ -545,6 +572,7 @@ pub struct MemPoolWalkSettings { impl Default for MemPoolWalkSettings { fn default() -> Self { MemPoolWalkSettings { + strategy: MemPoolWalkStrategy::GlobalFeeRate, max_walk_time_ms: u64::MAX, consider_no_estimate_tx_prob: 5, nonce_cache_size: 1024 * 1024, @@ -564,6 +592,7 @@ impl Default for MemPoolWalkSettings { impl MemPoolWalkSettings { pub fn zero() -> MemPoolWalkSettings { MemPoolWalkSettings { + strategy: MemPoolWalkStrategy::GlobalFeeRate, max_walk_time_ms: u64::MAX, consider_no_estimate_tx_prob: 5, nonce_cache_size: 1024 * 1024, @@ -820,6 +849,18 @@ const MEMPOOL_SCHEMA_7_TIME_ESTIMATES: &'static [&'static str] = &[ "#, ]; +const MEMPOOL_SCHEMA_8_NONCE_SORTING: &'static [&'static str] = &[ + r#" + -- Drop redundant mempool indexes, covered by unique constraints + DROP INDEX IF EXISTS "by_txid"; + DROP INDEX IF EXISTS "by_sponsor"; + DROP INDEX IF EXISTS "by_origin"; + "#, + r#" + INSERT INTO schema_version (version) VALUES (8) + "#, +]; + const MEMPOOL_INDEXES: &'static [&'static str] = &[ "CREATE INDEX IF NOT EXISTS by_txid ON mempool(txid);", "CREATE INDEX IF NOT EXISTS by_height ON mempool(height);", @@ -1307,6 +1348,9 @@ impl MemPoolDB { MemPoolDB::instantiate_schema_7(tx)?; } 7 => { + MemPoolDB::instantiate_schema_8(tx)?; + } + 8 => { break; } _ => { @@ -1393,6 +1437,16 @@ impl MemPoolDB { Ok(()) } + /// Optimize indexes for mempool visits + #[cfg_attr(test, mutants::skip)] + fn instantiate_schema_8(tx: &DBTx) -> Result<(), db_error> { + for sql_exec in MEMPOOL_SCHEMA_8_NONCE_SORTING { + tx.execute_batch(sql_exec)?; + } + + Ok(()) + } + #[cfg_attr(test, mutants::skip)] pub fn db_path(chainstate_root_path: &str) -> Result { let mut path = PathBuf::from(chainstate_root_path); @@ -1645,8 +1699,6 @@ impl MemPoolDB { debug!("Mempool walk for {}ms", settings.max_walk_time_ms,); - let tx_consideration_sampler = Uniform::new(0, 100); - let mut rng = rand::thread_rng(); let mut candidate_cache = CandidateCache::new(settings.candidate_retry_cache_size); let mut nonce_cache = NonceCache::new(settings.nonce_cache_size); @@ -1654,32 +1706,81 @@ impl MemPoolDB { // single transaction. This cannot grow to more than `settings.nonce_cache_size` entries. let mut retry_store = HashMap::new(); + // == Queries for `GlobalFeeRate` mempool walk strategy + // + // Selects mempool transactions only based on their fee rate. Transactions with NULL fee rates get randomly selected for + // consideration. + let tx_consideration_sampler = Uniform::new(0, 100); + let mut rng = rand::thread_rng(); let sql = " - SELECT txid, origin_nonce, origin_address, sponsor_nonce, sponsor_address, fee_rate - FROM mempool - WHERE fee_rate IS NULL - "; - let mut query_stmt_null = self - .db - .prepare(&sql) - .map_err(|err| Error::SqliteError(err))?; + SELECT txid, origin_nonce, origin_address, sponsor_nonce, sponsor_address, fee_rate + FROM mempool + WHERE fee_rate IS NULL + "; + let mut query_stmt_null = self.db.prepare(&sql).map_err(Error::SqliteError)?; let mut null_iterator = query_stmt_null .query(NO_PARAMS) - .map_err(|err| Error::SqliteError(err))?; - + .map_err(Error::SqliteError)?; let sql = " SELECT txid, origin_nonce, origin_address, sponsor_nonce, sponsor_address, fee_rate FROM mempool WHERE fee_rate IS NOT NULL ORDER BY fee_rate DESC "; - let mut query_stmt_fee = self - .db - .prepare(&sql) - .map_err(|err| Error::SqliteError(err))?; + let mut query_stmt_fee = self.db.prepare(&sql).map_err(Error::SqliteError)?; let mut fee_iterator = query_stmt_fee .query(NO_PARAMS) - .map_err(|err| Error::SqliteError(err))?; + .map_err(Error::SqliteError)?; + + // == Query for `NextNonceWithHighestFeeRate` mempool walk strategy + // + // Selects the next mempool transaction to consider 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 to consider only those that have the next expected nonce for both the origin and sponsor, + // when possible + // 2. Adds a "simulated" fee rate to transactions that don't have it by multiplying the mempool's maximum current fee rate + // by a random number. This helps us mix these transactions with others to guarantee they get processed in a reasonable + // order + // 3. Ranks transactions by prioritizing those with next nonces and higher fees (per origin and sponsor address) + // 4. Takes the top ranked transaction and returns it for evaluation + // + // This logic prevents miners from repeatedly visiting (and then skipping) high fee transactions that would get evaluated + // first based on their `fee_rate` but are otherwise non-mineable because they have very high or invalid nonces. A large + // volume of these transactions would cause considerable slowness when selecting valid transactions to mine. This query + // also makes sure transactions that have NULL `fee_rate`s are visited, because they will also get ranked according to + // their origin address nonce. + let sql = " + WITH nonce_filtered AS ( + SELECT txid, origin_nonce, origin_address, sponsor_nonce, sponsor_address, fee_rate, + CASE + WHEN fee_rate IS NULL THEN (ABS(RANDOM()) % 10000 / 10000.0) * (SELECT MAX(fee_rate) FROM mempool) + ELSE fee_rate + END AS sort_fee_rate + FROM mempool AS m + LEFT JOIN nonces AS no ON m.origin_address = no.address + LEFT JOIN nonces AS ns ON m.sponsor_address = ns.address + WHERE (no.address IS NULL OR m.origin_nonce = no.nonce) + AND (ns.address IS NULL OR m.sponsor_nonce = ns.nonce) + ), + address_nonce_ranked AS ( + SELECT *, + ROW_NUMBER() OVER ( + PARTITION BY origin_address + ORDER BY origin_nonce ASC, sort_fee_rate DESC + ) AS origin_rank, + ROW_NUMBER() OVER ( + PARTITION BY sponsor_address + ORDER BY sponsor_nonce ASC, sort_fee_rate DESC + ) AS sponsor_rank + FROM nonce_filtered + ) + SELECT txid, origin_nonce, origin_address, sponsor_nonce, sponsor_address, fee_rate + FROM address_nonce_ranked + ORDER BY origin_rank ASC, sponsor_rank ASC, sort_fee_rate DESC + LIMIT 1 + "; + let mut query_stmt_nonce_rank = self.db.prepare(&sql).map_err(Error::SqliteError)?; let stop_reason = loop { if start_time.elapsed().as_millis() > settings.max_walk_time_ms as u128 { @@ -1688,9 +1789,6 @@ impl MemPoolDB { break MempoolIterationStopReason::DeadlineReached; } - let start_with_no_estimate = - tx_consideration_sampler.sample(&mut rng) < settings.consider_no_estimate_tx_prob; - // First, try to read from the retry list let (candidate, update_estimate) = match candidate_cache.next() { Some(tx) => { @@ -1698,31 +1796,52 @@ impl MemPoolDB { (tx, update_estimate) } None => { - // When the retry list is empty, read from the mempool db, - // randomly selecting from either the null fee-rate transactions - // or those with fee-rate estimates. - let opt_tx = if start_with_no_estimate { - null_iterator - .next() - .map_err(|err| Error::SqliteError(err))? - } else { - fee_iterator.next().map_err(|err| Error::SqliteError(err))? - }; - match opt_tx { - Some(row) => (MemPoolTxInfoPartial::from_row(row)?, start_with_no_estimate), - None => { - // If the selected iterator is empty, check the other - match if start_with_no_estimate { - fee_iterator.next().map_err(|err| Error::SqliteError(err))? + // When the retry list is empty, read from the mempool db depending on the configured miner strategy + match settings.strategy { + MemPoolWalkStrategy::GlobalFeeRate => { + let start_with_no_estimate = tx_consideration_sampler.sample(&mut rng) + < settings.consider_no_estimate_tx_prob; + // randomly select from either the null fee-rate transactions or those with fee-rate estimates. + let opt_tx = if start_with_no_estimate { + null_iterator.next().map_err(Error::SqliteError)? } else { - null_iterator - .next() - .map_err(|err| Error::SqliteError(err))? - } { - Some(row) => ( - MemPoolTxInfoPartial::from_row(row)?, - !start_with_no_estimate, - ), + fee_iterator.next().map_err(Error::SqliteError)? + }; + match opt_tx { + Some(row) => { + (MemPoolTxInfoPartial::from_row(row)?, start_with_no_estimate) + } + None => { + // If the selected iterator is empty, check the other + match if start_with_no_estimate { + fee_iterator.next().map_err(Error::SqliteError)? + } else { + null_iterator.next().map_err(Error::SqliteError)? + } { + Some(row) => ( + MemPoolTxInfoPartial::from_row(row)?, + !start_with_no_estimate, + ), + None => { + debug!("No more transactions to consider in mempool"); + break MempoolIterationStopReason::NoMoreCandidates; + } + } + } + } + } + MemPoolWalkStrategy::NextNonceWithHighestFeeRate => { + match query_stmt_nonce_rank + .query(NO_PARAMS) + .map_err(Error::SqliteError)? + .next() + .map_err(Error::SqliteError)? + { + Some(row) => { + let tx = MemPoolTxInfoPartial::from_row(row)?; + let update_estimate = tx.fee_rate.is_none(); + (tx, update_estimate) + } None => { debug!("No more transactions to consider in mempool"); break MempoolIterationStopReason::NoMoreCandidates; @@ -1929,9 +2048,10 @@ impl MemPoolDB { // connection prevents us from beginning a transaction below (which requires a mutable // borrow). drop(null_iterator); - drop(fee_iterator); drop(query_stmt_null); + drop(fee_iterator); drop(query_stmt_fee); + drop(query_stmt_nonce_rank); if retry_store.len() > 0 { let tx = self.tx_begin()?;