mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-01-12 22:43:42 +08:00
Merge branch 'develop' into feat/http-rpc-refactor
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -391,6 +391,17 @@ 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"
|
||||
@@ -3440,6 +3451,7 @@ dependencies = [
|
||||
name = "stacks-signer"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"backoff",
|
||||
"bincode",
|
||||
"clap 4.4.1",
|
||||
"clarity",
|
||||
|
||||
@@ -20,6 +20,7 @@ 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"] }
|
||||
|
||||
@@ -101,6 +101,8 @@ 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<QualifiedContractIdentifier>,
|
||||
/// The Scalar representation of the private key for signer communication
|
||||
pub message_private_key: Scalar,
|
||||
/// The signer's Stacks private key
|
||||
@@ -133,8 +135,11 @@ struct RawConfigFile {
|
||||
pub node_host: String,
|
||||
/// endpoint to stackerdb receiver
|
||||
pub endpoint: String,
|
||||
/// contract identifier
|
||||
// FIXME: these contract's should go away in non testing scenarios. Make them both optionals.
|
||||
/// Stacker db contract identifier
|
||||
pub stackerdb_contract_id: String,
|
||||
/// pox contract identifier
|
||||
pub pox_contract_id: Option<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
|
||||
@@ -214,6 +219,17 @@ impl TryFrom<RawConfigFile> 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(
|
||||
@@ -265,6 +281,7 @@ impl TryFrom<RawConfigFile> for Config {
|
||||
node_host,
|
||||
endpoint,
|
||||
stackerdb_contract_id,
|
||||
pox_contract_id,
|
||||
message_private_key,
|
||||
stacks_private_key,
|
||||
stacks_address,
|
||||
|
||||
@@ -269,6 +269,7 @@ 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());
|
||||
|
||||
@@ -15,7 +15,7 @@ use wsts::state_machine::{OperationResult, PublicKeys};
|
||||
use wsts::v2;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::stacks_client::StacksClient;
|
||||
use crate::stacks_client::{retry_with_exponential_backoff, ClientError, StacksClient};
|
||||
|
||||
/// Which operation to perform
|
||||
#[derive(PartialEq, Clone)]
|
||||
@@ -36,6 +36,9 @@ 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
|
||||
@@ -48,7 +51,7 @@ pub enum State {
|
||||
pub struct RunLoop<C> {
|
||||
/// 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
|
||||
@@ -63,7 +66,27 @@ pub struct RunLoop<C> {
|
||||
}
|
||||
|
||||
impl<C: Coordinatable> RunLoop<C> {
|
||||
/// Helper function to actually execute the command and update state accordingly
|
||||
/// 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
|
||||
/// Returns true when it is successfully executed, else false
|
||||
fn execute_command(&mut self, command: &RunLoopCommand) -> bool {
|
||||
match command {
|
||||
@@ -73,7 +96,7 @@ impl<C: Coordinatable> RunLoop<C> {
|
||||
Ok(msg) => {
|
||||
let ack = self
|
||||
.stacks_client
|
||||
.send_message(self.signing_round.signer_id, msg);
|
||||
.send_message_with_retry(self.signing_round.signer_id, msg);
|
||||
debug!("ACK: {:?}", ack);
|
||||
self.state = State::Dkg;
|
||||
true
|
||||
@@ -99,7 +122,7 @@ impl<C: Coordinatable> RunLoop<C> {
|
||||
Ok(msg) => {
|
||||
let ack = self
|
||||
.stacks_client
|
||||
.send_message(self.signing_round.signer_id, msg);
|
||||
.send_message_with_retry(self.signing_round.signer_id, msg);
|
||||
debug!("ACK: {:?}", ack);
|
||||
self.state = State::Sign;
|
||||
true
|
||||
@@ -115,9 +138,14 @@ impl<C: Coordinatable> RunLoop<C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to check the current state, process the next command in the queue, and update state accordingly
|
||||
/// Attempt to 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) {
|
||||
@@ -205,26 +233,29 @@ impl From<&Config> for RunLoop<FrostCoordinator<v2::Aggregator>> {
|
||||
.iter()
|
||||
.map(|i| i - 1) // SigningRound::new (unlike SigningRound::from) doesn't do this
|
||||
.collect::<Vec<u32>>();
|
||||
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: 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),
|
||||
coordinator,
|
||||
signing_round,
|
||||
stacks_client,
|
||||
commands: VecDeque::new(),
|
||||
state: State::Idle,
|
||||
state: State::Uninitialized,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,10 +275,19 @@ impl<C: Coordinatable> SignerRunLoop<Vec<OperationResult>, RunLoopCommand> for R
|
||||
cmd: Option<RunLoopCommand>,
|
||||
res: Sender<Vec<OperationResult>>,
|
||||
) -> Option<Vec<OperationResult>> {
|
||||
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);
|
||||
}
|
||||
// First process any arrived events
|
||||
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
|
||||
if let Some(event) = event {
|
||||
let (outbound_messages, operation_results) = self.process_event(&event);
|
||||
debug!(
|
||||
@@ -257,7 +297,7 @@ impl<C: Coordinatable> SignerRunLoop<Vec<OperationResult>, RunLoopCommand> for R
|
||||
for msg in outbound_messages {
|
||||
let ack = self
|
||||
.stacks_client
|
||||
.send_message(self.signing_round.signer_id, msg);
|
||||
.send_message_with_retry(self.signing_round.signer_id, msg);
|
||||
if let Ok(ack) = ack {
|
||||
debug!("ACK: {:?}", ack);
|
||||
} else {
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use bincode::Error as BincodeError;
|
||||
use blockstack_lib::chainstate::stacks::{
|
||||
StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth,
|
||||
TransactionContractCall, TransactionPayload, TransactionPostConditionMode,
|
||||
TransactionSpendingCondition, TransactionVersion,
|
||||
use blockstack_lib::{
|
||||
burnchains::Txid,
|
||||
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 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::{codec, debug, warn};
|
||||
use wsts::net::{Message, Packet};
|
||||
use stacks_common::{
|
||||
codec::StacksMessageCodec,
|
||||
debug,
|
||||
types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey},
|
||||
warn,
|
||||
};
|
||||
use wsts::{
|
||||
net::{Message, Packet},
|
||||
Point, Scalar,
|
||||
};
|
||||
|
||||
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
|
||||
@@ -27,7 +46,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}")]
|
||||
Serialize(#[from] BincodeError),
|
||||
StackerDBSerializationError(#[from] BincodeError),
|
||||
/// Failed to sign stacker-db chunk
|
||||
#[error("Failed to sign stacker-db chunk: {0}")]
|
||||
FailToSign(#[from] StackerDBError),
|
||||
@@ -46,27 +65,24 @@ pub enum ClientError {
|
||||
/// Reqwest specific error occurred
|
||||
#[error("{0}")]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
/// 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),
|
||||
/// 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
|
||||
@@ -79,7 +95,7 @@ pub struct StacksClient {
|
||||
stacks_private_key: StacksPrivateKey,
|
||||
/// A map of a slot ID to last chunk version
|
||||
slot_versions: HashMap<u32, u32>,
|
||||
/// The RPC endpoint used to communicate HTTP endpoints with
|
||||
/// The stacks node HTTP base endpoint
|
||||
http_origin: String,
|
||||
/// The types of transactions
|
||||
tx_version: TransactionVersion,
|
||||
@@ -87,6 +103,8 @@ 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<QualifiedContractIdentifier>,
|
||||
}
|
||||
|
||||
impl From<&Config> for StacksClient {
|
||||
@@ -103,13 +121,14 @@ 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
|
||||
pub fn send_message(
|
||||
/// Sends messages to the stacker-db with an exponential backoff retry
|
||||
pub fn send_message_with_retry(
|
||||
&mut self,
|
||||
id: u32,
|
||||
message: Packet,
|
||||
@@ -122,7 +141,12 @@ 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 chunk_ack = self.stackerdb_session.put_chunk(chunk)?;
|
||||
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 {
|
||||
@@ -144,6 +168,23 @@ impl StacksClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the current DKG aggregate public key
|
||||
pub fn get_aggregate_public_key(&self) -> Result<Option<Point>, 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 {
|
||||
@@ -152,143 +193,194 @@ impl StacksClient {
|
||||
SLOTS_PER_USER
|
||||
}
|
||||
|
||||
fn serialize_sign_sig_tx_anchor_mode_version(
|
||||
&self,
|
||||
payload: TransactionPayload,
|
||||
sender_nonce: u64,
|
||||
tx_fee: u64,
|
||||
anchor_mode: TransactionAnchorMode,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
self.seralize_sign_sponsored_tx_anchor_mode_version(
|
||||
payload,
|
||||
None,
|
||||
sender_nonce,
|
||||
None,
|
||||
tx_fee,
|
||||
anchor_mode,
|
||||
)
|
||||
/// Helper function to retrieve the current reward cycle number from the stacks node
|
||||
fn get_current_reward_cycle(&self) -> Result<u64, ClientError> {
|
||||
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::<serde_json::Value>()?;
|
||||
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)))
|
||||
}
|
||||
|
||||
fn seralize_sign_sponsored_tx_anchor_mode_version(
|
||||
&self,
|
||||
payload: TransactionPayload,
|
||||
payer: Option<&StacksPrivateKey>,
|
||||
sender_nonce: u64,
|
||||
payer_nonce: Option<u64>,
|
||||
tx_fee: u64,
|
||||
anchor_mode: TransactionAnchorMode,
|
||||
) -> Result<Vec<u8>, 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);
|
||||
/// 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<u64, ClientError> {
|
||||
//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");
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
/// 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 mut unsigned_tx = StacksTransaction::new(self.tx_version, auth, payload);
|
||||
unsigned_tx.anchor_mode = anchor_mode;
|
||||
let response = retry_with_exponential_backoff(send_request)?;
|
||||
if !response.status().is_success() {
|
||||
return Err(ClientError::RequestFailure(response.status()));
|
||||
}
|
||||
let json_response = response.json::<serde_json::Value>()?;
|
||||
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<Option<Point>, 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<Txid, ClientError> {
|
||||
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<StacksTransaction, ClientError> {
|
||||
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(|_| ClientError::SignatureGenerationFailure)?;
|
||||
if let (Some(payer), Some(_)) = (payer, payer_nonce) {
|
||||
tx_signer
|
||||
.sign_sponsor(payer)
|
||||
.map_err(|_| ClientError::SponsorSignatureGenerationFailure)?;
|
||||
}
|
||||
.map_err(|e| ClientError::TransactionGenerationFailure(e.to_string()))?;
|
||||
|
||||
let Some(tx) = tx_signer.get_tx() else {
|
||||
return Err(ClientError::SignatureGenerationFailure);
|
||||
};
|
||||
|
||||
Ok(tx.serialize_to_vec())
|
||||
tx_signer
|
||||
.get_tx()
|
||||
.ok_or(ClientError::TransactionGenerationFailure(
|
||||
"Failed to generate transaction from a transaction signer".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// 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<Vec<u8>, ClientError> {
|
||||
let payload = TransactionContractCall {
|
||||
address: *contract_addr,
|
||||
contract_name,
|
||||
function_name,
|
||||
function_args: function_args.to_vec(),
|
||||
/// Helper function to submit a transaction to the Stacks node
|
||||
fn submit_tx(&self, tx: &StacksTransaction) -> Result<Txid, ClientError> {
|
||||
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 tx_fee = 0;
|
||||
|
||||
self.serialize_sign_sig_tx_anchor_mode_version(
|
||||
payload.into(),
|
||||
nonce,
|
||||
tx_fee,
|
||||
TransactionAnchorMode::OnChainOnly,
|
||||
)
|
||||
}
|
||||
|
||||
/// Submits a transaction to the Stacks node
|
||||
pub fn submit_tx(&self, tx: Vec<u8>) -> Result<String, ClientError> {
|
||||
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)
|
||||
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(
|
||||
pub fn read_only_contract_call_with_retry(
|
||||
&self,
|
||||
contract_addr: &StacksAddress,
|
||||
contract_name: ContractName,
|
||||
function_name: ClarityName,
|
||||
contract_name: &ContractName,
|
||||
function_name: &ClarityName,
|
||||
function_args: &[ClarityValue],
|
||||
) -> Result<String, ClientError> {
|
||||
debug!("Calling read-only function {}...", function_name);
|
||||
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()?;
|
||||
let args = function_args
|
||||
.iter()
|
||||
.map(|arg| arg.serialize_to_hex())
|
||||
.collect::<Vec<String>>();
|
||||
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()));
|
||||
}
|
||||
@@ -314,6 +406,46 @@ 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<F, E, T>(request_fn: F) -> Result<T, ClientError>
|
||||
where
|
||||
F: FnMut() -> Result<T, backoff::Error<E>>,
|
||||
{
|
||||
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
|
||||
@@ -334,9 +466,11 @@ fn slot_id(id: u32, message: &Message) -> u32 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{SocketAddr, TcpListener};
|
||||
use std::thread::spawn;
|
||||
use std::{
|
||||
io::{BufWriter, Read, Write},
|
||||
net::{SocketAddr, TcpListener},
|
||||
thread::spawn,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -380,10 +514,10 @@ mod tests {
|
||||
fn read_only_contract_call_200_success() {
|
||||
let config = TestConfig::new();
|
||||
let h = spawn(move || {
|
||||
config.client.read_only_contract_call(
|
||||
config.client.read_only_contract_call_with_retry(
|
||||
&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(),
|
||||
&[],
|
||||
)
|
||||
});
|
||||
@@ -395,14 +529,33 @@ 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(
|
||||
config.client.read_only_contract_call_with_retry(
|
||||
&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(),
|
||||
&[],
|
||||
)
|
||||
});
|
||||
@@ -419,17 +572,17 @@ mod tests {
|
||||
let config = TestConfig::new();
|
||||
// Simulate a 400 Bad Request response
|
||||
let h = spawn(move || {
|
||||
config.client.read_only_contract_call(
|
||||
config.client.read_only_contract_call_with_retry(
|
||||
&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!(
|
||||
dbg!(result),
|
||||
result,
|
||||
Err(ClientError::RequestFailure(
|
||||
reqwest::StatusCode::BAD_REQUEST
|
||||
))
|
||||
@@ -441,18 +594,170 @@ mod tests {
|
||||
let config = TestConfig::new();
|
||||
// Simulate a 400 Bad Request response
|
||||
let h = spawn(move || {
|
||||
config.client.read_only_contract_call(
|
||||
config.client.read_only_contract_call_with_retry(
|
||||
&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!(
|
||||
dbg!(result),
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ pub fn build_signer_config_tomls(
|
||||
signer_stacks_private_keys: &[StacksPrivateKey],
|
||||
num_keys: u32,
|
||||
node_host: &str,
|
||||
contract_id: &str,
|
||||
stackerdb_contract_id: &str,
|
||||
pox_contract_id: Option<&str>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Vec<String> {
|
||||
let num_signers = signer_stacks_private_keys.len() as u32;
|
||||
@@ -73,7 +74,7 @@ stacks_private_key = "{stacks_private_key}"
|
||||
node_host = "{node_host}"
|
||||
endpoint = "{endpoint}"
|
||||
network = "testnet"
|
||||
stackerdb_contract_id = "{contract_id}"
|
||||
stackerdb_contract_id = "{stackerdb_contract_id}"
|
||||
signer_id = {id}
|
||||
{signers_array}
|
||||
"#
|
||||
@@ -88,8 +89,18 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -55,11 +55,16 @@ 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<String>,
|
||||
) -> RunningNodes {
|
||||
for toml in signer_config_tomls {
|
||||
@@ -72,6 +77,12 @@ 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(),
|
||||
@@ -80,10 +91,7 @@ fn setup_stx_btc_node(
|
||||
}
|
||||
|
||||
conf.initial_balances.append(&mut initial_balances);
|
||||
conf.node.stacker_dbs.push(QualifiedContractIdentifier::new(
|
||||
to_addr(&signer_stacks_private_keys[0]).into(),
|
||||
"hello-world".into(),
|
||||
));
|
||||
conf.node.stacker_dbs.push(stackerdb_contract_id.clone());
|
||||
|
||||
info!("Make new BitcoinCoreController");
|
||||
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
|
||||
@@ -122,18 +130,30 @@ 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 contract-publish...");
|
||||
|
||||
info!("Send pox contract-publish...");
|
||||
|
||||
let tx = make_contract_publish(
|
||||
&signer_stacks_private_keys[0],
|
||||
publisher_private_key,
|
||||
0,
|
||||
10_000,
|
||||
"hello-world",
|
||||
&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,
|
||||
stackerdb_contract,
|
||||
);
|
||||
submit_tx(&http_origin, &tx);
|
||||
|
||||
// mine it
|
||||
info!("Mine it...");
|
||||
info!("Mining the pox and stackerdb contract...");
|
||||
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
|
||||
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
|
||||
|
||||
@@ -145,6 +165,28 @@ 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()) {
|
||||
@@ -159,6 +201,7 @@ fn test_stackerdb_dkg() {
|
||||
// Generate Signer Data
|
||||
let num_signers: u32 = 100;
|
||||
let num_keys: u32 = 4000;
|
||||
let publisher_private_key = StacksPrivateKey::new();
|
||||
let signer_stacks_private_keys = (0..num_signers)
|
||||
.map(|_| StacksPrivateKey::new())
|
||||
.collect::<Vec<StacksPrivateKey>>();
|
||||
@@ -170,17 +213,24 @@ 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 contract_id =
|
||||
QualifiedContractIdentifier::new(signer_stacks_addresses[0].into(), "hello-world".into());
|
||||
let stacker_db_contract_id = QualifiedContractIdentifier::new(
|
||||
to_addr(&publisher_private_key).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,
|
||||
&contract_id.to_string(),
|
||||
&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.
|
||||
);
|
||||
|
||||
@@ -215,15 +265,16 @@ 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],
|
||||
|
||||
Reference in New Issue
Block a user