Skip to content

Commit

Permalink
Import CPFP descriptor into Core and...
Browse files Browse the repository at this point in the history
...cpfp_secret in revaultd

The CPFP private key can be stored in a file called
`cpfp_secret` in the datadir. If the `cpfp_secret` file
is not found, CPFP will be disabled. If it's found with an
invalid key or a key not present in the descriptor, we error.
The CPFP descriptor is imported directly in Bitcoin Core,
as the revault_tx update modifies it to be a `multi`. This
commit introduces code for creating a new wallet, and some
functions for querying it.
We also create wallets with blank=true, as we don't want them to
have private keys.
This commit also renames the `KeyError` enum to `NoiseKeyError`,
and moves the "Not a fresh wallet..." debug message out of the loop,
so we don't spam the logs.
  • Loading branch information
danielabrozzoni committed Dec 10, 2021
1 parent 18494c6 commit eda096d
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 20 deletions.
73 changes: 64 additions & 9 deletions src/daemon/bitcoind/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ const RPC_SOCKET_TIMEOUT: u64 = 180;
// Labels used to tag utxos in the watchonly wallet
const DEPOSIT_UTXOS_LABEL: &str = "revault-deposit";
const UNVAULT_UTXOS_LABEL: &str = "revault-unvault";
const CPFP_UTXOS_LABEL: &str = "revault-cpfp";

pub struct BitcoinD {
node_client: Client,
watchonly_client: Client,
cpfp_client: Client,
}

macro_rules! params {
Expand All @@ -53,6 +55,7 @@ impl BitcoinD {
pub fn new(
config: &BitcoindConfig,
watchonly_wallet_path: String,
cpfp_wallet_path: String,
) -> Result<BitcoinD, BitcoindError> {
let cookie_string = fs::read_to_string(&config.cookie_path).map_err(|e| {
BitcoindError::Custom(format!("Reading cookie file: {}", e.to_string()))
Expand All @@ -67,10 +70,20 @@ impl BitcoinD {
.build(),
);

let url = format!("http://{}/wallet/{}", config.addr, watchonly_wallet_path);
let watchonly_url = format!("http://{}/wallet/{}", config.addr, watchonly_wallet_path);
let watchonly_client = Client::with_transport(
SimpleHttpTransport::builder()
.url(&url)
.url(&watchonly_url)
.map_err(BitcoindError::from)?
.timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT))
.cookie_auth(cookie_string.clone())
.build(),
);

let cpfp_url = format!("http://{}/wallet/{}", config.addr, cpfp_wallet_path);
let cpfp_client = Client::with_transport(
SimpleHttpTransport::builder()
.url(&cpfp_url)
.map_err(BitcoindError::from)?
.timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT))
.cookie_auth(cookie_string)
Expand All @@ -80,6 +93,7 @@ impl BitcoinD {
Ok(BitcoinD {
node_client,
watchonly_client,
cpfp_client,
})
}

Expand Down Expand Up @@ -230,6 +244,14 @@ impl BitcoinD {
self.make_requests(&self.node_client, requests)
}

fn make_cpfp_request<'a, 'b>(
&self,
method: &'a str,
params: &'b [Box<serde_json::value::RawValue>],
) -> Result<Json, BitcoindError> {
self.make_request(&self.cpfp_client, method, params)
}

pub fn getblockchaininfo(&self) -> Result<Json, BitcoindError> {
self.make_node_request("getblockchaininfo", &[])
}
Expand Down Expand Up @@ -297,13 +319,17 @@ impl BitcoinD {
})
}

pub fn createwallet_startup(&self, wallet_path: String) -> Result<(), BitcoindError> {
pub fn createwallet_startup(
&self,
wallet_path: String,
watchonly: bool,
) -> Result<(), BitcoindError> {
let res = self.make_node_request(
"createwallet",
&params!(
Json::String(wallet_path),
Json::Bool(true), // watchonly
Json::Bool(false), // blank
Json::Bool(watchonly), // watchonly
Json::Bool(true), // blank
Json::String("".to_string()), // passphrase,
Json::Bool(false), // avoid_reuse
Json::Bool(true), // descriptors
Expand Down Expand Up @@ -406,11 +432,17 @@ impl BitcoinD {

fn bulk_import_descriptors(
&self,
client: &Client,
descriptors: Vec<String>,
timestamp: u32,
label: String,
fresh_wallet: bool,
active: bool,
) -> Result<(), BitcoindError> {
if !fresh_wallet {
log::debug!("Not a fresh wallet, rescan *may* take some time.");
}

let all_descriptors: Vec<Json> = descriptors
.into_iter()
.map(|desc| {
Expand All @@ -423,25 +455,28 @@ impl BitcoinD {
if fresh_wallet {
Json::String("now".to_string())
} else {
log::debug!("Not a fresh wallet, rescan *may* take some time.");
Json::Number(serde_json::Number::from(timestamp))
},
);
desc_map.insert("label".to_string(), Json::String(label.clone()));
desc_map.insert("active".to_string(), Json::Bool(active));

Json::Object(desc_map)
})
.collect();

let res = self
.make_watchonly_request("importdescriptors", &params!(Json::Array(all_descriptors)))?;
let res = self.make_request(
&client,
"importdescriptors",
&params!(Json::Array(all_descriptors)),
)?;
if res.get(0).map(|x| x.get("success")) == Some(Some(&Json::Bool(true))) {
return Ok(());
}

Err(BitcoindError::Custom(format!(
"Error returned from 'importdescriptor': {:?}",
res.get("error")
res.get(0).map(|r| r.get("error"))
)))
}

Expand All @@ -452,10 +487,12 @@ impl BitcoinD {
fresh_wallet: bool,
) -> Result<(), BitcoindError> {
self.bulk_import_descriptors(
&self.watchonly_client,
descriptors,
timestamp,
DEPOSIT_UTXOS_LABEL.to_string(),
fresh_wallet,
false,
)
}

Expand All @@ -466,10 +503,28 @@ impl BitcoinD {
fresh_wallet: bool,
) -> Result<(), BitcoindError> {
self.bulk_import_descriptors(
&self.watchonly_client,
descriptors,
timestamp,
UNVAULT_UTXOS_LABEL.to_string(),
fresh_wallet,
false,
)
}

pub fn startup_import_cpfp_descriptor(
&self,
descriptor: String,
timestamp: u32,
fresh_wallet: bool,
) -> Result<(), BitcoindError> {
self.bulk_import_descriptors(
&self.cpfp_client,
vec![descriptor],
timestamp,
CPFP_UTXOS_LABEL.to_string(),
fresh_wallet,
true,
)
}

Expand Down
3 changes: 3 additions & 0 deletions src/daemon/bitcoind/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ pub fn start_bitcoind(revaultd: &mut RevaultD) -> Result<BitcoinD, BitcoindError
revaultd
.watchonly_wallet_file()
.expect("Wallet id is set at startup in setup_db()"),
revaultd
.cpfp_wallet_file()
.expect("Wallet id is set at startup in setup_db()"),
)
.map_err(|e| {
BitcoindError::Custom(format!("Could not connect to bitcoind: {}", e.to_string()))
Expand Down
47 changes: 45 additions & 2 deletions src/daemon/bitcoind/poller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ use crate::daemon::{
revaultd::{BlockchainTip, RevaultD, VaultStatus},
};
use revault_tx::{
bitcoin::{Amount, OutPoint, Txid},
bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, Txid},
miniscript::descriptor::{DescriptorSecretKey, DescriptorXKey, KeyMap, Wildcard},
transactions::{RevaultTransaction, UnvaultTransaction},
txins::RevaultTxIn,
txouts::RevaultTxOut,
Expand Down Expand Up @@ -1508,7 +1509,7 @@ fn maybe_create_wallet(revaultd: &mut RevaultD, bitcoind: &BitcoinD) -> Result<(
}
}

bitcoind.createwallet_startup(bitcoind_wallet_path)?;
bitcoind.createwallet_startup(bitcoind_wallet_path, true)?;
log::info!("Importing descriptors to bitcoind watchonly wallet.");

// Now, import descriptors.
Expand Down Expand Up @@ -1539,6 +1540,48 @@ fn maybe_create_wallet(revaultd: &mut RevaultD, bitcoind: &BitcoinD) -> Result<(
bitcoind.startup_import_unvault_descriptors(addresses, wallet.timestamp, fresh_wallet)?;
}

if let Some(cpfp_key) = revaultd.cpfp_key {
let cpfp_wallet_path = revaultd
.cpfp_wallet_file()
.expect("Wallet id is set at startup in setup_db()");

if !PathBuf::from(cpfp_wallet_path.clone()).exists() {
log::info!("Creating the CPFP wallet");
// Remove any leftover. This can happen if we delete the cpfp wallet but don't restart
// bitcoind.
while bitcoind.listwallets()?.contains(&cpfp_wallet_path) {
log::info!("Found a leftover cpfp wallet loaded on bitcoind. Removing it.");
if let Err(e) = bitcoind.unloadwallet(cpfp_wallet_path.clone()) {
log::error!("Unloading wallet '{}': '{}'", &cpfp_wallet_path, e);
}
}

bitcoind.createwallet_startup(cpfp_wallet_path, false)?;
log::info!("Importing descriptors to bitcoind cpfp wallet.");

// Now, import descriptors.
let mut keymap: KeyMap = KeyMap::new();
let cpfp_private_key = DescriptorSecretKey::XPrv(DescriptorXKey {
xkey: cpfp_key.clone(),
origin: None,
derivation_path: Default::default(),
wildcard: Wildcard::Unhardened,
});
let sign_ctx = Secp256k1::signing_only();
let cpfp_public_key = cpfp_private_key
.as_public(&sign_ctx)
.expect("We never use hardened");
keymap.insert(cpfp_public_key, cpfp_private_key);
let cpfp_desc = revaultd
.cpfp_descriptor
.inner()
.to_string_with_secret(&keymap);

bitcoind.startup_import_cpfp_descriptor(cpfp_desc, wallet.timestamp, fresh_wallet)?;
}
} else {
log::info!("Not creating the CPFP wallet, as we don't have a CPFP key");
}
Ok(())
}

Expand Down
Loading

0 comments on commit eda096d

Please sign in to comment.