Skip to content

Commit 85fdb5a

Browse files
committed
Merge branch 'develop' of https://github.com/stacks-network/stacks-core into feat/signer-state-conflict-resolution-strategies
2 parents 0085f73 + 8f9c5ed commit 85fdb5a

File tree

9 files changed

+442
-6
lines changed

9 files changed

+442
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to the versioning scheme outlined in the [README.md](README.md).
77

8-
## [Unreleased]
8+
## [3.1.0.0.8]
99

1010
### Added
1111

stacks-signer/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to the versioning scheme outlined in the [README.md](README.md).
77

8-
## [Unreleased]
8+
## [3.1.0.0.8.0]
99

1010
### Changed
1111

stacks-signer/src/monitoring/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,27 @@
1414
// You should have received a copy of the GNU General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
use stacks_common::define_named_enum;
18+
1719
#[cfg(feature = "monitoring_prom")]
1820
mod prometheus;
1921

2022
#[cfg(feature = "monitoring_prom")]
2123
mod server;
2224

25+
define_named_enum!(
26+
/// Represent different state change reason on signer agreement protocol
27+
SignerAgreementStateChangeReason {
28+
/// A new burn block has arrived
29+
BurnBlockArrival("burn_block_arrival"),
30+
/// A new stacks block has arrived
31+
StacksBlockArrival("stacks_block_arrival"),
32+
/// A miner is inactive when it should be starting its tenure
33+
InactiveMiner("inactive_miner"),
34+
/// Signer agreement protocol version has been upgraded
35+
ProtocolUpgrade("protocol_upgrade"),
36+
});
37+
2338
/// Actions for updating metrics
2439
#[cfg(feature = "monitoring_prom")]
2540
pub mod actions {
@@ -29,6 +44,7 @@ pub mod actions {
2944

3045
use crate::config::GlobalConfig;
3146
use crate::monitoring::prometheus::*;
47+
use crate::monitoring::SignerAgreementStateChangeReason;
3248
use crate::v0::signer_state::LocalStateMachine;
3349

3450
/// Update stacks tip height gauge
@@ -108,6 +124,16 @@ pub mod actions {
108124
.replace(state);
109125
}
110126

127+
/// Increment signer agreement state change reason counter
128+
pub fn increment_signer_agreement_state_change_reason(
129+
reason: SignerAgreementStateChangeReason,
130+
) {
131+
let label_value = reason.get_name();
132+
SIGNER_AGREEMENT_STATE_CHANGE_REASONS
133+
.with_label_values(&[&label_value])
134+
.inc();
135+
}
136+
111137
/// Start serving monitoring metrics.
112138
/// This will only serve the metrics if the `monitoring_prom` feature is enabled.
113139
pub fn start_serving_monitoring_metrics(config: GlobalConfig) -> Result<(), String> {
@@ -131,6 +157,7 @@ pub mod actions {
131157
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
132158
use stacks_common::info;
133159

160+
use crate::monitoring::SignerAgreementStateChangeReason;
134161
use crate::v0::signer_state::LocalStateMachine;
135162
use crate::GlobalConfig;
136163

@@ -179,6 +206,12 @@ pub mod actions {
179206
/// Record the current local state machine
180207
pub fn record_local_state(_state: LocalStateMachine) {}
181208

209+
/// Increment signer agreement state change reason counter
210+
pub fn increment_signer_agreement_state_change_reason(
211+
_reason: SignerAgreementStateChangeReason,
212+
) {
213+
}
214+
182215
/// Start serving monitoring metrics.
183216
/// This will only serve the metrics if the `monitoring_prom` feature is enabled.
184217
pub fn start_serving_monitoring_metrics(config: GlobalConfig) -> Result<(), String> {

stacks-signer/src/monitoring/prometheus.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ lazy_static! {
7979
vec![0.005, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 20.0, 30.0, 60.0, 120.0]
8080
), &[]).unwrap();
8181

82+
pub static ref SIGNER_AGREEMENT_STATE_CHANGE_REASONS: IntCounterVec = register_int_counter_vec!(
83+
"stacks_signer_agreement_state_change_reasons",
84+
"The number of state machine changes in signer agreement protocol. `reason` can be one of: 'burn_block_arrival', 'stacks_block_arrival', 'inactive_miner', 'protocol_upgrade'",
85+
&["reason"]
86+
).unwrap();
87+
8288
pub static ref SIGNER_LOCAL_STATE_MACHINE: Mutex<Option<LocalStateMachine>> = Mutex::new(None);
8389
}
8490

stacks-signer/src/v0/signer_state.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,11 @@ impl LocalStateMachine {
487487
"inactive_tenure_ch" => %inactive_tenure_ch,
488488
"new_active_tenure_ch" => %new_active_tenure_ch
489489
);
490+
491+
crate::monitoring::actions::increment_signer_agreement_state_change_reason(
492+
crate::monitoring::SignerAgreementStateChangeReason::InactiveMiner,
493+
);
494+
490495
Ok(())
491496
} else {
492497
warn!("Current miner timed out due to inactivity, but prior miner is not valid. Allowing current miner to continue");
@@ -609,6 +614,11 @@ impl LocalStateMachine {
609614
*parent_tenure_last_block = *block_id;
610615
*parent_tenure_last_block_height = height;
611616
*self = LocalStateMachine::Initialized(prior_state_machine);
617+
618+
crate::monitoring::actions::increment_signer_agreement_state_change_reason(
619+
crate::monitoring::SignerAgreementStateChangeReason::StacksBlockArrival,
620+
);
621+
612622
Ok(())
613623
}
614624

@@ -663,7 +673,7 @@ impl LocalStateMachine {
663673
// set self to uninitialized so that if this function errors,
664674
// self is left as uninitialized.
665675
let prior_state = std::mem::replace(self, Self::Uninitialized);
666-
let prior_state_machine = match prior_state {
676+
let prior_state_machine = match prior_state.clone() {
667677
// if the local state machine was uninitialized, just initialize it
668678
LocalStateMachine::Uninitialized => Self::place_holder(),
669679
LocalStateMachine::Initialized(signer_state_machine) => signer_state_machine,
@@ -742,6 +752,12 @@ impl LocalStateMachine {
742752
active_signer_protocol_version: prior_state_machine.active_signer_protocol_version,
743753
});
744754

755+
if prior_state != *self {
756+
crate::monitoring::actions::increment_signer_agreement_state_change_reason(
757+
crate::monitoring::SignerAgreementStateChangeReason::BurnBlockArrival,
758+
);
759+
}
760+
745761
Ok(())
746762
}
747763

stackslib/src/core/mempool.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2953,7 +2953,18 @@ pub fn try_flush_considered_txs(
29532953
let db_tx = conn.transaction()?;
29542954

29552955
for txid in considered_txs {
2956-
db_tx.execute(sql, params![txid])?;
2956+
match db_tx.execute(sql, params![txid]) {
2957+
Ok(_) => {}
2958+
Err(rusqlite::Error::SqliteFailure(err, _))
2959+
if err.code == rusqlite::ErrorCode::ConstraintViolation =>
2960+
{
2961+
// Ignore constraint violations (e.g., foreign key failure)
2962+
// This can happen if the txid was removed from the mempool DB
2963+
// before we could flush it to the considered_txs table.
2964+
continue;
2965+
}
2966+
Err(e) => return Err(e.into()),
2967+
}
29572968
}
29582969

29592970
db_tx.commit()?;

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

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12183,3 +12183,159 @@ fn v3_transaction_api_endpoint() {
1218312183

1218412184
run_loop_thread.join().unwrap();
1218512185
}
12186+
12187+
#[test]
12188+
#[ignore]
12189+
/// This test verifies that the miner can continue even if an insertion into
12190+
/// the `considered_txs` table fails due to a foreign key failure.
12191+
fn handle_considered_txs_foreign_key_failure() {
12192+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
12193+
return;
12194+
}
12195+
12196+
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
12197+
let prom_bind = "127.0.0.1:6000".to_string();
12198+
naka_conf.node.prometheus_bind = Some(prom_bind);
12199+
naka_conf.miner.nakamoto_attempt_time_ms = 5_000;
12200+
naka_conf.miner.tenure_cost_limit_per_block_percentage = None;
12201+
naka_conf.miner.mempool_walk_strategy = MemPoolWalkStrategy::NextNonceWithHighestFeeRate;
12202+
// setup senders
12203+
let send_amt = 1000;
12204+
let send_fee = 180;
12205+
let bad_sender_sk = Secp256k1PrivateKey::from_seed(&[30]);
12206+
let bad_sender_addr = tests::to_addr(&bad_sender_sk);
12207+
naka_conf.add_initial_balance(
12208+
PrincipalData::from(bad_sender_addr).to_string(),
12209+
send_amt + send_fee,
12210+
);
12211+
let good_sender_sk = Secp256k1PrivateKey::from_seed(&[31]);
12212+
let good_sender_addr = tests::to_addr(&good_sender_sk);
12213+
naka_conf.add_initial_balance(
12214+
PrincipalData::from(good_sender_addr).to_string(),
12215+
(send_amt + send_fee) * 2,
12216+
);
12217+
12218+
let sender_signer_sk = Secp256k1PrivateKey::random();
12219+
let sender_signer_addr = tests::to_addr(&sender_signer_sk);
12220+
let mut signers = TestSigners::new(vec![sender_signer_sk]);
12221+
naka_conf.add_initial_balance(PrincipalData::from(sender_signer_addr).to_string(), 100000);
12222+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
12223+
let stacker_sk = setup_stacker(&mut naka_conf);
12224+
let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
12225+
12226+
test_observer::spawn();
12227+
test_observer::register_any(&mut naka_conf);
12228+
12229+
let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
12230+
btcd_controller
12231+
.start_bitcoind()
12232+
.expect("Failed starting bitcoind");
12233+
let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None);
12234+
btc_regtest_controller.bootstrap_chain(201);
12235+
12236+
let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap();
12237+
let run_loop_stopper = run_loop.get_termination_switch();
12238+
let Counters {
12239+
blocks_processed,
12240+
naka_submitted_commits: commits_submitted,
12241+
..
12242+
} = run_loop.counters();
12243+
let counters = run_loop.counters();
12244+
12245+
let coord_channel = run_loop.coordinator_channels();
12246+
12247+
let run_loop_thread = thread::spawn(move || run_loop.start(None, 0));
12248+
wait_for_runloop(&blocks_processed);
12249+
boot_to_epoch_3(
12250+
&naka_conf,
12251+
&blocks_processed,
12252+
&[stacker_sk],
12253+
&[sender_signer_sk],
12254+
&mut Some(&mut signers),
12255+
&mut btc_regtest_controller,
12256+
);
12257+
12258+
info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner");
12259+
12260+
info!("Nakamoto miner started...");
12261+
blind_signer(&naka_conf, &signers, &counters);
12262+
12263+
wait_for_first_naka_block_commit(60, &commits_submitted);
12264+
12265+
next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel)
12266+
.unwrap();
12267+
12268+
let good_transfer_tx = make_stacks_transfer(
12269+
&good_sender_sk,
12270+
0,
12271+
send_fee,
12272+
naka_conf.burnchain.chain_id,
12273+
&recipient,
12274+
send_amt,
12275+
);
12276+
submit_tx(&http_origin, &good_transfer_tx);
12277+
12278+
wait_for(60, || {
12279+
let nonce = get_account(&http_origin, &good_sender_addr).nonce;
12280+
Ok(nonce == 1)
12281+
})
12282+
.expect("Timed out waiting for first block");
12283+
12284+
let height_before = get_chain_info(&naka_conf).stacks_tip_height;
12285+
12286+
// Initiate the transaction stall, then submit transactions.
12287+
TEST_MINE_STALL.set(true);
12288+
TEST_TX_STALL.set(true);
12289+
12290+
let bad_transfer_tx = make_stacks_transfer(
12291+
&bad_sender_sk,
12292+
0,
12293+
send_fee,
12294+
naka_conf.burnchain.chain_id,
12295+
&recipient,
12296+
send_amt,
12297+
);
12298+
let txid = submit_tx(&http_origin, &bad_transfer_tx);
12299+
info!("Bad transaction submitted: {txid}");
12300+
12301+
TEST_MINE_STALL.set(false);
12302+
12303+
// Sleep long enough to ensure that the miner has started processing the tx
12304+
sleep_ms(5_000);
12305+
12306+
info!("--------------------- Deleting tx from the mempool ---------------------");
12307+
// Delete the bad transaction from the mempool.
12308+
let mempool_db_path = format!(
12309+
"{}/nakamoto-neon/chainstate/mempool.sqlite",
12310+
naka_conf.node.working_dir
12311+
);
12312+
let conn = Connection::open(&mempool_db_path).unwrap();
12313+
conn.execute("DELETE FROM mempool WHERE txid = ?", [txid])
12314+
.unwrap();
12315+
12316+
// Unstall the transaction processing, so that the miner will resume.
12317+
TEST_TX_STALL.set(false);
12318+
12319+
info!("--------------------- Waiting for the block ---------------------");
12320+
12321+
// Now wait for the next block to be mined.
12322+
wait_for(30, || {
12323+
let height = get_chain_info(&naka_conf).stacks_tip_height;
12324+
Ok(height > height_before)
12325+
})
12326+
.expect("Timed out waiting for block");
12327+
12328+
let good_sender_nonce = get_account(&http_origin, &good_sender_addr).nonce;
12329+
let bad_sender_nonce = get_account(&http_origin, &bad_sender_addr).nonce;
12330+
12331+
assert_eq!(good_sender_nonce, 1);
12332+
assert_eq!(bad_sender_nonce, 1);
12333+
12334+
coord_channel
12335+
.lock()
12336+
.expect("Mutex poisoned")
12337+
.stop_chains_coordinator();
12338+
run_loop_stopper.store(false, Ordering::SeqCst);
12339+
12340+
run_loop_thread.join().unwrap();
12341+
}

0 commit comments

Comments
 (0)