Skip to content

Commit 3261d0d

Browse files
authored
Merge pull request #5238 from stacks-network/feat/naka-miner-heuristics
Feat: add heuristics to miner for nakamoto
2 parents 4ecfe28 + 86f0c1f commit 3261d0d

File tree

9 files changed

+508
-90
lines changed

9 files changed

+508
-90
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ jobs:
3232
- tests::bitcoin_regtest::bitcoind_integration_test
3333
- tests::integrations::integration_test_get_info
3434
- tests::neon_integrations::antientropy_integration_test
35-
- tests::neon_integrations::bad_microblock_pubkey
3635
- tests::neon_integrations::bitcoind_forking_test
3736
- tests::neon_integrations::bitcoind_integration_test
3837
- tests::neon_integrations::block_large_tx_integration_test
@@ -43,21 +42,26 @@ jobs:
4342
- tests::neon_integrations::fuzzed_median_fee_rate_estimation_test_window10
4443
- tests::neon_integrations::fuzzed_median_fee_rate_estimation_test_window5
4544
- tests::neon_integrations::liquid_ustx_integration
46-
- tests::neon_integrations::microblock_fork_poison_integration_test
47-
- tests::neon_integrations::microblock_integration_test
45+
# Microblock tests that are no longer needed on every CI run
46+
# (microblocks are unsupported starting in Epoch 2.5)
47+
# - tests::neon_integrations::bad_microblock_pubkey
48+
# - tests::neon_integrations::microblock_fork_poison_integration_test
49+
# - tests::neon_integrations::microblock_integration_test
50+
# - tests::neon_integrations::microblock_limit_hit_integration_test
51+
# - tests::neon_integrations::test_problematic_microblocks_are_not_mined
52+
# - tests::neon_integrations::test_problematic_microblocks_are_not_relayed_or_stored
53+
# - tests::neon_integrations::size_overflow_unconfirmed_invalid_stream_microblocks_integration_test
54+
# - tests::neon_integrations::size_overflow_unconfirmed_microblocks_integration_test
55+
# - tests::neon_integrations::size_overflow_unconfirmed_stream_microblocks_integration_test
56+
# - tests::neon_integrations::runtime_overflow_unconfirmed_microblocks_integration_test
4857
# Disable this flaky test. Microblocks are no longer supported anyways.
4958
# - tests::neon_integrations::microblock_large_tx_integration_test_FLAKY
50-
- tests::neon_integrations::microblock_limit_hit_integration_test
5159
- tests::neon_integrations::miner_submit_twice
5260
- tests::neon_integrations::mining_events_integration_test
5361
- tests::neon_integrations::pox_integration_test
5462
- tests::neon_integrations::push_boot_receipts
55-
- tests::neon_integrations::runtime_overflow_unconfirmed_microblocks_integration_test
5663
- tests::neon_integrations::should_fix_2771
5764
- tests::neon_integrations::size_check_integration_test
58-
- tests::neon_integrations::size_overflow_unconfirmed_invalid_stream_microblocks_integration_test
59-
- tests::neon_integrations::size_overflow_unconfirmed_microblocks_integration_test
60-
- tests::neon_integrations::size_overflow_unconfirmed_stream_microblocks_integration_test
6165
- tests::neon_integrations::stx_delegate_btc_integration_test
6266
- tests::neon_integrations::stx_transfer_btc_integration_test
6367
- tests::neon_integrations::stack_stx_burn_op_test
@@ -66,8 +70,6 @@ jobs:
6670
- tests::neon_integrations::test_flash_block_skip_tenure
6771
- tests::neon_integrations::test_problematic_blocks_are_not_mined
6872
- tests::neon_integrations::test_problematic_blocks_are_not_relayed_or_stored
69-
- tests::neon_integrations::test_problematic_microblocks_are_not_mined
70-
- tests::neon_integrations::test_problematic_microblocks_are_not_relayed_or_stored
7173
- tests::neon_integrations::test_problematic_txs_are_not_stored
7274
- tests::neon_integrations::use_latest_tip_integration_test
7375
- tests::neon_integrations::confirm_unparsed_ongoing_ops
@@ -81,7 +83,7 @@ jobs:
8183
- tests::epoch_25::microblocks_disabled
8284
- tests::should_succeed_handling_malformed_and_valid_txs
8385
- tests::nakamoto_integrations::simple_neon_integration
84-
- tests::nakamoto_integrations::simple_neon_integration_with_flash_blocks_on_epoch_3
86+
- tests::nakamoto_integrations::flash_blocks_on_epoch_3
8587
- tests::nakamoto_integrations::mine_multiple_per_tenure_integration
8688
- tests::nakamoto_integrations::block_proposal_api_endpoint
8789
- tests::nakamoto_integrations::miner_writes_proposed_block_to_stackerdb
@@ -90,6 +92,7 @@ jobs:
9092
- tests::nakamoto_integrations::follower_bootup
9193
- tests::nakamoto_integrations::forked_tenure_is_ignored
9294
- tests::nakamoto_integrations::nakamoto_attempt_time
95+
- tests::nakamoto_integrations::skip_mining_long_tx
9396
- tests::signer::v0::block_proposal_rejection
9497
- tests::signer::v0::miner_gather_signatures
9598
- tests::signer::v0::end_of_tenure

stackslib/src/chainstate/stacks/miner.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::collections::{HashMap, HashSet};
1818
use std::sync::atomic::{AtomicBool, Ordering};
1919
use std::sync::{Arc, Mutex};
2020
use std::thread::ThreadId;
21+
use std::time::Instant;
2122
use std::{cmp, fs, mem};
2223

2324
use clarity::vm::analysis::{CheckError, CheckErrors};
@@ -2211,6 +2212,15 @@ impl StacksBlockBuilder {
22112212
);
22122213
}
22132214

2215+
// nakamoto miner tenure start heuristic:
2216+
// mine an empty block so you can start your tenure quickly!
2217+
if let Some(tx) = initial_txs.first() {
2218+
if matches!(&tx.payload, TransactionPayload::TenureChange(_)) {
2219+
info!("Nakamoto miner heuristic: during tenure change blocks, produce a fast short block to begin tenure");
2220+
return Ok((false, tx_events));
2221+
}
2222+
}
2223+
22142224
mempool.reset_nonce_cache()?;
22152225
mempool.estimate_tx_rates(100, &block_limit, &stacks_epoch_id)?;
22162226

@@ -2221,6 +2231,7 @@ impl StacksBlockBuilder {
22212231

22222232
let mut invalidated_txs = vec![];
22232233
let mut to_drop_and_blacklist = vec![];
2234+
let mut update_timings = vec![];
22242235

22252236
let deadline = ts_start + u128::from(max_miner_time_ms);
22262237
let mut num_txs = 0;
@@ -2250,10 +2261,27 @@ impl StacksBlockBuilder {
22502261
if block_limit_hit == BlockLimitFunction::LIMIT_REACHED {
22512262
return Ok(None);
22522263
}
2253-
if get_epoch_time_ms() >= deadline {
2264+
let time_now = get_epoch_time_ms();
2265+
if time_now >= deadline {
22542266
debug!("Miner mining time exceeded ({} ms)", max_miner_time_ms);
22552267
return Ok(None);
22562268
}
2269+
if let Some(time_estimate) = txinfo.metadata.time_estimate_ms {
2270+
if time_now.saturating_add(time_estimate.into()) > deadline {
2271+
debug!("Mining tx would cause us to exceed our deadline, skipping";
2272+
"txid" => %txinfo.tx.txid(),
2273+
"deadline" => deadline,
2274+
"now" => time_now,
2275+
"estimate" => time_estimate);
2276+
return Ok(Some(
2277+
TransactionResult::skipped(
2278+
&txinfo.tx,
2279+
"Transaction would exceed deadline.".into(),
2280+
)
2281+
.convert_to_event(),
2282+
));
2283+
}
2284+
}
22572285

22582286
// skip transactions early if we can
22592287
if considered.contains(&txinfo.tx.txid()) {
@@ -2303,6 +2331,7 @@ impl StacksBlockBuilder {
23032331
considered.insert(txinfo.tx.txid());
23042332
num_considered += 1;
23052333

2334+
let tx_start = Instant::now();
23062335
let tx_result = builder.try_mine_tx_with_len(
23072336
epoch_tx,
23082337
&txinfo.tx,
@@ -2314,6 +2343,21 @@ impl StacksBlockBuilder {
23142343
let result_event = tx_result.convert_to_event();
23152344
match tx_result {
23162345
TransactionResult::Success(TransactionSuccess { receipt, .. }) => {
2346+
if txinfo.metadata.time_estimate_ms.is_none() {
2347+
// use i64 to avoid running into issues when storing in
2348+
// rusqlite.
2349+
let time_estimate_ms: i64 = tx_start
2350+
.elapsed()
2351+
.as_millis()
2352+
.try_into()
2353+
.unwrap_or_else(|_| i64::MAX);
2354+
let time_estimate_ms: u64 = time_estimate_ms
2355+
.try_into()
2356+
// should be unreachable
2357+
.unwrap_or_else(|_| 0);
2358+
update_timings.push((txinfo.tx.txid(), time_estimate_ms));
2359+
}
2360+
23172361
num_txs += 1;
23182362
if update_estimator {
23192363
if let Err(e) = estimator.notify_event(
@@ -2386,6 +2430,12 @@ impl StacksBlockBuilder {
23862430
},
23872431
);
23882432

2433+
if !update_timings.is_empty() {
2434+
if let Err(e) = mempool.update_tx_time_estimates(&update_timings) {
2435+
warn!("Error while updating time estimates for mempool"; "err" => ?e);
2436+
}
2437+
}
2438+
23892439
if to_drop_and_blacklist.len() > 0 {
23902440
let _ = mempool.drop_and_blacklist_txs(&to_drop_and_blacklist);
23912441
}

stackslib/src/core/mempool.rs

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ pub struct MemPoolTxMetadata {
460460
pub last_known_origin_nonce: Option<u64>,
461461
pub last_known_sponsor_nonce: Option<u64>,
462462
pub accept_time: u64,
463+
pub time_estimate_ms: Option<u64>,
463464
}
464465

465466
impl MemPoolTxMetadata {
@@ -594,6 +595,7 @@ impl FromRow<MemPoolTxMetadata> for MemPoolTxMetadata {
594595
let sponsor_nonce = u64::from_column(row, "sponsor_nonce")?;
595596
let last_known_sponsor_nonce = u64::from_column(row, "last_known_sponsor_nonce")?;
596597
let last_known_origin_nonce = u64::from_column(row, "last_known_origin_nonce")?;
598+
let time_estimate_ms: Option<u64> = row.get("time_estimate_ms")?;
597599

598600
Ok(MemPoolTxMetadata {
599601
txid,
@@ -609,6 +611,7 @@ impl FromRow<MemPoolTxMetadata> for MemPoolTxMetadata {
609611
last_known_origin_nonce,
610612
last_known_sponsor_nonce,
611613
accept_time,
614+
time_estimate_ms,
612615
})
613616
}
614617
}
@@ -624,10 +627,7 @@ impl FromRow<MemPoolTxInfo> for MemPoolTxInfo {
624627
return Err(db_error::ParseError);
625628
}
626629

627-
Ok(MemPoolTxInfo {
628-
tx: tx,
629-
metadata: md,
630-
})
630+
Ok(MemPoolTxInfo { tx, metadata: md })
631631
}
632632
}
633633

@@ -803,6 +803,16 @@ const MEMPOOL_SCHEMA_6_NONCES: &'static [&'static str] = &[
803803
"#,
804804
];
805805

806+
const MEMPOOL_SCHEMA_7_TIME_ESTIMATES: &'static [&'static str] = &[
807+
r#"
808+
-- ALLOW NULL
809+
ALTER TABLE mempool ADD COLUMN time_estimate_ms INTEGER;
810+
"#,
811+
r#"
812+
INSERT INTO schema_version (version) VALUES (7)
813+
"#,
814+
];
815+
806816
const MEMPOOL_INDEXES: &'static [&'static str] = &[
807817
"CREATE INDEX IF NOT EXISTS by_txid ON mempool(txid);",
808818
"CREATE INDEX IF NOT EXISTS by_height ON mempool(height);",
@@ -1287,6 +1297,9 @@ impl MemPoolDB {
12871297
MemPoolDB::instantiate_nonces(tx)?;
12881298
}
12891299
6 => {
1300+
MemPoolDB::instantiate_schema_7(tx)?;
1301+
}
1302+
7 => {
12901303
break;
12911304
}
12921305
_ => {
@@ -1363,6 +1376,16 @@ impl MemPoolDB {
13631376
Ok(())
13641377
}
13651378

1379+
/// Add the nonce table
1380+
#[cfg_attr(test, mutants::skip)]
1381+
fn instantiate_schema_7(tx: &DBTx) -> Result<(), db_error> {
1382+
for sql_exec in MEMPOOL_SCHEMA_7_TIME_ESTIMATES {
1383+
tx.execute_batch(sql_exec)?;
1384+
}
1385+
1386+
Ok(())
1387+
}
1388+
13661389
#[cfg_attr(test, mutants::skip)]
13671390
pub fn db_path(chainstate_root_path: &str) -> Result<String, db_error> {
13681391
let mut path = PathBuf::from(chainstate_root_path);
@@ -1992,21 +2015,7 @@ impl MemPoolDB {
19922015
nonce: u64,
19932016
) -> Result<Option<MemPoolTxMetadata>, db_error> {
19942017
let sql = format!(
1995-
"SELECT
1996-
txid,
1997-
origin_address,
1998-
origin_nonce,
1999-
sponsor_address,
2000-
sponsor_nonce,
2001-
tx_fee,
2002-
length,
2003-
consensus_hash,
2004-
block_header_hash,
2005-
height,
2006-
accept_time,
2007-
last_known_sponsor_nonce,
2008-
last_known_origin_nonce
2009-
FROM mempool WHERE {0}_address = ?1 AND {0}_nonce = ?2",
2018+
"SELECT * FROM mempool WHERE {0}_address = ?1 AND {0}_nonce = ?2",
20102019
if is_origin { "origin" } else { "sponsor" }
20112020
);
20122021
let args = params![addr.to_string(), u64_to_sql(nonce)?];
@@ -2650,6 +2659,20 @@ impl MemPoolDB {
26502659
Ok(())
26512660
}
26522661

2662+
/// Update the time estimates for the supplied txs in the mempool db
2663+
pub fn update_tx_time_estimates(&mut self, txs: &[(Txid, u64)]) -> Result<(), db_error> {
2664+
let sql = "UPDATE mempool SET time_estimate_ms = ? WHERE txid = ?";
2665+
let mempool_tx = self.tx_begin()?;
2666+
for (txid, time_estimate_ms) in txs.iter() {
2667+
mempool_tx
2668+
.tx
2669+
.execute(sql, params![time_estimate_ms, txid])?;
2670+
}
2671+
mempool_tx.commit()?;
2672+
2673+
Ok(())
2674+
}
2675+
26532676
/// Drop and blacklist transactions, so we don't re-broadcast them or re-fetch them.
26542677
/// Do *NOT* remove them from the bloom filter. This will cause them to continue to be
26552678
/// reported as present, which is exactly what we want because we don't want these transactions

stackslib/src/core/tests/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1381,7 +1381,7 @@ fn mempool_do_not_replace_tx() {
13811381
.unwrap_err();
13821382
assert!(match err_resp {
13831383
MemPoolRejection::ConflictingNonceInMempool => true,
1384-
_ => false,
1384+
e => panic!("Failed: {e:?}"),
13851385
});
13861386

13871387
assert!(MemPoolDB::db_has_tx(&mempool_tx, &prior_txid).unwrap());

testnet/stacks-node/src/nakamoto_node/miner.rs

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use stacks::chainstate::stacks::{
4040
use stacks::net::p2p::NetworkHandle;
4141
use stacks::net::stackerdb::StackerDBs;
4242
use stacks::net::{NakamotoBlocksData, StacksMessageType};
43+
use stacks::util::get_epoch_time_secs;
4344
use stacks::util::secp256k1::MessageSignature;
4445
use stacks_common::types::chainstate::{StacksAddress, StacksBlockId};
4546
use stacks_common::types::{PrivateKey, StacksEpochId};
@@ -949,6 +950,29 @@ impl BlockMinerThread {
949950
Some(vrf_proof)
950951
}
951952

953+
fn validate_timestamp_info(
954+
&self,
955+
current_timestamp_secs: u64,
956+
stacks_parent_header: &StacksHeaderInfo,
957+
) -> bool {
958+
let parent_timestamp = match stacks_parent_header.anchored_header.as_stacks_nakamoto() {
959+
Some(naka_header) => naka_header.timestamp,
960+
None => stacks_parent_header.burn_header_timestamp,
961+
};
962+
let time_since_parent_ms = current_timestamp_secs.saturating_sub(parent_timestamp) * 1000;
963+
if time_since_parent_ms < self.config.miner.min_time_between_blocks_ms {
964+
debug!("Parent block mined {time_since_parent_ms} ms ago. Required minimum gap between blocks is {} ms", self.config.miner.min_time_between_blocks_ms;
965+
"current_timestamp" => current_timestamp_secs,
966+
"parent_block_id" => %stacks_parent_header.index_block_hash(),
967+
"parent_block_height" => stacks_parent_header.stacks_block_height,
968+
"parent_block_timestamp" => stacks_parent_header.burn_header_timestamp,
969+
);
970+
false
971+
} else {
972+
true
973+
}
974+
}
975+
952976
/// Check that the provided block is not mined too quickly after the parent block.
953977
/// This is to ensure that the signers do not reject the block due to the block being mined within the same second as the parent block.
954978
fn validate_timestamp(&self, x: &NakamotoBlock) -> Result<bool, NakamotoNodeError> {
@@ -970,22 +994,7 @@ impl BlockMinerThread {
970994
);
971995
NakamotoNodeError::ParentNotFound
972996
})?;
973-
let current_timestamp = x.header.timestamp;
974-
let parent_timestamp = match stacks_parent_header.anchored_header.as_stacks_nakamoto() {
975-
Some(naka_header) => naka_header.timestamp,
976-
None => stacks_parent_header.burn_header_timestamp,
977-
};
978-
let time_since_parent_ms = current_timestamp.saturating_sub(parent_timestamp) * 1000;
979-
if time_since_parent_ms < self.config.miner.min_time_between_blocks_ms {
980-
debug!("Parent block mined {time_since_parent_ms} ms ago. Required minimum gap between blocks is {} ms", self.config.miner.min_time_between_blocks_ms;
981-
"current_timestamp" => current_timestamp,
982-
"parent_block_id" => %stacks_parent_header.index_block_hash(),
983-
"parent_block_height" => stacks_parent_header.stacks_block_height,
984-
"parent_block_timestamp" => stacks_parent_header.burn_header_timestamp,
985-
);
986-
return Ok(false);
987-
}
988-
Ok(true)
997+
Ok(self.validate_timestamp_info(x.header.timestamp, &stacks_parent_header))
989998
}
990999

9911000
// TODO: add tests from mutation testing results #4869
@@ -1042,6 +1051,17 @@ impl BlockMinerThread {
10421051

10431052
let signer_bitvec_len = reward_set.rewarded_addresses.len().try_into().ok();
10441053

1054+
if !self.validate_timestamp_info(
1055+
get_epoch_time_secs(),
1056+
&parent_block_info.stacks_parent_header,
1057+
) {
1058+
// treat a too-soon-to-mine block as an interrupt: this will let the caller sleep and then re-evaluate
1059+
// all the pre-mining checks (burnchain tip changes, signal interrupts, etc.)
1060+
return Err(NakamotoNodeError::MiningFailure(
1061+
ChainstateError::MinerAborted,
1062+
));
1063+
}
1064+
10451065
// build the block itself
10461066
let (mut block, consumed, size, tx_events) = NakamotoBlockBuilder::build_nakamoto_block(
10471067
&chain_state,

0 commit comments

Comments
 (0)