Skip to content

Commit e315248

Browse files
authored
Merge pull request #6129 from jferrant/bug/post-block-infinite-loop-signers
Signers should not infinitely loop when posting a block to the node
2 parents 2484812 + 8b2d41c commit e315248

File tree

4 files changed

+61
-55
lines changed

4 files changed

+61
-55
lines changed

stacks-signer/src/client/mod.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use libstackerdb::Error as StackerDBError;
2828
pub use stackerdb::*;
2929
pub use stacks_client::*;
3030
use stacks_common::codec::Error as CodecError;
31-
use stacks_common::debug;
31+
use stacks_common::{debug, warn};
3232

3333
/// Backoff timer initial interval in milliseconds
3434
const BACKOFF_INITIAL_INTERVAL: u64 = 128;
@@ -103,7 +103,7 @@ pub enum ClientError {
103103
pub fn retry_with_exponential_backoff<F, E, T>(request_fn: F) -> Result<T, ClientError>
104104
where
105105
F: FnMut() -> Result<T, backoff::Error<E>>,
106-
E: std::fmt::Debug,
106+
E: std::fmt::Debug + Into<ClientError>,
107107
{
108108
let notify = |err, dur| {
109109
debug!(
@@ -117,7 +117,16 @@ where
117117
.with_max_elapsed_time(Some(Duration::from_secs(BACKOFF_MAX_ELAPSED)))
118118
.build();
119119

120-
backoff::retry_notify(backoff_timer, request_fn, notify).map_err(|_| ClientError::RetryTimeout)
120+
backoff::retry_notify(backoff_timer, request_fn, notify).map_err(|e| match e {
121+
backoff::Error::Permanent(err) => {
122+
warn!("Non-retry error during request: {err:?}");
123+
err.into()
124+
}
125+
backoff::Error::Transient { err, .. } => {
126+
warn!("Exceeded max retries during request: {err:?}");
127+
err.into()
128+
}
129+
})
121130
}
122131

123132
#[cfg(test)]

stacks-signer/src/client/stacks_client.rs

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
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
use std::collections::{HashMap, VecDeque};
17-
use std::fmt::Display;
18-
use std::time::{Duration, Instant};
1917

2018
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
2119
use blockstack_lib::chainstate::stacks::boot::{NakamotoSignerEntry, SIGNERS_NAME};
@@ -602,36 +600,6 @@ impl StacksClient {
602600
Ok(account_entry)
603601
}
604602

605-
/// Post a block to the stacks-node, retry forever on errors.
606-
///
607-
/// In tests, this panics if the retry takes longer than 30 seconds.
608-
pub fn post_block_until_ok<F: Display>(&self, log_fmt: &F, block: &NakamotoBlock) -> bool {
609-
debug!("StacksClient: Posting block to stacks node";
610-
"signer_signature_hash" => %block.header.signer_signature_hash(),
611-
"block_id" => %block.header.block_id(),
612-
"block_height" => %block.header.chain_length,
613-
);
614-
let start_time = Instant::now();
615-
loop {
616-
match self.post_block(block) {
617-
Ok(block_push_result) => {
618-
debug!("{log_fmt}: Block pushed to stacks node: {block_push_result:?}");
619-
return block_push_result;
620-
}
621-
Err(e) => {
622-
if cfg!(any(test, feature = "testing"))
623-
&& start_time.elapsed() > Duration::from_secs(30)
624-
{
625-
panic!(
626-
"{log_fmt}: Timed out in test while pushing block to stacks node: {e}"
627-
);
628-
}
629-
warn!("{log_fmt}: Failed to push block to stacks node: {e}. Retrying...");
630-
}
631-
};
632-
}
633-
}
634-
635603
/// Try to post a completed nakamoto block to our connected stacks-node
636604
/// Returns `true` if the block was accepted or `false` if the block
637605
/// was rejected.
@@ -644,22 +612,30 @@ impl StacksClient {
644612
let path = format!("{}{}?broadcast=1", self.http_origin, postblock_v3::PATH);
645613
let timer = crate::monitoring::actions::new_rpc_call_timer(&path, &self.http_origin);
646614
let send_request = || {
647-
self.stacks_node_client
615+
let response = self
616+
.stacks_node_client
648617
.post(&path)
649618
.header("Content-Type", "application/octet-stream")
650619
.header(AUTHORIZATION, self.auth_password.clone())
651620
.body(block.serialize_to_vec())
652621
.send()
653622
.map_err(|e| {
654623
debug!("Failed to submit block to the Stacks node: {e:?}");
655-
backoff::Error::transient(e)
656-
})
624+
backoff::Error::transient(ClientError::from(e))
625+
})?;
626+
if !response.status().is_success() {
627+
warn!(
628+
"Failed to post block to stacks-node, will retry until limit reached";
629+
"http_status" => %response.status(),
630+
);
631+
return Err(backoff::Error::transient(ClientError::RequestFailure(
632+
response.status(),
633+
)));
634+
}
635+
Ok(response)
657636
};
658637
let response = retry_with_exponential_backoff(send_request)?;
659638
timer.stop_and_record();
660-
if !response.status().is_success() {
661-
return Err(ClientError::RequestFailure(response.status()));
662-
}
663639
let post_block_resp = response.json::<StacksBlockAcceptedData>()?;
664640
Ok(post_block_resp.accepted)
665641
}

stacks-signer/src/v0/signer.rs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ impl Signer {
455455
if self.test_skip_block_broadcast(b) {
456456
return;
457457
}
458-
stacks_client.post_block_until_ok(self, b);
458+
self.handle_post_block(stacks_client, b);
459459
}
460460
SignerMessage::MockProposal(mock_proposal) => {
461461
let epoch = match stacks_client.get_node_epoch() {
@@ -1528,12 +1528,11 @@ impl Signer {
15281528
}
15291529

15301530
fn broadcast_signed_block(
1531-
&self,
1531+
&mut self,
15321532
stacks_client: &StacksClient,
15331533
mut block: NakamotoBlock,
15341534
addrs_to_sigs: &HashMap<StacksAddress, MessageSignature>,
15351535
) {
1536-
let block_hash = block.header.signer_signature_hash();
15371536
// collect signatures for the block
15381537
let signatures: Vec<_> = self
15391538
.signer_addresses
@@ -1548,17 +1547,25 @@ impl Signer {
15481547
if self.test_skip_block_broadcast(&block) {
15491548
return;
15501549
}
1551-
debug!(
1552-
"{self}: Broadcasting Stacks block {} to node",
1553-
&block.block_id()
1554-
);
1555-
stacks_client.post_block_until_ok(self, &block);
1550+
self.handle_post_block(stacks_client, &block);
1551+
}
15561552

1557-
if let Err(e) = self
1558-
.signer_db
1559-
.set_block_broadcasted(&block_hash, get_epoch_time_secs())
1560-
{
1561-
warn!("{self}: Failed to set block broadcasted for {block_hash}: {e:?}");
1553+
/// Attempt to post a block to the stacks-node and handle the result
1554+
pub fn handle_post_block(&mut self, stacks_client: &StacksClient, block: &NakamotoBlock) {
1555+
let block_hash = block.header.signer_signature_hash();
1556+
match stacks_client.post_block(block) {
1557+
Ok(accepted) => {
1558+
debug!("{self}: Block {block_hash} accepted by stacks node: {accepted}");
1559+
if let Err(e) = self
1560+
.signer_db
1561+
.set_block_broadcasted(&block_hash, get_epoch_time_secs())
1562+
{
1563+
warn!("{self}: Failed to set block broadcasted for {block_hash}: {e:?}");
1564+
}
1565+
}
1566+
Err(e) => {
1567+
warn!("{self}: Failed to broadcast block {block_hash} to the node: {e}")
1568+
}
15621569
}
15631570
}
15641571

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3493,7 +3493,9 @@ fn tx_replay_rejected_when_forking_across_reward_cycle() {
34933493
let commits_count = submitted_commits.load(Ordering::SeqCst);
34943494
next_block_and(btc_controller, 60, || {
34953495
let commits_submitted = submitted_commits.load(Ordering::SeqCst);
3496-
Ok(commits_submitted > commits_count)
3496+
Ok(commits_submitted > commits_count
3497+
&& get_chain_info(&signer_test.running_nodes.conf).burn_block_height
3498+
> current_burn_height)
34973499
})
34983500
.unwrap();
34993501
}
@@ -3502,6 +3504,18 @@ fn tx_replay_rejected_when_forking_across_reward_cycle() {
35023504
assert_eq!(0, post_fork_tx_nonce);
35033505

35043506
info!("----- Check Signers Tx Replay state -----");
3507+
3508+
let info = get_chain_info(&signer_test.running_nodes.conf);
3509+
let all_signers = signer_test.signer_test_pks();
3510+
wait_for_state_machine_update(
3511+
30,
3512+
&info.pox_consensus,
3513+
info.burn_block_height,
3514+
None,
3515+
&all_signers,
3516+
SUPPORTED_SIGNER_PROTOCOL_VERSION,
3517+
)
3518+
.expect("Timed out waiting for signer states to update");
35053519
let (signer_states, _) = signer_test.get_burn_updated_states();
35063520
for state in signer_states {
35073521
assert!(

0 commit comments

Comments
 (0)