Skip to content

Commit 01f24fa

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 01f24fa

File tree

4 files changed

+142
-44
lines changed

4 files changed

+142
-44
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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ 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::{
7+
database::DatabaseError,
8+
revaultd::{ChecksumError, RevaultD},
9+
threadmessages::BitcoindMessageOut,
10+
};
711
use interface::{BitcoinD, WalletTransaction};
812
use poller::poller_main;
913
use revault_tx::bitcoin::{Network, Txid};
@@ -80,6 +84,12 @@ impl From<revault_tx::Error> for BitcoindError {
8084
}
8185
}
8286

87+
impl From<ChecksumError> for BitcoindError {
88+
fn from(e: ChecksumError) -> Self {
89+
Self::Custom(e.to_string())
90+
}
91+
}
92+
8393
fn check_bitcoind_network(
8494
bitcoind: &BitcoinD,
8595
config_network: &Network,

src/bitcoind/poller.rs

Lines changed: 7 additions & 16 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
@@ -1734,23 +1732,16 @@ fn maybe_create_wallet(revaultd: &mut RevaultD, bitcoind: &BitcoinD) -> Result<(
17341732
// currently supported by bitcoind) if there are more than 15 stakeholders.
17351733
// Therefore, we derive [max index] `addr()` descriptors to import into bitcoind, and handle
17361734
// the derivation index mess ourselves :'(
1737-
let addresses: Vec<_> = revaultd
1738-
.all_deposit_addresses()
1739-
.into_iter()
1740-
.map(|a| bitcoind.addr_descriptor(&a))
1741-
.collect::<Result<Vec<_>, _>>()?;
1735+
let addresses: Vec<_> = revaultd.all_deposit_descriptors();
17421736
log::trace!("Importing deposit descriptors '{:?}'", &addresses);
17431737
bitcoind.startup_import_deposit_descriptors(addresses, wallet.timestamp, fresh_wallet)?;
17441738

1739+
// Now, import the unvault address descriptors.
17451740
// As a consequence, we don't have enough information to opportunistically import a
17461741
// descriptor at the reception of a deposit anymore. Thus we need to blindly import *both*
17471742
// deposit and unvault descriptors..
17481743
// FIXME: maybe we actually have, with the derivation_index_map ?
1749-
let addresses: Vec<_> = revaultd
1750-
.all_unvault_addresses()
1751-
.into_iter()
1752-
.map(|a| bitcoind.addr_descriptor(&a))
1753-
.collect::<Result<Vec<_>, _>>()?;
1744+
let addresses: Vec<_> = revaultd.all_unvault_descriptors();
17541745
log::trace!("Importing unvault descriptors '{:?}'", &addresses);
17551746
bitcoind.startup_import_unvault_descriptors(addresses, wallet.timestamp, fresh_wallet)?;
17561747
}

src/revaultd.rs

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{
88
convert::TryFrom,
99
fmt, fs,
1010
io::{self, Read, Write},
11+
iter::FromIterator,
1112
net::SocketAddr,
1213
path::{Path, PathBuf},
1314
str::FromStr,
@@ -384,6 +385,93 @@ impl fmt::Display for DatadirError {
384385

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

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

608+
/// vault (deposit) address descriptor with checksum in canonical form (e.g.
609+
/// 'addr(ADDRESS)#CHECKSUM') for importing with bitcoind
610+
pub fn vault_desc(&self, child_number: ChildNumber) -> Result<String, ChecksumError> {
611+
let addr_desc = format!("addr({})", self.vault_address(child_number));
612+
Ok(format!("{}#{}", addr_desc, desc_checksum(&addr_desc)?))
613+
}
614+
520615
pub fn vault_address(&self, child_number: ChildNumber) -> Address {
521616
self.deposit_descriptor
522617
.derive(child_number, &self.secp_ctx)
@@ -525,6 +620,13 @@ impl RevaultD {
525620
.expect("deposit_descriptor is a wsh")
526621
}
527622

623+
/// unvault address descriptor with checksum in canonical form (e.g.
624+
/// 'addr(ADDRESS)#CHECKSUM') for importing with bitcoind
625+
pub fn unvault_desc(&self, child_number: ChildNumber) -> Result<String, ChecksumError> {
626+
let addr_desc = format!("addr({})", self.unvault_address(child_number));
627+
Ok(format!("{}#{}", addr_desc, desc_checksum(&addr_desc)?))
628+
}
629+
528630
pub fn unvault_address(&self, child_number: ChildNumber) -> Address {
529631
self.unvault_descriptor
530632
.derive(child_number, &self.secp_ctx)
@@ -601,38 +703,49 @@ impl RevaultD {
601703
self.vault_address(self.current_unused_index)
602704
}
603705

706+
pub fn last_deposit_desc(&self) -> Result<String, ChecksumError> {
707+
let raw_index: u32 = self.current_unused_index.into();
708+
// FIXME: this should fail instead of creating a hardened index
709+
self.vault_desc(ChildNumber::from(raw_index + self.gap_limit()))
710+
}
711+
604712
pub fn last_deposit_address(&self) -> Address {
605713
let raw_index: u32 = self.current_unused_index.into();
606714
// FIXME: this should fail instead of creating a hardened index
607715
self.vault_address(ChildNumber::from(raw_index + self.gap_limit()))
608716
}
609717

718+
pub fn last_unvault_desc(&self) -> Result<String, ChecksumError> {
719+
let raw_index: u32 = self.current_unused_index.into();
720+
// FIXME: this should fail instead of creating a hardened index
721+
self.unvault_desc(ChildNumber::from(raw_index + self.gap_limit()))
722+
}
723+
610724
pub fn last_unvault_address(&self) -> Address {
611725
let raw_index: u32 = self.current_unused_index.into();
612726
// FIXME: this should fail instead of creating a hardened index
613727
self.unvault_address(ChildNumber::from(raw_index + self.gap_limit()))
614728
}
615729

616-
/// All deposit addresses as strings up to the gap limit (100)
617-
pub fn all_deposit_addresses(&mut self) -> Vec<String> {
730+
/// All deposit address descriptors as strings up to the gap limit (100)
731+
pub fn all_deposit_descriptors(&mut self) -> Vec<String> {
618732
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()
733+
.values()
734+
.map(|child_num| {
735+
self.vault_desc(ChildNumber::from(*child_num))
736+
.expect("Failed checksum computation")
624737
})
625738
.collect()
626739
}
627740

628-
/// All unvault addresses as strings up to the gap limit (100)
629-
pub fn all_unvault_addresses(&mut self) -> Vec<String> {
741+
/// All unvault address descriptors as strings up to the gap limit (100)
742+
pub fn all_unvault_descriptors(&mut self) -> Vec<String> {
630743
let raw_index: u32 = self.current_unused_index.into();
631744
(0..raw_index + self.gap_limit())
632745
.map(|raw_index| {
633746
// FIXME: this should fail instead of creating a hardened index
634-
self.unvault_address(ChildNumber::from(raw_index))
635-
.to_string()
747+
self.unvault_desc(ChildNumber::from(raw_index))
748+
.expect("Failed to comput checksum")
636749
})
637750
.collect()
638751
}

0 commit comments

Comments
 (0)