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: impl bitcoin-core's gettxspendingprevout and getmempooldescendants RPCs (#759 part 1) #834

Merged
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
Loading