Skip to content

Commit

Permalink
Add donations tx type, consider them for signers utxo (#627)
Browse files Browse the repository at this point in the history
* feat: add donations tx type, consider them for signers utxo
  • Loading branch information
matteojug authored Oct 10, 2024
1 parent a3a927f commit 53b7393
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 40 deletions.
3 changes: 2 additions & 1 deletion signer/migrations/0003__create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ CREATE TYPE sbtc_signer.transaction_type AS ENUM (
'deposit_accept',
'withdraw_accept',
'withdraw_reject',
'rotate_keys'
'rotate_keys',
'donation'
);

CREATE TABLE sbtc_signer.bitcoin_blocks (
Expand Down
71 changes: 45 additions & 26 deletions signer/src/block_observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,32 +321,51 @@ where
// Look through all the UTXOs in the given transaction slice and
// keep the transactions where a UTXO is locked with a
// `scriptPubKey` controlled by the signers.
let sbtc_txs = txs
.iter()
.filter(|tx| {
// If any of the outputs are spent to one of the signers'
// addresses, then we care about it
tx.output
.iter()
.any(|tx_out| signer_script_pubkeys.contains(&tx_out.script_pubkey))
})
.map(|tx| {
let mut tx_bytes = Vec::new();
tx.consensus_encode(&mut tx_bytes)?;

// TODO: these aren't all sBTC transactions. Some of these
// could be "donations". One way to properly label these is
// to look at the scriptPubKey of the prevouts of the
// transaction.
Ok::<_, bitcoin::io::Error>(model::Transaction {
txid: tx.compute_txid().to_byte_array(),
tx: tx_bytes,
tx_type: model::TransactionType::SbtcTransaction,
block_hash: block_hash.to_byte_array(),
})
})
.collect::<Result<Vec<model::Transaction>, _>>()
.map_err(Error::BitcoinEncodeTransaction)?;
let mut sbtc_txs = Vec::new();
for tx in txs {
// If any of the outputs are spent to one of the signers'
// addresses, then we care about it
let outputs_spent_to_signers = tx
.output
.iter()
.any(|tx_out| signer_script_pubkeys.contains(&tx_out.script_pubkey));

if !outputs_spent_to_signers {
continue;
}

let mut tx_bytes = Vec::new();
tx.consensus_encode(&mut tx_bytes)
.map_err(Error::BitcoinEncodeTransaction)?;

// sBTC transactions have as first txin a signers spendable output
let mut tx_type = model::TransactionType::Donation;
if let Some(txin) = tx.input.first() {
let tx_info = self
.context
.get_bitcoin_client()
.get_tx(&txin.previous_output.txid)
.await?
.ok_or(Error::BitcoinTxMissing(txin.previous_output.txid, None))?;

let prevout = &tx_info
.tx
.tx_out(txin.previous_output.vout as usize)
.map_err(|_| Error::OutPointMissing(txin.previous_output))?
.script_pubkey;

if signer_script_pubkeys.contains(prevout) {
tx_type = model::TransactionType::SbtcTransaction;
}
};

sbtc_txs.push(model::Transaction {
txid: tx.compute_txid().to_byte_array(),
tx: tx_bytes,
tx_type,
block_hash: block_hash.to_byte_array(),
});
}

// Write these transactions into storage.
self.context
Expand Down
52 changes: 51 additions & 1 deletion signer/src/storage/in_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,53 @@ impl Store {
pub fn new_shared() -> SharedStore {
Arc::new(Mutex::new(Self::new()))
}

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

// Traverse the canonical chain backwards and find the first block containing relevant 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 = self.bitcoin_block_to_transactions.get(&block.block_hash)?;

let mut sbtc_txs = txs
.iter()
.filter_map(|tx| self.raw_transactions.get(&tx.into_bytes()))
.filter(|sbtc_tx| sbtc_tx.tx_type == model::TransactionType::Donation)
.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);
};

get_utxo(aggregate_key, sbtc_txs)
}
}

impl super::DbRead for SharedStore {
Expand Down Expand Up @@ -464,7 +511,10 @@ impl super::DbRead for SharedStore {
// `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);
// if no sbtc tx exists, consider donations
return store
.get_utxo_from_donation(chain_tip, aggregate_key, context_window)
.await;
};

get_utxo(aggregate_key, sbtc_txs)
Expand Down
2 changes: 2 additions & 0 deletions signer/src/storage/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@ pub enum TransactionType {
WithdrawReject,
/// A rotate keys call on Stacks.
RotateKeys,
/// A donation to signers aggregated key on Bitcoin.
Donation,
}

/// An identifier for a withdrawal request, comprised of the Stacks
Expand Down
97 changes: 96 additions & 1 deletion signer/src/storage/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,98 @@ impl PgStore {

Ok(model::TransactionIds { tx_ids, block_hashes })
}

async fn get_utxo_from_donation(
&self,
chain_tip: &model::BitcoinBlockHash,
aggregate_key: &PublicKey,
context_window: u16,
) -> Result<Option<SignerUtxo>, Error> {
// TODO(585): once the new table is ready, check if it can be used to simplify this
// TODO: currently returns the latest donation, we may want to return something else
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 = 'donation'
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 = 'donation' 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)
}
}

impl From<sqlx::PgPool> for PgStore {
Expand Down Expand Up @@ -1029,7 +1121,10 @@ impl super::DbRead for PgStore {

// `utxo_block` is the heighest block containing a valid utxo
let Some(utxo_block) = utxo_block else {
return Ok(None);
// if no sbtc tx exists, consider donations
return self
.get_utxo_from_donation(chain_tip, aggregate_key, context_window)
.await;
};
// Fetch all the sbtc txs in the same block
let sbtc_txs = sqlx::query_as::<_, model::Transaction>(
Expand Down
12 changes: 8 additions & 4 deletions signer/src/testing/storage/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,23 @@ impl TestData {
vec_diff(&mut self.transactions, &other.transactions);
}

/// Push sbtc txs to a specific bitcoin block
pub fn push_sbtc_txs(&mut self, block: &BitcoinBlockRef, sbtc_txs: Vec<bitcoin::Transaction>) {
/// Push bitcoin txs to a specific bitcoin block
pub fn push_bitcoin_txs(
&mut self,
block: &BitcoinBlockRef,
sbtc_txs: Vec<(model::TransactionType, bitcoin::Transaction)>,
) {
let mut bitcoin_transactions = vec![];
let mut transactions = vec![];

for tx in sbtc_txs {
for (tx_type, tx) in sbtc_txs {
let mut tx_bytes = Vec::new();
tx.consensus_encode(&mut tx_bytes).unwrap();

let tx = model::Transaction {
txid: tx.compute_txid().to_byte_array(),
tx: tx_bytes,
tx_type: model::TransactionType::SbtcTransaction,
tx_type,
block_hash: block.block_hash.into_bytes(),
};

Expand Down
Loading

0 comments on commit 53b7393

Please sign in to comment.