Skip to content

Commit

Permalink
feat: impl bitcoin-core's gettxspendingprevout and `getmempooldesce…
Browse files Browse the repository at this point in the history
…ndants` RPCs (#759 part 1) (#834)

* wip

* wip

* wip

* additional test and docs

* add impls to bitcoincoreclient

* revert error enum

* rename outpoint+comment+separate code

* outpoint already imported

* pr nits

* little refactor of rpc method

* missed bitcoin block gen 100
  • Loading branch information
cylewitruk authored Nov 13, 2024
1 parent d8df3ff commit a93ab6d
Show file tree
Hide file tree
Showing 10 changed files with 430 additions and 3 deletions.
2 changes: 1 addition & 1 deletion docker-compose.signer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ services:

bitcoind:
container_name: sbtc-bitcoind
image: lncm/bitcoind:v25.1
image: lncm/bitcoind:v27.0
volumes:
- ./signer/tests/service-configs/bitcoin.conf:/data/.bitcoin/bitcoin.conf:ro
restart: on-failure
Expand Down
2 changes: 1 addition & 1 deletion docker/docker-compose.ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
services:

bitcoind:
image: lncm/bitcoind:v25.0
image: lncm/bitcoind:v27.0
volumes:
- ../signer/tests/service-configs/bitcoin.conf:/data/.bitcoin/bitcoin.conf:ro
restart: on-failure
Expand Down
2 changes: 1 addition & 1 deletion docker/docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ services:

bitcoind:
container_name: bitcoind
image: lncm/bitcoind:v25.0
image: lncm/bitcoind:v27.0
volumes:
- ../signer/tests/service-configs/bitcoin.conf:/data/.bitcoin/bitcoin.conf:ro
restart: on-failure
Expand Down
13 changes: 13 additions & 0 deletions signer/src/bitcoin/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,17 @@ impl BitcoinInteract for ApiFallbackClient<BitcoinCoreClient> {
self.exec(|client, _| client.broadcast_transaction(tx))
.await
}

async fn find_mempool_transactions_spending_output(
&self,
outpoint: &bitcoin::OutPoint,
) -> Result<Vec<Txid>, Error> {
self.exec(|client, _| client.find_mempool_transactions_spending_output(outpoint))
.await
}

async fn find_mempool_descendants(&self, txid: &Txid) -> Result<Vec<Txid>, Error> {
self.exec(|client, _| client.find_mempool_descendants(txid))
.await
}
}
33 changes: 33 additions & 0 deletions signer/src/bitcoin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,37 @@ pub trait BitcoinInteract: Sync + Send {
&self,
tx: &bitcoin::Transaction,
) -> impl Future<Output = Result<(), Error>> + Send;

/// Find transactions in the mempool which spend the given output. `txid`
/// must be a known confirmed transaction.
///
/// This method returns an (unordered) list of transaction IDs which are in
/// the mempool and spend the given (confirmed) output.
///
/// If there are no transactions in the mempool which spend the given
/// output, an empty list is returned.
fn find_mempool_transactions_spending_output(
&self,
outpoint: &bitcoin::OutPoint,
) -> impl Future<Output = Result<Vec<Txid>, Error>> + Send;

/// Finds all transactions in the mempool which are descendants of the given
/// mempool transaction. `txid` must be a transaction in the mempool.
///
/// This method returns an (unordered) list of transaction IDs which are
/// both direct and indirect descendants of the given transaction, meaning
/// that they either directly spend an output of the given transaction or
/// spend an output of a transaction which is itself a descendant of the
/// given transaction.
///
/// If there are no descendants of the given transaction in the mempool, an
/// empty list is returned.
///
/// Use [`Self::find_mempool_transactions_spending_output`] to find
/// transactions in the mempool which spend an output of a confirmed
/// transaction if needed prior to calling this method.
fn find_mempool_descendants(
&self,
txid: &Txid,
) -> impl Future<Output = Result<Vec<Txid>, Error>> + Send;
}
116 changes: 116 additions & 0 deletions signer/src/bitcoin/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use bitcoin::Amount;
use bitcoin::Block;
use bitcoin::BlockHash;
use bitcoin::Denomination;
use bitcoin::OutPoint;
use bitcoin::ScriptBuf;
use bitcoin::Transaction;
use bitcoin::Txid;
Expand Down Expand Up @@ -124,6 +125,47 @@ pub struct BitcoinTxInfo {
pub block_time: u64,
}

/// A struct containing the response from bitcoin-core for a
/// `gettxspendingprevout` RPC call. The actual response is an array; this
/// struct represents a single element of that array.
///
/// # Notes
///
/// * This endpoint requires bitcoin-core v27.0 or later.
/// * Documentation for this endpoint can be found at
/// https://bitcoincore.org/en/doc/27.0.0/rpc/blockchain/gettxspendingprevout/
/// * This struct omits some fields returned from bitcoin-core: `txid` and
/// `vout`, which are just the txid and vout of the outpoint which was passed
/// as RPC arguments. We don't need them because we're not providing multiple
/// outpoints to check, so we don't need to map the results back to specific
/// outpoints.
#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize, serde::Serialize)]
pub struct TxSpendingPrevOut {
/// The txid of the transaction which spent the output.
#[serde(rename = "spendingtxid")]
pub spending_txid: Option<Txid>,
}

/// A struct representing an output of a transaction. This is necessary as
/// the [`bitcoin::OutPoint`] type does not serialize to the format that the
/// bitcoin-core RPC expects.
#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize, serde::Serialize)]
pub struct RpcOutPoint {
/// The txid of the transaction including the output.
pub txid: Txid,
/// The index of the output in the transaction.
pub vout: u32,
}

impl From<&OutPoint> for RpcOutPoint {
fn from(outpoint: &OutPoint) -> Self {
Self {
txid: outpoint.txid,
vout: outpoint.vout,
}
}
}

/// A description of an input into a transaction.
#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize, serde::Serialize)]
pub struct BitcoinTxVin {
Expand Down Expand Up @@ -299,6 +341,69 @@ impl BitcoinCoreClient {
}
}

/// Scan the Bitcoin node's mempool to find transactions spending the
/// provided output. This method uses the `gettxspendingprevout` RPC
/// endpoint.
///
/// # Notes
///
/// This method requires bitcoin-core v27 or later.
pub fn get_tx_spending_prevout(&self, outpoint: &OutPoint) -> Result<Vec<Txid>, Error> {
let rpc_outpoint = RpcOutPoint::from(outpoint);
let args = [serde_json::to_value(vec![rpc_outpoint]).map_err(Error::JsonSerialize)?];

let response = self
.inner
.call::<Vec<TxSpendingPrevOut>>("gettxspendingprevout", &args);

let results = match response {
Ok(response) => Ok(response),
Err(err) => Err(Error::BitcoinCoreGetTxSpendingPrevout(err, *outpoint)),
}?;

// We will get results for each outpoint we pass in, and if there is no
// transaction spending the outpoint then the `spending_txid` field will
// be `None`. We filter out the `None`s and collect the `Some`s into a
// vector of `Txid`s.
let txids = results
.into_iter()
.filter_map(|result| result.spending_txid)
.collect::<Vec<_>>();

Ok(txids)
}

/// Scan the Bitcoin node's mempool to find transactions that are
/// descendants of the provided transaction. This method uses the
/// `getmempooldescendants` RPC endpoint.
///
/// If the transaction is not in the mempool then an empty vector is
/// returned.
///
/// If there is a chain of transactions in the mempool which implicitly
/// depend on the provided transaction, then the entire chain of
/// transactions is returned, not just the immediate descendants.
///
/// The ordering of the transactions in the returned vector is not
/// guaranteed to be in any particular order.
///
/// # Notes
///
/// - This method requires bitcoin-core v27 or later.
/// - The RPC endpoint does not in itself return raw transaction data, so
/// [`Self::get_tx`] must be used to fetch each transaction separately.
pub fn get_mempool_descendants(&self, txid: &Txid) -> Result<Vec<Txid>, Error> {
let args = [serde_json::to_value(txid).map_err(Error::JsonSerialize)?];

let result = self.inner.call::<Vec<Txid>>("getmempooldescendants", &args);

match result {
Ok(txids) => Ok(txids),
Err(BtcRpcError::JsonRpc(JsonRpcError::Rpc(RpcError { code: -5, .. }))) => Ok(vec![]),
Err(err) => Err(Error::BitcoinCoreGetMempoolDescendants(err, *txid)),
}
}

/// Estimates the approximate fee in sats per vbyte needed for a
/// transaction to be confirmed within `num_blocks`.
///
Expand Down Expand Up @@ -369,4 +474,15 @@ impl BitcoinInteract for BitcoinCoreClient {
self.estimate_fee_rate(1)
.map(|estimate| estimate.sats_per_vbyte)
}

async fn find_mempool_transactions_spending_output(
&self,
outpoint: &OutPoint,
) -> Result<Vec<Txid>, Error> {
self.get_tx_spending_prevout(outpoint)
}

async fn find_mempool_descendants(&self, txid: &Txid) -> Result<Vec<Txid>, Error> {
self.get_mempool_descendants(txid)
}
}
8 changes: 8 additions & 0 deletions signer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ pub enum Error {
#[error("sweep transaction not found: {0}")]
MissingSweepTransaction(bitcoin::Txid),

/// Received an error in response to getmempooldescendants RPC call
#[error("bitcoin-core getmempooldescendants error for txid {1}: {0}")]
BitcoinCoreGetMempoolDescendants(bitcoincore_rpc::Error, bitcoin::Txid),

/// Received an error in response to gettxspendingprevout RPC call
#[error("bitcoin-core gettxspendingprevout error for outpoint: {0}")]
BitcoinCoreGetTxSpendingPrevout(#[source] bitcoincore_rpc::Error, bitcoin::OutPoint),

/// The nakamoto start height could not be determined.
#[error("nakamoto start height could not be determined")]
MissingNakamotoStartHeight,
Expand Down
11 changes: 11 additions & 0 deletions signer/src/testing/block_observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ impl BitcoinInteract for TestHarness {
async fn broadcast_transaction(&self, _tx: &bitcoin::Transaction) -> Result<(), Error> {
unimplemented!()
}

async fn find_mempool_transactions_spending_output(
&self,
_outpoint: &bitcoin::OutPoint,
) -> Result<Vec<Txid>, Error> {
unimplemented!()
}

async fn find_mempool_descendants(&self, _txid: &Txid) -> Result<Vec<Txid>, Error> {
unimplemented!()
}
}

impl StacksInteract for TestHarness {
Expand Down
11 changes: 11 additions & 0 deletions signer/src/testing/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,17 @@ impl BitcoinInteract for WrappedMock<MockBitcoinInteract> {
async fn broadcast_transaction(&self, tx: &bitcoin::Transaction) -> Result<(), Error> {
self.inner.lock().await.broadcast_transaction(tx).await
}

async fn find_mempool_transactions_spending_output(
&self,
_outpoint: &bitcoin::OutPoint,
) -> Result<Vec<Txid>, Error> {
unimplemented!()
}

async fn find_mempool_descendants(&self, _txid: &Txid) -> Result<Vec<Txid>, Error> {
unimplemented!()
}
}

impl StacksInteract for WrappedMock<MockStacksInteract> {
Expand Down
Loading

0 comments on commit a93ab6d

Please sign in to comment.