Skip to content

Commit e0ac86a

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

File tree

12 files changed

+478
-43
lines changed

12 files changed

+478
-43
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: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@ 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+
commands::CommandError,
8+
database::{
9+
interface::{db_spend_transaction, db_unvault_transaction_by_txid},
10+
DatabaseError,
11+
},
12+
revaultd::RevaultD,
13+
threadmessages::BitcoindMessageOut,
14+
};
715
use interface::{BitcoinD, WalletTransaction};
8-
use poller::poller_main;
16+
use poller::{cpfp_package, poller_main, should_cpfp, ToBeCpfped};
917
use revault_tx::bitcoin::{Network, Txid};
1018

1119
use std::{
@@ -187,6 +195,61 @@ fn wallet_transaction(bitcoind: &BitcoinD, txid: Txid) -> Option<WalletTransacti
187195
.ok()
188196
}
189197

198+
fn cpfp(
199+
revaultd: Arc<RwLock<RevaultD>>,
200+
bitcoind: Arc<RwLock<BitcoinD>>,
201+
txids: Vec<Txid>,
202+
feerate: f64,
203+
) -> Result<Vec<Txid>, CommandError> {
204+
let db_path = revaultd.read().unwrap().db_file();
205+
assert!(revaultd.read().unwrap().is_manager());
206+
207+
let mut cpfp_txs = Vec::with_capacity(txids.len());
208+
let mut cpfp_txids = Vec::with_capacity(txids.len());
209+
210+
// sats/vbyte -> sats/WU
211+
let sats_wu = feerate / 4.0;
212+
213+
// sats/WU -> sats/kWU
214+
let sats_kwu = (sats_wu * 1000.0) as u64;
215+
216+
for txid in txids.iter() {
217+
let spend_tx = db_spend_transaction(&db_path, &txid).expect("Database must be available");
218+
219+
if let Some(unwrap_spend_tx) = spend_tx {
220+
// If the transaction is of type SpendTransaction
221+
let psbt = unwrap_spend_tx.psbt;
222+
if should_cpfp(&bitcoind.read().unwrap(), &psbt, sats_kwu) {
223+
cpfp_txs.push(ToBeCpfped::Spend(psbt));
224+
cpfp_txids.push(txid.clone());
225+
}
226+
} else {
227+
let unvault_tx = match db_unvault_transaction_by_txid(&db_path, &txid)
228+
.expect("Database must be available")
229+
{
230+
Some(tx) => tx,
231+
None => return Err(CommandError::InvalidParams("Unknown Txid.".to_string())),
232+
};
233+
// The transaction type is asserted to be UnvaultTransaction
234+
let psbt = unvault_tx.psbt.assert_unvault();
235+
if should_cpfp(&bitcoind.read().unwrap(), &psbt, sats_kwu) {
236+
cpfp_txs.push(ToBeCpfped::Unvault(psbt));
237+
cpfp_txids.push(txid.clone());
238+
}
239+
}
240+
}
241+
242+
if cpfp_txids.len() > 0 {
243+
match cpfp_package(&revaultd, &bitcoind.read().unwrap(), cpfp_txs, sats_kwu) {
244+
Err(err) => return Err(CommandError::Bitcoind(err)),
245+
Ok(txids) => return Ok(txids),
246+
}
247+
} else {
248+
log::info!("Nothing to CPFP in the given list.");
249+
}
250+
Ok(cpfp_txids)
251+
}
252+
190253
/// The bitcoind event loop.
191254
/// Listens for bitcoind requests (wallet / chain) and poll bitcoind every 30 seconds,
192255
/// updating our state accordingly.
@@ -208,7 +271,8 @@ pub fn bitcoind_main_loop(
208271
let _bitcoind = bitcoind.clone();
209272
let _sync_progress = sync_progress.clone();
210273
let _shutdown = shutdown.clone();
211-
move || poller_main(revaultd, _bitcoind, _sync_progress, _shutdown)
274+
let _revaultd = revaultd.clone();
275+
move || poller_main(_revaultd, _bitcoind, _sync_progress, _shutdown)
212276
});
213277

214278
for msg in rx {
@@ -252,6 +316,23 @@ pub fn bitcoind_main_loop(
252316
))
253317
})?;
254318
}
319+
BitcoindMessageOut::CPFPTransaction(txids, feerate, resp_tx) => {
320+
log::trace!("Received 'cpfptransaction' from main thread");
321+
322+
let _bitcoind = bitcoind.clone();
323+
let _revaultd = revaultd.clone();
324+
resp_tx
325+
.send(
326+
cpfp(_revaultd, _bitcoind, txids, feerate)
327+
.map(|_v| {})
328+
.map_err(|e| {
329+
BitcoindError::Custom(format!("Error CPFPing transactions: {}", e))
330+
}),
331+
)
332+
.map_err(|e| {
333+
BitcoindError::Custom(format!("Sending transaction for CPFP: {}", e))
334+
})?;
335+
}
255336
}
256337
}
257338

src/bitcoind/poller.rs

Lines changed: 39 additions & 31 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
}
@@ -447,18 +447,17 @@ impl ToBeCpfped {
447447
}
448448
}
449449

450-
// CPFP a bunch of transactions, bumping their feerate by at least `target_feerate`.
451-
// `target_feerate` is expressed in sat/kWU.
452-
// All the transactions' feerate MUST be below `target_feerate`.
453-
fn cpfp_package(
450+
/// CPFP a bunch of transactions, bumping their feerate by at least `target_feerate`.
451+
/// `target_feerate` is expressed in sat/kWU.
452+
/// All the transactions' feerate MUST be below `target_feerate`.
453+
pub fn cpfp_package(
454454
revaultd: &Arc<RwLock<RevaultD>>,
455455
bitcoind: &BitcoinD,
456456
to_be_cpfped: Vec<ToBeCpfped>,
457457
target_feerate: u64,
458-
) -> Result<(), BitcoindError> {
458+
) -> Result<Vec<Txid>, BitcoindError> {
459459
let revaultd = revaultd.read().unwrap();
460460
let cpfp_descriptor = &revaultd.cpfp_descriptor;
461-
462461
// First of all, compute all the information we need from the to-be-cpfped transactions.
463462
let mut txids = HashSet::with_capacity(to_be_cpfped.len());
464463
let mut package_weight = 0;
@@ -473,14 +472,13 @@ fn cpfp_package(
473472
Some(txin) => txins.push(txin),
474473
None => {
475474
log::error!("No CPFP txin for tx '{}'", tx.txid());
476-
return Ok(());
475+
return Ok(txids.into_iter().collect());
477476
}
478477
}
479478
}
480479
let tx_feerate = (package_fees.as_sat() * 1_000 / package_weight) as u64; // to sats/kWU
481480
assert!(tx_feerate < target_feerate);
482481
let added_feerate = target_feerate - tx_feerate;
483-
484482
// Then construct the child PSBT
485483
let confirmed_cpfp_utxos: Vec<_> = bitcoind
486484
.list_unspent_cpfp()?
@@ -508,40 +506,43 @@ fn cpfp_package(
508506
Ok(tx) => tx,
509507
Err(TransactionCreationError::InsufficientFunds) => {
510508
// Well, we're poor.
511-
log::error!(
512-
"We wanted to feebump transactions '{:?}', but we don't have enough funds!",
513-
txids
514-
);
515-
return Ok(());
509+
return Err(BitcoindError::RevaultTx(
510+
revault_tx::Error::TransactionCreation(TransactionCreationError::InsufficientFunds),
511+
));
516512
}
517513
Err(e) => {
518-
log::error!("Error while creating CPFP transaction: '{}'", e);
519-
return Ok(());
514+
return Err(BitcoindError::RevaultTx(
515+
revault_tx::Error::TransactionCreation(e),
516+
));
520517
}
521518
};
522-
523519
// Finally, sign and (try to) broadcast the CPFP transaction
524520
let (complete, psbt_signed) = bitcoind.sign_psbt(psbt.psbt())?;
525521
if !complete {
526-
log::error!(
527-
"Bitcoind returned a non-finalized CPFP PSBT: {}",
528-
base64::encode(encode::serialize(&psbt_signed))
529-
);
530-
return Ok(());
522+
return Err(BitcoindError::Custom(
523+
format!(
524+
"Bitcoind returned a non-finalized CPFP PSBT: {}",
525+
base64::encode(encode::serialize(&psbt_signed))
526+
)
527+
.to_string(),
528+
));
531529
}
532-
533530
let final_tx = psbt_signed.extract_tx();
534531
if let Err(e) = bitcoind.broadcast_transaction(&final_tx) {
535-
log::error!("Error broadcasting '{:?}' CPFP tx: {}", txids, e);
536-
} else {
537-
log::info!("CPFPed transactions with ids '{:?}'", txids);
532+
return Err(BitcoindError::Custom(
533+
format!("Error broadcasting '{:?}' CPFP tx: {}", txids, e).to_string(),
534+
));
538535
}
539-
540-
Ok(())
536+
log::info!("CPFPed transactions with ids '{:?}'", txids);
537+
Ok(txids.into_iter().collect())
541538
}
542539

543-
// `target_feerate` is in sats/kWU
544-
fn should_cpfp(bitcoind: &BitcoinD, tx: &impl CpfpableTransaction, target_feerate: u64) -> bool {
540+
/// `target_feerate` is in sats/kWU
541+
pub fn should_cpfp(
542+
bitcoind: &BitcoinD,
543+
tx: &impl CpfpableTransaction,
544+
target_feerate: u64,
545+
) -> bool {
545546
bitcoind
546547
.get_wallet_transaction(&tx.txid())
547548
// In the unlikely (actually, shouldn't happen but hey) case where
@@ -599,7 +600,14 @@ fn maybe_cpfp_txs(
599600
// TODO: std transaction max size check and split
600601
// TODO: smarter RBF (especially opportunistically with the fee delta)
601602
if !to_cpfp.is_empty() {
602-
cpfp_package(revaultd, bitcoind, to_cpfp, current_feerate)?;
603+
match cpfp_package(revaultd, bitcoind, to_cpfp, current_feerate) {
604+
Err(e) => {
605+
log::error!("Error broadcasting CPFP: {}", e);
606+
}
607+
Ok(txids) => {
608+
log::info!("CPFPed transactions with ids '{:?}'", txids);
609+
}
610+
}
603611
} else {
604612
log::debug!("Nothing to CPFP");
605613
}

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/database/interface.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,41 @@ pub fn db_unvault_emer_transaction(
606606
.map(|mut rows| rows.pop())
607607
}
608608

609+
/// Get the Unvault transaction out of an Unvault txid
610+
pub fn db_unvault_transaction_by_txid(
611+
db_path: &Path,
612+
txid: &Txid,
613+
) -> Result<Option<DbTransaction>, DatabaseError> {
614+
Ok(db_query(
615+
db_path,
616+
"SELECT vaults.*, ptx.id, ptx.psbt, ptx.fullysigned FROM presigned_transactions as ptx \
617+
INNER JOIN vaults ON vaults.id = ptx.vault_id \
618+
WHERE ptx.txid = (?1) and type = (?2)",
619+
params![txid.to_vec(), TransactionType::Unvault as u32],
620+
|row| {
621+
let db_vault: DbVault = row.try_into()?;
622+
let offset = 15;
623+
624+
// FIXME: there is probably a more extensible way to implement the from()s so we don't
625+
// have to change all those when adding a column
626+
let id: u32 = row.get(offset)?;
627+
let psbt: Vec<u8> = row.get(offset + 1)?;
628+
let psbt = UnvaultTransaction::from_psbt_serialized(&psbt).expect("We store it");
629+
let is_fully_signed = row.get(offset + 2)?;
630+
let db_tx = DbTransaction {
631+
id,
632+
vault_id: db_vault.id,
633+
tx_type: TransactionType::Unvault,
634+
psbt: RevaultTx::Unvault(psbt),
635+
is_fully_signed,
636+
};
637+
638+
Ok(db_tx)
639+
},
640+
)?
641+
.pop())
642+
}
643+
609644
/// Get a vault and its Unvault transaction out of an Unvault txid
610645
pub fn db_vault_by_unvault_txid(
611646
db_path: &Path,

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
}

0 commit comments

Comments
 (0)