Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compute checksums for wallet descriptors without bitcoind #364

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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