From e89d1af4c99a0a4cb5cf15984c04fb4bbec078b1 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 14 Mar 2024 18:14:31 -0400 Subject: [PATCH] Subscribe signer to new Burn block events Signed-off-by: Jacinta Ferrant --- libsigner/src/events.rs | 14 +++++ stacks-signer/src/client/mod.rs | 19 ------ stacks-signer/src/runloop.rs | 81 ++++++++++++++----------- stacks-signer/src/signer.rs | 15 +++-- testnet/stacks-node/src/tests/signer.rs | 6 +- 5 files changed, 74 insertions(+), 61 deletions(-) diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index 1c29ec941..08d622509 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -73,6 +73,8 @@ pub enum SignerEvent { BlockValidationResponse(BlockValidateResponse), /// Status endpoint request StatusCheck, + /// A new burn block event was received + NewBurnBlock, } impl StacksMessageCodec for BlockProposalSigners { @@ -281,6 +283,8 @@ impl EventReceiver for SignerEventReceiver { process_stackerdb_event(event_receiver.local_addr, request, is_mainnet) } else if request.url() == "/proposal_response" { process_proposal_response(request) + } else if request.url() == "/new_burn_block" { + process_new_burn_block_event(request) } else { let url = request.url().to_string(); @@ -438,6 +442,16 @@ fn process_proposal_response(mut request: HttpRequest) -> Result Result { + debug!("Got burn_block event"); + let event = SignerEvent::NewBurnBlock; + if let Err(e) = request.respond(HttpResponse::empty(200u16)) { + error!("Failed to respond to request: {:?}", &e); + } + Ok(event) +} + fn get_signers_db_signer_set_message_id(name: &str) -> Option<(u32, u32)> { // Splitting the string by '-' let parts: Vec<&str> = name.split('-').collect(); diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index 8e4302904..9c828761b 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -23,7 +23,6 @@ use std::time::Duration; use clarity::vm::errors::Error as ClarityError; use clarity::vm::types::serialization::SerializationError; -use libsigner::RPCError; use libstackerdb::Error as StackerDBError; use slog::slog_debug; pub use stackerdb::*; @@ -48,9 +47,6 @@ pub enum ClientError { /// Failed to sign stacker-db chunk #[error("Failed to sign stacker-db chunk: {0}")] FailToSign(#[from] StackerDBError), - /// Failed to write to stacker-db due to RPC error - #[error("Failed to write to stacker-db instance: {0}")] - PutChunkFailed(#[from] RPCError), /// Stacker-db instance rejected the chunk #[error("Stacker-db rejected the chunk. Reason: {0}")] PutChunkRejected(String), @@ -72,33 +68,18 @@ pub enum ClientError { /// Failed to parse a Clarity value #[error("Received a malformed clarity value: {0}")] MalformedClarityValue(String), - /// Invalid Clarity Name - #[error("Invalid Clarity Name: {0}")] - InvalidClarityName(String), /// Backoff retry timeout #[error("Backoff retry timeout occurred. Stacks node may be down.")] RetryTimeout, /// Not connected #[error("Not connected")] NotConnected, - /// Invalid signing key - #[error("Signing key not represented in the list of signers")] - InvalidSigningKey, /// Clarity interpreter error #[error("Clarity interpreter error: {0}")] ClarityError(#[from] ClarityError), - /// Our stacks address does not belong to a registered signer - #[error("Our stacks address does not belong to a registered signer")] - NotRegistered, - /// Reward set not yet calculated for the given reward cycle - #[error("Reward set not yet calculated for reward cycle: {0}")] - RewardSetNotYetCalculated(u64), /// Malformed reward set #[error("Malformed contract data: {0}")] MalformedContractData(String), - /// No reward set exists for the given reward cycle - #[error("No reward set exists for reward cycle {0}")] - NoRewardSet(u64), /// Stacks node does not support a feature we need #[error("Stacks node does not support a required feature: {0}")] UnsupportedStacksFeature(String), diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 607bb8489..df5777134 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -44,12 +44,14 @@ pub struct RunLoopCommand { } /// The runloop state -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone, Copy)] pub enum State { /// The runloop is uninitialized Uninitialized, - /// The runloop is initialized - Initialized, + /// The runloop has no registered signers + NoRegisteredSigners, + /// The runloop has registered signers + RegisteredSigners, } /// The runloop for the stacks signer @@ -262,9 +264,9 @@ impl RunLoop { signer.next_signer_slot_ids = new_signer_config.signer_slot_ids.clone(); } } - self.stacks_signers - .insert(reward_index, Signer::from(new_signer_config)); - debug!("Reward cycle #{reward_cycle} Signer #{signer_id} initialized."); + let new_signer = Signer::from(new_signer_config); + info!("{new_signer} initialized."); + self.stacks_signers.insert(reward_index, new_signer); } else { // TODO: Update `current` here once the signer binary is tracking its own latest burnchain/stacks views. if current { @@ -277,7 +279,6 @@ impl RunLoop { } /// Refresh the signer configuration by retrieving the necessary information from the stacks node - /// Note: this will trigger DKG if required fn refresh_signers(&mut self, current_reward_cycle: u64) -> Result<(), ClientError> { let next_reward_cycle = current_reward_cycle.saturating_add(1); self.refresh_signer_config(current_reward_cycle, true); @@ -307,28 +308,15 @@ impl RunLoop { signer.coordinator.state = CoordinatorState::Idle; signer.state = SignerState::Idle; } - if signer.approved_aggregate_public_key.is_none() { - retry_with_exponential_backoff(|| { - signer - .update_dkg(&self.stacks_client) - .map_err(backoff::Error::transient) - })?; - } } - for i in to_delete.into_iter() { - if let Some(signer) = self.stacks_signers.remove(&i) { - info!("{signer}: Tenure has completed. Removing signer from runloop.",); - } + for idx in to_delete { + self.stacks_signers.remove(&idx); } - if self.stacks_signers.is_empty() { - info!("Signer is not registered for the current reward cycle ({current_reward_cycle}) or next reward cycle ({next_reward_cycle}). Waiting for confirmed registration..."); - self.state = State::Uninitialized; - return Err(ClientError::NotRegistered); - } - if self.state != State::Initialized { - info!("Signer runloop successfully initialized!"); - } - self.state = State::Initialized; + self.state = if self.stacks_signers.is_empty() { + State::NoRegisteredSigners + } else { + State::RegisteredSigners + }; Ok(()) } } @@ -362,19 +350,39 @@ impl SignerRunLoop, RunLoopCommand> for RunLoop { .map_err(backoff::Error::transient) }) else { error!("Failed to retrieve current reward cycle"); - warn!("Ignoring event: {event:?}"); return None; }; - if let Err(e) = self.refresh_signers(current_reward_cycle) { + if self.state == State::Uninitialized || event == Some(SignerEvent::NewBurnBlock) { + let old_state = self.state; if self.state == State::Uninitialized { - // If we were never actually initialized, we cannot process anything. Just return. - warn!("Failed to initialize signers. Are you sure this signer is correctly registered for the current or next reward cycle?"); - warn!("Ignoring event: {event:?}"); + info!("Initializing signer..."); + } else { + info!("New burn block event received. Refreshing signer state..."); + } + if let Err(e) = self.refresh_signers(current_reward_cycle) { + error!("Failed to refresh signers: {e}. Signer may have an outdated view of the network"); + } + if self.state == State::NoRegisteredSigners { + let next_reward_cycle = current_reward_cycle.saturating_add(1); + info!("Signer is not registered for the current reward cycle ({current_reward_cycle}) or next reward cycle ({next_reward_cycle}). Waiting for confirmed registration..."); return None; } - error!("Failed to refresh signers: {e}. Signer may have an outdated view of the network. Attempting to process event anyway."); + if old_state == State::Uninitialized { + info!("Signer successfully initialized."); + } else { + info!("Signer state successfully refreshed."); + }; } for signer in self.stacks_signers.values_mut() { + if signer.approved_aggregate_public_key.is_none() { + if let Err(e) = retry_with_exponential_backoff(|| { + signer + .update_dkg(&self.stacks_client) + .map_err(backoff::Error::transient) + }) { + error!("{signer}: failed to update DKG: {e}"); + } + } if signer.state == SignerState::TenureCompleted { warn!("{signer}: Signer's tenure has completed. This signer should have been cleaned up during refresh."); continue; @@ -383,12 +391,13 @@ impl SignerRunLoop, RunLoopCommand> for RunLoop { Some(SignerEvent::BlockValidationResponse(_)) => Some(current_reward_cycle % 2), // Block proposal events do have reward cycles, but each proposal has its own cycle, // and the vec could be heterogenous, so, don't differentiate. - Some(SignerEvent::ProposedBlocks(_)) => None, + Some(SignerEvent::ProposedBlocks(_)) + | Some(SignerEvent::NewBurnBlock) + | Some(SignerEvent::StatusCheck) + | None => None, Some(SignerEvent::SignerMessages(msg_parity, ..)) => { Some(u64::from(msg_parity) % 2) } - Some(SignerEvent::StatusCheck) => None, - None => None, }; let other_signer_parity = (signer.reward_cycle + 1) % 2; if event_parity == Some(other_signer_parity) { diff --git a/stacks-signer/src/signer.rs b/stacks-signer/src/signer.rs index 65c32dc1c..e8e0b8963 100644 --- a/stacks-signer/src/signer.rs +++ b/stacks-signer/src/signer.rs @@ -1123,6 +1123,7 @@ impl Signer { /// Update the DKG for the provided signer info, triggering it if required pub fn update_dkg(&mut self, stacks_client: &StacksClient) -> Result<(), ClientError> { let reward_cycle = self.reward_cycle; + let old_dkg = self.approved_aggregate_public_key; self.approved_aggregate_public_key = stacks_client.get_approved_aggregate_key(reward_cycle)?; if self.approved_aggregate_public_key.is_some() { @@ -1131,11 +1132,12 @@ impl Signer { // then overwrite our value accordingly. Otherwise, we will be locked out of the round and should not participate. self.coordinator .set_aggregate_public_key(self.approved_aggregate_public_key); - // We have an approved aggregate public key. Do nothing further - debug!( - "{self}: Have updated DKG value to {:?}.", - self.approved_aggregate_public_key - ); + if old_dkg != self.approved_aggregate_public_key { + debug!( + "{self}: updated DKG value to {:?}.", + self.approved_aggregate_public_key + ); + } return Ok(()); }; let coordinator_id = self.coordinator_selector.get_coordinator().0; @@ -1225,6 +1227,9 @@ impl Signer { Some(SignerEvent::StatusCheck) => { debug!("{self}: Received a status check event.") } + Some(SignerEvent::NewBurnBlock) => { + // Already handled this case in the main loop + } None => { // No event. Do nothing. debug!("{self}: No event received") diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index fb867db0a..8c2f52374 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -804,7 +804,11 @@ fn setup_stx_btc_node( naka_conf.events_observers.insert(EventObserverConfig { endpoint: format!("{}", signer_config.endpoint), - events_keys: vec![EventKeyType::StackerDBChunks, EventKeyType::BlockProposal], + events_keys: vec![ + EventKeyType::StackerDBChunks, + EventKeyType::BlockProposal, + EventKeyType::BurnchainBlocks, + ], }); }