Skip to content

Commit

Permalink
feat: implement get_signer_utxo for pg (#584)
Browse files Browse the repository at this point in the history
* feat: implement get_signer_utxo for pg, nits
  • Loading branch information
matteojug authored Oct 1, 2024
1 parent 9637ead commit 0d6e5d6
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 59 deletions.
8 changes: 6 additions & 2 deletions signer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ pub enum Error {
#[error("could not decode the bitcoin block: {0}")]
DecodeBitcoinBlock(#[source] bitcoin::consensus::encode::Error),

/// Parsing the Hex Error
#[error("could not decode the bitcoin transaction: {0}")]
DecodeBitcoinTransaction(#[source] bitcoin::consensus::encode::Error),

/// Parsing the Hex Error
#[error("could not decode the Nakamoto block with ID: {1}; {0}")]
DecodeNakamotoBlock(#[source] blockstack_lib::codec::Error, StacksBlockId),
Expand Down Expand Up @@ -309,8 +313,8 @@ pub enum Error {
#[error("missing signer utxo")]
MissingSignerUtxo,

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

/// Invalid signature
Expand Down
42 changes: 8 additions & 34 deletions signer/src/storage/in_memory.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
//! In-memory store implementation - useful for tests

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

Expand All @@ -21,6 +19,8 @@ use crate::stacks::events::WithdrawalCreateEvent;
use crate::stacks::events::WithdrawalRejectEvent;
use crate::storage::model;

use super::util::get_utxo;

/// A store wrapped in an Arc<Mutex<...>> for interior mutability
pub type SharedStore = Arc<Mutex<Store>>;

Expand Down Expand Up @@ -426,6 +426,7 @@ impl super::DbRead for SharedStore {
&self,
chain_tip: &model::BitcoinBlockHash,
aggregate_key: &PublicKey,
context_window: u16,
) -> Result<Option<SignerUtxo>, Error> {
let script_pubkey = aggregate_key.signers_script_pubkey();
let store = self.lock().await;
Expand All @@ -434,6 +435,7 @@ impl super::DbRead for SharedStore {

// 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))
.take(context_window as usize)
.filter_map(|block| {
let txs = store.bitcoin_block_to_transactions.get(&block.block_hash)?;

Expand Down Expand Up @@ -465,35 +467,7 @@ impl super::DbRead for SharedStore {
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),
}
get_utxo(aggregate_key, sbtc_txs)
}

async fn get_deposit_request_signer_votes(
Expand Down
2 changes: 2 additions & 0 deletions signer/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod in_memory;
pub mod model;
pub mod postgres;
pub mod sqlx;
pub mod util;

use std::future::Future;

Expand Down Expand Up @@ -147,6 +148,7 @@ pub trait DbRead {
&self,
chain_tip: &model::BitcoinBlockHash,
aggregate_key: &crate::keys::PublicKey,
context_window: u16,
) -> impl Future<Output = Result<Option<SignerUtxo>, Error>> + Send;

/// For the given outpoint and aggregate key, get the list all signer
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 @@ -225,7 +225,7 @@ pub struct TransactionIds {
}

/// A raw transaction on either Bitcoin or Stacks.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, sqlx::FromRow)]
#[cfg_attr(feature = "testing", derive(fake::Dummy))]
pub struct Transaction {
/// Transaction ID.
Expand Down
94 changes: 91 additions & 3 deletions signer/src/storage/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@
use std::collections::HashMap;
use std::sync::OnceLock;

use bitcoin::consensus::Decodable as _;
use bitcoin::hashes::Hash as _;
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
use blockstack_lib::chainstate::stacks::TransactionPayload;
use blockstack_lib::codec::StacksMessageCodec;
use blockstack_lib::types::chainstate::StacksBlockId;
use futures::StreamExt as _;
use sqlx::PgExecutor;

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;
use crate::stacks::events::WithdrawalRejectEvent;
use crate::storage::model;
use crate::storage::model::TransactionType;

use super::util::get_utxo;

/// All migration scripts from the `signer/migrations` directory.
static PGSQL_MIGRATIONS: include_dir::Dir =
include_dir::include_dir!("$CARGO_MANIFEST_DIR/migrations");
Expand Down Expand Up @@ -946,10 +951,93 @@ impl super::DbRead for PgStore {

async fn get_signer_utxo(
&self,
_chain_tip: &model::BitcoinBlockHash,
_aggregate_key: &PublicKey,
chain_tip: &model::BitcoinBlockHash,
aggregate_key: &PublicKey,
context_window: u16,
) -> Result<Option<SignerUtxo>, Error> {
unimplemented!() // TODO(538)
// TODO(585): once the new table is ready, check if it can be used to simplify this
let script_pubkey = aggregate_key.signers_script_pubkey();
let mut txs = sqlx::query_as::<_, model::Transaction>(
r#"
WITH RECURSIVE tx_block_chain AS (
SELECT
block_hash
, parent_hash
, 1 AS depth
FROM sbtc_signer.bitcoin_blocks
WHERE block_hash = $1
UNION ALL
SELECT
parent.block_hash
, parent.parent_hash
, child.depth + 1
FROM sbtc_signer.bitcoin_blocks AS parent
JOIN tx_block_chain AS child ON child.parent_hash = parent.block_hash
WHERE child.depth < $2
)
SELECT
txs.txid
, txs.tx
, txs.tx_type
, tbc.block_hash
FROM tx_block_chain AS tbc
JOIN sbtc_signer.bitcoin_transactions AS bt ON tbc.block_hash = bt.block_hash
JOIN sbtc_signer.transactions AS txs USING (txid)
WHERE txs.tx_type = 'sbtc_transaction'
ORDER BY tbc.depth ASC;
"#,
)
.bind(chain_tip)
.bind(context_window as i32)
.fetch(&self.0);

let mut utxo_block = None;
while let Some(tx) = txs.next().await {
let tx = tx.map_err(Error::SqlxQuery)?;
let bt_tx = bitcoin::Transaction::consensus_decode(&mut tx.tx.as_slice())
.map_err(Error::DecodeBitcoinTransaction)?;
if !bt_tx
.output
.first()
.is_some_and(|out| out.script_pubkey == script_pubkey)
{
continue;
}
utxo_block = Some(tx.block_hash);
break;
}

// `utxo_block` is the heighest block containing a valid utxo
let Some(utxo_block) = utxo_block else {
return Ok(None);
};
// Fetch all the sbtc txs in the same block
let sbtc_txs = sqlx::query_as::<_, model::Transaction>(
r#"
SELECT
txs.txid
, txs.tx
, txs.tx_type
, bt.block_hash
FROM sbtc_signer.transactions AS txs
JOIN sbtc_signer.bitcoin_transactions AS bt USING (txid)
WHERE txs.tx_type = 'sbtc_transaction' AND bt.block_hash = $1;
"#,
)
.bind(utxo_block)
.fetch_all(&self.0)
.await
.map_err(Error::SqlxQuery)?
.iter()
.map(|tx| {
bitcoin::Transaction::consensus_decode(&mut tx.tx.as_slice())
.map_err(Error::DecodeBitcoinTransaction)
})
.collect::<Result<Vec<bitcoin::Transaction>, _>>()?;

get_utxo(aggregate_key, sbtc_txs)
}

async fn in_canonical_bitcoin_blockchain(
Expand Down
46 changes: 46 additions & 0 deletions signer/src/storage/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! General utilities for the storage.

use std::collections::HashSet;

use crate::bitcoin::utxo::SignerUtxo;
use crate::error::Error;
use crate::keys::PublicKey;
use crate::keys::SignerScriptPubKey as _;

/// Given the sbtc txs in a block, returns the `aggregate_key` utxo (if there's exactly one)
pub fn get_utxo(
aggregate_key: &PublicKey,
sbtc_txs: Vec<bitcoin::Transaction>,
) -> Result<Option<SignerUtxo>, Error> {
let script_pubkey = aggregate_key.signers_script_pubkey();

let spent: HashSet<bitcoin::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 = bitcoin::OutPoint::new(tx.compute_txid(), 0);
if tx_out.script_pubkey == *script_pubkey && !spent.contains(&outpoint) {
return Some(SignerUtxo {
outpoint,
amount: tx_out.value.to_sat(),
// Txs are filtered based on the `aggregate_key` script pubkey
public_key: bitcoin::XOnlyPublicKey::from(aggregate_key),
});
}
}

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

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

0 comments on commit 0d6e5d6

Please sign in to comment.