diff --git a/Cargo.lock b/Cargo.lock index 4096da6da0b..09b8755188e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,17 +391,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "getrandom 0.2.8", - "instant", - "rand 0.8.5", -] - [[package]] name = "backtrace" version = "0.3.67" @@ -3552,7 +3541,6 @@ dependencies = [ name = "stacks-signer" version = "0.0.1" dependencies = [ - "backoff", "bincode", "clap 4.4.1", "clarity", diff --git a/stacks-signer/Cargo.toml b/stacks-signer/Cargo.toml index 50f501b51ad..ff488845311 100644 --- a/stacks-signer/Cargo.toml +++ b/stacks-signer/Cargo.toml @@ -20,7 +20,6 @@ name = "stacks-signer" path = "src/main.rs" [dependencies] -backoff = "0.4" bincode = "1.3.3" clarity = { path = "../clarity" } clap = { version = "4.1.1", features = ["derive", "env"] } diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index f198cf4044c..db9b11f38d7 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -105,8 +105,6 @@ pub struct Config { pub endpoint: SocketAddr, /// smart contract that controls the target stackerdb pub stackerdb_contract_id: QualifiedContractIdentifier, - /// smart contract that controls the target stackerdb - 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 @@ -139,11 +137,8 @@ struct RawConfigFile { pub node_host: String, /// 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 + /// contract identifier pub 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 @@ -223,17 +218,6 @@ impl TryFrom for Config { ) })?; - 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( @@ -285,7 +269,6 @@ impl TryFrom for Config { node_host, endpoint, 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 dce30b8d420..cf8b4bd72b5 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -269,7 +269,6 @@ fn handle_generate_files(args: GenerateFilesArgs) { args.num_keys, &args.db_args.host.to_string(), &args.db_args.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 7d6bf54d1a1..69a1904e000 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -15,7 +15,7 @@ use wsts::state_machine::{OperationResult, PublicKeys}; use wsts::v2; use crate::config::Config; -use crate::stacks_client::{retry_with_exponential_backoff, ClientError, StacksClient}; +use crate::stacks_client::StacksClient; /// Which operation to perform #[derive(PartialEq, Clone)] @@ -36,9 +36,6 @@ pub enum RunLoopCommand { /// The RunLoop state #[derive(PartialEq, Debug)] pub enum State { - // TODO: Uninitialized should indicate we need to replay events/configure the signer - /// The runloop signer is uninitialized - Uninitialized, /// The runloop is idle Idle, /// The runloop is executing a DKG round @@ -51,7 +48,7 @@ pub enum State { pub struct RunLoop { /// The timeout for events pub event_timeout: Duration, - /// The coordinator for inbound messages + /// 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 @@ -66,27 +63,7 @@ pub struct RunLoop { } impl RunLoop { - /// Initialize the signer, reading the stacker-db state and setting the aggregate public key - fn initialize(&mut self) -> Result<(), ClientError> { - // TODO: update to read stacker db to get state. - // Check if the aggregate key is set in the pox contract - if let Some(key) = self.stacks_client.get_aggregate_public_key()? { - debug!("Aggregate public key is set: {:?}", key); - self.coordinator.set_aggregate_public_key(Some(key)); - } else { - // 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 - && self.commands.front() != Some(&RunLoopCommand::Dkg) - { - self.commands.push_front(RunLoopCommand::Dkg); - } - } - self.state = State::Idle; - Ok(()) - } - - /// Execute the given command and update state accordingly + /// Helper function to actually execute the command and update state accordingly /// Returns true when it is successfully executed, else false fn execute_command(&mut self, command: &RunLoopCommand) -> bool { match command { @@ -96,7 +73,7 @@ impl RunLoop { Ok(msg) => { let ack = self .stacks_client - .send_message_with_retry(self.signing_round.signer_id, msg); + .send_message(self.signing_round.signer_id, msg); debug!("ACK: {:?}", ack); self.state = State::Dkg; true @@ -122,7 +99,7 @@ impl RunLoop { Ok(msg) => { let ack = self .stacks_client - .send_message_with_retry(self.signing_round.signer_id, msg); + .send_message(self.signing_round.signer_id, msg); debug!("ACK: {:?}", ack); self.state = State::Sign; true @@ -138,14 +115,9 @@ impl RunLoop { } } - /// Attempt to process the next command in the queue, and update state accordingly + /// Helper function to check the current state, process the next command in the queue, and update state accordingly fn process_next_command(&mut self) { match self.state { - State::Uninitialized => { - debug!( - "Signer is uninitialized. Waiting for aggregate public key from stacks node..." - ); - } State::Idle => { if let Some(command) = self.commands.pop_front() { while !self.execute_command(&command) { @@ -233,29 +205,26 @@ impl From<&Config> for RunLoop> { .iter() .map(|i| i - 1) // SigningRound::new (unlike SigningRound::from) doesn't do this .collect::>(); - let coordinator = FrostCoordinator::new( - total_signers, - total_keys, - threshold, - config.message_private_key, - ); - let signing_round = SigningRound::new( - threshold, - total_signers, - total_keys, - config.signer_id, - key_ids, - config.message_private_key, - config.signer_ids_public_keys.clone(), - ); - let stacks_client = StacksClient::from(config); RunLoop { event_timeout: config.event_timeout, - coordinator, - signing_round, - stacks_client, + coordinator: FrostCoordinator::new( + total_signers, + total_keys, + threshold, + config.message_private_key, + ), + signing_round: SigningRound::new( + threshold, + total_signers, + total_keys, + config.signer_id, + key_ids, + config.message_private_key, + config.signer_ids_public_keys.clone(), + ), + stacks_client: StacksClient::from(config), commands: VecDeque::new(), - state: State::Uninitialized, + state: State::Idle, } } } @@ -275,19 +244,10 @@ impl SignerRunLoop, RunLoopCommand> for R cmd: Option, res: Sender>, ) -> Option> { - info!( - "Running one pass for signer ID# {}. Current state: {:?}", - self.signing_round.signer_id, self.state - ); if let Some(command) = cmd { self.commands.push_back(command); } - 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 + // First process any arrived events if let Some(event) = event { let (outbound_messages, operation_results) = self.process_event(&event); debug!( @@ -297,7 +257,7 @@ impl SignerRunLoop, RunLoopCommand> for R for msg in outbound_messages { let ack = self .stacks_client - .send_message_with_retry(self.signing_round.signer_id, msg); + .send_message(self.signing_round.signer_id, msg); if let Ok(ack) = ack { debug!("ACK: {:?}", ack); } else { diff --git a/stacks-signer/src/stacks_client.rs b/stacks-signer/src/stacks_client.rs index 700858cfb47..e2117f63238 100644 --- a/stacks-signer/src/stacks_client.rs +++ b/stacks-signer/src/stacks_client.rs @@ -1,16 +1,10 @@ -use std::time::Duration; - use bincode::Error as BincodeError; -use blockstack_lib::{ - burnchains::Txid, - chainstate::stacks::{ - StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth, - TransactionContractCall, TransactionPayload, TransactionPostConditionMode, - TransactionSpendingCondition, TransactionVersion, - }, +use blockstack_lib::chainstate::stacks::{ + StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth, + TransactionContractCall, TransactionPayload, TransactionPostConditionMode, + TransactionSpendingCondition, TransactionVersion, }; use clarity::vm::{ - types::{serialization::SerializationError, QualifiedContractIdentifier, SequenceData}, Value as ClarityValue, {ClarityName, ContractName}, }; use hashbrown::HashMap; @@ -19,23 +13,16 @@ use libstackerdb::{Error as StackerDBError, StackerDBChunkAckData, StackerDBChun use serde_json::json; use slog::{slog_debug, slog_warn}; use stacks_common::{ + codec, codec::StacksMessageCodec, debug, types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}, warn, }; -use wsts::{ - net::{Message, Packet}, - Point, 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 @@ -46,7 +33,7 @@ pub const SLOTS_PER_USER: u32 = 10; pub enum ClientError { /// An error occurred serializing the message #[error("Unable to serialize stacker-db message: {0}")] - StackerDBSerializationError(#[from] BincodeError), + Serialize(#[from] BincodeError), /// Failed to sign stacker-db chunk #[error("Failed to sign stacker-db chunk: {0}")] FailToSign(#[from] StackerDBError), @@ -65,24 +52,27 @@ pub enum ClientError { /// 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), + /// Failure to submit a read only contract call + #[error("Failure to submit tx")] + TransactionSubmissionFailure, + /// Failed to sign with the provided private key + #[error("Failed to sign with the given private key")] + SignatureGenerationFailure, + /// Failed to sign with the provided private key + #[error("Failed to sign with the sponsor private key")] + SponsorSignatureGenerationFailure, + /// Failed to sign with the provided private key + #[error("Failed to serialize tx {0}")] + FailureToSerializeTx(String), + /// Failed to sign with the provided private key + #[error("{0}")] + FailureToDeserializeTx(#[from] codec::Error), + /// Failed to create a p2pkh spending condition + #[error("Failed to create p2pkh spending condition from public key {0}")] + FailureToCreateSpendingFromPublicKey(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 @@ -95,7 +85,7 @@ pub struct StacksClient { stacks_private_key: StacksPrivateKey, /// A map of a slot ID to last chunk version slot_versions: HashMap, - /// The stacks node HTTP base endpoint + /// The RPC endpoint used to communicate HTTP endpoints with http_origin: String, /// The types of transactions tx_version: TransactionVersion, @@ -103,8 +93,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 { @@ -121,14 +109,13 @@ 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(), } } } impl StacksClient { - /// Sends messages to the stacker-db with an exponential backoff retry - pub fn send_message_with_retry( + /// Sends messages to the stacker-db + pub fn send_message( &mut self, id: u32, message: Packet, @@ -141,12 +128,7 @@ impl StacksClient { 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)?; + let chunk_ack = self.stackerdb_session.put_chunk(chunk)?; self.slot_versions.insert(slot_id, slot_version); if chunk_ack.accepted { @@ -168,23 +150,6 @@ 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) - } - /// Retrieve the total number of slots allocated to a stacker-db writer #[allow(dead_code)] pub fn slots_per_user(&self) -> u32 { @@ -193,194 +158,143 @@ impl StacksClient { 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"); + fn serialize_sign_sig_tx_anchor_mode_version( + &self, + payload: TransactionPayload, + sender_nonce: u64, + tx_fee: u64, + anchor_mode: TransactionAnchorMode, + ) -> Result, ClientError> { + self.seralize_sign_sponsored_tx_anchor_mode_version( + payload, + None, + sender_nonce, + None, + tx_fee, + anchor_mode, + ) } - /// 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) + fn seralize_sign_sponsored_tx_anchor_mode_version( + &self, + payload: TransactionPayload, + payer: Option<&StacksPrivateKey>, + sender_nonce: u64, + payer_nonce: Option, + tx_fee: u64, + anchor_mode: TransactionAnchorMode, + ) -> Result, ClientError> { + let pubkey = StacksPublicKey::from_private(&self.stacks_private_key); + let mut sender_spending_condition = + TransactionSpendingCondition::new_singlesig_p2pkh(pubkey).ok_or( + ClientError::FailureToCreateSpendingFromPublicKey(pubkey.to_hex()), + )?; + sender_spending_condition.set_nonce(sender_nonce); + + let auth = match (payer, payer_nonce) { + (Some(payer), Some(payer_nonce)) => { + let pubkey = StacksPublicKey::from_private(payer); + let mut payer_spending_condition = + TransactionSpendingCondition::new_singlesig_p2pkh(pubkey).ok_or( + ClientError::FailureToCreateSpendingFromPublicKey(pubkey.to_hex()), + )?; + payer_spending_condition.set_nonce(payer_nonce); + payer_spending_condition.set_tx_fee(tx_fee); + TransactionAuth::Sponsored(sender_spending_condition, payer_spending_condition) + } + _ => { + sender_spending_condition.set_tx_fee(tx_fee); + TransactionAuth::Standard(sender_spending_condition) + } }; - 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)) - } + let mut unsigned_tx = StacksTransaction::new(self.tx_version, auth, payload); + unsigned_tx.anchor_mode = anchor_mode; + unsigned_tx.post_condition_mode = TransactionPostConditionMode::Allow; + unsigned_tx.chain_id = self.chain_id; - /// 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)) + let mut tx_signer = StacksTransactionSigner::new(&unsigned_tx); + tx_signer + .sign_origin(&self.stacks_private_key) + .map_err(|_| ClientError::SignatureGenerationFailure)?; + if let (Some(payer), Some(_)) = (payer, payer_nonce) { + tx_signer + .sign_sponsor(payer) + .map_err(|_| ClientError::SponsorSignatureGenerationFailure)?; } - } - /// 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) + let Some(tx) = tx_signer.get_tx() else { + return Err(ClientError::SignatureGenerationFailure); + }; + + Ok(tx.serialize_to_vec()) } - /// Helper function to create a stacks transaction for a modifying contract call - fn build_signed_transaction( + /// Creates a transaction for a contract call that can be submitted to a stacks node + pub fn transaction_contract_call( &self, + nonce: u64, contract_addr: &StacksAddress, contract_name: ContractName, function_name: ClarityName, function_args: &[ClarityValue], - ) -> Result { - let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { + ) -> Result, ClientError> { + let payload = 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()))?; + let tx_fee = 0; - tx_signer - .get_tx() - .ok_or(ClientError::TransactionGenerationFailure( - "Failed to generate transaction from a transaction signer".to_string(), - )) + self.serialize_sign_sig_tx_anchor_mode_version( + payload.into(), + nonce, + tx_fee, + TransactionAnchorMode::OnChainOnly, + ) } - /// 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())); + /// Submits a transaction to the Stacks node + pub fn submit_tx(&self, tx: Vec) -> Result { + let path = format!("{}/v2/transactions", self.http_origin); + let res = self + .stacks_node_client + .post(path) + .header("Content-Type", "application/octet-stream") + .body(tx.clone()) + .send()?; + if res.status().is_success() { + let res: String = res.json()?; + let tx_deserialized = StacksTransaction::consensus_deserialize(&mut &tx[..])?; + assert_eq!(res, tx_deserialized.txid().to_string()); + Ok(res) + } else { + Err(ClientError::TransactionSubmissionFailure) } - Ok(txid) } /// Makes a read only contract call to a stacks contract - pub fn read_only_contract_call_with_retry( + pub fn read_only_contract_call( &self, contract_addr: &StacksAddress, - contract_name: &ContractName, - function_name: &ClarityName, + 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)?; + let body = json!({"sender": self.stacks_address.to_string(), "arguments": function_args}) + .to_string(); + let path = format!( + "{}/v2/contracts/call-read/{contract_addr}/{contract_name}/{function_name}", + self.http_origin + ); + let response = self + .stacks_node_client + .post(path) + .header("Content-Type", "application/json") + .body(body) + .send()?; if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } @@ -406,46 +320,6 @@ impl StacksClient { .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 @@ -467,7 +341,7 @@ fn slot_id(id: u32, message: &Message) -> u32 { #[cfg(test)] mod tests { use std::{ - io::{BufWriter, Read, Write}, + io::{Read, Write}, net::{SocketAddr, TcpListener}, thread::spawn, }; @@ -514,10 +388,10 @@ mod tests { 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.read_only_contract_call( &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), + ContractName::try_from("contract-name").unwrap(), + ClarityName::try_from("function-name").unwrap(), &[], ) }); @@ -529,33 +403,14 @@ mod tests { 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.read_only_contract_call( &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), + ContractName::try_from("contract-name").unwrap(), + ClarityName::try_from("function-name").unwrap(), &[], ) }); @@ -572,17 +427,17 @@ mod tests { let config = TestConfig::new(); // Simulate a 400 Bad Request response let h = spawn(move || { - config.client.read_only_contract_call_with_retry( + config.client.read_only_contract_call( &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), + 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, + dbg!(result), Err(ClientError::RequestFailure( reqwest::StatusCode::BAD_REQUEST )) @@ -594,170 +449,18 @@ mod tests { let config = TestConfig::new(); // Simulate a 400 Bad Request response let h = spawn(move || { - config.client.read_only_contract_call_with_retry( + config.client.read_only_contract_call( &config.client.stacks_address, - &ContractName::try_from("contract-name").unwrap(), - &ClarityName::try_from("function-name").unwrap(), + 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, + dbg!(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/utils.rs b/stacks-signer/src/utils.rs index 86436f09af8..3c1e1b36cd0 100644 --- a/stacks-signer/src/utils.rs +++ b/stacks-signer/src/utils.rs @@ -14,8 +14,7 @@ pub fn build_signer_config_tomls( signer_stacks_private_keys: &[StacksPrivateKey], num_keys: u32, node_host: &str, - stackerdb_contract_id: &str, - pox_contract_id: Option<&str>, + contract_id: &str, timeout: Option, ) -> Vec { let num_signers = signer_stacks_private_keys.len() as u32; @@ -74,7 +73,7 @@ stacks_private_key = "{stacks_private_key}" node_host = "{node_host}" endpoint = "{endpoint}" network = "testnet" -stackerdb_contract_id = "{stackerdb_contract_id}" +stackerdb_contract_id = "{contract_id}" signer_id = {id} {signers_array} "# @@ -89,18 +88,8 @@ 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); } - signer_config_tomls } diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 6c9bc48886b..788474085bc 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -55,16 +55,11 @@ fn spawn_signer( signer.spawn(endpoint).unwrap() } -#[allow(clippy::too_many_arguments)] fn setup_stx_btc_node( conf: &mut NeonConfig, num_signers: u32, signer_stacks_private_keys: &[StacksPrivateKey], - publisher_private_key: &StacksPrivateKey, stackerdb_contract: &str, - stackerdb_contract_id: &QualifiedContractIdentifier, - pox_contract: &str, - pox_contract_id: &QualifiedContractIdentifier, signer_config_tomls: &Vec, ) -> RunningNodes { for toml in signer_config_tomls { @@ -77,12 +72,6 @@ fn setup_stx_btc_node( } let mut initial_balances = Vec::new(); - - initial_balances.push(InitialBalance { - address: to_addr(publisher_private_key).into(), - amount: 10_000_000_000_000, - }); - for i in 0..num_signers { initial_balances.push(InitialBalance { address: to_addr(&signer_stacks_private_keys[i as usize]).into(), @@ -91,7 +80,10 @@ 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(QualifiedContractIdentifier::new( + to_addr(&signer_stacks_private_keys[0]).into(), + "hello-world".into(), + )); info!("Make new BitcoinCoreController"); let mut btcd_controller = BitcoinCoreController::new(conf.clone()); @@ -130,30 +122,18 @@ fn setup_stx_btc_node( 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 contract-publish..."); let tx = make_contract_publish( - publisher_private_key, + &signer_stacks_private_keys[0], 0, 10_000, - &pox_contract_id.name, - pox_contract, - ); - submit_tx(&http_origin, &tx); - - info!("Send stacker-db contract-publish..."); - let tx = make_contract_publish( - publisher_private_key, - 1, - 10_000, - &stackerdb_contract_id.name, + "hello-world", stackerdb_contract, ); submit_tx(&http_origin, &tx); // mine it - info!("Mining the pox and stackerdb contract..."); + info!("Mine it..."); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); @@ -165,28 +145,6 @@ fn setup_stx_btc_node( } } -/// 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] fn test_stackerdb_dkg() { if env::var("BITCOIND_TEST") != Ok("1".into()) { @@ -201,7 +159,6 @@ fn test_stackerdb_dkg() { // Generate Signer Data let num_signers: u32 = 16; let num_keys: u32 = 40; - let publisher_private_key = StacksPrivateKey::new(); let signer_stacks_private_keys = (0..num_signers) .map(|_| StacksPrivateKey::new()) .collect::>(); @@ -213,24 +170,17 @@ fn test_stackerdb_dkg() { // 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 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(), - ); + let contract_id = + QualifiedContractIdentifier::new(signer_stacks_addresses[0].into(), "hello-world".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(), - Some(&pox_contract_id.to_string()), + &contract_id.to_string(), Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. ); @@ -265,16 +215,15 @@ fn test_stackerdb_dkg() { &mut conf, num_signers, &signer_stacks_private_keys, - &publisher_private_key, &stackerdb_contract, - &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::Dkg) + .expect("failed to send DKG command"); coordinator_cmd_send .send(RunLoopCommand::Sign { message: vec![1, 2, 3, 4, 5],