diff --git a/protobufs/stacks/signer/v1/requests.proto b/protobufs/stacks/signer/v1/requests.proto index 4b3b1e06..f485ce9b 100644 --- a/protobufs/stacks/signer/v1/requests.proto +++ b/protobufs/stacks/signer/v1/requests.proto @@ -26,16 +26,18 @@ message StacksTransactionSignRequest { // essentially a hash of the contract call struct, the nonce, the tx_fee // and a few other things. crypto.Uint256 digest = 4; + // The transaction ID of the associated contract call transaction. + crypto.Uint256 txid = 5; // The contract call transaction to sign. oneof contract_call { // The `complete-deposit` contract call - CompleteDeposit complete_deposit = 5; + CompleteDeposit complete_deposit = 6; // The `accept-withdrawal-request` contract call - AcceptWithdrawal accept_withdrawal = 6; + AcceptWithdrawal accept_withdrawal = 7; // The `reject-withdrawal-request` contract call - RejectWithdrawal reject_withdrawal = 7; + RejectWithdrawal reject_withdrawal = 8; // The `rotate-keys-wrapper` contract call - RotateKeys rotate_keys = 8; + RotateKeys rotate_keys = 9; } } diff --git a/signer/src/context/mod.rs b/signer/src/context/mod.rs index 7c4a5071..d7fc1583 100644 --- a/signer/src/context/mod.rs +++ b/signer/src/context/mod.rs @@ -30,15 +30,15 @@ pub trait Context: Clone + Sync + Send { /// Returns a handle to the application's termination signal. fn get_termination_handle(&self) -> TerminationHandle; /// Get a read-only handle to the signer storage. - fn get_storage(&self) -> impl DbRead + Clone + Sync + Send; + fn get_storage(&self) -> impl DbRead + Clone + Sync + Send + 'static; /// Get a read-write handle to the signer storage. - fn get_storage_mut(&self) -> impl DbRead + DbWrite + Clone + Sync + Send; + fn get_storage_mut(&self) -> impl DbRead + DbWrite + Clone + Sync + Send + 'static; /// Get a handle to a Bitcoin client. - fn get_bitcoin_client(&self) -> impl BitcoinInteract + Clone; + fn get_bitcoin_client(&self) -> impl BitcoinInteract + Clone + 'static; /// Get a handler to the Stacks client. - fn get_stacks_client(&self) -> impl StacksInteract + Clone; + fn get_stacks_client(&self) -> impl StacksInteract + Clone + 'static; /// Get a handle to a Emily client. - fn get_emily_client(&self) -> impl EmilyInteract + Clone; + fn get_emily_client(&self) -> impl EmilyInteract + Clone + 'static; } /// Signer context which is passed to different components within the @@ -71,7 +71,7 @@ pub struct SignerContext { impl SignerContext where - S: DbRead + DbWrite + Clone + Sync + Send, + S: DbRead + DbWrite + Clone + Sync + Send + 'static, BC: for<'a> TryFrom<&'a [Url]> + BitcoinInteract + Clone + 'static, ST: for<'a> TryFrom<&'a Settings> + StacksInteract + Clone + Sync + Send + 'static, EM: for<'a> TryFrom<&'a [Url]> + EmilyInteract + Clone + Sync + Send + 'static, @@ -125,10 +125,10 @@ where impl Context for SignerContext where - S: DbRead + DbWrite + Clone + Sync + Send, - BC: BitcoinInteract + Clone, - ST: StacksInteract + Clone + Sync + Send, - EM: EmilyInteract + Clone + Sync + Send, + S: DbRead + DbWrite + Clone + Sync + Send + 'static, + BC: BitcoinInteract + Clone + 'static, + ST: StacksInteract + Clone + Sync + Send + 'static, + EM: EmilyInteract + Clone + Sync + Send + 'static, { fn config(&self) -> &Settings { &self.config @@ -160,23 +160,23 @@ where TerminationHandle::new(self.term_tx.clone(), self.term_tx.subscribe()) } - fn get_storage(&self) -> impl DbRead + Clone + Sync + Send { + fn get_storage(&self) -> impl DbRead + Clone + Sync + Send + 'static { self.storage.clone() } - fn get_storage_mut(&self) -> impl DbRead + DbWrite + Clone + Sync + Send { + fn get_storage_mut(&self) -> impl DbRead + DbWrite + Clone + Sync + Send + 'static { self.storage.clone() } - fn get_bitcoin_client(&self) -> impl BitcoinInteract + Clone { + fn get_bitcoin_client(&self) -> impl BitcoinInteract + Clone + 'static { self.bitcoin_client.clone() } - fn get_stacks_client(&self) -> impl StacksInteract + Clone { + fn get_stacks_client(&self) -> impl StacksInteract + Clone + 'static { self.stacks_client.clone() } - fn get_emily_client(&self) -> impl EmilyInteract + Clone { + fn get_emily_client(&self) -> impl EmilyInteract + Clone + 'static { self.emily_client.clone() } } diff --git a/signer/src/error.rs b/signer/src/error.rs index 6e4bd368..9f6831f8 100644 --- a/signer/src/error.rs +++ b/signer/src/error.rs @@ -259,6 +259,11 @@ pub enum Error { #[error("failed to read migration script: {0}")] ReadSqlMigration(Cow<'static, str>), + /// An error when we exceeded the timeout when trying to sign a stacks + /// transaction. + #[error("took too long to receive enough signatures for transaction: {0}")] + SignatureTimeout(blockstack_lib::burnchains::Txid), + /// An error when attempting to generically decode bytes using the /// trait implementation. #[error("got an error wen attempting to call StacksMessageCodec::consensus_deserialize {0}")] @@ -281,6 +286,10 @@ pub enum Error { #[error("failed to make a request to the stacks Node: {0}")] StacksNodeRequest(#[source] reqwest::Error), + /// We failed to submit the transaction to the mempool. + #[error("{0}")] + StacksTxRejection(#[from] crate::stacks::api::TxRejection), + /// Reqwest error #[error("response from stacks node did not conform to the expected schema: {0}")] UnexpectedStacksResponse(#[source] reqwest::Error), diff --git a/signer/src/message.rs b/signer/src/message.rs index 158a70b6..9714f9f4 100644 --- a/signer/src/message.rs +++ b/signer/src/message.rs @@ -165,6 +165,8 @@ pub struct StacksTransactionSignRequest { /// It's essentially a hash of the contract call struct, the nonce, the /// tx_fee and a few other things. pub digest: [u8; 32], + /// The transaction ID of the associated contract call transaction. + pub txid: blockstack_lib::burnchains::Txid, } /// Represents a signature of a Stacks transaction. diff --git a/signer/src/proto/generated/stacks.signer.v1.rs b/signer/src/proto/generated/stacks.signer.v1.rs index f27fa6db..cf934e12 100644 --- a/signer/src/proto/generated/stacks.signer.v1.rs +++ b/signer/src/proto/generated/stacks.signer.v1.rs @@ -83,10 +83,13 @@ pub struct StacksTransactionSignRequest { /// and a few other things. #[prost(message, optional, tag = "4")] pub digest: ::core::option::Option, + /// The transaction ID of the associated contract call transaction. + #[prost(message, optional, tag = "5")] + pub txid: ::core::option::Option, /// The contract call transaction to sign. #[prost( oneof = "stacks_transaction_sign_request::ContractCall", - tags = "5, 6, 7, 8" + tags = "6, 7, 8, 9" )] pub contract_call: ::core::option::Option< stacks_transaction_sign_request::ContractCall, @@ -99,16 +102,16 @@ pub mod stacks_transaction_sign_request { #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum ContractCall { /// The `complete-deposit` contract call - #[prost(message, tag = "5")] + #[prost(message, tag = "6")] CompleteDeposit(super::CompleteDeposit), /// The `accept-withdrawal-request` contract call - #[prost(message, tag = "6")] + #[prost(message, tag = "7")] AcceptWithdrawal(super::AcceptWithdrawal), /// The `reject-withdrawal-request` contract call - #[prost(message, tag = "7")] + #[prost(message, tag = "8")] RejectWithdrawal(super::RejectWithdrawal), /// The `rotate-keys-wrapper` contract call - #[prost(message, tag = "8")] + #[prost(message, tag = "9")] RotateKeys(super::RotateKeys), } } diff --git a/signer/src/stacks/api.rs b/signer/src/stacks/api.rs index 13fc8cbe..4473fc88 100644 --- a/signer/src/stacks/api.rs +++ b/signer/src/stacks/api.rs @@ -172,7 +172,8 @@ impl GetNakamotoStartHeight for RPCPoxInfoData { /// The official documentation specifies what to expect when there is a /// rejection, and that documentation can be found here: /// https://github.com/stacks-network/stacks-core/blob/2.5.0.0.5/docs/rpc-endpoints.md -#[derive(Debug, serde::Deserialize)] +#[derive(Debug, Clone, Copy, serde::Deserialize, strum::IntoStaticStr)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] #[cfg_attr(feature = "testing", derive(serde::Serialize))] pub enum RejectionReason { /// From MemPoolRejection::SerializationFailure @@ -246,6 +247,15 @@ pub struct TxRejection { pub txid: Txid, } +impl std::fmt::Display for TxRejection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let reason_str: &'static str = self.reason.into(); + write!(f, "transaction rejected from stacks mempool: {reason_str}") + } +} + +impl std::error::Error for TxRejection {} + /// The response from a POST /v2/transactions request /// /// The stacks node returns three types of responses, either: diff --git a/signer/src/storage/model.rs b/signer/src/storage/model.rs index 73ada7b8..d9189cfc 100644 --- a/signer/src/storage/model.rs +++ b/signer/src/storage/model.rs @@ -269,6 +269,16 @@ pub struct SweptDepositRequest { pub amount: u64, } +impl SweptDepositRequest { + /// The OutPoint of the actual deposit + pub fn deposit_outpoint(&self) -> bitcoin::OutPoint { + bitcoin::OutPoint { + txid: self.txid.into(), + vout: self.output_index, + } + } +} + /// Withdraw request. #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, sqlx::FromRow)] #[cfg_attr(feature = "testing", derive(fake::Dummy))] @@ -610,6 +620,12 @@ impl From<[u8; 32]> for StacksBlockHash { #[serde(transparent)] pub struct StacksTxId(blockstack_lib::burnchains::Txid); +impl std::fmt::Display for StacksTxId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + impl Deref for StacksTxId { type Target = blockstack_lib::burnchains::Txid; fn deref(&self) -> &Self::Target { diff --git a/signer/src/testing/context.rs b/signer/src/testing/context.rs index 0101a17f..ff88ea59 100644 --- a/signer/src/testing/context.rs +++ b/signer/src/testing/context.rs @@ -162,10 +162,10 @@ impl impl Context for TestContext where - Storage: DbRead + DbWrite + Clone + Sync + Send, - Bitcoin: BitcoinInteract + Clone + Send + Sync, - Stacks: StacksInteract + Clone + Send + Sync, - Emily: EmilyInteract + Clone + Send + Sync, + Storage: DbRead + DbWrite + Clone + Sync + Send + 'static, + Bitcoin: BitcoinInteract + Clone + Send + Sync + 'static, + Stacks: StacksInteract + Clone + Send + Sync + 'static, + Emily: EmilyInteract + Clone + Send + Sync + 'static, { fn config(&self) -> &Settings { self.inner.config() @@ -189,25 +189,25 @@ where self.inner.get_termination_handle() } - fn get_storage(&self) -> impl crate::storage::DbRead + Clone + Sync + Send { + fn get_storage(&self) -> impl crate::storage::DbRead + Clone + Sync + Send + 'static { self.inner.get_storage() } fn get_storage_mut( &self, - ) -> impl crate::storage::DbRead + crate::storage::DbWrite + Clone + Sync + Send { + ) -> impl crate::storage::DbRead + crate::storage::DbWrite + Clone + Sync + Send + 'static { self.inner.get_storage_mut() } - fn get_bitcoin_client(&self) -> impl BitcoinInteract + Clone { + fn get_bitcoin_client(&self) -> impl BitcoinInteract + Clone + 'static { self.inner.get_bitcoin_client() } - fn get_stacks_client(&self) -> impl StacksInteract + Clone { + fn get_stacks_client(&self) -> impl StacksInteract + Clone + 'static { self.inner.get_stacks_client() } - fn get_emily_client(&self) -> impl EmilyInteract + Clone { + fn get_emily_client(&self) -> impl EmilyInteract + Clone + 'static { self.inner.get_emily_client() } } diff --git a/signer/src/testing/message.rs b/signer/src/testing/message.rs index 00dcdeb8..c6fdad30 100644 --- a/signer/src/testing/message.rs +++ b/signer/src/testing/message.rs @@ -11,6 +11,7 @@ use crate::message; use crate::stacks::contracts::ContractCall; use crate::stacks::contracts::RejectWithdrawalV1; use crate::storage::model::BitcoinBlockHash; +use crate::storage::model::StacksTxId; use crate::testing::dummy; impl message::SignerMessage { @@ -105,6 +106,7 @@ impl fake::Dummy for message::StacksTransactionSignRequest { nonce: 1, aggregate_key: PublicKey::from_private_key(&private_key), digest: config.fake_with_rng(rng), + txid: config.fake_with_rng::(rng).into(), } } } diff --git a/signer/src/testing/transaction_coordinator.rs b/signer/src/testing/transaction_coordinator.rs index 4502a5fc..700b41cd 100644 --- a/signer/src/testing/transaction_coordinator.rs +++ b/signer/src/testing/transaction_coordinator.rs @@ -39,13 +39,13 @@ const EMPTY_BITCOIN_TX: bitcoin::Transaction = bitcoin::Transaction { output: vec![], }; -struct EventLoopHarness { +struct TxCoordinatorEventLoopHarness { event_loop: EventLoop, context: C, is_started: Arc, } -impl EventLoopHarness +impl TxCoordinatorEventLoopHarness where C: Context + 'static, { @@ -208,7 +208,7 @@ where let private_key = Self::select_coordinator(&bitcoin_chain_tip.block_hash, &signer_info); // Bootstrap the tx coordinator within an event loop harness. - let event_loop_harness = EventLoopHarness::create( + let event_loop_harness = TxCoordinatorEventLoopHarness::create( self.context.clone(), network.connect(), self.context_window, @@ -697,17 +697,17 @@ where let (aggregate_key, all_dkg_shares) = signer_set.run_dkg(bitcoin_chain_tip, dkg_txid, rng).await; + let encrypted_dkg_shares = all_dkg_shares.first().unwrap(); + signer_set .write_as_rotate_keys_tx( &self.context.get_storage_mut(), &bitcoin_chain_tip, - all_dkg_shares.first().unwrap(), + encrypted_dkg_shares, rng, ) .await; - let encrypted_dkg_shares = all_dkg_shares.first().unwrap(); - storage .write_encrypted_dkg_shares(encrypted_dkg_shares) .await diff --git a/signer/src/testing/transaction_signer.rs b/signer/src/testing/transaction_signer.rs index 79139585..2da02b03 100644 --- a/signer/src/testing/transaction_signer.rs +++ b/signer/src/testing/transaction_signer.rs @@ -36,12 +36,12 @@ use tokio::time::error::Elapsed; use super::context::*; -struct EventLoopHarness { +struct TxSignerEventLoopHarness { context: Context, event_loop: EventLoop, } -impl EventLoopHarness +impl TxSignerEventLoopHarness where Ctx: Context + 'static, Rng: rand::RngCore + rand::CryptoRng + Send + Sync + 'static, @@ -161,7 +161,7 @@ where let mut network_rx = network.connect(); let mut signal_rx = self.context.get_signal_receiver(); - let event_loop_harness = EventLoopHarness::create( + let event_loop_harness = TxSignerEventLoopHarness::create( self.context.clone(), network.connect(), self.context_window, @@ -229,7 +229,7 @@ where let mut network_rx = network.connect(); let mut signal_rx = self.context.get_signal_receiver(); - let event_loop_harness = EventLoopHarness::create( + let event_loop_harness = TxSignerEventLoopHarness::create( self.context.clone(), network.connect(), self.context_window, @@ -316,7 +316,7 @@ where let mut event_loop_handles: Vec<_> = signer_info .into_iter() .map(|signer_info| { - let event_loop_harness = EventLoopHarness::create( + let event_loop_harness = TxSignerEventLoopHarness::create( build_context(), network.connect(), self.context_window, @@ -388,7 +388,7 @@ where let signer_info = testing::wsts::generate_signer_info(&mut rng, self.num_signers); let coordinator_signer_info = &signer_info.first().cloned().unwrap(); - let event_loop_harness = EventLoopHarness::create( + let event_loop_harness = TxSignerEventLoopHarness::create( self.context.clone(), network.connect(), self.context_window, @@ -503,7 +503,7 @@ where .clone() .into_iter() .map(|signer_info| { - let event_loop_harness = EventLoopHarness::create( + let event_loop_harness = TxSignerEventLoopHarness::create( build_context(), // NEED TO HAVE A NEW CONTEXT FOR EACH SIGNER network.connect(), self.context_window, @@ -584,7 +584,7 @@ where .clone() .into_iter() .map(|signer_info| { - let event_loop_harness = EventLoopHarness::create( + let event_loop_harness = TxSignerEventLoopHarness::create( build_context(), network.connect(), self.context_window, diff --git a/signer/src/transaction_coordinator.rs b/signer/src/transaction_coordinator.rs index de06f18f..dc3d72fb 100644 --- a/signer/src/transaction_coordinator.rs +++ b/signer/src/transaction_coordinator.rs @@ -7,8 +7,7 @@ use std::collections::BTreeSet; -use futures::StreamExt; -use futures::TryStreamExt; +use blockstack_lib::chainstate::stacks::StacksTransaction; use sha2::Digest; use crate::bitcoin::utxo; @@ -19,16 +18,19 @@ use crate::error::Error; use crate::keys::PrivateKey; use crate::keys::PublicKey; use crate::message; +use crate::message::Payload; use crate::message::StacksTransactionSignRequest; use crate::network; use crate::signature::SighashDigest; use crate::stacks::api::FeePriority; use crate::stacks::api::StacksInteract; +use crate::stacks::api::SubmitTxResponse; use crate::stacks::contracts::CompleteDepositV1; use crate::stacks::contracts::ContractCall; use crate::stacks::wallet::MultisigTx; use crate::stacks::wallet::SignerWallet; use crate::storage::model; +use crate::storage::model::StacksTxId; use crate::storage::DbRead as _; use crate::wsts_state_machine; @@ -258,7 +260,6 @@ where bitcoin_aggregate_key: &PublicKey, ) -> Result<(), Error> { let wallet = SignerWallet::load(&self.context, chain_tip).await?; - let db = self.context.get_storage(); let stacks = self.context.get_stacks_client(); // Fetch deposit and withdrawal requests from the database where @@ -272,7 +273,9 @@ where // For withdrawals, we need to have a record of the `request_id` // associated with the bitcoin transaction's outputs. - let deposit_requests = db + let deposit_requests = self + .context + .get_storage() .get_swept_deposit_requests(chain_tip, self.context_window) .await?; @@ -288,22 +291,65 @@ where let account = stacks.get_account(wallet.address()).await?; wallet.set_nonce(account.nonce); - // Generate - let _sign_requests = futures::stream::iter(deposit_requests) - .then(|req| { - self.construct_deposit_stacks_sign_request(req, bitcoin_aggregate_key, &wallet) - }) - .try_collect::>() - .await?; + for req in deposit_requests { + let outpoint = req.deposit_outpoint(); + let sign_request_fut = + self.construct_deposit_stacks_sign_request(req, bitcoin_aggregate_key, &wallet); + + let (sign_request, multi_tx) = match sign_request_fut.await { + Ok(res) => res, + Err(error) => { + tracing::error!(%error, "could not construct a transaction completing the deposit request"); + continue; + } + }; + + // If we fail to sign the transaction for some reason, we + // decrement the nonce by one, and try the next transaction. + // This is not a fatal error, since we could fail to sign the + // transaction because someone else is now the coordinator, and + // all of the signers are now ignoring us. + let process_request_fut = + self.process_sign_request(sign_request, chain_tip, multi_tx, &wallet); + + match process_request_fut.await { + Ok(txid) => { + tracing::info!(%txid, "successfully submitted complete-deposit transaction") + } + Err(error) => { + tracing::warn!( + %error, + txid = %outpoint.txid, + vout = %outpoint.vout, + "could not process the stacks sign request for a deposit" + ); + wallet.set_nonce(wallet.get_nonce().saturating_sub(1)); + } + } + } - // TODO: - // 1. Broadcast the sign requests - // 2. Gather the signatures into the transaction. - // 3. Broadcast the transaction to the stacks network. Then go home - // and relax. Ok(()) } + /// Sign and broadcast the stacks transaction + async fn process_sign_request( + &mut self, + sign_request: StacksTransactionSignRequest, + chain_tip: &model::BitcoinBlockHash, + multi_tx: MultisigTx, + wallet: &SignerWallet, + ) -> Result { + let tx = self + .sign_stacks_transaction(sign_request, multi_tx, chain_tip, wallet) + .await?; + + match self.context.get_stacks_client().submit_tx(&tx).await { + Ok(SubmitTxResponse::Acceptance(txid)) => Ok(txid.into()), + Ok(SubmitTxResponse::Rejection(err)) => Err(err.into()), + Err(err) => Err(err), + } + } + /// Transform the swept deposit request into a Stacks sign request /// object. /// @@ -316,7 +362,7 @@ where req: model::SweptDepositRequest, bitcoin_aggregate_key: &PublicKey, wallet: &SignerWallet, - ) -> Result { + ) -> Result<(StacksTransactionSignRequest, MultisigTx), Error> { let tx_info = self .context .get_bitcoin_client() @@ -326,10 +372,7 @@ where Error::BitcoinTxMissing(req.sweep_txid.into(), Some(req.sweep_block_hash.into())) })?; - let outpoint = bitcoin::OutPoint { - txid: req.txid.into(), - vout: req.output_index, - }; + let outpoint = req.deposit_outpoint(); let assessed_bitcoin_fee = tx_info .assess_input_fee(&outpoint) .ok_or_else(|| Error::OutPointMissing(outpoint))?; @@ -355,14 +398,80 @@ where .await?; let multi_tx = MultisigTx::new_tx(&contract_call, wallet, tx_fee); + let tx = multi_tx.tx(); - Ok(StacksTransactionSignRequest { + let sign_request = StacksTransactionSignRequest { aggregate_key: *bitcoin_aggregate_key, contract_call, - nonce: multi_tx.tx().get_origin_nonce(), - tx_fee: multi_tx.tx().get_tx_fee(), - digest: multi_tx.tx().digest(), - }) + nonce: tx.get_origin_nonce(), + tx_fee: tx.get_tx_fee(), + digest: tx.digest(), + txid: tx.txid(), + }; + + Ok((sign_request, multi_tx)) + } + + /// Attempt to sign the stacks transaction. + async fn sign_stacks_transaction( + &mut self, + req: StacksTransactionSignRequest, + mut multi_tx: MultisigTx, + chain_tip: &model::BitcoinBlockHash, + wallet: &SignerWallet, + ) -> Result { + // First we ask for the other signers to sign our transaction + let txid = req.txid; + if wallet.signatures_required() > 1 { + self.send_message(req, chain_tip).await?; + } + // Second we sign it ourselves + // + // TODO: Note that this is all pretty "loose". We haven't yet + // confirmed whether we are actually a part of the multi-sig wallet + // that we loaded. Thus, this signature could be invalid. This will + // change if we make the `SignerWallet` include the private key and + // have it verify that it is part of the signer set. This would + // make everything much more solid. + let private_key = self.context.config().signer.private_key; + let signature = crate::signature::sign_stacks_tx(multi_tx.tx(), &private_key); + multi_tx.add_signature(signature)?; + + let mut count = 1; + + let future = async { + while count < wallet.signatures_required() { + let msg = self.network.receive().await?; + // TODO: We need to verify these messages, but it is best + // to do that at the source when we receive the message. + + if &msg.bitcoin_chain_tip != chain_tip { + tracing::warn!(?msg, "concurrent signing round message observed"); + continue; + } + + let sig = match msg.inner.payload { + Payload::StacksTransactionSignature(sig) if sig.txid == txid => sig, + _ => continue, + }; + + match multi_tx.add_signature(sig.signature) { + Ok(_) => count += 1, + Err(error) => tracing::warn!( + %txid, + %error, + offending_public_key = %msg.signer_pub_key, + "got an invalid signature" + ), + } + } + + Ok::<_, Error>(multi_tx.finalize_transaction()) + }; + + tokio::time::timeout(self.signing_round_max_duration, future) + .await + .map_err(|_| Error::SignatureTimeout(txid))? } /// Coordinate a signing round for the given request @@ -493,7 +602,7 @@ where continue; } - let message::Payload::WstsMessage(wsts_msg) = msg.inner.payload else { + let Payload::WstsMessage(wsts_msg) = msg.inner.payload else { continue; }; @@ -673,7 +782,7 @@ where #[tracing::instrument(skip(self, msg))] async fn send_message( &mut self, - msg: impl Into, + msg: impl Into, bitcoin_chain_tip: &model::BitcoinBlockHash, ) -> Result<(), Error> { let msg = msg diff --git a/signer/src/transaction_signer.rs b/signer/src/transaction_signer.rs index 0df95e37..958ef8ab 100644 --- a/signer/src/transaction_signer.rs +++ b/signer/src/transaction_signer.rs @@ -22,6 +22,7 @@ use crate::keys::PublicKey; use crate::message; use crate::message::StacksTransactionSignRequest; use crate::network; +use crate::signature::SighashDigest as _; use crate::stacks::contracts::AsContractCall; use crate::stacks::contracts::ContractCall; use crate::stacks::contracts::ReqContext; @@ -267,12 +268,12 @@ where } ( - message::Payload::StacksTransactionSignRequest(_request), + message::Payload::StacksTransactionSignRequest(request), true, ChainTipStatus::Canonical, ) => { - - //TODO(255): Implement + self.handle_stacks_transaction_sign_request(request, &msg.bitcoin_chain_tip) + .await?; } ( @@ -413,19 +414,26 @@ where #[tracing::instrument(skip_all)] async fn handle_stacks_transaction_sign_request( &mut self, - ctx: &impl Context, request: &StacksTransactionSignRequest, bitcoin_chain_tip: &model::BitcoinBlockHash, ) -> Result<(), Error> { - self.assert_valid_stackstransaction_sign_request(ctx, request, bitcoin_chain_tip) + self.assert_valid_stacks_tx_sign_request(request, bitcoin_chain_tip) .await?; - let wallet = SignerWallet::load(ctx, bitcoin_chain_tip).await?; + // We need to set the nonce in order to get the exact transaction + // that we need to sign. + let wallet = SignerWallet::load(&self.context, bitcoin_chain_tip).await?; wallet.set_nonce(request.nonce); let multi_sig = MultisigTx::new_tx(&request.contract_call, &wallet, request.tx_fee); let txid = multi_sig.tx().txid(); + // TODO: Make this more robust. The signer that recieves this won't + // be able to use the signature if it's over the wrong digest, so + // maybe we should error here. + debug_assert_eq!(multi_sig.tx().digest(), request.digest); + debug_assert_eq!(txid, request.txid); + let signature = crate::signature::sign_stacks_tx(multi_sig.tx(), &self.signer_private_key); let msg = message::StacksTransactionSignature { txid, signature }; @@ -435,12 +443,14 @@ where Ok(()) } - async fn assert_valid_stackstransaction_sign_request( - &mut self, - ctx: &impl Context, - request: &message::StacksTransactionSignRequest, + async fn assert_valid_stacks_tx_sign_request( + &self, + request: &StacksTransactionSignRequest, chain_tip: &model::BitcoinBlockHash, ) -> Result<(), Error> { + if true { + return Ok(()); + } // TODO(255): Finish the implementation let req_ctx = ReqContext { chain_tip: BitcoinBlockRef { @@ -457,6 +467,8 @@ where // This is wrong deployer: StacksAddress::burn_address(false), }; + // TODO: Maybe check the transaction fee in the request? + let ctx = &self.context; match &request.contract_call { ContractCall::AcceptWithdrawalV1(contract) => contract.validate(ctx, &req_ctx).await, ContractCall::CompleteDepositV1(contract) => contract.validate(ctx, &req_ctx).await,