Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sign and broadcast Stacks transactions #617

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
10 changes: 6 additions & 4 deletions protobufs/stacks/signer/v1/requests.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
30 changes: 15 additions & 15 deletions signer/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -71,7 +71,7 @@ pub struct SignerContext<S, BC, ST, EM> {

impl<S, BC, ST, EM> SignerContext<S, BC, ST, EM>
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,
Expand Down Expand Up @@ -125,10 +125,10 @@ where

impl<S, BC, ST, EM> Context for SignerContext<S, BC, ST, EM>
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
Expand Down Expand Up @@ -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()
}
}
Expand Down
9 changes: 9 additions & 0 deletions signer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand All @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions signer/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
matteojug marked this conversation as resolved.
Show resolved Hide resolved
}

/// Represents a signature of a Stacks transaction.
Expand Down
13 changes: 8 additions & 5 deletions signer/src/proto/generated/stacks.signer.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ pub struct StacksTransactionSignRequest {
/// and a few other things.
#[prost(message, optional, tag = "4")]
pub digest: ::core::option::Option<super::super::super::crypto::Uint256>,
/// The transaction ID of the associated contract call transaction.
#[prost(message, optional, tag = "5")]
pub txid: ::core::option::Option<super::super::super::crypto::Uint256>,
/// 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,
Expand All @@ -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),
}
}
Expand Down
12 changes: 11 additions & 1 deletion signer/src/stacks/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions signer/src/storage/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 9 additions & 9 deletions signer/src/testing/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ impl<Storage, Bitcoin, Stacks>

impl<Storage, Bitcoin, Stacks, Emily> Context for TestContext<Storage, Bitcoin, Stacks, Emily>
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was it that made these all need to be '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()
Expand All @@ -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()
}
}
Expand Down
2 changes: 2 additions & 0 deletions signer/src/testing/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -105,6 +106,7 @@ impl fake::Dummy<fake::Faker> for message::StacksTransactionSignRequest {
nonce: 1,
aggregate_key: PublicKey::from_private_key(&private_key),
digest: config.fake_with_rng(rng),
txid: config.fake_with_rng::<StacksTxId, _>(rng).into(),
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions signer/src/testing/transaction_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ const EMPTY_BITCOIN_TX: bitcoin::Transaction = bitcoin::Transaction {
output: vec![],
};

struct EventLoopHarness<C> {
struct TxCoordinatorEventLoopHarness<C> {
event_loop: EventLoop<C>,
context: C,
is_started: Arc<AtomicBool>,
}

impl<C> EventLoopHarness<C>
impl<C> TxCoordinatorEventLoopHarness<C>
where
C: Context + 'static,
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions signer/src/testing/transaction_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ use tokio::time::error::Elapsed;

use super::context::*;

struct EventLoopHarness<Context, Rng> {
struct TxSignerEventLoopHarness<Context, Rng> {
context: Context,
event_loop: EventLoop<Context, Rng>,
}

impl<Ctx, Rng> EventLoopHarness<Ctx, Rng>
impl<Ctx, Rng> TxSignerEventLoopHarness<Ctx, Rng>
where
Ctx: Context + 'static,
Rng: rand::RngCore + rand::CryptoRng + Send + Sync + 'static,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading