diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index e819e66c3..fb0c3c9be 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -426,6 +426,7 @@ pub(crate) mod tests { let mut start_key_id = 1u32; let mut end_key_id = start_key_id; let mut signer_public_keys = HashMap::new(); + let mut signer_slot_ids = HashMap::new(); let mut coordinator_ids = vec![]; let stacks_address = config.stacks_address; let ecdsa_private_key = config.ecdsa_private_key; @@ -481,6 +482,7 @@ pub(crate) mod tests { &StacksPublicKey::from_slice(public_key.to_bytes().as_slice()) .expect("Failed to create stacks public key"), ); + signer_slot_ids.insert(address, signer_id); // Note in a real world situation, these would not always match signer_address_ids.insert(address, signer_id); addresses.push(address); start_key_id = end_key_id; @@ -493,6 +495,7 @@ pub(crate) mod tests { signer_slot_id: 0, key_ids: signer_key_ids.get(&0).cloned().unwrap_or_default(), registered_signers: RegisteredSignersInfo { + signer_slot_ids, public_keys, coordinator_key_ids, signer_key_ids, diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 63d75b40f..82eb50c41 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -17,7 +17,7 @@ use std::net::SocketAddr; use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; -use blockstack_lib::chainstate::stacks::boot::{RewardSet, SIGNERS_VOTING_NAME}; +use blockstack_lib::chainstate::stacks::boot::{RewardSet, SIGNERS_NAME, SIGNERS_VOTING_NAME}; use blockstack_lib::chainstate::stacks::{ StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth, TransactionContractCall, TransactionPayload, TransactionPostConditionMode, @@ -119,10 +119,7 @@ impl StacksClient { /// Calculate the ordered list of coordinator ids by comparing the provided public keys against the pox consensus hash pub fn calculate_coordinator_ids(&self, public_keys: &PublicKeys) -> Vec { - let pox_consensus_hash = match retry_with_exponential_backoff(|| { - self.get_pox_consenus_hash() - .map_err(backoff::Error::transient) - }) { + let pox_consensus_hash = match self.get_pox_consenus_hash() { Ok(hash) => hash, Err(e) => { debug!("Failed to get stacks tip consensus hash: {e:?}"); @@ -360,8 +357,12 @@ impl StacksClient { } /// Retrieve the vote of the signer for the given round - pub fn get_signer_vote(&self, round: u128) -> Result, ClientError> { - let reward_cycle = ClarityValue::UInt(self.get_current_reward_cycle()? as u128); + pub fn get_signer_vote( + &self, + reward_cycle: u64, + round: u128, + ) -> Result, ClientError> { + let reward_cycle = ClarityValue::UInt(reward_cycle as u128); let round = ClarityValue::UInt(round); let signer = ClarityValue::Principal(self.stacks_address.into()); let contract_addr = boot_code_addr(self.mainnet); @@ -381,10 +382,12 @@ impl StacksClient { if current_reward_cycle >= reward_cycle { // We have already entered into this reward cycle or beyond // therefore the reward set has already been calculated + debug!("Reward set has already been calculated for reward cycle {reward_cycle}."); return Ok(true); } if current_reward_cycle.wrapping_add(1) != reward_cycle { // We are not in the prepare phase of the reward cycle as the upcoming cycle nor are we in the current reward cycle... + debug!("Reward set has not been calculated for reward cycle {reward_cycle}. We are not in the requested reward cycle yet."); return Ok(false); } let burn_block_height = self.get_burn_block_height()?; @@ -393,7 +396,7 @@ impl StacksClient { } /// Get the reward set from the stacks node for the given reward cycle - fn get_reward_set(&self, reward_cycle: u64) -> Result { + pub fn get_reward_set(&self, reward_cycle: u64) -> Result { debug!("Getting reward set for reward cycle {reward_cycle}..."); let send_request = || { self.stacks_node_client @@ -409,16 +412,25 @@ impl StacksClient { Ok(stackers_response.stacker_set) } - /// Get registered signers info for the given reward cycle + /// Get the registered signers for a specific reward cycle + /// Returns None if no signers are registered or its not Nakamoto cycle pub fn get_registered_signers_info( &self, reward_cycle: u64, ) -> Result, ClientError> { - let reward_set = self.get_reward_set(reward_cycle)?; - let Some(reward_set_signers) = reward_set.signers else { + debug!("Getting registered signers for reward cycle {reward_cycle}..."); + let Ok(reward_set) = self.get_reward_set(reward_cycle) else { + warn!("No reward set found for reward cycle {reward_cycle}."); return Ok(None); }; - + let Some(reward_set_signers) = reward_set.signers else { + warn!("No reward set signers found for reward cycle {reward_cycle}."); + return Ok(None); + }; + if reward_set_signers.is_empty() { + warn!("No registered signers found for reward cycle {reward_cycle}."); + return Ok(None); + } // signer uses a Vec for its key_ids, but coordinator uses a HashSet for each signer since it needs to do lots of lookups let mut weight_end = 1; let mut coordinator_key_ids = HashMap::with_capacity(4000); @@ -432,10 +444,10 @@ impl StacksClient { for (i, entry) in reward_set_signers.iter().enumerate() { let signer_id = u32::try_from(i).expect("FATAL: number of signers exceeds u32::MAX"); let ecdsa_public_key = ecdsa::PublicKey::try_from(entry.signing_key.as_slice()).map_err(|e| { - ClientError::CorruptedRewardSet(format!( - "Reward cycle {reward_cycle} failed to convert signing key to ecdsa::PublicKey: {e}" - )) - })?; + ClientError::CorruptedRewardSet(format!( + "Reward cycle {reward_cycle} failed to convert signing key to ecdsa::PublicKey: {e}" + )) + })?; let signer_public_key = Point::try_from(&Compressed::from(ecdsa_public_key.to_bytes())) .map_err(|e| { ClientError::CorruptedRewardSet(format!( @@ -443,10 +455,10 @@ impl StacksClient { )) })?; let stacks_public_key = StacksPublicKey::from_slice(entry.signing_key.as_slice()).map_err(|e| { - ClientError::CorruptedRewardSet(format!( - "Reward cycle {reward_cycle} failed to convert signing key to StacksPublicKey: {e}" - )) - })?; + ClientError::CorruptedRewardSet(format!( + "Reward cycle {reward_cycle} failed to convert signing key to StacksPublicKey: {e}" + )) + })?; let stacks_address = StacksAddress::p2pkh(self.mainnet, &stacks_public_key); @@ -467,12 +479,36 @@ impl StacksClient { .push(key_id); } } + + let signer_set = + u32::try_from(reward_cycle % 2).expect("FATAL: reward_cycle % 2 exceeds u32::MAX"); + let signer_stackerdb_contract_id = boot_code_id(SIGNERS_NAME, self.mainnet); + // Get the signer writers from the stacker-db to find the signer slot id + let signer_slots_weights = self + .get_stackerdb_signer_slots(&signer_stackerdb_contract_id, signer_set) + .unwrap(); + let mut signer_slot_ids = HashMap::with_capacity(signer_slots_weights.len()); + for (index, (address, _)) in signer_slots_weights.into_iter().enumerate() { + signer_slot_ids.insert( + address, + u32::try_from(index).expect("FATAL: number of signers exceeds u32::MAX"), + ); + } + + for address in signer_address_ids.keys().into_iter() { + if !signer_slot_ids.contains_key(address) { + debug!("Signer {address} does not have a slot id in the stackerdb"); + return Ok(None); + } + } + Ok(Some(RegisteredSignersInfo { public_keys, signer_key_ids, signer_address_ids, signer_public_keys, coordinator_key_ids, + signer_slot_ids, })) } @@ -502,6 +538,7 @@ impl StacksClient { /// Get the current reward cycle from the stacks node pub fn get_current_reward_cycle(&self) -> Result { let pox_data = self.get_pox_data()?; + println!("GOT REWARD CYCLE: {}", pox_data.reward_cycle_id); Ok(pox_data.reward_cycle_id) } diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index a24f07b4f..d06f3a5f8 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -124,6 +124,9 @@ pub struct RegisteredSignersInfo { pub signer_address_ids: HashMap, /// The public keys for the reward cycle pub public_keys: PublicKeys, + /// The signer slot id for a signer address registered in stackerdb + /// This corresponds to their unique index when voting in a reward cycle + pub signer_slot_ids: HashMap, } /// The Configuration info needed for an individual signer per reward cycle diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index f21025f87..1aa3bf28f 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -16,8 +16,6 @@ use std::sync::mpsc::Sender; use std::time::Duration; -use blockstack_lib::chainstate::stacks::boot::SIGNERS_NAME; -use blockstack_lib::util_lib::boot::boot_code_id; use hashbrown::HashMap; use libsigner::{SignerEvent, SignerRunLoop}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; @@ -85,41 +83,32 @@ impl RunLoop { // Accounts for Pre nakamoto by simply using the second block of a prepare phase as the criteria return Err(ClientError::RewardSetNotYetCalculated(reward_cycle)); } - let current_addr = self.stacks_client.get_signer_address(); - - let signer_set = - u32::try_from(reward_cycle % 2).expect("FATAL: reward_cycle % 2 exceeds u32::MAX"); - let signer_stackerdb_contract_id = - boot_code_id(SIGNERS_NAME, self.config.network.is_mainnet()); - // Get the signer writers from the stacker-db to find the signer slot id - let Some(signer_slot_id) = self - .stacks_client - .get_stackerdb_signer_slots(&signer_stackerdb_contract_id, signer_set)? - .iter() - .position(|(address, _)| address == current_addr) - .map(|pos| u32::try_from(pos).expect("FATAL: number of signers exceeds u32::MAX")) - else { - warn!( - "Signer {current_addr} was not found in stacker db. Must not be registered for this reward cycle {reward_cycle}." - ); - return Ok(None); - }; - // We can only register for a reward cycle if a reward set exists. We know that it should exist due to our earlier check for reward_set_calculated let Some(registered_signers) = self .stacks_client .get_registered_signers_info(reward_cycle)? else { warn!( - "No reward set found for reward cycle {reward_cycle}. Must not be a valid Nakamoto reward cycle." + "Failed to retrieve registered signers info for reward cycle {reward_cycle}. Must not be a valid Nakamoto reward cycle." ); return Ok(None); }; - let Some(signer_id) = registered_signers.signer_address_ids.get(current_addr) else { - warn!("Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}."); + + let current_addr = self.stacks_client.get_signer_address(); + + let Some(signer_slot_id) = registered_signers.signer_slot_ids.get(current_addr) else { + warn!( + "Signer {current_addr} was not found in stacker db. Must not be registered for this reward cycle {reward_cycle}." + ); return Ok(None); }; - debug!( + let Some(signer_id) = registered_signers.signer_address_ids.get(current_addr) else { + warn!( + "Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}." + ); + return Ok(None); + }; + info!( "Signer #{signer_id} ({current_addr}) is registered for reward cycle {reward_cycle}." ); let key_ids = registered_signers @@ -133,7 +122,7 @@ impl RunLoop { Ok(Some(SignerConfig { reward_cycle, signer_id: *signer_id, - signer_slot_id, + signer_slot_id: *signer_slot_id, key_ids, registered_signers, coordinator_ids, @@ -155,7 +144,7 @@ impl RunLoop { let reward_index = reward_cycle % 2; let mut needs_refresh = false; if let Some(stacks_signer) = self.stacks_signers.get_mut(&reward_index) { - let old_reward_cycle = stacks_signer.reward_cycle; + let old_reward_cycle = stacks_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}. No need to update it's state machines.") @@ -176,6 +165,8 @@ impl RunLoop { } else { // Nothing to initialize. Signer is not registered for this reward cycle debug!("Signer is not registered for reward cycle {reward_cycle}. Nothing to initialize."); + self.stacks_signers + .insert(reward_index, Signer::from(reward_cycle)); } } Ok(()) @@ -183,13 +174,9 @@ 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_with_retry(&mut self) -> Result<(), ClientError> { + fn refresh_signers_with_retry(&mut self, current_reward_cycle: u64) -> Result<(), ClientError> { + let next_reward_cycle = current_reward_cycle.saturating_add(1); retry_with_exponential_backoff(|| { - let current_reward_cycle = self - .stacks_client - .get_current_reward_cycle() - .map_err(backoff::Error::transient)?; - let next_reward_cycle = current_reward_cycle.saturating_add(1); if let Err(e) = self.refresh_signer_config(current_reward_cycle) { match e { ClientError::NotRegistered => { @@ -214,20 +201,22 @@ impl RunLoop { } } for stacks_signer in self.stacks_signers.values_mut() { - let updated_coordinator = stacks_signer - .coordinator_selector - .refresh_coordinator(&self.stacks_client); - if updated_coordinator { - debug!( - "Signer #{}: Coordinator has been updated. Resetting state to Idle.", - stacks_signer.signer_id - ); - stacks_signer.coordinator.state = CoordinatorState::Idle; - stacks_signer.state = SignerState::Idle; + if let Signer::Registered(signer) = stacks_signer { + let updated_coordinator = signer + .coordinator_selector + .refresh_coordinator(&self.stacks_client); + if updated_coordinator { + debug!( + "Signer #{}: Coordinator has been updated. Resetting state to Idle.", + signer.signer_id + ); + signer.coordinator.state = CoordinatorState::Idle; + signer.state = SignerState::Idle; + } + signer + .update_dkg(&self.stacks_client, current_reward_cycle) + .map_err(backoff::Error::transient)?; } - stacks_signer - .update_dkg(&self.stacks_client) - .map_err(backoff::Error::transient)?; } if self.stacks_signers.is_empty() { info!("Signer is not registered for the current {current_reward_cycle} or next {next_reward_cycle} reward cycles. Waiting for confirmed registration..."); @@ -239,23 +228,6 @@ impl RunLoop { Ok(()) }) } - - /// Cleanup stale signers that have exceeded their tenure - fn cleanup_stale_signers(&mut self) { - let mut to_delete = Vec::with_capacity(self.stacks_signers.len()); - for (index, stacks_signer) in self.stacks_signers.iter() { - if stacks_signer.state == SignerState::TenureExceeded { - debug!( - "Deleting signer for stale reward cycle: {}.", - stacks_signer.reward_cycle - ); - to_delete.push(*index); - } - } - for index in to_delete { - self.stacks_signers.remove(&index); - } - } } impl SignerRunLoop, RunLoopCommand> for RunLoop { @@ -277,7 +249,15 @@ impl SignerRunLoop, RunLoopCommand> for RunLoop { "Running one pass for the signer. Current state: {:?}", self.state ); - if let Err(e) = self.refresh_signers_with_retry() { + 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. Ignoring event: {event:?}"); + return None; + }; + if let Err(e) = self.refresh_signers_with_retry(current_reward_cycle) { if self.state == State::Uninitialized { // If we were never actually initialized, we cannot process anything. Just return. error!("Failed to initialize signers. Are you sure this signer is correctly registered for the current or next reward cycle?"); @@ -290,21 +270,31 @@ impl SignerRunLoop, RunLoopCommand> for RunLoop { if let Some(command) = cmd { let reward_cycle = command.reward_cycle; if let Some(stacks_signer) = self.stacks_signers.get_mut(&(reward_cycle % 2)) { - if stacks_signer.reward_cycle != reward_cycle { - warn!( - "Signer #{}: not registered for reward cycle {reward_cycle}. Ignoring command: {command:?}", stacks_signer.signer_id + match stacks_signer { + Signer::Registered(signer) => { + if signer.reward_cycle != reward_cycle { + warn!( + "Signer #{}: not registered for reward cycle {reward_cycle}. Ignoring command: {command:?}", signer.signer_id ); - } else { - info!( + } else { + info!( "Signer #{}: Queuing an external runloop command ({:?}): {command:?}", - stacks_signer.signer_id, - stacks_signer + signer.signer_id, + signer .signing_round .public_keys .signers - .get(&stacks_signer.signer_id) + .get(&signer.signer_id) ); - stacks_signer.commands.push_back(command.command); + signer.commands.push_back(command.command); + } + } + Signer::Unregistered(_) => { + warn!( + "Signer: not registered for reward cycle {reward_cycle}. Ignoring command: {command:?}" + ); + return None; + } } } else { warn!( @@ -313,19 +303,29 @@ impl SignerRunLoop, RunLoopCommand> for RunLoop { } } for stacks_signer in self.stacks_signers.values_mut() { - if let Err(e) = - stacks_signer.process_event(&self.stacks_client, event.as_ref(), res.clone()) - { - error!( - "Signer #{} for reward cycle {} errored processing event: {e}", - stacks_signer.signer_id, stacks_signer.reward_cycle - ); + match stacks_signer { + Signer::Registered(signer) => { + if let Err(e) = signer.process_event( + &self.stacks_client, + event.as_ref(), + res.clone(), + current_reward_cycle, + ) { + error!( + "Signer #{} for reward cycle {} errored processing event: {e}", + signer.signer_id, signer.reward_cycle + ); + } + // After processing event, run the next command for each signer + signer.process_next_command(&self.stacks_client); + } + Signer::Unregistered(_) => { + warn!( + "Signer is not registered for any reward cycle. Ignoring event: {event:?}" + ); + } } - // After processing event, run the next command for each signer - stacks_signer.process_next_command(&self.stacks_client); } - // Cleanup any stale signers - self.cleanup_stale_signers(); None } } diff --git a/stacks-signer/src/signer.rs b/stacks-signer/src/signer.rs index 7c91ed315..9ae809c0e 100644 --- a/stacks-signer/src/signer.rs +++ b/stacks-signer/src/signer.rs @@ -110,12 +110,65 @@ pub enum State { Idle, /// The signer is executing a DKG or Sign round OperationInProgress, - /// The Signer has exceeded its tenure - TenureExceeded, +} +/// The stacks signer for a reward cycle +pub enum Signer { + /// A registered signer + Registered(RegisteredSigner), + /// An unregistered signer + Unregistered(UnregisteredSigner), } -/// The stacks signer for the rewrad cycle -pub struct Signer { +impl Signer { + /// Get the reward cycle of the internal signer + pub fn reward_cycle(&self) -> u64 { + match self { + Self::Registered(signer) => signer.reward_cycle, + Self::Unregistered(signer) => signer.reward_cycle, + } + } + + /// Get the state of the internal signer + pub fn state(&self) -> State { + match self { + Self::Registered(signer) => signer.state.clone(), + Self::Unregistered(signer) => signer.state.clone(), + } + } +} + +/// The stacks signer unregistered for the reward cycle +pub struct UnregisteredSigner { + /// The reward cycle this signer belongs to + pub reward_cycle: u64, + /// the state of the signer (Can only be Idle) + pub state: State, +} + +impl UnregisteredSigner { + /// Create a new signer which is not registered for the reward cycle + pub fn new(reward_cycle: u64) -> Self { + Self { + reward_cycle, + state: State::Idle, + } + } +} + +impl From for Signer { + fn from(signer_config: SignerConfig) -> Self { + Self::Registered(RegisteredSigner::from(signer_config)) + } +} + +impl From for Signer { + fn from(reward_cycle: u64) -> Self { + Self::Unregistered(UnregisteredSigner::new(reward_cycle)) + } +} + +/// The stacks signer registered for the reward cycle +pub struct RegisteredSigner { /// The coordinator for inbound messages for a specific reward cycle pub coordinator: FireCoordinator, /// The signing round used to sign messages for a specific reward cycle @@ -143,7 +196,7 @@ pub struct Signer { pub coordinator_selector: Selector, } -impl From for Signer { +impl From for RegisteredSigner { fn from(signer_config: SignerConfig) -> Self { let stackerdb = StackerDB::from(&signer_config); @@ -200,7 +253,7 @@ impl From for Signer { } } -impl Signer { +impl RegisteredSigner { /// Finish an operation and update the coordinator selector accordingly fn finish_operation(&mut self) { self.state = State::Idle; @@ -315,13 +368,6 @@ impl Signer { self.signer_id, ); } - State::TenureExceeded => { - // We have exceeded our tenure. Do nothing... - debug!( - "Signer #{}: Waiting to clean up signer for reward cycle {}", - self.signer_id, self.reward_cycle - ); - } } } @@ -331,6 +377,7 @@ impl Signer { stacks_client: &StacksClient, block_validate_response: &BlockValidateResponse, res: Sender>, + current_reward_cycle: u64, ) { let block_info = match block_validate_response { BlockValidateResponse::Ok(block_validate_ok) => { @@ -341,7 +388,11 @@ impl Signer { debug!("Signer #{}: Received a block validate response for a block we have not seen before. Ignoring...", self.signer_id); return; }; - let is_valid = self.verify_block_transactions(stacks_client, &block_info.block); + let is_valid = self.verify_block_transactions( + stacks_client, + &block_info.block, + current_reward_cycle, + ); block_info.valid = Some(is_valid); info!( "Signer #{}: Treating block validation for block {} as valid: {:?}", @@ -386,7 +437,7 @@ impl Signer { msg: Message::NonceRequest(nonce_request), sig: vec![], }; - self.handle_packets(stacks_client, res, &[packet]); + self.handle_packets(stacks_client, res, &[packet], current_reward_cycle); } else { let coordinator_id = self.coordinator_selector.get_coordinator().0; if block_info.valid.unwrap_or(false) @@ -422,6 +473,7 @@ impl Signer { stacks_client: &StacksClient, res: Sender>, messages: &[SignerMessage], + current_reward_cycle: u64, ) { let coordinator_pubkey = self.coordinator_selector.get_coordinator().1; let packets: Vec = messages @@ -435,7 +487,7 @@ impl Signer { } }) .collect(); - self.handle_packets(stacks_client, res, &packets); + self.handle_packets(stacks_client, res, &packets, current_reward_cycle); } /// Handle proposed blocks submitted by the miners to stackerdb @@ -465,6 +517,7 @@ impl Signer { stacks_client: &StacksClient, res: Sender>, packets: &[Packet], + current_reward_cycle: u64, ) { let signer_outbound_messages = self .signing_round @@ -492,7 +545,7 @@ impl Signer { if !operation_results.is_empty() { // We have finished a signing or DKG round, either successfully or due to error. // Regardless of the why, update our state to Idle as we should not expect the operation to continue. - self.process_operation_results(stacks_client, &operation_results); + self.process_operation_results(stacks_client, &operation_results, current_reward_cycle); self.send_operation_results(res, operation_results); self.finish_operation(); } else if !packets.is_empty() && self.coordinator.state != CoordinatorState::Idle { @@ -609,6 +662,7 @@ impl Signer { &mut self, stacks_client: &StacksClient, block: &NakamotoBlock, + current_reward_cycle: u64, ) -> bool { let aggregate_key = retry_with_exponential_backoff(|| { stacks_client @@ -629,7 +683,7 @@ impl Signer { .cloned() .collect::>(); if let Ok(expected_transactions) = - self.get_filtered_transactions(stacks_client, &signer_ids) + self.get_filtered_transactions(stacks_client, &signer_ids, current_reward_cycle) { //It might be worth building a hashset of the blocks' txids and checking that against the expected transaction's txid. let block_tx_hashset = block.txs.iter().map(|tx| tx.txid()).collect::>(); @@ -703,6 +757,7 @@ impl Signer { &self, stacks_client: &StacksClient, transaction: StacksTransaction, + current_reward_cycle: u64, ) -> Option { // Filter out transactions that have already been confirmed (can happen if a signer did not update stacker db since the last block was processed) let origin_address = transaction.origin_address(); @@ -741,8 +796,13 @@ impl Signer { return None; } let Ok(valid) = retry_with_exponential_backoff(|| { - self.verify_payload(stacks_client, &transaction, *origin_signer_id) - .map_err(backoff::Error::transient) + self.verify_payload( + stacks_client, + &transaction, + *origin_signer_id, + current_reward_cycle, + ) + .map_err(backoff::Error::transient) }) else { warn!( "Signer #{}: Unable to validate transaction payload. Filtering ({}).", @@ -773,6 +833,7 @@ impl Signer { stacks_client: &StacksClient, transaction: &StacksTransaction, origin_signer_id: u32, + current_reward_cycle: u64, ) -> Result { let TransactionPayload::ContractCall(payload) = &transaction.payload else { // Not a contract call so not a special cased vote for aggregate public key transaction @@ -795,7 +856,7 @@ impl Signer { // The signer is attempting to vote for another signer id than their own return Ok(false); } - let next_reward_cycle = stacks_client.get_current_reward_cycle()?.wrapping_add(1); + let next_reward_cycle = current_reward_cycle.wrapping_add(1); if reward_cycle != next_reward_cycle { // The signer is attempting to vote for a reward cycle that is not the next reward cycle return Ok(false); @@ -832,12 +893,15 @@ impl Signer { &mut self, stacks_client: &StacksClient, signer_ids: &[u32], + current_reward_cycle: u64, ) -> Result, ClientError> { let transactions = self .stackerdb .get_signer_transactions_with_retry(signer_ids)? .into_iter() - .filter_map(|transaction| self.verify_signer_transaction(stacks_client, transaction)) + .filter_map(|transaction| { + self.verify_signer_transaction(stacks_client, transaction, current_reward_cycle) + }) .collect(); Ok(transactions) } @@ -910,6 +974,7 @@ impl Signer { &mut self, stacks_client: &StacksClient, operation_results: &[OperationResult], + current_reward_cycle: u64, ) { for operation_result in operation_results { // Signers only every trigger non-taproot signing rounds over blocks. Ignore SignTaproot results @@ -922,7 +987,7 @@ impl Signer { debug!("Signer #{}: Received a signature result for a taproot signature. Nothing to broadcast as we currently sign blocks with a FROST signature.", self.signer_id); } OperationResult::Dkg(point) => { - self.process_dkg(stacks_client, point); + self.process_dkg(stacks_client, point, current_reward_cycle); } OperationResult::SignError(e) => { self.process_sign_error(e); @@ -935,7 +1000,12 @@ impl Signer { } /// Process a dkg result by broadcasting a vote to the stacks node - fn process_dkg(&mut self, stacks_client: &StacksClient, point: &Point) { + fn process_dkg( + &mut self, + stacks_client: &StacksClient, + point: &Point, + current_reward_cycle: u64, + ) { let epoch = stacks_client .get_node_epoch_with_retry() .unwrap_or(StacksEpochId::Epoch24); @@ -949,7 +1019,7 @@ impl Signer { None }; // Get our current nonce from the stacks node and compare it against what we have sitting in the stackerdb instance - let nonce = self.get_next_nonce(stacks_client); + let nonce = self.get_next_nonce(stacks_client, current_reward_cycle); match stacks_client.build_vote_for_aggregate_public_key( self.stackerdb.get_signer_slot_id(), self.coordinator.current_dkg_id, @@ -959,7 +1029,9 @@ impl Signer { nonce, ) { Ok(transaction) => { - if let Err(e) = self.broadcast_dkg_vote(stacks_client, transaction, epoch) { + if let Err(e) = + self.broadcast_dkg_vote(stacks_client, transaction, epoch, current_reward_cycle) + { warn!( "Signer #{}: Failed to broadcast DKG vote ({point:?}): {e:?}", self.signer_id @@ -976,7 +1048,7 @@ impl Signer { } /// Get the next available nonce, taking into consideration the nonce we have sitting in stackerdb as well as the account nonce - fn get_next_nonce(&mut self, stacks_client: &StacksClient) -> u64 { + fn get_next_nonce(&mut self, stacks_client: &StacksClient, current_reward_cycle: u64) -> u64 { let signer_address = stacks_client.get_signer_address(); let mut next_nonce = stacks_client .get_account_nonce(signer_address) @@ -988,7 +1060,7 @@ impl Signer { }) .unwrap_or(0); - let current_transactions = self.get_filtered_transactions(stacks_client, &[self.signer_id]).map_err(|e| { + let current_transactions = self.get_filtered_transactions(stacks_client, &[self.signer_id], current_reward_cycle).map_err(|e| { warn!("Signer #{}: Failed to get old transactions: {e:?}. Defaulting to account nonce.", self.signer_id); }).unwrap_or_default(); @@ -1008,6 +1080,7 @@ impl Signer { stacks_client: &StacksClient, new_transaction: StacksTransaction, epoch: StacksEpochId, + current_reward_cycle: u64, ) -> Result<(), ClientError> { let txid = new_transaction.txid(); let aggregate_key = retry_with_exponential_backoff(|| { @@ -1048,7 +1121,7 @@ impl Signer { ); vec![] } else { - let mut new_transactions = self.get_filtered_transactions(stacks_client, &[self.signer_id]).map_err(|e| { + let mut new_transactions = self.get_filtered_transactions(stacks_client, &[self.signer_id], current_reward_cycle).map_err(|e| { warn!("Signer #{}: Failed to get old transactions: {e:?}. Potentially overwriting our existing stackerDB transactions", self.signer_id); }).unwrap_or_default(); new_transactions.push(new_transaction); @@ -1232,7 +1305,11 @@ 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> { + pub fn update_dkg( + &mut self, + stacks_client: &StacksClient, + current_reward_cycle: u64, + ) -> Result<(), ClientError> { let reward_cycle = self.reward_cycle; let new_aggregate_public_key = stacks_client.get_approved_aggregate_key(reward_cycle)?; let old_aggregate_public_key = self.coordinator.get_aggregate_public_key(); @@ -1260,7 +1337,7 @@ impl Signer { ); // Have I already voted and have a pending transaction? Check stackerdb for the same round number and reward cycle vote transaction // TODO: might be better to store these transactions on the side to prevent having to query the stacker db for every signer (only do on initilaization of a new signer for example and then listen for stacker db updates after that) - let old_transactions = self.get_filtered_transactions(stacks_client, &[self.signer_id]).map_err(|e| { + let old_transactions = self.get_filtered_transactions(stacks_client, &[self.signer_id], current_reward_cycle).map_err(|e| { warn!("Signer #{}: Failed to get old transactions: {e:?}. Potentially overwriting our existing transactions", self.signer_id); }).unwrap_or_default(); // Check if we have an existing vote transaction for the same round and reward cycle @@ -1318,21 +1395,8 @@ impl Signer { stacks_client: &StacksClient, event: Option<&SignerEvent>, res: Sender>, + current_reward_cycle: u64, ) -> Result<(), ClientError> { - let current_reward_cycle = retry_with_exponential_backoff(|| { - stacks_client - .get_current_reward_cycle() - .map_err(backoff::Error::transient) - })?; - if current_reward_cycle > self.reward_cycle { - // We have advanced past our tenure as a signer. Nothing to do. - info!( - "Signer #{}: Signer has passed its tenure. Ignoring event...", - self.signer_id - ); - self.state = State::TenureExceeded; - return Ok(()); - } debug!("Signer #{}: Processing event: {event:?}", self.signer_id); match event { Some(SignerEvent::BlockValidationResponse(block_validate_response)) => { @@ -1340,7 +1404,12 @@ impl Signer { "Signer #{}: Received a block proposal result from the stacks node...", self.signer_id ); - self.handle_block_validate_response(stacks_client, block_validate_response, res) + self.handle_block_validate_response( + stacks_client, + block_validate_response, + res, + current_reward_cycle, + ) } Some(SignerEvent::SignerMessages(signer_set, messages)) => { if *signer_set != self.stackerdb.get_signer_set() { @@ -1352,7 +1421,7 @@ impl Signer { self.signer_id, messages.len() ); - self.handle_signer_messages(stacks_client, res, messages); + self.handle_signer_messages(stacks_client, res, messages, current_reward_cycle); } Some(SignerEvent::ProposedBlocks(blocks)) => { if current_reward_cycle != self.reward_cycle { @@ -1431,7 +1500,7 @@ mod tests { }; use crate::client::{StacksClient, VOTE_FUNCTION_NAME}; use crate::config::GlobalConfig; - use crate::signer::{BlockInfo, Signer}; + use crate::signer::{BlockInfo, RegisteredSigner}; #[test] #[serial] @@ -1440,7 +1509,7 @@ mod tests { // Create a runloop of a valid signer let config = GlobalConfig::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); let (signer_config, _ordered_addresses) = generate_signer_config(&config, 5, 20); - let mut signer = Signer::from(signer_config); + let mut signer = RegisteredSigner::from(signer_config); let signer_private_key = config.stacks_private_key; let non_signer_private_key = StacksPrivateKey::new(); @@ -1560,7 +1629,7 @@ mod tests { let stacks_client = StacksClient::from(&config); let h = spawn(move || { signer - .get_filtered_transactions(&stacks_client, &[0]) + .get_filtered_transactions(&stacks_client, &[0], 0) .unwrap() }); @@ -1589,7 +1658,7 @@ mod tests { let config = GlobalConfig::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); let (signer_config, _ordered_addresses) = generate_signer_config(&config, 5, 20); let stacks_client = StacksClient::from(&config); - let mut signer = Signer::from(signer_config); + let mut signer = RegisteredSigner::from(signer_config); let signer_private_key = config.stacks_private_key; let vote_contract_id = boot_code_id(SIGNERS_VOTING_NAME, signer.mainnet); @@ -1651,7 +1720,7 @@ mod tests { BlockInfo::new(block.clone()), ); - let h = spawn(move || signer.verify_block_transactions(&stacks_client, &block)); + let h = spawn(move || signer.verify_block_transactions(&stacks_client, &block, 0)); // Simulate the response to the request for transactions with the expected transaction let signer_message = SignerMessage::Transactions(vec![valid_tx]); @@ -1706,7 +1775,7 @@ mod tests { signer_config.reward_cycle = 1; // valid transaction - let signer = Signer::from(signer_config.clone()); + let signer = RegisteredSigner::from(signer_config.clone()); let stacks_client = StacksClient::from(&config); let signer_private_key = config.stacks_private_key; @@ -1734,13 +1803,6 @@ mod tests { ) .unwrap(); - let reward_cycle_response = build_get_pox_data_response( - Some(signer.reward_cycle.saturating_sub(1)), - None, - None, - None, - ) - .0; let pox_info_response = build_get_pox_data_response( Some(signer.reward_cycle.saturating_sub(1)), Some(0), @@ -1754,12 +1816,15 @@ mod tests { let h = spawn(move || { assert!(signer - .verify_payload(&stacks_client, &valid_transaction, signer.signer_id) + .verify_payload( + &stacks_client, + &valid_transaction, + signer.signer_id, + signer.reward_cycle.saturating_sub(1) + ) .unwrap()) }); let mock_server = mock_server_from_config(&config); - write_response(mock_server, reward_cycle_response.as_bytes()); - let mock_server = mock_server_from_config(&config); write_response(mock_server, pox_info_response.as_bytes()); let mock_server = mock_server_from_config(&config); write_response(mock_server, peer_info.as_bytes()); @@ -1769,7 +1834,7 @@ mod tests { write_response(mock_server, last_round_response.as_bytes()); h.join().unwrap(); - let signer = Signer::from(signer_config.clone()); + let signer = RegisteredSigner::from(signer_config.clone()); // Create a invalid transaction that is not a contract call let invalid_not_contract_call = StacksTransaction { version: TransactionVersion::Testnet, @@ -1926,13 +1991,18 @@ mod tests { invalid_function_arg_4, ] { let result = signer - .verify_payload(&stacks_client, &tx, signer.signer_id) + .verify_payload( + &stacks_client, + &tx, + signer.signer_id, + signer.reward_cycle.saturating_sub(1), + ) .unwrap(); assert!(!result); } // Invalid reward cycle (voting for the current is not allowed. only the next) - let signer = Signer::from(signer_config.clone()); + let signer = RegisteredSigner::from(signer_config.clone()); let invalid_reward_cycle = StacksClient::build_signed_contract_call_transaction( &contract_addr, contract_name.clone(), @@ -1945,19 +2015,20 @@ mod tests { 10, ) .unwrap(); - let (reward_cycle_response, _) = - build_get_pox_data_response(Some(signer.reward_cycle), None, None, None); let h = spawn(move || { assert!(!signer - .verify_payload(&stacks_client, &invalid_reward_cycle, signer.signer_id) + .verify_payload( + &stacks_client, + &invalid_reward_cycle, + signer.signer_id, + signer.reward_cycle + ) .unwrap()) }); - let mock_server = mock_server_from_config(&config); - write_response(mock_server, reward_cycle_response.as_bytes()); h.join().unwrap(); // Invalid block height to vote - let signer = Signer::from(signer_config.clone()); + let signer = RegisteredSigner::from(signer_config.clone()); let stacks_client = StacksClient::from(&config); let invalid_reward_set = StacksClient::build_signed_contract_call_transaction( &contract_addr, @@ -1972,14 +2043,6 @@ mod tests { ) .unwrap(); - // Valid reward cycle vote - let reward_cycle_response = build_get_pox_data_response( - Some(signer.reward_cycle.saturating_sub(1)), - None, - None, - None, - ) - .0; // Invalid reward set not calculated (not in the second block onwards of the prepare phase) let pox_info_response = build_get_pox_data_response( Some(signer.reward_cycle.saturating_sub(1)), @@ -1992,19 +2055,22 @@ mod tests { let h = spawn(move || { assert!(!signer - .verify_payload(&stacks_client, &invalid_reward_set, signer.signer_id) + .verify_payload( + &stacks_client, + &invalid_reward_set, + signer.signer_id, + signer.reward_cycle.saturating_sub(1) + ) .unwrap()) }); let mock_server = mock_server_from_config(&config); - write_response(mock_server, reward_cycle_response.as_bytes()); - let mock_server = mock_server_from_config(&config); write_response(mock_server, pox_info_response.as_bytes()); let mock_server = mock_server_from_config(&config); write_response(mock_server, peer_info.as_bytes()); h.join().unwrap(); // Already voted - let signer = Signer::from(signer_config.clone()); + let signer = RegisteredSigner::from(signer_config.clone()); let stacks_client = StacksClient::from(&config); let invalid_already_voted = StacksClient::build_signed_contract_call_transaction( &contract_addr, @@ -2019,14 +2085,6 @@ mod tests { ) .unwrap(); - // Valid reward cycle vote - let reward_cycle_response = build_get_pox_data_response( - Some(signer.reward_cycle.saturating_sub(1)), - None, - None, - None, - ) - .0; let pox_info_response = build_get_pox_data_response( Some(signer.reward_cycle.saturating_sub(1)), Some(0), @@ -2039,12 +2097,15 @@ mod tests { let h = spawn(move || { assert!(!signer - .verify_payload(&stacks_client, &invalid_already_voted, signer.signer_id) + .verify_payload( + &stacks_client, + &invalid_already_voted, + signer.signer_id, + signer.reward_cycle.saturating_sub(1) + ) .unwrap()) }); let mock_server = mock_server_from_config(&config); - write_response(mock_server, reward_cycle_response.as_bytes()); - let mock_server = mock_server_from_config(&config); write_response(mock_server, pox_info_response.as_bytes()); let mock_server = mock_server_from_config(&config); write_response(mock_server, peer_info.as_bytes()); @@ -2053,7 +2114,7 @@ mod tests { h.join().unwrap(); // Already voted - let signer = Signer::from(signer_config.clone()); + let signer = RegisteredSigner::from(signer_config.clone()); let stacks_client = StacksClient::from(&config); let round: u128 = 0; let invalid_already_voted = StacksClient::build_signed_contract_call_transaction( @@ -2075,13 +2136,6 @@ mod tests { .unwrap(); // invalid round number - let reward_cycle_response = build_get_pox_data_response( - Some(signer.reward_cycle.saturating_sub(1)), - None, - None, - None, - ) - .0; let pox_info_response = build_get_pox_data_response( Some(signer.reward_cycle.saturating_sub(1)), Some(0), @@ -2095,12 +2149,15 @@ mod tests { let h = spawn(move || { assert!(!signer - .verify_payload(&stacks_client, &invalid_already_voted, signer.signer_id) + .verify_payload( + &stacks_client, + &invalid_already_voted, + signer.signer_id, + signer.reward_cycle.saturating_sub(1) + ) .unwrap()) }); let mock_server = mock_server_from_config(&config); - write_response(mock_server, reward_cycle_response.as_bytes()); - let mock_server = mock_server_from_config(&config); write_response(mock_server, pox_info_response.as_bytes()); let mock_server = mock_server_from_config(&config); write_response(mock_server, peer_info.as_bytes());