Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/signers get round info and use it to determine if a vote has failed #4569

Merged
merged 3 commits into from Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions stacks-signer/src/client/mod.rs
Expand Up @@ -518,4 +518,28 @@ pub(crate) mod tests {
db_path: config.db_path.clone(),
}
}

pub fn build_get_round_info_response(info: Option<(u64, u64)>) -> String {
let clarity_value = if let Some((vote_count, vote_weight)) = info {
ClarityValue::some(ClarityValue::Tuple(
TupleData::from_data(vec![
("votes-count".into(), ClarityValue::UInt(vote_count as u128)),
(
"votes-weight".into(),
ClarityValue::UInt(vote_weight as u128),
),
])
.expect("BUG: Failed to create clarity value from tuple data"),
))
.expect("BUG: Failed to create clarity value from tuple data")
} else {
ClarityValue::none()
};
build_read_only_response(&clarity_value)
}

pub fn build_get_weight_threshold_response(threshold: u64) -> String {
let clarity_value = ClarityValue::UInt(threshold as u128);
build_read_only_response(&clarity_value)
}
}
71 changes: 70 additions & 1 deletion stacks-signer/src/client/stacks_client.rs
Expand Up @@ -274,6 +274,47 @@ impl StacksClient {
}
}

/// Retrieve the current consumed weight for the given reward cycle and DKG round
pub fn get_round_vote_weight(
&self,
reward_cycle: u64,
round_id: u64,
) -> Result<Option<u128>, ClientError> {
let function_name = ClarityName::from("get-round-info");
let pox_contract_id = boot_code_id(SIGNERS_VOTING_NAME, self.mainnet);
jferrant marked this conversation as resolved.
Show resolved Hide resolved
let function_args = &[
ClarityValue::UInt(reward_cycle as u128),
ClarityValue::UInt(round_id as u128),
];
let value = self.read_only_contract_call(
&pox_contract_id.issuer.into(),
&pox_contract_id.name,
&function_name,
function_args,
)?;
let inner_data = value.expect_optional()?;
let Some(inner_data) = inner_data else {
return Ok(None);
};
let round_info = inner_data.expect_tuple()?;
let votes_weight = round_info.get("votes-weight")?.to_owned().expect_u128()?;
Ok(Some(votes_weight))
}

/// Retrieve the weight threshold required to approve a DKG vote
pub fn get_vote_threshold_weight(&self, reward_cycle: u64) -> Result<u128, ClientError> {
let function_name = ClarityName::from("get-threshold-weight");
let pox_contract_id = boot_code_id(SIGNERS_VOTING_NAME, self.mainnet);
let function_args = &[ClarityValue::UInt(reward_cycle as u128)];
let value = self.read_only_contract_call(
&pox_contract_id.issuer.into(),
&pox_contract_id.name,
&function_name,
function_args,
)?;
Ok(value.expect_u128()?)
}

/// Retrieve the current account nonce for the provided address
pub fn get_account_nonce(&self, address: &StacksAddress) -> Result<u64, ClientError> {
let account_entry = self.get_account_entry_with_retry(address)?;
Expand Down Expand Up @@ -644,7 +685,8 @@ mod tests {
use crate::client::tests::{
build_account_nonce_response, build_get_approved_aggregate_key_response,
build_get_last_round_response, build_get_peer_info_response, build_get_pox_data_response,
build_get_vote_for_aggregate_key_response, build_read_only_response, write_response,
build_get_round_info_response, build_get_vote_for_aggregate_key_response,
build_get_weight_threshold_response, build_read_only_response, write_response,
MockServerClient,
};

Expand Down Expand Up @@ -1194,4 +1236,31 @@ mod tests {
write_response(mock.server, key_response.as_bytes());
assert_eq!(h.join().unwrap().unwrap(), None);
}

#[test]
fn get_round_vote_weight_should_succeed() {
let mock = MockServerClient::new();
let vote_count = rand::thread_rng().next_u64();
let weight = rand::thread_rng().next_u64();
let round_response = build_get_round_info_response(Some((vote_count, weight)));
let h = spawn(move || mock.client.get_round_vote_weight(0, 0));
write_response(mock.server, round_response.as_bytes());
assert_eq!(h.join().unwrap().unwrap(), Some(weight as u128));

let mock = MockServerClient::new();
let round_response = build_get_round_info_response(None);
let h = spawn(move || mock.client.get_round_vote_weight(0, 0));
write_response(mock.server, round_response.as_bytes());
assert_eq!(h.join().unwrap().unwrap(), None);
}

#[test]
fn get_vote_threshold_weight_should_succeed() {
let mock = MockServerClient::new();
let weight = rand::thread_rng().next_u64();
let round_response = build_get_weight_threshold_response(weight);
let h = spawn(move || mock.client.get_vote_threshold_weight(0));
write_response(mock.server, round_response.as_bytes());
assert_eq!(h.join().unwrap().unwrap(), weight as u128);
}
}
101 changes: 65 additions & 36 deletions stacks-signer/src/signer.rs
Expand Up @@ -1230,49 +1230,78 @@ impl Signer {
}
return Ok(());
};
let coordinator_id = self.get_coordinator(current_reward_cycle).0;
if Some(self.signer_id) == coordinator_id && self.state == State::Idle {
debug!("{self}: Checking if old vote transaction exists in StackerDB...");
// Have I already voted and have a pending transaction? Check stackerdb for the same round number and reward cycle vote transaction
// Only get the account nonce of THIS signer as we only care about our own votes, not other signer votes
let signer_address = stacks_client.get_signer_address();
let account_nonces = self.get_account_nonces(stacks_client, &[*signer_address]);
let old_transactions = self.get_signer_transactions(&account_nonces).map_err(|e| {
if self.state != State::Idle
|| Some(self.signer_id) != self.get_coordinator(current_reward_cycle).0
{
// We are not the coordinator or we are in the middle of an operation. Do not attempt to queue DKG
return Ok(());
}
debug!("{self}: Checking if old DKG vote transaction exists in StackerDB...");
// Have I already voted, but the vote is still pending in StackerDB? Check stackerdb for the same round number and reward cycle vote transaction
// Only get the account nonce of THIS signer as we only care about our own votes, not other signer votes
let signer_address = stacks_client.get_signer_address();
let account_nonces = self.get_account_nonces(stacks_client, &[*signer_address]);
let old_transactions = self.get_signer_transactions(&account_nonces).map_err(|e| {
warn!("{self}: Failed to get old signer transactions: {e:?}. May trigger DKG unnecessarily");
}).unwrap_or_default();
// Check if we have an existing vote transaction for the same round and reward cycle
for transaction in old_transactions.iter() {
let params =
// Check if we have an existing vote transaction for the same round and reward cycle
for transaction in old_transactions.iter() {
let params =
NakamotoSigners::parse_vote_for_aggregate_public_key(transaction).unwrap_or_else(|| panic!("BUG: {self}: Received an invalid {SIGNERS_VOTING_FUNCTION_NAME} transaction in an already filtered list: {transaction:?}"));
if Some(params.aggregate_key) == self.coordinator.aggregate_public_key
&& params.voting_round == self.coordinator.current_dkg_id
&& reward_cycle == self.reward_cycle
{
debug!("{self}: Not triggering a DKG round. Already have a pending vote transaction.";
"txid" => %transaction.txid(),
"aggregate_key" => %params.aggregate_key,
"voting_round" => params.voting_round
);
return Ok(());
}
}
if stacks_client
.get_vote_for_aggregate_public_key(
self.coordinator.current_dkg_id,
self.reward_cycle,
*stacks_client.get_signer_address(),
)?
.is_some()
if Some(params.aggregate_key) == self.coordinator.aggregate_public_key
&& params.voting_round == self.coordinator.current_dkg_id
&& reward_cycle == self.reward_cycle
{
// TODO Check if the vote failed and we need to retrigger the DKG round not just if we have already voted...
// TODO need logic to trigger another DKG round if a certain amount of time passes and we still have no confirmed DKG vote
debug!("{self}: Not triggering a DKG round. Already voted and we may need to wait for more votes to arrive.");
debug!("{self}: Not triggering a DKG round. Already have a pending vote transaction.";
"txid" => %transaction.txid(),
"aggregate_key" => %params.aggregate_key,
"voting_round" => params.voting_round
);
return Ok(());
}
if self.commands.front() != Some(&Command::Dkg) {
info!("{self} is the current coordinator and must trigger DKG. Queuing DKG command...");
self.commands.push_front(Command::Dkg);
}
if let Some(aggregate_key) = stacks_client.get_vote_for_aggregate_public_key(
self.coordinator.current_dkg_id,
self.reward_cycle,
*stacks_client.get_signer_address(),
)? {
let Some(round_weight) = stacks_client
.get_round_vote_weight(self.reward_cycle, self.coordinator.current_dkg_id)?
else {
// This only will happen if soemhow we registered as a signer and were granted no weight which should not really ever happen.
jferrant marked this conversation as resolved.
Show resolved Hide resolved
error!("{self}: already voted for DKG, but no round vote weight found. We either have no voting power or the contract is corrupted.";
"voting_round" => self.coordinator.current_dkg_id,
"aggregate_key" => %aggregate_key
);
return Ok(());
};
let threshold_weight = stacks_client.get_vote_threshold_weight(self.reward_cycle)?;
if round_weight < threshold_weight {
// The threshold weight has not been met yet. We should wait for more votes to arrive.
// TODO: this should be on a timeout of some kind. We should not wait forever for the threshold to be met.
// See https://github.com/stacks-network/stacks-core/issues/4568
debug!("{self}: Not triggering a DKG round. Weight threshold has not been met yet. Waiting for more votes to arrive.";
"voting_round" => self.coordinator.current_dkg_id,
"aggregate_key" => %aggregate_key,
"round_weight" => round_weight,
"threshold_weight" => threshold_weight
);
return Ok(());
}
debug!("{self}: Vote for DKG failed. Triggering a DKG round.";
"voting_round" => self.coordinator.current_dkg_id,
"aggregate_key" => %aggregate_key,
"round_weight" => round_weight,
"threshold_weight" => threshold_weight
);
} else {
debug!("{self}: Triggering a DKG round.");
}
if self.commands.front() != Some(&Command::Dkg) {
info!("{self} is the current coordinator and must trigger DKG. Queuing DKG command...");
self.commands.push_front(Command::Dkg);
} else {
debug!("{self}: DKG command already queued...");
}
Ok(())
}
Expand Down