Skip to content

Commit ffde1b4

Browse files
authored
Merge branch 'develop' into fix/4145
2 parents b4af50d + 64bbb91 commit ffde1b4

File tree

6 files changed

+237
-8
lines changed

6 files changed

+237
-8
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ jobs:
135135
- tests::signer::v0::block_commit_delay
136136
- tests::signer::v0::continue_after_fast_block_no_sortition
137137
- tests::signer::v0::block_validation_response_timeout
138+
- tests::signer::v0::block_validation_check_rejection_timeout_heuristic
138139
- tests::signer::v0::block_validation_pending_table
139140
- tests::signer::v0::new_tenure_while_validating_previous_scenario
140141
- tests::signer::v0::tenure_extend_after_bad_commit

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1414
### Changed
1515

1616
- Miner will include other transactions in blocks with tenure extend transactions (#5760)
17+
- Add `block_rejection_timeout_steps` to miner configuration for defining rejections-based timeouts while waiting for signers response (#5705)
1718
- Miner will not issue a tenure extend until at least half of the block budget has been spent (#5757)
1819

1920
### Fixed

stackslib/src/config/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
pub mod chain_data;
1818

19-
use std::collections::{HashMap, HashSet};
19+
use std::collections::{BTreeMap, HashMap, HashSet};
2020
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
2121
use std::path::PathBuf;
2222
use std::str::FromStr;
@@ -2184,6 +2184,8 @@ pub struct MinerConfig {
21842184
pub tenure_timeout: Duration,
21852185
/// Percentage of block budget that must be used before attempting a time-based tenure extend
21862186
pub tenure_extend_cost_threshold: u64,
2187+
/// Define the timeout to apply while waiting for signers responses, based on the amount of rejections
2188+
pub block_rejection_timeout_steps: HashMap<u32, Duration>,
21872189
}
21882190

21892191
impl Default for MinerConfig {
@@ -2223,6 +2225,15 @@ impl Default for MinerConfig {
22232225
tenure_extend_poll_secs: Duration::from_secs(DEFAULT_TENURE_EXTEND_POLL_SECS),
22242226
tenure_timeout: Duration::from_secs(DEFAULT_TENURE_TIMEOUT_SECS),
22252227
tenure_extend_cost_threshold: DEFAULT_TENURE_EXTEND_COST_THRESHOLD,
2228+
2229+
block_rejection_timeout_steps: {
2230+
let mut rejections_timeouts_default_map = HashMap::<u32, Duration>::new();
2231+
rejections_timeouts_default_map.insert(0, Duration::from_secs(600));
2232+
rejections_timeouts_default_map.insert(10, Duration::from_secs(300));
2233+
rejections_timeouts_default_map.insert(20, Duration::from_secs(150));
2234+
rejections_timeouts_default_map.insert(30, Duration::from_secs(0));
2235+
rejections_timeouts_default_map
2236+
},
22262237
}
22272238
}
22282239
}
@@ -2620,6 +2631,7 @@ pub struct MinerConfigFile {
26202631
pub tenure_extend_poll_secs: Option<u64>,
26212632
pub tenure_timeout_secs: Option<u64>,
26222633
pub tenure_extend_cost_threshold: Option<u64>,
2634+
pub block_rejection_timeout_steps: Option<HashMap<String, u64>>,
26232635
}
26242636

26252637
impl MinerConfigFile {
@@ -2763,6 +2775,24 @@ impl MinerConfigFile {
27632775
tenure_extend_poll_secs: self.tenure_extend_poll_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_extend_poll_secs),
27642776
tenure_timeout: self.tenure_timeout_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_timeout),
27652777
tenure_extend_cost_threshold: self.tenure_extend_cost_threshold.unwrap_or(miner_default_config.tenure_extend_cost_threshold),
2778+
2779+
block_rejection_timeout_steps: {
2780+
if let Some(block_rejection_timeout_items) = self.block_rejection_timeout_steps {
2781+
let mut rejection_timeout_durations = HashMap::<u32, Duration>::new();
2782+
for (slice, seconds) in block_rejection_timeout_items.iter() {
2783+
match slice.parse::<u32>() {
2784+
Ok(slice_slot) => rejection_timeout_durations.insert(slice_slot, Duration::from_secs(*seconds)),
2785+
Err(e) => panic!("block_rejection_timeout_steps keys must be unsigned integers: {}", e)
2786+
};
2787+
}
2788+
if !rejection_timeout_durations.contains_key(&0) {
2789+
panic!("block_rejection_timeout_steps requires a definition for the '0' key/step");
2790+
}
2791+
rejection_timeout_durations
2792+
} else{
2793+
miner_default_config.block_rejection_timeout_steps
2794+
}
2795+
}
27662796
})
27672797
}
27682798
}

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

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

16+
use std::collections::BTreeMap;
17+
use std::ops::Bound::Included;
1618
use std::sync::atomic::AtomicBool;
1719
use std::sync::{Arc, Mutex};
1820
use std::thread::JoinHandle;
21+
use std::time::{Duration, Instant};
1922

2023
use libsigner::v0::messages::{MinerSlotID, SignerMessage as SignerMessageV0};
2124
use libsigner::{BlockProposal, SignerSession, StackerDBSession};
@@ -66,6 +69,8 @@ pub struct SignerCoordinator {
6669
/// Rather, this burn block is used to determine whether or not a new
6770
/// burn block has arrived since this thread started.
6871
burn_tip_at_start: ConsensusHash,
72+
/// The timeout configuration based on the percentage of rejections
73+
block_rejection_timeout_steps: BTreeMap<u32, Duration>,
6974
}
7075

7176
impl SignerCoordinator {
@@ -101,6 +106,14 @@ impl SignerCoordinator {
101106
let miners_contract_id = boot_code_id(MINERS_NAME, is_mainnet);
102107
let miners_session = StackerDBSession::new(&rpc_socket.to_string(), miners_contract_id);
103108

109+
// build a BTreeMap of the various timeout steps
110+
let mut block_rejection_timeout_steps = BTreeMap::<u32, Duration>::new();
111+
for (percentage, duration) in config.miner.block_rejection_timeout_steps.iter() {
112+
let rejections_amount =
113+
((f64::from(listener.total_weight) / 100.0) * f64::from(*percentage)) as u32;
114+
block_rejection_timeout_steps.insert(rejections_amount, *duration);
115+
}
116+
104117
let mut sc = Self {
105118
message_key,
106119
is_mainnet,
@@ -111,6 +124,7 @@ impl SignerCoordinator {
111124
keep_running,
112125
listener_thread: None,
113126
burn_tip_at_start: burn_tip_at_start.clone(),
127+
block_rejection_timeout_steps,
114128
};
115129

116130
// Spawn the signer DB listener thread
@@ -293,16 +307,38 @@ impl SignerCoordinator {
293307
sortdb: &SortitionDB,
294308
counters: &Counters,
295309
) -> Result<Vec<MessageSignature>, NakamotoNodeError> {
310+
// the amount of current rejections (used to eventually modify the timeout)
311+
let mut rejections: u32 = 0;
312+
// default timeout (the 0 entry must be always present)
313+
let mut rejections_timeout = self
314+
.block_rejection_timeout_steps
315+
.get(&rejections)
316+
.ok_or_else(|| {
317+
NakamotoNodeError::SigningCoordinatorFailure(
318+
"Invalid rejection timeout step function definition".into(),
319+
)
320+
})?;
321+
322+
// this is used to track the start of the waiting cycle
323+
let rejections_timer = Instant::now();
296324
loop {
325+
// At every iteration wait for the block_status.
326+
// Exit when the amount of confirmations/rejections reaches the threshold (or until timeout)
327+
// Based on the amount of rejections, eventually modify the timeout.
297328
let block_status = match self.stackerdb_comms.wait_for_block_status(
298329
block_signer_sighash,
299330
EVENT_RECEIVER_POLL,
300331
|status| {
301-
status.total_weight_signed < self.weight_threshold
302-
&& status
303-
.total_reject_weight
304-
.saturating_add(self.weight_threshold)
305-
<= self.total_weight
332+
// rejections-based timeout expired?
333+
if rejections_timer.elapsed() > *rejections_timeout {
334+
return false;
335+
}
336+
// number or rejections changed?
337+
if status.total_reject_weight != rejections {
338+
return false;
339+
}
340+
// enough signatures?
341+
return status.total_weight_signed < self.weight_threshold;
306342
},
307343
)? {
308344
Some(status) => status,
@@ -336,10 +372,44 @@ impl SignerCoordinator {
336372
return Err(NakamotoNodeError::BurnchainTipChanged);
337373
}
338374

375+
if rejections_timer.elapsed() > *rejections_timeout {
376+
warn!("Timed out while waiting for responses from signers";
377+
"elapsed" => rejections_timer.elapsed().as_secs(),
378+
"rejections_timeout" => rejections_timeout.as_secs(),
379+
"rejections" => rejections,
380+
"rejections_threshold" => self.total_weight.saturating_sub(self.weight_threshold)
381+
);
382+
return Err(NakamotoNodeError::SigningCoordinatorFailure(
383+
"Timed out while waiting for signatures".into(),
384+
));
385+
}
386+
339387
continue;
340388
}
341389
};
342390

391+
if rejections != block_status.total_reject_weight {
392+
rejections = block_status.total_reject_weight;
393+
let (rejections_step, new_rejections_timeout) = self
394+
.block_rejection_timeout_steps
395+
.range((Included(0), Included(rejections)))
396+
.last()
397+
.ok_or_else(|| {
398+
NakamotoNodeError::SigningCoordinatorFailure(
399+
"Invalid rejection timeout step function definition".into(),
400+
)
401+
})?;
402+
rejections_timeout = new_rejections_timeout;
403+
info!("Number of received rejections updated, resetting timeout";
404+
"rejections" => rejections,
405+
"rejections_timeout" => rejections_timeout.as_secs(),
406+
"rejections_step" => rejections_step,
407+
"rejections_threshold" => self.total_weight.saturating_sub(self.weight_threshold));
408+
409+
counters.set_miner_current_rejections_timeout_secs(rejections_timeout.as_secs());
410+
counters.set_miner_current_rejections(rejections);
411+
}
412+
343413
if block_status
344414
.total_reject_weight
345415
.saturating_add(self.weight_threshold)
@@ -357,10 +427,18 @@ impl SignerCoordinator {
357427
"block_signer_sighash" => %block_signer_sighash,
358428
);
359429
return Ok(block_status.gathered_signatures.values().cloned().collect());
360-
} else {
430+
} else if rejections_timer.elapsed() > *rejections_timeout {
431+
warn!("Timed out while waiting for responses from signers";
432+
"elapsed" => rejections_timer.elapsed().as_secs(),
433+
"rejections_timeout" => rejections_timeout.as_secs(),
434+
"rejections" => rejections,
435+
"rejections_threshold" => self.total_weight.saturating_sub(self.weight_threshold)
436+
);
361437
return Err(NakamotoNodeError::SigningCoordinatorFailure(
362-
"Unblocked without reaching the threshold".into(),
438+
"Timed out while waiting for signatures".into(),
363439
));
440+
} else {
441+
continue;
364442
}
365443
}
366444
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ pub struct Counters {
118118
pub naka_miner_directives: RunLoopCounter,
119119
pub naka_submitted_commit_last_stacks_tip: RunLoopCounter,
120120

121+
pub naka_miner_current_rejections: RunLoopCounter,
122+
pub naka_miner_current_rejections_timeout_secs: RunLoopCounter,
123+
121124
#[cfg(test)]
122125
pub naka_skip_commit_op: TestFlag<bool>,
123126
}
@@ -214,6 +217,14 @@ impl Counters {
214217
pub fn set_microblocks_processed(&self, value: u64) {
215218
Counters::set(&self.microblocks_processed, value)
216219
}
220+
221+
pub fn set_miner_current_rejections_timeout_secs(&self, value: u64) {
222+
Counters::set(&self.naka_miner_current_rejections_timeout_secs, value)
223+
}
224+
225+
pub fn set_miner_current_rejections(&self, value: u32) {
226+
Counters::set(&self.naka_miner_current_rejections, u64::from(value))
227+
}
217228
}
218229

219230
/// Coordinating a node running in neon mode.

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7895,6 +7895,114 @@ fn block_validation_response_timeout() {
78957895
);
78967896
}
78977897

7898+
// Verify that the miner timeout while waiting for signers will change accordingly
7899+
// to rejections.
7900+
#[test]
7901+
#[ignore]
7902+
fn block_validation_check_rejection_timeout_heuristic() {
7903+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
7904+
return;
7905+
}
7906+
7907+
info!("------------------------- Test Setup -------------------------");
7908+
let num_signers = 20;
7909+
let timeout = Duration::from_secs(30);
7910+
let sender_sk = Secp256k1PrivateKey::random();
7911+
let sender_addr = tests::to_addr(&sender_sk);
7912+
let send_amt = 100;
7913+
let send_fee = 180;
7914+
7915+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
7916+
num_signers,
7917+
vec![(sender_addr, send_amt + send_fee)],
7918+
|config| {
7919+
config.block_proposal_validation_timeout = timeout;
7920+
},
7921+
|config| {
7922+
config.miner.block_rejection_timeout_steps.clear();
7923+
config
7924+
.miner
7925+
.block_rejection_timeout_steps
7926+
.insert(0, Duration::from_secs(123));
7927+
config
7928+
.miner
7929+
.block_rejection_timeout_steps
7930+
.insert(10, Duration::from_secs(20));
7931+
config
7932+
.miner
7933+
.block_rejection_timeout_steps
7934+
.insert(15, Duration::from_secs(10));
7935+
config
7936+
.miner
7937+
.block_rejection_timeout_steps
7938+
.insert(20, Duration::from_secs(99));
7939+
},
7940+
None,
7941+
None,
7942+
);
7943+
7944+
let all_signers: Vec<_> = signer_test
7945+
.signer_stacks_private_keys
7946+
.iter()
7947+
.map(StacksPublicKey::from_private)
7948+
.collect();
7949+
7950+
signer_test.boot_to_epoch_3();
7951+
7952+
// note we just use mined nakamoto_blocks as the second block is not going to be confirmed
7953+
7954+
let mut test_rejections = |signer_split_index: usize, expected_timeout: u64| {
7955+
let blocks_before = test_observer::get_mined_nakamoto_blocks().len();
7956+
let (ignore_signers, reject_signers) = all_signers.split_at(signer_split_index);
7957+
7958+
info!("------------------------- Check Rejections-based timeout with {} rejections -------------------------", reject_signers.len());
7959+
7960+
TEST_REJECT_ALL_BLOCK_PROPOSAL.set(reject_signers.to_vec());
7961+
TEST_IGNORE_ALL_BLOCK_PROPOSALS.set(ignore_signers.to_vec());
7962+
7963+
next_block_and(
7964+
&mut signer_test.running_nodes.btc_regtest_controller,
7965+
30,
7966+
|| Ok(test_observer::get_mined_nakamoto_blocks().len() > blocks_before),
7967+
)
7968+
.unwrap();
7969+
7970+
signer_test
7971+
.wait_for_block_rejections(timeout.as_secs(), &reject_signers)
7972+
.unwrap();
7973+
7974+
wait_for(60, || {
7975+
Ok(signer_test
7976+
.running_nodes
7977+
.counters
7978+
.naka_miner_current_rejections
7979+
.get()
7980+
>= reject_signers.len() as u64)
7981+
})
7982+
.unwrap();
7983+
assert_eq!(
7984+
signer_test
7985+
.running_nodes
7986+
.counters
7987+
.naka_miner_current_rejections_timeout_secs
7988+
.get(),
7989+
expected_timeout
7990+
);
7991+
};
7992+
7993+
test_rejections(19, 123);
7994+
test_rejections(18, 20);
7995+
test_rejections(17, 10);
7996+
test_rejections(16, 99);
7997+
7998+
// reset reject/ignore
7999+
TEST_REJECT_ALL_BLOCK_PROPOSAL.set(vec![]);
8000+
TEST_IGNORE_ALL_BLOCK_PROPOSALS.set(vec![]);
8001+
8002+
info!("------------------------- Shutdown -------------------------");
8003+
signer_test.shutdown();
8004+
}
8005+
78988006
/// Test scenario:
78998007
///
79008008
/// - when a signer submits a block validation request and

0 commit comments

Comments
 (0)