From 565b424bfae0f6664a2126059403f8dc0beb3e78 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 5 Dec 2023 16:40:52 -0500 Subject: [PATCH 01/27] Seperate stacks node calls from stacker db specific calls Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/mod.rs | 91 ++++ stacks-signer/src/client/stackerdb.rs | 101 ++++ stacks-signer/src/client/stacks_client.rs | 600 ++++++++++++++++++++++ stacks-signer/src/lib.rs | 4 +- stacks-signer/src/runloop.rs | 14 +- stacks-signer/src/utils.rs | 2 +- 6 files changed, 804 insertions(+), 8 deletions(-) create mode 100644 stacks-signer/src/client/mod.rs create mode 100644 stacks-signer/src/client/stackerdb.rs create mode 100644 stacks-signer/src/client/stacks_client.rs diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs new file mode 100644 index 0000000000..c90473e64a --- /dev/null +++ b/stacks-signer/src/client/mod.rs @@ -0,0 +1,91 @@ +/// The stacker db module for communicating with the stackerdb contract +mod stackerdb; +/// The stacks node client module for communicating with the stacks node +mod stacks_client; + +use std::time::Duration; + +use clarity::vm::types::serialization::SerializationError; +use clarity::vm::Value as ClarityValue; +use libsigner::RPCError; +use libstackerdb::Error as StackerDBError; +use slog::slog_debug; +pub use stackerdb::*; +pub use stacks_client::*; +use stacks_common::debug; + +/// Backoff timer initial interval in milliseconds +const BACKOFF_INITIAL_INTERVAL: u64 = 128; +/// Backoff timer max interval in milliseconds +const BACKOFF_MAX_INTERVAL: u64 = 16384; + +/// Temporary placeholder for the number of slots allocated to a stacker-db writer. This will be retrieved from the stacker-db instance in the future +/// See: https://github.com/stacks-network/stacks-blockchain/issues/3921 +/// Is equal to the number of message types +pub const SIGNER_SLOTS_PER_USER: u32 = 10; +/// The number of miner slots available per miner +pub const MINER_SLOTS_PER_USER: u32 = 1; + +#[derive(thiserror::Error, Debug)] +/// Client error type +pub enum ClientError { + /// An error occurred serializing the message + #[error("Unable to serialize stacker-db message: {0}")] + StackerDBSerializationError(#[from] bincode::Error), + /// Failed to sign stacker-db chunk + #[error("Failed to sign stacker-db chunk: {0}")] + FailToSign(#[from] StackerDBError), + /// Failed to write to stacker-db due to RPC error + #[error("Failed to write to stacker-db instance: {0}")] + PutChunkFailed(#[from] RPCError), + /// Stacker-db instance rejected the chunk + #[error("Stacker-db rejected the chunk. Reason: {0}")] + PutChunkRejected(String), + /// Failed to find a given json entry + #[error("Invalid JSON entry: {0}")] + InvalidJsonEntry(String), + /// Failed to call a read only function + #[error("Failed to call read only function. {0}")] + ReadOnlyFailure(String), + /// Reqwest specific error occurred + #[error("{0}")] + ReqwestError(#[from] reqwest::Error), + /// Failed to build and sign a new Stacks transaction. + #[error("Failed to generate transaction from a transaction signer: {0}")] + TransactionGenerationFailure(String), + /// Stacks node client request failed + #[error("Stacks node client request failed: {0}")] + RequestFailure(reqwest::StatusCode), + /// Failed to serialize a Clarity value + #[error("Failed to serialize Clarity value: {0}")] + ClaritySerializationError(#[from] SerializationError), + /// Failed to parse a Clarity value + #[error("Recieved a malformed clarity value: {0}")] + MalformedClarityValue(ClarityValue), + /// Invalid Clarity Name + #[error("Invalid Clarity Name: {0}")] + InvalidClarityName(String), + /// Backoff retry timeout + #[error("Backoff retry timeout occurred. Stacks node may be down.")] + RetryTimeout, +} + +/// Retry a function F with an exponential backoff and notification on transient failure +pub fn retry_with_exponential_backoff(request_fn: F) -> Result +where + F: FnMut() -> Result>, +{ + let notify = |_err, dur| { + debug!( + "Failed to connect to stacks-node. Next attempt in {:?}", + dur + ); + }; + + let backoff_timer = backoff::ExponentialBackoffBuilder::new() + .with_initial_interval(Duration::from_millis(BACKOFF_INITIAL_INTERVAL)) + .with_max_interval(Duration::from_millis(BACKOFF_MAX_INTERVAL)) + .build(); + + backoff::retry_notify(backoff_timer, request_fn, notify).map_err(|_| ClientError::RetryTimeout) +} diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs new file mode 100644 index 0000000000..df7e413186 --- /dev/null +++ b/stacks-signer/src/client/stackerdb.rs @@ -0,0 +1,101 @@ +use hashbrown::HashMap; +use libsigner::{SignerSession, StackerDBSession}; +use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; +use slog::{slog_debug, slog_warn}; +use stacks_common::types::chainstate::StacksPrivateKey; +use stacks_common::{debug, warn}; +use wsts::net::{Message, Packet}; + +use super::ClientError; +use crate::client::retry_with_exponential_backoff; +use crate::config::Config; + +/// Temporary placeholder for the number of slots allocated to a stacker-db writer. This will be retrieved from the stacker-db instance in the future +/// See: https://github.com/stacks-network/stacks-blockchain/issues/3921 +/// Is equal to the number of message types +pub const SLOTS_PER_USER: u32 = 10; + +/// The StackerDB client for communicating with both .signers and .miners contracts +pub struct StackerDB { + /// The stacker-db session for the signer StackerDB + signers_stackerdb_session: StackerDBSession, + /// The private key used in all stacks node communications + stacks_private_key: StacksPrivateKey, + /// A map of a slot ID to last chunk version + slot_versions: HashMap, +} + +impl From<&Config> for StackerDB { + fn from(config: &Config) -> Self { + Self { + signers_stackerdb_session: StackerDBSession::new( + config.node_host, + config.stackerdb_contract_id.clone(), + ), + stacks_private_key: config.stacks_private_key, + slot_versions: HashMap::new(), + } + } +} + +impl StackerDB { + /// Sends messages to the stacker-db with an exponential backoff retry + pub fn send_message_with_retry( + &mut self, + id: u32, + message: Packet, + ) -> Result { + let message_bytes = bincode::serialize(&message)?; + let slot_id = slot_id(id, &message.msg); + + loop { + let slot_version = *self.slot_versions.entry(slot_id).or_insert(0) + 1; + let mut chunk = StackerDBChunkData::new(slot_id, slot_version, message_bytes.clone()); + chunk.sign(&self.stacks_private_key)?; + debug!("Sending a chunk to stackerdb!\n{:?}", chunk.clone()); + let send_request = || { + self.signers_stackerdb_session + .put_chunk(chunk.clone()) + .map_err(backoff::Error::transient) + }; + let chunk_ack: StackerDBChunkAckData = retry_with_exponential_backoff(send_request)?; + self.slot_versions.insert(slot_id, slot_version); + + if chunk_ack.accepted { + debug!("Chunk accepted by stackerdb: {:?}", chunk_ack); + return Ok(chunk_ack); + } else { + warn!("Chunk rejected by stackerdb: {:?}", chunk_ack); + } + if let Some(reason) = chunk_ack.reason { + // TODO: fix this jankiness. Update stackerdb to use an error code mapping instead of just a string + // See: https://github.com/stacks-network/stacks-blockchain/issues/3917 + if reason == "Data for this slot and version already exist" { + warn!("Failed to send message to stackerdb due to wrong version number {}. Incrementing and retrying...", slot_version); + } else { + warn!("Failed to send message to stackerdb: {}", reason); + return Err(ClientError::PutChunkRejected(reason)); + } + } + } + } +} + +/// Helper function to determine the slot ID for the provided stacker-db writer id and the message type +fn slot_id(id: u32, message: &Message) -> u32 { + let slot_id = match message { + Message::DkgBegin(_) => 0, + Message::DkgPrivateBegin(_) => 1, + Message::DkgEnd(_) => 2, + Message::DkgPublicShares(_) => 4, + Message::DkgPrivateShares(_) => 5, + Message::NonceRequest(_) => 6, + Message::NonceResponse(_) => 7, + Message::SignatureShareRequest(_) => 8, + Message::SignatureShareResponse(_) => 9, + }; + SLOTS_PER_USER * id + slot_id +} + +#[cfg(test)] +mod tests {} diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs new file mode 100644 index 0000000000..e1fbbb61cd --- /dev/null +++ b/stacks-signer/src/client/stacks_client.rs @@ -0,0 +1,600 @@ +use blockstack_lib::burnchains::Txid; +use blockstack_lib::chainstate::stacks::{ + StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth, + TransactionContractCall, TransactionPayload, TransactionPostConditionMode, + TransactionSpendingCondition, TransactionVersion, +}; +use clarity::vm::types::{QualifiedContractIdentifier, SequenceData}; +use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; +use serde_json::json; +use slog::slog_debug; +use stacks_common::codec::StacksMessageCodec; +use stacks_common::debug; +use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}; +use wsts::curve::point::Point; +use wsts::curve::scalar::Scalar; + +use crate::client::{retry_with_exponential_backoff, ClientError}; +use crate::config::Config; + +/// The Stacks signer client used to communicate with the stacks node +pub struct StacksClient { + /// The stacks address of the signer + stacks_address: StacksAddress, + /// The private key used in all stacks node communications + stacks_private_key: StacksPrivateKey, + /// The stacks node HTTP base endpoint + http_origin: String, + /// The types of transactions + tx_version: TransactionVersion, + /// The chain we are interacting with + chain_id: u32, + /// The Client used to make HTTP connects + stacks_node_client: reqwest::blocking::Client, + /// The pox contract ID + pox_contract_id: Option, +} + +impl From<&Config> for StacksClient { + fn from(config: &Config) -> Self { + Self { + stacks_private_key: config.stacks_private_key, + stacks_address: config.stacks_address, + http_origin: format!("http://{}", config.node_host), + tx_version: config.network.to_transaction_version(), + chain_id: config.network.to_chain_id(), + stacks_node_client: reqwest::blocking::Client::new(), + pox_contract_id: config.pox_contract_id.clone(), + } + } +} + +impl StacksClient { + /// Retrieve the current DKG aggregate public key + pub fn get_aggregate_public_key(&self) -> Result, ClientError> { + let reward_cycle = self.get_current_reward_cycle()?; + let function_name_str = "get-aggregate-public-key"; // FIXME: this may need to be modified to match .pox-4 + let function_name = ClarityName::try_from(function_name_str) + .map_err(|_| ClientError::InvalidClarityName(function_name_str.to_string()))?; + let (contract_addr, contract_name) = self.get_pox_contract()?; + let function_args = &[ClarityValue::UInt(reward_cycle as u128)]; + let contract_response_hex = self.read_only_contract_call_with_retry( + &contract_addr, + &contract_name, + &function_name, + function_args, + )?; + self.parse_aggregate_public_key(&contract_response_hex) + } + + /// Helper function to retrieve the current reward cycle number from the stacks node + fn get_current_reward_cycle(&self) -> Result { + let send_request = || { + self.stacks_node_client + .get(self.pox_path()) + .send() + .map_err(backoff::Error::transient) + }; + let response = retry_with_exponential_backoff(send_request)?; + if !response.status().is_success() { + return Err(ClientError::RequestFailure(response.status())); + } + let json_response = response.json::()?; + let entry = "current_cycle"; + json_response + .get(entry) + .and_then(|cycle: &serde_json::Value| cycle.get("id")) + .and_then(|id| id.as_u64()) + .ok_or_else(|| ClientError::InvalidJsonEntry(format!("{}.id", entry))) + } + + /// Helper function to retrieve the next possible nonce for the signer from the stacks node + #[allow(dead_code)] + fn get_next_possible_nonce(&self) -> Result { + //FIXME: use updated RPC call to get mempool nonces. Depends on https://github.com/stacks-network/stacks-blockchain/issues/4000 + todo!("Get the next possible nonce from the stacks node"); + } + + /// Helper function to retrieve the pox contract address and name from the stacks node + fn get_pox_contract(&self) -> Result<(StacksAddress, ContractName), ClientError> { + // Check if we have overwritten the pox contract ID in the config + if let Some(pox_contract) = self.pox_contract_id.clone() { + return Ok((pox_contract.issuer.into(), pox_contract.name)); + } + // TODO: we may want to cache the pox contract inside the client itself (calling this function once on init) + // https://github.com/stacks-network/stacks-blockchain/issues/4005 + let send_request = || { + self.stacks_node_client + .get(self.pox_path()) + .send() + .map_err(backoff::Error::transient) + }; + let response = retry_with_exponential_backoff(send_request)?; + if !response.status().is_success() { + return Err(ClientError::RequestFailure(response.status())); + } + let json_response = response.json::()?; + let entry = "contract_id"; + let contract_id_string = json_response + .get(entry) + .and_then(|id: &serde_json::Value| id.as_str()) + .ok_or_else(|| ClientError::InvalidJsonEntry(entry.to_string()))?; + let id = QualifiedContractIdentifier::parse(contract_id_string).unwrap(); + Ok((id.issuer.into(), id.name)) + } + + /// Helper function that attempts to deserialize a clarity hex string as the aggregate public key + fn parse_aggregate_public_key(&self, hex: &str) -> Result, ClientError> { + let public_key_clarity_value = ClarityValue::try_deserialize_hex_untyped(hex)?; + if let ClarityValue::Optional(optional_data) = public_key_clarity_value.clone() { + if let Some(ClarityValue::Sequence(SequenceData::Buffer(public_key))) = + optional_data.data.map(|boxed| *boxed) + { + if public_key.data.len() != 32 { + return Err(ClientError::MalformedClarityValue(public_key_clarity_value)); + } + let mut bytes = [0_u8; 32]; + bytes.copy_from_slice(&public_key.data); + Ok(Some(Point::from(Scalar::from(bytes)))) + } else { + Ok(None) + } + } else { + Err(ClientError::MalformedClarityValue(public_key_clarity_value)) + } + } + + /// Sends a transaction to the stacks node for a modifying contract call + #[allow(dead_code)] + fn transaction_contract_call( + &self, + contract_addr: &StacksAddress, + contract_name: ContractName, + function_name: ClarityName, + function_args: &[ClarityValue], + ) -> Result { + debug!("Making a contract call to {contract_addr}.{contract_name}..."); + let signed_tx = self.build_signed_transaction( + contract_addr, + contract_name, + function_name, + function_args, + )?; + self.submit_tx(&signed_tx) + } + + /// Helper function to create a stacks transaction for a modifying contract call + fn build_signed_transaction( + &self, + contract_addr: &StacksAddress, + contract_name: ContractName, + function_name: ClarityName, + function_args: &[ClarityValue], + ) -> Result { + let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { + address: *contract_addr, + contract_name, + function_name, + function_args: function_args.to_vec(), + }); + let public_key = StacksPublicKey::from_private(&self.stacks_private_key); + let tx_auth = TransactionAuth::Standard( + TransactionSpendingCondition::new_singlesig_p2pkh(public_key).ok_or( + ClientError::TransactionGenerationFailure(format!( + "Failed to create spending condition from public key: {}", + public_key.to_hex() + )), + )?, + ); + + let mut unsigned_tx = StacksTransaction::new(self.tx_version, tx_auth, tx_payload); + + // FIXME: Because signers are given priority, we can put down a tx fee of 0 + // https://github.com/stacks-network/stacks-blockchain/issues/4006 + // Note: if set to 0 now, will cause a failure (MemPoolRejection::FeeTooLow) + unsigned_tx.set_tx_fee(10_000); + unsigned_tx.set_origin_nonce(self.get_next_possible_nonce()?); + + unsigned_tx.anchor_mode = TransactionAnchorMode::Any; + unsigned_tx.post_condition_mode = TransactionPostConditionMode::Allow; + unsigned_tx.chain_id = self.chain_id; + + let mut tx_signer = StacksTransactionSigner::new(&unsigned_tx); + tx_signer + .sign_origin(&self.stacks_private_key) + .map_err(|e| ClientError::TransactionGenerationFailure(e.to_string()))?; + + tx_signer + .get_tx() + .ok_or(ClientError::TransactionGenerationFailure( + "Failed to generate transaction from a transaction signer".to_string(), + )) + } + + /// Helper function to submit a transaction to the Stacks node + fn submit_tx(&self, tx: &StacksTransaction) -> Result { + let txid = tx.txid(); + let tx = tx.serialize_to_vec(); + let send_request = || { + self.stacks_node_client + .post(self.transaction_path()) + .header("Content-Type", "application/octet-stream") + .body(tx.clone()) + .send() + .map_err(backoff::Error::transient) + }; + let response = retry_with_exponential_backoff(send_request)?; + if !response.status().is_success() { + return Err(ClientError::RequestFailure(response.status())); + } + Ok(txid) + } + + /// Makes a read only contract call to a stacks contract + pub fn read_only_contract_call_with_retry( + &self, + contract_addr: &StacksAddress, + contract_name: &ContractName, + function_name: &ClarityName, + function_args: &[ClarityValue], + ) -> Result { + debug!("Calling read-only function {}...", function_name); + let args = function_args + .iter() + .map(|arg| arg.serialize_to_hex()) + .collect::>(); + let body = + json!({"sender": self.stacks_address.to_string(), "arguments": args}).to_string(); + let path = self.read_only_path(contract_addr, contract_name, function_name); + let send_request = || { + self.stacks_node_client + .post(path.clone()) + .header("Content-Type", "application/json") + .body(body.clone()) + .send() + .map_err(backoff::Error::transient) + }; + let response = retry_with_exponential_backoff(send_request)?; + if !response.status().is_success() { + return Err(ClientError::RequestFailure(response.status())); + } + let response = response.json::()?; + if !response + .get("okay") + .map(|val| val.as_bool().unwrap_or(false)) + .unwrap_or(false) + { + let cause = response + .get("cause") + .ok_or(ClientError::InvalidJsonEntry("cause".to_string()))?; + return Err(ClientError::ReadOnlyFailure(format!( + "{}: {}", + function_name, cause + ))); + } + let result = response + .get("result") + .ok_or(ClientError::InvalidJsonEntry("result".to_string()))? + .as_str() + .ok_or_else(|| ClientError::ReadOnlyFailure("Expected string result.".to_string()))? + .to_string(); + Ok(result) + } + + fn pox_path(&self) -> String { + format!("{}/v2/pox", self.http_origin) + } + + fn transaction_path(&self) -> String { + format!("{}/v2/transactions", self.http_origin) + } + + fn read_only_path( + &self, + contract_addr: &StacksAddress, + contract_name: &ContractName, + function_name: &ClarityName, + ) -> String { + format!( + "{}/v2/contracts/call-read/{contract_addr}/{contract_name}/{function_name}", + self.http_origin + ) + } +} + +#[cfg(test)] +mod tests { + use std::io::{BufWriter, Read, Write}; + use std::net::{SocketAddr, TcpListener}; + use std::thread::spawn; + + use super::*; + use crate::client::ClientError; + + struct TestConfig { + mock_server: TcpListener, + client: StacksClient, + } + + impl TestConfig { + pub fn new() -> Self { + let mut config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); + + let mut mock_server_addr = SocketAddr::from(([127, 0, 0, 1], 0)); + // Ask the OS to assign a random port to listen on by passing 0 + let mock_server = TcpListener::bind(mock_server_addr).unwrap(); + + // Update the config to use this port + mock_server_addr.set_port(mock_server.local_addr().unwrap().port()); + config.node_host = mock_server_addr; + + let client = StacksClient::from(&config); + Self { + mock_server, + client, + } + } + } + + fn write_response(mock_server: TcpListener, bytes: &[u8]) -> [u8; 1024] { + debug!("Writing a response..."); + let mut request_bytes = [0u8; 1024]; + { + let mut stream = mock_server.accept().unwrap().0; + let _ = stream.read(&mut request_bytes).unwrap(); + stream.write_all(bytes).unwrap(); + } + request_bytes + } + + #[test] + fn read_only_contract_call_200_success() { + let config = TestConfig::new(); + let h = spawn(move || { + config.client.read_only_contract_call_with_retry( + &config.client.stacks_address, + &ContractName::try_from("contract-name").unwrap(), + &ClarityName::try_from("function-name").unwrap(), + &[], + ) + }); + write_response( + config.mock_server, + b"HTTP/1.1 200 OK\n\n{\"okay\":true,\"result\":\"0x070d0000000473425443\"}", + ); + let result = h.join().unwrap().unwrap(); + assert_eq!(result, "0x070d0000000473425443"); + } + + #[test] + fn read_only_contract_call_with_function_args_200_success() { + let config = TestConfig::new(); + let h = spawn(move || { + config.client.read_only_contract_call_with_retry( + &config.client.stacks_address, + &ContractName::try_from("contract-name").unwrap(), + &ClarityName::try_from("function-name").unwrap(), + &[ClarityValue::UInt(10_u128)], + ) + }); + write_response( + config.mock_server, + b"HTTP/1.1 200 OK\n\n{\"okay\":true,\"result\":\"0x070d0000000473425443\"}", + ); + let result = h.join().unwrap().unwrap(); + assert_eq!(result, "0x070d0000000473425443"); + } + + #[test] + fn read_only_contract_call_200_failure() { + let config = TestConfig::new(); + let h = spawn(move || { + config.client.read_only_contract_call_with_retry( + &config.client.stacks_address, + &ContractName::try_from("contract-name").unwrap(), + &ClarityName::try_from("function-name").unwrap(), + &[], + ) + }); + write_response( + config.mock_server, + b"HTTP/1.1 200 OK\n\n{\"okay\":false,\"cause\":\"Some reason\"}", + ); + let result = h.join().unwrap(); + assert!(matches!(result, Err(ClientError::ReadOnlyFailure(_)))); + } + + #[test] + fn read_only_contract_call_400_failure() { + let config = TestConfig::new(); + // Simulate a 400 Bad Request response + let h = spawn(move || { + config.client.read_only_contract_call_with_retry( + &config.client.stacks_address, + &ContractName::try_from("contract-name").unwrap(), + &ClarityName::try_from("function-name").unwrap(), + &[], + ) + }); + write_response(config.mock_server, b"HTTP/1.1 400 Bad Request\n\n"); + let result = h.join().unwrap(); + assert!(matches!( + result, + Err(ClientError::RequestFailure( + reqwest::StatusCode::BAD_REQUEST + )) + )); + } + + #[test] + fn read_only_contract_call_404_failure() { + let config = TestConfig::new(); + // Simulate a 400 Bad Request response + let h = spawn(move || { + config.client.read_only_contract_call_with_retry( + &config.client.stacks_address, + &ContractName::try_from("contract-name").unwrap(), + &ClarityName::try_from("function-name").unwrap(), + &[], + ) + }); + write_response(config.mock_server, b"HTTP/1.1 404 Not Found\n\n"); + let result = h.join().unwrap(); + assert!(matches!( + result, + Err(ClientError::RequestFailure(reqwest::StatusCode::NOT_FOUND)) + )); + } + + #[test] + fn pox_contract_success() { + let config = TestConfig::new(); + let h = spawn(move || config.client.get_pox_contract()); + write_response( + config.mock_server, + b"HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\"}", + ); + let (address, name) = h.join().unwrap().unwrap(); + assert_eq!( + (address.to_string().as_str(), name.to_string().as_str()), + ("ST000000000000000000002AMW42H", "pox-3") + ); + } + + #[test] + fn valid_reward_cycle_should_succeed() { + let config = TestConfig::new(); + let h = spawn(move || config.client.get_current_reward_cycle()); + write_response( + config.mock_server, + b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"id\":506,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":5690000000000,\"is_pox_active\":false}}", + ); + let current_cycle_id = h.join().unwrap().unwrap(); + assert_eq!(506, current_cycle_id); + } + + #[test] + fn invalid_reward_cycle_should_fail() { + let config = TestConfig::new(); + let h = spawn(move || config.client.get_current_reward_cycle()); + write_response( + config.mock_server, + b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"id\":\"fake id\", \"is_pox_active\":false}}", + ); + let res = h.join().unwrap(); + assert!(matches!(res, Err(ClientError::InvalidJsonEntry(_)))); + } + + #[test] + fn missing_reward_cycle_should_fail() { + let config = TestConfig::new(); + let h = spawn(move || config.client.get_current_reward_cycle()); + write_response( + config.mock_server, + b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"is_pox_active\":false}}", + ); + let res = h.join().unwrap(); + assert!(matches!(res, Err(ClientError::InvalidJsonEntry(_)))); + } + + #[test] + fn parse_valid_aggregate_public_key_should_succeed() { + let config = TestConfig::new(); + let clarity_value_hex = + "0x0a0200000020b8c8b0652cb2851a52374c7acd47181eb031e8fa5c62883f636e0d4fe695d6ca"; + let result = config + .client + .parse_aggregate_public_key(clarity_value_hex) + .unwrap(); + assert_eq!( + result.map(|point| point.to_string()), + Some("yzwdjwPz36Has1MSkg8JGwo38avvATkiTZvRiH1e5MLd".to_string()) + ); + + let clarity_value_hex = "0x09"; + let result = config + .client + .parse_aggregate_public_key(clarity_value_hex) + .unwrap(); + assert!(result.is_none()); + } + + #[test] + fn parse_invalid_aggregate_public_key_should_fail() { + let config = TestConfig::new(); + let clarity_value_hex = "0x00"; + let result = config.client.parse_aggregate_public_key(clarity_value_hex); + assert!(matches!( + result, + Err(ClientError::ClaritySerializationError(..)) + )); + // TODO: add further tests for malformed clarity values (an optional of any other type for example) + } + + #[ignore] + #[test] + fn transaction_contract_call_should_send_bytes_to_node() { + let config = TestConfig::new(); + let tx = config + .client + .build_signed_transaction( + &config.client.stacks_address, + ContractName::try_from("contract-name").unwrap(), + ClarityName::try_from("function-name").unwrap(), + &[], + ) + .unwrap(); + + let mut tx_bytes = [0u8; 1024]; + { + let mut tx_bytes_writer = BufWriter::new(&mut tx_bytes[..]); + tx.consensus_serialize(&mut tx_bytes_writer).unwrap(); + tx_bytes_writer.flush().unwrap(); + } + + let bytes_len = tx_bytes + .iter() + .enumerate() + .rev() + .find(|(_, &x)| x != 0) + .unwrap() + .0 + + 1; + + let tx_clone = tx.clone(); + let h = spawn(move || config.client.submit_tx(&tx_clone)); + + let request_bytes = write_response( + config.mock_server, + format!("HTTP/1.1 200 OK\n\n{}", tx.txid()).as_bytes(), + ); + let returned_txid = h.join().unwrap().unwrap(); + + assert_eq!(returned_txid, tx.txid()); + assert!( + request_bytes + .windows(bytes_len) + .any(|window| window == &tx_bytes[..bytes_len]), + "Request bytes did not contain the transaction bytes" + ); + } + + #[ignore] + #[test] + fn transaction_contract_call_should_succeed() { + let config = TestConfig::new(); + let h = spawn(move || { + config.client.transaction_contract_call( + &config.client.stacks_address, + ContractName::try_from("contract-name").unwrap(), + ClarityName::try_from("function-name").unwrap(), + &[], + ) + }); + write_response( + config.mock_server, + b"HTTP/1.1 200 OK\n\n4e99f99bc4a05437abb8c7d0c306618f45b203196498e2ebe287f10497124958", + ); + assert!(h.join().unwrap().is_ok()); + } +} diff --git a/stacks-signer/src/lib.rs b/stacks-signer/src/lib.rs index 2a14245b6c..e5b8350f5b 100644 --- a/stacks-signer/src/lib.rs +++ b/stacks-signer/src/lib.rs @@ -5,11 +5,11 @@ Usage documentation can be found in the [README](https://github.com/Trust-Machin */ /// The cli module for the signer binary pub mod cli; +/// The signer client for communicating with stackerdb/stacks nodes +pub mod client; /// The configuration module for the signer pub mod config; /// The primary runloop for the signer pub mod runloop; -/// The signer client for communicating with stackerdb/stacks nodes -pub mod stacks_client; /// Util functions pub mod utils; diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 4aef62a391..7337546a07 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -15,8 +15,8 @@ use wsts::state_machine::signer::Signer; use wsts::state_machine::{OperationResult, PublicKeys}; use wsts::v2; +use crate::client::{retry_with_exponential_backoff, ClientError, StackerDB, StacksClient}; use crate::config::Config; -use crate::stacks_client::{retry_with_exponential_backoff, ClientError, StacksClient}; /// Which operation to perform #[derive(PartialEq, Clone)] @@ -58,8 +58,10 @@ pub struct RunLoop { // TODO: update this to use frost_signer directly instead of the frost signing round // See: https://github.com/stacks-network/stacks-blockchain/issues/3913 pub signing_round: Signer, - /// The stacks client + /// The stacks node client pub stacks_client: StacksClient, + /// The stacker db client + pub stackerdb: StackerDB, /// Received Commands that need to be processed pub commands: VecDeque, /// The current state @@ -96,7 +98,7 @@ impl RunLoop { match self.coordinator.start_dkg_round() { Ok(msg) => { let ack = self - .stacks_client + .stackerdb .send_message_with_retry(self.signing_round.signer_id, msg); debug!("ACK: {:?}", ack); self.state = State::Dkg; @@ -122,7 +124,7 @@ impl RunLoop { { Ok(msg) => { let ack = self - .stacks_client + .stackerdb .send_message_with_retry(self.signing_round.signer_id, msg); debug!("ACK: {:?}", ack); self.state = State::Sign; @@ -266,11 +268,13 @@ impl From<&Config> for RunLoop> { config.signer_ids_public_keys.clone(), ); let stacks_client = StacksClient::from(config); + let stackerdb = StackerDB::from(config); RunLoop { event_timeout: config.event_timeout, coordinator, signing_round, stacks_client, + stackerdb, commands: VecDeque::new(), state: State::Uninitialized, } @@ -313,7 +317,7 @@ impl SignerRunLoop, RunLoopCommand> for Run ); for msg in outbound_messages { let ack = self - .stacks_client + .stackerdb .send_message_with_retry(self.signing_round.signer_id, msg); if let Ok(ack) = ack { debug!("ACK: {:?}", ack); diff --git a/stacks-signer/src/utils.rs b/stacks-signer/src/utils.rs index 5664fd7076..585ad73ceb 100644 --- a/stacks-signer/src/utils.rs +++ b/stacks-signer/src/utils.rs @@ -7,7 +7,7 @@ use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey}; use wsts::curve::ecdsa; use wsts::curve::scalar::Scalar; -use crate::stacks_client::SLOTS_PER_USER; +use crate::client::SLOTS_PER_USER; /// Helper function for building a signer config for each provided signer private key pub fn build_signer_config_tomls( From f12737dd2434fbf9c32e3b1d56097700e37717cb Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 5 Dec 2023 16:53:20 -0500 Subject: [PATCH 02/27] Add miners stackerdb and update cli Signed-off-by: Jacinta Ferrant --- stacks-signer/src/cli.rs | 23 +++++--- stacks-signer/src/client/mod.rs | 7 --- stacks-signer/src/client/stackerdb.rs | 14 +++-- stacks-signer/src/config.rs | 43 ++++++++++----- stacks-signer/src/main.rs | 48 +++++++++-------- stacks-signer/src/tests/conf/signer-0.toml | 3 +- stacks-signer/src/tests/conf/signer-1.toml | 3 +- stacks-signer/src/tests/conf/signer-2.toml | 3 +- stacks-signer/src/tests/conf/signer-3.toml | 3 +- stacks-signer/src/tests/conf/signer-4.toml | 3 +- stacks-signer/src/utils.rs | 15 +++--- testnet/stacks-node/src/tests/signer.rs | 62 ++++++++++++++++------ 12 files changed, 147 insertions(+), 80 deletions(-) diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs index ab0e6649a3..0e368ac4c8 100644 --- a/stacks-signer/src/cli.rs +++ b/stacks-signer/src/cli.rs @@ -125,19 +125,28 @@ pub struct RunDkgArgs { #[derive(Parser, Debug, Clone)] /// Arguments for the generate-files command pub struct GenerateFilesArgs { - /// The base arguments - #[clap(flatten)] - pub db_args: StackerDBArgs, + /// The Stacks node to connect to + #[arg(long)] + pub host: SocketAddr, + /// The signers stacker-db contract to use. Must be in the format of "STACKS_ADDRESS.CONTRACT_NAME" + #[arg(short, long, value_parser = parse_contract)] + pub signers_contract: QualifiedContractIdentifier, + /// The miners stacker-db contract to use. Must be in the format of "STACKS_ADDRESS.CONTRACT_NAME" + #[arg(short, long, value_parser = parse_contract)] + pub miners_contract: QualifiedContractIdentifier, #[arg( long, - required_unless_present = "private_keys", - conflicts_with = "private_keys" + required_unless_present = "signer_private_keys", + conflicts_with = "signer_private_keys" )] /// The number of signers to generate pub num_signers: Option, #[clap(long, value_name = "FILE")] - /// A path to a file containing a list of hexadecimal Stacks private keys - pub private_keys: Option, + /// A path to a file containing a list of hexadecimal Stacks private keys of the signers + pub signer_private_keys: Option, + /// The Stacks private key to use in hexademical format for the miner + #[arg(long, value_parser = parse_private_key)] + pub miner_private_key: StacksPrivateKey, #[arg(long)] /// The total number of key ids to distribute among the signers pub num_keys: u32, diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index c90473e64a..440586eb35 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -19,13 +19,6 @@ const BACKOFF_INITIAL_INTERVAL: u64 = 128; /// Backoff timer max interval in milliseconds const BACKOFF_MAX_INTERVAL: u64 = 16384; -/// Temporary placeholder for the number of slots allocated to a stacker-db writer. This will be retrieved from the stacker-db instance in the future -/// See: https://github.com/stacks-network/stacks-blockchain/issues/3921 -/// Is equal to the number of message types -pub const SIGNER_SLOTS_PER_USER: u32 = 10; -/// The number of miner slots available per miner -pub const MINER_SLOTS_PER_USER: u32 = 1; - #[derive(thiserror::Error, Debug)] /// Client error type pub enum ClientError { diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index df7e413186..776fa8455f 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -13,12 +13,16 @@ use crate::config::Config; /// Temporary placeholder for the number of slots allocated to a stacker-db writer. This will be retrieved from the stacker-db instance in the future /// See: https://github.com/stacks-network/stacks-blockchain/issues/3921 /// Is equal to the number of message types -pub const SLOTS_PER_USER: u32 = 10; +pub const SIGNER_SLOTS_PER_USER: u32 = 10; +/// The number of miner slots available per miner +pub const MINER_SLOTS_PER_USER: u32 = 1; /// The StackerDB client for communicating with both .signers and .miners contracts pub struct StackerDB { /// The stacker-db session for the signer StackerDB signers_stackerdb_session: StackerDBSession, + /// The stacker-db session for the .miners StackerDB + _miners_stackerdb_session: StackerDBSession, /// The private key used in all stacks node communications stacks_private_key: StacksPrivateKey, /// A map of a slot ID to last chunk version @@ -30,7 +34,11 @@ impl From<&Config> for StackerDB { Self { signers_stackerdb_session: StackerDBSession::new( config.node_host, - config.stackerdb_contract_id.clone(), + config.signers_stackerdb_contract_id.clone(), + ), + _miners_stackerdb_session: StackerDBSession::new( + config.node_host, + config.miners_stackerdb_contract_id.clone(), ), stacks_private_key: config.stacks_private_key, slot_versions: HashMap::new(), @@ -94,7 +102,7 @@ fn slot_id(id: u32, message: &Message) -> u32 { Message::SignatureShareRequest(_) => 8, Message::SignatureShareResponse(_) => 9, }; - SLOTS_PER_USER * id + slot_id + SIGNER_SLOTS_PER_USER * id + slot_id } #[cfg(test)] diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 190a6f82c8..c298ead275 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -99,9 +99,11 @@ pub struct Config { pub node_host: SocketAddr, /// endpoint to the stackerdb receiver pub endpoint: SocketAddr, - /// smart contract that controls the target stackerdb - pub stackerdb_contract_id: QualifiedContractIdentifier, - /// smart contract that controls the target stackerdb + /// smart contract that controls the target signers' stackerdb + pub signers_stackerdb_contract_id: QualifiedContractIdentifier, + /// smart contract that controls the target .miners stackerdb + pub miners_stackerdb_contract_id: QualifiedContractIdentifier, + /// the pox contract identifier to use pub pox_contract_id: Option, /// The Scalar representation of the private key for signer communication pub message_private_key: Scalar, @@ -144,8 +146,10 @@ struct RawConfigFile { /// endpoint to stackerdb receiver pub endpoint: String, // FIXME: these contract's should go away in non testing scenarios. Make them both optionals. - /// Stacker db contract identifier - pub stackerdb_contract_id: String, + /// Signers' Stacker db contract identifier + pub signers_stackerdb_contract_id: String, + /// Miners' Stacker db contract identifier + pub miners_stackerdb_contract_id: String, /// pox contract identifier pub pox_contract_id: Option, /// the 32 byte ECDSA private key used to sign blocks, chunks, and transactions @@ -219,13 +223,25 @@ impl TryFrom for Config { raw_data.endpoint.clone(), ))?; - let stackerdb_contract_id = - QualifiedContractIdentifier::parse(&raw_data.stackerdb_contract_id).map_err(|_| { - ConfigError::BadField( - "stackerdb_contract_id".to_string(), - raw_data.stackerdb_contract_id, - ) - })?; + let signers_stackerdb_contract_id = QualifiedContractIdentifier::parse( + &raw_data.signers_stackerdb_contract_id, + ) + .map_err(|_| { + ConfigError::BadField( + "signers_stackerdb_contract_id".to_string(), + raw_data.signers_stackerdb_contract_id, + ) + })?; + + let miners_stackerdb_contract_id = QualifiedContractIdentifier::parse( + &raw_data.miners_stackerdb_contract_id, + ) + .map_err(|_| { + ConfigError::BadField( + "miners_stackerdb_contract_id".to_string(), + raw_data.miners_stackerdb_contract_id, + ) + })?; let pox_contract_id = if let Some(id) = raw_data.pox_contract_id.as_ref() { Some(QualifiedContractIdentifier::parse(id).map_err(|_| { @@ -288,7 +304,8 @@ impl TryFrom for Config { Ok(Self { node_host, endpoint, - stackerdb_contract_id, + signers_stackerdb_contract_id, + miners_stackerdb_contract_id, pox_contract_id, message_private_key, stacks_private_key, diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 4f6c762c1e..1dc2290b10 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -47,6 +47,7 @@ use stacks_signer::cli::{ Cli, Command, GenerateFilesArgs, GetChunkArgs, GetLatestChunkArgs, PutChunkArgs, RunDkgArgs, SignArgs, StackerDBArgs, }; +use stacks_signer::client::{MINER_SLOTS_PER_USER, SIGNER_SLOTS_PER_USER}; use stacks_signer::config::{Config, Network}; use stacks_signer::runloop::{RunLoop, RunLoopCommand}; use stacks_signer::utils::{build_signer_config_tomls, build_stackerdb_contract}; @@ -87,7 +88,7 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let config = Config::try_from(path).unwrap(); let (cmd_send, cmd_recv) = channel(); let (res_send, res_recv) = channel(); - let ev = StackerDBEventReceiver::new(vec![config.stackerdb_contract_id.clone()]); + let ev = StackerDBEventReceiver::new(vec![config.signers_stackerdb_contract_id.clone()]); let runloop: RunLoop> = RunLoop::from(&config); let mut signer: Signer< RunLoopCommand, @@ -247,7 +248,7 @@ fn handle_run(args: RunDkgArgs) { fn handle_generate_files(args: GenerateFilesArgs) { debug!("Generating files..."); - let signer_stacks_private_keys = if let Some(path) = args.private_keys { + let signer_stacks_private_keys = if let Some(path) = args.signer_private_keys { let file = File::open(&path).unwrap(); let reader = io::BufReader::new(file); @@ -274,36 +275,37 @@ fn handle_generate_files(args: GenerateFilesArgs) { .iter() .map(|key| to_addr(key, &args.network)) .collect::>(); - // Build the stackerdb contract - let stackerdb_contract = build_stackerdb_contract(&signer_stacks_addresses); + let miner_stacks_address = to_addr(&args.miner_private_key, &args.network); + // Build the signer and miner stackerdb contract + let signer_stackerdb_contract = + build_stackerdb_contract(&signer_stacks_addresses, SIGNER_SLOTS_PER_USER); + let miner_stackerdb_contract = + build_stackerdb_contract(&[miner_stacks_address], MINER_SLOTS_PER_USER); + write_file(&args.dir, "signers.clar", &signer_stackerdb_contract); + write_file(&args.dir, "miners.clar", &miner_stackerdb_contract); + let signer_config_tomls = build_signer_config_tomls( &signer_stacks_private_keys, args.num_keys, - &args.db_args.host.to_string(), - &args.db_args.contract.to_string(), + &args.host.to_string(), + &args.signers_contract.to_string(), + &args.miners_contract.to_string(), None, args.timeout.map(Duration::from_millis), ); debug!("Built {:?} signer config tomls.", signer_config_tomls.len()); for (i, file_contents) in signer_config_tomls.iter().enumerate() { - let signer_conf_path = args.dir.join(format!("signer-{}.toml", i)); - let signer_conf_filename = signer_conf_path.to_str().unwrap(); - let mut signer_conf_file = File::create(signer_conf_filename).unwrap(); - signer_conf_file - .write_all(file_contents.as_bytes()) - .unwrap(); - println!("Created signer config toml file: {}", signer_conf_filename); + write_file(&args.dir, &format!("signer-{}.toml", i), file_contents); } - let stackerdb_contract_path = args.dir.join("stackerdb.clar"); - let stackerdb_contract_filename = stackerdb_contract_path.to_str().unwrap(); - let mut stackerdb_contract_file = File::create(stackerdb_contract_filename).unwrap(); - stackerdb_contract_file - .write_all(stackerdb_contract.as_bytes()) - .unwrap(); - println!( - "Created stackerdb clarity contract: {}", - stackerdb_contract_filename - ); +} + +/// Helper function for writing the given contents to filename in the given directory +fn write_file(dir: &PathBuf, filename: &str, contents: &str) { + let file_path = dir.join(filename); + let filename = file_path.to_str().unwrap(); + let mut file = File::create(filename).unwrap(); + file.write_all(contents.as_bytes()).unwrap(); + println!("Created file: {}", filename); } fn main() { diff --git a/stacks-signer/src/tests/conf/signer-0.toml b/stacks-signer/src/tests/conf/signer-0.toml index ee510d563e..226a30eb7b 100644 --- a/stacks-signer/src/tests/conf/signer-0.toml +++ b/stacks-signer/src/tests/conf/signer-0.toml @@ -4,7 +4,8 @@ stacks_private_key = "69be0e68947fa7128702761151dc8d9b39ee1401e547781bb2ec3e5b4e node_host = "127.0.0.1:20443" endpoint = "localhost:30000" network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 0 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-1.toml b/stacks-signer/src/tests/conf/signer-1.toml index 73d5cb6a69..e3f6f68cbd 100644 --- a/stacks-signer/src/tests/conf/signer-1.toml +++ b/stacks-signer/src/tests/conf/signer-1.toml @@ -4,7 +4,8 @@ stacks_private_key = "fd5a538e8548e9d6a4a4060a43d0142356df022a4b8fd8ed4a7d066382 node_host = "127.0.0.1:20443" endpoint = "localhost:30001" network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 1 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-2.toml b/stacks-signer/src/tests/conf/signer-2.toml index 7ff263940d..0140dadad0 100644 --- a/stacks-signer/src/tests/conf/signer-2.toml +++ b/stacks-signer/src/tests/conf/signer-2.toml @@ -4,7 +4,8 @@ stacks_private_key = "74e8e8550a5210b89461128c600e4bf611d1553e6809308bc012dbb0fb node_host = "127.0.0.1:20443" endpoint = "localhost:30002" network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 2 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-3.toml b/stacks-signer/src/tests/conf/signer-3.toml index e7ac219a40..8cc8889f52 100644 --- a/stacks-signer/src/tests/conf/signer-3.toml +++ b/stacks-signer/src/tests/conf/signer-3.toml @@ -4,7 +4,8 @@ stacks_private_key = "803fa7b9c8a39ed368f160b3dcbfaa8f677fc157ffbccb46ee3e4a32a3 node_host = "127.0.0.1:20443" endpoint = "localhost:30003" network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 3 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-4.toml b/stacks-signer/src/tests/conf/signer-4.toml index c2eb3f37d0..999e066a09 100644 --- a/stacks-signer/src/tests/conf/signer-4.toml +++ b/stacks-signer/src/tests/conf/signer-4.toml @@ -4,7 +4,8 @@ stacks_private_key = "1bfdf386114aacf355fe018a1ec7ac728fa05ca20a6131a70f686291bb node_host = "127.0.0.1:20443" endpoint = "localhost:30004" network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 4 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/utils.rs b/stacks-signer/src/utils.rs index 585ad73ceb..6011a3a170 100644 --- a/stacks-signer/src/utils.rs +++ b/stacks-signer/src/utils.rs @@ -7,14 +7,13 @@ use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey}; use wsts::curve::ecdsa; use wsts::curve::scalar::Scalar; -use crate::client::SLOTS_PER_USER; - /// Helper function for building a signer config for each provided signer private key pub fn build_signer_config_tomls( signer_stacks_private_keys: &[StacksPrivateKey], num_keys: u32, node_host: &str, - stackerdb_contract_id: &str, + signers_stackerdb_contract_id: &str, + miners_stackerdb_contract_id: &str, pox_contract_id: Option<&str>, timeout: Option, ) -> Vec { @@ -74,7 +73,8 @@ stacks_private_key = "{stacks_private_key}" node_host = "{node_host}" endpoint = "{endpoint}" network = "testnet" -stackerdb_contract_id = "{stackerdb_contract_id}" +signers_stackerdb_contract_id = "{signers_stackerdb_contract_id}" +miners_stackerdb_contract_id = "{miners_stackerdb_contract_id}" signer_id = {id} {signers_array} "# @@ -105,7 +105,10 @@ pox_contract_id = "{pox_contract_id}" } /// Helper function for building a stackerdb contract from the provided signer stacks addresses -pub fn build_stackerdb_contract(signer_stacks_addresses: &[StacksAddress]) -> String { +pub fn build_stackerdb_contract( + signer_stacks_addresses: &[StacksAddress], + slots_per_user: u32, +) -> String { let mut stackerdb_contract = String::new(); // " stackerdb_contract += " ;; stacker DB\n"; stackerdb_contract += " (define-read-only (stackerdb-get-signer-slots)\n"; @@ -115,7 +118,7 @@ pub fn build_stackerdb_contract(signer_stacks_addresses: &[StacksAddress]) -> St stackerdb_contract += format!(" signer: '{},\n", signer_stacks_address).as_str(); stackerdb_contract += - format!(" num-slots: u{}\n", SLOTS_PER_USER).as_str(); + format!(" num-slots: u{}\n", slots_per_user).as_str(); stackerdb_contract += " }\n"; } stackerdb_contract += " )))\n"; diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index bb7f3d6446..933e3e0c6f 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -6,6 +6,7 @@ use clarity::vm::types::QualifiedContractIdentifier; use libsigner::{RunningSigner, Signer, StackerDBEventReceiver}; use stacks::chainstate::stacks::StacksPrivateKey; use stacks_common::types::chainstate::StacksAddress; +use stacks_signer::client::{MINER_SLOTS_PER_USER, SIGNER_SLOTS_PER_USER}; use stacks_signer::config::Config as SignerConfig; use stacks_signer::runloop::RunLoopCommand; use stacks_signer::utils::{build_signer_config_tomls, build_stackerdb_contract}; @@ -38,7 +39,10 @@ fn spawn_signer( sender: Sender>, ) -> RunningSigner> { let config = stacks_signer::config::Config::load_from_str(data).unwrap(); - let ev = StackerDBEventReceiver::new(vec![config.stackerdb_contract_id.clone()]); + let ev = StackerDBEventReceiver::new(vec![ + config.miners_stackerdb_contract_id.clone(), + config.signers_stackerdb_contract_id.clone(), + ]); let runloop: stacks_signer::runloop::RunLoop> = stacks_signer::runloop::RunLoop::from(&config); let mut signer: Signer< @@ -61,8 +65,10 @@ fn setup_stx_btc_node( num_signers: u32, signer_stacks_private_keys: &[StacksPrivateKey], publisher_private_key: &StacksPrivateKey, - stackerdb_contract: &str, - stackerdb_contract_id: &QualifiedContractIdentifier, + signers_stackerdb_contract: &str, + signers_stackerdb_contract_id: &QualifiedContractIdentifier, + miners_stackerdb_contract: &str, + miners_stackerdb_contract_id: &QualifiedContractIdentifier, pox_contract: &str, pox_contract_id: &QualifiedContractIdentifier, signer_config_tomls: &Vec, @@ -91,7 +97,12 @@ fn setup_stx_btc_node( } conf.initial_balances.append(&mut initial_balances); - conf.node.stacker_dbs.push(stackerdb_contract_id.clone()); + conf.node + .stacker_dbs + .push(signers_stackerdb_contract_id.clone()); + conf.node + .stacker_dbs + .push(miners_stackerdb_contract_id.clone()); info!("Make new BitcoinCoreController"); let mut btcd_controller = BitcoinCoreController::new(conf.clone()); @@ -143,13 +154,23 @@ fn setup_stx_btc_node( ); submit_tx(&http_origin, &tx); - info!("Send stacker-db contract-publish..."); + info!("Send signers stacker-db contract-publish..."); let tx = make_contract_publish( publisher_private_key, 1, tx_fee, - &stackerdb_contract_id.name, - stackerdb_contract, + &signers_stackerdb_contract_id.name, + signers_stackerdb_contract, + ); + submit_tx(&http_origin, &tx); + + info!("Send miners stacker-db contract-publish..."); + let tx = make_contract_publish( + publisher_private_key, + 2, + tx_fee, + &miners_stackerdb_contract_id.name, + miners_stackerdb_contract, ); submit_tx(&http_origin, &tx); @@ -211,6 +232,8 @@ fn test_stackerdb_dkg() { .iter() .map(to_addr) .collect::>(); + let miner_private_key = StacksPrivateKey::new(); + let miner_stacks_address = to_addr(&miner_private_key); // Setup the neon node let (mut conf, _) = neon_integration_test_conf(); @@ -219,19 +242,24 @@ fn test_stackerdb_dkg() { let pox_contract = build_pox_contract(num_signers); let pox_contract_id = QualifiedContractIdentifier::new(to_addr(&publisher_private_key).into(), "pox-4".into()); - // Build the stackerdb contract - let stackerdb_contract = build_stackerdb_contract(&signer_stacks_addresses); - let stacker_db_contract_id = QualifiedContractIdentifier::new( - to_addr(&publisher_private_key).into(), - "hello-world".into(), - ); + // Build the stackerdb contracts + let signers_stackerdb_contract = + build_stackerdb_contract(&signer_stacks_addresses, SIGNER_SLOTS_PER_USER); + let signers_stacker_db_contract_id = + QualifiedContractIdentifier::new(to_addr(&publisher_private_key).into(), "signers".into()); + + let miners_stackerdb_contract = + build_stackerdb_contract(&[miner_stacks_address], MINER_SLOTS_PER_USER); + let miners_stacker_db_contract_id = + QualifiedContractIdentifier::new(to_addr(&publisher_private_key).into(), "miners".into()); // Setup the signer and coordinator configurations let signer_configs = build_signer_config_tomls( &signer_stacks_private_keys, num_keys, &conf.node.rpc_bind, - &stacker_db_contract_id.to_string(), + &signers_stacker_db_contract_id.to_string(), + &miners_stacker_db_contract_id.to_string(), Some(&pox_contract_id.to_string()), Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. ); @@ -270,8 +298,10 @@ fn test_stackerdb_dkg() { num_signers, &signer_stacks_private_keys, &publisher_private_key, - &stackerdb_contract, - &stacker_db_contract_id, + &signers_stackerdb_contract, + &signers_stacker_db_contract_id, + &miners_stackerdb_contract, + &miners_stacker_db_contract_id, &pox_contract, &pox_contract_id, &signer_configs, From 609830569fe925afdba8b82bf86571c21097600a Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 6 Dec 2023 10:04:28 -0500 Subject: [PATCH 03/27] Seperate miner and signer events Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/stackerdb.rs | 15 +++++- stacks-signer/src/main.rs | 4 +- stacks-signer/src/runloop.rs | 74 ++++++++++++++++----------- 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 776fa8455f..ac3870b0c2 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -1,3 +1,4 @@ +use clarity::vm::types::QualifiedContractIdentifier; use hashbrown::HashMap; use libsigner::{SignerSession, StackerDBSession}; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; @@ -22,7 +23,7 @@ pub struct StackerDB { /// The stacker-db session for the signer StackerDB signers_stackerdb_session: StackerDBSession, /// The stacker-db session for the .miners StackerDB - _miners_stackerdb_session: StackerDBSession, + miners_stackerdb_session: StackerDBSession, /// The private key used in all stacks node communications stacks_private_key: StacksPrivateKey, /// A map of a slot ID to last chunk version @@ -36,7 +37,7 @@ impl From<&Config> for StackerDB { config.node_host, config.signers_stackerdb_contract_id.clone(), ), - _miners_stackerdb_session: StackerDBSession::new( + miners_stackerdb_session: StackerDBSession::new( config.node_host, config.miners_stackerdb_contract_id.clone(), ), @@ -87,6 +88,16 @@ impl StackerDB { } } } + + /// Retrieve the miner contract id + pub fn miners_contract_id(&self) -> &QualifiedContractIdentifier { + &self.miners_stackerdb_session.stackerdb_contract_id + } + + /// Retrieve the signer contract id + pub fn signers_contract_id(&self) -> &QualifiedContractIdentifier { + &self.signers_stackerdb_session.stackerdb_contract_id + } } /// Helper function to determine the slot ID for the provided stacker-db writer id and the message type diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 1dc2290b10..a3270f8fac 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -29,7 +29,7 @@ extern crate toml; use std::fs::File; use std::io::{self, BufRead, Write}; use std::net::SocketAddr; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; @@ -300,7 +300,7 @@ fn handle_generate_files(args: GenerateFilesArgs) { } /// Helper function for writing the given contents to filename in the given directory -fn write_file(dir: &PathBuf, filename: &str, contents: &str) { +fn write_file(dir: &Path, filename: &str, contents: &str) { let file_path = dir.join(filename); let filename = file_path.to_str().unwrap(); let mut file = File::create(filename).unwrap(); diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 7337546a07..f8c7a0fef7 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -166,11 +166,16 @@ impl RunLoop { } } - /// Process the event as both a signer and a coordinator - fn process_event( + /// Process the event as a miner message from the miner stacker-db + fn process_event_miner( &mut self, - event: &StackerDBChunksEvent, + _event: &StackerDBChunksEvent, ) -> (Vec, Vec) { + todo!("Process miner event") + } + + /// Process the event as a signer message from the signer stacker-db + fn process_event_signer(&mut self, event: &StackerDBChunksEvent) -> Vec { // Determine the current coordinator id and public key for verification let (_coordinator_id, coordinator_public_key) = calculate_coordinator(&self.signing_round.public_keys); @@ -196,15 +201,30 @@ impl RunLoop { vec![] }); // Next process the message as the coordinator - let (messages, results) = self + let (messages, operation_results) = self .coordinator .process_inbound_messages(&inbound_messages) .unwrap_or_else(|e| { - error!("Failed to process inbound messages as a coordinator: {e}"); + error!("Failed to process inbound messages as a signer: {e}"); (vec![], vec![]) }); + outbound_messages.extend(messages); - (outbound_messages, results) + debug!( + "Sending {} messages to other stacker-db instances.", + outbound_messages.len() + ); + for msg in outbound_messages { + let ack = self + .stackerdb + .send_message_with_retry(self.signing_round.signer_id, msg); + if let Ok(ack) = ack { + debug!("ACK: {:?}", ack); + } else { + warn!("Failed to send message to stacker-db instance: {:?}", ack); + } + } + operation_results } } @@ -310,32 +330,26 @@ impl SignerRunLoop, RunLoopCommand> for Run } // Process any arrived events if let Some(event) = event { - let (outbound_messages, operation_results) = self.process_event(&event); - debug!( - "Sending {} messages to other stacker-db instances.", - outbound_messages.len() - ); - for msg in outbound_messages { - let ack = self - .stackerdb - .send_message_with_retry(self.signing_round.signer_id, msg); - if let Ok(ack) = ack { - debug!("ACK: {:?}", ack); - } else { - warn!("Failed to send message to stacker-db instance: {:?}", ack); - } - } - - let nmb_results = operation_results.len(); - if nmb_results > 0 { - // We finished our command. Update the state - self.state = State::Idle; - match res.send(operation_results) { - Ok(_) => debug!("Successfully sent {} operation result(s)", nmb_results), - Err(e) => { - warn!("Failed to send operation results: {:?}", e); + if event.contract_id == *self.stackerdb.miners_contract_id() { + self.process_event_miner(&event); + } else if event.contract_id == *self.stackerdb.signers_contract_id() { + let operation_results = self.process_event_signer(&event); + let nmb_results = operation_results.len(); + if nmb_results > 0 { + // We finished our command. Update the state + self.state = State::Idle; + match res.send(operation_results) { + Ok(_) => debug!("Successfully sent {} operation result(s)", nmb_results), + Err(e) => { + warn!("Failed to send operation results: {:?}", e); + } } } + } else { + warn!( + "Received event from unknown contract ID: {}", + event.contract_id + ); } } // The process the next command From a10dffa0bdf0a77f4b614b7311d988386411ceae Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 6 Dec 2023 10:39:58 -0500 Subject: [PATCH 04/27] Add block specific slot and StackerDBMessage type to stackerdb.rs Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/mod.rs | 3 +- stacks-signer/src/client/stackerdb.rs | 80 ++++++++++++++++++++------- stacks-signer/src/runloop.rs | 6 +- 3 files changed, 66 insertions(+), 23 deletions(-) diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index 440586eb35..fcc614c6e3 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -12,6 +12,7 @@ use libstackerdb::Error as StackerDBError; use slog::slog_debug; pub use stackerdb::*; pub use stacks_client::*; +use stacks_common::codec::Error as CodecError; use stacks_common::debug; /// Backoff timer initial interval in milliseconds @@ -24,7 +25,7 @@ const BACKOFF_MAX_INTERVAL: u64 = 16384; pub enum ClientError { /// An error occurred serializing the message #[error("Unable to serialize stacker-db message: {0}")] - StackerDBSerializationError(#[from] bincode::Error), + StackerDBSerializationError(#[from] CodecError), /// Failed to sign stacker-db chunk #[error("Failed to sign stacker-db chunk: {0}")] FailToSign(#[from] StackerDBError), diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index ac3870b0c2..8e776e935f 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -1,8 +1,10 @@ +use blockstack_lib::chainstate::nakamoto::NakamotoBlock; use clarity::vm::types::QualifiedContractIdentifier; use hashbrown::HashMap; use libsigner::{SignerSession, StackerDBSession}; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; use slog::{slog_debug, slog_warn}; +use stacks_common::codec::{Error as CodecError, StacksMessageCodec}; use stacks_common::types::chainstate::StacksPrivateKey; use stacks_common::{debug, warn}; use wsts::net::{Message, Packet}; @@ -18,6 +20,62 @@ pub const SIGNER_SLOTS_PER_USER: u32 = 10; /// The number of miner slots available per miner pub const MINER_SLOTS_PER_USER: u32 = 1; +// The slot IDS for each message type +const DKG_BEGIN_SLOT_ID: u32 = 0; +const DKG_PRIVATE_BEGIN_SLOT_ID: u32 = 1; +const DKG_END_SLOT_ID: u32 = 2; +const DKG_PUBLIC_SHARES_SLOT_ID: u32 = 3; +const DKG_PRIVATE_SHARES_SLOT_ID: u32 = 4; +const NONCE_REQUEST_SLOT_ID: u32 = 5; +const NONCE_RESPONSE_SLOT_ID: u32 = 6; +const SIGNATURE_SHARE_REQUEST_SLOT_ID: u32 = 7; +const SIGNATURE_SHARE_RESPONSE_SLOT_ID: u32 = 8; +const BLOCK_SLOT_ID: u32 = 9; + +/// The StackerDB messages that can be sent through the .signers contract +pub enum StackerDBMessage { + /// The latest Nakamoto block for miners to observe + Block(NakamotoBlock), + /// DKG and Signing round data for other signers to observe + Packet(Packet), +} + +impl From for StackerDBMessage { + fn from(packet: Packet) -> Self { + Self::Packet(packet) + } +} + +impl StacksMessageCodec for StackerDBMessage { + fn consensus_serialize(&self, _fd: &mut W) -> Result<(), CodecError> { + todo!() + } + + fn consensus_deserialize(_fd: &mut R) -> Result { + todo!() + } +} + +impl StackerDBMessage { + /// Helper function to determine the slot ID for the provided stacker-db writer id + pub fn slot_id(&self, id: u32) -> u32 { + let slot_id = match self { + StackerDBMessage::Packet(packet) => match packet.msg { + Message::DkgBegin(_) => DKG_BEGIN_SLOT_ID, + Message::DkgPrivateBegin(_) => DKG_PRIVATE_BEGIN_SLOT_ID, + Message::DkgEnd(_) => DKG_END_SLOT_ID, + Message::DkgPublicShares(_) => DKG_PUBLIC_SHARES_SLOT_ID, + Message::DkgPrivateShares(_) => DKG_PRIVATE_SHARES_SLOT_ID, + Message::NonceRequest(_) => NONCE_REQUEST_SLOT_ID, + Message::NonceResponse(_) => NONCE_RESPONSE_SLOT_ID, + Message::SignatureShareRequest(_) => SIGNATURE_SHARE_REQUEST_SLOT_ID, + Message::SignatureShareResponse(_) => SIGNATURE_SHARE_RESPONSE_SLOT_ID, + }, + Self::Block(_block) => BLOCK_SLOT_ID, + }; + SIGNER_SLOTS_PER_USER * id + slot_id + } +} /// The StackerDB client for communicating with both .signers and .miners contracts pub struct StackerDB { /// The stacker-db session for the signer StackerDB @@ -52,10 +110,10 @@ impl StackerDB { pub fn send_message_with_retry( &mut self, id: u32, - message: Packet, + message: StackerDBMessage, ) -> Result { - let message_bytes = bincode::serialize(&message)?; - let slot_id = slot_id(id, &message.msg); + let message_bytes = message.serialize_to_vec(); + let slot_id = message.slot_id(id); loop { let slot_version = *self.slot_versions.entry(slot_id).or_insert(0) + 1; @@ -100,21 +158,5 @@ impl StackerDB { } } -/// Helper function to determine the slot ID for the provided stacker-db writer id and the message type -fn slot_id(id: u32, message: &Message) -> u32 { - let slot_id = match message { - Message::DkgBegin(_) => 0, - Message::DkgPrivateBegin(_) => 1, - Message::DkgEnd(_) => 2, - Message::DkgPublicShares(_) => 4, - Message::DkgPrivateShares(_) => 5, - Message::NonceRequest(_) => 6, - Message::NonceResponse(_) => 7, - Message::SignatureShareRequest(_) => 8, - Message::SignatureShareResponse(_) => 9, - }; - SIGNER_SLOTS_PER_USER * id + slot_id -} - #[cfg(test)] mod tests {} diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index f8c7a0fef7..7f36cc0955 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -99,7 +99,7 @@ impl RunLoop { Ok(msg) => { let ack = self .stackerdb - .send_message_with_retry(self.signing_round.signer_id, msg); + .send_message_with_retry(self.signing_round.signer_id, msg.into()); debug!("ACK: {:?}", ack); self.state = State::Dkg; true @@ -125,7 +125,7 @@ impl RunLoop { Ok(msg) => { let ack = self .stackerdb - .send_message_with_retry(self.signing_round.signer_id, msg); + .send_message_with_retry(self.signing_round.signer_id, msg.into()); debug!("ACK: {:?}", ack); self.state = State::Sign; true @@ -217,7 +217,7 @@ impl RunLoop { for msg in outbound_messages { let ack = self .stackerdb - .send_message_with_retry(self.signing_round.signer_id, msg); + .send_message_with_retry(self.signing_round.signer_id, msg.into()); if let Ok(ack) = ack { debug!("ACK: {:?}", ack); } else { From 6dee337881c101711cd6d63c2e238d7228c56496 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 6 Dec 2023 14:24:11 -0500 Subject: [PATCH 05/27] Delete stale stacks_client.rs Signed-off-by: Jacinta Ferrant --- stacks-signer/src/stacks_client.rs | 754 ----------------------------- 1 file changed, 754 deletions(-) delete mode 100644 stacks-signer/src/stacks_client.rs diff --git a/stacks-signer/src/stacks_client.rs b/stacks-signer/src/stacks_client.rs deleted file mode 100644 index cc70a0b8ce..0000000000 --- a/stacks-signer/src/stacks_client.rs +++ /dev/null @@ -1,754 +0,0 @@ -use std::time::Duration; - -use bincode::Error as BincodeError; -use blockstack_lib::burnchains::Txid; -use blockstack_lib::chainstate::stacks::{ - StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth, - TransactionContractCall, TransactionPayload, TransactionPostConditionMode, - TransactionSpendingCondition, TransactionVersion, -}; -use clarity::vm::types::serialization::SerializationError; -use clarity::vm::types::{QualifiedContractIdentifier, SequenceData}; -use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; -use hashbrown::HashMap; -use libsigner::{RPCError, SignerSession, StackerDBSession}; -use libstackerdb::{Error as StackerDBError, StackerDBChunkAckData, StackerDBChunkData}; -use serde_json::json; -use slog::{slog_debug, slog_warn}; -use stacks_common::codec::StacksMessageCodec; -use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}; -use stacks_common::{debug, warn}; -use wsts::curve::point::Point; -use wsts::curve::scalar::Scalar; -use wsts::net::{Message, Packet}; - -use crate::config::Config; - -/// Backoff timer initial interval in milliseconds -const BACKOFF_INITIAL_INTERVAL: u64 = 128; -/// Backoff timer max interval in milliseconds -const BACKOFF_MAX_INTERVAL: u64 = 16384; - -/// Temporary placeholder for the number of slots allocated to a stacker-db writer. This will be retrieved from the stacker-db instance in the future -/// See: https://github.com/stacks-network/stacks-blockchain/issues/3921 -/// Is equal to the number of message types -pub const SLOTS_PER_USER: u32 = 10; - -#[derive(thiserror::Error, Debug)] -/// Client error type -pub enum ClientError { - /// An error occurred serializing the message - #[error("Unable to serialize stacker-db message: {0}")] - StackerDBSerializationError(#[from] BincodeError), - /// Failed to sign stacker-db chunk - #[error("Failed to sign stacker-db chunk: {0}")] - FailToSign(#[from] StackerDBError), - /// Failed to write to stacker-db due to RPC error - #[error("Failed to write to stacker-db instance: {0}")] - PutChunkFailed(#[from] RPCError), - /// Stacker-db instance rejected the chunk - #[error("Stacker-db rejected the chunk. Reason: {0}")] - PutChunkRejected(String), - /// Failed to find a given json entry - #[error("Invalid JSON entry: {0}")] - InvalidJsonEntry(String), - /// Failed to call a read only function - #[error("Failed to call read only function. {0}")] - ReadOnlyFailure(String), - /// Reqwest specific error occurred - #[error("{0}")] - ReqwestError(#[from] reqwest::Error), - /// Failed to build and sign a new Stacks transaction. - #[error("Failed to generate transaction from a transaction signer: {0}")] - TransactionGenerationFailure(String), - /// Stacks node client request failed - #[error("Stacks node client request failed: {0}")] - RequestFailure(reqwest::StatusCode), - /// Failed to serialize a Clarity value - #[error("Failed to serialize Clarity value: {0}")] - ClaritySerializationError(#[from] SerializationError), - /// Failed to parse a Clarity value - #[error("Recieved a malformed clarity value: {0}")] - MalformedClarityValue(ClarityValue), - /// Invalid Clarity Name - #[error("Invalid Clarity Name: {0}")] - InvalidClarityName(String), - /// Backoff retry timeout - #[error("Backoff retry timeout occurred. Stacks node may be down.")] - RetryTimeout, -} - -/// The Stacks signer client used to communicate with the stacker-db instance -pub struct StacksClient { - /// The stacker-db session - stackerdb_session: StackerDBSession, - /// The stacks address of the signer - stacks_address: StacksAddress, - /// The private key used in all stacks node communications - stacks_private_key: StacksPrivateKey, - /// A map of a slot ID to last chunk version - slot_versions: HashMap, - /// The stacks node HTTP base endpoint - http_origin: String, - /// The types of transactions - tx_version: TransactionVersion, - /// The chain we are interacting with - chain_id: u32, - /// The Client used to make HTTP connects - stacks_node_client: reqwest::blocking::Client, - /// The pox contract ID - pox_contract_id: Option, -} - -impl From<&Config> for StacksClient { - fn from(config: &Config) -> Self { - Self { - stackerdb_session: StackerDBSession::new( - config.node_host, - config.stackerdb_contract_id.clone(), - ), - stacks_private_key: config.stacks_private_key, - stacks_address: config.stacks_address, - slot_versions: HashMap::new(), - http_origin: format!("http://{}", config.node_host), - tx_version: config.network.to_transaction_version(), - chain_id: config.network.to_chain_id(), - stacks_node_client: reqwest::blocking::Client::new(), - pox_contract_id: config.pox_contract_id.clone(), - } - } -} - -impl StacksClient { - /// Sends messages to the stacker-db with an exponential backoff retry - pub fn send_message_with_retry( - &mut self, - id: u32, - message: Packet, - ) -> Result { - let message_bytes = bincode::serialize(&message)?; - let slot_id = slot_id(id, &message.msg); - - loop { - let slot_version = *self.slot_versions.entry(slot_id).or_insert(0) + 1; - let mut chunk = StackerDBChunkData::new(slot_id, slot_version, message_bytes.clone()); - chunk.sign(&self.stacks_private_key)?; - debug!("Sending a chunk to stackerdb!\n{:?}", chunk.clone()); - let send_request = || { - self.stackerdb_session - .put_chunk(chunk.clone()) - .map_err(backoff::Error::transient) - }; - let chunk_ack: StackerDBChunkAckData = retry_with_exponential_backoff(send_request)?; - self.slot_versions.insert(slot_id, slot_version); - - if chunk_ack.accepted { - debug!("Chunk accepted by stackerdb: {:?}", chunk_ack); - return Ok(chunk_ack); - } else { - warn!("Chunk rejected by stackerdb: {:?}", chunk_ack); - } - if let Some(reason) = chunk_ack.reason { - // TODO: fix this jankiness. Update stackerdb to use an error code mapping instead of just a string - // See: https://github.com/stacks-network/stacks-blockchain/issues/3917 - if reason == "Data for this slot and version already exist" { - warn!("Failed to send message to stackerdb due to wrong version number {}. Incrementing and retrying...", slot_version); - } else { - warn!("Failed to send message to stackerdb: {}", reason); - return Err(ClientError::PutChunkRejected(reason)); - } - } - } - } - - /// Retrieve the current DKG aggregate public key - pub fn get_aggregate_public_key(&self) -> Result, ClientError> { - let reward_cycle = self.get_current_reward_cycle()?; - let function_name_str = "get-aggregate-public-key"; // FIXME: this may need to be modified to match .pox-4 - let function_name = ClarityName::try_from(function_name_str) - .map_err(|_| ClientError::InvalidClarityName(function_name_str.to_string()))?; - let (contract_addr, contract_name) = self.get_pox_contract()?; - let function_args = &[ClarityValue::UInt(reward_cycle as u128)]; - let contract_response_hex = self.read_only_contract_call_with_retry( - &contract_addr, - &contract_name, - &function_name, - function_args, - )?; - self.parse_aggregate_public_key(&contract_response_hex) - } - - /// Retrieve the total number of slots allocated to a stacker-db writer - #[allow(dead_code)] - pub fn slots_per_user(&self) -> u32 { - // TODO: retrieve this from the stackerdb instance and make it a function of a given signer public key - // See: https://github.com/stacks-network/stacks-blockchain/issues/3921 - SLOTS_PER_USER - } - - /// Helper function to retrieve the current reward cycle number from the stacks node - fn get_current_reward_cycle(&self) -> Result { - let send_request = || { - self.stacks_node_client - .get(self.pox_path()) - .send() - .map_err(backoff::Error::transient) - }; - let response = retry_with_exponential_backoff(send_request)?; - if !response.status().is_success() { - return Err(ClientError::RequestFailure(response.status())); - } - let json_response = response.json::()?; - let entry = "current_cycle"; - json_response - .get(entry) - .and_then(|cycle: &serde_json::Value| cycle.get("id")) - .and_then(|id| id.as_u64()) - .ok_or_else(|| ClientError::InvalidJsonEntry(format!("{}.id", entry))) - } - - /// Helper function to retrieve the next possible nonce for the signer from the stacks node - #[allow(dead_code)] - fn get_next_possible_nonce(&self) -> Result { - //FIXME: use updated RPC call to get mempool nonces. Depends on https://github.com/stacks-network/stacks-blockchain/issues/4000 - todo!("Get the next possible nonce from the stacks node"); - } - - /// Helper function to retrieve the pox contract address and name from the stacks node - fn get_pox_contract(&self) -> Result<(StacksAddress, ContractName), ClientError> { - // Check if we have overwritten the pox contract ID in the config - if let Some(pox_contract) = self.pox_contract_id.clone() { - return Ok((pox_contract.issuer.into(), pox_contract.name)); - } - // TODO: we may want to cache the pox contract inside the client itself (calling this function once on init) - // https://github.com/stacks-network/stacks-blockchain/issues/4005 - let send_request = || { - self.stacks_node_client - .get(self.pox_path()) - .send() - .map_err(backoff::Error::transient) - }; - let response = retry_with_exponential_backoff(send_request)?; - if !response.status().is_success() { - return Err(ClientError::RequestFailure(response.status())); - } - let json_response = response.json::()?; - let entry = "contract_id"; - let contract_id_string = json_response - .get(entry) - .and_then(|id: &serde_json::Value| id.as_str()) - .ok_or_else(|| ClientError::InvalidJsonEntry(entry.to_string()))?; - let id = QualifiedContractIdentifier::parse(contract_id_string).unwrap(); - Ok((id.issuer.into(), id.name)) - } - - /// Helper function that attempts to deserialize a clarity hex string as the aggregate public key - fn parse_aggregate_public_key(&self, hex: &str) -> Result, ClientError> { - let public_key_clarity_value = ClarityValue::try_deserialize_hex_untyped(hex)?; - if let ClarityValue::Optional(optional_data) = public_key_clarity_value.clone() { - if let Some(ClarityValue::Sequence(SequenceData::Buffer(public_key))) = - optional_data.data.map(|boxed| *boxed) - { - if public_key.data.len() != 32 { - return Err(ClientError::MalformedClarityValue(public_key_clarity_value)); - } - let mut bytes = [0_u8; 32]; - bytes.copy_from_slice(&public_key.data); - Ok(Some(Point::from(Scalar::from(bytes)))) - } else { - Ok(None) - } - } else { - Err(ClientError::MalformedClarityValue(public_key_clarity_value)) - } - } - - /// Sends a transaction to the stacks node for a modifying contract call - #[allow(dead_code)] - fn transaction_contract_call( - &self, - contract_addr: &StacksAddress, - contract_name: ContractName, - function_name: ClarityName, - function_args: &[ClarityValue], - ) -> Result { - debug!("Making a contract call to {contract_addr}.{contract_name}..."); - let signed_tx = self.build_signed_transaction( - contract_addr, - contract_name, - function_name, - function_args, - )?; - self.submit_tx(&signed_tx) - } - - /// Helper function to create a stacks transaction for a modifying contract call - fn build_signed_transaction( - &self, - contract_addr: &StacksAddress, - contract_name: ContractName, - function_name: ClarityName, - function_args: &[ClarityValue], - ) -> Result { - let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { - address: *contract_addr, - contract_name, - function_name, - function_args: function_args.to_vec(), - }); - let public_key = StacksPublicKey::from_private(&self.stacks_private_key); - let tx_auth = TransactionAuth::Standard( - TransactionSpendingCondition::new_singlesig_p2pkh(public_key).ok_or( - ClientError::TransactionGenerationFailure(format!( - "Failed to create spending condition from public key: {}", - public_key.to_hex() - )), - )?, - ); - - let mut unsigned_tx = StacksTransaction::new(self.tx_version, tx_auth, tx_payload); - - // FIXME: Because signers are given priority, we can put down a tx fee of 0 - // https://github.com/stacks-network/stacks-blockchain/issues/4006 - // Note: if set to 0 now, will cause a failure (MemPoolRejection::FeeTooLow) - unsigned_tx.set_tx_fee(10_000); - unsigned_tx.set_origin_nonce(self.get_next_possible_nonce()?); - - unsigned_tx.anchor_mode = TransactionAnchorMode::Any; - unsigned_tx.post_condition_mode = TransactionPostConditionMode::Allow; - unsigned_tx.chain_id = self.chain_id; - - let mut tx_signer = StacksTransactionSigner::new(&unsigned_tx); - tx_signer - .sign_origin(&self.stacks_private_key) - .map_err(|e| ClientError::TransactionGenerationFailure(e.to_string()))?; - - tx_signer - .get_tx() - .ok_or(ClientError::TransactionGenerationFailure( - "Failed to generate transaction from a transaction signer".to_string(), - )) - } - - /// Helper function to submit a transaction to the Stacks node - fn submit_tx(&self, tx: &StacksTransaction) -> Result { - let txid = tx.txid(); - let tx = tx.serialize_to_vec(); - let send_request = || { - self.stacks_node_client - .post(self.transaction_path()) - .header("Content-Type", "application/octet-stream") - .body(tx.clone()) - .send() - .map_err(backoff::Error::transient) - }; - let response = retry_with_exponential_backoff(send_request)?; - if !response.status().is_success() { - return Err(ClientError::RequestFailure(response.status())); - } - Ok(txid) - } - - /// Makes a read only contract call to a stacks contract - pub fn read_only_contract_call_with_retry( - &self, - contract_addr: &StacksAddress, - contract_name: &ContractName, - function_name: &ClarityName, - function_args: &[ClarityValue], - ) -> Result { - debug!("Calling read-only function {}...", function_name); - let args = function_args - .iter() - .map(|arg| arg.serialize_to_hex()) - .collect::>(); - let body = - json!({"sender": self.stacks_address.to_string(), "arguments": args}).to_string(); - let path = self.read_only_path(contract_addr, contract_name, function_name); - let send_request = || { - self.stacks_node_client - .post(path.clone()) - .header("Content-Type", "application/json") - .body(body.clone()) - .send() - .map_err(backoff::Error::transient) - }; - let response = retry_with_exponential_backoff(send_request)?; - if !response.status().is_success() { - return Err(ClientError::RequestFailure(response.status())); - } - let response = response.json::()?; - if !response - .get("okay") - .map(|val| val.as_bool().unwrap_or(false)) - .unwrap_or(false) - { - let cause = response - .get("cause") - .ok_or(ClientError::InvalidJsonEntry("cause".to_string()))?; - return Err(ClientError::ReadOnlyFailure(format!( - "{}: {}", - function_name, cause - ))); - } - let result = response - .get("result") - .ok_or(ClientError::InvalidJsonEntry("result".to_string()))? - .as_str() - .ok_or_else(|| ClientError::ReadOnlyFailure("Expected string result.".to_string()))? - .to_string(); - Ok(result) - } - - fn pox_path(&self) -> String { - format!("{}/v2/pox", self.http_origin) - } - - fn transaction_path(&self) -> String { - format!("{}/v2/transactions", self.http_origin) - } - - fn read_only_path( - &self, - contract_addr: &StacksAddress, - contract_name: &ContractName, - function_name: &ClarityName, - ) -> String { - format!( - "{}/v2/contracts/call-read/{contract_addr}/{contract_name}/{function_name}", - self.http_origin - ) - } -} - -/// Retry a function F with an exponential backoff and notification on transient failure -pub fn retry_with_exponential_backoff(request_fn: F) -> Result -where - F: FnMut() -> Result>, -{ - let notify = |_err, dur| { - debug!( - "Failed to connect to stacks-node. Next attempt in {:?}", - dur - ); - }; - - let backoff_timer = backoff::ExponentialBackoffBuilder::new() - .with_initial_interval(Duration::from_millis(BACKOFF_INITIAL_INTERVAL)) - .with_max_interval(Duration::from_millis(BACKOFF_MAX_INTERVAL)) - .build(); - - backoff::retry_notify(backoff_timer, request_fn, notify).map_err(|_| ClientError::RetryTimeout) -} - -/// Helper function to determine the slot ID for the provided stacker-db writer id and the message type -fn slot_id(id: u32, message: &Message) -> u32 { - let slot_id = match message { - Message::DkgBegin(_) => 0, - Message::DkgPrivateBegin(_) => 1, - Message::DkgEnd(_) => 2, - Message::DkgPublicShares(_) => 4, - Message::DkgPrivateShares(_) => 5, - Message::NonceRequest(_) => 6, - Message::NonceResponse(_) => 7, - Message::SignatureShareRequest(_) => 8, - Message::SignatureShareResponse(_) => 9, - }; - SLOTS_PER_USER * id + slot_id -} - -#[cfg(test)] -mod tests { - use std::io::{BufWriter, Read, Write}; - use std::net::{SocketAddr, TcpListener}; - use std::thread::spawn; - - use super::*; - - struct TestConfig { - mock_server: TcpListener, - client: StacksClient, - } - - impl TestConfig { - pub fn new() -> Self { - let mut config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); - - let mut mock_server_addr = SocketAddr::from(([127, 0, 0, 1], 0)); - // Ask the OS to assign a random port to listen on by passing 0 - let mock_server = TcpListener::bind(mock_server_addr).unwrap(); - - // Update the config to use this port - mock_server_addr.set_port(mock_server.local_addr().unwrap().port()); - config.node_host = mock_server_addr; - - let client = StacksClient::from(&config); - Self { - mock_server, - client, - } - } - } - - fn write_response(mock_server: TcpListener, bytes: &[u8]) -> [u8; 1024] { - debug!("Writing a response..."); - let mut request_bytes = [0u8; 1024]; - { - let mut stream = mock_server.accept().unwrap().0; - let _ = stream.read(&mut request_bytes).unwrap(); - stream.write_all(bytes).unwrap(); - } - request_bytes - } - - #[test] - fn read_only_contract_call_200_success() { - let config = TestConfig::new(); - let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), - &[], - ) - }); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n{\"okay\":true,\"result\":\"0x070d0000000473425443\"}", - ); - let result = h.join().unwrap().unwrap(); - assert_eq!(result, "0x070d0000000473425443"); - } - - #[test] - fn read_only_contract_call_with_function_args_200_success() { - let config = TestConfig::new(); - let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), - &[ClarityValue::UInt(10_u128)], - ) - }); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n{\"okay\":true,\"result\":\"0x070d0000000473425443\"}", - ); - let result = h.join().unwrap().unwrap(); - assert_eq!(result, "0x070d0000000473425443"); - } - - #[test] - fn read_only_contract_call_200_failure() { - let config = TestConfig::new(); - let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), - &[], - ) - }); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n{\"okay\":false,\"cause\":\"Some reason\"}", - ); - let result = h.join().unwrap(); - assert!(matches!(result, Err(ClientError::ReadOnlyFailure(_)))); - } - - #[test] - fn read_only_contract_call_400_failure() { - let config = TestConfig::new(); - // Simulate a 400 Bad Request response - let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), - &[], - ) - }); - write_response(config.mock_server, b"HTTP/1.1 400 Bad Request\n\n"); - let result = h.join().unwrap(); - assert!(matches!( - result, - Err(ClientError::RequestFailure( - reqwest::StatusCode::BAD_REQUEST - )) - )); - } - - #[test] - fn read_only_contract_call_404_failure() { - let config = TestConfig::new(); - // Simulate a 400 Bad Request response - let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), - &[], - ) - }); - write_response(config.mock_server, b"HTTP/1.1 404 Not Found\n\n"); - let result = h.join().unwrap(); - assert!(matches!( - result, - Err(ClientError::RequestFailure(reqwest::StatusCode::NOT_FOUND)) - )); - } - - #[test] - fn pox_contract_success() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_pox_contract()); - write_response( - config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\"}", - ); - let (address, name) = h.join().unwrap().unwrap(); - assert_eq!( - (address.to_string().as_str(), name.to_string().as_str()), - ("ST000000000000000000002AMW42H", "pox-3") - ); - } - - #[test] - fn valid_reward_cycle_should_succeed() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_current_reward_cycle()); - write_response( - config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"id\":506,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":5690000000000,\"is_pox_active\":false}}", - ); - let current_cycle_id = h.join().unwrap().unwrap(); - assert_eq!(506, current_cycle_id); - } - - #[test] - fn invalid_reward_cycle_should_fail() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_current_reward_cycle()); - write_response( - config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"id\":\"fake id\", \"is_pox_active\":false}}", - ); - let res = h.join().unwrap(); - assert!(matches!(res, Err(ClientError::InvalidJsonEntry(_)))); - } - - #[test] - fn missing_reward_cycle_should_fail() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_current_reward_cycle()); - write_response( - config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"is_pox_active\":false}}", - ); - let res = h.join().unwrap(); - assert!(matches!(res, Err(ClientError::InvalidJsonEntry(_)))); - } - - #[test] - fn parse_valid_aggregate_public_key_should_succeed() { - let config = TestConfig::new(); - let clarity_value_hex = - "0x0a0200000020b8c8b0652cb2851a52374c7acd47181eb031e8fa5c62883f636e0d4fe695d6ca"; - let result = config - .client - .parse_aggregate_public_key(clarity_value_hex) - .unwrap(); - assert_eq!( - result.map(|point| point.to_string()), - Some("yzwdjwPz36Has1MSkg8JGwo38avvATkiTZvRiH1e5MLd".to_string()) - ); - - let clarity_value_hex = "0x09"; - let result = config - .client - .parse_aggregate_public_key(clarity_value_hex) - .unwrap(); - assert!(result.is_none()); - } - - #[test] - fn parse_invalid_aggregate_public_key_should_fail() { - let config = TestConfig::new(); - let clarity_value_hex = "0x00"; - let result = config.client.parse_aggregate_public_key(clarity_value_hex); - assert!(matches!( - result, - Err(ClientError::ClaritySerializationError(..)) - )); - // TODO: add further tests for malformed clarity values (an optional of any other type for example) - } - - #[ignore] - #[test] - fn transaction_contract_call_should_send_bytes_to_node() { - let config = TestConfig::new(); - let tx = config - .client - .build_signed_transaction( - &config.client.stacks_address, - ContractName::try_from("contract-name").unwrap(), - ClarityName::try_from("function-name").unwrap(), - &[], - ) - .unwrap(); - - let mut tx_bytes = [0u8; 1024]; - { - let mut tx_bytes_writer = BufWriter::new(&mut tx_bytes[..]); - tx.consensus_serialize(&mut tx_bytes_writer).unwrap(); - tx_bytes_writer.flush().unwrap(); - } - - let bytes_len = tx_bytes - .iter() - .enumerate() - .rev() - .find(|(_, &x)| x != 0) - .unwrap() - .0 - + 1; - - let tx_clone = tx.clone(); - let h = spawn(move || config.client.submit_tx(&tx_clone)); - - let request_bytes = write_response( - config.mock_server, - format!("HTTP/1.1 200 OK\n\n{}", tx.txid()).as_bytes(), - ); - let returned_txid = h.join().unwrap().unwrap(); - - assert_eq!(returned_txid, tx.txid()); - assert!( - request_bytes - .windows(bytes_len) - .any(|window| window == &tx_bytes[..bytes_len]), - "Request bytes did not contain the transaction bytes" - ); - } - - #[ignore] - #[test] - fn transaction_contract_call_should_succeed() { - let config = TestConfig::new(); - let h = spawn(move || { - config.client.transaction_contract_call( - &config.client.stacks_address, - ContractName::try_from("contract-name").unwrap(), - ClarityName::try_from("function-name").unwrap(), - &[], - ) - }); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n4e99f99bc4a05437abb8c7d0c306618f45b203196498e2ebe287f10497124958", - ); - assert!(h.join().unwrap().is_ok()); - } -} From d7e3624132a1011c5584f329dee0ea9c1dd913b6 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 6 Dec 2023 14:55:01 -0500 Subject: [PATCH 06/27] Filter out message types from the signer and miner StackerDBMessages Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/stackerdb.rs | 3 ++ stacks-signer/src/runloop.rs | 65 ++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 8e776e935f..01bc12d9a9 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -35,6 +35,9 @@ const BLOCK_SLOT_ID: u32 = 9; /// The StackerDB messages that can be sent through the .signers contract pub enum StackerDBMessage { /// The latest Nakamoto block for miners to observe + // TODO: update this to use a struct that lists optional error code if the block is invalid + // to prove that the signers have considered the block but rejected it. This should include + // hints about how to fix the block Block(NakamotoBlock), /// DKG and Signing round data for other signers to observe Packet(Packet), diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 7f36cc0955..786119d73f 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -5,6 +5,7 @@ use std::time::Duration; use hashbrown::{HashMap, HashSet}; use libsigner::{SignerRunLoop, StackerDBChunksEvent}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; +use stacks_common::codec::{read_next, StacksMessageCodec}; use stacks_common::{debug, error, info, warn}; use wsts::common::MerkleRoot; use wsts::curve::ecdsa; @@ -15,7 +16,9 @@ use wsts::state_machine::signer::Signer; use wsts::state_machine::{OperationResult, PublicKeys}; use wsts::v2; -use crate::client::{retry_with_exponential_backoff, ClientError, StackerDB, StacksClient}; +use crate::client::{ + retry_with_exponential_backoff, ClientError, StackerDB, StackerDBMessage, StacksClient, +}; use crate::config::Config; /// Which operation to perform @@ -167,11 +170,36 @@ impl RunLoop { } /// Process the event as a miner message from the miner stacker-db - fn process_event_miner( - &mut self, - _event: &StackerDBChunksEvent, - ) -> (Vec, Vec) { - todo!("Process miner event") + fn process_event_miner(&mut self, event: &StackerDBChunksEvent) { + // Determine the current coordinator id and public key for verification + let (coordinator_id, _coordinator_public_key) = + calculate_coordinator(&self.signing_round.public_keys); + event.modified_slots.iter().for_each(|chunk| { + let mut ptr = &chunk.data[..]; + let Some(stacker_db_message) = read_next::(&mut ptr).ok() else { + warn!("Received an unrecognized message type from .miners stacker-db slot id {}: {:?}", chunk.slot_id, ptr); + return; + }; + match stacker_db_message { + StackerDBMessage::Packet(_packet) => { + // We should never actually be receiving packets from the miner stacker-db. + warn!( + "Received a packet from the miner stacker-db. This should never happen..." + ); + } + StackerDBMessage::Block(block) => { + // Received a block proposal from the miner. + // If the signer is the coordinator, then trigger a Signing round for the block + if coordinator_id == self.signing_round.signer_id { + self.commands.push_back(RunLoopCommand::Sign { + message: block.serialize_to_vec(), + is_taproot: false, + merkle_root: None, + }); + } + } + } + }); } /// Process the event as a signer message from the signer stacker-db @@ -184,11 +212,26 @@ impl RunLoop { .modified_slots .iter() .filter_map(|chunk| { - let packet = bincode::deserialize::(&chunk.data).ok()?; - if packet.verify(&self.signing_round.public_keys, coordinator_public_key) { - Some(packet) - } else { - None + let mut ptr = &chunk.data[..]; + let Some(stacker_db_message) = read_next::(&mut ptr).ok() else { + warn!("Received an unrecognized message type from .signers stacker-db slot id {}: {:?}", chunk.slot_id, ptr); + return None; + }; + match stacker_db_message { + StackerDBMessage::Packet(packet) => { + if packet.verify( + &self.signing_round.public_keys, + coordinator_public_key, + ) { + Some(packet) + } else { + None + } + } + StackerDBMessage::Block(_block) => { + // Blocks are meant to be read by observing miners. Ignore them. + None + } } }) .collect(); From 0f680cde532e226b025ea3ddc247fdd2a2eaa530 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 6 Dec 2023 14:55:33 -0500 Subject: [PATCH 07/27] Add serde to StackerDBMessage types Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/stackerdb.rs | 156 +++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 5 deletions(-) diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 01bc12d9a9..62d2e80ed4 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -32,6 +32,33 @@ const SIGNATURE_SHARE_REQUEST_SLOT_ID: u32 = 7; const SIGNATURE_SHARE_RESPONSE_SLOT_ID: u32 = 8; const BLOCK_SLOT_ID: u32 = 9; +/// This is required for easy serialization of the various StackerDBMessage types +#[repr(u8)] +enum TypePrefix { + Block, + Packet, +} + +impl TypePrefix { + /// Convert a u8 to a TypePrefix + fn from_u8(value: u8) -> Option { + match value { + 0 => Some(Self::Block), + 1 => Some(Self::Packet), + _ => None, + } + } +} + +impl From<&StackerDBMessage> for TypePrefix { + fn from(message: &StackerDBMessage) -> TypePrefix { + match message { + StackerDBMessage::Block(_) => TypePrefix::Block, + StackerDBMessage::Packet(_) => TypePrefix::Packet, + } + } +} + /// The StackerDB messages that can be sent through the .signers contract pub enum StackerDBMessage { /// The latest Nakamoto block for miners to observe @@ -50,12 +77,39 @@ impl From for StackerDBMessage { } impl StacksMessageCodec for StackerDBMessage { - fn consensus_serialize(&self, _fd: &mut W) -> Result<(), CodecError> { - todo!() + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + fd.write_all(&[TypePrefix::from(self) as u8]) + .map_err(CodecError::WriteError)?; + match self { + StackerDBMessage::Packet(packet) => { + let message_bytes = bincode::serialize(&packet) + .map_err(|e| CodecError::SerializeError(e.to_string()))?; + message_bytes.consensus_serialize(fd) + } + StackerDBMessage::Block(block) => block.consensus_serialize(fd), + } } - fn consensus_deserialize(_fd: &mut R) -> Result { - todo!() + fn consensus_deserialize(fd: &mut R) -> Result { + let mut prefix = [0]; + fd.read_exact(&mut prefix) + .map_err(|e| CodecError::DeserializeError(e.to_string()))?; + let prefix = TypePrefix::from_u8(prefix[0]).ok_or(CodecError::DeserializeError( + "Bad StackerDBMessage prefix".into(), + ))?; + + match prefix { + TypePrefix::Packet => { + let message_bytes = Vec::::consensus_deserialize(fd)?; + let packet = bincode::deserialize(&message_bytes) + .map_err(|e| CodecError::DeserializeError(e.to_string()))?; + Ok(Self::Packet(packet)) + } + TypePrefix::Block => { + let block = NakamotoBlock::consensus_deserialize(fd)?; + Ok(StackerDBMessage::Block(block)) + } + } } } @@ -162,4 +216,96 @@ impl StackerDB { } #[cfg(test)] -mod tests {} +mod tests { + use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; + use blockstack_lib::chainstate::stacks::StacksTransaction; + use rand_core::OsRng; + use stacks_common::codec::StacksMessageCodec; + use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash}; + use stacks_common::util::hash::{MerkleTree, Sha512Trunc256Sum}; + use stacks_common::util::secp256k1::{MessageSignature, SchnorrSignature}; + use wsts::curve::scalar::Scalar; + use wsts::net::{Message, Packet, Signable, SignatureShareRequest}; + + use super::StackerDBMessage; + + #[test] + fn serde_stackerdb_message_block() { + let txs: Vec = vec![]; + let mut header = NakamotoBlockHeader { + version: 1, + chain_length: 2, + burn_spent: 3, + consensus_hash: ConsensusHash([0x04; 20]), + parent_block_id: StacksBlockId([0x05; 32]), + tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), + state_index_root: TrieHash([0x07; 32]), + miner_signature: MessageSignature::empty(), + signer_signature: SchnorrSignature::default(), + }; + let txid_vecs = txs.iter().map(|tx| tx.txid().as_bytes().to_vec()).collect(); + + let merkle_tree = MerkleTree::::new(&txid_vecs); + let tx_merkle_root = merkle_tree.root(); + + header.tx_merkle_root = tx_merkle_root; + + let block = NakamotoBlock { header, txs }; + + let msg = StackerDBMessage::Block(block.clone()); + let serialized_bytes = msg.serialize_to_vec(); + let deserialized_msg = + StackerDBMessage::consensus_deserialize(&mut &serialized_bytes[..]).unwrap(); + match deserialized_msg { + StackerDBMessage::Block(deserialized_block) => { + assert_eq!(deserialized_block, block); + } + _ => panic!("Wrong message type. Expected StackerDBMessage::Block"), + } + } + + #[test] + fn serde_stackerdb_message_packet() { + let mut rng = OsRng; + let private_key = Scalar::random(&mut rng); + let to_sign = "One, two, three, four, five? That's amazing. I've got the same combination on my luggage.".as_bytes(); + let sig_share_request = SignatureShareRequest { + dkg_id: 1, + sign_id: 5, + sign_iter_id: 4, + nonce_responses: vec![], + message: to_sign.to_vec(), + is_taproot: false, + merkle_root: None, + }; + let packet = Packet { + sig: sig_share_request + .sign(&private_key) + .expect("Failed to sign SignatureShareRequest"), + msg: Message::SignatureShareRequest(sig_share_request), + }; + + let msg = StackerDBMessage::Packet(packet.clone()); + let serialized_bytes = msg.serialize_to_vec(); + let deserialized_msg = + StackerDBMessage::consensus_deserialize(&mut &serialized_bytes[..]).unwrap(); + match deserialized_msg { + StackerDBMessage::Packet(deserialized_packet) => { + assert_eq!(deserialized_packet.sig, packet.sig); + match deserialized_packet.msg { + Message::SignatureShareRequest(deserialized_message) => { + assert_eq!(deserialized_message.dkg_id, 1); + assert_eq!(deserialized_message.sign_id, 5); + assert_eq!(deserialized_message.sign_iter_id, 4); + assert!(deserialized_message.nonce_responses.is_empty()); + assert_eq!(deserialized_message.message.as_slice(), to_sign); + assert!(!deserialized_message.is_taproot); + assert!(deserialized_message.merkle_root.is_none()); + } + _ => panic!("Wrong message type. Expected Message::SignatureShareRequest"), + } + } + _ => panic!("Wrong message type. Expected StackerDBMessage::Packet."), + } + } +} From 57a400ac11e913c5f3255502a6d647e6ab5849fe Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 6 Dec 2023 15:48:56 -0500 Subject: [PATCH 08/27] Add TODO for miner public key verification Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/stackerdb.rs | 1 + stacks-signer/src/client/stacks_client.rs | 27 +++++++++++++++++++++++ stacks-signer/src/runloop.rs | 22 ++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 62d2e80ed4..ed97adcc50 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -65,6 +65,7 @@ pub enum StackerDBMessage { // TODO: update this to use a struct that lists optional error code if the block is invalid // to prove that the signers have considered the block but rejected it. This should include // hints about how to fix the block + // Update to use NakamotoBlockProposal. Depends on https://github.com/stacks-network/stacks-core/pull/4084 Block(NakamotoBlock), /// DKG and Signing round data for other signers to observe Packet(Packet), diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index e1fbbb61cd..d3fb3c570f 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -1,4 +1,5 @@ use blockstack_lib::burnchains::Txid; +use blockstack_lib::chainstate::nakamoto::NakamotoBlock; use blockstack_lib::chainstate::stacks::{ StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth, TransactionContractCall, TransactionPayload, TransactionPostConditionMode, @@ -50,6 +51,28 @@ impl From<&Config> for StacksClient { } impl StacksClient { + /// Retrieve the current miner public key + pub fn get_miner_public_key(&self) -> Result { + // TODO: Depends on https://github.com/stacks-network/stacks-core/issues/4018 + todo!("Get the miner public key from the stacks node to verify the miner blocks were signed by the correct miner"); + } + + /// Check if the proposed Nakamoto block is a valid block + pub fn is_valid_nakamoto_block(&self, _block: &NakamotoBlock) -> Result { + // TODO: Depends on https://github.com/stacks-network/stacks-core/issues/3866 + let send_request = || { + self.stacks_node_client + .get(self.block_proposal_path()) + .send() + .map_err(backoff::Error::transient) + }; + let response = retry_with_exponential_backoff(send_request)?; + if !response.status().is_success() { + return Err(ClientError::RequestFailure(response.status())); + } + todo!("Call the appropriate RPC endpoint to check if the proposed Nakamoto block is valid"); + } + /// Retrieve the current DKG aggregate public key pub fn get_aggregate_public_key(&self) -> Result, ClientError> { let reward_cycle = self.get_current_reward_cycle()?; @@ -300,6 +323,10 @@ impl StacksClient { self.http_origin ) } + + fn block_proposal_path(&self) -> String { + format!("{}/v2/block-proposal", self.http_origin) + } } #[cfg(test)] diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 786119d73f..f3df4bac03 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -191,6 +191,27 @@ impl RunLoop { // Received a block proposal from the miner. // If the signer is the coordinator, then trigger a Signing round for the block if coordinator_id == self.signing_round.signer_id { + // Don't bother triggering a signing round for the block if it is invalid + if !self.stacks_client.is_valid_nakamoto_block(&block).unwrap_or_else(|e| { + warn!("Failed to validate block: {:?}", e); + false + }) { + warn!("Received an invalid block proposal from the miner. Ignoring block proposal: {:?}", block); + return; + } + + // TODO: dependent on https://github.com/stacks-network/stacks-core/issues/4018 + // let miner_public_key = self.stacks_client.get_miner_public_key().expect("Failed to get miner public key. Cannot verify blocks."); + // let Some(block_miner_public_key) = block.header.recover_miner_pk() else { + // warn!("Failed to recover miner public key from block. Ignoring block proposal: {:?}", block); + // return; + // }; + // if block_miner_public_key != miner_public_key { + // warn!("Received a block proposal signed with an invalid miner public key. Ignoring block proposal: {:?}.", block); + // return; + // } + + // This is a block proposal from the miner. Trigger a signing round for it. self.commands.push_back(RunLoopCommand::Sign { message: block.serialize_to_vec(), is_taproot: false, @@ -236,6 +257,7 @@ impl RunLoop { }) .collect(); // First process all messages as a signer + // TODO: deserialize the packet into a block and verify its contents let mut outbound_messages = self .signing_round .process_inbound_messages(&inbound_messages) From 1f2cdd52c3fc732934bd144155cfddc9669c0bcc Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 11 Dec 2023 08:53:13 -0500 Subject: [PATCH 09/27] Add copyright to lib.rs and mod.rs files Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/mod.rs | 16 ++++++++++++++++ stacks-signer/src/client/stacks_client.rs | 2 +- stacks-signer/src/lib.rs | 17 +++++++++++++++++ stacks-signer/src/runloop.rs | 2 +- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index fcc614c6e3..72dc9cae91 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -1,3 +1,19 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + /// The stacker db module for communicating with the stackerdb contract mod stackerdb; /// The stacks node client module for communicating with the stacks node diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index d3fb3c570f..6c317881da 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -59,7 +59,7 @@ impl StacksClient { /// Check if the proposed Nakamoto block is a valid block pub fn is_valid_nakamoto_block(&self, _block: &NakamotoBlock) -> Result { - // TODO: Depends on https://github.com/stacks-network/stacks-core/issues/3866 + // TODO: Depends on https://github.com/stacks-network/stacks-core/issues/3866 let send_request = || { self.stacks_node_client .get(self.block_proposal_path()) diff --git a/stacks-signer/src/lib.rs b/stacks-signer/src/lib.rs index e5b8350f5b..c0a8a11f7c 100644 --- a/stacks-signer/src/lib.rs +++ b/stacks-signer/src/lib.rs @@ -3,6 +3,23 @@ # stacks-signer: a libary for creating a Stacks compliant signer. A default implementation binary is also provided. Usage documentation can be found in the [README](https://github.com/Trust-Machines/core-eng/stacks-signer-api/README.md). */ + +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + /// The cli module for the signer binary pub mod cli; /// The signer client for communicating with stackerdb/stacks nodes diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index f3df4bac03..3fa6e19d65 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -210,7 +210,7 @@ impl RunLoop { // warn!("Received a block proposal signed with an invalid miner public key. Ignoring block proposal: {:?}.", block); // return; // } - + // This is a block proposal from the miner. Trigger a signing round for it. self.commands.push_back(RunLoopCommand::Sign { message: block.serialize_to_vec(), From bb06c55144da5ecf96ffe9d479c2c4e8cf01725e Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 11 Dec 2023 09:00:40 -0500 Subject: [PATCH 10/27] Update put_chunk to take a ref and cleanup clippy Signed-off-by: Jacinta Ferrant --- libsigner/src/session.rs | 6 +++--- stacks-signer/src/client/stackerdb.rs | 4 ++-- stacks-signer/src/main.rs | 2 +- stacks-signer/src/runloop.rs | 7 ++++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/libsigner/src/session.rs b/libsigner/src/session.rs index b65e43467c..e5dbd67f35 100644 --- a/libsigner/src/session.rs +++ b/libsigner/src/session.rs @@ -44,7 +44,7 @@ pub trait SignerSession { /// query the replica for zero or more latest chunks fn get_latest_chunks(&mut self, slot_ids: &[u32]) -> Result>>, RPCError>; /// Upload a chunk to the stacker DB instance - fn put_chunk(&mut self, chunk: StackerDBChunkData) -> Result; + fn put_chunk(&mut self, chunk: &StackerDBChunkData) -> Result; /// Get a single chunk with the given version /// Returns Ok(Some(..)) if the chunk exists @@ -207,9 +207,9 @@ impl SignerSession for StackerDBSession { } /// upload a chunk - fn put_chunk(&mut self, chunk: StackerDBChunkData) -> Result { + fn put_chunk(&mut self, chunk: &StackerDBChunkData) -> Result { let body = - serde_json::to_vec(&chunk).map_err(|e| RPCError::Deserialize(format!("{:?}", &e)))?; + serde_json::to_vec(chunk).map_err(|e| RPCError::Deserialize(format!("{:?}", &e)))?; let path = stackerdb_post_chunk_path(self.stackerdb_contract_id.clone()); let resp_bytes = self.rpc_request("POST", &path, Some("application/json"), &body)?; let ack: StackerDBChunkAckData = serde_json::from_slice(&resp_bytes) diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index ed97adcc50..6ea13040b4 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -177,10 +177,10 @@ impl StackerDB { let slot_version = *self.slot_versions.entry(slot_id).or_insert(0) + 1; let mut chunk = StackerDBChunkData::new(slot_id, slot_version, message_bytes.clone()); chunk.sign(&self.stacks_private_key)?; - debug!("Sending a chunk to stackerdb!\n{:?}", chunk.clone()); + debug!("Sending a chunk to stackerdb!\n{:?}", &chunk); let send_request = || { self.signers_stackerdb_session - .put_chunk(chunk.clone()) + .put_chunk(&chunk) .map_err(backoff::Error::transient) }; let chunk_ack: StackerDBChunkAckData = retry_with_exponential_backoff(send_request)?; diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index a3270f8fac..18ef2ca6f7 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -189,7 +189,7 @@ fn handle_put_chunk(args: PutChunkArgs) { let mut session = stackerdb_session(args.db_args.host, args.db_args.contract); let mut chunk = StackerDBChunkData::new(args.slot_id, args.slot_version, args.data); chunk.sign(&args.private_key).unwrap(); - let chunk_ack = session.put_chunk(chunk).unwrap(); + let chunk_ack = session.put_chunk(&chunk).unwrap(); println!("{}", serde_json::to_string(&chunk_ack).unwrap()); } diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 3fa6e19d65..d119cc1cc0 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -191,11 +191,12 @@ impl RunLoop { // Received a block proposal from the miner. // If the signer is the coordinator, then trigger a Signing round for the block if coordinator_id == self.signing_round.signer_id { - // Don't bother triggering a signing round for the block if it is invalid - if !self.stacks_client.is_valid_nakamoto_block(&block).unwrap_or_else(|e| { + let is_valid_block = self.stacks_client.is_valid_nakamoto_block(&block).unwrap_or_else(|e| { warn!("Failed to validate block: {:?}", e); false - }) { + }); + // Don't bother triggering a signing round for the block if it is invalid + if !is_valid_block { warn!("Received an invalid block proposal from the miner. Ignoring block proposal: {:?}", block); return; } From 868bd3ae101d762454df2adba43e2f401ae30cc7 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 10 Jan 2024 10:43:16 -0500 Subject: [PATCH 11/27] Cleanup stacks client to deserialize specific response types isntead of generic json blobs Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/mod.rs | 3 - stacks-signer/src/client/stackerdb.rs | 12 +-- stacks-signer/src/client/stacks_client.rs | 95 ++++++++++------------- 3 files changed, 45 insertions(+), 65 deletions(-) diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index 72dc9cae91..73e6d756f3 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -51,9 +51,6 @@ pub enum ClientError { /// Stacker-db instance rejected the chunk #[error("Stacker-db rejected the chunk. Reason: {0}")] PutChunkRejected(String), - /// Failed to find a given json entry - #[error("Invalid JSON entry: {0}")] - InvalidJsonEntry(String), /// Failed to call a read only function #[error("Failed to call read only function. {0}")] ReadOnlyFailure(String), diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 6ea13040b4..b53467b5e7 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -59,13 +59,9 @@ impl From<&StackerDBMessage> for TypePrefix { } } -/// The StackerDB messages that can be sent through the .signers contract +/// The StackerDB messages that can be sent through the observed contracts pub enum StackerDBMessage { /// The latest Nakamoto block for miners to observe - // TODO: update this to use a struct that lists optional error code if the block is invalid - // to prove that the signers have considered the block but rejected it. This should include - // hints about how to fix the block - // Update to use NakamotoBlockProposal. Depends on https://github.com/stacks-network/stacks-core/pull/4084 Block(NakamotoBlock), /// DKG and Signing round data for other signers to observe Packet(Packet), @@ -219,12 +215,12 @@ impl StackerDB { #[cfg(test)] mod tests { use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; - use blockstack_lib::chainstate::stacks::StacksTransaction; + use blockstack_lib::chainstate::stacks::{StacksTransaction, ThresholdSignature}; use rand_core::OsRng; use stacks_common::codec::StacksMessageCodec; use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash}; use stacks_common::util::hash::{MerkleTree, Sha512Trunc256Sum}; - use stacks_common::util::secp256k1::{MessageSignature, SchnorrSignature}; + use stacks_common::util::secp256k1::MessageSignature; use wsts::curve::scalar::Scalar; use wsts::net::{Message, Packet, Signable, SignatureShareRequest}; @@ -242,7 +238,7 @@ mod tests { tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), state_index_root: TrieHash([0x07; 32]), miner_signature: MessageSignature::empty(), - signer_signature: SchnorrSignature::default(), + signer_signature: ThresholdSignature::mock(), }; let txid_vecs = txs.iter().map(|tx| tx.txid().as_bytes().to_vec()).collect(); diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 6c317881da..f6aed8b137 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -5,6 +5,9 @@ use blockstack_lib::chainstate::stacks::{ TransactionContractCall, TransactionPayload, TransactionPostConditionMode, TransactionSpendingCondition, TransactionVersion, }; +use blockstack_lib::net::api::callreadonly::CallReadOnlyResponse; +use blockstack_lib::net::api::getpoxinfo::RPCPoxInfoData; +use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; use clarity::vm::types::{QualifiedContractIdentifier, SequenceData}; use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; use serde_json::json; @@ -59,7 +62,6 @@ impl StacksClient { /// Check if the proposed Nakamoto block is a valid block pub fn is_valid_nakamoto_block(&self, _block: &NakamotoBlock) -> Result { - // TODO: Depends on https://github.com/stacks-network/stacks-core/issues/3866 let send_request = || { self.stacks_node_client .get(self.block_proposal_path()) @@ -70,7 +72,17 @@ impl StacksClient { if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } - todo!("Call the appropriate RPC endpoint to check if the proposed Nakamoto block is valid"); + let validate_block_response = response.json::()?; + match validate_block_response { + BlockValidateResponse::Ok(validate_block_ok) => { + debug!("Block validation succeeded: {:?}", validate_block_ok); + Ok(true) + } + BlockValidateResponse::Reject(validate_block_reject) => { + debug!("Block validation failed: {:?}", validate_block_reject); + Ok(false) + } + } } /// Retrieve the current DKG aggregate public key @@ -90,8 +102,8 @@ impl StacksClient { self.parse_aggregate_public_key(&contract_response_hex) } - /// Helper function to retrieve the current reward cycle number from the stacks node - fn get_current_reward_cycle(&self) -> Result { + // Helper function to retrieve the pox data from the stacks node + fn get_pox_data(&self) -> Result { let send_request = || { self.stacks_node_client .get(self.pox_path()) @@ -102,13 +114,14 @@ impl StacksClient { if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } - let json_response = response.json::()?; - let entry = "current_cycle"; - json_response - .get(entry) - .and_then(|cycle: &serde_json::Value| cycle.get("id")) - .and_then(|id| id.as_u64()) - .ok_or_else(|| ClientError::InvalidJsonEntry(format!("{}.id", entry))) + let pox_info_data = response.json::()?; + Ok(pox_info_data) + } + + /// Helper function to retrieve the current reward cycle number from the stacks node + fn get_current_reward_cycle(&self) -> Result { + let pox_data = self.get_pox_data()?; + Ok(pox_data.reward_cycle_id) } /// Helper function to retrieve the next possible nonce for the signer from the stacks node @@ -124,25 +137,10 @@ impl StacksClient { if let Some(pox_contract) = self.pox_contract_id.clone() { return Ok((pox_contract.issuer.into(), pox_contract.name)); } - // TODO: we may want to cache the pox contract inside the client itself (calling this function once on init) - // https://github.com/stacks-network/stacks-blockchain/issues/4005 - let send_request = || { - self.stacks_node_client - .get(self.pox_path()) - .send() - .map_err(backoff::Error::transient) - }; - let response = retry_with_exponential_backoff(send_request)?; - if !response.status().is_success() { - return Err(ClientError::RequestFailure(response.status())); - } - let json_response = response.json::()?; - let entry = "contract_id"; - let contract_id_string = json_response - .get(entry) - .and_then(|id: &serde_json::Value| id.as_str()) - .ok_or_else(|| ClientError::InvalidJsonEntry(entry.to_string()))?; - let id = QualifiedContractIdentifier::parse(contract_id_string).unwrap(); + let pox_data = self.get_pox_data()?; + let contract_id = pox_data.contract_id.as_str(); + let err_msg = format!("Stacks node returned an invalid pox contract id: {contract_id}"); + let id = QualifiedContractIdentifier::parse(contract_id).expect(&err_msg); Ok((id.issuer.into(), id.name)) } @@ -281,27 +279,16 @@ impl StacksClient { if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } - let response = response.json::()?; - if !response - .get("okay") - .map(|val| val.as_bool().unwrap_or(false)) - .unwrap_or(false) - { - let cause = response - .get("cause") - .ok_or(ClientError::InvalidJsonEntry("cause".to_string()))?; + let call_read_only_response = response.json::()?; + if !call_read_only_response.okay { return Err(ClientError::ReadOnlyFailure(format!( - "{}: {}", - function_name, cause + "{function_name}: {}", + call_read_only_response + .cause + .unwrap_or("unknown".to_string()) ))); } - let result = response - .get("result") - .ok_or(ClientError::InvalidJsonEntry("result".to_string()))? - .as_str() - .ok_or_else(|| ClientError::ReadOnlyFailure("Expected string result.".to_string()))? - .to_string(); - Ok(result) + Ok(call_read_only_response.result.unwrap_or_default()) } fn pox_path(&self) -> String { @@ -325,7 +312,7 @@ impl StacksClient { } fn block_proposal_path(&self) -> String { - format!("{}/v2/block-proposal", self.http_origin) + format!("{}/v2/block_proposal", self.http_origin) } } @@ -479,7 +466,7 @@ mod tests { let h = spawn(move || config.client.get_pox_contract()); write_response( config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\"}", + b"HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"pox_activation_threshold_ustx\":829371801288885,\"first_burnchain_block_height\":2000000,\"current_burnchain_block_height\":2572192,\"prepare_phase_block_length\":50,\"reward_phase_block_length\":1000,\"reward_slots\":2000,\"rejection_fraction\":12,\"total_liquid_supply_ustx\":41468590064444294,\"current_cycle\":{\"id\":544,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":853258144644000,\"is_pox_active\":true},\"next_cycle\":{\"id\":545,\"min_threshold_ustx\":5190000000000,\"min_increment_ustx\":5183573758055,\"stacked_ustx\":847278759574000,\"prepare_phase_start_block_height\":2572200,\"blocks_until_prepare_phase\":8,\"reward_phase_start_block_height\":2572250,\"blocks_until_reward_phase\":58,\"ustx_until_pox_rejection\":4976230807733304},\"min_amount_ustx\":5190000000000,\"prepare_cycle_length\":50,\"reward_cycle_id\":544,\"reward_cycle_length\":1050,\"rejection_votes_left_required\":4976230807733304,\"next_reward_cycle_in\":58,\"contract_versions\":[{\"contract_id\":\"ST000000000000000000002AMW42H.pox\",\"activation_burnchain_block_height\":2000000,\"first_reward_cycle_id\":0},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-2\",\"activation_burnchain_block_height\":2422102,\"first_reward_cycle_id\":403},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"activation_burnchain_block_height\":2432545,\"first_reward_cycle_id\":412}]}", ); let (address, name) = h.join().unwrap().unwrap(); assert_eq!( @@ -494,10 +481,10 @@ mod tests { let h = spawn(move || config.client.get_current_reward_cycle()); write_response( config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"id\":506,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":5690000000000,\"is_pox_active\":false}}", + b"HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"pox_activation_threshold_ustx\":829371801288885,\"first_burnchain_block_height\":2000000,\"current_burnchain_block_height\":2572192,\"prepare_phase_block_length\":50,\"reward_phase_block_length\":1000,\"reward_slots\":2000,\"rejection_fraction\":12,\"total_liquid_supply_ustx\":41468590064444294,\"current_cycle\":{\"id\":544,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":853258144644000,\"is_pox_active\":true},\"next_cycle\":{\"id\":545,\"min_threshold_ustx\":5190000000000,\"min_increment_ustx\":5183573758055,\"stacked_ustx\":847278759574000,\"prepare_phase_start_block_height\":2572200,\"blocks_until_prepare_phase\":8,\"reward_phase_start_block_height\":2572250,\"blocks_until_reward_phase\":58,\"ustx_until_pox_rejection\":4976230807733304},\"min_amount_ustx\":5190000000000,\"prepare_cycle_length\":50,\"reward_cycle_id\":544,\"reward_cycle_length\":1050,\"rejection_votes_left_required\":4976230807733304,\"next_reward_cycle_in\":58,\"contract_versions\":[{\"contract_id\":\"ST000000000000000000002AMW42H.pox\",\"activation_burnchain_block_height\":2000000,\"first_reward_cycle_id\":0},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-2\",\"activation_burnchain_block_height\":2422102,\"first_reward_cycle_id\":403},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"activation_burnchain_block_height\":2432545,\"first_reward_cycle_id\":412}]}", ); let current_cycle_id = h.join().unwrap().unwrap(); - assert_eq!(506, current_cycle_id); + assert_eq!(544, current_cycle_id); } #[test] @@ -509,7 +496,7 @@ mod tests { b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"id\":\"fake id\", \"is_pox_active\":false}}", ); let res = h.join().unwrap(); - assert!(matches!(res, Err(ClientError::InvalidJsonEntry(_)))); + assert!(matches!(res, Err(ClientError::ReqwestError(_)))); } #[test] @@ -521,7 +508,7 @@ mod tests { b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"is_pox_active\":false}}", ); let res = h.join().unwrap(); - assert!(matches!(res, Err(ClientError::InvalidJsonEntry(_)))); + assert!(matches!(res, Err(ClientError::ReqwestError(_)))); } #[test] From 8da428b312adb70706b02c7f82ce22bfbe178186 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 10 Jan 2024 11:35:29 -0500 Subject: [PATCH 12/27] is_valid_nakamoto_block should be making a post not a get request Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/stacks_client.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index f6aed8b137..28a60c59f5 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -7,7 +7,7 @@ use blockstack_lib::chainstate::stacks::{ }; use blockstack_lib::net::api::callreadonly::CallReadOnlyResponse; use blockstack_lib::net::api::getpoxinfo::RPCPoxInfoData; -use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; +use blockstack_lib::net::api::postblock_proposal::{BlockValidateResponse, NakamotoBlockProposal}; use clarity::vm::types::{QualifiedContractIdentifier, SequenceData}; use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; use serde_json::json; @@ -61,17 +61,25 @@ impl StacksClient { } /// Check if the proposed Nakamoto block is a valid block - pub fn is_valid_nakamoto_block(&self, _block: &NakamotoBlock) -> Result { + pub fn is_valid_nakamoto_block(&self, block: NakamotoBlock) -> Result { + let block_proposal = NakamotoBlockProposal { + block, + chain_id: self.chain_id, + }; let send_request = || { self.stacks_node_client - .get(self.block_proposal_path()) + .post(&self.block_proposal_path()) + .header("Content-Type", "application/json") + .json(&block_proposal) .send() .map_err(backoff::Error::transient) }; + let response = retry_with_exponential_backoff(send_request)?; if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } + // TODO: this is actually an aysnc call. It will not return the JSON response as below. It uses the event dispatcher instead let validate_block_response = response.json::()?; match validate_block_response { BlockValidateResponse::Ok(validate_block_ok) => { From 17e137a98ba7ee4aa276aceb1fce4d4e9ad493cd Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 10 Jan 2024 18:27:55 -0500 Subject: [PATCH 13/27] WIP: add block events to libsigner Signed-off-by: Jacinta Ferrant --- Cargo.lock | 6 +- Cargo.toml | 2 +- libsigner/Cargo.toml | 3 + libsigner/src/events.rs | 111 +++++++++++------- libsigner/src/libsigner.rs | 3 +- libsigner/src/runloop.rs | 8 +- libsigner/src/tests/mod.rs | 51 ++++---- stacks-signer/src/client/stacks_client.rs | 31 ++--- stacks-signer/src/config.rs | 4 +- stacks-signer/src/main.rs | 11 +- stackslib/Cargo.toml | 1 - stackslib/src/net/api/poststackerdbchunk.rs | 9 ++ testnet/stacks-node/src/event_dispatcher.rs | 2 +- .../stacks-node/src/nakamoto_node/miner.rs | 2 +- .../src/tests/neon_integrations.rs | 5 +- testnet/stacks-node/src/tests/signer.rs | 10 +- 16 files changed, 151 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a22e40e3f5..5d5061bb7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,6 +1971,7 @@ dependencies = [ name = "libsigner" version = "0.0.1" dependencies = [ + "bincode", "clarity", "libc", "libstackerdb", @@ -1984,8 +1985,10 @@ dependencies = [ "slog-json", "slog-term", "stacks-common", + "stackslib", "thiserror", "tiny_http", + "wsts", ] [[package]] @@ -3616,7 +3619,6 @@ dependencies = [ "integer-sqrt", "lazy_static", "libc", - "libsigner", "libstackerdb", "mio 0.6.23", "nix", @@ -4714,8 +4716,6 @@ dependencies = [ [[package]] name = "wsts" version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2cb1ef1b26d526daae40c1ee657c83bbedaeefd7196f827b40ca79d13f0f34" dependencies = [ "aes-gcm 0.10.2", "bs58 0.5.0", diff --git a/Cargo.toml b/Cargo.toml index e409b94158..ebc7261cf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ # Dependencies we want to keep the same between workspace members [workspace.dependencies] -wsts = "6.0" +wsts = { path = "../wsts" } rand_core = "0.6" rand = "0.8" diff --git a/libsigner/Cargo.toml b/libsigner/Cargo.toml index 8500ef55fa..73fad53a8b 100644 --- a/libsigner/Cargo.toml +++ b/libsigner/Cargo.toml @@ -17,6 +17,7 @@ path = "./src/libsigner.rs" [dependencies] clarity = { path = "../clarity" } +bincode = "1.3.3" libc = "0.2" libstackerdb = { path = "../libstackerdb" } serde = "1" @@ -26,8 +27,10 @@ slog = { version = "2.5.2", features = [ "max_level_trace" ] } slog-term = "2.6.0" slog-json = { version = "2.3.0", optional = true } stacks-common = { path = "../stacks-common" } +stackslib = { path = "../stackslib"} thiserror = "1.0" tiny_http = "0.12" +wsts = { workspace = true } [dependencies.serde_json] version = "1.0" diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index 23f5d0e4bf..a86d47df59 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -20,23 +20,31 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::Sender; use std::sync::Arc; +use blockstack_lib::chainstate::nakamoto::NakamotoBlock; +use blockstack_lib::chainstate::stacks::boot::MINERS_NAME; +use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; +use blockstack_lib::net::api::poststackerdbchunk::StackerDBChunksEvent; +use blockstack_lib::util_lib::boot::boot_code_id; use clarity::vm::types::QualifiedContractIdentifier; -use libstackerdb::StackerDBChunkData; use serde::{Deserialize, Serialize}; +use stacks_common::codec::{ + read_next, read_next_at_most, write_next, Error as CodecError, StacksMessageCodec, +}; use tiny_http::{ Method as HttpMethod, Request as HttpRequest, Response as HttpResponse, Server as HttpServer, }; +use wsts::net::{Message, Packet}; use crate::http::{decode_http_body, decode_http_request}; use crate::EventError; -/// Event structure for newly-arrived StackerDB data +/// Event enum for newly-arrived signer subscribed events #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct StackerDBChunksEvent { - /// The contract ID for the StackerDB instance - pub contract_id: QualifiedContractIdentifier, - /// The chunk data for newly-modified slots - pub modified_slots: Vec, +pub enum SignerEvent { + /// A new stackerDB chunk was received + StackerDB(StackerDBChunksEvent), + /// A new block proposal was received + BlockProposal(BlockValidateResponse), } /// Trait to implement a stop-signaler for the event receiver thread. @@ -47,7 +55,7 @@ pub trait EventStopSignaler { fn send(&mut self); } -/// Trait to implement to handle StackerDB events sent by the Stacks node +/// Trait to implement to handle StackerDB and BlockProposal events sent by the Stacks node pub trait EventReceiver { /// The implementation of ST will ensure that a call to ST::send() will cause /// the call to `is_stopped()` below to return true. @@ -56,11 +64,11 @@ pub trait EventReceiver { /// Open a server socket to the given socket address. fn bind(&mut self, listener: SocketAddr) -> Result; /// Return the next event - fn next_event(&mut self) -> Result; + fn next_event(&mut self) -> Result; /// Add a downstream event consumer - fn add_consumer(&mut self, event_out: Sender); + fn add_consumer(&mut self, event_out: Sender); /// Forward the event to downstream consumers - fn forward_event(&mut self, ev: StackerDBChunksEvent) -> bool; + fn forward_event(&mut self, ev: SignerEvent) -> bool; /// Determine if the receiver should hang up fn is_stopped(&self) -> bool; /// Get a stop signal instance that, when sent, will cause this receiver to stop accepting new @@ -100,25 +108,25 @@ pub trait EventReceiver { } } -/// Event receiver for StackerDB events -pub struct StackerDBEventReceiver { - /// contracts we're listening for +/// Event receiver for Signer events +pub struct SignerEventReceiver { + /// stacker db contracts we're listening for pub stackerdb_contract_ids: Vec, /// Address we bind to local_addr: Option, /// server socket that listens for HTTP POSTs from the node http_server: Option, /// channel into which to write newly-discovered data - out_channels: Vec>, + out_channels: Vec>, /// inter-thread stop variable -- if set to true, then the `main_loop` will exit stop_signal: Arc, } -impl StackerDBEventReceiver { - /// Make a new StackerDB event receiver, and return both the receiver and the read end of a +impl SignerEventReceiver { + /// Make a new Signer event receiver, and return both the receiver and the read end of a /// channel into which node-received data can be obtained. - pub fn new(contract_ids: Vec) -> StackerDBEventReceiver { - StackerDBEventReceiver { + pub fn new(contract_ids: Vec) -> SignerEventReceiver { + SignerEventReceiver { stackerdb_contract_ids: contract_ids, http_server: None, local_addr: None, @@ -130,7 +138,7 @@ impl StackerDBEventReceiver { /// Do something with the socket pub fn with_server(&mut self, todo: F) -> Result where - F: FnOnce(&mut StackerDBEventReceiver, &mut HttpServer) -> R, + F: FnOnce(&mut SignerEventReceiver, &mut HttpServer) -> R, { let mut server = if let Some(s) = self.http_server.take() { s @@ -146,22 +154,22 @@ impl StackerDBEventReceiver { } /// Stop signaler implementation -pub struct StackerDBStopSignaler { +pub struct SignerStopSignaler { stop_signal: Arc, local_addr: SocketAddr, } -impl StackerDBStopSignaler { +impl SignerStopSignaler { /// Make a new stop signaler - pub fn new(sig: Arc, local_addr: SocketAddr) -> StackerDBStopSignaler { - StackerDBStopSignaler { + pub fn new(sig: Arc, local_addr: SocketAddr) -> SignerStopSignaler { + SignerStopSignaler { stop_signal: sig, local_addr, } } } -impl EventStopSignaler for StackerDBStopSignaler { +impl EventStopSignaler for SignerStopSignaler { fn send(&mut self) { self.stop_signal.store(true, Ordering::SeqCst); // wake up the thread so the atomicbool can be checked @@ -179,8 +187,8 @@ impl EventStopSignaler for StackerDBStopSignaler { } } -impl EventReceiver for StackerDBEventReceiver { - type ST = StackerDBStopSignaler; +impl EventReceiver for SignerEventReceiver { + type ST = SignerStopSignaler; /// Start listening on the given socket address. /// Returns the address that was bound. @@ -194,7 +202,7 @@ impl EventReceiver for StackerDBEventReceiver { /// Wait for the node to post something, and then return it. /// Errors are recoverable -- the caller should call this method again even if it returns an /// error. - fn next_event(&mut self) -> Result { + fn next_event(&mut self) -> Result { self.with_server(|event_receiver, http_server| { let mut request = http_server.recv()?; @@ -209,27 +217,31 @@ impl EventReceiver for StackerDBEventReceiver { &request.method(), ))); } - if request.url() != "/stackerdb_chunks" { - let url = request.url().to_string(); + if request.url() == "/stackerdb_chunks" { + let mut body = String::new(); + request + .as_reader() + .read_to_string(&mut body) + .expect("failed to read body"); - info!( - "[{:?}] next_event got request with unexpected url {}, return OK so other side doesn't keep sending this", - event_receiver.local_addr, - request.url() - ); + let event: StackerDBChunksEvent = + serde_json::from_slice(body.as_bytes()).map_err(|e| { + EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)) + })?; request .respond(HttpResponse::empty(200u16)) .expect("response failed"); - Err(EventError::UnrecognizedEvent(url)) - } else { + + Ok(SignerEvent::StackerDB(event)) + } else if request.url() == "/proposal_response" { let mut body = String::new(); request .as_reader() .read_to_string(&mut body) .expect("failed to read body"); - let event: StackerDBChunksEvent = + let event: BlockValidateResponse = serde_json::from_slice(body.as_bytes()).map_err(|e| { EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)) })?; @@ -238,7 +250,20 @@ impl EventReceiver for StackerDBEventReceiver { .respond(HttpResponse::empty(200u16)) .expect("response failed"); - Ok(event) + Ok(SignerEvent::BlockProposal(event)) + } else { + let url = request.url().to_string(); + + info!( + "[{:?}] next_event got request with unexpected url {}, return OK so other side doesn't keep sending this", + event_receiver.local_addr, + request.url() + ); + + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + Err(EventError::UnrecognizedEvent(url)) } })? } @@ -251,7 +276,7 @@ impl EventReceiver for StackerDBEventReceiver { /// Forward an event /// Return true on success; false on error. /// Returning false terminates the event receiver. - fn forward_event(&mut self, ev: StackerDBChunksEvent) -> bool { + fn forward_event(&mut self, ev: SignerEvent) -> bool { if self.out_channels.is_empty() { // nothing to do error!("No channels connected to event receiver"); @@ -275,15 +300,15 @@ impl EventReceiver for StackerDBEventReceiver { } /// Add an event consumer. A received event will be forwarded to this Sender. - fn add_consumer(&mut self, out_channel: Sender) { + fn add_consumer(&mut self, out_channel: Sender) { self.out_channels.push(out_channel); } /// Get a stopped signaler. The caller can then use it to terminate the event receiver loop, /// even if it's in a different thread. - fn get_stop_signaler(&mut self) -> Result { + fn get_stop_signaler(&mut self) -> Result { if let Some(local_addr) = self.local_addr { - Ok(StackerDBStopSignaler::new( + Ok(SignerStopSignaler::new( self.stop_signal.clone(), local_addr, )) diff --git a/libsigner/src/libsigner.rs b/libsigner/src/libsigner.rs index 3ab25f46e9..b7f983f8c3 100644 --- a/libsigner/src/libsigner.rs +++ b/libsigner/src/libsigner.rs @@ -44,8 +44,7 @@ mod session; pub use crate::error::{EventError, RPCError}; pub use crate::events::{ - EventReceiver, EventStopSignaler, StackerDBChunksEvent, StackerDBEventReceiver, - StackerDBStopSignaler, + EventReceiver, EventStopSignaler, SignerEvent, SignerEventReceiver, SignerStopSignaler, }; pub use crate::runloop::{RunningSigner, Signer, SignerRunLoop}; pub use crate::session::{SignerSession, StackerDBSession}; diff --git a/libsigner/src/runloop.rs b/libsigner/src/runloop.rs index 2f4bbcf46b..d1a2474a33 100644 --- a/libsigner/src/runloop.rs +++ b/libsigner/src/runloop.rs @@ -28,7 +28,7 @@ use stacks_common::deps_common::ctrlc as termination; use stacks_common::deps_common::ctrlc::SignalId; use crate::error::EventError; -use crate::events::{EventReceiver, EventStopSignaler, StackerDBChunksEvent}; +use crate::events::{EventReceiver, EventStopSignaler, SignerEvent}; /// Some libcs, like musl, have a very small stack size. /// Make sure it's big enough. @@ -45,12 +45,12 @@ pub trait SignerRunLoop { fn set_event_timeout(&mut self, timeout: Duration); /// Getter for the event poll timeout fn get_event_timeout(&self) -> Duration; - /// Run one pass of the event loop, given new StackerDB events discovered since the last pass. + /// Run one pass of the event loop, given new Signer events discovered since the last pass. /// Returns Some(R) if this is the final pass -- the runloop evaluated to R /// Returns None to keep running. fn run_one_pass( &mut self, - event: Option, + event: Option, cmd: Option, res: Sender, ) -> Option; @@ -64,7 +64,7 @@ pub trait SignerRunLoop { /// This would run in a separate thread from the event receiver. fn main_loop( &mut self, - event_recv: Receiver, + event_recv: Receiver, command_recv: Receiver, result_send: Sender, mut event_stop_signaler: EVST, diff --git a/libsigner/src/tests/mod.rs b/libsigner/src/tests/mod.rs index ffe8d4d9ee..3e16bf4729 100644 --- a/libsigner/src/tests/mod.rs +++ b/libsigner/src/tests/mod.rs @@ -22,18 +22,20 @@ use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; use std::{mem, thread}; +use blockstack_lib::net::api::poststackerdbchunk::StackerDBChunksEvent; use clarity::vm::types::QualifiedContractIdentifier; use libstackerdb::StackerDBChunkData; use stacks_common::util::secp256k1::Secp256k1PrivateKey; use stacks_common::util::sleep_ms; -use crate::{Signer, SignerRunLoop, StackerDBChunksEvent, StackerDBEventReceiver}; +use crate::events::SignerEvent; +use crate::{Signer, SignerEventReceiver, SignerRunLoop}; /// Simple runloop implementation. It receives `max_events` events and returns `events` from the /// last call to `run_one_pass` as its final state. struct SimpleRunLoop { poll_timeout: Duration, - events: Vec, + events: Vec, max_events: usize, } @@ -51,7 +53,7 @@ enum Command { Empty, } -impl SignerRunLoop, Command> for SimpleRunLoop { +impl SignerRunLoop, Command> for SimpleRunLoop { fn set_event_timeout(&mut self, timeout: Duration) { self.poll_timeout = timeout; } @@ -62,10 +64,10 @@ impl SignerRunLoop, Command> for SimpleRunLoop { fn run_one_pass( &mut self, - event: Option, + event: Option, _cmd: Option, - _res: Sender>, - ) -> Option> { + _res: Sender>, + ) -> Option> { debug!("Got event: {:?}", &event); if let Some(event) = event { self.events.push(event); @@ -85,7 +87,7 @@ impl SignerRunLoop, Command> for SimpleRunLoop { /// and the signer runloop. #[test] fn test_simple_signer() { - let ev = StackerDBEventReceiver::new(vec![QualifiedContractIdentifier::parse( + let ev = SignerEventReceiver::new(vec![QualifiedContractIdentifier::parse( "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world", ) .unwrap()]); @@ -100,13 +102,13 @@ fn test_simple_signer() { let mut chunk = StackerDBChunkData::new(i as u32, 1, "hello world".as_bytes().to_vec()); chunk.sign(&privk).unwrap(); - let chunk_event = StackerDBChunksEvent { + let chunk_event = SignerEvent::StackerDB(StackerDBChunksEvent { contract_id: QualifiedContractIdentifier::parse( "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world", ) .unwrap(), modified_slots: vec![chunk], - }; + }); chunks.push(chunk_event); } @@ -124,14 +126,19 @@ fn test_simple_signer() { } }; - let body = serde_json::to_string(&thread_chunks[num_sent]).unwrap(); - let req = format!("POST /stackerdb_chunks HTTP/1.0\r\nConnection: close\r\nContent-Length: {}\r\n\r\n{}", &body.len(), body); - debug!("Send:\n{}", &req); + match &thread_chunks[num_sent] { + SignerEvent::StackerDB(ev) => { + let body = serde_json::to_string(ev).unwrap(); + let req = format!("POST /stackerdb_chunks HTTP/1.0\r\nConnection: close\r\nContent-Length: {}\r\n\r\n{}", &body.len(), body); + debug!("Send:\n{}", &req); - sock.write_all(req.as_bytes()).unwrap(); - sock.flush().unwrap(); + sock.write_all(req.as_bytes()).unwrap(); + sock.flush().unwrap(); - num_sent += 1; + num_sent += 1; + } + _ => panic!("Unexpected event type"), + } } }); @@ -139,17 +146,19 @@ fn test_simple_signer() { sleep_ms(5000); let mut accepted_events = running_signer.stop().unwrap(); - chunks.sort_by(|ev1, ev2| { - ev1.modified_slots[0] + chunks.sort_by(|ev1, ev2| match (ev1, ev2) { + (SignerEvent::StackerDB(ev1), SignerEvent::StackerDB(ev2)) => ev1.modified_slots[0] .slot_id .partial_cmp(&ev2.modified_slots[0].slot_id) - .unwrap() + .unwrap(), + _ => panic!("Unexpected event type"), }); - accepted_events.sort_by(|ev1, ev2| { - ev1.modified_slots[0] + accepted_events.sort_by(|ev1, ev2| match (ev1, ev2) { + (SignerEvent::StackerDB(ev1), SignerEvent::StackerDB(ev2)) => ev1.modified_slots[0] .slot_id .partial_cmp(&ev2.modified_slots[0].slot_id) - .unwrap() + .unwrap(), + _ => panic!("Unexpected event type"), }); // runloop got the event that the mocked stacks node sent diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 28a60c59f5..d80bbe9269 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -7,7 +7,7 @@ use blockstack_lib::chainstate::stacks::{ }; use blockstack_lib::net::api::callreadonly::CallReadOnlyResponse; use blockstack_lib::net::api::getpoxinfo::RPCPoxInfoData; -use blockstack_lib::net::api::postblock_proposal::{BlockValidateResponse, NakamotoBlockProposal}; +use blockstack_lib::net::api::postblock_proposal::NakamotoBlockProposal; use clarity::vm::types::{QualifiedContractIdentifier, SequenceData}; use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; use serde_json::json; @@ -60,15 +60,15 @@ impl StacksClient { todo!("Get the miner public key from the stacks node to verify the miner blocks were signed by the correct miner"); } - /// Check if the proposed Nakamoto block is a valid block - pub fn is_valid_nakamoto_block(&self, block: NakamotoBlock) -> Result { + /// Submit the block proposal to the stacks node. The block will be validated and returned via the HTTP endpoint for Block events. + pub fn submit_block_for_validation(&self, block: NakamotoBlock) -> Result<(), ClientError> { let block_proposal = NakamotoBlockProposal { block, chain_id: self.chain_id, }; let send_request = || { self.stacks_node_client - .post(&self.block_proposal_path()) + .post(self.block_proposal_path()) .header("Content-Type", "application/json") .json(&block_proposal) .send() @@ -80,17 +80,18 @@ impl StacksClient { return Err(ClientError::RequestFailure(response.status())); } // TODO: this is actually an aysnc call. It will not return the JSON response as below. It uses the event dispatcher instead - let validate_block_response = response.json::()?; - match validate_block_response { - BlockValidateResponse::Ok(validate_block_ok) => { - debug!("Block validation succeeded: {:?}", validate_block_ok); - Ok(true) - } - BlockValidateResponse::Reject(validate_block_reject) => { - debug!("Block validation failed: {:?}", validate_block_reject); - Ok(false) - } - } + // let validate_block_response = response.json::()?; + // match validate_block_response { + // BlockValidateResponse::Ok(validate_block_ok) => { + // debug!("Block validation succeeded: {:?}", validate_block_ok); + // Ok(true) + // } + // BlockValidateResponse::Reject(validate_block_reject) => { + // debug!("Block validation failed: {:?}", validate_block_reject); + // Ok(false) + // } + // } + Ok(()) } /// Retrieve the current DKG aggregate public key diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index c298ead275..4e1a53dffe 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -97,7 +97,7 @@ impl Network { pub struct Config { /// endpoint to the stacks node pub node_host: SocketAddr, - /// endpoint to the stackerdb receiver + /// endpoint to the event receiver pub endpoint: SocketAddr, /// smart contract that controls the target signers' stackerdb pub signers_stackerdb_contract_id: QualifiedContractIdentifier, @@ -143,7 +143,7 @@ struct RawSigners { struct RawConfigFile { /// endpoint to stacks node pub node_host: String, - /// endpoint to stackerdb receiver + /// endpoint to event receiver pub endpoint: String, // FIXME: these contract's should go away in non testing scenarios. Make them both optionals. /// Signers' Stacker db contract identifier diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 18ef2ca6f7..5187f9a522 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -35,7 +35,7 @@ use std::time::Duration; use clap::Parser; use clarity::vm::types::QualifiedContractIdentifier; -use libsigner::{RunningSigner, Signer, SignerSession, StackerDBEventReceiver, StackerDBSession}; +use libsigner::{RunningSigner, Signer, SignerEventReceiver, SignerSession, StackerDBSession}; use libstackerdb::StackerDBChunkData; use slog::slog_debug; use stacks_common::address::{ @@ -58,7 +58,7 @@ use wsts::state_machine::OperationResult; use wsts::v2; struct SpawnedSigner { - running_signer: RunningSigner>, + running_signer: RunningSigner>, cmd_send: Sender, res_recv: Receiver>, } @@ -88,16 +88,15 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let config = Config::try_from(path).unwrap(); let (cmd_send, cmd_recv) = channel(); let (res_send, res_recv) = channel(); - let ev = StackerDBEventReceiver::new(vec![config.signers_stackerdb_contract_id.clone()]); + let ev = SignerEventReceiver::new(vec![config.signers_stackerdb_contract_id.clone()]); let runloop: RunLoop> = RunLoop::from(&config); let mut signer: Signer< RunLoopCommand, Vec, RunLoop>, - StackerDBEventReceiver, + SignerEventReceiver, > = Signer::new(runloop, ev, cmd_recv, res_send); - let endpoint = config.endpoint; - let running_signer = signer.spawn(endpoint).unwrap(); + let running_signer = signer.spawn(config.endpoint).unwrap(); SpawnedSigner { running_signer, cmd_send, diff --git a/stackslib/Cargo.toml b/stackslib/Cargo.toml index 748f5b07d3..c505d8429b 100644 --- a/stackslib/Cargo.toml +++ b/stackslib/Cargo.toml @@ -54,7 +54,6 @@ clarity = { path = "../clarity" } stacks-common = { path = "../stacks-common" } pox-locking = { path = "../pox-locking" } libstackerdb = { path = "../libstackerdb" } -libsigner = { path = "../libsigner" } siphasher = "0.3.7" wsts = {workspace = true} rand_core = {workspace = true} diff --git a/stackslib/src/net/api/poststackerdbchunk.rs b/stackslib/src/net/api/poststackerdbchunk.rs index 3ca82b4141..a006cf386b 100644 --- a/stackslib/src/net/api/poststackerdbchunk.rs +++ b/stackslib/src/net/api/poststackerdbchunk.rs @@ -54,6 +54,15 @@ use crate::net::{ }; use crate::util_lib::db::{DBConn, Error as DBError}; +/// Event structure for newly-arrived StackerDB data +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StackerDBChunksEvent { + /// The contract ID for the StackerDB instance + pub contract_id: QualifiedContractIdentifier, + /// The chunk data for newly-modified slots + pub modified_slots: Vec, +} + #[derive(Clone)] pub struct RPCPostStackerDBChunkRequestHandler { pub contract_identifier: Option, diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index 3619970fc0..98c08ba4a0 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -10,7 +10,6 @@ use clarity::vm::costs::ExecutionCost; use clarity::vm::events::{FTEventType, NFTEventType, STXEventType}; use clarity::vm::types::{AssetIdentifier, QualifiedContractIdentifier, Value}; use http_types::{Method, Request, Url}; -pub use libsigner::StackerDBChunksEvent; use serde_json::json; use stacks::burnchains::{PoxConstants, Txid}; use stacks::chainstate::burn::operations::BlockstackOperationType; @@ -33,6 +32,7 @@ use stacks::libstackerdb::StackerDBChunkData; use stacks::net::api::postblock_proposal::{ BlockValidateOk, BlockValidateReject, BlockValidateResponse, }; +use stacks::net::api::poststackerdbchunk::StackerDBChunksEvent; use stacks::net::atlas::{Attachment, AttachmentInstance}; use stacks::net::stackerdb::StackerDBEventDispatcher; use stacks_common::codec::StacksMessageCodec; diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 03e3e29bc2..cc9b25c61b 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -196,7 +196,7 @@ impl BlockMinerThread { let miner_contract_id = boot_code_id(MINERS_NAME, self.config.is_mainnet()); let mut miners_stackerdb = StackerDBSession::new(rpc_sock, miner_contract_id); - match miners_stackerdb.put_chunk(chunk) { + match miners_stackerdb.put_chunk(&chunk) { Ok(ack) => { info!("Proposed block to stackerdb: {ack:?}"); } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 13c5c10573..c9d529bf21 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -178,12 +178,11 @@ pub mod test_observer { use std::thread; use stacks::net::api::postblock_proposal::BlockValidateResponse; + use stacks::net::api::poststackerdbchunk::StackerDBChunksEvent; use warp::Filter; use {tokio, warp}; - use crate::event_dispatcher::{ - MinedBlockEvent, MinedMicroblockEvent, MinedNakamotoBlockEvent, StackerDBChunksEvent, - }; + use crate::event_dispatcher::{MinedBlockEvent, MinedMicroblockEvent, MinedNakamotoBlockEvent}; pub const EVENT_OBSERVER_PORT: u16 = 50303; diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 933e3e0c6f..4a4e6e55c9 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -3,7 +3,7 @@ use std::time::Duration; use std::{env, thread}; use clarity::vm::types::QualifiedContractIdentifier; -use libsigner::{RunningSigner, Signer, StackerDBEventReceiver}; +use libsigner::{RunningSigner, Signer, SignerEventReceiver}; use stacks::chainstate::stacks::StacksPrivateKey; use stacks_common::types::chainstate::StacksAddress; use stacks_signer::client::{MINER_SLOTS_PER_USER, SIGNER_SLOTS_PER_USER}; @@ -37,9 +37,9 @@ fn spawn_signer( data: &str, receiver: Receiver, sender: Sender>, -) -> RunningSigner> { +) -> RunningSigner> { let config = stacks_signer::config::Config::load_from_str(data).unwrap(); - let ev = StackerDBEventReceiver::new(vec![ + let ev = SignerEventReceiver::new(vec![ config.miners_stackerdb_contract_id.clone(), config.signers_stackerdb_contract_id.clone(), ]); @@ -49,7 +49,7 @@ fn spawn_signer( RunLoopCommand, Vec, stacks_signer::runloop::RunLoop>, - StackerDBEventReceiver, + SignerEventReceiver, > = Signer::new(runloop, ev, receiver, sender); let endpoint = config.endpoint; info!( @@ -78,7 +78,7 @@ fn setup_stx_btc_node( conf.events_observers.insert(EventObserverConfig { endpoint: format!("{}", signer_config.endpoint), - events_keys: vec![EventKeyType::StackerDBChunks], + events_keys: vec![EventKeyType::StackerDBChunks, EventKeyType::BlockProposal], }); } From b921f2701577e0f79c247632d575a924c17e2b18 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 10 Jan 2024 18:43:48 -0500 Subject: [PATCH 14/27] Add block events to libsigner Signed-off-by: Jacinta Ferrant --- stacks-signer/src/runloop.rs | 79 ++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index d119cc1cc0..9ffcd649f7 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -2,8 +2,10 @@ use std::collections::VecDeque; use std::sync::mpsc::Sender; use std::time::Duration; +use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; +use blockstack_lib::net::api::poststackerdbchunk::StackerDBChunksEvent; use hashbrown::{HashMap, HashSet}; -use libsigner::{SignerRunLoop, StackerDBChunksEvent}; +use libsigner::{SignerEvent, SignerRunLoop}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; use stacks_common::codec::{read_next, StacksMessageCodec}; use stacks_common::{debug, error, info, warn}; @@ -169,11 +171,26 @@ impl RunLoop { } } + /// Handle block proposal from the miners stacker-db contract + fn handle_block_validate_response(&mut self, block_validate_response: BlockValidateResponse) { + match block_validate_response { + BlockValidateResponse::Ok(block_validate_ok) => { + // This is a valid block proposal from the miner. Trigger a signing round for it. + self.commands.push_back(RunLoopCommand::Sign { + message: block_validate_ok.block.serialize_to_vec(), + is_taproot: false, + merkle_root: None, + }); + } + BlockValidateResponse::Reject(_block_validate_reject) => { + // TODO: send a message to the miner to let them know their block was rejected + todo!("Send a message to the miner to let them know their block was rejected"); + } + } + } + /// Process the event as a miner message from the miner stacker-db fn process_event_miner(&mut self, event: &StackerDBChunksEvent) { - // Determine the current coordinator id and public key for verification - let (coordinator_id, _coordinator_public_key) = - calculate_coordinator(&self.signing_round.public_keys); event.modified_slots.iter().for_each(|chunk| { let mut ptr = &chunk.data[..]; let Some(stacker_db_message) = read_next::(&mut ptr).ok() else { @@ -189,17 +206,10 @@ impl RunLoop { } StackerDBMessage::Block(block) => { // Received a block proposal from the miner. - // If the signer is the coordinator, then trigger a Signing round for the block - if coordinator_id == self.signing_round.signer_id { - let is_valid_block = self.stacks_client.is_valid_nakamoto_block(&block).unwrap_or_else(|e| { + // Submit it to the stacks node to validate it before triggering a signing round. + self.stacks_client.submit_block_for_validation(block).unwrap_or_else(|e| { warn!("Failed to validate block: {:?}", e); - false }); - // Don't bother triggering a signing round for the block if it is invalid - if !is_valid_block { - warn!("Received an invalid block proposal from the miner. Ignoring block proposal: {:?}", block); - return; - } // TODO: dependent on https://github.com/stacks-network/stacks-core/issues/4018 // let miner_public_key = self.stacks_client.get_miner_public_key().expect("Failed to get miner public key. Cannot verify blocks."); @@ -212,19 +222,28 @@ impl RunLoop { // return; // } - // This is a block proposal from the miner. Trigger a signing round for it. - self.commands.push_back(RunLoopCommand::Sign { - message: block.serialize_to_vec(), - is_taproot: false, - merkle_root: None, - }); - } } } }); } /// Process the event as a signer message from the signer stacker-db + fn handle_stackerdb_event(&mut self, event: &StackerDBChunksEvent) -> Vec { + if event.contract_id == *self.stackerdb.miners_contract_id() { + self.process_event_miner(event); + vec![] + } else if event.contract_id == *self.stackerdb.signers_contract_id() { + self.process_event_signer(event) + } else { + warn!( + "Received an event from an unrecognized contract ID: {:?}", + event.contract_id + ); + vec![] + } + } + + // Process the event as a signer message from the signer stacker-db fn process_event_signer(&mut self, event: &StackerDBChunksEvent) -> Vec { // Determine the current coordinator id and public key for verification let (_coordinator_id, coordinator_public_key) = @@ -378,7 +397,7 @@ impl SignerRunLoop, RunLoopCommand> for Run fn run_one_pass( &mut self, - event: Option, + event: Option, cmd: Option, res: Sender>, ) -> Option> { @@ -395,11 +414,13 @@ impl SignerRunLoop, RunLoopCommand> for Run .expect("Failed to connect to initialize due to timeout. Stacks node may be down."); } // Process any arrived events - if let Some(event) = event { - if event.contract_id == *self.stackerdb.miners_contract_id() { - self.process_event_miner(&event); - } else if event.contract_id == *self.stackerdb.signers_contract_id() { - let operation_results = self.process_event_signer(&event); + match event { + Some(SignerEvent::BlockProposal(block_validate_response)) => { + self.handle_block_validate_response(block_validate_response) + } + Some(SignerEvent::StackerDB(event)) => { + let operation_results = self.handle_stackerdb_event(&event); + let nmb_results = operation_results.len(); if nmb_results > 0 { // We finished our command. Update the state @@ -411,12 +432,8 @@ impl SignerRunLoop, RunLoopCommand> for Run } } } - } else { - warn!( - "Received event from unknown contract ID: {}", - event.contract_id - ); } + None => debug!("No event received"), } // The process the next command // Must be called AFTER processing the event as the state may update to IDLE due to said event. From 693d87674771f31cb4306e8a50afdd3bd8800c65 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 10 Jan 2024 18:48:42 -0500 Subject: [PATCH 15/27] Add block to BlockValidateReject Signed-off-by: Jacinta Ferrant --- stackslib/src/net/api/postblock_proposal.rs | 72 +++++++++++++++------ 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/stackslib/src/net/api/postblock_proposal.rs b/stackslib/src/net/api/postblock_proposal.rs index b2416d7a6e..1c6613f8d7 100644 --- a/stackslib/src/net/api/postblock_proposal.rs +++ b/stackslib/src/net/api/postblock_proposal.rs @@ -90,6 +90,8 @@ fn hex_deser_block<'de, D: serde::Deserializer<'de>>(d: D) -> Result> for BlockValidateRespons } } -impl From for BlockValidateReject -where - T: Into, -{ - fn from(value: T) -> Self { - let ce: ChainError = value.into(); - BlockValidateReject { - reason: format!("Chainstate Error: {ce}"), - reason_code: ValidateRejectCode::ChainstateError, - } - } -} - /// Represents a block proposed to the `v2/block_proposal` endpoint for validation #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct NakamotoBlockProposal { @@ -182,16 +171,28 @@ impl NakamotoBlockProposal { let mainnet = self.chain_id == CHAIN_ID_MAINNET; if self.chain_id != chainstate.chain_id || mainnet != chainstate.mainnet { return Err(BlockValidateReject { + block: self.block.clone(), reason_code: ValidateRejectCode::InvalidBlock, reason: "Wrong network/chain_id".into(), }); } let burn_dbconn = sortdb.index_conn(); - let sort_tip = SortitionDB::get_canonical_sortition_tip(sortdb.conn())?; + let sort_tip = SortitionDB::get_canonical_sortition_tip(sortdb.conn()).map_err(|ce| { + BlockValidateReject { + block: self.block.clone(), + reason: format!("Chainstate Error: {ce}"), + reason_code: ValidateRejectCode::ChainstateError, + } + })?; let mut db_handle = sortdb.index_handle(&sort_tip); let expected_burn = - NakamotoChainState::get_expected_burns(&mut db_handle, chainstate.db(), &self.block)?; + NakamotoChainState::get_expected_burns(&mut db_handle, chainstate.db(), &self.block) + .map_err(|ce| BlockValidateReject { + block: self.block.clone(), + reason: format!("Chainstate Error: {ce}"), + reason_code: ValidateRejectCode::ChainstateError, + })?; // Static validation checks NakamotoChainState::validate_nakamoto_block_burnchain( @@ -200,14 +201,25 @@ impl NakamotoBlockProposal { &self.block, mainnet, self.chain_id, - )?; + ) + .map_err(|ce| BlockValidateReject { + block: self.block.clone(), + reason: format!("Chainstate Error: {ce}"), + reason_code: ValidateRejectCode::ChainstateError, + })?; // Validate txs against chainstate let parent_stacks_header = NakamotoChainState::get_block_header( chainstate.db(), &self.block.header.parent_block_id, - )? + ) + .map_err(|ce| BlockValidateReject { + block: self.block.clone(), + reason: format!("Chainstate Error: {ce}"), + reason_code: ValidateRejectCode::ChainstateError, + })? .ok_or_else(|| BlockValidateReject { + block: self.block.clone(), reason_code: ValidateRejectCode::InvalidBlock, reason: "Invalid parent block".into(), })?; @@ -232,11 +244,27 @@ impl NakamotoBlockProposal { self.block.header.burn_spent, tenure_change, coinbase, - )?; + ) + .map_err(|ce| BlockValidateReject { + block: self.block.clone(), + reason: format!("Chainstate Error: {ce}"), + reason_code: ValidateRejectCode::ChainstateError, + })?; - let mut miner_tenure_info = - builder.load_tenure_info(chainstate, &burn_dbconn, tenure_cause)?; - let mut tenure_tx = builder.tenure_begin(&burn_dbconn, &mut miner_tenure_info)?; + let mut miner_tenure_info = builder + .load_tenure_info(chainstate, &burn_dbconn, tenure_cause) + .map_err(|ce| BlockValidateReject { + block: self.block.clone(), + reason: format!("Chainstate Error: {ce}"), + reason_code: ValidateRejectCode::ChainstateError, + })?; + let mut tenure_tx = builder + .tenure_begin(&burn_dbconn, &mut miner_tenure_info) + .map_err(|ce| BlockValidateReject { + block: self.block.clone(), + reason: format!("Chainstate Error: {ce}"), + reason_code: ValidateRejectCode::ChainstateError, + })?; for (i, tx) in self.block.txs.iter().enumerate() { let tx_len = tx.tx_len(); @@ -264,6 +292,7 @@ impl NakamotoBlockProposal { "tx" => ?tx, ); return Err(BlockValidateReject { + block: self.block.clone(), reason, reason_code: ValidateRejectCode::BadTransaction, }); @@ -293,6 +322,7 @@ impl NakamotoBlockProposal { //"computed_block" => %serde_json::to_string(&serde_json::to_value(&block).unwrap()).unwrap(), ); return Err(BlockValidateReject { + block: self.block.clone(), reason: "Block hash is not as expected".into(), reason_code: ValidateRejectCode::BadBlockHash, }); From 9bc40c6ddf40f7e4ebcdda89f36f1ec99f44f87f Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 11 Jan 2024 14:15:35 -0500 Subject: [PATCH 16/27] Remove use of pox contract and miners contract configs and update test Signed-off-by: Jacinta Ferrant --- Cargo.lock | 1 - libsigner/Cargo.toml | 1 - libsigner/src/events.rs | 31 ++ stacks-signer/src/cli.rs | 6 - stacks-signer/src/client/stackerdb.rs | 225 +++---------- stacks-signer/src/client/stacks_client.rs | 102 ++---- stacks-signer/src/config.rs | 35 +-- stacks-signer/src/main.rs | 8 +- stacks-signer/src/runloop.rs | 245 ++++++++------- stacks-signer/src/tests/conf/signer-0.toml | 1 - stacks-signer/src/tests/conf/signer-1.toml | 1 - stacks-signer/src/tests/conf/signer-2.toml | 1 - stacks-signer/src/tests/conf/signer-3.toml | 1 - stacks-signer/src/tests/conf/signer-4.toml | 1 - stacks-signer/src/utils.rs | 11 - .../src/tests/nakamoto_integrations.rs | 8 +- testnet/stacks-node/src/tests/signer.rs | 296 ++++++++---------- 17 files changed, 394 insertions(+), 580 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d5061bb7f..03febb2e39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,7 +1971,6 @@ dependencies = [ name = "libsigner" version = "0.0.1" dependencies = [ - "bincode", "clarity", "libc", "libstackerdb", diff --git a/libsigner/Cargo.toml b/libsigner/Cargo.toml index 73fad53a8b..ee7338ea17 100644 --- a/libsigner/Cargo.toml +++ b/libsigner/Cargo.toml @@ -17,7 +17,6 @@ path = "./src/libsigner.rs" [dependencies] clarity = { path = "../clarity" } -bincode = "1.3.3" libc = "0.2" libstackerdb = { path = "../libstackerdb" } serde = "1" diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index a86d47df59..a8ab01563f 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -38,6 +38,35 @@ use wsts::net::{Message, Packet}; use crate::http::{decode_http_body, decode_http_request}; use crate::EventError; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[repr(u8)] +enum EventPrefix { + /// A StackerDB event + StackerDB, + /// A block proposal event + BlockProposal, +} + +impl From<&SignerEvent> for EventPrefix { + fn from(event: &SignerEvent) -> Self { + match event { + SignerEvent::StackerDB(_) => EventPrefix::StackerDB, + SignerEvent::BlockProposal(_) => EventPrefix::BlockProposal, + } + } +} +impl TryFrom for EventPrefix { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(EventPrefix::StackerDB), + 1 => Ok(EventPrefix::BlockProposal), + _ => Err(()), + } + } +} + /// Event enum for newly-arrived signer subscribed events #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum SignerEvent { @@ -218,6 +247,7 @@ impl EventReceiver for SignerEventReceiver { ))); } if request.url() == "/stackerdb_chunks" { + debug!("Got stackerdb_chunks event"); let mut body = String::new(); request .as_reader() @@ -235,6 +265,7 @@ impl EventReceiver for SignerEventReceiver { Ok(SignerEvent::StackerDB(event)) } else if request.url() == "/proposal_response" { + debug!("Got proposal_response event"); let mut body = String::new(); request .as_reader() diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs index 0e368ac4c8..65aa8ccafc 100644 --- a/stacks-signer/src/cli.rs +++ b/stacks-signer/src/cli.rs @@ -131,9 +131,6 @@ pub struct GenerateFilesArgs { /// The signers stacker-db contract to use. Must be in the format of "STACKS_ADDRESS.CONTRACT_NAME" #[arg(short, long, value_parser = parse_contract)] pub signers_contract: QualifiedContractIdentifier, - /// The miners stacker-db contract to use. Must be in the format of "STACKS_ADDRESS.CONTRACT_NAME" - #[arg(short, long, value_parser = parse_contract)] - pub miners_contract: QualifiedContractIdentifier, #[arg( long, required_unless_present = "signer_private_keys", @@ -144,9 +141,6 @@ pub struct GenerateFilesArgs { #[clap(long, value_name = "FILE")] /// A path to a file containing a list of hexadecimal Stacks private keys of the signers pub signer_private_keys: Option, - /// The Stacks private key to use in hexademical format for the miner - #[arg(long, value_parser = parse_private_key)] - pub miner_private_key: StacksPrivateKey, #[arg(long)] /// The total number of key ids to distribute among the signers pub num_keys: u32, diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index b53467b5e7..7032bcdcb2 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -1,10 +1,12 @@ +use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; +use blockstack_lib::net::api::postblock_proposal::ValidateRejectCode; use clarity::vm::types::QualifiedContractIdentifier; use hashbrown::HashMap; use libsigner::{SignerSession, StackerDBSession}; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; +use serde_derive::{Deserialize, Serialize}; use slog::{slog_debug, slog_warn}; -use stacks_common::codec::{Error as CodecError, StacksMessageCodec}; use stacks_common::types::chainstate::StacksPrivateKey; use stacks_common::{debug, warn}; use wsts::net::{Message, Packet}; @@ -17,8 +19,6 @@ use crate::config::Config; /// See: https://github.com/stacks-network/stacks-blockchain/issues/3921 /// Is equal to the number of message types pub const SIGNER_SLOTS_PER_USER: u32 = 10; -/// The number of miner slots available per miner -pub const MINER_SLOTS_PER_USER: u32 = 1; // The slot IDS for each message type const DKG_BEGIN_SLOT_ID: u32 = 0; @@ -32,89 +32,63 @@ const SIGNATURE_SHARE_REQUEST_SLOT_ID: u32 = 7; const SIGNATURE_SHARE_RESPONSE_SLOT_ID: u32 = 8; const BLOCK_SLOT_ID: u32 = 9; -/// This is required for easy serialization of the various StackerDBMessage types -#[repr(u8)] -enum TypePrefix { - Block, - Packet, +/// The messages being sent through the stacker db contracts +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum SignerMessage { + /// The signed/validated Nakamoto block for miners to observe + BlockResponse(BlockResponse), + /// DKG and Signing round data for other signers to observe + Packet(Packet), } -impl TypePrefix { - /// Convert a u8 to a TypePrefix - fn from_u8(value: u8) -> Option { - match value { - 0 => Some(Self::Block), - 1 => Some(Self::Packet), - _ => None, - } - } +/// The response that a signer sends back to observing miners +/// either accepting or rejecting a Nakamoto block with the corresponding reason +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum BlockResponse { + /// The Nakamoto block was accepted and therefore signed + Accepted(NakamotoBlock), + /// The Nakamoto block was rejected and therefore not signed + Rejected(BlockRejection), } -impl From<&StackerDBMessage> for TypePrefix { - fn from(message: &StackerDBMessage) -> TypePrefix { - match message { - StackerDBMessage::Block(_) => TypePrefix::Block, - StackerDBMessage::Packet(_) => TypePrefix::Packet, - } - } +/// A rejection response from a signer for a proposed block +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BlockRejection { + /// The reason for the rejection + pub reason: String, + /// The reason code for the rejection + pub reason_code: RejectCode, + /// The block that was rejected + pub block: NakamotoBlock, } -/// The StackerDB messages that can be sent through the observed contracts -pub enum StackerDBMessage { - /// The latest Nakamoto block for miners to observe - Block(NakamotoBlock), - /// DKG and Signing round data for other signers to observe - Packet(Packet), +/// This enum is used to supply a `reason_code` for block rejections +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[repr(u8)] +pub enum RejectCode { + /// RPC endpoint Validation failed + ValidationFailed(ValidateRejectCode), + /// Missing expected transactions + MissingTransactions(Vec), } -impl From for StackerDBMessage { +impl From for SignerMessage { fn from(packet: Packet) -> Self { Self::Packet(packet) } } -impl StacksMessageCodec for StackerDBMessage { - fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { - fd.write_all(&[TypePrefix::from(self) as u8]) - .map_err(CodecError::WriteError)?; - match self { - StackerDBMessage::Packet(packet) => { - let message_bytes = bincode::serialize(&packet) - .map_err(|e| CodecError::SerializeError(e.to_string()))?; - message_bytes.consensus_serialize(fd) - } - StackerDBMessage::Block(block) => block.consensus_serialize(fd), - } - } - - fn consensus_deserialize(fd: &mut R) -> Result { - let mut prefix = [0]; - fd.read_exact(&mut prefix) - .map_err(|e| CodecError::DeserializeError(e.to_string()))?; - let prefix = TypePrefix::from_u8(prefix[0]).ok_or(CodecError::DeserializeError( - "Bad StackerDBMessage prefix".into(), - ))?; - - match prefix { - TypePrefix::Packet => { - let message_bytes = Vec::::consensus_deserialize(fd)?; - let packet = bincode::deserialize(&message_bytes) - .map_err(|e| CodecError::DeserializeError(e.to_string()))?; - Ok(Self::Packet(packet)) - } - TypePrefix::Block => { - let block = NakamotoBlock::consensus_deserialize(fd)?; - Ok(StackerDBMessage::Block(block)) - } - } +impl From for SignerMessage { + fn from(block_response: BlockResponse) -> Self { + Self::BlockResponse(block_response) } } -impl StackerDBMessage { +impl SignerMessage { /// Helper function to determine the slot ID for the provided stacker-db writer id pub fn slot_id(&self, id: u32) -> u32 { let slot_id = match self { - StackerDBMessage::Packet(packet) => match packet.msg { + Self::Packet(packet) => match packet.msg { Message::DkgBegin(_) => DKG_BEGIN_SLOT_ID, Message::DkgPrivateBegin(_) => DKG_PRIVATE_BEGIN_SLOT_ID, Message::DkgEnd(_) => DKG_END_SLOT_ID, @@ -125,17 +99,16 @@ impl StackerDBMessage { Message::SignatureShareRequest(_) => SIGNATURE_SHARE_REQUEST_SLOT_ID, Message::SignatureShareResponse(_) => SIGNATURE_SHARE_RESPONSE_SLOT_ID, }, - Self::Block(_block) => BLOCK_SLOT_ID, + Self::BlockResponse(_) => BLOCK_SLOT_ID, }; SIGNER_SLOTS_PER_USER * id + slot_id } } -/// The StackerDB client for communicating with both .signers and .miners contracts + +/// The StackerDB client for communicating with the .signers contract pub struct StackerDB { /// The stacker-db session for the signer StackerDB signers_stackerdb_session: StackerDBSession, - /// The stacker-db session for the .miners StackerDB - miners_stackerdb_session: StackerDBSession, /// The private key used in all stacks node communications stacks_private_key: StacksPrivateKey, /// A map of a slot ID to last chunk version @@ -149,10 +122,6 @@ impl From<&Config> for StackerDB { config.node_host, config.signers_stackerdb_contract_id.clone(), ), - miners_stackerdb_session: StackerDBSession::new( - config.node_host, - config.miners_stackerdb_contract_id.clone(), - ), stacks_private_key: config.stacks_private_key, slot_versions: HashMap::new(), } @@ -160,13 +129,13 @@ impl From<&Config> for StackerDB { } impl StackerDB { - /// Sends messages to the stacker-db with an exponential backoff retry + /// Sends messages to the .signers stacker-db with an exponential backoff retry pub fn send_message_with_retry( &mut self, id: u32, - message: StackerDBMessage, + message: SignerMessage, ) -> Result { - let message_bytes = message.serialize_to_vec(); + let message_bytes = bincode::serialize(&message).unwrap(); let slot_id = message.slot_id(id); loop { @@ -201,108 +170,8 @@ impl StackerDB { } } - /// Retrieve the miner contract id - pub fn miners_contract_id(&self) -> &QualifiedContractIdentifier { - &self.miners_stackerdb_session.stackerdb_contract_id - } - /// Retrieve the signer contract id pub fn signers_contract_id(&self) -> &QualifiedContractIdentifier { &self.signers_stackerdb_session.stackerdb_contract_id } } - -#[cfg(test)] -mod tests { - use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; - use blockstack_lib::chainstate::stacks::{StacksTransaction, ThresholdSignature}; - use rand_core::OsRng; - use stacks_common::codec::StacksMessageCodec; - use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash}; - use stacks_common::util::hash::{MerkleTree, Sha512Trunc256Sum}; - use stacks_common::util::secp256k1::MessageSignature; - use wsts::curve::scalar::Scalar; - use wsts::net::{Message, Packet, Signable, SignatureShareRequest}; - - use super::StackerDBMessage; - - #[test] - fn serde_stackerdb_message_block() { - let txs: Vec = vec![]; - let mut header = NakamotoBlockHeader { - version: 1, - chain_length: 2, - burn_spent: 3, - consensus_hash: ConsensusHash([0x04; 20]), - parent_block_id: StacksBlockId([0x05; 32]), - tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), - state_index_root: TrieHash([0x07; 32]), - miner_signature: MessageSignature::empty(), - signer_signature: ThresholdSignature::mock(), - }; - let txid_vecs = txs.iter().map(|tx| tx.txid().as_bytes().to_vec()).collect(); - - let merkle_tree = MerkleTree::::new(&txid_vecs); - let tx_merkle_root = merkle_tree.root(); - - header.tx_merkle_root = tx_merkle_root; - - let block = NakamotoBlock { header, txs }; - - let msg = StackerDBMessage::Block(block.clone()); - let serialized_bytes = msg.serialize_to_vec(); - let deserialized_msg = - StackerDBMessage::consensus_deserialize(&mut &serialized_bytes[..]).unwrap(); - match deserialized_msg { - StackerDBMessage::Block(deserialized_block) => { - assert_eq!(deserialized_block, block); - } - _ => panic!("Wrong message type. Expected StackerDBMessage::Block"), - } - } - - #[test] - fn serde_stackerdb_message_packet() { - let mut rng = OsRng; - let private_key = Scalar::random(&mut rng); - let to_sign = "One, two, three, four, five? That's amazing. I've got the same combination on my luggage.".as_bytes(); - let sig_share_request = SignatureShareRequest { - dkg_id: 1, - sign_id: 5, - sign_iter_id: 4, - nonce_responses: vec![], - message: to_sign.to_vec(), - is_taproot: false, - merkle_root: None, - }; - let packet = Packet { - sig: sig_share_request - .sign(&private_key) - .expect("Failed to sign SignatureShareRequest"), - msg: Message::SignatureShareRequest(sig_share_request), - }; - - let msg = StackerDBMessage::Packet(packet.clone()); - let serialized_bytes = msg.serialize_to_vec(); - let deserialized_msg = - StackerDBMessage::consensus_deserialize(&mut &serialized_bytes[..]).unwrap(); - match deserialized_msg { - StackerDBMessage::Packet(deserialized_packet) => { - assert_eq!(deserialized_packet.sig, packet.sig); - match deserialized_packet.msg { - Message::SignatureShareRequest(deserialized_message) => { - assert_eq!(deserialized_message.dkg_id, 1); - assert_eq!(deserialized_message.sign_id, 5); - assert_eq!(deserialized_message.sign_iter_id, 4); - assert!(deserialized_message.nonce_responses.is_empty()); - assert_eq!(deserialized_message.message.as_slice(), to_sign); - assert!(!deserialized_message.is_taproot); - assert!(deserialized_message.merkle_root.is_none()); - } - _ => panic!("Wrong message type. Expected Message::SignatureShareRequest"), - } - } - _ => panic!("Wrong message type. Expected StackerDBMessage::Packet."), - } - } -} diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index d80bbe9269..677f722420 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -1,5 +1,6 @@ use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; +use blockstack_lib::chainstate::stacks::boot::POX_4_NAME; use blockstack_lib::chainstate::stacks::{ StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth, TransactionContractCall, TransactionPayload, TransactionPostConditionMode, @@ -8,15 +9,15 @@ use blockstack_lib::chainstate::stacks::{ use blockstack_lib::net::api::callreadonly::CallReadOnlyResponse; use blockstack_lib::net::api::getpoxinfo::RPCPoxInfoData; use blockstack_lib::net::api::postblock_proposal::NakamotoBlockProposal; -use clarity::vm::types::{QualifiedContractIdentifier, SequenceData}; +use blockstack_lib::util_lib::boot::boot_code_id; use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; use serde_json::json; use slog::slog_debug; use stacks_common::codec::StacksMessageCodec; +use stacks_common::consts::CHAIN_ID_MAINNET; use stacks_common::debug; use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}; -use wsts::curve::point::Point; -use wsts::curve::scalar::Scalar; +use wsts::curve::point::{Compressed, Point}; use crate::client::{retry_with_exponential_backoff, ClientError}; use crate::config::Config; @@ -35,8 +36,6 @@ pub struct StacksClient { chain_id: u32, /// The Client used to make HTTP connects stacks_node_client: reqwest::blocking::Client, - /// The pox contract ID - pox_contract_id: Option, } impl From<&Config> for StacksClient { @@ -48,7 +47,6 @@ impl From<&Config> for StacksClient { tx_version: config.network.to_transaction_version(), chain_id: config.network.to_chain_id(), stacks_node_client: reqwest::blocking::Client::new(), - pox_contract_id: config.pox_contract_id.clone(), } } } @@ -79,32 +77,20 @@ impl StacksClient { if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } - // TODO: this is actually an aysnc call. It will not return the JSON response as below. It uses the event dispatcher instead - // let validate_block_response = response.json::()?; - // match validate_block_response { - // BlockValidateResponse::Ok(validate_block_ok) => { - // debug!("Block validation succeeded: {:?}", validate_block_ok); - // Ok(true) - // } - // BlockValidateResponse::Reject(validate_block_reject) => { - // debug!("Block validation failed: {:?}", validate_block_reject); - // Ok(false) - // } - // } Ok(()) } /// Retrieve the current DKG aggregate public key pub fn get_aggregate_public_key(&self) -> Result, ClientError> { let reward_cycle = self.get_current_reward_cycle()?; - let function_name_str = "get-aggregate-public-key"; // FIXME: this may need to be modified to match .pox-4 + let function_name_str = "get-aggregate-public-key"; let function_name = ClarityName::try_from(function_name_str) .map_err(|_| ClientError::InvalidClarityName(function_name_str.to_string()))?; - let (contract_addr, contract_name) = self.get_pox_contract()?; + let pox_contract_id = boot_code_id(POX_4_NAME, self.chain_id == CHAIN_ID_MAINNET); let function_args = &[ClarityValue::UInt(reward_cycle as u128)]; let contract_response_hex = self.read_only_contract_call_with_retry( - &contract_addr, - &contract_name, + &pox_contract_id.issuer.into(), + &pox_contract_id.name, &function_name, function_args, )?; @@ -113,6 +99,7 @@ impl StacksClient { // Helper function to retrieve the pox data from the stacks node fn get_pox_data(&self) -> Result { + debug!("Getting pox data..."); let send_request = || { self.stacks_node_client .get(self.pox_path()) @@ -140,38 +127,25 @@ impl StacksClient { todo!("Get the next possible nonce from the stacks node"); } - /// Helper function to retrieve the pox contract address and name from the stacks node - fn get_pox_contract(&self) -> Result<(StacksAddress, ContractName), ClientError> { - // Check if we have overwritten the pox contract ID in the config - if let Some(pox_contract) = self.pox_contract_id.clone() { - return Ok((pox_contract.issuer.into(), pox_contract.name)); - } - let pox_data = self.get_pox_data()?; - let contract_id = pox_data.contract_id.as_str(); - let err_msg = format!("Stacks node returned an invalid pox contract id: {contract_id}"); - let id = QualifiedContractIdentifier::parse(contract_id).expect(&err_msg); - Ok((id.issuer.into(), id.name)) - } - /// Helper function that attempts to deserialize a clarity hex string as the aggregate public key fn parse_aggregate_public_key(&self, hex: &str) -> Result, ClientError> { - let public_key_clarity_value = ClarityValue::try_deserialize_hex_untyped(hex)?; - if let ClarityValue::Optional(optional_data) = public_key_clarity_value.clone() { - if let Some(ClarityValue::Sequence(SequenceData::Buffer(public_key))) = - optional_data.data.map(|boxed| *boxed) - { - if public_key.data.len() != 32 { - return Err(ClientError::MalformedClarityValue(public_key_clarity_value)); - } - let mut bytes = [0_u8; 32]; - bytes.copy_from_slice(&public_key.data); - Ok(Some(Point::from(Scalar::from(bytes)))) - } else { - Ok(None) - } - } else { - Err(ClientError::MalformedClarityValue(public_key_clarity_value)) - } + debug!("Parsing aggregate public key: {hex}..."); + // Due to pox 4 definition, the aggregate public key is always an optional clarity value hence the use of expect + // If this fails, we have bigger problems than the signer crashing... + let value_opt = ClarityValue::try_deserialize_hex_untyped(hex)?.expect_optional(); + let Some(value) = value_opt else { + return Ok(None); + }; + // A point should have 33 bytes exactly due to the pox 4 definition hence the use of expect + // If this fails, we have bigger problems than the signer crashing... + let data = value.clone().expect_buff(33); + // It is possible that the point was invalid though when voted upon and this cannot be prevented by pox 4 definitions... + // Pass up this error if the conversions fail. + let compressed_data = Compressed::try_from(data.as_slice()) + .map_err(|_e| ClientError::MalformedClarityValue(value.clone()))?; + let point = Point::try_from(&compressed_data) + .map_err(|_e| ClientError::MalformedClarityValue(value))?; + Ok(Some(point)) } /// Sends a transaction to the stacks node for a modifying contract call @@ -268,7 +242,10 @@ impl StacksClient { function_name: &ClarityName, function_args: &[ClarityValue], ) -> Result { - debug!("Calling read-only function {}...", function_name); + debug!( + "Calling read-only function {function_name} with args {:?}...", + function_args + ); let args = function_args .iter() .map(|arg| arg.serialize_to_hex()) @@ -469,21 +446,6 @@ mod tests { )); } - #[test] - fn pox_contract_success() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_pox_contract()); - write_response( - config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"pox_activation_threshold_ustx\":829371801288885,\"first_burnchain_block_height\":2000000,\"current_burnchain_block_height\":2572192,\"prepare_phase_block_length\":50,\"reward_phase_block_length\":1000,\"reward_slots\":2000,\"rejection_fraction\":12,\"total_liquid_supply_ustx\":41468590064444294,\"current_cycle\":{\"id\":544,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":853258144644000,\"is_pox_active\":true},\"next_cycle\":{\"id\":545,\"min_threshold_ustx\":5190000000000,\"min_increment_ustx\":5183573758055,\"stacked_ustx\":847278759574000,\"prepare_phase_start_block_height\":2572200,\"blocks_until_prepare_phase\":8,\"reward_phase_start_block_height\":2572250,\"blocks_until_reward_phase\":58,\"ustx_until_pox_rejection\":4976230807733304},\"min_amount_ustx\":5190000000000,\"prepare_cycle_length\":50,\"reward_cycle_id\":544,\"reward_cycle_length\":1050,\"rejection_votes_left_required\":4976230807733304,\"next_reward_cycle_in\":58,\"contract_versions\":[{\"contract_id\":\"ST000000000000000000002AMW42H.pox\",\"activation_burnchain_block_height\":2000000,\"first_reward_cycle_id\":0},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-2\",\"activation_burnchain_block_height\":2422102,\"first_reward_cycle_id\":403},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"activation_burnchain_block_height\":2432545,\"first_reward_cycle_id\":412}]}", - ); - let (address, name) = h.join().unwrap().unwrap(); - assert_eq!( - (address.to_string().as_str(), name.to_string().as_str()), - ("ST000000000000000000002AMW42H", "pox-3") - ); - } - #[test] fn valid_reward_cycle_should_succeed() { let config = TestConfig::new(); @@ -524,14 +486,14 @@ mod tests { fn parse_valid_aggregate_public_key_should_succeed() { let config = TestConfig::new(); let clarity_value_hex = - "0x0a0200000020b8c8b0652cb2851a52374c7acd47181eb031e8fa5c62883f636e0d4fe695d6ca"; + "0x0a020000002103beca18a0e51ea31d8e66f58a245d54791b277ad08e1e9826bf5f814334ac77e0"; let result = config .client .parse_aggregate_public_key(clarity_value_hex) .unwrap(); assert_eq!( result.map(|point| point.to_string()), - Some("yzwdjwPz36Has1MSkg8JGwo38avvATkiTZvRiH1e5MLd".to_string()) + Some("27XiJwhYDWdUrYAFNejKDhmY22jU1hmwyQ5nVDUJZPmbm".to_string()) ); let clarity_value_hex = "0x09"; diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 4e1a53dffe..61ca95fa3a 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -55,7 +55,7 @@ pub enum ConfigError { UnsupportedAddressVersion, } -#[derive(serde::Deserialize, Debug, Clone)] +#[derive(serde::Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "lowercase")] /// The Stacks network to use. pub enum Network { @@ -101,10 +101,6 @@ pub struct Config { pub endpoint: SocketAddr, /// smart contract that controls the target signers' stackerdb pub signers_stackerdb_contract_id: QualifiedContractIdentifier, - /// smart contract that controls the target .miners stackerdb - pub miners_stackerdb_contract_id: QualifiedContractIdentifier, - /// the pox contract identifier to use - pub pox_contract_id: Option, /// The Scalar representation of the private key for signer communication pub message_private_key: Scalar, /// The signer's Stacks private key @@ -145,13 +141,9 @@ struct RawConfigFile { pub node_host: String, /// endpoint to event receiver pub endpoint: String, - // FIXME: these contract's should go away in non testing scenarios. Make them both optionals. + // FIXME: this should go away once .signers contract exists /// Signers' Stacker db contract identifier pub signers_stackerdb_contract_id: String, - /// Miners' Stacker db contract identifier - pub miners_stackerdb_contract_id: String, - /// pox contract identifier - pub pox_contract_id: Option, /// the 32 byte ECDSA private key used to sign blocks, chunks, and transactions pub message_private_key: String, /// The hex representation of the signer's Stacks private key used for communicating @@ -233,27 +225,6 @@ impl TryFrom for Config { ) })?; - let miners_stackerdb_contract_id = QualifiedContractIdentifier::parse( - &raw_data.miners_stackerdb_contract_id, - ) - .map_err(|_| { - ConfigError::BadField( - "miners_stackerdb_contract_id".to_string(), - raw_data.miners_stackerdb_contract_id, - ) - })?; - - let pox_contract_id = if let Some(id) = raw_data.pox_contract_id.as_ref() { - Some(QualifiedContractIdentifier::parse(id).map_err(|_| { - ConfigError::BadField( - "pox_contract_id".to_string(), - raw_data.pox_contract_id.unwrap_or("".to_string()), - ) - })?) - } else { - None - }; - let message_private_key = Scalar::try_from(raw_data.message_private_key.as_str()).map_err(|_| { ConfigError::BadField( @@ -305,8 +276,6 @@ impl TryFrom for Config { node_host, endpoint, signers_stackerdb_contract_id, - miners_stackerdb_contract_id, - pox_contract_id, message_private_key, stacks_private_key, stacks_address, diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 5187f9a522..1a41918712 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -47,7 +47,7 @@ use stacks_signer::cli::{ Cli, Command, GenerateFilesArgs, GetChunkArgs, GetLatestChunkArgs, PutChunkArgs, RunDkgArgs, SignArgs, StackerDBArgs, }; -use stacks_signer::client::{MINER_SLOTS_PER_USER, SIGNER_SLOTS_PER_USER}; +use stacks_signer::client::SIGNER_SLOTS_PER_USER; use stacks_signer::config::{Config, Network}; use stacks_signer::runloop::{RunLoop, RunLoopCommand}; use stacks_signer::utils::{build_signer_config_tomls, build_stackerdb_contract}; @@ -274,22 +274,16 @@ fn handle_generate_files(args: GenerateFilesArgs) { .iter() .map(|key| to_addr(key, &args.network)) .collect::>(); - let miner_stacks_address = to_addr(&args.miner_private_key, &args.network); // Build the signer and miner stackerdb contract let signer_stackerdb_contract = build_stackerdb_contract(&signer_stacks_addresses, SIGNER_SLOTS_PER_USER); - let miner_stackerdb_contract = - build_stackerdb_contract(&[miner_stacks_address], MINER_SLOTS_PER_USER); write_file(&args.dir, "signers.clar", &signer_stackerdb_contract); - write_file(&args.dir, "miners.clar", &miner_stackerdb_contract); let signer_config_tomls = build_signer_config_tomls( &signer_stacks_private_keys, args.num_keys, &args.host.to_string(), &args.signers_contract.to_string(), - &args.miners_contract.to_string(), - None, args.timeout.map(Duration::from_millis), ); debug!("Built {:?} signer config tomls.", signer_config_tomls.len()); diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 9ffcd649f7..068cdfaee8 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -2,8 +2,11 @@ use std::collections::VecDeque; use std::sync::mpsc::Sender; use std::time::Duration; +use blockstack_lib::chainstate::nakamoto::NakamotoBlock; +use blockstack_lib::chainstate::stacks::boot::MINERS_NAME; use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; use blockstack_lib::net::api::poststackerdbchunk::StackerDBChunksEvent; +use blockstack_lib::util_lib::boot::boot_code_id; use hashbrown::{HashMap, HashSet}; use libsigner::{SignerEvent, SignerRunLoop}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; @@ -19,9 +22,10 @@ use wsts::state_machine::{OperationResult, PublicKeys}; use wsts::v2; use crate::client::{ - retry_with_exponential_backoff, ClientError, StackerDB, StackerDBMessage, StacksClient, + retry_with_exponential_backoff, BlockRejection, BlockResponse, ClientError, RejectCode, + SignerMessage, StackerDB, StacksClient, }; -use crate::config::Config; +use crate::config::{Config, Network}; /// Which operation to perform #[derive(PartialEq, Clone)] @@ -71,6 +75,8 @@ pub struct RunLoop { pub commands: VecDeque, /// The current state pub state: State, + /// Wether mainnet or not + pub mainnet: bool, } impl RunLoop { @@ -82,6 +88,7 @@ impl RunLoop { debug!("Aggregate public key is set: {:?}", key); self.coordinator.set_aggregate_public_key(Some(key)); } else { + debug!("Aggregate public key is not set. Coordinator must trigger DKG..."); // Update the state to IDLE so we don't needlessy requeue the DKG command. let (coordinator_id, _) = calculate_coordinator(&self.signing_round.public_keys); if coordinator_id == self.signing_round.signer_id @@ -171,130 +178,144 @@ impl RunLoop { } } - /// Handle block proposal from the miners stacker-db contract + /// Handle the block validate response returned from our prior calls to submit a block for validation fn handle_block_validate_response(&mut self, block_validate_response: BlockValidateResponse) { match block_validate_response { BlockValidateResponse::Ok(block_validate_ok) => { // This is a valid block proposal from the miner. Trigger a signing round for it. - self.commands.push_back(RunLoopCommand::Sign { - message: block_validate_ok.block.serialize_to_vec(), - is_taproot: false, - merkle_root: None, - }); - } - BlockValidateResponse::Reject(_block_validate_reject) => { - // TODO: send a message to the miner to let them know their block was rejected - todo!("Send a message to the miner to let them know their block was rejected"); + let (coordinator_id, _) = calculate_coordinator(&self.signing_round.public_keys); + if coordinator_id == self.signing_round.signer_id { + // We are the coordinator. Trigger a signing round for this block + self.commands.push_back(RunLoopCommand::Sign { + message: block_validate_ok.block.serialize_to_vec(), + is_taproot: false, + merkle_root: None, + }); + } } - } - } - - /// Process the event as a miner message from the miner stacker-db - fn process_event_miner(&mut self, event: &StackerDBChunksEvent) { - event.modified_slots.iter().for_each(|chunk| { - let mut ptr = &chunk.data[..]; - let Some(stacker_db_message) = read_next::(&mut ptr).ok() else { - warn!("Received an unrecognized message type from .miners stacker-db slot id {}: {:?}", chunk.slot_id, ptr); - return; - }; - match stacker_db_message { - StackerDBMessage::Packet(_packet) => { - // We should never actually be receiving packets from the miner stacker-db. + BlockValidateResponse::Reject(block_validate_reject) => { + warn!( + "Received a block proposal that was rejected by the stacks node: {:?}", + block_validate_reject + ); + // TODO: submit a rejection response to the .signers contract for miners + // to observe so they know to ignore it and to prove signers are doing work + let block_rejection = BlockRejection { + block: block_validate_reject.block, + reason: block_validate_reject.reason, + reason_code: RejectCode::ValidationFailed(block_validate_reject.reason_code), + }; + let message = + SignerMessage::BlockResponse(BlockResponse::Rejected(block_rejection)); + if let Err(e) = self + .stackerdb + .send_message_with_retry(self.signing_round.signer_id, message) + { warn!( - "Received a packet from the miner stacker-db. This should never happen..." + "Failed to send block rejection response to stacker-db: {:?}", + e ); } - StackerDBMessage::Block(block) => { - // Received a block proposal from the miner. - // Submit it to the stacks node to validate it before triggering a signing round. - self.stacks_client.submit_block_for_validation(block).unwrap_or_else(|e| { - warn!("Failed to validate block: {:?}", e); - }); - - // TODO: dependent on https://github.com/stacks-network/stacks-core/issues/4018 - // let miner_public_key = self.stacks_client.get_miner_public_key().expect("Failed to get miner public key. Cannot verify blocks."); - // let Some(block_miner_public_key) = block.header.recover_miner_pk() else { - // warn!("Failed to recover miner public key from block. Ignoring block proposal: {:?}", block); - // return; - // }; - // if block_miner_public_key != miner_public_key { - // warn!("Received a block proposal signed with an invalid miner public key. Ignoring block proposal: {:?}.", block); - // return; - // } - - } } - }); - } - - /// Process the event as a signer message from the signer stacker-db - fn handle_stackerdb_event(&mut self, event: &StackerDBChunksEvent) -> Vec { - if event.contract_id == *self.stackerdb.miners_contract_id() { - self.process_event_miner(event); - vec![] - } else if event.contract_id == *self.stackerdb.signers_contract_id() { - self.process_event_signer(event) - } else { - warn!( - "Received an event from an unrecognized contract ID: {:?}", - event.contract_id - ); - vec![] } } - // Process the event as a signer message from the signer stacker-db - fn process_event_signer(&mut self, event: &StackerDBChunksEvent) -> Vec { - // Determine the current coordinator id and public key for verification + // Handle the stackerdb chunk event as a signer message + fn handle_stackerdb_chunk_event_signers( + &mut self, + stackerdb_chunk_event: StackerDBChunksEvent, + res: Sender>, + ) { let (_coordinator_id, coordinator_public_key) = calculate_coordinator(&self.signing_round.public_keys); - // Filter out invalid messages - let inbound_messages: Vec = event + + let inbound_messages: Vec = stackerdb_chunk_event .modified_slots .iter() .filter_map(|chunk| { - let mut ptr = &chunk.data[..]; - let Some(stacker_db_message) = read_next::(&mut ptr).ok() else { - warn!("Received an unrecognized message type from .signers stacker-db slot id {}: {:?}", chunk.slot_id, ptr); - return None; + // We only care about verified wsts packets. Ignore anything else + let signer_message = bincode::deserialize::(&chunk.data).ok()?; + let packet = match signer_message { + SignerMessage::Packet(packet) => packet, + _ => return None, // This is a message for miners to observe. Ignore it. }; - match stacker_db_message { - StackerDBMessage::Packet(packet) => { - if packet.verify( - &self.signing_round.public_keys, - coordinator_public_key, - ) { - Some(packet) - } else { - None - } - } - StackerDBMessage::Block(_block) => { - // Blocks are meant to be read by observing miners. Ignore them. - None - } + if packet.verify(&self.signing_round.public_keys, &coordinator_public_key) { + debug!("Verified wsts packet: {:?}", &packet); + Some(packet) + } else { + None } }) .collect(); + // First process all messages as a signer // TODO: deserialize the packet into a block and verify its contents - let mut outbound_messages = self + // TODO: we need to be able to sign yes or no on a block...this needs to propogate + // to the singning round/coordinator that we are signing yes or no on a block + // self.verify_block_transactions(&block); + let signer_outbound_messages = self .signing_round .process_inbound_messages(&inbound_messages) .unwrap_or_else(|e| { error!("Failed to process inbound messages as a signer: {e}"); vec![] }); + // Next process the message as the coordinator - let (messages, operation_results) = self + let (coordinator_outbound_messages, operation_results) = self .coordinator .process_inbound_messages(&inbound_messages) .unwrap_or_else(|e| { - error!("Failed to process inbound messages as a signer: {e}"); + error!("Failed to process inbound messages as a coordinator: {e}"); (vec![], vec![]) }); - outbound_messages.extend(messages); + self.send_outbound_messages(signer_outbound_messages); + self.send_outbound_messages(coordinator_outbound_messages); + self.send_operation_results(res, operation_results); + } + + // Handle the stackerdb chunk event as a miner message + fn handle_stackerdb_chunk_event_miners(&mut self, stackerdb_chunk_event: StackerDBChunksEvent) { + for chunk in &stackerdb_chunk_event.modified_slots { + let mut ptr = &chunk.data[..]; + let Some(block) = read_next::(&mut ptr).ok() else { + warn!("Received an unrecognized message type from .miners stacker-db slot id {}: {:?}", chunk.slot_id, ptr); + continue; + }; + + // Received a block proposal from the miner. Submit it for verification. + self.stacks_client + .submit_block_for_validation(block) + .unwrap_or_else(|e| { + warn!("Failed to submit block for validation: {:?}", e); + }); + } + } + + /// Helper function to send operation results across the provided channel + fn send_operation_results( + &mut self, + res: Sender>, + operation_results: Vec, + ) { + let nmb_results = operation_results.len(); + if nmb_results > 0 { + // We finished our command. Update the state + self.state = State::Idle; + match res.send(operation_results) { + Ok(_) => { + debug!("Successfully sent {} operation result(s)", nmb_results) + } + Err(e) => { + warn!("Failed to send operation results: {:?}", e); + } + } + } + } + + // Helper function for sending packets through stackerdb + fn send_outbound_messages(&mut self, outbound_messages: Vec) { debug!( "Sending {} messages to other stacker-db instances.", outbound_messages.len() @@ -309,7 +330,6 @@ impl RunLoop { warn!("Failed to send message to stacker-db instance: {:?}", ack); } } - operation_results } } @@ -382,6 +402,7 @@ impl From<&Config> for RunLoop> { stackerdb, commands: VecDeque::new(), state: State::Uninitialized, + mainnet: config.network == Network::Mainnet, } } } @@ -408,33 +429,43 @@ impl SignerRunLoop, RunLoopCommand> for Run if let Some(command) = cmd { self.commands.push_back(command); } + // TODO: This should be called every time as DKG can change at any time...but until we have the node + // set up to receive cast votes...just do on initialization. if self.state == State::Uninitialized { let request_fn = || self.initialize().map_err(backoff::Error::transient); retry_with_exponential_backoff(request_fn) .expect("Failed to connect to initialize due to timeout. Stacks node may be down."); } // Process any arrived events + debug!("Processing event: {:?}", event); match event { Some(SignerEvent::BlockProposal(block_validate_response)) => { + debug!("Received a block proposal result from the stacks node..."); self.handle_block_validate_response(block_validate_response) } - Some(SignerEvent::StackerDB(event)) => { - let operation_results = self.handle_stackerdb_event(&event); - - let nmb_results = operation_results.len(); - if nmb_results > 0 { - // We finished our command. Update the state - self.state = State::Idle; - match res.send(operation_results) { - Ok(_) => debug!("Successfully sent {} operation result(s)", nmb_results), - Err(e) => { - warn!("Failed to send operation results: {:?}", e); - } - } + Some(SignerEvent::StackerDB(stackerdb_chunk_event)) => { + if stackerdb_chunk_event.contract_id == *self.stackerdb.signers_contract_id() { + debug!("Received a StackerDB event for the .signers contract..."); + self.handle_stackerdb_chunk_event_signers(stackerdb_chunk_event, res); + } else if stackerdb_chunk_event.contract_id + == boot_code_id(MINERS_NAME, self.mainnet) + { + debug!("Received a StackerDB event for the .miners contract..."); + self.handle_stackerdb_chunk_event_miners(stackerdb_chunk_event); + } else { + // Ignore non miner or signer messages + debug!( + "Received a StackerDB event for an unrecognized contract id: {:?}. Ignoring...", + stackerdb_chunk_event.contract_id + ); } } - None => debug!("No event received"), + None => { + // No event. Do nothing. + debug!("No event received") + } } + // The process the next command // Must be called AFTER processing the event as the state may update to IDLE due to said event. self.process_next_command(); @@ -443,9 +474,9 @@ impl SignerRunLoop, RunLoopCommand> for Run } /// Helper function for determining the coordinator public key given the the public keys -fn calculate_coordinator(public_keys: &PublicKeys) -> (u32, &ecdsa::PublicKey) { +fn calculate_coordinator(public_keys: &PublicKeys) -> (u32, ecdsa::PublicKey) { // TODO: do some sort of VRF here to calculate the public key // See: https://github.com/stacks-network/stacks-blockchain/issues/3915 // Mockamato just uses the first signer_id as the coordinator for now - (0, public_keys.signers.get(&0).unwrap()) + (0, public_keys.signers.get(&0).cloned().unwrap()) } diff --git a/stacks-signer/src/tests/conf/signer-0.toml b/stacks-signer/src/tests/conf/signer-0.toml index 226a30eb7b..dc9bbf61c2 100644 --- a/stacks-signer/src/tests/conf/signer-0.toml +++ b/stacks-signer/src/tests/conf/signer-0.toml @@ -5,7 +5,6 @@ node_host = "127.0.0.1:20443" endpoint = "localhost:30000" network = "testnet" signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 0 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-1.toml b/stacks-signer/src/tests/conf/signer-1.toml index e3f6f68cbd..c0988c9c8d 100644 --- a/stacks-signer/src/tests/conf/signer-1.toml +++ b/stacks-signer/src/tests/conf/signer-1.toml @@ -5,7 +5,6 @@ node_host = "127.0.0.1:20443" endpoint = "localhost:30001" network = "testnet" signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 1 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-2.toml b/stacks-signer/src/tests/conf/signer-2.toml index 0140dadad0..b6987b71b6 100644 --- a/stacks-signer/src/tests/conf/signer-2.toml +++ b/stacks-signer/src/tests/conf/signer-2.toml @@ -5,7 +5,6 @@ node_host = "127.0.0.1:20443" endpoint = "localhost:30002" network = "testnet" signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 2 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-3.toml b/stacks-signer/src/tests/conf/signer-3.toml index 8cc8889f52..114ea38218 100644 --- a/stacks-signer/src/tests/conf/signer-3.toml +++ b/stacks-signer/src/tests/conf/signer-3.toml @@ -5,7 +5,6 @@ node_host = "127.0.0.1:20443" endpoint = "localhost:30003" network = "testnet" signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 3 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-4.toml b/stacks-signer/src/tests/conf/signer-4.toml index 999e066a09..37a68f1035 100644 --- a/stacks-signer/src/tests/conf/signer-4.toml +++ b/stacks-signer/src/tests/conf/signer-4.toml @@ -5,7 +5,6 @@ node_host = "127.0.0.1:20443" endpoint = "localhost:30004" network = "testnet" signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -miners_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.miners-stackerdb" signer_id = 4 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/utils.rs b/stacks-signer/src/utils.rs index 6011a3a170..b99087bfe0 100644 --- a/stacks-signer/src/utils.rs +++ b/stacks-signer/src/utils.rs @@ -13,8 +13,6 @@ pub fn build_signer_config_tomls( num_keys: u32, node_host: &str, signers_stackerdb_contract_id: &str, - miners_stackerdb_contract_id: &str, - pox_contract_id: Option<&str>, timeout: Option, ) -> Vec { let num_signers = signer_stacks_private_keys.len() as u32; @@ -74,7 +72,6 @@ node_host = "{node_host}" endpoint = "{endpoint}" network = "testnet" signers_stackerdb_contract_id = "{signers_stackerdb_contract_id}" -miners_stackerdb_contract_id = "{miners_stackerdb_contract_id}" signer_id = {id} {signers_array} "# @@ -89,14 +86,6 @@ event_timeout = {event_timeout_ms} "# ) } - if let Some(pox_contract_id) = pox_contract_id { - signer_config_toml = format!( - r#" -{signer_config_toml} -pox_contract_id = "{pox_contract_id}" -"# - ); - } signer_config_tomls.push(signer_config_toml); } diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 05ae6da4a5..0bbd826ca2 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -238,7 +238,7 @@ where /// Mine a bitcoin block, and wait until: /// (1) a new block has been processed by the coordinator -fn next_block_and_process_new_stacks_block( +pub fn next_block_and_process_new_stacks_block( btc_controller: &mut BitcoinRegtestController, timeout_secs: u64, coord_channels: &Arc>, @@ -263,7 +263,7 @@ fn next_block_and_process_new_stacks_block( /// (1) a new block has been processed by the coordinator /// (2) 2 block commits have been issued ** or ** more than 10 seconds have /// passed since (1) occurred -fn next_block_and_mine_commit( +pub fn next_block_and_mine_commit( btc_controller: &mut BitcoinRegtestController, timeout_secs: u64, coord_channels: &Arc>, @@ -320,7 +320,7 @@ fn next_block_and_mine_commit( }) } -fn setup_stacker(naka_conf: &mut Config) -> Secp256k1PrivateKey { +pub fn setup_stacker(naka_conf: &mut Config) -> Secp256k1PrivateKey { let stacker_sk = Secp256k1PrivateKey::new(); let stacker_address = tests::to_addr(&stacker_sk); naka_conf.add_initial_balance( @@ -333,7 +333,7 @@ fn setup_stacker(naka_conf: &mut Config) -> Secp256k1PrivateKey { /// /// * `stacker_sk` - must be a private key for sending a large `stack-stx` transaction in order /// for pox-4 to activate -fn boot_to_epoch_3( +pub fn boot_to_epoch_3( naka_conf: &Config, blocks_processed: &RunLoopCounter, stacker_sk: Secp256k1PrivateKey, diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 4a4e6e55c9..b561fe1460 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -1,13 +1,18 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{env, thread}; use clarity::vm::types::QualifiedContractIdentifier; use libsigner::{RunningSigner, Signer, SignerEventReceiver}; +use stacks::chainstate::coordinator::comm::CoordinatorChannels; +use stacks::chainstate::stacks::boot::MINERS_NAME; use stacks::chainstate::stacks::StacksPrivateKey; -use stacks_common::types::chainstate::StacksAddress; -use stacks_signer::client::{MINER_SLOTS_PER_USER, SIGNER_SLOTS_PER_USER}; -use stacks_signer::config::Config as SignerConfig; +use stacks::util_lib::boot::boot_code_id; +use stacks_common::types::chainstate::{StacksAddress, StacksPublicKey}; +use stacks_signer::client::SIGNER_SLOTS_PER_USER; +use stacks_signer::config::{Config as SignerConfig, Network}; use stacks_signer::runloop::RunLoopCommand; use stacks_signer::utils::{build_signer_config_tomls, build_stackerdb_contract}; use tracing_subscriber::prelude::*; @@ -17,19 +22,26 @@ use wsts::state_machine::OperationResult; use wsts::v2; use crate::config::{Config as NeonConfig, EventKeyType, EventObserverConfig, InitialBalance}; +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, naka_neon_integration_conf, setup_stacker, +}; use crate::tests::neon_integrations::{ - neon_integration_test_conf, next_block_and_wait, submit_tx, wait_for_runloop, + next_block_and_wait, submit_tx, test_observer, wait_for_runloop, }; use crate::tests::{make_contract_publish, to_addr}; -use crate::{neon, BitcoinRegtestController, BurnchainController}; +use crate::{BitcoinRegtestController, BurnchainController}; // Helper struct for holding the btc and stx neon nodes #[allow(dead_code)] struct RunningNodes { pub btc_regtest_controller: BitcoinRegtestController, pub btcd_controller: BitcoinCoreController, - pub join_handle: thread::JoinHandle<()>, + pub run_loop_thread: thread::JoinHandle<()>, + pub run_loop_stopper: Arc, + pub coord_channel: Arc>, pub conf: NeonConfig, } @@ -40,7 +52,7 @@ fn spawn_signer( ) -> RunningSigner> { let config = stacks_signer::config::Config::load_from_str(data).unwrap(); let ev = SignerEventReceiver::new(vec![ - config.miners_stackerdb_contract_id.clone(), + boot_code_id(MINERS_NAME, config.network == Network::Mainnet), config.signers_stackerdb_contract_id.clone(), ]); let runloop: stacks_signer::runloop::RunLoop> = @@ -61,27 +73,33 @@ fn spawn_signer( #[allow(clippy::too_many_arguments)] fn setup_stx_btc_node( - conf: &mut NeonConfig, + mut naka_conf: NeonConfig, num_signers: u32, signer_stacks_private_keys: &[StacksPrivateKey], publisher_private_key: &StacksPrivateKey, signers_stackerdb_contract: &str, signers_stackerdb_contract_id: &QualifiedContractIdentifier, - miners_stackerdb_contract: &str, - miners_stackerdb_contract_id: &QualifiedContractIdentifier, - pox_contract: &str, - pox_contract_id: &QualifiedContractIdentifier, signer_config_tomls: &Vec, ) -> RunningNodes { + // Spawn the endpoints for observing signers for toml in signer_config_tomls { let signer_config = SignerConfig::load_from_str(toml).unwrap(); - conf.events_observers.insert(EventObserverConfig { + naka_conf.events_observers.insert(EventObserverConfig { endpoint: format!("{}", signer_config.endpoint), events_keys: vec![EventKeyType::StackerDBChunks, EventKeyType::BlockProposal], }); } + // Spawn a test observer for verification purposes + test_observer::spawn(); + let observer_port = test_observer::EVENT_OBSERVER_PORT; + naka_conf.events_observers.insert(EventObserverConfig { + endpoint: format!("localhost:{observer_port}"), + events_keys: vec![EventKeyType::StackerDBChunks, EventKeyType::BlockProposal], + }); + + // The signers need some initial balances in order to pay for epoch 2.5 transaction votes let mut initial_balances = Vec::new(); initial_balances.push(InitialBalance { @@ -95,34 +113,38 @@ fn setup_stx_btc_node( amount: 10_000_000_000_000, }); } - - conf.initial_balances.append(&mut initial_balances); - conf.node + naka_conf.initial_balances.append(&mut initial_balances); + naka_conf + .node .stacker_dbs .push(signers_stackerdb_contract_id.clone()); - conf.node - .stacker_dbs - .push(miners_stackerdb_contract_id.clone()); + naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000); + + let stacker_sk = setup_stacker(&mut naka_conf); info!("Make new BitcoinCoreController"); - let mut btcd_controller = BitcoinCoreController::new(conf.clone()); + let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone()); btcd_controller .start_bitcoind() .map_err(|_e| ()) .expect("Failed starting bitcoind"); info!("Make new BitcoinRegtestController"); - let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); info!("Bootstraping..."); btc_regtest_controller.bootstrap_chain(201); info!("Chain bootstrapped..."); - let mut run_loop = neon::RunLoop::new(conf.clone()); - let blocks_processed = run_loop.get_blocks_processed_arc(); + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, .. + } = run_loop.counters(); - let join_handle = thread::spawn(move || run_loop.start(None, 0)); + let coord_channel = run_loop.coordinator_channels(); + let run_loop_thread = thread::spawn(move || run_loop.start(None, 0)); // Give the run loop some time to start up! info!("Wait for runloop..."); @@ -140,75 +162,43 @@ fn setup_stx_btc_node( info!("Mine third block..."); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - let http_origin = format!("http://{}", &conf.node.rpc_bind); - - info!("Send pox contract-publish..."); + info!("Send signers stacker-db contract-publish..."); + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); let tx_fee = 100_000; let tx = make_contract_publish( publisher_private_key, 0, tx_fee, - &pox_contract_id.name, - pox_contract, - ); - submit_tx(&http_origin, &tx); - - info!("Send signers stacker-db contract-publish..."); - let tx = make_contract_publish( - publisher_private_key, - 1, - tx_fee, &signers_stackerdb_contract_id.name, signers_stackerdb_contract, ); submit_tx(&http_origin, &tx); - - info!("Send miners stacker-db contract-publish..."); - let tx = make_contract_publish( - publisher_private_key, - 2, - tx_fee, - &miners_stackerdb_contract_id.name, - miners_stackerdb_contract, - ); - submit_tx(&http_origin, &tx); - // mine it - info!("Mining the pox and stackerdb contract..."); + info!("Mining the stackerdb contract: {signers_stackerdb_contract_id}"); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + info!("Boot to epoch 3.0 to activate pox-4..."); + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + stacker_sk, + StacksPublicKey::new(), + &mut btc_regtest_controller, + ); + + info!("Pox 4 activated and ready for signers to perform DKG and sign!"); RunningNodes { btcd_controller, btc_regtest_controller, - join_handle, - conf: conf.clone(), + run_loop_thread, + run_loop_stopper, + coord_channel, + conf: naka_conf, } } -/// Helper function for building our fake pox contract -pub fn build_pox_contract(num_signers: u32) -> String { - let mut pox_contract = String::new(); // " - pox_contract += r#" -;; data vars -;; -(define-data-var aggregate-public-key (optional (buff 33)) none) -"#; - pox_contract += &format!("(define-data-var num-signers uint u{num_signers})\n"); - pox_contract += r#" - -;; read only functions -;; - -(define-read-only (get-aggregate-public-key (reward-cycle uint)) - (var-get aggregate-public-key) -) - -"#; - pox_contract -} - #[test] #[ignore] fn test_stackerdb_dkg() { @@ -221,6 +211,7 @@ fn test_stackerdb_dkg() { .with(EnvFilter::from_default_env()) .init(); + info!("------------------------- Test Setup -------------------------"); // Generate Signer Data let num_signers: u32 = 10; let num_keys: u32 = 400; @@ -232,35 +223,22 @@ fn test_stackerdb_dkg() { .iter() .map(to_addr) .collect::>(); - let miner_private_key = StacksPrivateKey::new(); - let miner_stacks_address = to_addr(&miner_private_key); - - // Setup the neon node - let (mut conf, _) = neon_integration_test_conf(); - // Build our simulated pox-4 stacks contract TODO: replace this with the real deal? - let pox_contract = build_pox_contract(num_signers); - let pox_contract_id = - QualifiedContractIdentifier::new(to_addr(&publisher_private_key).into(), "pox-4".into()); - // Build the stackerdb contracts + // Build the stackerdb signers contract + // TODO: Remove this once it is a boot contract let signers_stackerdb_contract = build_stackerdb_contract(&signer_stacks_addresses, SIGNER_SLOTS_PER_USER); let signers_stacker_db_contract_id = QualifiedContractIdentifier::new(to_addr(&publisher_private_key).into(), "signers".into()); - let miners_stackerdb_contract = - build_stackerdb_contract(&[miner_stacks_address], MINER_SLOTS_PER_USER); - let miners_stacker_db_contract_id = - QualifiedContractIdentifier::new(to_addr(&publisher_private_key).into(), "miners".into()); + let (naka_conf, _miner_account) = naka_neon_integration_conf(None); // Setup the signer and coordinator configurations let signer_configs = build_signer_config_tomls( &signer_stacks_private_keys, num_keys, - &conf.node.rpc_bind, + &naka_conf.node.rpc_bind, &signers_stacker_db_contract_id.to_string(), - &miners_stacker_db_contract_id.to_string(), - Some(&pox_contract_id.to_string()), Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. ); @@ -290,78 +268,82 @@ fn test_stackerdb_dkg() { res_receivers.push(coordinator_res_recv); - // Let's wrap the node in a lifetime to ensure stopping the signers doesn't cause issues. - { - // Setup the nodes and deploy the contract to it - let _node = setup_stx_btc_node( - &mut conf, - num_signers, - &signer_stacks_private_keys, - &publisher_private_key, - &signers_stackerdb_contract, - &signers_stacker_db_contract_id, - &miners_stackerdb_contract, - &miners_stacker_db_contract_id, - &pox_contract, - &pox_contract_id, - &signer_configs, - ); - - let now = std::time::Instant::now(); - info!("signer_runloop: spawn send commands to do dkg and then sign"); - coordinator_cmd_send - .send(RunLoopCommand::Sign { - message: vec![1, 2, 3, 4, 5], - is_taproot: false, - merkle_root: None, - }) - .expect("failed to send Sign command"); - coordinator_cmd_send - .send(RunLoopCommand::Sign { - message: vec![1, 2, 3, 4, 5], - is_taproot: true, - merkle_root: None, - }) - .expect("failed to send Sign command"); - for recv in res_receivers.iter() { - let mut aggregate_group_key = None; - let mut frost_signature = None; - let mut schnorr_proof = None; - loop { - let results = recv.recv().expect("failed to recv results"); - for result in results { - match result { - OperationResult::Dkg(point) => { - info!("Received aggregate_group_key {point}"); - aggregate_group_key = Some(point); - } - OperationResult::Sign(sig) => { - info!("Received Signature ({},{})", &sig.R, &sig.z); - frost_signature = Some(sig); - } - OperationResult::SignTaproot(proof) => { - info!("Received SchnorrProof ({},{})", &proof.r, &proof.s); - schnorr_proof = Some(proof); - } - OperationResult::DkgError(dkg_error) => { - panic!("Received DkgError {:?}", dkg_error); - } - OperationResult::SignError(sign_error) => { - panic!("Received SignError {}", sign_error); - } + // Setup the nodes and deploy the contract to it + let node = setup_stx_btc_node( + naka_conf, + num_signers, + &signer_stacks_private_keys, + &publisher_private_key, + &signers_stackerdb_contract, + &signers_stacker_db_contract_id, + &signer_configs, + ); + + info!("------------------------- Test DKG and Sign -------------------------"); + let now = std::time::Instant::now(); + info!("signer_runloop: spawn send commands to do dkg and then sign"); + coordinator_cmd_send + .send(RunLoopCommand::Dkg) + .expect("failed to send Dkg command"); + coordinator_cmd_send + .send(RunLoopCommand::Sign { + message: vec![1, 2, 3, 4, 5], + is_taproot: false, + merkle_root: None, + }) + .expect("failed to send non taproot Sign command"); + coordinator_cmd_send + .send(RunLoopCommand::Sign { + message: vec![1, 2, 3, 4, 5], + is_taproot: true, + merkle_root: None, + }) + .expect("failed to send taproot Sign command"); + for recv in res_receivers.iter() { + let mut aggregate_group_key = None; + let mut frost_signature = None; + let mut schnorr_proof = None; + loop { + let results = recv.recv().expect("failed to recv results"); + for result in results { + match result { + OperationResult::Dkg(point) => { + info!("Received aggregate_group_key {point}"); + aggregate_group_key = Some(point); + } + OperationResult::Sign(sig) => { + info!("Received Signature ({},{})", &sig.R, &sig.z); + frost_signature = Some(sig); + } + OperationResult::SignTaproot(proof) => { + info!("Received SchnorrProof ({},{})", &proof.r, &proof.s); + schnorr_proof = Some(proof); + } + OperationResult::DkgError(dkg_error) => { + panic!("Received DkgError {:?}", dkg_error); + } + OperationResult::SignError(sign_error) => { + panic!("Received SignError {}", sign_error); } - } - if aggregate_group_key.is_some() - && frost_signature.is_some() - && schnorr_proof.is_some() - { - break; } } + if aggregate_group_key.is_some() && frost_signature.is_some() && schnorr_proof.is_some() + { + break; + } } - let elapsed = now.elapsed(); - info!("DKG and Sign Time Elapsed: {:.2?}", elapsed); } + let elapsed = now.elapsed(); + info!("DKG and Sign Time Elapsed: {:.2?}", elapsed); + + node.coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + + node.run_loop_stopper.store(false, Ordering::SeqCst); + + node.run_loop_thread.join().unwrap(); // Stop the signers for signer in running_signers { assert!(signer.stop().is_none()); From 595fb452c52e962fb5321691e1d7989362676c27 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 11 Jan 2024 14:40:55 -0500 Subject: [PATCH 17/27] Cleanup signer test to easily add another Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/tests/signer.rs | 211 ++++++++++++++---------- 1 file changed, 123 insertions(+), 88 deletions(-) diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index b561fe1460..52f316dc17 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -45,6 +45,120 @@ struct RunningNodes { pub conf: NeonConfig, } +struct SignerTest { + // The stx and bitcoin nodes and their run loops + pub running_nodes: RunningNodes, + // The channel for sending commands to the coordinator + pub coordinator_cmd_sender: Sender, + // The channels for sending commands to the signers + pub _signer_cmd_senders: Vec>, + // The channels for receiving results from both the coordinator and the signers + pub result_receivers: Vec>>, + // The running coordinator and its threads + pub running_coordinator: RunningSigner>, + // The running signer and its threads + pub running_signers: Vec>>, +} + +impl SignerTest { + fn new(num_signers: u32, num_keys: u32) -> Self { + // Generate Signer Data + let publisher_private_key = StacksPrivateKey::new(); + let signer_stacks_private_keys = (0..num_signers) + .map(|_| StacksPrivateKey::new()) + .collect::>(); + let signer_stacks_addresses = signer_stacks_private_keys + .iter() + .map(to_addr) + .collect::>(); + + // Build the stackerdb signers contract + // TODO: Remove this once it is a boot contract + let signers_stackerdb_contract = + build_stackerdb_contract(&signer_stacks_addresses, SIGNER_SLOTS_PER_USER); + let signers_stacker_db_contract_id = QualifiedContractIdentifier::new( + to_addr(&publisher_private_key).into(), + "signers".into(), + ); + + let (naka_conf, _miner_account) = naka_neon_integration_conf(None); + + // Setup the signer and coordinator configurations + let signer_configs = build_signer_config_tomls( + &signer_stacks_private_keys, + num_keys, + &naka_conf.node.rpc_bind, + &signers_stacker_db_contract_id.to_string(), + Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. + ); + + let mut running_signers = vec![]; + let mut _signer_cmd_senders = vec![]; + // Spawn all the signers first to listen to the coordinator request for dkg + let mut result_receivers = Vec::new(); + for i in (1..num_signers).rev() { + let (cmd_send, cmd_recv) = channel(); + let (res_send, res_recv) = channel(); + info!("spawn signer"); + let running_signer = spawn_signer(&signer_configs[i as usize], cmd_recv, res_send); + running_signers.push(running_signer); + _signer_cmd_senders.push(cmd_send); + result_receivers.push(res_recv); + } + // Spawn coordinator second + let (coordinator_cmd_sender, coordinator_cmd_recv) = channel(); + let (coordinator_res_send, coordinator_res_receiver) = channel(); + info!("spawn coordinator"); + let running_coordinator = spawn_signer( + &signer_configs[0], + coordinator_cmd_recv, + coordinator_res_send, + ); + + result_receivers.push(coordinator_res_receiver); + + // Setup the nodes and deploy the contract to it + let node = setup_stx_btc_node( + naka_conf, + num_signers, + &signer_stacks_private_keys, + &publisher_private_key, + &signers_stackerdb_contract, + &signers_stacker_db_contract_id, + &signer_configs, + ); + + Self { + running_nodes: node, + result_receivers, + _signer_cmd_senders, + coordinator_cmd_sender, + running_coordinator, + running_signers, + } + } + + fn shutdown(self) { + self.running_nodes + .coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + + self.running_nodes + .run_loop_stopper + .store(false, Ordering::SeqCst); + + self.running_nodes.run_loop_thread.join().unwrap(); + // Stop the signers + for signer in self.running_signers { + assert!(signer.stop().is_none()); + } + // Stop the coordinator + assert!(self.running_coordinator.stop().is_none()); + } +} + fn spawn_signer( data: &str, receiver: Receiver, @@ -71,7 +185,6 @@ fn spawn_signer( signer.spawn(endpoint).unwrap() } -#[allow(clippy::too_many_arguments)] fn setup_stx_btc_node( mut naka_conf: NeonConfig, num_signers: u32, @@ -198,7 +311,6 @@ fn setup_stx_btc_node( conf: naka_conf, } } - #[test] #[ignore] fn test_stackerdb_dkg() { @@ -212,94 +324,31 @@ fn test_stackerdb_dkg() { .init(); info!("------------------------- Test Setup -------------------------"); - // Generate Signer Data - let num_signers: u32 = 10; - let num_keys: u32 = 400; - let publisher_private_key = StacksPrivateKey::new(); - let signer_stacks_private_keys = (0..num_signers) - .map(|_| StacksPrivateKey::new()) - .collect::>(); - let signer_stacks_addresses = signer_stacks_private_keys - .iter() - .map(to_addr) - .collect::>(); - - // Build the stackerdb signers contract - // TODO: Remove this once it is a boot contract - let signers_stackerdb_contract = - build_stackerdb_contract(&signer_stacks_addresses, SIGNER_SLOTS_PER_USER); - let signers_stacker_db_contract_id = - QualifiedContractIdentifier::new(to_addr(&publisher_private_key).into(), "signers".into()); - - let (naka_conf, _miner_account) = naka_neon_integration_conf(None); - - // Setup the signer and coordinator configurations - let signer_configs = build_signer_config_tomls( - &signer_stacks_private_keys, - num_keys, - &naka_conf.node.rpc_bind, - &signers_stacker_db_contract_id.to_string(), - Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. - ); - - // The test starts here - let mut running_signers = vec![]; - // Spawn all the signers first to listen to the coordinator request for dkg - let mut signer_cmd_senders = Vec::new(); - let mut res_receivers = Vec::new(); - for i in (1..num_signers).rev() { - let (cmd_send, cmd_recv) = channel(); - let (res_send, res_recv) = channel(); - info!("spawn signer"); - let running_signer = spawn_signer(&signer_configs[i as usize], cmd_recv, res_send); - running_signers.push(running_signer); - signer_cmd_senders.push(cmd_send); - res_receivers.push(res_recv); - } - // Spawn coordinator second - let (coordinator_cmd_send, coordinator_cmd_recv) = channel(); - let (coordinator_res_send, coordinator_res_recv) = channel(); - info!("spawn coordinator"); - let running_coordinator = spawn_signer( - &signer_configs[0], - coordinator_cmd_recv, - coordinator_res_send, - ); - - res_receivers.push(coordinator_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, - &publisher_private_key, - &signers_stackerdb_contract, - &signers_stacker_db_contract_id, - &signer_configs, - ); - + let signer_test = SignerTest::new(10, 400); info!("------------------------- Test DKG and Sign -------------------------"); let now = std::time::Instant::now(); info!("signer_runloop: spawn send commands to do dkg and then sign"); - coordinator_cmd_send + signer_test + .coordinator_cmd_sender .send(RunLoopCommand::Dkg) .expect("failed to send Dkg command"); - coordinator_cmd_send + signer_test + .coordinator_cmd_sender .send(RunLoopCommand::Sign { message: vec![1, 2, 3, 4, 5], is_taproot: false, merkle_root: None, }) .expect("failed to send non taproot Sign command"); - coordinator_cmd_send + signer_test + .coordinator_cmd_sender .send(RunLoopCommand::Sign { message: vec![1, 2, 3, 4, 5], is_taproot: true, merkle_root: None, }) .expect("failed to send taproot Sign command"); - for recv in res_receivers.iter() { + for recv in signer_test.result_receivers.iter() { let mut aggregate_group_key = None; let mut frost_signature = None; let mut schnorr_proof = None; @@ -335,19 +384,5 @@ fn test_stackerdb_dkg() { } let elapsed = now.elapsed(); info!("DKG and Sign Time Elapsed: {:.2?}", elapsed); - - node.coord_channel - .lock() - .expect("Mutex poisoned") - .stop_chains_coordinator(); - - node.run_loop_stopper.store(false, Ordering::SeqCst); - - node.run_loop_thread.join().unwrap(); - // Stop the signers - for signer in running_signers { - assert!(signer.stop().is_none()); - } - // Stop the coordinator - assert!(running_coordinator.stop().is_none()); + signer_test.shutdown(); } From ff49b79a5a0c9c39f1f888c76a45faacd57f94e3 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 11 Jan 2024 15:42:25 -0500 Subject: [PATCH 18/27] Add test to handle block written to miners stacker db and fix signature to be across signature hash Signed-off-by: Jacinta Ferrant --- .github/workflows/bitcoin-tests.yml | 1 + stacks-signer/src/runloop.rs | 5 +- testnet/stacks-node/src/tests/signer.rs | 137 +++++++++++++++++++++++- 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index aa02b5e6ff..417d68e2b1 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -74,6 +74,7 @@ jobs: - tests::nakamoto_integrations::mine_multiple_per_tenure_integration - tests::nakamoto_integrations::block_proposal_api_endpoint - tests::nakamoto_integrations::miner_writes_proposed_block_to_stackerdb + - tests::signer::stackerdb_block_proposal steps: ## Setup test environment - name: Setup Test Environment diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 068cdfaee8..197d2e1d3d 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -10,7 +10,7 @@ use blockstack_lib::util_lib::boot::boot_code_id; use hashbrown::{HashMap, HashSet}; use libsigner::{SignerEvent, SignerRunLoop}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; -use stacks_common::codec::{read_next, StacksMessageCodec}; +use stacks_common::codec::read_next; use stacks_common::{debug, error, info, warn}; use wsts::common::MerkleRoot; use wsts::curve::ecdsa; @@ -186,8 +186,9 @@ impl RunLoop { let (coordinator_id, _) = calculate_coordinator(&self.signing_round.public_keys); if coordinator_id == self.signing_round.signer_id { // We are the coordinator. Trigger a signing round for this block + let signature_hash = block_validate_ok.block.header.signature_hash().expect("BUG: Stacks node should never return a validated block with an invalid signature hash"); self.commands.push_back(RunLoopCommand::Sign { - message: block_validate_ok.block.serialize_to_vec(), + message: signature_hash.0.to_vec(), is_taproot: false, merkle_root: None, }); diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 52f316dc17..59c181fee2 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -9,6 +9,7 @@ use libsigner::{RunningSigner, Signer, SignerEventReceiver}; use stacks::chainstate::coordinator::comm::CoordinatorChannels; use stacks::chainstate::stacks::boot::MINERS_NAME; use stacks::chainstate::stacks::StacksPrivateKey; +use stacks::net::api::postblock_proposal::BlockValidateResponse; use stacks::util_lib::boot::boot_code_id; use stacks_common::types::chainstate::{StacksAddress, StacksPublicKey}; use stacks_signer::client::SIGNER_SLOTS_PER_USER; @@ -26,7 +27,8 @@ 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, naka_neon_integration_conf, setup_stacker, + boot_to_epoch_3, naka_neon_integration_conf, next_block_and, next_block_and_mine_commit, + setup_stacker, }; use crate::tests::neon_integrations::{ next_block_and_wait, submit_tx, test_observer, wait_for_runloop, @@ -41,6 +43,9 @@ struct RunningNodes { pub btcd_controller: BitcoinCoreController, pub run_loop_thread: thread::JoinHandle<()>, pub run_loop_stopper: Arc, + pub vrfs_submitted: Arc, + pub commits_submitted: Arc, + pub blocks_processed: Arc, pub coord_channel: Arc>, pub conf: NeonConfig, } @@ -253,7 +258,10 @@ fn setup_stx_btc_node( let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); let run_loop_stopper = run_loop.get_termination_switch(); let Counters { - blocks_processed, .. + blocks_processed, + naka_submitted_vrfs: vrfs_submitted, + naka_submitted_commits: commits_submitted, + .. } = run_loop.counters(); let coord_channel = run_loop.coordinator_channels(); @@ -307,13 +315,17 @@ fn setup_stx_btc_node( btc_regtest_controller, run_loop_thread, run_loop_stopper, + vrfs_submitted, + commits_submitted, + blocks_processed, coord_channel, conf: naka_conf, } } + #[test] #[ignore] -fn test_stackerdb_dkg() { +fn stackerdb_dkg_sign() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } @@ -386,3 +398,120 @@ fn test_stackerdb_dkg() { info!("DKG and Sign Time Elapsed: {:.2?}", elapsed); signer_test.shutdown(); } + +#[test] +#[ignore] +fn stackerdb_block_proposal() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + info!("------------------------- Test Setup -------------------------"); + let mut signer_test = SignerTest::new(5, 5); + + // 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"); + signer_test + .coordinator_cmd_sender + .send(RunLoopCommand::Dkg) + .expect("failed to send Dkg command"); + let mut aggregate_public_key = None; + let recv = signer_test + .result_receivers + .last() + .expect("Failed to get coordinator recv"); + let results = recv.recv().expect("failed to recv results"); + for result in results { + match result { + OperationResult::Dkg(point) => { + info!("Received aggregate_group_key {point}"); + aggregate_public_key = Some(point); + break; + } + _ => { + panic!("Received Unexpected result"); + } + } + } + let aggregate_public_key = aggregate_public_key.expect("Failed to get aggregate public key"); + + let (vrfs_submitted, commits_submitted) = ( + signer_test.running_nodes.vrfs_submitted.clone(), + signer_test.running_nodes.commits_submitted.clone(), + ); + + info!("Mining a Nakamoto tenure..."); + + // first block wakes up the run loop, wait until a key registration has been submitted. + next_block_and( + &mut signer_test.running_nodes.btc_regtest_controller, + 60, + || { + let vrf_count = vrfs_submitted.load(Ordering::SeqCst); + Ok(vrf_count >= 1) + }, + ) + .unwrap(); + + // second block should confirm the VRF register, wait until a block commit is submitted + next_block_and( + &mut signer_test.running_nodes.btc_regtest_controller, + 60, + || { + let commits_count = commits_submitted.load(Ordering::SeqCst); + Ok(commits_count >= 1) + }, + ) + .unwrap(); + + // Mine 1 nakamoto tenure + next_block_and_mine_commit( + &mut signer_test.running_nodes.btc_regtest_controller, + 60, + &signer_test.running_nodes.coord_channel, + &commits_submitted, + ) + .unwrap(); + + info!("------------------------- Test Block Processed -------------------------"); + //Wait for the block to show up in the test observer + let validate_responses = test_observer::get_proposal_responses(); + let proposed_block = match validate_responses.first().expect("No block proposal") { + BlockValidateResponse::Ok(block_validated) => block_validated.block.clone(), + _ => panic!("Unexpected response"), + }; + let recv = signer_test + .result_receivers + .last() + .expect("Failed to retreive coordinator recv"); + let results = recv.recv().expect("failed to recv results"); + let mut signature = None; + for result in results { + match result { + OperationResult::Sign(sig) => { + info!("Received Signature ({},{})", &sig.R, &sig.z); + signature = Some(sig); + break; + } + _ => { + panic!("Unexpected operation result"); + } + } + } + let signature = signature.expect("Failed to get signature"); + let signature_hash = proposed_block + .header + .signature_hash() + .expect("Unable to retrieve signature hash from proposed block"); + assert!( + signature.verify(&aggregate_public_key, signature_hash.0.as_slice()), + "Signature verification failed" + ); + signer_test.shutdown(); +} From c45bc09483869b6e2681a5c1392f22dc01aee5c2 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 12 Jan 2024 08:37:13 -0500 Subject: [PATCH 19/27] Add braindumped psuedo code function for extracting block responses from signature Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/stackerdb.rs | 22 +++++- stacks-signer/src/client/stacks_client.rs | 6 -- stacks-signer/src/runloop.rs | 83 ++++++++++++++++++----- 3 files changed, 87 insertions(+), 24 deletions(-) diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 7032bcdcb2..5cd103f5fc 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -1,6 +1,6 @@ use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; -use blockstack_lib::net::api::postblock_proposal::ValidateRejectCode; +use blockstack_lib::net::api::postblock_proposal::{BlockValidateReject, ValidateRejectCode}; use clarity::vm::types::QualifiedContractIdentifier; use hashbrown::HashMap; use libsigner::{SignerSession, StackerDBSession}; @@ -62,6 +62,16 @@ pub struct BlockRejection { pub block: NakamotoBlock, } +impl From for BlockRejection { + fn from(reject: BlockValidateReject) -> Self { + Self { + reason: reject.reason, + reason_code: RejectCode::ValidationFailed(reject.reason_code), + block: reject.block, + } + } +} + /// This enum is used to supply a `reason_code` for block rejections #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[repr(u8)] @@ -70,6 +80,10 @@ pub enum RejectCode { ValidationFailed(ValidateRejectCode), /// Missing expected transactions MissingTransactions(Vec), + // No Consensus Reached + //NoConsensusReached, + // Consensus No Reached + //ConsensusNo(Signature), } impl From for SignerMessage { @@ -84,6 +98,12 @@ impl From for SignerMessage { } } +impl From for SignerMessage { + fn from(block_rejection: BlockRejection) -> Self { + Self::BlockResponse(BlockResponse::Rejected(block_rejection)) + } +} + impl SignerMessage { /// Helper function to determine the slot ID for the provided stacker-db writer id pub fn slot_id(&self, id: u32) -> u32 { diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 677f722420..9a690f79b7 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -52,12 +52,6 @@ impl From<&Config> for StacksClient { } impl StacksClient { - /// Retrieve the current miner public key - pub fn get_miner_public_key(&self) -> Result { - // TODO: Depends on https://github.com/stacks-network/stacks-core/issues/4018 - todo!("Get the miner public key from the stacks node to verify the miner blocks were signed by the correct miner"); - } - /// Submit the block proposal to the stacks node. The block will be validated and returned via the HTTP endpoint for Block events. pub fn submit_block_for_validation(&self, block: NakamotoBlock) -> Result<(), ClientError> { let block_proposal = NakamotoBlockProposal { diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 197d2e1d3d..140d8af562 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -22,8 +22,8 @@ use wsts::state_machine::{OperationResult, PublicKeys}; use wsts::v2; use crate::client::{ - retry_with_exponential_backoff, BlockRejection, BlockResponse, ClientError, RejectCode, - SignerMessage, StackerDB, StacksClient, + retry_with_exponential_backoff, BlockRejection, ClientError, SignerMessage, StackerDB, + StacksClient, }; use crate::config::{Config, Network}; @@ -64,8 +64,6 @@ pub struct RunLoop { /// The coordinator for inbound messages pub coordinator: C, /// The signing round used to sign messages - // TODO: update this to use frost_signer directly instead of the frost signing round - // See: https://github.com/stacks-network/stacks-blockchain/issues/3913 pub signing_round: Signer, /// The stacks node client pub stacks_client: StacksClient, @@ -199,23 +197,14 @@ impl RunLoop { "Received a block proposal that was rejected by the stacks node: {:?}", block_validate_reject ); - // TODO: submit a rejection response to the .signers contract for miners + // Submit a rejection response to the .signers contract for miners // to observe so they know to ignore it and to prove signers are doing work - let block_rejection = BlockRejection { - block: block_validate_reject.block, - reason: block_validate_reject.reason, - reason_code: RejectCode::ValidationFailed(block_validate_reject.reason_code), - }; - let message = - SignerMessage::BlockResponse(BlockResponse::Rejected(block_rejection)); + let block_rejection = BlockRejection::from(block_validate_reject); if let Err(e) = self .stackerdb - .send_message_with_retry(self.signing_round.signer_id, message) + .send_message_with_retry(self.signing_round.signer_id, block_rejection.into()) { - warn!( - "Failed to send block rejection response to stacker-db: {:?}", - e - ); + warn!("Failed to send block rejection to stacker-db: {:?}", e); } } } @@ -273,6 +262,7 @@ impl RunLoop { self.send_outbound_messages(signer_outbound_messages); self.send_outbound_messages(coordinator_outbound_messages); + self.send_block_response_messages(&operation_results); self.send_operation_results(res, operation_results); } @@ -294,6 +284,65 @@ impl RunLoop { } } + /// Helper function to extract block proposals from signature results and braodcast them to the stackerdb slot + fn send_block_response_messages(&mut self, _operation_results: &[OperationResult]) { + //TODO: Deserialize the signature result and broadcast an appropriate Reject or Approval message to stackerdb + // https://github.com/stacks-network/stacks-core/issues/3930 + // for result in operation_results { + // match result { + // OperationResult::Sign(signature) => { + // debug!("Successfully signed message: {:?}", signature); + // if signature.verify( + // &self + // .coordinator + // .get_aggregate_public_key() + // .expect("How could we have signed with no DKG?"), + // &block.unwrap().header.signature_hash().0, + // ) { + // block.header.signer_signature = Some(signature); + // let message = SignerMessage::BlockResponse(BlockResponse::Accepted(block)); + // // Submit the accepted signature to the stacks node + // if let Err(e) = self.stackerdb.send_message_with_retry( + // self.signing_round.signer_id, + // message, + // ) { + // warn!("Failed to send block rejection to stacker-db: {:?}", e); + // } + // } else if false // match against the hash of the block + "no" + // { + // warn!("Failed to verify signature: {:?}", signature); + // let block_rejection = BlockRejection { + // block, + // reject_code: RejectCode::ConsensusNo(signature), + // reason: "Consensus no vote".to_string() + // }; + // if let Err(e) = self + // .stackerdb + // .send_message_with_retry(self.signing_round.signer_id, block_rejection.into()) + // { + // warn!( + // "Failed to send block rejection to stacker-db: {:?}", + // e + // ); + // } + // } else { // No consensus reached + // if let Err(e) = self + // .stackerdb + // .send_message_with_retry(self.signing_round.signer_id, block_rejection.into()) + // { + // warn!( + // "Failed to send block rejection to stacker-db: {:?}", + // e + // ); + // } + // } + // }, + // _ => { + // // Nothing to do + // } + // } + } + /// Helper function to send operation results across the provided channel fn send_operation_results( &mut self, From bef51771afbe9c34d590eb833c0d4972401f08d8 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 12 Jan 2024 08:39:47 -0500 Subject: [PATCH 20/27] Add braindumped psuedo code function for extracting block responses from signature Signed-off-by: Jacinta Ferrant --- stacks-signer/src/runloop.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 140d8af562..c4ae8eb574 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -274,7 +274,8 @@ impl RunLoop { warn!("Received an unrecognized message type from .miners stacker-db slot id {}: {:?}", chunk.slot_id, ptr); continue; }; - + //TODO: trigger the signing round here instead. Then deserialize the block and call the validation as you validate its contents + // https://github.com/stacks-network/stacks-core/issues/3930 // Received a block proposal from the miner. Submit it for verification. self.stacks_client .submit_block_for_validation(block) From e8d0770343d1742692f4cf4ea4dd1150c7f717fc Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 12 Jan 2024 08:48:59 -0500 Subject: [PATCH 21/27] CRC: remove unused EventPrefix and fix copyright year from 2023 to 2024 in stacks-signer code Signed-off-by: Jacinta Ferrant --- libsigner/src/events.rs | 29 ----------------------------- stacks-signer/src/client/mod.rs | 2 +- stacks-signer/src/lib.rs | 2 +- 3 files changed, 2 insertions(+), 31 deletions(-) diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index a8ab01563f..7e50226ef6 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -38,35 +38,6 @@ use wsts::net::{Message, Packet}; use crate::http::{decode_http_body, decode_http_request}; use crate::EventError; -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[repr(u8)] -enum EventPrefix { - /// A StackerDB event - StackerDB, - /// A block proposal event - BlockProposal, -} - -impl From<&SignerEvent> for EventPrefix { - fn from(event: &SignerEvent) -> Self { - match event { - SignerEvent::StackerDB(_) => EventPrefix::StackerDB, - SignerEvent::BlockProposal(_) => EventPrefix::BlockProposal, - } - } -} -impl TryFrom for EventPrefix { - type Error = (); - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(EventPrefix::StackerDB), - 1 => Ok(EventPrefix::BlockProposal), - _ => Err(()), - } - } -} - /// Event enum for newly-arrived signer subscribed events #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum SignerEvent { diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index 73e6d756f3..ec7e8e8235 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/stacks-signer/src/lib.rs b/stacks-signer/src/lib.rs index c0a8a11f7c..cadb72c8a4 100644 --- a/stacks-signer/src/lib.rs +++ b/stacks-signer/src/lib.rs @@ -5,7 +5,7 @@ Usage documentation can be found in the [README](https://github.com/Trust-Machin */ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by From ef116a47ffe49ec4439a0adcd39011117d4467fe Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 12 Jan 2024 09:18:46 -0500 Subject: [PATCH 22/27] Filter out unknown contract ids from libsigner events Signed-off-by: Jacinta Ferrant --- libsigner/src/error.rs | 5 +++++ libsigner/src/events.rs | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/libsigner/src/error.rs b/libsigner/src/error.rs index fec6a1e8f1..101a1b35e9 100644 --- a/libsigner/src/error.rs +++ b/libsigner/src/error.rs @@ -16,6 +16,8 @@ use std::io; +use clarity::vm::types::QualifiedContractIdentifier; + /// Errors originating from doing an RPC request to the Stacks node #[derive(thiserror::Error, Debug)] pub enum RPCError { @@ -66,4 +68,7 @@ pub enum EventError { /// Unrecognized event error #[error("Unrecognized event: {0}")] UnrecognizedEvent(String), + /// Unrecognized stacker DB contract error + #[error("Unrecognized StackerDB contract: {0}")] + UnrecognizedStackerDBContract(QualifiedContractIdentifier), } diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index 7e50226ef6..cc05d5db0d 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -138,7 +138,7 @@ impl SignerEventReceiver { /// Do something with the socket pub fn with_server(&mut self, todo: F) -> Result where - F: FnOnce(&mut SignerEventReceiver, &mut HttpServer) -> R, + F: FnOnce(&SignerEventReceiver, &mut HttpServer, &[QualifiedContractIdentifier]) -> R, { let mut server = if let Some(s) = self.http_server.take() { s @@ -146,7 +146,7 @@ impl SignerEventReceiver { return Err(EventError::NotBound); }; - let res = todo(self, &mut server); + let res = todo(self, &mut server, &self.stackerdb_contract_ids); self.http_server = Some(server); Ok(res) @@ -203,7 +203,7 @@ impl EventReceiver for SignerEventReceiver { /// Errors are recoverable -- the caller should call this method again even if it returns an /// error. fn next_event(&mut self) -> Result { - self.with_server(|event_receiver, http_server| { + self.with_server(|event_receiver, http_server, contract_ids| { let mut request = http_server.recv()?; // were we asked to terminate? @@ -230,6 +230,18 @@ impl EventReceiver for SignerEventReceiver { EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)) })?; + if !contract_ids.contains(&event.contract_id) { + info!( + "[{:?}] next_event got event from an unexpected contract id {}, return OK so other side doesn't keep sending this", + event_receiver.local_addr, + event.contract_id + ); + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + return Err(EventError::UnrecognizedStackerDBContract(event.contract_id)); + } + request .respond(HttpResponse::empty(200u16)) .expect("response failed"); From 6712ac960ec2ec6d7025de4c4bfd8de33e06a378 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 12 Jan 2024 10:24:16 -0500 Subject: [PATCH 23/27] Update wsts version to 6.1 to use PartialEq change in Packet Signed-off-by: Jacinta Ferrant --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ebc7261cf9..33f1720b77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ # Dependencies we want to keep the same between workspace members [workspace.dependencies] -wsts = { path = "../wsts" } +wsts = "6.1" rand_core = "0.6" rand = "0.8" From 01089b65504a173805a8694c63de746d773a42b2 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 12 Jan 2024 12:34:50 -0500 Subject: [PATCH 24/27] Add stackerdb_dkg_sign test to CI Signed-off-by: Jacinta Ferrant --- .github/workflows/bitcoin-tests.yml | 1 + Cargo.lock | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 417d68e2b1..0cf9efa761 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -74,6 +74,7 @@ jobs: - tests::nakamoto_integrations::mine_multiple_per_tenure_integration - tests::nakamoto_integrations::block_proposal_api_endpoint - tests::nakamoto_integrations::miner_writes_proposed_block_to_stackerdb + - tests::signer::stackerdb_dkg_sign - tests::signer::stackerdb_block_proposal steps: ## Setup test environment diff --git a/Cargo.lock b/Cargo.lock index 03febb2e39..1eaf731f65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4714,7 +4714,9 @@ dependencies = [ [[package]] name = "wsts" -version = "6.0.0" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c7db3d3fe28c359e0cdb7f7ad83e3316bda0ba982b8cd1bf0fbe73ae4127e4b" dependencies = [ "aes-gcm 0.10.2", "bs58 0.5.0", From c870f6e1b55a99f8db613c52e42ca035f5c34e96 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 12 Jan 2024 15:55:09 -0500 Subject: [PATCH 25/27] CRC: add rustdocs to test, cleanup error handling in event.rs, and revert postblock_proposal api changes Signed-off-by: Jacinta Ferrant --- libsigner/src/events.rs | 30 +++++-- stackslib/src/net/api/postblock_proposal.rs | 94 +++++++++------------ testnet/stacks-node/src/tests/signer.rs | 37 ++++++-- 3 files changed, 94 insertions(+), 67 deletions(-) diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index cc05d5db0d..aa770820b9 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -220,10 +220,19 @@ impl EventReceiver for SignerEventReceiver { if request.url() == "/stackerdb_chunks" { debug!("Got stackerdb_chunks event"); let mut body = String::new(); - request + if let Err(e) = request .as_reader() - .read_to_string(&mut body) - .expect("failed to read body"); + .read_to_string(&mut body) { + error!("Failed to read body: {:?}", &e); + + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + return Err(EventError::MalformedRequest(format!( + "Failed to read body: {:?}", + &e + ))); + } let event: StackerDBChunksEvent = serde_json::from_slice(body.as_bytes()).map_err(|e| { @@ -250,10 +259,19 @@ impl EventReceiver for SignerEventReceiver { } else if request.url() == "/proposal_response" { debug!("Got proposal_response event"); let mut body = String::new(); - request + if let Err(e) = request .as_reader() - .read_to_string(&mut body) - .expect("failed to read body"); + .read_to_string(&mut body) { + error!("Failed to read body: {:?}", &e); + + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + return Err(EventError::MalformedRequest(format!( + "Failed to read body: {:?}", + &e + ))); + } let event: BlockValidateResponse = serde_json::from_slice(body.as_bytes()).map_err(|e| { diff --git a/stackslib/src/net/api/postblock_proposal.rs b/stackslib/src/net/api/postblock_proposal.rs index 1c6613f8d7..4091aabb5a 100644 --- a/stackslib/src/net/api/postblock_proposal.rs +++ b/stackslib/src/net/api/postblock_proposal.rs @@ -96,6 +96,25 @@ pub struct BlockValidateReject { pub reason_code: ValidateRejectCode, } +#[derive(Debug, Clone, PartialEq)] +pub struct BlockValidateRejectReason { + pub reason: String, + pub reason_code: ValidateRejectCode, +} + +impl From for BlockValidateRejectReason +where + T: Into, +{ + fn from(value: T) -> Self { + let ce: ChainError = value.into(); + Self { + reason: format!("Chainstate Error: {ce}"), + reason_code: ValidateRejectCode::ChainstateError, + } + } +} + /// A response for block proposal validation /// that the stacks-node thinks is acceptable. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -144,7 +163,13 @@ impl NakamotoBlockProposal { thread::Builder::new() .name("block-proposal".into()) .spawn(move || { - let result = self.validate(&sortdb, &mut chainstate); + let result = + self.validate(&sortdb, &mut chainstate) + .map_err(|reason| BlockValidateReject { + block: self.block.clone(), + reason_code: reason.reason_code, + reason: reason.reason, + }); receiver.notify_proposal_result(result); }) } @@ -163,36 +188,24 @@ impl NakamotoBlockProposal { &self, sortdb: &SortitionDB, chainstate: &mut StacksChainState, // not directly used; used as a handle to open other chainstates - ) -> Result { + ) -> Result { let ts_start = get_epoch_time_ms(); // Measure time from start of function let time_elapsed = || get_epoch_time_ms().saturating_sub(ts_start); let mainnet = self.chain_id == CHAIN_ID_MAINNET; if self.chain_id != chainstate.chain_id || mainnet != chainstate.mainnet { - return Err(BlockValidateReject { - block: self.block.clone(), + return Err(BlockValidateRejectReason { reason_code: ValidateRejectCode::InvalidBlock, reason: "Wrong network/chain_id".into(), }); } let burn_dbconn = sortdb.index_conn(); - let sort_tip = SortitionDB::get_canonical_sortition_tip(sortdb.conn()).map_err(|ce| { - BlockValidateReject { - block: self.block.clone(), - reason: format!("Chainstate Error: {ce}"), - reason_code: ValidateRejectCode::ChainstateError, - } - })?; + let sort_tip = SortitionDB::get_canonical_sortition_tip(sortdb.conn())?; let mut db_handle = sortdb.index_handle(&sort_tip); let expected_burn = - NakamotoChainState::get_expected_burns(&mut db_handle, chainstate.db(), &self.block) - .map_err(|ce| BlockValidateReject { - block: self.block.clone(), - reason: format!("Chainstate Error: {ce}"), - reason_code: ValidateRejectCode::ChainstateError, - })?; + NakamotoChainState::get_expected_burns(&mut db_handle, chainstate.db(), &self.block)?; // Static validation checks NakamotoChainState::validate_nakamoto_block_burnchain( @@ -201,25 +214,14 @@ impl NakamotoBlockProposal { &self.block, mainnet, self.chain_id, - ) - .map_err(|ce| BlockValidateReject { - block: self.block.clone(), - reason: format!("Chainstate Error: {ce}"), - reason_code: ValidateRejectCode::ChainstateError, - })?; + )?; // Validate txs against chainstate let parent_stacks_header = NakamotoChainState::get_block_header( chainstate.db(), &self.block.header.parent_block_id, - ) - .map_err(|ce| BlockValidateReject { - block: self.block.clone(), - reason: format!("Chainstate Error: {ce}"), - reason_code: ValidateRejectCode::ChainstateError, - })? - .ok_or_else(|| BlockValidateReject { - block: self.block.clone(), + )? + .ok_or_else(|| BlockValidateRejectReason { reason_code: ValidateRejectCode::InvalidBlock, reason: "Invalid parent block".into(), })?; @@ -244,27 +246,11 @@ impl NakamotoBlockProposal { self.block.header.burn_spent, tenure_change, coinbase, - ) - .map_err(|ce| BlockValidateReject { - block: self.block.clone(), - reason: format!("Chainstate Error: {ce}"), - reason_code: ValidateRejectCode::ChainstateError, - })?; + )?; - let mut miner_tenure_info = builder - .load_tenure_info(chainstate, &burn_dbconn, tenure_cause) - .map_err(|ce| BlockValidateReject { - block: self.block.clone(), - reason: format!("Chainstate Error: {ce}"), - reason_code: ValidateRejectCode::ChainstateError, - })?; - let mut tenure_tx = builder - .tenure_begin(&burn_dbconn, &mut miner_tenure_info) - .map_err(|ce| BlockValidateReject { - block: self.block.clone(), - reason: format!("Chainstate Error: {ce}"), - reason_code: ValidateRejectCode::ChainstateError, - })?; + let mut miner_tenure_info = + builder.load_tenure_info(chainstate, &burn_dbconn, tenure_cause)?; + let mut tenure_tx = builder.tenure_begin(&burn_dbconn, &mut miner_tenure_info)?; for (i, tx) in self.block.txs.iter().enumerate() { let tx_len = tx.tx_len(); @@ -291,8 +277,7 @@ impl NakamotoBlockProposal { "reason" => %reason, "tx" => ?tx, ); - return Err(BlockValidateReject { - block: self.block.clone(), + return Err(BlockValidateRejectReason { reason, reason_code: ValidateRejectCode::BadTransaction, }); @@ -321,8 +306,7 @@ impl NakamotoBlockProposal { //"expected_block" => %serde_json::to_string(&serde_json::to_value(&self.block).unwrap()).unwrap(), //"computed_block" => %serde_json::to_string(&serde_json::to_value(&block).unwrap()).unwrap(), ); - return Err(BlockValidateReject { - block: self.block.clone(), + return Err(BlockValidateRejectReason { reason: "Block hash is not as expected".into(), reason_code: ValidateRejectCode::BadBlockHash, }); diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 59c181fee2..160f83bdb1 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -401,6 +401,22 @@ fn stackerdb_dkg_sign() { #[test] #[ignore] +/// Test that a signer can respond to a miners request for a signature on a block proposal +/// +/// 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. and signers perform a DKG round (this should be removed +/// once we have proper casting of the vote during epoch 2.5). +/// +/// Test Execution: +/// The node attempts to mine a Nakamoto tenure, sending a block to the observing signers via the +/// .miners stacker db instance. The signers submit the block to the stacks node for verification. +/// Upon receiving a Block Validation response approving the block, the signers perform a signing +/// round across its signature hash. +/// +/// Test Assertion: +/// Signers return an operation result containing a valid signature across the miner's Nakamoto block's signature hash. +/// TODO: update this test to assert that the signers broadcast a Nakamoto block response back to the miners fn stackerdb_block_proposal() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -480,12 +496,6 @@ fn stackerdb_block_proposal() { .unwrap(); info!("------------------------- Test Block Processed -------------------------"); - //Wait for the block to show up in the test observer - let validate_responses = test_observer::get_proposal_responses(); - let proposed_block = match validate_responses.first().expect("No block proposal") { - BlockValidateResponse::Ok(block_validated) => block_validated.block.clone(), - _ => panic!("Unexpected response"), - }; let recv = signer_test .result_receivers .last() @@ -505,6 +515,21 @@ fn stackerdb_block_proposal() { } } let signature = signature.expect("Failed to get signature"); + // Wait for the block to show up in the test observer (Don't have to wait long as if we have received a signature, + // we know that the signers have already received their block proposal events via their event observers) + let t_start = std::time::Instant::now(); + while test_observer::get_proposal_responses().is_empty() { + assert!( + t_start.elapsed() < Duration::from_secs(30), + "Timed out while waiting for block proposal event" + ); + thread::sleep(Duration::from_secs(1)); + } + let validate_responses = test_observer::get_proposal_responses(); + let proposed_block = match validate_responses.first().expect("No block proposal") { + BlockValidateResponse::Ok(block_validated) => block_validated.block.clone(), + _ => panic!("Unexpected response"), + }; let signature_hash = proposed_block .header .signature_hash() From 32caa82f8c5ce782cbb240544d838ed96237129d Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 16 Jan 2024 09:13:55 -0500 Subject: [PATCH 26/27] Remove unnecessary changes to cli and configs Signed-off-by: Jacinta Ferrant --- stacks-signer/src/cli.rs | 6 +++--- stacks-signer/src/client/stackerdb.rs | 2 +- stacks-signer/src/config.rs | 24 ++++++++++------------ stacks-signer/src/main.rs | 4 ++-- stacks-signer/src/tests/conf/signer-0.toml | 2 +- stacks-signer/src/tests/conf/signer-1.toml | 2 +- stacks-signer/src/tests/conf/signer-2.toml | 2 +- stacks-signer/src/tests/conf/signer-3.toml | 2 +- stacks-signer/src/tests/conf/signer-4.toml | 2 +- stacks-signer/src/utils.rs | 4 ++-- testnet/stacks-node/src/tests/signer.rs | 14 ++++++------- 11 files changed, 31 insertions(+), 33 deletions(-) diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs index 65aa8ccafc..ad7b40e067 100644 --- a/stacks-signer/src/cli.rs +++ b/stacks-signer/src/cli.rs @@ -133,14 +133,14 @@ pub struct GenerateFilesArgs { pub signers_contract: QualifiedContractIdentifier, #[arg( long, - required_unless_present = "signer_private_keys", - conflicts_with = "signer_private_keys" + required_unless_present = "private_keys", + conflicts_with = "private_keys" )] /// The number of signers to generate pub num_signers: Option, #[clap(long, value_name = "FILE")] /// A path to a file containing a list of hexadecimal Stacks private keys of the signers - pub signer_private_keys: Option, + pub private_keys: Option, #[arg(long)] /// The total number of key ids to distribute among the signers pub num_keys: u32, diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 5cd103f5fc..4b1c5e5e53 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -140,7 +140,7 @@ impl From<&Config> for StackerDB { Self { signers_stackerdb_session: StackerDBSession::new( config.node_host, - config.signers_stackerdb_contract_id.clone(), + config.stackerdb_contract_id.clone(), ), stacks_private_key: config.stacks_private_key, slot_versions: HashMap::new(), diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 61ca95fa3a..aa031e7eb1 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -100,7 +100,7 @@ pub struct Config { /// endpoint to the event receiver pub endpoint: SocketAddr, /// smart contract that controls the target signers' stackerdb - pub signers_stackerdb_contract_id: QualifiedContractIdentifier, + pub stackerdb_contract_id: QualifiedContractIdentifier, /// The Scalar representation of the private key for signer communication pub message_private_key: Scalar, /// The signer's Stacks private key @@ -141,9 +141,9 @@ struct RawConfigFile { pub node_host: String, /// endpoint to event receiver pub endpoint: String, - // FIXME: this should go away once .signers contract exists + // FIXME: this should go away once .signers contract exists at pox-4 instantiation /// Signers' Stacker db contract identifier - pub signers_stackerdb_contract_id: String, + pub stackerdb_contract_id: String, /// the 32 byte ECDSA private key used to sign blocks, chunks, and transactions pub message_private_key: String, /// The hex representation of the signer's Stacks private key used for communicating @@ -215,15 +215,13 @@ impl TryFrom for Config { raw_data.endpoint.clone(), ))?; - let signers_stackerdb_contract_id = QualifiedContractIdentifier::parse( - &raw_data.signers_stackerdb_contract_id, - ) - .map_err(|_| { - ConfigError::BadField( - "signers_stackerdb_contract_id".to_string(), - raw_data.signers_stackerdb_contract_id, - ) - })?; + let stackerdb_contract_id = + QualifiedContractIdentifier::parse(&raw_data.stackerdb_contract_id).map_err(|_| { + ConfigError::BadField( + "stackerdb_contract_id".to_string(), + raw_data.stackerdb_contract_id, + ) + })?; let message_private_key = Scalar::try_from(raw_data.message_private_key.as_str()).map_err(|_| { @@ -275,7 +273,7 @@ impl TryFrom for Config { Ok(Self { node_host, endpoint, - signers_stackerdb_contract_id, + stackerdb_contract_id, message_private_key, stacks_private_key, stacks_address, diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 1a41918712..a04d6a24f6 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -88,7 +88,7 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let config = Config::try_from(path).unwrap(); let (cmd_send, cmd_recv) = channel(); let (res_send, res_recv) = channel(); - let ev = SignerEventReceiver::new(vec![config.signers_stackerdb_contract_id.clone()]); + let ev = SignerEventReceiver::new(vec![config.stackerdb_contract_id.clone()]); let runloop: RunLoop> = RunLoop::from(&config); let mut signer: Signer< RunLoopCommand, @@ -247,7 +247,7 @@ fn handle_run(args: RunDkgArgs) { fn handle_generate_files(args: GenerateFilesArgs) { debug!("Generating files..."); - let signer_stacks_private_keys = if let Some(path) = args.signer_private_keys { + let signer_stacks_private_keys = if let Some(path) = args.private_keys { let file = File::open(&path).unwrap(); let reader = io::BufReader::new(file); diff --git a/stacks-signer/src/tests/conf/signer-0.toml b/stacks-signer/src/tests/conf/signer-0.toml index dc9bbf61c2..ee510d563e 100644 --- a/stacks-signer/src/tests/conf/signer-0.toml +++ b/stacks-signer/src/tests/conf/signer-0.toml @@ -4,7 +4,7 @@ stacks_private_key = "69be0e68947fa7128702761151dc8d9b39ee1401e547781bb2ec3e5b4e node_host = "127.0.0.1:20443" endpoint = "localhost:30000" network = "testnet" -signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" signer_id = 0 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-1.toml b/stacks-signer/src/tests/conf/signer-1.toml index c0988c9c8d..73d5cb6a69 100644 --- a/stacks-signer/src/tests/conf/signer-1.toml +++ b/stacks-signer/src/tests/conf/signer-1.toml @@ -4,7 +4,7 @@ stacks_private_key = "fd5a538e8548e9d6a4a4060a43d0142356df022a4b8fd8ed4a7d066382 node_host = "127.0.0.1:20443" endpoint = "localhost:30001" network = "testnet" -signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" signer_id = 1 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-2.toml b/stacks-signer/src/tests/conf/signer-2.toml index b6987b71b6..7ff263940d 100644 --- a/stacks-signer/src/tests/conf/signer-2.toml +++ b/stacks-signer/src/tests/conf/signer-2.toml @@ -4,7 +4,7 @@ stacks_private_key = "74e8e8550a5210b89461128c600e4bf611d1553e6809308bc012dbb0fb node_host = "127.0.0.1:20443" endpoint = "localhost:30002" network = "testnet" -signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" signer_id = 2 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-3.toml b/stacks-signer/src/tests/conf/signer-3.toml index 114ea38218..e7ac219a40 100644 --- a/stacks-signer/src/tests/conf/signer-3.toml +++ b/stacks-signer/src/tests/conf/signer-3.toml @@ -4,7 +4,7 @@ stacks_private_key = "803fa7b9c8a39ed368f160b3dcbfaa8f677fc157ffbccb46ee3e4a32a3 node_host = "127.0.0.1:20443" endpoint = "localhost:30003" network = "testnet" -signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" signer_id = 3 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/tests/conf/signer-4.toml b/stacks-signer/src/tests/conf/signer-4.toml index 37a68f1035..c2eb3f37d0 100644 --- a/stacks-signer/src/tests/conf/signer-4.toml +++ b/stacks-signer/src/tests/conf/signer-4.toml @@ -4,7 +4,7 @@ stacks_private_key = "1bfdf386114aacf355fe018a1ec7ac728fa05ca20a6131a70f686291bb node_host = "127.0.0.1:20443" endpoint = "localhost:30004" network = "testnet" -signers_stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" +stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" signer_id = 4 signers = [ {public_key = "swBaKxfzs4pQne7spxhrkF6AtB34WEcreAkJ8mPcqx3t", key_ids = [1, 2, 3, 4]} diff --git a/stacks-signer/src/utils.rs b/stacks-signer/src/utils.rs index b99087bfe0..5d882e74cc 100644 --- a/stacks-signer/src/utils.rs +++ b/stacks-signer/src/utils.rs @@ -12,7 +12,7 @@ pub fn build_signer_config_tomls( signer_stacks_private_keys: &[StacksPrivateKey], num_keys: u32, node_host: &str, - signers_stackerdb_contract_id: &str, + stackerdb_contract_id: &str, timeout: Option, ) -> Vec { let num_signers = signer_stacks_private_keys.len() as u32; @@ -71,7 +71,7 @@ stacks_private_key = "{stacks_private_key}" node_host = "{node_host}" endpoint = "{endpoint}" network = "testnet" -signers_stackerdb_contract_id = "{signers_stackerdb_contract_id}" +stackerdb_contract_id = "{stackerdb_contract_id}" signer_id = {id} {signers_array} "# diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 160f83bdb1..08a928d092 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -172,7 +172,7 @@ fn spawn_signer( let config = stacks_signer::config::Config::load_from_str(data).unwrap(); let ev = SignerEventReceiver::new(vec![ boot_code_id(MINERS_NAME, config.network == Network::Mainnet), - config.signers_stackerdb_contract_id.clone(), + config.stackerdb_contract_id.clone(), ]); let runloop: stacks_signer::runloop::RunLoop> = stacks_signer::runloop::RunLoop::from(&config); @@ -195,8 +195,8 @@ fn setup_stx_btc_node( num_signers: u32, signer_stacks_private_keys: &[StacksPrivateKey], publisher_private_key: &StacksPrivateKey, - signers_stackerdb_contract: &str, - signers_stackerdb_contract_id: &QualifiedContractIdentifier, + stackerdb_contract: &str, + stackerdb_contract_id: &QualifiedContractIdentifier, signer_config_tomls: &Vec, ) -> RunningNodes { // Spawn the endpoints for observing signers @@ -235,7 +235,7 @@ fn setup_stx_btc_node( naka_conf .node .stacker_dbs - .push(signers_stackerdb_contract_id.clone()); + .push(stackerdb_contract_id.clone()); naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000); let stacker_sk = setup_stacker(&mut naka_conf); @@ -291,12 +291,12 @@ fn setup_stx_btc_node( publisher_private_key, 0, tx_fee, - &signers_stackerdb_contract_id.name, - signers_stackerdb_contract, + &stackerdb_contract_id.name, + stackerdb_contract, ); submit_tx(&http_origin, &tx); // mine it - info!("Mining the stackerdb contract: {signers_stackerdb_contract_id}"); + info!("Mining the signers stackerdb contract: {stackerdb_contract_id}"); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); From 52ccb11f82d27fc6379a826d61cd1d02a90376ec Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 16 Jan 2024 16:52:05 -0500 Subject: [PATCH 27/27] CRC: add copyright to all files, remove commented out code, and move StackerDBChunksEvent to event.rs Signed-off-by: Jacinta Ferrant --- libsigner/src/events.rs | 2 +- libsigner/src/tests/mod.rs | 2 +- stacks-signer/src/cli.rs | 15 ++++ stacks-signer/src/client/stackerdb.rs | 19 +++-- stacks-signer/src/client/stacks_client.rs | 15 ++++ stacks-signer/src/runloop.rs | 70 +++++-------------- stacks-signer/src/utils.rs | 15 ++++ stackslib/src/chainstate/stacks/events.rs | 10 +++ stackslib/src/net/api/poststackerdbchunk.rs | 9 --- testnet/stacks-node/src/event_dispatcher.rs | 4 +- .../src/tests/neon_integrations.rs | 2 +- 11 files changed, 91 insertions(+), 72 deletions(-) diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index aa770820b9..dde39a3f83 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -22,8 +22,8 @@ use std::sync::Arc; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; use blockstack_lib::chainstate::stacks::boot::MINERS_NAME; +use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent; use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; -use blockstack_lib::net::api::poststackerdbchunk::StackerDBChunksEvent; use blockstack_lib::util_lib::boot::boot_code_id; use clarity::vm::types::QualifiedContractIdentifier; use serde::{Deserialize, Serialize}; diff --git a/libsigner/src/tests/mod.rs b/libsigner/src/tests/mod.rs index 3e16bf4729..0048b7435c 100644 --- a/libsigner/src/tests/mod.rs +++ b/libsigner/src/tests/mod.rs @@ -22,7 +22,7 @@ use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; use std::{mem, thread}; -use blockstack_lib::net::api::poststackerdbchunk::StackerDBChunksEvent; +use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent; use clarity::vm::types::QualifiedContractIdentifier; use libstackerdb::StackerDBChunkData; use stacks_common::util::secp256k1::Secp256k1PrivateKey; diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs index ad7b40e067..d5b549fd1a 100644 --- a/stacks-signer/src/cli.rs +++ b/stacks-signer/src/cli.rs @@ -1,3 +1,18 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . use std::io::{self, Read}; use std::net::SocketAddr; use std::path::PathBuf; diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 4b1c5e5e53..4631ecbd4d 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -1,3 +1,18 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; use blockstack_lib::net::api::postblock_proposal::{BlockValidateReject, ValidateRejectCode}; @@ -80,10 +95,6 @@ pub enum RejectCode { ValidationFailed(ValidateRejectCode), /// Missing expected transactions MissingTransactions(Vec), - // No Consensus Reached - //NoConsensusReached, - // Consensus No Reached - //ConsensusNo(Signature), } impl From for SignerMessage { diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 9a690f79b7..e8a39b82cf 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -1,3 +1,18 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; use blockstack_lib::chainstate::stacks::boot::POX_4_NAME; diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index c4ae8eb574..5f68359a1c 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -1,11 +1,26 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . use std::collections::VecDeque; use std::sync::mpsc::Sender; use std::time::Duration; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; use blockstack_lib::chainstate::stacks::boot::MINERS_NAME; +use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent; use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; -use blockstack_lib::net::api::poststackerdbchunk::StackerDBChunksEvent; use blockstack_lib::util_lib::boot::boot_code_id; use hashbrown::{HashMap, HashSet}; use libsigner::{SignerEvent, SignerRunLoop}; @@ -289,59 +304,6 @@ impl RunLoop { fn send_block_response_messages(&mut self, _operation_results: &[OperationResult]) { //TODO: Deserialize the signature result and broadcast an appropriate Reject or Approval message to stackerdb // https://github.com/stacks-network/stacks-core/issues/3930 - // for result in operation_results { - // match result { - // OperationResult::Sign(signature) => { - // debug!("Successfully signed message: {:?}", signature); - // if signature.verify( - // &self - // .coordinator - // .get_aggregate_public_key() - // .expect("How could we have signed with no DKG?"), - // &block.unwrap().header.signature_hash().0, - // ) { - // block.header.signer_signature = Some(signature); - // let message = SignerMessage::BlockResponse(BlockResponse::Accepted(block)); - // // Submit the accepted signature to the stacks node - // if let Err(e) = self.stackerdb.send_message_with_retry( - // self.signing_round.signer_id, - // message, - // ) { - // warn!("Failed to send block rejection to stacker-db: {:?}", e); - // } - // } else if false // match against the hash of the block + "no" - // { - // warn!("Failed to verify signature: {:?}", signature); - // let block_rejection = BlockRejection { - // block, - // reject_code: RejectCode::ConsensusNo(signature), - // reason: "Consensus no vote".to_string() - // }; - // if let Err(e) = self - // .stackerdb - // .send_message_with_retry(self.signing_round.signer_id, block_rejection.into()) - // { - // warn!( - // "Failed to send block rejection to stacker-db: {:?}", - // e - // ); - // } - // } else { // No consensus reached - // if let Err(e) = self - // .stackerdb - // .send_message_with_retry(self.signing_round.signer_id, block_rejection.into()) - // { - // warn!( - // "Failed to send block rejection to stacker-db: {:?}", - // e - // ); - // } - // } - // }, - // _ => { - // // Nothing to do - // } - // } } /// Helper function to send operation results across the provided channel diff --git a/stacks-signer/src/utils.rs b/stacks-signer/src/utils.rs index 5d882e74cc..5e7af9a4e0 100644 --- a/stacks-signer/src/utils.rs +++ b/stacks-signer/src/utils.rs @@ -1,3 +1,18 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . use std::time::Duration; use rand_core::OsRng; diff --git a/stackslib/src/chainstate/stacks/events.rs b/stackslib/src/chainstate/stacks/events.rs index 625b3002c0..a744d126b4 100644 --- a/stackslib/src/chainstate/stacks/events.rs +++ b/stackslib/src/chainstate/stacks/events.rs @@ -4,6 +4,7 @@ pub use clarity::vm::events::StacksTransactionEvent; use clarity::vm::types::{ AssetIdentifier, PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, Value, }; +use libstackerdb::StackerDBChunkData; use stacks_common::codec::StacksMessageCodec; use stacks_common::types::chainstate::{BlockHeaderHash, StacksAddress}; use stacks_common::util::hash::to_hex; @@ -86,3 +87,12 @@ impl From<(NakamotoBlock, BlockHeaderHash)> for StacksBlockEventData { } } } + +/// Event structure for newly-arrived StackerDB data +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StackerDBChunksEvent { + /// The contract ID for the StackerDB instance + pub contract_id: QualifiedContractIdentifier, + /// The chunk data for newly-modified slots + pub modified_slots: Vec, +} diff --git a/stackslib/src/net/api/poststackerdbchunk.rs b/stackslib/src/net/api/poststackerdbchunk.rs index a006cf386b..3ca82b4141 100644 --- a/stackslib/src/net/api/poststackerdbchunk.rs +++ b/stackslib/src/net/api/poststackerdbchunk.rs @@ -54,15 +54,6 @@ use crate::net::{ }; use crate::util_lib::db::{DBConn, Error as DBError}; -/// Event structure for newly-arrived StackerDB data -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct StackerDBChunksEvent { - /// The contract ID for the StackerDB instance - pub contract_id: QualifiedContractIdentifier, - /// The chunk data for newly-modified slots - pub modified_slots: Vec, -} - #[derive(Clone)] pub struct RPCPostStackerDBChunkRequestHandler { pub contract_identifier: Option, diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index 98c08ba4a0..6c48ae9214 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -21,7 +21,8 @@ use stacks::chainstate::stacks::db::accounts::MinerReward; use stacks::chainstate::stacks::db::unconfirmed::ProcessedUnconfirmedState; use stacks::chainstate::stacks::db::{MinerRewardInfo, StacksHeaderInfo}; use stacks::chainstate::stacks::events::{ - StacksBlockEventData, StacksTransactionEvent, StacksTransactionReceipt, TransactionOrigin, + StackerDBChunksEvent, StacksBlockEventData, StacksTransactionEvent, StacksTransactionReceipt, + TransactionOrigin, }; use stacks::chainstate::stacks::miner::TransactionEvent; use stacks::chainstate::stacks::{ @@ -32,7 +33,6 @@ use stacks::libstackerdb::StackerDBChunkData; use stacks::net::api::postblock_proposal::{ BlockValidateOk, BlockValidateReject, BlockValidateResponse, }; -use stacks::net::api::poststackerdbchunk::StackerDBChunksEvent; use stacks::net::atlas::{Attachment, AttachmentInstance}; use stacks::net::stackerdb::StackerDBEventDispatcher; use stacks_common::codec::StacksMessageCodec; diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index c9d529bf21..9cb4d1a33a 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -177,8 +177,8 @@ pub mod test_observer { use std::sync::Mutex; use std::thread; + use stacks::chainstate::stacks::events::StackerDBChunksEvent; use stacks::net::api::postblock_proposal::BlockValidateResponse; - use stacks::net::api::poststackerdbchunk::StackerDBChunksEvent; use warp::Filter; use {tokio, warp};