Skip to content

Commit a011fea

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

File tree

14 files changed

+572
-120
lines changed

14 files changed

+572
-120
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: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@ 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::{poller_main, should_cpfp, ToBeCpfped};
917
use revault_tx::bitcoin::{Network, Txid};
18+
use utils::cpfp_package;
1019

1120
use std::{
1221
sync::{
@@ -187,6 +196,61 @@ fn wallet_transaction(bitcoind: &BitcoinD, txid: Txid) -> Option<WalletTransacti
187196
.ok()
188197
}
189198

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

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

src/bitcoind/poller.rs

Lines changed: 18 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
bitcoind::{
44
interface::{BitcoinD, DepositsState, SyncInfo, UnvaultsState, UtxoInfo},
55
utils::{
6-
cancel_txids, emer_txid, populate_deposit_cache, populate_unvaults_cache,
6+
cancel_txids, cpfp_package, emer_txid, populate_deposit_cache, populate_unvaults_cache,
77
presigned_transactions, unemer_txid, unvault_txid, unvault_txin_from_deposit,
88
vault_deposit_utxo,
99
},
@@ -31,20 +31,16 @@ use crate::{
3131
revaultd::{BlockchainTip, RevaultD, VaultStatus},
3232
};
3333
use revault_tx::{
34-
bitcoin::{consensus::encode, secp256k1, Amount, OutPoint, Txid},
35-
error::TransactionCreationError,
34+
bitcoin::{secp256k1, Amount, OutPoint, Txid},
3635
miniscript::descriptor::{DescriptorSecretKey, DescriptorXKey, KeyMap, Wildcard},
3736
scripts::CpfpDescriptor,
38-
transactions::{
39-
CpfpTransaction, CpfpableTransaction, RevaultTransaction, SpendTransaction,
40-
UnvaultTransaction,
41-
},
37+
transactions::{CpfpableTransaction, RevaultTransaction, SpendTransaction, UnvaultTransaction},
4238
txins::{CpfpTxIn, RevaultTxIn},
43-
txouts::{CpfpTxOut, RevaultTxOut},
39+
txouts::RevaultTxOut,
4440
};
4541

4642
use std::{
47-
collections::{HashMap, HashSet},
43+
collections::HashMap,
4844
path::{Path, PathBuf},
4945
sync::{
5046
atomic::{AtomicBool, Ordering},
@@ -408,7 +404,7 @@ fn mark_confirmed_emers(
408404
Ok(())
409405
}
410406

411-
enum ToBeCpfped {
407+
pub enum ToBeCpfped {
412408
Spend(SpendTransaction),
413409
Unvault(UnvaultTransaction),
414410
}
@@ -447,101 +443,12 @@ impl ToBeCpfped {
447443
}
448444
}
449445

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(
454-
revaultd: &Arc<RwLock<RevaultD>>,
446+
/// `target_feerate` is in sats/kWU
447+
pub fn should_cpfp(
455448
bitcoind: &BitcoinD,
456-
to_be_cpfped: Vec<ToBeCpfped>,
449+
tx: &impl CpfpableTransaction,
457450
target_feerate: u64,
458-
) -> Result<(), BitcoindError> {
459-
let revaultd = revaultd.read().unwrap();
460-
let cpfp_descriptor = &revaultd.cpfp_descriptor;
461-
462-
// First of all, compute all the information we need from the to-be-cpfped transactions.
463-
let mut txids = HashSet::with_capacity(to_be_cpfped.len());
464-
let mut package_weight = 0;
465-
let mut package_fees = Amount::from_sat(0);
466-
let mut txins = Vec::with_capacity(to_be_cpfped.len());
467-
for tx in to_be_cpfped.iter() {
468-
txids.insert(tx.txid());
469-
package_weight += tx.max_weight();
470-
package_fees += tx.fees();
471-
assert!(((package_fees.as_sat() * 1000 / package_weight) as u64) < target_feerate);
472-
match tx.cpfp_txin(cpfp_descriptor, &revaultd.secp_ctx) {
473-
Some(txin) => txins.push(txin),
474-
None => {
475-
log::error!("No CPFP txin for tx '{}'", tx.txid());
476-
return Ok(());
477-
}
478-
}
479-
}
480-
let tx_feerate = (package_fees.as_sat() * 1_000 / package_weight) as u64; // to sats/kWU
481-
assert!(tx_feerate < target_feerate);
482-
let added_feerate = target_feerate - tx_feerate;
483-
484-
// Then construct the child PSBT
485-
let confirmed_cpfp_utxos: Vec<_> = bitcoind
486-
.list_unspent_cpfp()?
487-
.into_iter()
488-
.filter_map(|l| {
489-
// Not considering our own outputs nor UTXOs still in mempool
490-
if txids.contains(&l.outpoint.txid) || l.confirmations < 1 {
491-
None
492-
} else {
493-
let txout = CpfpTxOut::new(
494-
Amount::from_sat(l.txo.value),
495-
&revaultd.derived_cpfp_descriptor(l.derivation_index.expect("Must be here")),
496-
);
497-
Some(CpfpTxIn::new(l.outpoint, txout))
498-
}
499-
})
500-
.collect();
501-
let psbt = match CpfpTransaction::from_txins(
502-
txins,
503-
package_weight,
504-
package_fees,
505-
added_feerate,
506-
confirmed_cpfp_utxos,
507-
) {
508-
Ok(tx) => tx,
509-
Err(TransactionCreationError::InsufficientFunds) => {
510-
// 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(());
516-
}
517-
Err(e) => {
518-
log::error!("Error while creating CPFP transaction: '{}'", e);
519-
return Ok(());
520-
}
521-
};
522-
523-
// Finally, sign and (try to) broadcast the CPFP transaction
524-
let (complete, psbt_signed) = bitcoind.sign_psbt(psbt.psbt())?;
525-
if !complete {
526-
log::error!(
527-
"Bitcoind returned a non-finalized CPFP PSBT: {}",
528-
base64::encode(encode::serialize(&psbt_signed))
529-
);
530-
return Ok(());
531-
}
532-
533-
let final_tx = psbt_signed.extract_tx();
534-
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);
538-
}
539-
540-
Ok(())
541-
}
542-
543-
// `target_feerate` is in sats/kWU
544-
fn should_cpfp(bitcoind: &BitcoinD, tx: &impl CpfpableTransaction, target_feerate: u64) -> bool {
451+
) -> bool {
545452
bitcoind
546453
.get_wallet_transaction(&tx.txid())
547454
// In the unlikely (actually, shouldn't happen but hey) case where
@@ -599,7 +506,14 @@ fn maybe_cpfp_txs(
599506
// TODO: std transaction max size check and split
600507
// TODO: smarter RBF (especially opportunistically with the fee delta)
601508
if !to_cpfp.is_empty() {
602-
cpfp_package(revaultd, bitcoind, to_cpfp, current_feerate)?;
509+
match cpfp_package(revaultd, bitcoind, to_cpfp, current_feerate) {
510+
Err(e) => {
511+
log::error!("Error broadcasting CPFP: {}", e);
512+
}
513+
Ok(txids) => {
514+
log::info!("CPFPed transactions with ids '{:?}'", txids);
515+
}
516+
}
603517
} else {
604518
log::debug!("Nothing to CPFP");
605519
}

0 commit comments

Comments
 (0)