Skip to content

Commit

Permalink
Compute checksums for wallet descriptors without bitcoind
Browse files Browse the repository at this point in the history
The desc_checksum function and poly_mod were taken from rust-miniscript: https://github.com/rust-bitcoin/rust-miniscript/blob/master/src/descriptor/checksum.rs
Hopefully the desc_checksum function will be publicly exposed in the future and we can remove it.
  • Loading branch information
JSwambo committed Feb 22, 2022
1 parent 3ab3c6a commit b08f5a3
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 45 deletions.
16 changes: 0 additions & 16 deletions src/bitcoind/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,22 +381,6 @@ impl BitcoinD {
}
}

/// Constructs an `addr()` descriptor out of an address
pub fn addr_descriptor(&self, address: &str) -> Result<String, BitcoindError> {
let desc_wo_checksum = format!("addr({})", address);

Ok(self
.make_watchonly_request(
"getdescriptorinfo",
&params!(Json::String(desc_wo_checksum)),
)?
.get("descriptor")
.expect("No 'descriptor' in 'getdescriptorinfo'")
.as_str()
.expect("'descriptor' in 'getdescriptorinfo' isn't a string anymore")
.to_string())
}

fn bulk_import_descriptors(
&self,
client: &Client,
Expand Down
12 changes: 11 additions & 1 deletion src/bitcoind/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ pub mod poller;
pub mod utils;

use crate::config::BitcoindConfig;
use crate::{database::DatabaseError, revaultd::RevaultD, threadmessages::BitcoindMessageOut};
use crate::{
database::DatabaseError,
revaultd::{ChecksumError, RevaultD},
threadmessages::BitcoindMessageOut,
};
use interface::{BitcoinD, WalletTransaction};
use poller::poller_main;
use revault_tx::bitcoin::{Network, Txid};
Expand Down Expand Up @@ -80,6 +84,12 @@ impl From<revault_tx::Error> for BitcoindError {
}
}

impl From<ChecksumError> for BitcoindError {
fn from(e: ChecksumError) -> Self {
Self::Custom(e.to_string())
}
}

fn check_bitcoind_network(
bitcoind: &BitcoinD,
config_network: &Network,
Expand Down
23 changes: 7 additions & 16 deletions src/bitcoind/poller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1383,13 +1383,11 @@ fn handle_new_deposit(
})?;
db_update_deposit_index(&revaultd.read().unwrap().db_file(), new_index)?;
revaultd.write().unwrap().current_unused_index = new_index;
let next_addr = bitcoind
.addr_descriptor(&revaultd.read().unwrap().last_deposit_address().to_string())?;
let next_addr = revaultd.read().unwrap().last_deposit_desc()?;
bitcoind.import_fresh_deposit_descriptor(next_addr)?;
let next_addr = bitcoind
.addr_descriptor(&revaultd.read().unwrap().last_unvault_address().to_string())?;
bitcoind.import_fresh_unvault_descriptor(next_addr)?;

let next_addr = revaultd.read().unwrap().last_unvault_desc()?;
bitcoind.import_fresh_unvault_descriptor(next_addr)?;
log::debug!(
"Incremented deposit derivation index from {}",
current_first_index
Expand Down Expand Up @@ -1725,7 +1723,7 @@ fn maybe_create_wallet(revaultd: &mut RevaultD, bitcoind: &BitcoinD) -> Result<(
bitcoind.createwallet_startup(bitcoind_wallet_path, true)?;
log::info!("Importing descriptors to bitcoind watchonly wallet.");

// Now, import descriptors.
// Now, import deposit address descriptors.
// In theory, we could just import the vault (deposit) descriptor expressed using xpubs, give a
// range to bitcoind as the gap limit, and be fine.
// Unfortunately we cannot just import descriptors as is, since bitcoind does not support
Expand All @@ -1734,23 +1732,16 @@ fn maybe_create_wallet(revaultd: &mut RevaultD, bitcoind: &BitcoinD) -> Result<(
// currently supported by bitcoind) if there are more than 15 stakeholders.
// Therefore, we derive [max index] `addr()` descriptors to import into bitcoind, and handle
// the derivation index mess ourselves :'(
let addresses: Vec<_> = revaultd
.all_deposit_addresses()
.into_iter()
.map(|a| bitcoind.addr_descriptor(&a))
.collect::<Result<Vec<_>, _>>()?;
let addresses: Vec<_> = revaultd.all_deposit_descriptors();
log::trace!("Importing deposit descriptors '{:?}'", &addresses);
bitcoind.startup_import_deposit_descriptors(addresses, wallet.timestamp, fresh_wallet)?;

// Now, import the unvault address descriptors.
// As a consequence, we don't have enough information to opportunistically import a
// descriptor at the reception of a deposit anymore. Thus we need to blindly import *both*
// deposit and unvault descriptors..
// FIXME: maybe we actually have, with the derivation_index_map ?
let addresses: Vec<_> = revaultd
.all_unvault_addresses()
.into_iter()
.map(|a| bitcoind.addr_descriptor(&a))
.collect::<Result<Vec<_>, _>>()?;
let addresses: Vec<_> = revaultd.all_unvault_descriptors();
log::trace!("Importing unvault descriptors '{:?}'", &addresses);
bitcoind.startup_import_unvault_descriptors(addresses, wallet.timestamp, fresh_wallet)?;
}
Expand Down
135 changes: 124 additions & 11 deletions src/revaultd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{
convert::TryFrom,
fmt, fs,
io::{self, Read, Write},
iter::FromIterator,
net::SocketAddr,
path::{Path, PathBuf},
str::FromStr,
Expand Down Expand Up @@ -384,6 +385,93 @@ impl fmt::Display for DatadirError {

impl std::error::Error for DatadirError {}

#[derive(Debug)]
pub enum ChecksumError {
Checksum(String),
}

impl fmt::Display for ChecksumError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Checksum(e) => {
write!(f, "Error computing checksum: {}", e)
}
}
}
}

impl std::error::Error for ChecksumError {}

const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";

fn poly_mod(mut c: u64, val: u64) -> u64 {
let c0 = c >> 35;

c = ((c & 0x7ffffffff) << 5) ^ val;
if c0 & 1 > 0 {
c ^= 0xf5dee51989
};
if c0 & 2 > 0 {
c ^= 0xa9fdca3312
};
if c0 & 4 > 0 {
c ^= 0x1bab10e32d
};
if c0 & 8 > 0 {
c ^= 0x3706b1677a
};
if c0 & 16 > 0 {
c ^= 0x644d626ffd
};

c
}

/// Compute the checksum of a descriptor
/// Note that this function does not check if the
/// descriptor string is syntactically correct or not.
/// This only computes the checksum
pub fn desc_checksum(desc: &str) -> Result<String, ChecksumError> {
let mut c = 1;
let mut cls = 0;
let mut clscount = 0;

for ch in desc.chars() {
let pos = INPUT_CHARSET
.find(ch)
.ok_or(ChecksumError::Checksum(format!(
"Invalid character in checksum: '{}'",
ch
)))? as u64;
c = poly_mod(c, pos & 31);
cls = cls * 3 + (pos >> 5);
clscount += 1;
if clscount == 3 {
c = poly_mod(c, cls);
cls = 0;
clscount = 0;
}
}
if clscount > 0 {
c = poly_mod(c, cls);
}
(0..8).for_each(|_| c = poly_mod(c, 0));
c ^= 1;

let mut chars = Vec::with_capacity(8);
for j in 0..8 {
chars.push(
CHECKSUM_CHARSET
.chars()
.nth(((c >> (5 * (7 - j))) & 31) as usize)
.unwrap(),
);
}

Ok(String::from_iter(chars))
}

impl RevaultD {
/// Creates our global state by consuming the static configuration
pub fn from_config(config: Config) -> Result<RevaultD, StartupError> {
Expand Down Expand Up @@ -517,6 +605,13 @@ impl RevaultD {
NoisePubKey(curve25519::scalarmult_base(&scalar).0)
}

/// vault (deposit) address descriptor with checksum in canonical form (e.g.
/// 'addr(ADDRESS)#CHECKSUM') for importing with bitcoind
pub fn vault_desc(&self, child_number: ChildNumber) -> Result<String, ChecksumError> {
let addr_desc = format!("addr({})", self.vault_address(child_number));
Ok(format!("{}#{}", addr_desc, desc_checksum(&addr_desc)?))
}

pub fn vault_address(&self, child_number: ChildNumber) -> Address {
self.deposit_descriptor
.derive(child_number, &self.secp_ctx)
Expand All @@ -525,6 +620,13 @@ impl RevaultD {
.expect("deposit_descriptor is a wsh")
}

/// unvault address descriptor with checksum in canonical form (e.g.
/// 'addr(ADDRESS)#CHECKSUM') for importing with bitcoind
pub fn unvault_desc(&self, child_number: ChildNumber) -> Result<String, ChecksumError> {
let addr_desc = format!("addr({})", self.unvault_address(child_number));
Ok(format!("{}#{}", addr_desc, desc_checksum(&addr_desc)?))
}

pub fn unvault_address(&self, child_number: ChildNumber) -> Address {
self.unvault_descriptor
.derive(child_number, &self.secp_ctx)
Expand Down Expand Up @@ -601,38 +703,49 @@ impl RevaultD {
self.vault_address(self.current_unused_index)
}

pub fn last_deposit_desc(&self) -> Result<String, ChecksumError> {
let raw_index: u32 = self.current_unused_index.into();
// FIXME: this should fail instead of creating a hardened index
self.vault_desc(ChildNumber::from(raw_index + self.gap_limit()))
}

pub fn last_deposit_address(&self) -> Address {
let raw_index: u32 = self.current_unused_index.into();
// FIXME: this should fail instead of creating a hardened index
self.vault_address(ChildNumber::from(raw_index + self.gap_limit()))
}

pub fn last_unvault_desc(&self) -> Result<String, ChecksumError> {
let raw_index: u32 = self.current_unused_index.into();
// FIXME: this should fail instead of creating a hardened index
self.unvault_desc(ChildNumber::from(raw_index + self.gap_limit()))
}

pub fn last_unvault_address(&self) -> Address {
let raw_index: u32 = self.current_unused_index.into();
// FIXME: this should fail instead of creating a hardened index
self.unvault_address(ChildNumber::from(raw_index + self.gap_limit()))
}

/// All deposit addresses as strings up to the gap limit (100)
pub fn all_deposit_addresses(&mut self) -> Vec<String> {
/// All deposit address descriptors as strings up to the gap limit (100)
pub fn all_deposit_descriptors(&mut self) -> Vec<String> {
self.derivation_index_map
.keys()
.map(|s| {
Address::from_script(s, self.bitcoind_config.network)
.expect("Created from P2WSH address")
.to_string()
.values()
.map(|child_num| {
self.vault_desc(ChildNumber::from(*child_num))
.expect("Failed checksum computation")
})
.collect()
}

/// All unvault addresses as strings up to the gap limit (100)
pub fn all_unvault_addresses(&mut self) -> Vec<String> {
/// All unvault address descriptors as strings up to the gap limit (100)
pub fn all_unvault_descriptors(&mut self) -> Vec<String> {
let raw_index: u32 = self.current_unused_index.into();
(0..raw_index + self.gap_limit())
.map(|raw_index| {
// FIXME: this should fail instead of creating a hardened index
self.unvault_address(ChildNumber::from(raw_index))
.to_string()
self.unvault_desc(ChildNumber::from(raw_index))
.expect("Failed to comput checksum")
})
.collect()
}
Expand Down
2 changes: 1 addition & 1 deletion tests/servers/cosignerd

0 comments on commit b08f5a3

Please sign in to comment.