Skip to content

Commit

Permalink
Merge branch 'main' into feat/run-transaction-coord
Browse files Browse the repository at this point in the history
  • Loading branch information
cylewitruk committed Oct 1, 2024
2 parents ffab92f + a6f70f7 commit e70717d
Show file tree
Hide file tree
Showing 16 changed files with 535 additions and 94 deletions.
16 changes: 3 additions & 13 deletions signer/src/bitcoin/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ use bitcoin::Txid;
use bitcoincore_rpc::RpcApi as _;
use url::Url;

use crate::bitcoin::utxo;
use crate::bitcoin::utxo::SignerUtxo;
use crate::bitcoin::BitcoinInteract;
use crate::error::Error;
use crate::keys::PublicKey;
use crate::util::ApiFallbackClient;
use crate::{error::Error, util::ApiFallbackClient};

use super::{utxo, BitcoinInteract};

use super::rpc::BitcoinCoreClient;
use super::rpc::BitcoinTxInfo;
Expand Down Expand Up @@ -69,13 +66,6 @@ impl BitcoinInteract for ApiFallbackClient<BitcoinCoreClient> {
todo!() // TODO(542)
}

async fn get_signer_utxo(
&self,
_aggregate_key: &PublicKey,
) -> Result<Option<SignerUtxo>, Error> {
todo!() // TODO(538)
}

async fn get_last_fee(&self, _utxo: bitcoin::OutPoint) -> Result<Option<utxo::Fees>, Error> {
todo!() // TODO(541)
}
Expand Down
7 changes: 0 additions & 7 deletions signer/src/bitcoin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use rpc::BitcoinTxInfo;
use rpc::GetTxResponse;

use crate::error::Error;
use crate::keys::PublicKey;

pub mod client;
pub mod fees;
Expand Down Expand Up @@ -41,12 +40,6 @@ pub trait BitcoinInteract: Sync + Send {
// This should be implemented with the help of the `fees::EstimateFees` trait
fn estimate_fee_rate(&self) -> impl std::future::Future<Output = Result<f64, Error>> + Send;

/// Get the outstanding signer UTXO
fn get_signer_utxo(
&self,
aggregate_key: &PublicKey,
) -> impl Future<Output = Result<Option<utxo::SignerUtxo>, Error>> + Send;

/// Get the total fee amount and the fee rate for the last transaction that
/// used the given UTXO as an input.
fn get_last_fee(
Expand Down
8 changes: 0 additions & 8 deletions signer/src/bitcoin/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ use url::Url;

use crate::bitcoin::BitcoinInteract;
use crate::error::Error;
use crate::keys::PublicKey;

/// A slimmed down type representing a response from bitcoin-core's
/// getrawtransaction RPC.
Expand Down Expand Up @@ -344,13 +343,6 @@ impl BitcoinInteract for BitcoinCoreClient {
todo!()
}

async fn get_signer_utxo(
&self,
_: &PublicKey,
) -> Result<Option<super::utxo::SignerUtxo>, Error> {
todo!()
}

async fn get_last_fee(&self, _: OutPoint) -> Result<Option<super::utxo::Fees>, Error> {
todo!()
}
Expand Down
2 changes: 1 addition & 1 deletion signer/src/bitcoin/utxo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ impl<'a> Requests<'a> {
/// taproot. This is necessary because the signers collectively generate
/// Schnorr signatures, which requires taproot.
/// * The taproot script for each signer UTXO is a key-spend only script.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SignerUtxo {
/// The outpoint of the signers' UTXO
pub outpoint: OutPoint,
Expand Down
8 changes: 1 addition & 7 deletions signer/src/block_observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ where
let sbtc_txs = txs
.iter()
.filter(|tx| {
// If any of the outputs are spend to one of the signers'
// If any of the outputs are spent to one of the signers'
// addresses, then we care about it
tx.output
.iter()
Expand Down Expand Up @@ -828,12 +828,6 @@ mod tests {
unimplemented!()
}

async fn get_signer_utxo(
&self,
_point: &PublicKey,
) -> Result<Option<utxo::SignerUtxo>, Error> {
unimplemented!()
}
async fn get_last_fee(
&self,
_utxo: bitcoin::OutPoint,
Expand Down
4 changes: 4 additions & 0 deletions signer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ pub enum Error {
#[error("missing signer utxo")]
MissingSignerUtxo,

/// Too many signer utxo
#[error("too many signer utxo")]
TooManySignerUtxos,

/// Invalid signature
#[error("invalid signature")]
InvalidSignature,
Expand Down
90 changes: 88 additions & 2 deletions signer/src/storage/in_memory.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
//! In-memory store implementation - useful for tests

use bitcoin::consensus::Decodable;
use bitcoin::OutPoint;
use blockstack_lib::types::chainstate::StacksBlockId;
use futures::StreamExt;
use futures::TryStreamExt;
use secp256k1::XOnlyPublicKey;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::Mutex;

use crate::bitcoin::utxo::SignerUtxo;
use crate::error::Error;
use crate::keys::PublicKey;
use crate::keys::SignerScriptPubKey as _;
use crate::stacks::events::CompletedDepositEvent;
use crate::stacks::events::WithdrawalAcceptEvent;
use crate::stacks::events::WithdrawalCreateEvent;
Expand Down Expand Up @@ -46,6 +51,9 @@ pub struct Store {
/// Withdraw signers
pub withdrawal_request_to_signers: HashMap<WithdrawalRequestPk, Vec<model::WithdrawalSigner>>,

/// Raw transaction data
pub raw_transactions: HashMap<[u8; 32], model::Transaction>,

/// Bitcoin blocks to transactions
pub bitcoin_block_to_transactions: HashMap<model::BitcoinBlockHash, Vec<model::BitcoinTxId>>,

Expand Down Expand Up @@ -414,6 +422,80 @@ impl super::DbRead for SharedStore {
.collect())
}

async fn get_signer_utxo(
&self,
chain_tip: &model::BitcoinBlockHash,
aggregate_key: &PublicKey,
) -> Result<Option<SignerUtxo>, Error> {
let script_pubkey = aggregate_key.signers_script_pubkey();
let store = self.lock().await;
let bitcoin_blocks = &store.bitcoin_blocks;
let first = bitcoin_blocks.get(chain_tip);

// Traverse the canonical chain backwards and find the first block containing relevant sbtc tx(s)
let sbtc_txs = std::iter::successors(first, |block| bitcoin_blocks.get(&block.parent_hash))
.filter_map(|block| {
let txs = store.bitcoin_block_to_transactions.get(&block.block_hash)?;

let mut sbtc_txs = txs
.iter()
.filter_map(|tx| store.raw_transactions.get(&tx.into_bytes()))
.filter(|sbtc_tx| sbtc_tx.tx_type == model::TransactionType::SbtcTransaction)
.filter_map(|tx| {
bitcoin::Transaction::consensus_decode(&mut tx.tx.as_slice()).ok()
})
.filter(|tx| {
tx.output
.first()
.is_some_and(|out| out.script_pubkey == script_pubkey)
})
.peekable();

if sbtc_txs.peek().is_some() {
Some(sbtc_txs.collect::<Vec<_>>())
} else {
None
}
})
.next();

// `sbtc_txs` contains all the txs in the highest canonical block where the first
// output is spendable by script_pubkey
let Some(sbtc_txs) = sbtc_txs else {
return Ok(None);
};

let spent: HashSet<OutPoint> = sbtc_txs
.iter()
.flat_map(|tx| tx.input.iter().map(|txin| txin.previous_output))
.collect();

let utxos = sbtc_txs
.iter()
.flat_map(|tx| {
if let Some(tx_out) = tx.output.first() {
let outpoint = OutPoint::new(tx.compute_txid(), 0);
if !spent.contains(&outpoint) {
return Some(SignerUtxo {
outpoint,
amount: tx_out.value.to_sat(),
// Txs were filtered based on the `aggregate_key` script pubkey
public_key: XOnlyPublicKey::from(aggregate_key),
});
}
}

None
})
.collect::<Vec<_>>();

match utxos[..] {
[] => Ok(None),
[utxo] => Ok(Some(utxo)),
_ => Err(Error::TooManySignerUtxos),
}
}

async fn get_deposit_request_signer_votes(
&self,
txid: &model::BitcoinTxId,
Expand Down Expand Up @@ -636,8 +718,12 @@ impl super::DbWrite for SharedStore {
Ok(())
}

async fn write_transaction(&self, _transaction: &model::Transaction) -> Result<(), Error> {
// Currently not needed in-memory since it's not required by any queries
async fn write_transaction(&self, transaction: &model::Transaction) -> Result<(), Error> {
self.lock()
.await
.raw_transactions
.insert(transaction.txid, transaction.clone());

Ok(())
}

Expand Down
19 changes: 19 additions & 0 deletions signer/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::future::Future;

use blockstack_lib::types::chainstate::StacksBlockId;

use crate::bitcoin::utxo::SignerUtxo;
use crate::error::Error;
use crate::keys::PublicKey;
use crate::stacks::events::CompletedDepositEvent;
Expand Down Expand Up @@ -130,6 +131,24 @@ pub trait DbRead {
&self,
) -> impl Future<Output = Result<Vec<model::Bytes>, Error>> + Send;

/// Get the outstanding signer UTXO.
///
/// Under normal conditions, the signer will have only one UTXO they can spend.
/// The specific UTXO we want is one such that:
/// 1. The transaction is in a block on the canonical bitcoin blockchain.
/// 2. The output is the first output in the transaction.
/// 3. The output's `scriptPubKey` matches `aggregate_key`.
/// 4. The output is unspent. It is possible for more than one transaction
/// within the same block to satisfy points 1-3, but if the signers
/// have one or more transactions within a block, exactly one output
/// satisfying points 1-3 will be unspent.
/// 5. The block that includes the transaction that satisfies points 1-4 has the greatest height of all such blocks.
fn get_signer_utxo(
&self,
chain_tip: &model::BitcoinBlockHash,
aggregate_key: &crate::keys::PublicKey,
) -> impl Future<Output = Result<Option<SignerUtxo>, Error>> + Send;

/// For the given outpoint and aggregate key, get the list all signer
/// votes in the signer set.
fn get_deposit_request_signer_votes(
Expand Down
2 changes: 1 addition & 1 deletion signer/src/storage/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub struct StacksBlock {
}

/// Deposit request.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, sqlx::FromRow)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, sqlx::FromRow)]
#[cfg_attr(feature = "testing", derive(fake::Dummy))]
pub struct DepositRequest {
/// Transaction ID of the deposit request transaction.
Expand Down
9 changes: 9 additions & 0 deletions signer/src/storage/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use blockstack_lib::codec::StacksMessageCodec;
use blockstack_lib::types::chainstate::StacksBlockId;
use sqlx::PgExecutor;

use crate::bitcoin::utxo::SignerUtxo;
use crate::error::Error;
use crate::keys::PublicKey;
use crate::stacks::events::CompletedDepositEvent;
Expand Down Expand Up @@ -943,6 +944,14 @@ impl super::DbRead for PgStore {
.map_err(Error::SqlxQuery)
}

async fn get_signer_utxo(
&self,
_chain_tip: &model::BitcoinBlockHash,
_aggregate_key: &PublicKey,
) -> Result<Option<SignerUtxo>, Error> {
unimplemented!() // TODO(538)
}

async fn in_canonical_bitcoin_blockchain(
&self,
chain_tip: &model::BitcoinBlockRef,
Expand Down
7 changes: 0 additions & 7 deletions signer/src/testing/api_clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,6 @@ impl BitcoinInteract for NoopApiClient {
unimplemented!()
}

async fn get_signer_utxo(
&self,
_aggregate_key: &crate::keys::PublicKey,
) -> Result<Option<crate::bitcoin::utxo::SignerUtxo>, Error> {
unimplemented!()
}

async fn get_last_fee(
&self,
_utxo: bitcoin::OutPoint,
Expand Down
7 changes: 0 additions & 7 deletions signer/src/testing/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,6 @@ impl BitcoinInteract for WrappedMock<MockBitcoinInteract> {
self.inner.lock().await.estimate_fee_rate().await
}

async fn get_signer_utxo(
&self,
aggregate_key: &crate::keys::PublicKey,
) -> Result<Option<crate::bitcoin::utxo::SignerUtxo>, Error> {
self.inner.lock().await.get_signer_utxo(aggregate_key).await
}

async fn get_last_fee(
&self,
utxo: bitcoin::OutPoint,
Expand Down
Loading

0 comments on commit e70717d

Please sign in to comment.