Skip to content

Commit 014f44b

Browse files
committed
feat: integration test for retry pending block validations
1 parent a9c7794 commit 014f44b

File tree

5 files changed

+237
-1
lines changed

5 files changed

+237
-1
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ jobs:
131131
- tests::signer::v0::block_commit_delay
132132
- tests::signer::v0::continue_after_fast_block_no_sortition
133133
- tests::signer::v0::block_validation_response_timeout
134+
- tests::signer::v0::block_validation_pending_table
134135
- tests::signer::v0::tenure_extend_after_bad_commit
135136
- tests::signer::v0::block_proposal_max_age_rejections
136137
- tests::nakamoto_integrations::burn_ops_integration_test

stacks-signer/src/signerdb.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ use blockstack_lib::util_lib::db::{
2424
query_row, query_rows, sqlite_open, table_exists, tx_begin_immediate, u64_to_sql,
2525
Error as DBError,
2626
};
27+
#[cfg(any(test, feature = "testing"))]
28+
use blockstack_lib::util_lib::db::{FromColumn, FromRow};
2729
use clarity::types::chainstate::{BurnchainHeaderHash, StacksAddress};
2830
use libsigner::BlockProposal;
2931
use rusqlite::functions::FunctionFlags;
@@ -1060,6 +1062,16 @@ impl SignerDb {
10601062
Ok(())
10611063
}
10621064

1065+
/// For tests, fetch all pending block validations
1066+
#[cfg(any(test, feature = "testing"))]
1067+
pub fn get_all_pending_block_validations(
1068+
&self,
1069+
) -> Result<Vec<PendingBlockValidation>, DBError> {
1070+
let qry = "SELECT signer_signature_hash, added_time FROM block_validations_pending";
1071+
let args = params![];
1072+
query_rows(&self.db, qry, args)
1073+
}
1074+
10631075
/// Return the start time (epoch time in seconds) and the processing time in milliseconds of the tenure (idenfitied by consensus_hash).
10641076
fn get_tenure_times(&self, tenure: &ConsensusHash) -> Result<(u64, u64), DBError> {
10651077
let query = "SELECT tenure_change, proposed_time, validation_time_ms FROM blocks WHERE consensus_hash = ?1 AND state = ?2 ORDER BY stacks_height DESC";
@@ -1134,6 +1146,27 @@ where
11341146
.map_err(DBError::SerializationError)
11351147
}
11361148

1149+
/// For tests, a struct to represent a pending block validation
1150+
#[cfg(any(test, feature = "testing"))]
1151+
pub struct PendingBlockValidation {
1152+
/// The signer signature hash of the block
1153+
pub signer_signature_hash: Sha512Trunc256Sum,
1154+
/// The time at which the block was added to the pending table
1155+
pub added_time: u64,
1156+
}
1157+
1158+
#[cfg(any(test, feature = "testing"))]
1159+
impl FromRow<PendingBlockValidation> for PendingBlockValidation {
1160+
fn from_row(row: &rusqlite::Row) -> Result<Self, DBError> {
1161+
let signer_signature_hash = Sha512Trunc256Sum::from_column(row, "signer_signature_hash")?;
1162+
let added_time = row.get_unwrap(1);
1163+
Ok(PendingBlockValidation {
1164+
signer_signature_hash,
1165+
added_time,
1166+
})
1167+
}
1168+
}
1169+
11371170
#[cfg(test)]
11381171
mod tests {
11391172
use std::fs;

stackslib/src/net/api/postblock_proposal.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ impl From<Result<BlockValidateOk, BlockValidateReject>> for BlockValidateRespons
175175
}
176176
}
177177

178+
impl BlockValidateResponse {
179+
/// Get the signer signature hash from the response
180+
pub fn signer_signature_hash(&self) -> Sha512Trunc256Sum {
181+
match self {
182+
BlockValidateResponse::Ok(o) => o.signer_signature_hash,
183+
BlockValidateResponse::Reject(r) => r.signer_signature_hash,
184+
}
185+
}
186+
}
187+
178188
/// Represents a block proposed to the `v3/block_proposal` endpoint for validation
179189
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180190
pub struct NakamotoBlockProposal {

testnet/stacks-node/src/tests/signer/mod.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use clarity::vm::types::PrincipalData;
3939
use libsigner::v0::messages::{
4040
BlockAccepted, BlockResponse, MessageSlotID, PeerInfo, SignerMessage,
4141
};
42-
use libsigner::{SignerEntries, SignerEventTrait};
42+
use libsigner::{BlockProposal, SignerEntries, SignerEventTrait};
4343
use stacks::chainstate::coordinator::comm::CoordinatorChannels;
4444
use stacks::chainstate::nakamoto::signer_set::NakamotoSigners;
4545
use stacks::chainstate::stacks::boot::{NakamotoSignerEntry, SIGNERS_NAME};
@@ -678,6 +678,25 @@ impl<S: Signer<T> + Send + 'static, T: SignerEventTrait + 'static> SignerTest<Sp
678678
}
679679
}
680680

681+
/// Get miner stackerDB messages
682+
pub fn get_miner_proposal_messages(&self) -> Vec<BlockProposal> {
683+
let proposals: Vec<_> = test_observer::get_stackerdb_chunks()
684+
.into_iter()
685+
.flat_map(|chunk| chunk.modified_slots)
686+
.filter_map(|chunk| {
687+
let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
688+
else {
689+
return None;
690+
};
691+
match message {
692+
SignerMessage::BlockProposal(proposal) => Some(proposal),
693+
_ => None,
694+
}
695+
})
696+
.collect();
697+
proposals
698+
}
699+
681700
/// Get /v2/info from the node
682701
pub fn get_peer_info(&self) -> PeerInfo {
683702
self.stacks_client

testnet/stacks-node/src/tests/signer/v0.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ use stacks_common::util::sleep_ms;
6060
use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView};
6161
use stacks_signer::client::{SignerSlotID, StackerDB};
6262
use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network};
63+
use stacks_signer::signerdb::SignerDb;
6364
use stacks_signer::v0::signer::{
6465
TEST_IGNORE_ALL_BLOCK_PROPOSALS, TEST_PAUSE_BLOCK_BROADCAST, TEST_REJECT_ALL_BLOCK_PROPOSAL,
6566
TEST_SKIP_BLOCK_BROADCAST,
@@ -7767,6 +7768,178 @@ fn block_validation_response_timeout() {
77677768
);
77687769
}
77697770

7771+
/// Test that, when a signer submit a block validation request and
7772+
/// gets a 429 the signer stores the pending request and submits
7773+
/// it again after the current block validation request finishes.
7774+
#[test]
7775+
#[ignore]
7776+
fn block_validation_pending_table() {
7777+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
7778+
return;
7779+
}
7780+
7781+
tracing_subscriber::registry()
7782+
.with(fmt::layer())
7783+
.with(EnvFilter::from_default_env())
7784+
.init();
7785+
7786+
info!("------------------------- Test Setup -------------------------");
7787+
let num_signers = 5;
7788+
let timeout = Duration::from_secs(30);
7789+
let sender_sk = Secp256k1PrivateKey::new();
7790+
let sender_addr = tests::to_addr(&sender_sk);
7791+
let send_amt = 100;
7792+
let send_fee = 180;
7793+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
7794+
let short_timeout = Duration::from_secs(20);
7795+
7796+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
7797+
num_signers,
7798+
vec![(sender_addr, send_amt + send_fee)],
7799+
|config| {
7800+
config.block_proposal_validation_timeout = timeout;
7801+
},
7802+
|_| {},
7803+
None,
7804+
None,
7805+
);
7806+
let db_path = signer_test.signer_configs[0].db_path.clone();
7807+
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
7808+
signer_test.boot_to_epoch_3();
7809+
7810+
info!("----- Starting test -----";
7811+
"db_path" => db_path.clone().to_str(),
7812+
);
7813+
signer_test.mine_and_verify_confirmed_naka_block(timeout, num_signers, true);
7814+
TEST_VALIDATE_DELAY_DURATION_SECS
7815+
.lock()
7816+
.unwrap()
7817+
.replace(30);
7818+
7819+
let signer_db = SignerDb::new(db_path).unwrap();
7820+
7821+
let proposals_before = signer_test.get_miner_proposal_messages().len();
7822+
7823+
let peer_info = signer_test.get_peer_info();
7824+
7825+
// submit a tx so that the miner will attempt to mine an extra block
7826+
let sender_nonce = 0;
7827+
let transfer_tx = make_stacks_transfer(
7828+
&sender_sk,
7829+
sender_nonce,
7830+
send_fee,
7831+
signer_test.running_nodes.conf.burnchain.chain_id,
7832+
&recipient,
7833+
send_amt,
7834+
);
7835+
submit_tx(&http_origin, &transfer_tx);
7836+
7837+
info!("----- Waiting for miner to propose a block -----");
7838+
7839+
// Wait for the miner to propose a block
7840+
wait_for(30, || {
7841+
Ok(signer_test.get_miner_proposal_messages().len() > proposals_before)
7842+
})
7843+
.expect("Timed out waiting for miner to propose a block");
7844+
7845+
info!("----- Proposing a concurrent block -----");
7846+
let proposal_conf = ProposalEvalConfig {
7847+
first_proposal_burn_block_timing: Duration::from_secs(0),
7848+
block_proposal_timeout: Duration::from_secs(100),
7849+
tenure_last_block_proposal_timeout: Duration::from_secs(30),
7850+
tenure_idle_timeout: Duration::from_secs(300),
7851+
};
7852+
let mut block = NakamotoBlock {
7853+
header: NakamotoBlockHeader::empty(),
7854+
txs: vec![],
7855+
};
7856+
block.header.timestamp = get_epoch_time_secs();
7857+
7858+
let view = SortitionsView::fetch_view(proposal_conf, &signer_test.stacks_client).unwrap();
7859+
block.header.pox_treatment = BitVec::ones(1).unwrap();
7860+
block.header.consensus_hash = view.cur_sortition.consensus_hash;
7861+
block.header.chain_length = peer_info.stacks_tip_height + 1;
7862+
let block_signer_signature_hash = block.header.signer_signature_hash();
7863+
signer_test.propose_block(block.clone(), short_timeout);
7864+
7865+
info!(
7866+
"----- Waiting for a pending block proposal in SignerDb -----";
7867+
"signer_signature_hash" => block_signer_signature_hash.to_hex(),
7868+
);
7869+
let mut last_log = Instant::now();
7870+
last_log -= Duration::from_secs(5);
7871+
wait_for(120, || {
7872+
let sighash = match signer_db.get_pending_block_validation() {
7873+
Ok(Some(sighash)) => sighash,
7874+
Err(e) => {
7875+
error!("Failed to get pending block validation: {e}");
7876+
panic!("Failed to get pending block validation");
7877+
}
7878+
Ok(None) => {
7879+
if last_log.elapsed() > Duration::from_secs(5) {
7880+
info!("----- No pending block validations found -----");
7881+
last_log = Instant::now();
7882+
}
7883+
return Ok(false);
7884+
}
7885+
};
7886+
if last_log.elapsed() > Duration::from_secs(5) && sighash != block_signer_signature_hash {
7887+
let pending_block_validations = signer_db
7888+
.get_all_pending_block_validations()
7889+
.expect("Failed to get pending block validations");
7890+
info!(
7891+
"----- Received a different pending block proposal -----";
7892+
"db_signer_signature_hash" => sighash.to_hex(),
7893+
"proposed_signer_signature_hash" => block_signer_signature_hash.to_hex(),
7894+
"pending_block_validations" => pending_block_validations.iter()
7895+
.map(|p| p.signer_signature_hash.to_hex())
7896+
.collect::<Vec<String>>()
7897+
.join(", "),
7898+
);
7899+
last_log = Instant::now();
7900+
}
7901+
Ok(sighash == block_signer_signature_hash)
7902+
})
7903+
.expect("Timed out waiting for pending block proposal");
7904+
7905+
// Set the delay to 0 so that the block validation finishes quickly
7906+
TEST_VALIDATE_DELAY_DURATION_SECS.lock().unwrap().take();
7907+
7908+
info!("----- Waiting for pending block validation to be submitted -----");
7909+
7910+
wait_for(30, || {
7911+
let proposal_responses = test_observer::get_proposal_responses();
7912+
let found_proposal = proposal_responses
7913+
.iter()
7914+
.any(|p| p.signer_signature_hash() == block_signer_signature_hash);
7915+
Ok(found_proposal)
7916+
})
7917+
.expect("Timed out waiting for pending block validation to be submitted");
7918+
7919+
info!("----- Waiting for pending block validation to be removed -----");
7920+
wait_for(30, || {
7921+
let Ok(Some(sighash)) = signer_db.get_pending_block_validation() else {
7922+
// There are no pending block validations
7923+
return Ok(true);
7924+
};
7925+
Ok(sighash != block_signer_signature_hash)
7926+
})
7927+
.expect("Timed out waiting for pending block validation to be removed");
7928+
7929+
// for test cleanup we need to wait for block rejections
7930+
let signer_keys = signer_test
7931+
.signer_configs
7932+
.iter()
7933+
.map(|c| StacksPublicKey::from_private(&c.stacks_private_key))
7934+
.collect::<Vec<_>>();
7935+
signer_test
7936+
.wait_for_block_rejections(30, &signer_keys)
7937+
.expect("Timed out waiting for block rejections");
7938+
7939+
info!("------------------------- Shutdown -------------------------");
7940+
signer_test.shutdown();
7941+
}
7942+
77707943
#[test]
77717944
#[ignore]
77727945
/// Test that a miner will extend its tenure after the succeding miner fails to mine a block.

0 commit comments

Comments
 (0)