Skip to content

Commit 446283c

Browse files
committed
Addition of new RPC command for manual cpfp.
Along with tests.
1 parent cd21ab8 commit 446283c

File tree

11 files changed

+478
-8
lines changed

11 files changed

+478
-8
lines changed

doc/API.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Note that all addresses are bech32-encoded *version 0* native Segwit `scriptPubK
2626
| [`setspendtx`](#setspendtx) | Announce and broadcast this Spend transaction |
2727
| [`gethistory`](#gethistory) | Retrieve history of funds |
2828
| [`emergency`](#emergency) | Broadcast all Emergency signed transactions |
29+
| [`cpfp`](#cpfp) | Manually trigger the cpfp for transactions. |
2930

3031

3132

@@ -483,6 +484,20 @@ of inflows and outflows net of any change amount (that is technically a transact
483484
None; the `result` field will be set to the empty object `{}`. Any value should be
484485
disregarded for forward compatibility.
485486

487+
### `cpfp`
488+
489+
#### Request
490+
491+
| Field | Type | Description |
492+
| -------------- | ------ | ---------------------------------------------- |
493+
| `txids` | array | Array of Txids that must be CPFPed |
494+
| `feerate` |float | The new target feerate. |
495+
496+
#### Response
497+
498+
None; the `result` field will be set to the empty object `{}`. Any value should be
499+
disregarded for forward compatibility.
500+
486501

487502
## User flows
488503

src/bitcoind/mod.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ pub mod poller;
33
pub mod utils;
44

55
use crate::config::BitcoindConfig;
6-
use crate::{database::DatabaseError, revaultd::RevaultD, threadmessages::BitcoindMessageOut};
6+
use crate::{
7+
database::{
8+
interface::{db_spend_transaction, db_vault_by_unvault_txid},
9+
DatabaseError,
10+
},
11+
revaultd::RevaultD,
12+
threadmessages::BitcoindMessageOut,
13+
};
714
use interface::{BitcoinD, WalletTransaction};
8-
use poller::poller_main;
15+
use poller::{cpfp_package, poller_main, should_manual_cpfp, ToBeCpfped};
916
use revault_tx::bitcoin::{Network, Txid};
1017

1118
use std::{
@@ -187,6 +194,55 @@ fn wallet_transaction(bitcoind: &BitcoinD, txid: Txid) -> Option<WalletTransacti
187194
.ok()
188195
}
189196

197+
fn cpfp(
198+
revaultd: Arc<RwLock<RevaultD>>,
199+
bitcoind: Arc<RwLock<BitcoinD>>,
200+
txids: Vec<Txid>,
201+
feerate: f64,
202+
) -> Result<(), BitcoindError> {
203+
let db_path = revaultd.read().unwrap().db_file();
204+
assert!(revaultd.read().unwrap().is_manager());
205+
206+
let mut cpfp_txs = Vec::with_capacity(txids.len());
207+
let mut counter = 0;
208+
209+
for txid in txids.iter() {
210+
let spend_tx = db_spend_transaction(&db_path, &txid).expect("Database must be available");
211+
212+
if let Some(unwrap_spend_tx) = spend_tx {
213+
// If the transaction is of type SpendTransaction
214+
if should_manual_cpfp(&bitcoind.read().unwrap(), *txid) {
215+
cpfp_txs.push(ToBeCpfped::Spend(unwrap_spend_tx.psbt));
216+
counter += 1;
217+
}
218+
} else {
219+
let unvault_pair =
220+
db_vault_by_unvault_txid(&db_path, &txid).expect("Database must be available");
221+
let unvault_tx = match unvault_pair {
222+
Some((_vault, tx)) => tx,
223+
None => return Err(BitcoindError::Custom("Unknown Txid.".to_string())),
224+
};
225+
// The transaction type is asserted to be UnvaultTransaction
226+
if should_manual_cpfp(&bitcoind.read().unwrap(), *txid) {
227+
cpfp_txs.push(ToBeCpfped::Unvault(unvault_tx.psbt.assert_unvault()));
228+
counter += 1;
229+
}
230+
}
231+
}
232+
233+
// sats/vbyte -> sats/WU
234+
let sats_wu = feerate / 4.0;
235+
// sats/WU -> msats/WU
236+
let msats_wu = (sats_wu * 1000.0) as u64;
237+
238+
if counter != 0 {
239+
cpfp_package(&revaultd, &bitcoind.read().unwrap(), cpfp_txs, msats_wu)
240+
} else {
241+
log::info!("Nothing to CPFP in the given list.");
242+
Ok(())
243+
}
244+
}
245+
190246
/// The bitcoind event loop.
191247
/// Listens for bitcoind requests (wallet / chain) and poll bitcoind every 30 seconds,
192248
/// updating our state accordingly.
@@ -208,7 +264,8 @@ pub fn bitcoind_main_loop(
208264
let _bitcoind = bitcoind.clone();
209265
let _sync_progress = sync_progress.clone();
210266
let _shutdown = shutdown.clone();
211-
move || poller_main(revaultd, _bitcoind, _sync_progress, _shutdown)
267+
let _revaultd = revaultd.clone();
268+
move || poller_main(_revaultd, _bitcoind, _sync_progress, _shutdown)
212269
});
213270

214271
for msg in rx {
@@ -252,6 +309,16 @@ pub fn bitcoind_main_loop(
252309
))
253310
})?;
254311
}
312+
BitcoindMessageOut::CPFPTransaction(txids, feerate, resp_tx) => {
313+
log::trace!("Received 'cpfptransaction' from main thread");
314+
315+
resp_tx
316+
.send(cpfp(revaultd, bitcoind, txids, feerate))
317+
.map_err(|e| {
318+
BitcoindError::Custom(format!("Sending transaction for CPFP: {}", e))
319+
})?;
320+
return Ok(());
321+
}
255322
}
256323
}
257324

src/bitcoind/poller.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ fn mark_confirmed_emers(
408408
Ok(())
409409
}
410410

411-
enum ToBeCpfped {
411+
pub enum ToBeCpfped {
412412
Spend(SpendTransaction),
413413
Unvault(UnvaultTransaction),
414414
}
@@ -450,7 +450,7 @@ impl ToBeCpfped {
450450
// CPFP a bunch of transactions, bumping their feerate by at least `target_feerate`.
451451
// `target_feerate` is expressed in sat/kWU.
452452
// All the transactions' feerate MUST be below `target_feerate`.
453-
fn cpfp_package(
453+
pub fn cpfp_package(
454454
revaultd: &Arc<RwLock<RevaultD>>,
455455
bitcoind: &BitcoinD,
456456
to_be_cpfped: Vec<ToBeCpfped>,
@@ -540,6 +540,13 @@ fn cpfp_package(
540540
Ok(())
541541
}
542542

543+
pub fn should_manual_cpfp(bitcoind: &BitcoinD, txid: Txid) -> bool {
544+
bitcoind
545+
.get_wallet_transaction(&txid)
546+
.map(|w| w.blockheight.is_none())
547+
.unwrap_or(true)
548+
}
549+
//
543550
// `target_feerate` is in sats/kWU
544551
fn should_cpfp(bitcoind: &BitcoinD, tx: &impl CpfpableTransaction, target_feerate: u64) -> bool {
545552
bitcoind

src/commands/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,23 @@ impl DaemonControl {
14431443
let revaultd = self.revaultd.read().unwrap();
14441444
gethistory(&revaultd, &self.bitcoind_conn, start, end, limit, kind)
14451445
}
1446+
1447+
/// Manually trigger a CPFP for the given transaction ID.
1448+
///
1449+
/// ## Errors
1450+
/// - we don't have access to a CPFP private key
1451+
/// - the caller is not a manager
1452+
pub fn manual_cpfp(&self, txids: &Vec<Txid>, feerate: f64) -> Result<(), CommandError> {
1453+
let revaultd = self.revaultd.read().unwrap();
1454+
1455+
if revaultd.cpfp_key.is_none() {
1456+
return Err(CommandError::MissingCpfpKey);
1457+
}
1458+
manager_only!(revaultd);
1459+
1460+
self.bitcoind_conn.cpfp_tx(txids.to_vec(), feerate)?;
1461+
Ok(())
1462+
}
14461463
}
14471464

14481465
/// Descriptors the daemon was configured with

src/jsonrpc/api.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ pub trait RpcApi {
214214
end: u32,
215215
limit: u64,
216216
) -> jsonrpc_core::Result<serde_json::Value>;
217+
218+
// Manually cpfp the given transaction id.
219+
#[rpc(meta, name = "cpfp")]
220+
fn cpfp(
221+
&self,
222+
meta: Self::Metadata,
223+
txids: Vec<Txid>,
224+
feerate: f64,
225+
) -> jsonrpc_core::Result<serde_json::Value>;
217226
}
218227

219228
macro_rules! parse_vault_status {
@@ -301,6 +310,10 @@ impl RpcApi for RpcImpl {
301310
"emergency": [
302311

303312
],
313+
"cpfp": [
314+
"txids",
315+
"feerate",
316+
],
304317
}))
305318
}
306319

@@ -513,4 +526,16 @@ impl RpcApi for RpcImpl {
513526
"events": events,
514527
}))
515528
}
529+
530+
// manual CPFP command
531+
// feerate will be in sat/vbyte
532+
fn cpfp(
533+
&self,
534+
meta: Self::Metadata,
535+
txids: Vec<Txid>,
536+
feerate: f64,
537+
) -> jsonrpc_core::Result<serde_json::Value> {
538+
meta.daemon_control.manual_cpfp(&txids, feerate)?;
539+
Ok(json!({}))
540+
}
516541
}

src/threadmessages.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub enum BitcoindMessageOut {
4141
Vec<BitcoinTransaction>,
4242
SyncSender<Result<(), BitcoindError>>,
4343
),
44+
CPFPTransaction(Vec<Txid>, f64, SyncSender<Result<(), BitcoindError>>),
4445
}
4546

4647
/// Interface to communicate with bitcoind client thread.
@@ -49,6 +50,7 @@ pub trait BitcoindThread {
4950
fn broadcast(&self, transactions: Vec<BitcoinTransaction>) -> Result<(), BitcoindError>;
5051
fn shutdown(&self);
5152
fn sync_progress(&self) -> f64;
53+
fn cpfp_tx(&self, txids: Vec<Txid>, feerate: f64) -> Result<(), BitcoindError>;
5254
}
5355

5456
/// Interface to the bitcoind thread using synchronous MPSCs
@@ -98,6 +100,18 @@ impl<'a> BitcoindThread for BitcoindSender {
98100

99101
bitrep_rx.recv().expect("Receiving from bitcoind thread")
100102
}
103+
104+
fn cpfp_tx(&self, txids: Vec<Txid>, feerate: f64) -> Result<(), BitcoindError> {
105+
let (bitrep_tx, bitrep_rx) = sync_channel(0);
106+
self.0
107+
.send(BitcoindMessageOut::CPFPTransaction(
108+
txids, feerate, bitrep_tx,
109+
))
110+
.expect("Sending to bitcoind thread");
111+
bitrep_rx.recv().expect("Receiving from bitcoind thread")?;
112+
113+
Ok(())
114+
}
101115
}
102116

103117
impl From<Sender<BitcoindMessageOut>> for BitcoindSender {

src/utils.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,5 +178,8 @@ addr = "127.0.0.1:8332"
178178
fn sync_progress(&self) -> f64 {
179179
1.0
180180
}
181+
fn cpfp_tx(&self, _txid: Vec<Txid>, _feerate: f64) -> Result<(), BitcoindError> {
182+
Ok(())
183+
}
181184
}
182185
}

tests/servers/cosignerd

tests/servers/miradord

0 commit comments

Comments
 (0)