Skip to content

Commit 50c8be3

Browse files
committed
Compute checksums for wallet descriptors without bitcoind
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.
1 parent 0d765be commit 50c8be3

File tree

4 files changed

+134
-43
lines changed

4 files changed

+134
-43
lines changed

src/bitcoind/interface.rs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -380,22 +380,6 @@ impl BitcoinD {
380380
}
381381
}
382382

383-
/// Constructs an `addr()` descriptor out of an address
384-
pub fn addr_descriptor(&self, address: &str) -> Result<String, BitcoindError> {
385-
let desc_wo_checksum = format!("addr({})", address);
386-
387-
Ok(self
388-
.make_watchonly_request(
389-
"getdescriptorinfo",
390-
&params!(Json::String(desc_wo_checksum)),
391-
)?
392-
.get("descriptor")
393-
.expect("No 'descriptor' in 'getdescriptorinfo'")
394-
.as_str()
395-
.expect("'descriptor' in 'getdescriptorinfo' isn't a string anymore")
396-
.to_string())
397-
}
398-
399383
fn bulk_import_descriptors(
400384
&self,
401385
client: &Client,

src/bitcoind/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ 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::{database::DatabaseError, revaultd::{RevaultD, ChecksumError}, threadmessages::BitcoindMessageOut};
77
use interface::{BitcoinD, WalletTransaction};
88
use poller::poller_main;
99
use revault_tx::bitcoin::{Network, Txid};
@@ -80,6 +80,12 @@ impl From<revault_tx::Error> for BitcoindError {
8080
}
8181
}
8282

83+
impl From<ChecksumError> for BitcoindError {
84+
fn from(e: ChecksumError) -> Self {
85+
Self::Custom(e.to_string())
86+
}
87+
}
88+
8389
fn check_bitcoind_network(
8490
bitcoind: &BitcoinD,
8591
config_network: &Network,

src/bitcoind/poller.rs

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,13 +1383,11 @@ fn handle_new_deposit(
13831383
})?;
13841384
db_update_deposit_index(&revaultd.read().unwrap().db_file(), new_index)?;
13851385
revaultd.write().unwrap().current_unused_index = new_index;
1386-
let next_addr = bitcoind
1387-
.addr_descriptor(&revaultd.read().unwrap().last_deposit_address().to_string())?;
1386+
let next_addr = revaultd.read().unwrap().last_deposit_desc()?;
13881387
bitcoind.import_fresh_deposit_descriptor(next_addr)?;
1389-
let next_addr = bitcoind
1390-
.addr_descriptor(&revaultd.read().unwrap().last_unvault_address().to_string())?;
1391-
bitcoind.import_fresh_unvault_descriptor(next_addr)?;
13921388

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

1728-
// Now, import descriptors.
1726+
// Now, import deposit address descriptors.
17291727
// In theory, we could just import the vault (deposit) descriptor expressed using xpubs, give a
17301728
// range to bitcoind as the gap limit, and be fine.
17311729
// Unfortunately we cannot just import descriptors as is, since bitcoind does not support
@@ -1735,22 +1733,17 @@ fn maybe_create_wallet(revaultd: &mut RevaultD, bitcoind: &BitcoinD) -> Result<(
17351733
// Therefore, we derive [max index] `addr()` descriptors to import into bitcoind, and handle
17361734
// the derivation index mess ourselves :'(
17371735
let addresses: Vec<_> = revaultd
1738-
.all_deposit_addresses()
1739-
.into_iter()
1740-
.map(|a| bitcoind.addr_descriptor(&a))
1741-
.collect::<Result<Vec<_>, _>>()?;
1736+
.all_deposit_descriptors();
17421737
log::trace!("Importing deposit descriptors '{:?}'", &addresses);
17431738
bitcoind.startup_import_deposit_descriptors(addresses, wallet.timestamp, fresh_wallet)?;
17441739

1740+
// Now, import the unvault address descriptors.
17451741
// As a consequence, we don't have enough information to opportunistically import a
17461742
// descriptor at the reception of a deposit anymore. Thus we need to blindly import *both*
17471743
// deposit and unvault descriptors..
17481744
// FIXME: maybe we actually have, with the derivation_index_map ?
17491745
let addresses: Vec<_> = revaultd
1750-
.all_unvault_addresses()
1751-
.into_iter()
1752-
.map(|a| bitcoind.addr_descriptor(&a))
1753-
.collect::<Result<Vec<_>, _>>()?;
1746+
.all_unvault_descriptors();
17541747
log::trace!("Importing unvault descriptors '{:?}'", &addresses);
17551748
bitcoind.startup_import_unvault_descriptors(addresses, wallet.timestamp, fresh_wallet)?;
17561749
}

src/revaultd.rs

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
use std::{
77
collections::HashMap,
88
convert::TryFrom,
9-
fmt, fs,
9+
fmt, fs, iter::FromIterator,
1010
io::{self, Read, Write},
1111
net::SocketAddr,
1212
path::{Path, PathBuf},
@@ -384,6 +384,91 @@ impl fmt::Display for DatadirError {
384384

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

387+
#[derive(Debug)]
388+
pub enum ChecksumError {
389+
Checksum(String),
390+
}
391+
392+
impl fmt::Display for ChecksumError {
393+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
394+
match self {
395+
Self::Checksum(e) => {
396+
write!(f, "Error computing checksum: {}", e)
397+
}
398+
}
399+
}
400+
}
401+
402+
impl std::error::Error for ChecksumError {}
403+
404+
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
405+
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
406+
407+
fn poly_mod(mut c: u64, val: u64) -> u64 {
408+
let c0 = c >> 35;
409+
410+
c = ((c & 0x7ffffffff) << 5) ^ val;
411+
if c0 & 1 > 0 {
412+
c ^= 0xf5dee51989
413+
};
414+
if c0 & 2 > 0 {
415+
c ^= 0xa9fdca3312
416+
};
417+
if c0 & 4 > 0 {
418+
c ^= 0x1bab10e32d
419+
};
420+
if c0 & 8 > 0 {
421+
c ^= 0x3706b1677a
422+
};
423+
if c0 & 16 > 0 {
424+
c ^= 0x644d626ffd
425+
};
426+
427+
c
428+
}
429+
430+
/// Compute the checksum of a descriptor
431+
/// Note that this function does not check if the
432+
/// descriptor string is syntactically correct or not.
433+
/// This only computes the checksum
434+
pub fn desc_checksum(desc: &str) -> Result<String, ChecksumError> {
435+
let mut c = 1;
436+
let mut cls = 0;
437+
let mut clscount = 0;
438+
439+
for ch in desc.chars() {
440+
let pos = INPUT_CHARSET.find(ch).ok_or(ChecksumError::Checksum(format!(
441+
"Invalid character in checksum: '{}'",
442+
ch
443+
)))? as u64;
444+
c = poly_mod(c, pos & 31);
445+
cls = cls * 3 + (pos >> 5);
446+
clscount += 1;
447+
if clscount == 3 {
448+
c = poly_mod(c, cls);
449+
cls = 0;
450+
clscount = 0;
451+
}
452+
}
453+
if clscount > 0 {
454+
c = poly_mod(c, cls);
455+
}
456+
(0..8).for_each(|_| c = poly_mod(c, 0));
457+
c ^= 1;
458+
459+
let mut chars = Vec::with_capacity(8);
460+
for j in 0..8 {
461+
chars.push(
462+
CHECKSUM_CHARSET
463+
.chars()
464+
.nth(((c >> (5 * (7 - j))) & 31) as usize)
465+
.unwrap(),
466+
);
467+
}
468+
469+
Ok(String::from_iter(chars))
470+
}
471+
387472
impl RevaultD {
388473
/// Creates our global state by consuming the static configuration
389474
pub fn from_config(config: Config) -> Result<RevaultD, StartupError> {
@@ -517,6 +602,13 @@ impl RevaultD {
517602
NoisePubKey(curve25519::scalarmult_base(&scalar).0)
518603
}
519604

605+
/// vault (deposit) address descriptor with checksum in canonical form (e.g.
606+
/// 'addr(ADDRESS)#CHECKSUM') for importing with bitcoind
607+
pub fn vault_desc(&self, child_number: ChildNumber) -> Result<String, ChecksumError> {
608+
let addr_desc = format!("addr({})", self.vault_address(child_number));
609+
Ok(format!("{}#{}", addr_desc, desc_checksum(&addr_desc)?))
610+
}
611+
520612
pub fn vault_address(&self, child_number: ChildNumber) -> Address {
521613
self.deposit_descriptor
522614
.derive(child_number, &self.secp_ctx)
@@ -525,6 +617,13 @@ impl RevaultD {
525617
.expect("deposit_descriptor is a wsh")
526618
}
527619

620+
/// unvault address descriptor with checksum in canonical form (e.g.
621+
/// 'addr(ADDRESS)#CHECKSUM') for importing with bitcoind
622+
pub fn unvault_desc(&self, child_number: ChildNumber) -> Result<String, ChecksumError> {
623+
let addr_desc = format!("addr({})", self.unvault_address(child_number));
624+
Ok(format!("{}#{}", addr_desc, desc_checksum(&addr_desc)?))
625+
}
626+
528627
pub fn unvault_address(&self, child_number: ChildNumber) -> Address {
529628
self.unvault_descriptor
530629
.derive(child_number, &self.secp_ctx)
@@ -601,38 +700,47 @@ impl RevaultD {
601700
self.vault_address(self.current_unused_index)
602701
}
603702

703+
pub fn last_deposit_desc(&self) -> Result<String, ChecksumError> {
704+
let raw_index: u32 = self.current_unused_index.into();
705+
// FIXME: this should fail instead of creating a hardened index
706+
self.vault_desc(ChildNumber::from(raw_index + self.gap_limit()))
707+
}
708+
604709
pub fn last_deposit_address(&self) -> Address {
605710
let raw_index: u32 = self.current_unused_index.into();
606711
// FIXME: this should fail instead of creating a hardened index
607712
self.vault_address(ChildNumber::from(raw_index + self.gap_limit()))
608713
}
609714

715+
pub fn last_unvault_desc(&self) -> Result<String, ChecksumError> {
716+
let raw_index: u32 = self.current_unused_index.into();
717+
// FIXME: this should fail instead of creating a hardened index
718+
self.unvault_desc(ChildNumber::from(raw_index + self.gap_limit()))
719+
}
720+
610721
pub fn last_unvault_address(&self) -> Address {
611722
let raw_index: u32 = self.current_unused_index.into();
612723
// FIXME: this should fail instead of creating a hardened index
613724
self.unvault_address(ChildNumber::from(raw_index + self.gap_limit()))
614725
}
615726

616-
/// All deposit addresses as strings up to the gap limit (100)
617-
pub fn all_deposit_addresses(&mut self) -> Vec<String> {
727+
/// All deposit address descriptors as strings up to the gap limit (100)
728+
pub fn all_deposit_descriptors(&mut self) -> Vec<String> {
618729
self.derivation_index_map
619-
.keys()
620-
.map(|s| {
621-
Address::from_script(s, self.bitcoind_config.network)
622-
.expect("Created from P2WSH address")
623-
.to_string()
730+
.values()
731+
.map(|child_num| {
732+
self.vault_desc(ChildNumber::from(*child_num)).expect("Failed checksum computation")
624733
})
625734
.collect()
626735
}
627736

628-
/// All unvault addresses as strings up to the gap limit (100)
629-
pub fn all_unvault_addresses(&mut self) -> Vec<String> {
737+
/// All unvault address descriptors as strings up to the gap limit (100)
738+
pub fn all_unvault_descriptors(&mut self) -> Vec<String> {
630739
let raw_index: u32 = self.current_unused_index.into();
631740
(0..raw_index + self.gap_limit())
632741
.map(|raw_index| {
633742
// FIXME: this should fail instead of creating a hardened index
634-
self.unvault_address(ChildNumber::from(raw_index))
635-
.to_string()
743+
self.unvault_desc(ChildNumber::from(raw_index)).expect("Failed to comput checksum")
636744
})
637745
.collect()
638746
}

0 commit comments

Comments
 (0)