Skip to content

Commit 442519e

Browse files
authoredMar 20, 2025··
Merge pull request #5924 from hstove/feat/rbf-when-config-changed
feat: issue RBF when burnchain/miner config changed
2 parents e21c0dd + 477eae6 commit 442519e

File tree

6 files changed

+242
-30
lines changed

6 files changed

+242
-30
lines changed
 

‎CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
77

88
## [Unreleased]
99

10-
### Added"
10+
### Added
11+
1112
- Add fee information to transaction log ending with "success" or "skipped", while building a new block
13+
- When a miner's config file is updated (ie with a new fee rate), a new block commit is issued using
14+
the new values ([#5924](https://github.com/stacks-network/stacks-core/pull/5924))
1215

1316
### Changed
1417

‎stackslib/src/config/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,7 @@ impl std::default::Default for Config {
11971197
}
11981198
}
11991199

1200-
#[derive(Clone, Debug, Default, Deserialize)]
1200+
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
12011201
pub struct BurnchainConfig {
12021202
pub chain: String,
12031203
pub mode: String,

‎testnet/stacks-node/src/globals.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use stacks::chainstate::coordinator::comm::CoordinatorChannels;
1010
use stacks::chainstate::stacks::db::unconfirmed::UnconfirmedTxMap;
1111
use stacks::chainstate::stacks::db::StacksChainState;
1212
use stacks::chainstate::stacks::miner::MinerStatus;
13-
use stacks::config::MinerConfig;
13+
use stacks::config::{BurnchainConfig, MinerConfig};
1414
use stacks::net::NetworkResult;
1515
use stacks_common::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, ConsensusHash};
1616

@@ -63,6 +63,8 @@ pub struct Globals<T> {
6363
pub leader_key_registration_state: Arc<Mutex<LeaderKeyRegistrationState>>,
6464
/// Last miner config loaded
6565
last_miner_config: Arc<Mutex<Option<MinerConfig>>>,
66+
/// Last burnchain config
67+
last_burnchain_config: Arc<Mutex<Option<BurnchainConfig>>>,
6668
/// Last miner spend amount
6769
last_miner_spend_amount: Arc<Mutex<Option<u64>>>,
6870
/// burnchain height at which we start mining
@@ -93,6 +95,7 @@ impl<T> Clone for Globals<T> {
9395
should_keep_running: self.should_keep_running.clone(),
9496
leader_key_registration_state: self.leader_key_registration_state.clone(),
9597
last_miner_config: self.last_miner_config.clone(),
98+
last_burnchain_config: self.last_burnchain_config.clone(),
9699
last_miner_spend_amount: self.last_miner_spend_amount.clone(),
97100
start_mining_height: self.start_mining_height.clone(),
98101
estimated_winning_probs: self.estimated_winning_probs.clone(),
@@ -125,6 +128,7 @@ impl<T> Globals<T> {
125128
should_keep_running,
126129
leader_key_registration_state: Arc::new(Mutex::new(leader_key_registration_state)),
127130
last_miner_config: Arc::new(Mutex::new(None)),
131+
last_burnchain_config: Arc::new(Mutex::new(None)),
128132
last_miner_spend_amount: Arc::new(Mutex::new(None)),
129133
start_mining_height: Arc::new(Mutex::new(start_mining_height)),
130134
estimated_winning_probs: Arc::new(Mutex::new(HashMap::new())),
@@ -355,6 +359,28 @@ impl<T> Globals<T> {
355359
}
356360
}
357361

362+
/// Get the last burnchain config
363+
pub fn get_last_burnchain_config(&self) -> Option<BurnchainConfig> {
364+
match self.last_burnchain_config.lock() {
365+
Ok(last_burnchain_config) => (*last_burnchain_config).clone(),
366+
Err(_e) => {
367+
error!("FATAL; failed to lock last burnchain config");
368+
panic!();
369+
}
370+
}
371+
}
372+
373+
/// Set the last burnchain config
374+
pub fn set_last_burnchain_config(&self, burnchain_config: BurnchainConfig) {
375+
match self.last_burnchain_config.lock() {
376+
Ok(ref mut last_burnchain_config) => **last_burnchain_config = Some(burnchain_config),
377+
Err(_e) => {
378+
error!("FATAL; failed to lock last burnchain config");
379+
panic!();
380+
}
381+
}
382+
}
383+
358384
/// Get the last miner spend amount
359385
pub fn get_last_miner_spend_amount(&self) -> Option<u64> {
360386
match self.last_miner_spend_amount.lock() {

‎testnet/stacks-node/src/nakamoto_node/relayer.rs

+62-27
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use stacks::chainstate::stacks::miner::{
4242
set_mining_spend_amount, signal_mining_blocked, signal_mining_ready,
4343
};
4444
use stacks::chainstate::stacks::Error as ChainstateError;
45+
use stacks::config::BurnchainConfig;
4546
use stacks::core::mempool::MemPoolDB;
4647
use stacks::core::STACKS_EPOCH_3_1_MARKER;
4748
use stacks::monitoring::increment_stx_blocks_mined_counter;
@@ -1101,29 +1102,7 @@ impl RelayerThread {
11011102
return Err(NakamotoNodeError::SnapshotNotFoundForChainTip);
11021103
};
11031104

1104-
let burnchain_config = self.config.get_burnchain_config();
1105-
let last_miner_spend_opt = self.globals.get_last_miner_spend_amount();
1106-
let force_remine = if let Some(last_miner_spend_amount) = last_miner_spend_opt {
1107-
last_miner_spend_amount != burnchain_config.burn_fee_cap
1108-
} else {
1109-
false
1110-
};
1111-
if force_remine {
1112-
info!(
1113-
"Miner config changed; updating spend amount {}",
1114-
burnchain_config.burn_fee_cap
1115-
);
1116-
}
1117-
1118-
self.globals
1119-
.set_last_miner_spend_amount(burnchain_config.burn_fee_cap);
1120-
1121-
set_mining_spend_amount(
1122-
self.globals.get_miner_status(),
1123-
burnchain_config.burn_fee_cap,
1124-
);
1125-
// amount of burnchain tokens (e.g. sats) we'll spend across the PoX outputs
1126-
let burn_fee_cap = burnchain_config.burn_fee_cap;
1105+
let (_, burnchain_config) = self.check_burnchain_config_changed();
11271106

11281107
// let's commit, but target the current burnchain tip with our modulus so the commit is
11291108
// only valid if it lands in the targeted burnchain block height
@@ -1155,7 +1134,7 @@ impl RelayerThread {
11551134
highest_tenure_start_block_header.index_block_hash().0,
11561135
),
11571136
// the rest of this is the same as epoch2x commits, modulo the new epoch marker
1158-
burn_fee: burn_fee_cap,
1137+
burn_fee: burnchain_config.burn_fee_cap,
11591138
apparent_sender: sender,
11601139
key_block_ptr: u32::try_from(key.block_height)
11611140
.expect("FATAL: burn block height exceeded u32"),
@@ -1703,9 +1682,11 @@ impl RelayerThread {
17031682

17041683
// update local state
17051684
last_committed.set_txid(&txid);
1706-
self.globals
1707-
.counters
1708-
.bump_naka_submitted_commits(last_committed.burn_tip.block_height, tip_height);
1685+
self.globals.counters.bump_naka_submitted_commits(
1686+
last_committed.burn_tip.block_height,
1687+
tip_height,
1688+
last_committed.block_commit.burn_fee,
1689+
);
17091690
self.last_committed = Some(last_committed);
17101691

17111692
Ok(())
@@ -1768,6 +1749,21 @@ impl RelayerThread {
17681749
"burnchain view changed?" => %burnchain_changed,
17691750
"highest tenure changed?" => %highest_tenure_changed);
17701751

1752+
// If the miner spend or config has changed, we want to RBF with new config values.
1753+
let (burnchain_config_changed, _) = self.check_burnchain_config_changed();
1754+
let miner_config_changed = self.check_miner_config_changed();
1755+
1756+
if burnchain_config_changed || miner_config_changed {
1757+
info!("Miner spend or config changed; issuing block commit with new values";
1758+
"miner_spend_changed" => %burnchain_config_changed,
1759+
"miner_config_changed" => %miner_config_changed,
1760+
);
1761+
return Ok(Some(RelayerDirective::IssueBlockCommit(
1762+
stacks_tip_ch,
1763+
stacks_tip_bh,
1764+
)));
1765+
}
1766+
17711767
if !burnchain_changed && !highest_tenure_changed {
17721768
// nothing to do
17731769
return Ok(None);
@@ -2136,6 +2132,45 @@ impl RelayerThread {
21362132
debug!("Relayer: handled directive"; "continue_running" => continue_running);
21372133
continue_running
21382134
}
2135+
2136+
/// Reload config.burnchain to see if burn_fee_cap has changed.
2137+
/// If it has, update the miner spend amount and return true.
2138+
pub fn check_burnchain_config_changed(&self) -> (bool, BurnchainConfig) {
2139+
let burnchain_config = self.config.get_burnchain_config();
2140+
let last_burnchain_config_opt = self.globals.get_last_burnchain_config();
2141+
let burnchain_config_changed =
2142+
if let Some(last_burnchain_config) = last_burnchain_config_opt {
2143+
last_burnchain_config != burnchain_config
2144+
} else {
2145+
false
2146+
};
2147+
2148+
self.globals
2149+
.set_last_miner_spend_amount(burnchain_config.burn_fee_cap);
2150+
self.globals
2151+
.set_last_burnchain_config(burnchain_config.clone());
2152+
2153+
set_mining_spend_amount(
2154+
self.globals.get_miner_status(),
2155+
burnchain_config.burn_fee_cap,
2156+
);
2157+
2158+
(burnchain_config_changed, burnchain_config)
2159+
}
2160+
2161+
pub fn check_miner_config_changed(&self) -> bool {
2162+
let miner_config = self.config.get_miner_config();
2163+
let last_miner_config_opt = self.globals.get_last_miner_config();
2164+
let miner_config_changed = if let Some(last_miner_config) = last_miner_config_opt {
2165+
last_miner_config != miner_config
2166+
} else {
2167+
false
2168+
};
2169+
2170+
self.globals.set_last_miner_config(miner_config);
2171+
2172+
miner_config_changed
2173+
}
21392174
}
21402175

21412176
#[cfg(test)]

‎testnet/stacks-node/src/run_loop/neon.rs

+6
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub struct Counters {
117117
pub naka_signer_pushed_blocks: RunLoopCounter,
118118
pub naka_miner_directives: RunLoopCounter,
119119
pub naka_submitted_commit_last_stacks_tip: RunLoopCounter,
120+
pub naka_submitted_commit_last_commit_amount: RunLoopCounter,
120121

121122
pub naka_miner_current_rejections: RunLoopCounter,
122123
pub naka_miner_current_rejections_timeout_secs: RunLoopCounter,
@@ -178,6 +179,7 @@ impl Counters {
178179
&self,
179180
committed_burn_height: u64,
180181
committed_stacks_height: u64,
182+
committed_sats_amount: u64,
181183
) {
182184
Counters::inc(&self.naka_submitted_commits);
183185
Counters::set(
@@ -188,6 +190,10 @@ impl Counters {
188190
&self.naka_submitted_commit_last_stacks_tip,
189191
committed_stacks_height,
190192
);
193+
Counters::set(
194+
&self.naka_submitted_commit_last_commit_amount,
195+
committed_sats_amount,
196+
);
191197
}
192198

193199
pub fn bump_naka_mined_blocks(&self) {

‎testnet/stacks-node/src/tests/nakamoto_integrations.rs

+142
Original file line numberDiff line numberDiff line change
@@ -11237,3 +11237,145 @@ fn reload_miner_config() {
1123711237

1123811238
run_loop_thread.join().unwrap();
1123911239
}
11240+
11241+
/// Test that a new block commit is issued when the miner spend or config changes.
11242+
///
11243+
/// The test boots into Nakamoto. Then, it waits for a block commit on the most recent
11244+
/// tip. The config is updated, and then the test ensures that a new commit was submitted after that
11245+
/// config change.
11246+
#[test]
11247+
#[ignore]
11248+
fn rbf_on_config_change() {
11249+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
11250+
return;
11251+
}
11252+
11253+
let (mut conf, _miner_account) = naka_neon_integration_conf(None);
11254+
let password = "12345".to_string();
11255+
let _http_origin = format!("http://{}", &conf.node.rpc_bind);
11256+
conf.connection_options.auth_token = Some(password.clone());
11257+
conf.miner.wait_on_interim_blocks = Duration::from_secs(1);
11258+
conf.node.next_initiative_delay = 500;
11259+
let stacker_sk = setup_stacker(&mut conf);
11260+
let signer_sk = Secp256k1PrivateKey::random();
11261+
let signer_addr = tests::to_addr(&signer_sk);
11262+
let sender_sk = Secp256k1PrivateKey::random();
11263+
let recipient_sk = Secp256k1PrivateKey::random();
11264+
let _recipient_addr = tests::to_addr(&recipient_sk);
11265+
// setup sender + recipient for some test stx transfers
11266+
// these are necessary for the interim blocks to get mined at all
11267+
let sender_addr = tests::to_addr(&sender_sk);
11268+
let old_burn_fee_cap: u64 = 100000;
11269+
conf.burnchain.burn_fee_cap = old_burn_fee_cap;
11270+
conf.add_initial_balance(PrincipalData::from(sender_addr).to_string(), 1000000);
11271+
conf.add_initial_balance(PrincipalData::from(signer_addr).to_string(), 100000);
11272+
11273+
test_observer::spawn();
11274+
test_observer::register(&mut conf, &[EventKeyType::AnyEvent]);
11275+
11276+
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
11277+
btcd_controller
11278+
.start_bitcoind()
11279+
.expect("Failed starting bitcoind");
11280+
let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
11281+
btc_regtest_controller.bootstrap_chain(201);
11282+
11283+
let conf_path =
11284+
std::env::temp_dir().join(format!("miner-config-test-{}.toml", rand::random::<u64>()));
11285+
conf.config_path = Some(conf_path.clone().to_str().unwrap().to_string());
11286+
11287+
// Make a minimum-viable config file
11288+
let update_config = |burn_fee_cap: u64, sats_vbyte: u64| {
11289+
use std::io::Write;
11290+
11291+
let new_config = format!(
11292+
r#"
11293+
[burnchain]
11294+
burn_fee_cap = {}
11295+
satoshis_per_byte = {}
11296+
"#,
11297+
burn_fee_cap, sats_vbyte,
11298+
);
11299+
// Write to a file
11300+
let mut file = File::create(&conf_path).unwrap();
11301+
file.write_all(new_config.as_bytes()).unwrap();
11302+
};
11303+
11304+
let mut run_loop = boot_nakamoto::BootRunLoop::new(conf.clone()).unwrap();
11305+
let run_loop_stopper = run_loop.get_termination_switch();
11306+
let counters = run_loop.counters();
11307+
let Counters {
11308+
blocks_processed,
11309+
naka_submitted_commits: commits_submitted,
11310+
..
11311+
} = run_loop.counters();
11312+
11313+
let coord_channel = run_loop.coordinator_channels();
11314+
11315+
let run_loop_thread = thread::spawn(move || run_loop.start(None, 0));
11316+
let mut signers = TestSigners::new(vec![signer_sk]);
11317+
wait_for_runloop(&blocks_processed);
11318+
boot_to_epoch_3(
11319+
&conf,
11320+
&blocks_processed,
11321+
&[stacker_sk],
11322+
&[signer_sk],
11323+
&mut Some(&mut signers),
11324+
&mut btc_regtest_controller,
11325+
);
11326+
11327+
info!("------------------------- Reached Epoch 3.0 -------------------------");
11328+
11329+
blind_signer(&conf, &signers, &counters);
11330+
11331+
wait_for_first_naka_block_commit(60, &commits_submitted);
11332+
11333+
next_block_and_mine_commit(&mut btc_regtest_controller, 60, &conf, &counters).unwrap();
11334+
11335+
let burnchain = conf.get_burnchain();
11336+
let sortdb = burnchain.open_sortition_db(true).unwrap();
11337+
11338+
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
11339+
let stacks_height = tip.stacks_block_height;
11340+
11341+
let mut last_log = Instant::now();
11342+
last_log -= Duration::from_secs(5);
11343+
wait_for(30, || {
11344+
let last_commit = &counters.naka_submitted_commit_last_stacks_tip.get();
11345+
if last_log.elapsed() >= Duration::from_secs(5) {
11346+
info!(
11347+
"---- last_commit: {:?} stacks_height: {:?} ---- ",
11348+
last_commit, stacks_height
11349+
);
11350+
last_log = Instant::now();
11351+
}
11352+
Ok(*last_commit >= stacks_height)
11353+
})
11354+
.expect("Failed to wait for last commit");
11355+
11356+
let commits_before = counters.naka_submitted_commits.get();
11357+
11358+
let commit_amount_before = counters.naka_submitted_commit_last_commit_amount.get();
11359+
11360+
info!("---- Updating config ----");
11361+
11362+
update_config(155000, 57);
11363+
11364+
wait_for(30, || {
11365+
let commit_count = &counters.naka_submitted_commits.get();
11366+
Ok(*commit_count > commits_before)
11367+
})
11368+
.expect("Expected new commit after config change");
11369+
11370+
let commit_amount_after = counters.naka_submitted_commit_last_commit_amount.get();
11371+
assert_eq!(commit_amount_after, 155000);
11372+
assert_ne!(commit_amount_after, commit_amount_before);
11373+
11374+
coord_channel
11375+
.lock()
11376+
.expect("Mutex poisoned")
11377+
.stop_chains_coordinator();
11378+
run_loop_stopper.store(false, Ordering::SeqCst);
11379+
11380+
run_loop_thread.join().unwrap();
11381+
}

0 commit comments

Comments
 (0)
Please sign in to comment.