Fix dkg and sign test

Signed-off-by: Jacinta Ferrant <jacinta@trustmachines.co>
This commit is contained in:
Jacinta Ferrant
2024-02-11 13:01:56 -08:00
parent 63cca9ca47
commit 89541b25cd
7 changed files with 396 additions and 228 deletions

View File

@@ -156,7 +156,7 @@ pub(crate) mod tests {
use wsts::state_machine::PublicKeys;
use super::*;
use crate::config::{GlobalConfig, RewardCycleConfig};
use crate::config::{GlobalConfig, RegisteredSignersInfo, RewardCycleConfig};
pub struct MockServerClient {
pub server: TcpListener,
@@ -397,8 +397,8 @@ pub(crate) mod tests {
format!("HTTP/1.1 200 OK\n\n{{\"okay\":true,\"result\":\"{hex}\"}}")
}
/// Generate a random reward cycle config
/// Optionally include a signer pubilc key to set as the first signer id with signer id 0 and signer slot id 0
/// Generate a random reward cycle config for signer with id 0 and slot id 0
/// Optionally include a signer pubilc key to use for the signer
pub fn generate_reward_cycle_config(
num_signers: u32,
num_keys: u32,
@@ -490,15 +490,17 @@ pub(crate) mod tests {
}
(
RewardCycleConfig {
public_keys,
key_ids: signer_key_ids.get(&0).cloned().unwrap_or_default(),
signer_key_ids,
coordinator_key_ids,
signer_slot_id: 0,
signer_id: 0,
reward_cycle,
signer_address_ids,
signer_public_keys,
signer_id: 0,
signer_slot_id: 0,
key_ids: signer_key_ids.get(&0).cloned().unwrap_or_default(),
registered_signers: RegisteredSignersInfo {
public_keys,
coordinator_key_ids,
signer_key_ids,
signer_address_ids,
signer_public_keys,
},
},
addresses,
)

View File

@@ -1,3 +1,5 @@
use std::net::SocketAddr;
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
//
@@ -30,10 +32,11 @@ use blockstack_lib::net::api::postblock_proposal::NakamotoBlockProposal;
use blockstack_lib::util_lib::boot::{boot_code_addr, boot_code_id};
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier};
use clarity::vm::{ClarityName, ContractName, Value as ClarityValue};
use hashbrown::{HashMap, HashSet};
use serde_json::json;
use slog::{slog_debug, slog_warn};
use stacks_common::codec::StacksMessageCodec;
use stacks_common::consts::CHAIN_ID_MAINNET;
use stacks_common::consts::{CHAIN_ID_MAINNET, CHAIN_ID_TESTNET};
use stacks_common::types::chainstate::{
ConsensusHash, StacksAddress, StacksPrivateKey, StacksPublicKey,
};
@@ -45,7 +48,7 @@ use wsts::curve::point::{Compressed, Point};
use wsts::state_machine::PublicKeys;
use crate::client::{retry_with_exponential_backoff, ClientError};
use crate::config::GlobalConfig;
use crate::config::{GlobalConfig, RegisteredSignersInfo};
/// The name of the function for casting a DKG result to signer vote contract
pub const VOTE_FUNCTION_NAME: &str = "vote-for-aggregate-public-key";
@@ -81,6 +84,30 @@ impl From<&GlobalConfig> for StacksClient {
}
impl StacksClient {
/// Create a new signer StacksClient with the provided private key, stacks node host endpoint, and version
pub fn new(stacks_private_key: StacksPrivateKey, node_host: SocketAddr, mainnet: bool) -> Self {
let pubkey = StacksPublicKey::from_private(&stacks_private_key);
let tx_version = if mainnet {
TransactionVersion::Mainnet
} else {
TransactionVersion::Testnet
};
let chain_id = if mainnet {
CHAIN_ID_MAINNET
} else {
CHAIN_ID_TESTNET
};
let stacks_address = StacksAddress::p2pkh(mainnet, &pubkey);
Self {
stacks_private_key,
stacks_address,
http_origin: format!("http://{}", node_host),
tx_version,
chain_id,
stacks_node_client: reqwest::blocking::Client::new(),
}
}
/// Get our signer address
pub fn get_signer_address(&self) -> &StacksAddress {
&self.stacks_address
@@ -184,6 +211,7 @@ impl StacksClient {
reward_cycle: u64,
signer: StacksAddress,
) -> Result<Option<Point>, ClientError> {
debug!("Getting vote for aggregate public key...");
let function_name = ClarityName::from("get-vote");
let function_args = &[
ClarityValue::UInt(reward_cycle as u128),
@@ -373,7 +401,7 @@ impl StacksClient {
}
/// Get the reward set from the stacks node for the given reward cycle
pub fn get_reward_set(&self, reward_cycle: u64) -> Result<RewardSet, ClientError> {
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
@@ -389,6 +417,76 @@ impl StacksClient {
Ok(stackers_response.stacker_set)
}
/// Get registered signers info for the given reward 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 {
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);
let mut signer_key_ids = HashMap::with_capacity(reward_set_signers.len());
let mut signer_address_ids = HashMap::with_capacity(reward_set_signers.len());
let mut public_keys = PublicKeys {
signers: HashMap::with_capacity(reward_set_signers.len()),
key_ids: HashMap::with_capacity(4000),
};
let mut signer_public_keys = HashMap::with_capacity(reward_set_signers.len());
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}"
))
})?;
let signer_public_key = Point::try_from(&Compressed::from(ecdsa_public_key.to_bytes()))
.map_err(|e| {
ClientError::CorruptedRewardSet(format!(
"Reward cycle {reward_cycle} failed to convert signing key to Point: {e}"
))
})?;
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}"
))
})?;
let stacks_address = StacksAddress::p2pkh(
self.tx_version == TransactionVersion::Mainnet,
&stacks_public_key,
);
signer_address_ids.insert(stacks_address, signer_id);
signer_public_keys.insert(signer_id, signer_public_key);
let weight_start = weight_end;
weight_end = weight_start + entry.slots;
for key_id in weight_start..weight_end {
public_keys.key_ids.insert(key_id, ecdsa_public_key);
public_keys.signers.insert(signer_id, ecdsa_public_key);
coordinator_key_ids
.entry(signer_id)
.or_insert(HashSet::with_capacity(entry.slots as usize))
.insert(key_id);
signer_key_ids
.entry(signer_id)
.or_insert(Vec::with_capacity(entry.slots as usize))
.push(key_id);
}
}
Ok(Some(RegisteredSignersInfo {
public_keys,
signer_key_ids,
signer_address_ids,
signer_public_keys,
coordinator_key_ids,
}))
}
// Helper function to retrieve the pox data from the stacks node
fn get_pox_data(&self) -> Result<RPCPoxInfoData, ClientError> {
debug!("Getting pox data...");
@@ -1327,7 +1425,10 @@ mod tests {
#[test]
fn calculate_coordinator_different_consensus_hashes_produces_unique_results() {
let number_of_tests = 5;
let generated_public_keys = generate_reward_cycle_config(10, 4000, None).0.public_keys;
let generated_public_keys = generate_reward_cycle_config(10, 4000, None)
.0
.registered_signers
.public_keys;
let mut results = Vec::new();
for _ in 0..number_of_tests {
@@ -1365,7 +1466,10 @@ mod tests {
} else {
Some(same_hash)
};
let generated_public_keys = generate_reward_cycle_config(10, 4000, None).0.public_keys;
let generated_public_keys = generate_reward_cycle_config(10, 4000, None)
.0
.registered_signers
.public_keys;
for _ in 0..count {
let mock = MockServerClient::new();
let generated_public_keys = generated_public_keys.clone();

View File

@@ -110,15 +110,9 @@ impl Network {
}
}
/// The Configuration info needed for an individual signer per reward cycle
/// The registered signer information for a specific reward cycle
#[derive(Debug, Clone)]
pub struct RewardCycleConfig {
/// The index into the signers list of this signer's key (may be different from signer_id)
pub signer_slot_id: u32,
/// The signer ID assigned to this signer
pub signer_id: u32,
/// The reward cycle of the configuration
pub reward_cycle: u64,
pub struct RegisteredSignersInfo {
/// The signer to key ids mapping for the coordinator
pub coordinator_key_ids: HashMap<u32, HashSet<u32>>,
/// The signer to key ids mapping for the signers
@@ -129,8 +123,21 @@ pub struct RewardCycleConfig {
pub signer_address_ids: HashMap<StacksAddress, u32>,
/// The public keys for the reward cycle
pub public_keys: PublicKeys,
}
/// The Configuration info needed for an individual signer per reward cycle
#[derive(Debug, Clone)]
pub struct RewardCycleConfig {
/// The reward cycle of the configuration
pub reward_cycle: u64,
/// The signer ID assigned to this signer
pub signer_id: u32,
/// The index into the signers list of this signer's key (may be different from signer_id)
pub signer_slot_id: u32,
/// This signer's key ids
pub key_ids: Vec<u32>,
/// The registered signers for this reward cycle
pub registered_signers: RegisteredSignersInfo,
}
/// The parsed configuration for the signer

View File

@@ -18,14 +18,11 @@ use std::time::Duration;
use blockstack_lib::chainstate::stacks::boot::SIGNERS_NAME;
use blockstack_lib::util_lib::boot::boot_code_id;
use hashbrown::{HashMap, HashSet};
use hashbrown::HashMap;
use libsigner::{SignerEvent, SignerRunLoop};
use slog::{slog_debug, slog_error, slog_info, slog_warn};
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::{OperationResult, PublicKeys};
use wsts::state_machine::OperationResult;
use crate::client::{retry_with_exponential_backoff, ClientError, StacksClient};
use crate::config::{GlobalConfig, RewardCycleConfig};
@@ -108,81 +105,33 @@ impl RunLoop {
};
// 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(reward_set_signers) = self.stacks_client.get_reward_set(reward_cycle)?.signers
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."
);
return Ok(None);
};
let mut weight_end = 1;
// 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 coordinator_key_ids = HashMap::with_capacity(4000);
let mut signer_key_ids = HashMap::with_capacity(reward_set_signers.len());
let mut signer_address_ids = HashMap::with_capacity(reward_set_signers.len());
let mut public_keys = PublicKeys {
signers: HashMap::with_capacity(reward_set_signers.len()),
key_ids: HashMap::with_capacity(4000),
};
let mut signer_public_keys = HashMap::with_capacity(reward_set_signers.len());
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}"
))
})?;
let signer_public_key = Point::try_from(&Compressed::from(ecdsa_public_key.to_bytes()))
.map_err(|e| {
ClientError::CorruptedRewardSet(format!(
"Reward cycle {reward_cycle} failed to convert signing key to Point: {e}"
))
})?;
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}"
))
})?;
let stacks_address =
StacksAddress::p2pkh(self.config.network.is_mainnet(), &stacks_public_key);
signer_address_ids.insert(stacks_address, signer_id);
signer_public_keys.insert(signer_id, signer_public_key);
let weight_start = weight_end;
weight_end = weight_start + entry.slots;
for key_id in weight_start..weight_end {
public_keys.key_ids.insert(key_id, ecdsa_public_key);
public_keys.signers.insert(signer_id, ecdsa_public_key);
coordinator_key_ids
.entry(signer_id)
.or_insert(HashSet::with_capacity(entry.slots as usize))
.insert(key_id);
signer_key_ids
.entry(signer_id)
.or_insert(Vec::with_capacity(entry.slots as usize))
.push(key_id);
}
}
let Some(signer_id) = signer_address_ids.get(current_addr) else {
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);
};
debug!(
"Signer #{signer_id} ({current_addr}) is registered for reward cycle {reward_cycle}."
);
let key_ids = signer_key_ids.get(signer_id).cloned().unwrap_or_default();
let key_ids = registered_signers
.signer_key_ids
.get(signer_id)
.cloned()
.unwrap_or_default();
Ok(Some(RewardCycleConfig {
reward_cycle,
signer_id: *signer_id,
signer_slot_id,
signer_address_ids,
key_ids,
coordinator_key_ids,
signer_key_ids,
public_keys,
signer_public_keys,
registered_signers,
}))
}
@@ -319,14 +268,23 @@ impl SignerRunLoop<Vec<OperationResult>, RunLoopCommand> for RunLoop {
if let Some(stacks_signer) = self.stacks_signers.get_mut(&(reward_cycle % 2)) {
if stacks_signer.reward_cycle != reward_cycle {
warn!(
"Signer is not registered for reward cycle {reward_cycle}. Ignoring command: {command:?}"
"Signer #{}: not registered for reward cycle {reward_cycle}. Ignoring command: {command:?}", stacks_signer.signer_id
);
} else {
info!(
"Signer #{}: Queuing an external runloop command ({:?}): {command:?}",
stacks_signer.signer_id,
stacks_signer
.signing_round
.public_keys
.signers
.get(&stacks_signer.signer_id)
);
stacks_signer.commands.push_back(command.command);
}
} else {
warn!(
"Signer is not registered for reward cycle {reward_cycle}. Ignoring command: {command:?}"
"No signer registered for reward cycle {reward_cycle}. Ignoring command: {command:?}"
);
}
}

View File

@@ -106,10 +106,8 @@ pub enum Command {
pub enum State {
/// The signer is idle, waiting for messages and commands
Idle,
/// The signer is executing a DKG round
Dkg,
/// The signer is executing a signing round
Sign,
/// The signer is executing a DKG or Sign round
OperationInProgress,
/// The Signer has exceeded its tenure
TenureExceeded,
}
@@ -146,10 +144,22 @@ impl Signer {
pub fn from_configs(config: &GlobalConfig, reward_cycle_config: RewardCycleConfig) -> Self {
let stackerdb = StackerDB::from_configs(config, &reward_cycle_config);
let num_signers = u32::try_from(reward_cycle_config.public_keys.signers.len())
.expect("FATAL: Too many registered signers to fit in a u32");
let num_keys = u32::try_from(reward_cycle_config.public_keys.key_ids.len())
.expect("FATAL: Too many key ids to fit in a u32");
let num_signers = u32::try_from(
reward_cycle_config
.registered_signers
.public_keys
.signers
.len(),
)
.expect("FATAL: Too many registered signers to fit in a u32");
let num_keys = u32::try_from(
reward_cycle_config
.registered_signers
.public_keys
.key_ids
.len(),
)
.expect("FATAL: Too many key ids to fit in a u32");
let threshold = num_keys * 7 / 10;
let dkg_threshold = num_keys * 9 / 10;
@@ -164,8 +174,8 @@ impl Signer {
dkg_end_timeout: config.dkg_end_timeout,
nonce_timeout: config.nonce_timeout,
sign_timeout: config.sign_timeout,
signer_key_ids: reward_cycle_config.coordinator_key_ids,
signer_public_keys: reward_cycle_config.signer_public_keys,
signer_key_ids: reward_cycle_config.registered_signers.coordinator_key_ids,
signer_public_keys: reward_cycle_config.registered_signers.signer_public_keys,
};
let coordinator = FireCoordinator::new(coordinator_config);
@@ -176,7 +186,7 @@ impl Signer {
reward_cycle_config.signer_id,
reward_cycle_config.key_ids,
config.ecdsa_private_key,
reward_cycle_config.public_keys,
reward_cycle_config.registered_signers.public_keys,
);
Self {
coordinator,
@@ -187,7 +197,7 @@ impl Signer {
stackerdb,
is_mainnet: config.network.is_mainnet(),
signer_id: reward_cycle_config.signer_id,
signer_address_ids: reward_cycle_config.signer_address_ids,
signer_address_ids: reward_cycle_config.registered_signers.signer_address_ids,
reward_cycle: reward_cycle_config.reward_cycle,
tx_fee_ms: config.tx_fee_ms,
}
@@ -220,7 +230,7 @@ impl Signer {
Ok(msg) => {
let ack = self.stackerdb.send_message_with_retry(msg.into());
debug!("Signer #{}: ACK: {ack:?}", self.signer_id);
self.state = State::Dkg;
self.state = State::OperationInProgress;
}
Err(e) => {
error!("Signer #{}: Failed to start DKG: {e:?}", self.signer_id);
@@ -232,13 +242,6 @@ impl Signer {
is_taproot,
merkle_root,
} => {
let epoch = stacks_client
.get_node_epoch_with_retry()
.unwrap_or(StacksEpochId::Epoch24);
if epoch != StacksEpochId::Epoch30 {
debug!("Signer #{}: cannot sign blocks in pre Epoch 3.0. Ignoring the sign command.", self.signer_id);
return;
};
let signer_signature_hash = block.header.signer_signature_hash();
let block_info = self
.blocks
@@ -257,7 +260,7 @@ impl Signer {
Ok(msg) => {
let ack = self.stackerdb.send_message_with_retry(msg.into());
debug!("Signer #{}: ACK: {ack:?}", self.signer_id);
self.state = State::Sign;
self.state = State::OperationInProgress;
block_info.signed_over = true;
}
Err(e) => {
@@ -275,11 +278,11 @@ impl Signer {
pub fn process_next_command(&mut self, stacks_client: &StacksClient) {
match self.state {
State::Idle => {
let (coordinator_id, _) =
let (coordinator_id, coordinator_pk) =
stacks_client.calculate_coordinator(&self.signing_round.public_keys);
if coordinator_id != self.signer_id {
debug!(
"Signer #{}: Not the coordinator. Will not process any commands...",
"Signer #{}: Not the coordinator. (Coordinator is {coordinator_id:?}, {coordinator_pk:?}). Will not process any commands...",
self.signer_id
);
return;
@@ -294,12 +297,12 @@ impl Signer {
);
}
}
State::Dkg | State::Sign => {
State::OperationInProgress => {
// We cannot execute the next command until the current one is finished...
// Do nothing...
debug!(
"Signer #{}: Waiting for {:?} operation to finish",
self.signer_id, self.state
"Signer #{}: Waiting for operation to finish",
self.signer_id,
);
}
State::TenureExceeded => {
@@ -479,6 +482,8 @@ impl Signer {
self.state = State::Idle;
self.process_operation_results(stacks_client, &operation_results);
self.send_operation_results(res, operation_results);
} else if self.coordinator.state != CoordinatorState::Idle {
self.state = State::OperationInProgress;
}
self.send_outbound_messages(signer_outbound_messages);
self.send_outbound_messages(coordinator_outbound_messages);
@@ -1169,6 +1174,10 @@ impl Signer {
&& self.signer_id == coordinator_id
&& self.coordinator.state == CoordinatorState::Idle
{
debug!(
"Signer #{}: Checking if old transactions exist",
self.signer_id
);
// 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| {

View File

@@ -585,11 +585,11 @@ fn signer_vote_if_needed(
pub fn boot_to_epoch_3_reward_set(
naka_conf: &Config,
blocks_processed: &RunLoopCounter,
stacker_sks: &[Secp256k1PrivateKey],
signer_pks: &[StacksPublicKey],
stacker_sks: &[StacksPrivateKey],
signer_sks: &[StacksPrivateKey],
btc_regtest_controller: &mut BitcoinRegtestController,
) {
assert_eq!(stacker_sks.len(), signer_pks.len());
assert_eq!(stacker_sks.len(), signer_sks.len());
let epochs = naka_conf.burnchain.epochs.clone().unwrap();
let epoch_3 = &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap()];
@@ -617,7 +617,7 @@ pub fn boot_to_epoch_3_reward_set(
.get_burnchain()
.block_height_to_reward_cycle(block_height)
.unwrap();
for (stacker_sk, signer_pk) in stacker_sks.iter().zip(signer_pks.iter()) {
for (stacker_sk, signer_sk) in stacker_sks.iter().zip(signer_sks.iter()) {
let pox_addr = PoxAddress::from_legacy(
AddressHashMode::SerializeP2PKH,
tests::to_addr(&stacker_sk).bytes,
@@ -626,7 +626,7 @@ pub fn boot_to_epoch_3_reward_set(
pox_addr.clone().as_clarity_tuple().unwrap().into();
let signature = make_pox_4_signer_key_signature(
&pox_addr,
stacker_sk,
&signer_sk,
reward_cycle.into(),
&Pox4SignatureTopic::StackStx,
CHAIN_ID_TESTNET,
@@ -634,6 +634,8 @@ pub fn boot_to_epoch_3_reward_set(
)
.unwrap()
.to_rsv();
let signer_pk = StacksPublicKey::from_private(signer_sk);
let stacking_tx = tests::make_contract_call(
&stacker_sk,
0,

View File

@@ -6,7 +6,6 @@ use std::time::{Duration, Instant};
use std::{env, thread};
use clarity::boot_util::boot_code_id;
use hashbrown::HashMap;
use libsigner::{
BlockResponse, RejectCode, RunningSigner, Signer, SignerEventReceiver, SignerMessage,
BLOCK_MSG_ID, TRANSACTIONS_MSG_ID,
@@ -26,9 +25,7 @@ use stacks::util_lib::strings::StacksString;
use stacks_common::bitvec::BitVec;
use stacks_common::codec::read_next;
use stacks_common::consts::SIGNER_SLOTS_PER_USER;
use stacks_common::types::chainstate::{
ConsensusHash, StacksAddress, StacksBlockId, StacksPublicKey, TrieHash,
};
use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId, StacksPublicKey, TrieHash};
use stacks_common::types::StacksEpochId;
use stacks_common::util::hash::{MerkleTree, Sha512Trunc256Sum};
use stacks_common::util::secp256k1::MessageSignature;
@@ -46,7 +43,7 @@ use crate::neon::Counters;
use crate::run_loop::boot_nakamoto;
use crate::tests::bitcoin_regtest::BitcoinCoreController;
use crate::tests::nakamoto_integrations::{
boot_to_epoch_3_reward_set, naka_neon_integration_conf, next_block_and,
boot_to_epoch_3, boot_to_epoch_3_reward_set, naka_neon_integration_conf, next_block_and,
next_block_and_mine_commit, POX_4_DEFAULT_STACKER_BALANCE,
};
use crate::tests::neon_integrations::{
@@ -73,11 +70,11 @@ struct SignerTest {
// The stx and bitcoin nodes and their run loops
pub running_nodes: RunningNodes,
// The channels for sending commands to the signers
pub signer_cmd_senders: HashMap<u32, Sender<RunLoopCommand>>,
pub signer_cmd_senders: Vec<Sender<RunLoopCommand>>,
// The channels for receiving results from the signers
pub result_receivers: Vec<Receiver<Vec<OperationResult>>>,
// The running signer and its threads
pub running_signers: HashMap<u32, RunningSigner<SignerEventReceiver, Vec<OperationResult>>>,
pub running_signers: Vec<RunningSigner<SignerEventReceiver, Vec<OperationResult>>>,
// the private keys of the signers
pub signer_stacks_private_keys: Vec<StacksPrivateKey>,
// link to the stacks node
@@ -85,15 +82,16 @@ struct SignerTest {
}
impl SignerTest {
fn new(num_signers: u32, _num_keys: u32) -> Self {
fn new(num_signers: usize, disable_signing_key: bool) -> Self {
// Generate Signer Data
let signer_stacks_private_keys = (0..num_signers)
.map(|_| StacksPrivateKey::new())
.collect::<Vec<StacksPrivateKey>>();
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
naka_conf.miner.self_signing_key = None;
if disable_signing_key {
naka_conf.miner.self_signing_key = None;
}
// Setup the signer and coordinator configurations
let signer_configs = build_signer_config_tomls(
&signer_stacks_private_keys,
@@ -102,29 +100,24 @@ impl SignerTest {
&Network::Testnet,
);
let mut running_signers = HashMap::new();
let mut signer_cmd_senders = HashMap::new();
let mut running_signers = Vec::new();
let mut signer_cmd_senders = Vec::new();
let mut result_receivers = Vec::new();
// Spawn all signers before the node to ensure their listening ports are open for the node event observer to bind to
for i in (0..num_signers).rev() {
for i in 0..num_signers {
let (cmd_send, cmd_recv) = channel();
let (res_send, res_recv) = channel();
info!("spawn signer");
running_signers.insert(
i,
spawn_signer(&signer_configs[i as usize], cmd_recv, res_send),
);
signer_cmd_senders.insert(i, cmd_send);
running_signers.push(spawn_signer(
&signer_configs[i as usize],
cmd_recv,
res_send,
));
signer_cmd_senders.push(cmd_send);
result_receivers.push(res_recv);
}
// Setup the nodes and deploy the contract to it
let node = setup_stx_btc_node(
naka_conf,
num_signers,
&signer_stacks_private_keys,
&signer_configs,
);
let node = setup_stx_btc_node(naka_conf, &signer_stacks_private_keys, &signer_configs);
let config = SignerConfig::load_from_str(&signer_configs[0]).unwrap();
let stacks_client = StacksClient::from(&config);
@@ -138,6 +131,74 @@ impl SignerTest {
}
}
fn run_until_epoch_3_boundary(&mut self) {
let epochs = self.running_nodes.conf.burnchain.epochs.clone().unwrap();
let epoch_3 =
&epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap()];
let epoch_30_boundary = epoch_3.start_height - 1;
// advance to epoch 3.0 and trigger a sign round (cannot vote on blocks in pre epoch 3.0)
run_until_burnchain_height(
&mut self.running_nodes.btc_regtest_controller,
&self.running_nodes.blocks_processed,
epoch_30_boundary,
&self.running_nodes.conf,
);
info!("Avanced to Nakamoto! Ready to Sign Blocks!");
}
fn get_current_reward_cycle(&self) -> u64 {
let block_height = self
.running_nodes
.btc_regtest_controller
.get_headers_height();
self.running_nodes
.btc_regtest_controller
.get_burnchain()
.block_height_to_reward_cycle(block_height)
.unwrap()
}
// Will panic if called on a reward cycle that has not had its signers calculated yet
fn get_coordinator_sender(&self, reward_cycle: u64) -> &Sender<RunLoopCommand> {
debug!(
"Getting current coordinator for reward cycle {:?}",
reward_cycle
);
// Calculate which signer is the coordinator
let private_key = StacksPrivateKey::new();
let node_host = self
.running_nodes
.conf
.node
.rpc_bind
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
// Use the stacks client to calculate the current registered signers and their coordinator
let stacks_client = StacksClient::new(private_key, node_host, false);
let (coordinator_id, coordinator_pk) = stacks_client.calculate_coordinator(
&stacks_client
.get_registered_signers_info(reward_cycle)
.unwrap()
.unwrap()
.public_keys,
);
let coordinator_index = self
.signer_stacks_private_keys
.iter()
.position(|sk| {
let pubkey = StacksPublicKey::from_private(sk);
let coordinator_pk_bytes = coordinator_pk.to_bytes();
let pubkey_bytes = pubkey.to_bytes_compressed();
coordinator_pk_bytes.as_slice() == pubkey_bytes.as_slice()
})
.unwrap();
debug!("Coordinator is {coordinator_id:?} ({coordinator_pk:?}). Command sender found at index: {coordinator_index:?}");
self.signer_cmd_senders.get(coordinator_index).unwrap()
}
fn shutdown(self) {
self.running_nodes
.coord_channel
@@ -151,7 +212,7 @@ impl SignerTest {
self.running_nodes.run_loop_thread.join().unwrap();
// Stop the signers
for (_id, signer) in self.running_signers {
for signer in self.running_signers {
assert!(signer.stop().is_none());
}
}
@@ -178,9 +239,8 @@ fn spawn_signer(
fn setup_stx_btc_node(
mut naka_conf: NeonConfig,
num_signers: u32,
signer_stacks_private_keys: &[StacksPrivateKey],
signer_config_tomls: &Vec<String>,
signer_config_tomls: &[String],
) -> RunningNodes {
// Spawn the endpoints for observing signers
for toml in signer_config_tomls {
@@ -204,9 +264,9 @@ fn setup_stx_btc_node(
let mut initial_balances = Vec::new();
// TODO: separate keys for stacking and signing (because they'll be different in prod)
for i in 0..num_signers {
for key in signer_stacks_private_keys {
initial_balances.push(InitialBalance {
address: to_addr(&signer_stacks_private_keys[i as usize]).into(),
address: to_addr(key).into(),
amount: POX_4_DEFAULT_STACKER_BALANCE,
});
}
@@ -224,7 +284,6 @@ fn setup_stx_btc_node(
}
}
}
info!("Make new BitcoinCoreController");
let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
btcd_controller
@@ -268,16 +327,6 @@ fn setup_stx_btc_node(
info!("Mine third block...");
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
info!("Boot to epoch 2.5 to activate pox-4...");
boot_to_epoch_3_reward_set(
&naka_conf,
&blocks_processed,
signer_stacks_private_keys,
signer_stacks_private_keys,
&mut btc_regtest_controller,
);
info!("Pox 4 activated and Nakamoto's first reward set calculated! Ready for signers to perform DKG!");
RunningNodes {
btcd_controller,
btc_regtest_controller,
@@ -339,16 +388,43 @@ fn stackerdb_dkg_sign() {
let mut msg = block.header.signer_signature_hash().0.to_vec();
msg.push(b'n');
let signer_test = SignerTest::new(10, 400);
let timeout = Duration::from_secs(200);
let mut signer_test = SignerTest::new(10, false);
info!("Boot to epoch 3.0 reward calculation...");
boot_to_epoch_3_reward_set(
&signer_test.running_nodes.conf,
&signer_test.running_nodes.blocks_processed,
&signer_test.signer_stacks_private_keys,
&signer_test.signer_stacks_private_keys,
&mut signer_test.running_nodes.btc_regtest_controller,
);
info!("Pox 4 activated and at epoch 3.0 reward set calculation (2nd block of its prepare phase)! Ready for signers to perform DKG and Sign!");
// Determine the coordinator
// we have just calculated the reward set for the next reward cycle hence the + 1
let reward_cycle = signer_test.get_current_reward_cycle().wrapping_add(1);
let coordinator_sender = signer_test.get_coordinator_sender(reward_cycle);
info!("------------------------- Test DKG -------------------------");
info!("signer_runloop: spawn send commands to do dkg");
info!("signer_runloop: spawn send commands to do DKG");
let dkg_now = Instant::now();
let mut key = Point::default();
let dkg_command = RunLoopCommand {
reward_cycle,
command: SignerCommand::Dkg,
};
coordinator_sender
.send(dkg_command)
.expect("failed to send DKG command");
info!("signer_runloop: waiting for DKG results");
for recv in signer_test.result_receivers.iter() {
let mut aggregate_public_key = None;
loop {
let results = recv.recv().expect("failed to recv dkg results");
let results = recv
.recv_timeout(timeout)
.expect("failed to recv dkg results");
for result in results {
match result {
OperationResult::Sign(sig) => {
@@ -369,20 +445,27 @@ fn stackerdb_dkg_sign() {
}
}
}
if aggregate_public_key.is_some() || dkg_now.elapsed() > Duration::from_secs(200) {
if aggregate_public_key.is_some() || dkg_now.elapsed() > timeout {
break;
}
}
key = aggregate_public_key.expect("Failed to get aggregate public key within 200 seconds");
key = aggregate_public_key.expect(&format!(
"Failed to get aggregate public key within {timeout:?}"
));
}
let dkg_elapsed = dkg_now.elapsed();
// We can't sign a block
signer_test.run_until_epoch_3_boundary();
info!("------------------------- Test Sign -------------------------");
// Determine the coordinator of the current node height
let reward_cycle = signer_test.get_current_reward_cycle();
let coordinator_sender = signer_test.get_coordinator_sender(reward_cycle);
let sign_now = Instant::now();
info!("signer_runloop: spawn send commands to do dkg and then sign");
let sign_command = RunLoopCommand {
reward_cycle: 11,
reward_cycle,
command: SignerCommand::Sign {
block: block.clone(),
is_taproot: false,
@@ -390,26 +473,26 @@ fn stackerdb_dkg_sign() {
},
};
let sign_taproot_command = RunLoopCommand {
reward_cycle: 11,
reward_cycle,
command: SignerCommand::Sign {
block: block.clone(),
is_taproot: true,
merkle_root: None,
},
};
for cmd_sender in signer_test.signer_cmd_senders.values() {
cmd_sender
.send(sign_command.clone())
.expect("failed to send non taproot Sign command");
cmd_sender
.send(sign_taproot_command.clone())
.expect("failed to send taproot Sign command");
}
coordinator_sender
.send(sign_command)
.expect("failed to send Sign command");
coordinator_sender
.send(sign_taproot_command)
.expect("failed to send Sign taproot command");
for recv in signer_test.result_receivers.iter() {
let mut frost_signature = None;
let mut schnorr_proof = None;
loop {
let results = recv.recv().expect("failed to recv signature results");
let results = recv
.recv_timeout(timeout)
.expect("failed to recv signature results");
for result in results {
match result {
OperationResult::Sign(sig) => {
@@ -432,19 +515,20 @@ fn stackerdb_dkg_sign() {
}
}
if (frost_signature.is_some() && schnorr_proof.is_some())
|| sign_now.elapsed() > Duration::from_secs(200)
|| sign_now.elapsed() > timeout
{
break;
}
}
let frost_signature =
frost_signature.expect("Failed to get frost signature within 100 seconds");
frost_signature.expect(&format!("Failed to get frost signature within {timeout:?}"));
assert!(
frost_signature.verify(&key, msg.as_slice()),
"Signature verification failed"
);
let schnorr_proof =
schnorr_proof.expect("Failed to get schnorr proof signature within 100 seconds");
let schnorr_proof = schnorr_proof.expect(&format!(
"Failed to get schnorr proof signature within {timeout:?}"
));
let tweaked_key = wsts::compute::tweaked_public_key(&key, None);
assert!(
schnorr_proof.verify(&tweaked_key.x(), &msg.as_slice()),
@@ -464,7 +548,7 @@ fn stackerdb_dkg_sign() {
///
/// Test Setup:
/// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind.
/// The stacks node is advanced to epoch 3.0, triggering signers to perform DKG round.
/// The stacks node is advanced to epoch 3.0. DKG foricbly triggered to set the key correctly
///
/// Test Execution:
/// The node attempts to mine a Nakamoto tenure, sending a block to the observing signers via the
@@ -487,16 +571,36 @@ fn stackerdb_block_proposal() {
.init();
info!("------------------------- Test Setup -------------------------");
let mut signer_test = SignerTest::new(5, 5);
let mut signer_test = SignerTest::new(5, true);
let (_vrfs_submitted, commits_submitted) = (
signer_test.running_nodes.vrfs_submitted.clone(),
signer_test.running_nodes.commits_submitted.clone(),
);
boot_to_epoch_3(
&signer_test.running_nodes.conf,
&signer_test.running_nodes.blocks_processed,
&signer_test.signer_stacks_private_keys,
&signer_test.signer_stacks_private_keys,
&mut signer_test.running_nodes.btc_regtest_controller,
);
// Determine the coordinator
let reward_cycle = signer_test.get_current_reward_cycle();
let coordinator_sender = signer_test.get_coordinator_sender(reward_cycle);
// Forcibly run DKG to overwrite the self signing aggregate key in the contract
info!("------------------------- Wait for DKG -------------------------");
info!("signer_runloop: spawn send commands to do dkg");
let dkg_now = Instant::now();
let mut key = Point::default();
let dkg_command = RunLoopCommand {
reward_cycle,
command: SignerCommand::Dkg,
};
coordinator_sender
.send(dkg_command)
.expect("failed to send DKG command");
for recv in signer_test.result_receivers.iter() {
let mut aggregate_public_key = None;
loop {
@@ -531,42 +635,16 @@ fn stackerdb_block_proposal() {
}
let dkg_elapsed = dkg_now.elapsed();
let epochs = signer_test
.running_nodes
.conf
.burnchain
.epochs
.clone()
.unwrap();
let epoch_3 = &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap()];
let epoch_30_boundary = epoch_3.start_height - 1;
info!(
"Advancing to Epoch 3.0 Boundary";
"Epoch 3.0 Boundary" => epoch_30_boundary,
);
// Advance to epoch 3.0
run_until_burnchain_height(
&mut signer_test.running_nodes.btc_regtest_controller,
&signer_test.running_nodes.blocks_processed,
epoch_30_boundary,
&signer_test.running_nodes.conf,
);
info!("Avanced to Nakamoto! Ready to Sign Blocks!");
info!("------------------------- Test Block Processed -------------------------");
let sign_now = Instant::now();
// Mine 1 nakamoto tenure
next_block_and_mine_commit(
let _ = next_block_and_mine_commit(
&mut signer_test.running_nodes.btc_regtest_controller,
60,
&signer_test.running_nodes.coord_channel,
&commits_submitted,
)
.unwrap();
);
let recv = signer_test
.result_receivers
@@ -694,7 +772,7 @@ fn stackerdb_block_proposal_missing_transactions() {
.init();
info!("------------------------- Test Setup -------------------------");
let mut signer_test = SignerTest::new(5, 5);
let mut signer_test = SignerTest::new(5, false);
let host = signer_test
.running_nodes
@@ -733,7 +811,7 @@ fn stackerdb_block_proposal_missing_transactions() {
.signer_stacks_private_keys
.iter()
.find(|pk| {
let addr = StacksAddress::p2pkh(false, &StacksPublicKey::from_private(pk));
let addr = to_addr(pk);
addr == signer_address_1
})
.cloned()
@@ -742,10 +820,7 @@ fn stackerdb_block_proposal_missing_transactions() {
let mut stackerdb_1 = StackerDB::new(host, signer_private_key_1, false, 1, 0);
debug!("Signer address is {}", &signer_address_1);
assert_eq!(
signer_address_1,
StacksAddress::p2pkh(false, &StacksPublicKey::from_private(&signer_private_key_1))
);
assert_eq!(signer_address_1, to_addr(&signer_private_key_1),);
// Create a valid transaction signed by the signer private key coresponding to the slot into which it is being inserted (signer id 0)
let mut valid_tx = StacksTransaction {
@@ -770,10 +845,7 @@ fn stackerdb_block_proposal_missing_transactions() {
let invalid_signer_private_key = StacksPrivateKey::new();
debug!(
"Invalid address is {}",
&StacksAddress::p2pkh(
false,
&StacksPublicKey::from_private(&invalid_signer_private_key)
)
to_addr(&invalid_signer_private_key)
);
let mut invalid_tx = StacksTransaction {
version: TransactionVersion::Testnet,
@@ -792,18 +864,32 @@ fn stackerdb_block_proposal_missing_transactions() {
};
invalid_tx.set_origin_nonce(0);
info!("Boot to epoch 3.0 reward calculation...");
boot_to_epoch_3_reward_set(
&signer_test.running_nodes.conf,
&signer_test.running_nodes.blocks_processed,
&signer_test.signer_stacks_private_keys,
&signer_test.signer_stacks_private_keys,
&mut signer_test.running_nodes.btc_regtest_controller,
);
info!("Pox 4 activated and at epoch 3.0 reward set calculation (2nd block of its prepare phase)! Ready for signers to perform DKG and Sign!");
// Determine the coordinator
// we have just calculated the reward set for the next reward cycle hence the + 1
let reward_cycle = signer_test.get_current_reward_cycle().wrapping_add(1);
let coordinator_sender = signer_test.get_coordinator_sender(reward_cycle);
// First run DKG in order to sign the block that arrives from the miners following a nakamoto block production
// TODO: remove this forcibly running DKG once we have casting of the vote automagically happening during epoch 2.5
info!("signer_runloop: spawn send commands to do dkg");
let dkg_command = RunLoopCommand {
reward_cycle: 11,
reward_cycle,
command: SignerCommand::Dkg,
};
for cmd_sender in signer_test.signer_cmd_senders.values() {
cmd_sender
.send(dkg_command.clone())
.expect("failed to send Dkg command");
}
coordinator_sender
.send(dkg_command)
.expect("failed to send DKG command");
let recv = signer_test
.result_receivers
.last()