If no reward set is found for a reward cycle, do not check again

Signed-off-by: Jacinta Ferrant <jacinta@trustmachines.co>
This commit is contained in:
Jacinta Ferrant
2024-02-16 18:50:43 -05:00
parent 3fa40b738a
commit efd6352b58
5 changed files with 311 additions and 211 deletions

View File

@@ -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,

View File

@@ -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<u32> {
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<Option<Point>, 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<Option<Point>, 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<RewardSet, ClientError> {
pub fn get_reward_set(&self, reward_cycle: u64) -> Result<RewardSet, ClientError> {
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<Option<RegisteredSignersInfo>, 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<u32> 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<u64, ClientError> {
let pox_data = self.get_pox_data()?;
println!("GOT REWARD CYCLE: {}", pox_data.reward_cycle_id);
Ok(pox_data.reward_cycle_id)
}

View File

@@ -124,6 +124,9 @@ pub struct RegisteredSignersInfo {
pub signer_address_ids: HashMap<StacksAddress, u32>,
/// 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<StacksAddress, u32>,
}
/// The Configuration info needed for an individual signer per reward cycle

View File

@@ -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<Vec<OperationResult>, RunLoopCommand> for RunLoop {
@@ -277,7 +249,15 @@ impl SignerRunLoop<Vec<OperationResult>, 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<Vec<OperationResult>, 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<Vec<OperationResult>, 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
}
}

View File

@@ -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<SignerConfig> for Signer {
fn from(signer_config: SignerConfig) -> Self {
Self::Registered(RegisteredSigner::from(signer_config))
}
}
impl From<u64> 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<v2::Aggregator>,
/// 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<SignerConfig> for Signer {
impl From<SignerConfig> for RegisteredSigner {
fn from(signer_config: SignerConfig) -> Self {
let stackerdb = StackerDB::from(&signer_config);
@@ -200,7 +253,7 @@ impl From<SignerConfig> 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<Vec<OperationResult>>,
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<Vec<OperationResult>>,
messages: &[SignerMessage],
current_reward_cycle: u64,
) {
let coordinator_pubkey = self.coordinator_selector.get_coordinator().1;
let packets: Vec<Packet> = 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<Vec<OperationResult>>,
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::<Vec<_>>();
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::<HashSet<_>>();
@@ -703,6 +757,7 @@ impl Signer {
&self,
stacks_client: &StacksClient,
transaction: StacksTransaction,
current_reward_cycle: u64,
) -> Option<StacksTransaction> {
// 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<bool, ClientError> {
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<Vec<StacksTransaction>, 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<Vec<OperationResult>>,
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());