Skip to content

Commit 9cf1ed1

Browse files
committed
add block_limit_hit test for NextNonceWithHighestFeeRate
1 parent 2935a9d commit 9cf1ed1

File tree

1 file changed

+171
-40
lines changed

1 file changed

+171
-40
lines changed

testnet/stacks-node/src/tests/neon_integrations.rs

Lines changed: 171 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use stacks::clarity_cli::vm_execute as execute;
3939
use stacks::cli;
4040
use stacks::codec::StacksMessageCodec;
4141
use stacks::config::{EventKeyType, EventObserverConfig, FeeEstimatorName, InitialBalance};
42-
use stacks::core::mempool::MemPoolWalkTxTypes;
42+
use stacks::core::mempool::{MemPoolWalkStrategy, MemPoolWalkTxTypes};
4343
use stacks::core::test_util::{
4444
make_contract_call, make_contract_publish, make_contract_publish_microblock_only,
4545
make_microblock, make_stacks_transfer_mblock_only, make_stacks_transfer_serialized, to_addr,
@@ -4527,19 +4527,26 @@ fn mining_events_integration_test() {
45274527
channel.stop_chains_coordinator();
45284528
}
45294529

4530-
/// This test checks that the limit behavior in the miner works as expected for anchored block
4531-
/// building. When we first hit the block limit, the limit behavior switches to
4532-
/// `CONTRACT_LIMIT_HIT`, during which stx transfers are still allowed, and contract related
4533-
/// transactions are skipped.
4534-
/// Note: the test is sensitive to the order in which transactions are mined; it is written
4535-
/// expecting that transactions are traversed in the order tx_1, tx_2, tx_3, and tx_4.
4536-
#[test]
4537-
#[ignore]
4538-
fn block_limit_hit_integration_test() {
4539-
if env::var("BITCOIND_TEST") != Ok("1".into()) {
4540-
return;
4541-
}
4542-
4530+
/// Sets up and runs a block limit integration test with the specified mempool walk strategy.
4531+
///
4532+
/// This function creates a controlled test environment to verify how different mempool walking
4533+
/// strategies affect transaction ordering when block limits are hit. It simulates a scenario
4534+
/// where:
4535+
///
4536+
/// - tx1: High-fee (555k) oversize contract from addr1 (nonce 0) - (oversize - causes block limit)
4537+
/// - tx2: High-fee (555k) oversize contract from addr1 (nonce 1) - (oversize - depends on tx1)
4538+
/// - tx3: Lower-fee (150k) medium contract from addr2 (nonce 0) - (medium size)
4539+
/// - tx4: Low-fee (180) STX transfer from addr3 (nonce 0) - (tiny)
4540+
///
4541+
/// The key difference between strategies:
4542+
/// - GlobalFeeRate: Prioritizes by fee rate globally, uses candidate_cache for nonce-invalid transactions
4543+
/// - NextNonceWithHighestFeeRate: Pre-filters to only valid-nonce transactions, then prioritizes by fee within account groups
4544+
///
4545+
/// # Test Environment Setup
4546+
/// - 3 accounts with 10M STX each
4547+
/// - Bitcoin regtest with 201 blocks bootstrapped
4548+
/// - Microblocks enabled with 30s wait time
4549+
fn setup_block_limit_test(strategy: MemPoolWalkStrategy) -> (Vec<serde_json::Value>, [String; 4]) {
45434550
// 700 invocations
45444551
let max_contract_src = format!(
45454552
"(define-private (work) (begin {} 1))
@@ -4588,22 +4595,23 @@ fn block_limit_hit_integration_test() {
45884595
let spender_sk = StacksPrivateKey::random();
45894596
let addr = to_addr(&spender_sk);
45904597
let second_spender_sk = StacksPrivateKey::random();
4591-
let second_spender_addr: PrincipalData = to_addr(&second_spender_sk).into();
4598+
let second_spender_addr = to_addr(&second_spender_sk);
45924599
let third_spender_sk = StacksPrivateKey::random();
4593-
let third_spender_addr: PrincipalData = to_addr(&third_spender_sk).into();
4600+
let third_spender_addr = to_addr(&third_spender_sk);
45944601

45954602
let (mut conf, _miner_account) = neon_integration_test_conf();
4603+
conf.miner.mempool_walk_strategy = strategy;
45964604

45974605
conf.initial_balances.push(InitialBalance {
45984606
address: addr.into(),
45994607
amount: 10_000_000,
46004608
});
46014609
conf.initial_balances.push(InitialBalance {
4602-
address: second_spender_addr.clone(),
4610+
address: second_spender_addr.into(),
46034611
amount: 10_000_000,
46044612
});
46054613
conf.initial_balances.push(InitialBalance {
4606-
address: third_spender_addr.clone(),
4614+
address: third_spender_addr.into(),
46074615
amount: 10_000_000,
46084616
});
46094617

@@ -4702,6 +4710,7 @@ fn block_limit_hit_integration_test() {
47024710
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
47034711
sleep_ms(20_000);
47044712

4713+
// Verify nonces
47054714
let res = get_account(&http_origin, &addr);
47064715
assert_eq!(res.nonce, 2);
47074716

@@ -4714,30 +4723,152 @@ fn block_limit_hit_integration_test() {
47144723
let mined_block_events = test_observer::get_blocks();
47154724
assert_eq!(mined_block_events.len(), 5);
47164725

4717-
let tx_third_block = mined_block_events[3]
4718-
.get("transactions")
4719-
.unwrap()
4720-
.as_array()
4721-
.unwrap();
4722-
assert_eq!(tx_third_block.len(), 3);
4723-
let txid_1_exp = tx_third_block[1].get("txid").unwrap().as_str().unwrap();
4724-
let txid_4_exp = tx_third_block[2].get("txid").unwrap().as_str().unwrap();
4725-
assert_eq!(format!("0x{txid_1}"), txid_1_exp);
4726-
assert_eq!(format!("0x{txid_4}"), txid_4_exp);
4727-
4728-
let tx_fourth_block = mined_block_events[4]
4729-
.get("transactions")
4730-
.unwrap()
4731-
.as_array()
4732-
.unwrap();
4733-
assert_eq!(tx_fourth_block.len(), 3);
4734-
let txid_2_exp = tx_fourth_block[1].get("txid").unwrap().as_str().unwrap();
4735-
let txid_3_exp = tx_fourth_block[2].get("txid").unwrap().as_str().unwrap();
4736-
assert_eq!(format!("0x{txid_2}"), txid_2_exp);
4737-
assert_eq!(format!("0x{txid_3}"), txid_3_exp);
4738-
47394726
test_observer::clear();
47404727
channel.stop_chains_coordinator();
4728+
(mined_block_events, [txid_1, txid_2, txid_3, txid_4])
4729+
}
4730+
4731+
/// Tests block limit behavior with GlobalFeeRate mempool walk strategy.
4732+
///
4733+
/// This test verifies that when using GlobalFeeRate strategy, transactions are selected
4734+
/// purely based on fee rate without considering nonce dependencies within accounts.
4735+
///
4736+
/// # Expected Behavior with GlobalFeeRate
4737+
/// The miner processes transactions in pure fee-rate order using candidate caching:
4738+
///
4739+
/// **Initial Processing Order:** tx1/tx2 (555k fee) → tx3 (150k fee) → tx4 (180 fee)
4740+
/// **Block 3 Results:**
4741+
/// - tx1 gets mined (highest fee, valid nonce 0)
4742+
/// - tx2 has invalid nonce (1), cached for later retry
4743+
/// - tx3 blocked by contract limit, deferred to next block
4744+
/// - tx4 gets mined (STX transfers allowed after contract limit)
4745+
///
4746+
/// **Block 4 Results:**
4747+
/// - tx2 now valid (addr1 nonce advanced to 1), gets mined
4748+
/// - tx3 gets mined (no contract limit in new block)
4749+
///
4750+
/// **Final Distribution:**
4751+
/// - Block 3: 3 transactions (coinbase + tx1 + tx4)
4752+
/// - Block 4: 3 transactions (coinbase + tx2 + tx3)
4753+
#[test]
4754+
#[ignore]
4755+
fn block_limit_hit_integration_test_global_fee_rate() {
4756+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
4757+
return;
4758+
}
4759+
4760+
let (events, [txid_1, txid_2, txid_3, txid_4]) =
4761+
setup_block_limit_test(MemPoolWalkStrategy::GlobalFeeRate);
4762+
4763+
// Block 3: should contain tx1 and tx4 (GlobalFeeRate strategy)
4764+
let block3_txs = events[3]["transactions"].as_array().unwrap();
4765+
assert_eq!(
4766+
block3_txs.len(),
4767+
3,
4768+
"Block 3 should have 3 transactions (coinbase + 2 user txs)"
4769+
);
4770+
4771+
let txid_1_exp = block3_txs[1].get("txid").unwrap().as_str().unwrap();
4772+
let txid_4_exp = block3_txs[2].get("txid").unwrap().as_str().unwrap();
4773+
assert_eq!(
4774+
format!("0x{}", txid_1),
4775+
txid_1_exp,
4776+
"tx1 should be in block 3"
4777+
);
4778+
assert_eq!(
4779+
format!("0x{}", txid_4),
4780+
txid_4_exp,
4781+
"tx4 should be in block 3"
4782+
);
4783+
4784+
// Block 4: should contain tx2 and tx3
4785+
let block4_txs = events[4]["transactions"].as_array().unwrap();
4786+
assert_eq!(
4787+
block4_txs.len(),
4788+
3,
4789+
"Block 4 should have 3 transactions (coinbase + 2 user txs)"
4790+
);
4791+
4792+
let txid_2_exp = block4_txs[1].get("txid").unwrap().as_str().unwrap();
4793+
let txid_3_exp = block4_txs[2].get("txid").unwrap().as_str().unwrap();
4794+
assert_eq!(
4795+
format!("0x{}", txid_2),
4796+
txid_2_exp,
4797+
"tx2 should be in block 4"
4798+
);
4799+
assert_eq!(
4800+
format!("0x{}", txid_3),
4801+
txid_3_exp,
4802+
"tx3 should be in block 4"
4803+
);
4804+
}
4805+
4806+
/// Tests block limit behavior with NextNonceWithHighestFeeRate mempool walk strategy.
4807+
///
4808+
/// This test verifies that when using NextNonceWithHighestFeeRate strategy, transactions
4809+
/// are grouped by account and selected based on the next valid nonce, with the strategy
4810+
/// re-querying the mempool after nonce state changes.
4811+
///
4812+
/// # Expected Behavior with NextNonceWithHighestFeeRate
4813+
/// The miner uses a multi-pass approach with nonce-aware querying:
4814+
///
4815+
/// **Pass 1 - Initial Query:** tx1, tx3, tx4 (tx2 filtered out: nonce 1 > expected 0)
4816+
/// **Pass 1 - Processing Order:** tx4 → tx3 → tx1 (account ranking + fee prioritization)
4817+
/// **Pass 1 - Results:**
4818+
/// - tx4 gets mined (STX transfer, low cost)
4819+
/// - tx3 gets mined (medium contract fits remaining budget)
4820+
/// - tx1 gets mined (oversize contract, nearly exhausts block budget)
4821+
/// - Nonce cache updated: addr1 nonce advances to 1
4822+
///
4823+
/// **Pass 2 - Re-query:** tx2 now becomes valid (addr1 nonce = 1)
4824+
/// **Pass 2 - Processing:** tx2 exceeds remaining block budget, skipped to next block
4825+
///
4826+
/// **Block 3 Final:** 4 transactions (coinbase + tx4 + tx3 + tx1)
4827+
/// **Block 4:** 2 transactions (coinbase + tx2)
4828+
#[test]
4829+
#[ignore]
4830+
fn block_limit_hit_integration_test_next_nonce_highest_fee() {
4831+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
4832+
return;
4833+
}
4834+
4835+
let (events, [txid_1, txid_2, txid_3, txid_4]) =
4836+
setup_block_limit_test(MemPoolWalkStrategy::NextNonceWithHighestFeeRate);
4837+
4838+
// Block 3: should contain tx1, tx3, and tx4 (NextNonceWithHighestFeeRate strategy)
4839+
let block3_txs = events[3]["transactions"].as_array().unwrap();
4840+
assert_eq!(block3_txs.len(), 4, "Block 3 should have 4 transactions");
4841+
4842+
// Extract transaction IDs from block 3 (skip coinbase at index 0)
4843+
let block3_txids: Vec<String> = block3_txs[1..]
4844+
.iter()
4845+
.map(|tx| tx.get("txid").unwrap().as_str().unwrap().to_string())
4846+
.collect();
4847+
4848+
// Check that tx1, tx3, and tx4 are in block 3
4849+
assert!(
4850+
block3_txids.contains(&format!("0x{}", txid_1)),
4851+
"tx1 should be in block 3"
4852+
);
4853+
assert!(
4854+
block3_txids.contains(&format!("0x{}", txid_3)),
4855+
"tx3 should be in block 3"
4856+
);
4857+
assert!(
4858+
block3_txids.contains(&format!("0x{}", txid_4)),
4859+
"tx4 should be in block 3"
4860+
);
4861+
4862+
// Block 4: should contain tx2 (NextNonceWithHighestFeeRate strategy)
4863+
let block4_txs = events[4]["transactions"].as_array().unwrap();
4864+
assert_eq!(block4_txs.len(), 2, "Block 4 should have 2 transactions");
4865+
4866+
let txid_2_exp = block4_txs[1].get("txid").unwrap().as_str().unwrap();
4867+
assert_eq!(
4868+
format!("0x{}", txid_2),
4869+
txid_2_exp,
4870+
"tx2 should be in block 4"
4871+
);
47414872
}
47424873

47434874
#[test]

0 commit comments

Comments
 (0)