Skip to content

Commit

Permalink
Addition of new RPC command for manual cpfp.
Browse files Browse the repository at this point in the history
Along with tests.
  • Loading branch information
Zshan0 committed Jun 18, 2022
1 parent cd21ab8 commit d4c0832
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 8 deletions.
15 changes: 15 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Note that all addresses are bech32-encoded *version 0* native Segwit `scriptPubK
| [`setspendtx`](#setspendtx) | Announce and broadcast this Spend transaction |
| [`gethistory`](#gethistory) | Retrieve history of funds |
| [`emergency`](#emergency) | Broadcast all Emergency signed transactions |
| [`cpfp`](#cpfp) | Manually trigger the cpfp for transactions. |



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

### `cpfp`

#### Request

| Field | Type | Description |
| -------------- | ------ | ---------------------------------------------- |
| `txids` | array | Array of Txids that must be CPFPed |
| `feerate` |float | The new target feerate. |

#### Response

None; the `result` field will be set to the empty object `{}`. Any value should be
disregarded for forward compatibility.


## User flows

Expand Down
61 changes: 58 additions & 3 deletions src/bitcoind/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ pub mod poller;
pub mod utils;

use crate::config::BitcoindConfig;
use crate::{database::DatabaseError, revaultd::RevaultD, threadmessages::BitcoindMessageOut};
use crate::{
database::{
interface::{db_spend_transaction, db_vault_by_unvault_txid},
DatabaseError,
},
revaultd::RevaultD,
threadmessages::BitcoindMessageOut,
};
use interface::{BitcoinD, WalletTransaction};
use poller::poller_main;
use poller::{cpfp_package, poller_main, ToBeCpfped};
use revault_tx::bitcoin::{Network, Txid};

use std::{
Expand Down Expand Up @@ -187,6 +194,43 @@ fn wallet_transaction(bitcoind: &BitcoinD, txid: Txid) -> Option<WalletTransacti
.ok()
}

fn cpfp(
revaultd: Arc<RwLock<RevaultD>>,
bitcoind: Arc<RwLock<BitcoinD>>,
txids: Vec<Txid>,
feerate: f64,
) -> Result<(), BitcoindError> {
let db_path = revaultd.read().unwrap().db_file();
assert!(revaultd.read().unwrap().is_manager());

let mut cpfp_txs = Vec::with_capacity(txids.len());

for txid in txids.iter() {
let spend_tx = db_spend_transaction(&db_path, &txid).expect("Database must be available");

if let Some(unwrap_spend_tx) = spend_tx {
// If the transaction is of type SpendTransaction
cpfp_txs.push(ToBeCpfped::Spend(unwrap_spend_tx.psbt));
} else {
// The transaction type is asserted to be UnvaultTransaction
let unvault_tx = db_vault_by_unvault_txid(&db_path, &txid)
.expect("Database must be available")
.unwrap()
.1
.psbt
.assert_unvault();
cpfp_txs.push(ToBeCpfped::Unvault(unvault_tx));
}
}

// sats/vbyte -> sats/WU
let sats_wu = feerate / 4.0;
// sats/WU -> msats/WU
let msats_wu = (sats_wu * 1000.0) as u64;

cpfp_package(&revaultd, &bitcoind.read().unwrap(), cpfp_txs, msats_wu)
}

/// The bitcoind event loop.
/// Listens for bitcoind requests (wallet / chain) and poll bitcoind every 30 seconds,
/// updating our state accordingly.
Expand All @@ -208,7 +252,8 @@ pub fn bitcoind_main_loop(
let _bitcoind = bitcoind.clone();
let _sync_progress = sync_progress.clone();
let _shutdown = shutdown.clone();
move || poller_main(revaultd, _bitcoind, _sync_progress, _shutdown)
let _revaultd = revaultd.clone();
move || poller_main(_revaultd, _bitcoind, _sync_progress, _shutdown)
});

for msg in rx {
Expand Down Expand Up @@ -252,6 +297,16 @@ pub fn bitcoind_main_loop(
))
})?;
}
BitcoindMessageOut::CPFPTransaction(txids, feerate, resp_tx) => {
log::trace!("Received 'cpfptransaction' from main thread");

resp_tx
.send(cpfp(revaultd, bitcoind, txids, feerate))
.map_err(|e| {
BitcoindError::Custom(format!("Sending transaction for CPFP: {}", e))
})?;
return Ok(());
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/bitcoind/poller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ fn mark_confirmed_emers(
Ok(())
}

enum ToBeCpfped {
pub enum ToBeCpfped {
Spend(SpendTransaction),
Unvault(UnvaultTransaction),
}
Expand Down Expand Up @@ -450,7 +450,7 @@ impl ToBeCpfped {
// CPFP a bunch of transactions, bumping their feerate by at least `target_feerate`.
// `target_feerate` is expressed in sat/kWU.
// All the transactions' feerate MUST be below `target_feerate`.
fn cpfp_package(
pub fn cpfp_package(
revaultd: &Arc<RwLock<RevaultD>>,
bitcoind: &BitcoinD,
to_be_cpfped: Vec<ToBeCpfped>,
Expand Down
17 changes: 17 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,23 @@ impl DaemonControl {
let revaultd = self.revaultd.read().unwrap();
gethistory(&revaultd, &self.bitcoind_conn, start, end, limit, kind)
}

/// Manually trigger a CPFP for the given transaction ID.
///
/// ## Errors
/// - we don't have access to a CPFP private key
/// - the caller is not a manager
pub fn manual_cpfp(&self, txids: &Vec<Txid>, feerate: f64) -> Result<(), CommandError> {
let revaultd = self.revaultd.read().unwrap();

if revaultd.cpfp_key.is_none() {
return Err(CommandError::MissingCpfpKey);
}
manager_only!(revaultd);

self.bitcoind_conn.cpfp_tx(txids.to_vec(), feerate)?;
Ok(())
}
}

/// Descriptors the daemon was configured with
Expand Down
25 changes: 25 additions & 0 deletions src/jsonrpc/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@ pub trait RpcApi {
end: u32,
limit: u64,
) -> jsonrpc_core::Result<serde_json::Value>;

// Manually cpfp the given transaction id.
#[rpc(meta, name = "cpfp")]
fn cpfp(
&self,
meta: Self::Metadata,
txids: Vec<Txid>,
feerate: f64,
) -> jsonrpc_core::Result<serde_json::Value>;
}

macro_rules! parse_vault_status {
Expand Down Expand Up @@ -301,6 +310,10 @@ impl RpcApi for RpcImpl {
"emergency": [

],
"cpfp": [
"txids",
"feerate",
],
}))
}

Expand Down Expand Up @@ -513,4 +526,16 @@ impl RpcApi for RpcImpl {
"events": events,
}))
}

// manual CPFP command
// feerate will be in sat/vbyte [TODO API.md]
fn cpfp(
&self,
meta: Self::Metadata,
txids: Vec<Txid>,
feerate: f64,
) -> jsonrpc_core::Result<serde_json::Value> {
meta.daemon_control.manual_cpfp(&txids, feerate)?;
Ok(json!({}))
}
}
14 changes: 14 additions & 0 deletions src/threadmessages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub enum BitcoindMessageOut {
Vec<BitcoinTransaction>,
SyncSender<Result<(), BitcoindError>>,
),
CPFPTransaction(Vec<Txid>, f64, SyncSender<Result<(), BitcoindError>>),
}

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

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

bitrep_rx.recv().expect("Receiving from bitcoind thread")
}

fn cpfp_tx(&self, txids: Vec<Txid>, feerate: f64) -> Result<(), BitcoindError> {
let (bitrep_tx, bitrep_rx) = sync_channel(0);
self.0
.send(BitcoindMessageOut::CPFPTransaction(
txids, feerate, bitrep_tx,
))
.expect("Sending to bitcoind thread");
bitrep_rx.recv().expect("Receiving from bitcoind thread")?;

Ok(())
}
}

impl From<Sender<BitcoindMessageOut>> for BitcoindSender {
Expand Down
3 changes: 3 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,8 @@ addr = "127.0.0.1:8332"
fn sync_progress(&self) -> f64 {
1.0
}
fn cpfp_tx(&self, _txid: Vec<Txid>, _feerate: f64) -> Result<(), BitcoindError> {
Ok(())
}
}
}
2 changes: 1 addition & 1 deletion tests/servers/coordinatord
2 changes: 1 addition & 1 deletion tests/servers/cosignerd
Submodule cosignerd updated 2 files
+5 −9 Cargo.lock
+5 −5 Cargo.toml
2 changes: 1 addition & 1 deletion tests/servers/miradord
Submodule miradord updated 2 files
+5 −7 Cargo.lock
+3 −3 Cargo.toml
66 changes: 66 additions & 0 deletions tests/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,3 +1348,69 @@ def test_gethistory(revault_network, bitcoind, executor):
)
== 5
)


COIN = 10**8


def get_unvault_txids(wallet, vaults):
unvault_txids = []
for vault in vaults:
deposit = f"{vault['txid']}:{vault['vout']}"
unvault_psbt = serializations.PSBT()
unvault_b64 = wallet.rpc.listpresignedtransactions([deposit])[
"presigned_transactions"
][0]["unvault"]
unvault_psbt.deserialize(unvault_b64)
unvault_psbt.tx.calc_sha256()
unvault_txids.append(unvault_psbt.tx.hash)
return unvault_txids


@pytest.mark.skipif(not POSTGRES_IS_SETUP, reason="Needs Postgres for servers db")
def test_manual_cpfp(revault_network, bitcoind):
CSV = 12
revault_network.deploy(
2,
1,
csv=CSV,
bitcoind_rpc_mocks={"estimatesmartfee": {"feerate": 0.0005}}, # 50 sats/vbyte
)
man = revault_network.mans()[0]
vaults = revault_network.fundmany([1, 2, 3])

# Broadcast the unvaults and get their txids
for vault in vaults:
revault_network.secure_vault(vault)
revault_network.activate_vault(vault)
revault_network.broadcast_unvaults_anyhow(vaults, priority=True)
unvault_txids = get_unvault_txids(man, vaults)
for w in revault_network.participants():
wait_for(
lambda: len(w.rpc.listvaults(["unvaulting"])["vaults"]) == len(vaults),
)

# If the feerate isn't significantly lower than the estimate, we won't feebump.
# Note the Unvault txs have a fixed 24sat/vb feerate.
entry = bitcoind.rpc.getmempoolentry(unvault_txids[0])
assert int(entry["fees"]["base"] * COIN / entry["vsize"]) == 24
revault_network.bitcoind_proxy.mocks["estimatesmartfee"] = {
"feerate": 26 * 1_000 / COIN
}
bitcoind.generate_blocks_censor(1, unvault_txids)
man.wait_for_logs(["Checking if transactions need CPFP...", "Nothing to CPFP"])

# The feerate is still kept the same and we will trigger CPFP manually using
# rpc call. This should behave in the same way as an automatic CPFP trigger
man.rpc.cpfp(unvault_txids, 50)
revault_network.bitcoind_proxy.mocks["estimatesmartfee"] = {
"feerate": 26 * 1_000 / COIN
}
bitcoind.generate_blocks_censor(1, unvault_txids)
man.wait_for_log("CPFPed transactions")
wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == len(unvault_txids) + 1)
for unvault_txid in unvault_txids:
entry = bitcoind.rpc.getmempoolentry(unvault_txid)
assert entry["descendantcount"] == 2
package_feerate = entry["fees"]["descendant"] * COIN / entry["descendantsize"]
assert package_feerate >= 50

0 comments on commit d4c0832

Please sign in to comment.