diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index 1c29ec941e..e90fc3e852 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -31,6 +31,7 @@ use blockstack_lib::util_lib::boot::boot_code_id; use clarity::vm::types::serialization::SerializationError; use clarity::vm::types::QualifiedContractIdentifier; use serde::{Deserialize, Serialize}; +use serde_json::Value; use stacks_common::codec::{ read_next, read_next_at_most, read_next_exact, write_next, Error as CodecError, StacksMessageCodec, @@ -73,6 +74,8 @@ pub enum SignerEvent { BlockValidationResponse(BlockValidateResponse), /// Status endpoint request StatusCheck, + /// A new burn block event was received with the given burnchain block height + NewBurnBlock(u64), } impl StacksMessageCodec for BlockProposalSigners { @@ -281,6 +284,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 +443,38 @@ fn process_proposal_response(mut request: HttpRequest) -> Result Result { + debug!("Got burn_block event"); + let mut body = String::new(); + if let Err(e) = request.as_reader().read_to_string(&mut body) { + error!("Failed to read body: {:?}", &e); + + if let Err(e) = request.respond(HttpResponse::empty(200u16)) { + error!("Failed to respond to request: {:?}", &e); + } + return Err(EventError::MalformedRequest(format!( + "Failed to read body: {:?}", + &e + ))); + } + #[derive(Debug, Deserialize)] + struct TempBurnBlockEvent { + burn_block_hash: String, + burn_block_height: u64, + reward_recipients: Vec, + reward_slot_holders: Vec, + burn_amount: u64, + } + let temp: TempBurnBlockEvent = serde_json::from_slice(body.as_bytes()) + .map_err(|e| EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)))?; + let event = SignerEvent::NewBurnBlock(temp.burn_block_height); + 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 8e4302904c..9c828761bc 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/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index b6a7accdc0..12fdc8fc38 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -169,7 +169,7 @@ impl StackerDB { warn!("Failed to send message to stackerdb due to wrong version number. Attempted {}. Expected {}. Retrying...", slot_version, slot_metadata.slot_version); slot_version = slot_metadata.slot_version; } else { - warn!("Failed to send message to stackerdb due to wrong version number. Attempted {}. Expected unkown version number. Incrementing and retrying...", slot_version); + warn!("Failed to send message to stackerdb due to wrong version number. Attempted {}. Expected unknown version number. Incrementing and retrying...", slot_version); } if let Some(versions) = self.slot_versions.get_mut(&msg_id) { // NOTE: per the above, this is always executed diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 1cf142e13d..540ae828ec 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -46,6 +46,7 @@ use wsts::curve::point::{Compressed, Point}; use crate::client::{retry_with_exponential_backoff, ClientError}; use crate::config::GlobalConfig; +use crate::runloop::RewardCycleInfo; /// The Stacks signer client used to communicate with the stacks node #[derive(Clone, Debug)] @@ -363,16 +364,23 @@ impl StacksClient { Ok(peer_info.burn_block_height) } - /// Get the current reward cycle from the stacks node - pub fn get_current_reward_cycle(&self) -> Result { + /// Get the current reward cycle info from the stacks node + pub fn get_current_reward_cycle_info(&self) -> Result { let pox_data = self.get_pox_data()?; let blocks_mined = pox_data .current_burnchain_block_height .saturating_sub(pox_data.first_burnchain_block_height); - let reward_cycle_length = pox_data + let reward_phase_block_length = pox_data .reward_phase_block_length .saturating_add(pox_data.prepare_phase_block_length); - Ok(blocks_mined / reward_cycle_length) + let reward_cycle = blocks_mined / reward_phase_block_length; + Ok(RewardCycleInfo { + reward_cycle, + reward_phase_block_length, + prepare_phase_block_length: pox_data.prepare_phase_block_length, + first_burnchain_block_height: pox_data.first_burnchain_block_height, + last_burnchain_block_height: pox_data.current_burnchain_block_height, + }) } /// Helper function to retrieve the account info from the stacks node for a specific address @@ -735,9 +743,9 @@ mod tests { fn valid_reward_cycle_should_succeed() { let mock = MockServerClient::new(); let (pox_data_response, pox_data) = build_get_pox_data_response(None, None, None, None); - let h = spawn(move || mock.client.get_current_reward_cycle()); + let h = spawn(move || mock.client.get_current_reward_cycle_info()); write_response(mock.server, pox_data_response.as_bytes()); - let current_cycle_id = h.join().unwrap().unwrap(); + let current_cycle_info = h.join().unwrap().unwrap(); let blocks_mined = pox_data .current_burnchain_block_height .saturating_sub(pox_data.first_burnchain_block_height); @@ -745,13 +753,13 @@ mod tests { .reward_phase_block_length .saturating_add(pox_data.prepare_phase_block_length); let id = blocks_mined / reward_cycle_length; - assert_eq!(current_cycle_id, id); + assert_eq!(current_cycle_info.reward_cycle, id); } #[test] fn invalid_reward_cycle_should_fail() { let mock = MockServerClient::new(); - let h = spawn(move || mock.client.get_current_reward_cycle()); + let h = spawn(move || mock.client.get_current_reward_cycle_info()); write_response( mock.server, b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"id\":\"fake id\", \"is_pox_active\":false}}", diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 607bb8489a..58c5acddbf 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -17,22 +17,21 @@ use std::collections::VecDeque; use std::sync::mpsc::Sender; use std::time::Duration; -use blockstack_lib::chainstate::burn::ConsensusHashExtensions; +use blockstack_lib::burnchains::PoxConstants; use blockstack_lib::chainstate::stacks::boot::{NakamotoSignerEntry, SIGNERS_NAME}; use blockstack_lib::util_lib::boot::boot_code_id; use hashbrown::{HashMap, HashSet}; use libsigner::{SignerEvent, SignerRunLoop}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; -use stacks_common::types::chainstate::{ConsensusHash, StacksAddress, StacksPublicKey}; +use stacks_common::types::chainstate::{StacksAddress, StacksPublicKey}; use stacks_common::{debug, error, info, warn}; use wsts::curve::ecdsa; use wsts::curve::point::{Compressed, Point}; -use wsts::state_machine::coordinator::State as CoordinatorState; use wsts::state_machine::{OperationResult, PublicKeys}; use crate::client::{retry_with_exponential_backoff, ClientError, StacksClient}; use crate::config::{GlobalConfig, ParsedSignerEntries, SignerConfig}; -use crate::signer::{Command as SignerCommand, Signer, SignerSlotID, State as SignerState}; +use crate::signer::{Command as SignerCommand, Signer, SignerSlotID}; /// Which operation to perform #[derive(PartialEq, Clone, Debug)] @@ -44,12 +43,51 @@ 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 current reward cycle info +#[derive(PartialEq, Debug, Clone, Copy)] +pub struct RewardCycleInfo { + /// The current reward cycle + pub reward_cycle: u64, + /// The reward phase cycle length + pub reward_phase_block_length: u64, + /// The prepare phase length + pub prepare_phase_block_length: u64, + /// The first burn block height + pub first_burnchain_block_height: u64, + /// The burnchain block height of the last query + pub last_burnchain_block_height: u64, +} + +impl RewardCycleInfo { + /// Check if the provided burnchain block height is part of the reward cycle + pub fn is_in_reward_cycle(&self, burnchain_block_height: u64) -> bool { + let blocks_mined = burnchain_block_height.saturating_sub(self.first_burnchain_block_height); + let reward_cycle_length = self + .reward_phase_block_length + .saturating_add(self.prepare_phase_block_length); + let reward_cycle = blocks_mined / reward_cycle_length; + self.reward_cycle == reward_cycle + } + + /// Check if the provided burnchain block height is in the prepare phase + pub fn is_in_prepare_phase(&self, burnchain_block_height: u64) -> bool { + PoxConstants::static_is_in_prepare_phase( + self.first_burnchain_block_height, + self.reward_phase_block_length, + self.prepare_phase_block_length, + burnchain_block_height, + ) + } } /// The runloop for the stacks signer @@ -65,6 +103,8 @@ pub struct RunLoop { pub state: State, /// The commands received thus far pub commands: VecDeque, + /// The current reward cycle info. Only None if the runloop is uninitialized + pub current_reward_cycle_info: Option, } impl From for RunLoop { @@ -77,6 +117,7 @@ impl From for RunLoop { stacks_signers: HashMap::with_capacity(2), state: State::Uninitialized, commands: VecDeque::new(), + current_reward_cycle_info: None, } } } @@ -229,30 +270,18 @@ impl RunLoop { } /// Refresh signer configuration for a specific reward cycle - fn refresh_signer_config(&mut self, reward_cycle: u64, current: bool) { + fn refresh_signer_config(&mut self, reward_cycle: u64) { let reward_index = reward_cycle % 2; - let mut needs_refresh = false; - if let Some(signer) = self.stacks_signers.get_mut(&reward_index) { - let old_reward_cycle = signer.reward_cycle; - if old_reward_cycle == reward_cycle { - //If the signer is already registered for the reward cycle, we don't need to do anything further here - debug!("Signer is already configured for reward cycle {reward_cycle}.") - } else { - needs_refresh = true; - } - } else { - needs_refresh = true; - }; - if needs_refresh { - if let Some(new_signer_config) = self.get_signer_config(reward_cycle) { - let signer_id = new_signer_config.signer_id; - debug!("Signer is registered for reward cycle {reward_cycle} as signer #{signer_id}. Initializing signer state."); + if let Some(new_signer_config) = self.get_signer_config(reward_cycle) { + let signer_id = new_signer_config.signer_id; + debug!("Signer is registered for reward cycle {reward_cycle} as signer #{signer_id}. Initializing signer state."); + if reward_cycle != 0 { let prior_reward_cycle = reward_cycle.saturating_sub(1); let prior_reward_set = prior_reward_cycle % 2; if let Some(signer) = self.stacks_signers.get_mut(&prior_reward_set) { if signer.reward_cycle == prior_reward_cycle { // The signers have been calculated for the next reward cycle. Update the current one - debug!("{signer}: Next reward cycle ({reward_cycle}) signer set calculated. Reconfiguring signer."); + debug!("{signer}: Next reward cycle ({reward_cycle}) signer set calculated. Reconfiguring current reward cycle signer."); signer.next_signer_addresses = new_signer_config .signer_entries .signer_ids @@ -262,74 +291,86 @@ 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."); - } else { - // TODO: Update `current` here once the signer binary is tracking its own latest burnchain/stacks views. - if current { - warn!("Signer is not registered for the current reward cycle ({reward_cycle}). Waiting for confirmed registration..."); - } else { - debug!("Signer is not registered for reward cycle {reward_cycle}. Waiting for confirmed registration..."); - } + } + let new_signer = Signer::from(new_signer_config); + info!("{new_signer} initialized."); + self.stacks_signers.insert(reward_index, new_signer); + } else { + warn!("Signer is not registered for reward cycle {reward_cycle}. Waiting for confirmed registration..."); + } + } + + fn initialize_runloop(&mut self) -> Result<(), ClientError> { + debug!("Initializing signer runloop..."); + let reward_cycle_info = retry_with_exponential_backoff(|| { + self.stacks_client + .get_current_reward_cycle_info() + .map_err(backoff::Error::transient) + })?; + let current_reward_cycle = reward_cycle_info.reward_cycle; + self.refresh_signer_config(current_reward_cycle); + // We should only attempt to initialize the next reward cycle signer if we are in the prepare phase of the next reward cycle + if reward_cycle_info.is_in_prepare_phase(reward_cycle_info.last_burnchain_block_height) { + self.refresh_signer_config(current_reward_cycle.saturating_add(1)); + } + self.current_reward_cycle_info = Some(reward_cycle_info); + if self.stacks_signers.is_empty() { + self.state = State::NoRegisteredSigners; + } else { + self.state = State::RegisteredSigners; + } + Ok(()) + } + + fn refresh_runloop(&mut self, current_burn_block_height: u64) -> Result<(), ClientError> { + let reward_cycle_info = self + .current_reward_cycle_info + .as_mut() + .expect("FATAL: cannot be an initialized signer with no reward cycle info."); + // First ensure we refresh our view of the current reward cycle information + if !reward_cycle_info.is_in_reward_cycle(current_burn_block_height) { + let new_reward_cycle_info = retry_with_exponential_backoff(|| { + self.stacks_client + .get_current_reward_cycle_info() + .map_err(backoff::Error::transient) + })?; + *reward_cycle_info = new_reward_cycle_info; + } + let current_reward_cycle = reward_cycle_info.reward_cycle; + // We should only attempt to refresh the signer if we are not configured for the next reward cycle yet and we received a new burn block for its prepare phase + if reward_cycle_info.is_in_prepare_phase(current_burn_block_height) { + let next_reward_cycle = current_reward_cycle.saturating_add(1); + if self + .stacks_signers + .get(&(next_reward_cycle % 2)) + .map(|signer| signer.reward_cycle != next_reward_cycle) + .unwrap_or(true) + { + info!("Received a new burnchain block height ({current_burn_block_height}) in the prepare phase of the next reward cycle ({next_reward_cycle}). Checking for signer registration..."); + self.refresh_signer_config(next_reward_cycle); } } + self.cleanup_stale_signers(current_reward_cycle); + if self.stacks_signers.is_empty() { + self.state = State::NoRegisteredSigners; + } else { + self.state = State::RegisteredSigners; + } + Ok(()) } - /// 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); - self.refresh_signer_config(next_reward_cycle, false); - // TODO: do not use an empty consensus hash - let pox_consensus_hash = ConsensusHash::empty(); + fn cleanup_stale_signers(&mut self, current_reward_cycle: u64) { let mut to_delete = Vec::new(); for (idx, signer) in &mut self.stacks_signers { if signer.reward_cycle < current_reward_cycle { debug!("{signer}: Signer's tenure has completed."); - // We don't really need this state, but it's useful for debugging - signer.state = SignerState::TenureCompleted; to_delete.push(*idx); continue; } - let old_coordinator_id = signer.coordinator_selector.get_coordinator().0; - let updated_coordinator_id = signer - .coordinator_selector - .refresh_coordinator(&pox_consensus_hash); - if old_coordinator_id != updated_coordinator_id { - debug!( - "{signer}: Coordinator updated. Resetting state to Idle."; - "old_coordinator_id" => {old_coordinator_id}, - "updated_coordinator_id" => {updated_coordinator_id}, - "pox_consensus_hash" => %pox_consensus_hash - ); - 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.",); - } - } - 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!"); + for idx in to_delete { + self.stacks_signers.remove(&idx); } - self.state = State::Initialized; - Ok(()) } } @@ -355,46 +396,61 @@ impl SignerRunLoop, RunLoopCommand> for RunLoop { if let Some(cmd) = cmd { self.commands.push_back(cmd); } - // TODO: queue events and process them potentially after initialization success (similar to commands)? - let Ok(current_reward_cycle) = retry_with_exponential_backoff(|| { - self.stacks_client - .get_current_reward_cycle() - .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 { - // 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:?}"); + if self.state == State::Uninitialized { + if let Err(e) = self.initialize_runloop() { + error!("Failed to initialize signer runloop: {e}."); + if let Some(event) = event { + warn!("Ignoring event: {event:?}"); + } return None; } - error!("Failed to refresh signers: {e}. Signer may have an outdated view of the network. Attempting to process event anyway."); + } else if let Some(SignerEvent::NewBurnBlock(current_burn_block_height)) = event { + if let Err(e) = self.refresh_runloop(current_burn_block_height) { + error!("Failed to refresh signer runloop: {e}."); + warn!("Signer may have an outdated view of the network."); + } } - for signer in self.stacks_signers.values_mut() { - if signer.state == SignerState::TenureCompleted { - warn!("{signer}: Signer's tenure has completed. This signer should have been cleaned up during refresh."); - continue; + let current_reward_cycle = self + .current_reward_cycle_info + .as_ref() + .expect("FATAL: cannot be an initialized signer with no reward cycle info.") + .reward_cycle; + if self.state == State::NoRegisteredSigners { + let next_reward_cycle = current_reward_cycle.saturating_add(1); + if let Some(event) = event { + info!("Signer is not registered for the current reward cycle ({current_reward_cycle}) or next reward cycle ({next_reward_cycle}). Waiting for confirmed registration..."); + warn!("Ignoring event: {event:?}"); } + return None; + } + for signer in self.stacks_signers.values_mut() { let event_parity = match event { 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) { continue; } + 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}"); + } + } + signer.refresh_coordinator(); if let Err(e) = signer.process_event( &self.stacks_client, event.as_ref(), diff --git a/stacks-signer/src/signer.rs b/stacks-signer/src/signer.rs index 65c32dc1cc..f33da4304c 100644 --- a/stacks-signer/src/signer.rs +++ b/stacks-signer/src/signer.rs @@ -18,6 +18,7 @@ use std::path::PathBuf; use std::sync::mpsc::Sender; use std::time::Instant; +use blockstack_lib::chainstate::burn::ConsensusHashExtensions; use blockstack_lib::chainstate::nakamoto::signer_set::NakamotoSigners; use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockVote}; use blockstack_lib::chainstate::stacks::boot::SIGNERS_VOTING_FUNCTION_NAME; @@ -30,7 +31,7 @@ use libsigner::{ use serde_derive::{Deserialize, Serialize}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; use stacks_common::codec::{read_next, StacksMessageCodec}; -use stacks_common::types::chainstate::StacksAddress; +use stacks_common::types::chainstate::{ConsensusHash, StacksAddress}; use stacks_common::types::StacksEpochId; use stacks_common::util::hash::Sha512Trunc256Sum; use stacks_common::{debug, error, info, warn}; @@ -128,8 +129,6 @@ pub enum State { Idle, /// The signer is executing a DKG or Sign round OperationInProgress, - /// The signer's reward cycle has finished - TenureCompleted, } /// The stacks signer registered for the reward cycle @@ -256,6 +255,26 @@ impl From for Signer { } impl Signer { + /// Refresh the coordinator selector + pub fn refresh_coordinator(&mut self) { + // TODO: do not use an empty consensus hash + let pox_consensus_hash = ConsensusHash::empty(); + let old_coordinator_id = self.coordinator_selector.get_coordinator().0; + let updated_coordinator_id = self + .coordinator_selector + .refresh_coordinator(&pox_consensus_hash); + if old_coordinator_id != updated_coordinator_id { + debug!( + "{self}: Coordinator updated. Resetting state to Idle."; + "old_coordinator_id" => {old_coordinator_id}, + "updated_coordinator_id" => {updated_coordinator_id}, + "pox_consensus_hash" => %pox_consensus_hash + ); + self.coordinator.state = CoordinatorState::Idle; + self.state = State::Idle; + } + } + /// Finish an operation and update the coordinator selector accordingly fn finish_operation(&mut self) { self.state = State::Idle; @@ -375,9 +394,6 @@ impl Signer { // We cannot execute the next command until the current one is finished... debug!("{self}: Waiting for coordinator {coordinator_id:?} operation to finish. Coordinator state = {:?}", self.coordinator.state); } - State::TenureCompleted => { - warn!("{self}: Tenure completed. This signer should have been cleaned up during refresh.",); - } } } @@ -1123,6 +1139,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 +1148,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 +1243,9 @@ impl Signer { Some(SignerEvent::StatusCheck) => { debug!("{self}: Received a status check event.") } + Some(SignerEvent::NewBurnBlock(height)) => { + debug!("{self}: Receved a new burn block event for block height {height}") + } None => { // No event. Do nothing. debug!("{self}: No event received") diff --git a/stackslib/src/net/api/poststackerdbchunk.rs b/stackslib/src/net/api/poststackerdbchunk.rs index 1d35a8b908..2f28dd3f2d 100644 --- a/stackslib/src/net/api/poststackerdbchunk.rs +++ b/stackslib/src/net/api/poststackerdbchunk.rs @@ -151,6 +151,7 @@ impl StackerDBErrorCodes { match code { 0 => Some(Self::DataAlreadyExists), 1 => Some(Self::NoSuchSlot), + 2 => Some(Self::BadSigner), _ => None, } } @@ -228,28 +229,17 @@ impl RPCRequestHandler for RPCPostStackerDBChunkRequestHandler { } }; - let (reason, slot_metadata_opt, err_code) = - if let Some(slot_metadata) = slot_metadata_opt { - let code = if let NetError::BadSlotSigner(..) = e { - StackerDBErrorCodes::BadSigner - } else { - StackerDBErrorCodes::DataAlreadyExists - }; - - ( - serde_json::to_string(&code.clone().into_json()) - .unwrap_or("(unable to encode JSON)".to_string()), - Some(slot_metadata), - code, - ) + let err_code = if slot_metadata_opt.is_some() { + if let NetError::BadSlotSigner(..) = e { + StackerDBErrorCodes::BadSigner } else { - ( - serde_json::to_string(&StackerDBErrorCodes::NoSuchSlot.into_json()) - .unwrap_or("(unable to encode JSON)".to_string()), - None, - StackerDBErrorCodes::DataAlreadyExists, - ) - }; + StackerDBErrorCodes::DataAlreadyExists + } + } else { + StackerDBErrorCodes::NoSuchSlot + }; + let reason = serde_json::to_string(&err_code.clone().into_json()) + .unwrap_or("(unable to encode JSON)".to_string()); let ack = StackerDBChunkAckData { accepted: false, diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index fb867db0a3..cc861cd682 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -202,7 +202,8 @@ impl SignerTest { let current_block_height = self .running_nodes .btc_regtest_controller - .get_headers_height(); + .get_headers_height() + .saturating_sub(1); // Must subtract 1 since get_headers_height returns current block height + 1 let curr_reward_cycle = self.get_current_reward_cycle(); let next_reward_cycle = curr_reward_cycle.saturating_add(1); let next_reward_cycle_height = self @@ -221,15 +222,14 @@ impl SignerTest { let current_block_height = self .running_nodes .btc_regtest_controller - .get_headers_height(); + .get_headers_height() + .saturating_sub(1); // Must subtract 1 since get_headers_height returns current block height + 1 let reward_cycle_height = self .running_nodes .btc_regtest_controller .get_burnchain() .reward_cycle_to_block_height(reward_cycle); - reward_cycle_height - .saturating_sub(current_block_height) - .saturating_sub(1) + reward_cycle_height.saturating_sub(current_block_height) } // Only call after already past the epoch 3.0 boundary @@ -245,23 +245,26 @@ impl SignerTest { .running_nodes .btc_regtest_controller .get_headers_height() + .saturating_sub(1) // Must subtract 1 since get_headers_height returns current block height + 1 .saturating_add(nmb_blocks_to_mine_to_dkg); + let mut point = None; info!("Mining {nmb_blocks_to_mine_to_dkg} Nakamoto block(s) to reach DKG calculation at block height {end_block_height}"); for i in 1..=nmb_blocks_to_mine_to_dkg { info!("Mining Nakamoto block #{i} of {nmb_blocks_to_mine_to_dkg}"); self.mine_nakamoto_block(timeout); let hash = self.wait_for_validate_ok_response(timeout); - let signatures = self.wait_for_frost_signatures(timeout); + let (signatures, points) = if i != nmb_blocks_to_mine_to_dkg { + (self.wait_for_frost_signatures(timeout), vec![]) + } else { + self.wait_for_dkg_and_frost_signatures(timeout) + }; // Verify the signers accepted the proposed block and are using the new DKG to sign it for signature in &signatures { assert!(signature.verify(&set_dkg, hash.0.as_slice())); } + point = points.last().copied(); } - if nmb_blocks_to_mine_to_dkg == 0 { - None - } else { - Some(self.wait_for_dkg(timeout)) - } + point } // Only call after already past the epoch 3.0 boundary @@ -292,7 +295,13 @@ impl SignerTest { ) } if total_nmb_blocks_to_mine >= nmb_blocks_to_reward_cycle { - debug!("Mining {nmb_blocks_to_reward_cycle} Nakamoto block(s) to reach the next reward cycle boundary."); + let end_block_height = self + .running_nodes + .btc_regtest_controller + .get_headers_height() + .saturating_sub(1) // Must subtract 1 since get_headers_height returns current block height + 1 + .saturating_add(nmb_blocks_to_reward_cycle); + debug!("Mining {nmb_blocks_to_reward_cycle} Nakamoto block(s) to reach the next reward cycle boundary at {end_block_height}."); for i in 1..=nmb_blocks_to_reward_cycle { debug!("Mining Nakamoto block #{i} of {nmb_blocks_to_reward_cycle}"); let curr_reward_cycle = self.get_current_reward_cycle(); @@ -314,7 +323,8 @@ impl SignerTest { blocks_to_dkg = self.nmb_blocks_to_reward_set_calculation(); } } - for _ in 1..=total_nmb_blocks_to_mine { + for i in 1..=total_nmb_blocks_to_mine { + info!("Mining Nakamoto block #{i} of {total_nmb_blocks_to_mine} to reach {burnchain_height}"); let curr_reward_cycle = self.get_current_reward_cycle(); let set_dkg = self .stacks_client @@ -421,6 +431,61 @@ impl SignerTest { key } + fn wait_for_dkg_and_frost_signatures( + &mut self, + timeout: Duration, + ) -> (Vec, Vec) { + debug!("Waiting for DKG and frost signatures..."); + let mut sigs = Vec::new(); + let mut keys = Vec::new(); + let sign_now = Instant::now(); + for recv in self.result_receivers.iter() { + let mut frost_signature = None; + let mut aggregate_public_key = None; + loop { + let results = recv + .recv_timeout(timeout) + .expect("failed to recv dkg and signature results"); + for result in results { + match result { + OperationResult::Sign(sig) => { + info!("Received Signature ({},{})", &sig.R, &sig.z); + frost_signature = Some(sig); + } + OperationResult::SignTaproot(proof) => { + panic!("Received SchnorrProof ({},{})", &proof.r, &proof.s); + } + OperationResult::DkgError(dkg_error) => { + panic!("Received DkgError {:?}", dkg_error); + } + OperationResult::SignError(sign_error) => { + panic!("Received SignError {}", sign_error); + } + OperationResult::Dkg(point) => { + info!("Received aggregate_group_key {point}"); + aggregate_public_key = Some(point); + } + } + } + if (frost_signature.is_some() && aggregate_public_key.is_some()) + || sign_now.elapsed() > timeout + { + break; + } + } + + let frost_signature = frost_signature + .expect(&format!("Failed to get frost signature within {timeout:?}")); + let key = aggregate_public_key.expect(&format!( + "Failed to get aggregate public key within {timeout:?}" + )); + sigs.push(frost_signature); + keys.push(key); + } + debug!("Finished waiting for DKG and frost signatures!"); + (sigs, keys) + } + fn wait_for_frost_signatures(&mut self, timeout: Duration) -> Vec { debug!("Waiting for frost signatures..."); let mut results = Vec::new(); @@ -804,7 +869,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, + ], }); }