Skip to content

Commit cdc6c11

Browse files
committed
Addition of new RPC command along for manual cpfp.
Along with tests.
1 parent b47212b commit cdc6c11

File tree

10 files changed

+227
-8
lines changed

10 files changed

+227
-8
lines changed

src/bitcoind/mod.rs

Lines changed: 64 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, ToBeCpfped};
916
use revault_tx::bitcoin::{Network, Txid};
1017

1118
use std::{
@@ -187,6 +194,47 @@ 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+
208+
for txid in txids.iter() {
209+
log::debug!("[ZEE] looping over txids {}", txid);
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+
log::debug!("[ZEE] spend_tx {:?}", unwrap_spend_tx);
215+
cpfp_txs.push(ToBeCpfped::Spend(unwrap_spend_tx.psbt));
216+
} else {
217+
// If not SpendTransaction then it must be UnvaultTransaction
218+
let unvault_tx = db_vault_by_unvault_txid(&db_path, &txid)
219+
.expect("Database must be available")
220+
.unwrap()
221+
.1
222+
.psbt
223+
.unwrap_unvault()
224+
.clone();
225+
log::debug!("[ZEE] unvault_tx {:?}", unvault_tx);
226+
cpfp_txs.push(ToBeCpfped::Unvault(unvault_tx));
227+
}
228+
}
229+
230+
// sats/vbyte -> sats/WU
231+
let sats_wu = feerate / 4.0;
232+
// sats/WU -> msats/WU
233+
let msats_wu = (sats_wu * 1000.0) as u64;
234+
235+
cpfp_package(&revaultd, &bitcoind.read().unwrap(), cpfp_txs, msats_wu)
236+
}
237+
190238
/// The bitcoind event loop.
191239
/// Listens for bitcoind requests (wallet / chain) and poll bitcoind every 30 seconds,
192240
/// updating our state accordingly.
@@ -208,7 +256,8 @@ pub fn bitcoind_main_loop(
208256
let _bitcoind = bitcoind.clone();
209257
let _sync_progress = sync_progress.clone();
210258
let _shutdown = shutdown.clone();
211-
move || poller_main(revaultd, _bitcoind, _sync_progress, _shutdown)
259+
let _revaultd = revaultd.clone();
260+
move || poller_main(_revaultd, _bitcoind, _sync_progress, _shutdown)
212261
});
213262

214263
for msg in rx {
@@ -252,6 +301,18 @@ pub fn bitcoind_main_loop(
252301
))
253302
})?;
254303
}
304+
BitcoindMessageOut::CPFPTransaction(txids, feerate, resp_tx) => {
305+
log::trace!("Received 'cpfptransaction' from main thread");
306+
log::debug!("[ZEE] Received 'cpfptransaction' from main thread");
307+
308+
assert!(revaultd.read().unwrap().is_manager());
309+
resp_tx
310+
.send(cpfp(revaultd, bitcoind, txids, feerate))
311+
.map_err(|e| {
312+
BitcoindError::Custom(format!("Sending transaction for CPFP: {}", e))
313+
})?;
314+
return Ok(());
315+
}
255316
}
256317
}
257318

src/bitcoind/poller.rs

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

411-
enum ToBeCpfped {
411+
#[derive(Debug)]
412+
pub enum ToBeCpfped {
412413
Spend(SpendTransaction),
413414
Unvault(UnvaultTransaction),
414415
}
@@ -450,7 +451,7 @@ impl ToBeCpfped {
450451
// CPFP a bunch of transactions, bumping their feerate by at least `target_feerate`.
451452
// `target_feerate` is expressed in sat/kWU.
452453
// All the transactions' feerate MUST be below `target_feerate`.
453-
fn cpfp_package(
454+
pub fn cpfp_package(
454455
revaultd: &Arc<RwLock<RevaultD>>,
455456
bitcoind: &BitcoinD,
456457
to_be_cpfped: Vec<ToBeCpfped>,
@@ -459,6 +460,11 @@ fn cpfp_package(
459460
let revaultd = revaultd.read().unwrap();
460461
let cpfp_descriptor = &revaultd.cpfp_descriptor;
461462

463+
log::debug!(
464+
"[ZEE] inside cpfp_package {:?} {:?}",
465+
to_be_cpfped,
466+
target_feerate
467+
);
462468
// First of all, compute all the information we need from the to-be-cpfped transactions.
463469
let mut txids = HashSet::with_capacity(to_be_cpfped.len());
464470
let mut package_weight = 0;

src/commands/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,24 @@ impl DaemonControl {
14211421
let revaultd = self.revaultd.read().unwrap();
14221422
gethistory(&revaultd, &self.bitcoind_conn, start, end, limit, kind)
14231423
}
1424+
1425+
/// Manually trigger a CPFP for the given transaction ID.
1426+
///
1427+
/// ## Errors
1428+
/// - we don't have access to a CPFP private key
1429+
/// - the caller is not a manager
1430+
pub fn manual_cpfp(&self, txids: &Vec<Txid>, feerate: f64) -> Result<(), CommandError> {
1431+
let revaultd = self.revaultd.read().unwrap();
1432+
1433+
if revaultd.cpfp_key.is_none() {
1434+
return Err(CommandError::MissingCpfpKey);
1435+
}
1436+
manager_only!(revaultd);
1437+
1438+
log::debug!("[ZEE] Triggering bitcoind_conn.cpfp_tx from main thread.");
1439+
self.bitcoind_conn.cpfp_tx(txids.to_vec(), feerate)?;
1440+
Ok(())
1441+
}
14241442
}
14251443

14261444
/// Descriptors the daemon was configured with

src/jsonrpc/api.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,23 @@ 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>;
226+
227+
#[rpc(meta, name = "ccpfp")]
228+
fn ccpfp(
229+
&self,
230+
meta: Self::Metadata,
231+
txids: Vec<Txid>,
232+
feerate: u64,
233+
) -> jsonrpc_core::Result<serde_json::Value>;
217234
}
218235

219236
macro_rules! parse_vault_status {
@@ -301,6 +318,10 @@ impl RpcApi for RpcImpl {
301318
"emergency": [
302319

303320
],
321+
"cpfp": [
322+
"txids",
323+
"feerate",
324+
],
304325
}))
305326
}
306327

@@ -513,4 +534,34 @@ impl RpcApi for RpcImpl {
513534
"events": events,
514535
}))
515536
}
537+
538+
// manual CPFP command
539+
// feerate will be in sat/vbyte [TODO API.md]
540+
fn cpfp(
541+
&self,
542+
meta: Self::Metadata,
543+
txids: Vec<Txid>,
544+
feerate: f64,
545+
) -> jsonrpc_core::Result<serde_json::Value> {
546+
log::debug!(
547+
"[ZEE] Triggering manual_cpfp from jsonrpc/api.rs. {:?} {}",
548+
txids,
549+
feerate
550+
);
551+
meta.daemon_control.manual_cpfp(&txids, feerate)?;
552+
Ok(json!({}))
553+
}
554+
fn ccpfp(
555+
&self,
556+
_: Self::Metadata,
557+
txids: Vec<Txid>,
558+
feerate: u64,
559+
) -> jsonrpc_core::Result<serde_json::Value> {
560+
log::debug!(
561+
"Triggering ccpfp from jsonrpc/api.rs. {:?} {}",
562+
txids,
563+
feerate
564+
);
565+
Ok(json!({}))
566+
}
516567
}

src/threadmessages.rs

Lines changed: 15 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,19 @@ 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+
log::debug!("[ZEE] Sending 'cpfptransaction' from main thread");
106+
let (bitrep_tx, bitrep_rx) = sync_channel(0);
107+
self.0
108+
.send(BitcoindMessageOut::CPFPTransaction(
109+
txids, feerate, bitrep_tx,
110+
))
111+
.expect("Sending to bitcoind thread");
112+
bitrep_rx.recv().expect("Receiving from bitcoind thread")?;
113+
114+
Ok(())
115+
}
101116
}
102117

103118
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

tests/test_rpc.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,3 +1348,68 @@ def test_gethistory(revault_network, bitcoind, executor):
13481348
)
13491349
== 5
13501350
)
1351+
1352+
1353+
COIN = 10**8
1354+
1355+
1356+
def get_unvault_txids(wallet, vaults):
1357+
unvault_txids = []
1358+
for vault in vaults:
1359+
deposit = f"{vault['txid']}:{vault['vout']}"
1360+
unvault_psbt = serializations.PSBT()
1361+
unvault_b64 = wallet.rpc.listpresignedtransactions([deposit])[
1362+
"presigned_transactions"
1363+
][0]["unvault"]
1364+
unvault_psbt.deserialize(unvault_b64)
1365+
unvault_psbt.tx.calc_sha256()
1366+
unvault_txids.append(unvault_psbt.tx.hash)
1367+
return unvault_txids
1368+
1369+
1370+
@pytest.mark.skipif(not POSTGRES_IS_SETUP, reason="Needs Postgres for servers db")
1371+
def test_manual_cpfp(revault_network, bitcoind):
1372+
CSV = 12
1373+
revault_network.deploy(
1374+
2,
1375+
1,
1376+
csv=CSV,
1377+
bitcoind_rpc_mocks={"estimatesmartfee": {"feerate": 0.0005}}, # 50 sats/vbyte
1378+
)
1379+
man = revault_network.mans()[0]
1380+
vaults = revault_network.fundmany([1, 2, 3])
1381+
1382+
# Broadcast the unvaults and get their txids
1383+
for vault in vaults:
1384+
revault_network.secure_vault(vault)
1385+
revault_network.activate_vault(vault)
1386+
spend_psbt = revault_network.broadcast_unvaults_anyhow(vaults, priority=True)
1387+
unvault_txids = get_unvault_txids(man, vaults)
1388+
spend_txid = spend_psbt.tx.hash
1389+
for w in revault_network.participants():
1390+
wait_for(
1391+
lambda: len(w.rpc.listvaults(["unvaulting"])["vaults"]) == len(vaults),
1392+
)
1393+
1394+
# If the feerate isn't significantly lower than the estimate, we won't feebump.
1395+
# Note the Unvault txs have a fixed 24sat/vb feerate.
1396+
entry = bitcoind.rpc.getmempoolentry(unvault_txids[0])
1397+
assert int(entry["fees"]["base"] * COIN / entry["vsize"]) == 24
1398+
revault_network.bitcoind_proxy.mocks["estimatesmartfee"] = {
1399+
"feerate": 26 * 1_000 / COIN
1400+
}
1401+
bitcoind.generate_blocks_censor(1, unvault_txids)
1402+
man.wait_for_logs(["Checking if transactions need CPFP...", "Nothing to CPFP"])
1403+
1404+
# Now if we set a high-enough target feerate, this'll trigger the CPFP.
1405+
revault_network.bitcoind_proxy.mocks["estimatesmartfee"] = {
1406+
"feerate": 50 * 1_000 / COIN
1407+
}
1408+
bitcoind.generate_blocks_censor(1, unvault_txids)
1409+
man.wait_for_log("CPFPed transactions")
1410+
wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == len(unvault_txids) + 1)
1411+
for unvault_txid in unvault_txids:
1412+
entry = bitcoind.rpc.getmempoolentry(unvault_txid)
1413+
assert entry["descendantcount"] == 2
1414+
package_feerate = entry["fees"]["descendant"] * COIN / entry["descendantsize"]
1415+
assert package_feerate >= 50

0 commit comments

Comments
 (0)