Skip to content

Commit 808ddcc

Browse files
committed
fix: allow considered_txs insertion foreign key failures
If a transaction was removed from the mempool before we add it to the `considered_txs` table, that's okay, just ignore the error and proceed.
1 parent 405f779 commit 808ddcc

File tree

2 files changed

+168
-1
lines changed

2 files changed

+168
-1
lines changed

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
@@ -12211,3 +12211,159 @@ fn v3_transaction_api_endpoint() {
1221112211

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

0 commit comments

Comments
 (0)