Merge #374: Cleanup the descriptors module

9394be645c698591da9c477dd77363010cb3298e [bugfix] descriptors: fix parsing of descriptor with 1-of-N multisig (Antoine Poinsot)
1a13b7a6f820e92ff436198bffc78b8ad785a758 descriptors: rename InheritanceDescriptor into SinglePathLianaDesc (Antoine Poinsot)
8d1c6de5dde85583a6fb3a03458774d113ccb7b9 descriptors: rename MultipathDescriptor into LianaDescriptor (Antoine Poinsot)
f6885e358bfe78a790226e6246dad4922cf82d02 descriptors: cleanup error types (Antoine Poinsot)
647d65fe045a71158041cfb4bf5b98e5200db2e8 descriptors: create Liana descriptors through the policy (Antoine Poinsot)
9b866300be53be8a4e49e7913be25ff8887eac63 descriptors: merge the semantic analysis in one place (Antoine Poinsot)
cd566b91af07a53f9651c034ef4da95a8a033c56 descriptors: rename LianaDescInfo into LianaPolicy (Antoine Poinsot)
757009536b489b333ddb6a2d6bf237196e930e4e descriptors: make sure there is at least one timelocked path when parsing (Antoine Poinsot)
eebfa4755944f14aaa23f8b1a293a1b6a0f0f30f descriptors: move descriptor policy analysis into its own submodule (Antoine Poinsot)
c0dd63dfb2b6831666fb260581dce36e6f7601fa descriptors: move the LianaDescKey to the keys submodules (Antoine Poinsot)
7772ae8d8a74dc0f52be0381a77e68e4d2e8478f descriptors: move derived keys into their own submodule (Antoine Poinsot)
9e78ac7e8dd8bfb7170f64aa314aea30921aca4b descriptors: make the module a folder. (Antoine Poinsot)

Pull request description:

  We've been piling a bunch of new features since this module was first architectured, and it has become messy. This led to duplicate code, a confusing interface (`InheritanceDescriptor`, `LianaDescInfo`, ..) and more importantly bugs.

  This is a complete re-organization of the module in view of introducing multi-paths descriptors soon. This PR contains two bugfixes but aside from that it should not change (correct) behaviour. It does however completely break the interface.

  The new interface makes a lot more sense:
  - A `LianaPolicy` representing a Liana spending policy, from which you can get the parameters for the various spending path, and you can create from those parameters.
  - A `LianaDescriptor` which can be created from a `LianaPolicy`, and from which you can infer a `LianaPolicy` to retrieve the parameters of each spending path.

  This bijection (although it will soon become a surjection as we'll introduce the Miniscript policy compiler to create a `LianaDescriptor` from a `LianaPolicy`) makes the life of a client of the API easier, but it also harmonizes the code: we've centralized the Miniscript Semantic Policy checks of a descriptor in a single place to make sure that we can parse only what, and all, descriptors we can create.

ACKs for top commit:
  edouardparis:
    ACK 9394be645c698591da9c477dd77363010cb3298e

Tree-SHA512: 784eee825644db43417ec040f85b9e20ab72bcc545eed68a2b9b5a5945f86bea6e2d7b091e438b7ba8d4e0a6963459f2b29af59995a407a3c509b5be0fd06e9b
This commit is contained in:
Antoine Poinsot 2023-03-28 12:21:47 +02:00
commit 2b76180cdf
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
14 changed files with 920 additions and 791 deletions

View File

@ -5,7 +5,7 @@ mod utils;
use crate::{
bitcoin::{Block, BlockChainTip},
config,
descriptors::MultipathDescriptor,
descriptors::LianaDescriptor,
};
use utils::{block_before_date, roundup_progress};
@ -485,7 +485,7 @@ impl BitcoinD {
}
// Import the receive and change descriptors from the multipath descriptor to bitcoind.
fn import_descriptor(&self, desc: &MultipathDescriptor) -> Option<String> {
fn import_descriptor(&self, desc: &LianaDescriptor) -> Option<String> {
let descriptors = [desc.receive_descriptor(), desc.change_descriptor()]
.iter()
.map(|desc| {
@ -553,7 +553,7 @@ impl BitcoinD {
/// Create the watchonly wallet on bitcoind, and import it the main descriptor.
pub fn create_watchonly_wallet(
&self,
main_descriptor: &MultipathDescriptor,
main_descriptor: &LianaDescriptor,
) -> Result<(), BitcoindError> {
// Remove any leftover. This can happen if we delete the watchonly wallet but don't restart
// bitcoind.
@ -627,7 +627,7 @@ impl BitcoinD {
/// Perform various sanity checks of our watchonly wallet.
pub fn wallet_sanity_checks(
&self,
main_descriptor: &MultipathDescriptor,
main_descriptor: &LianaDescriptor,
) -> Result<(), BitcoindError> {
// Check our watchonly wallet is loaded
if self
@ -919,7 +919,7 @@ impl BitcoinD {
pub fn start_rescan(
&self,
desc: &MultipathDescriptor,
desc: &LianaDescriptor,
timestamp: u32,
) -> Result<(), BitcoindError> {
// Re-import the receive and change descriptors to the watchonly wallet for the purpose of

View File

@ -55,7 +55,7 @@ pub trait BitcoinInterface: Send {
fn received_coins(
&self,
tip: &BlockChainTip,
descs: &[descriptors::InheritanceDescriptor],
descs: &[descriptors::SinglePathLianaDesc],
) -> Vec<UTxO>;
/// Get all coins that were confirmed, and at what height and time. Along with "expired"
@ -87,7 +87,7 @@ pub trait BitcoinInterface: Send {
/// the given date.
fn start_rescan(
&self,
desc: &descriptors::MultipathDescriptor,
desc: &descriptors::LianaDescriptor,
timestamp: u32,
) -> Result<(), String>;
@ -131,7 +131,7 @@ impl BitcoinInterface for d::BitcoinD {
fn received_coins(
&self,
tip: &BlockChainTip,
descs: &[descriptors::InheritanceDescriptor],
descs: &[descriptors::SinglePathLianaDesc],
) -> Vec<UTxO> {
let lsb_res = self.list_since_block(&tip.hash);
@ -288,7 +288,7 @@ impl BitcoinInterface for d::BitcoinD {
fn start_rescan(
&self,
desc: &descriptors::MultipathDescriptor,
desc: &descriptors::LianaDescriptor,
timestamp: u32,
) -> Result<(), String> {
// FIXME: in theory i think this could potentially fail to actually start the rescan.
@ -338,7 +338,7 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
fn received_coins(
&self,
tip: &BlockChainTip,
descs: &[descriptors::InheritanceDescriptor],
descs: &[descriptors::SinglePathLianaDesc],
) -> Vec<UTxO> {
self.lock().unwrap().received_coins(tip, descs)
}
@ -374,7 +374,7 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
fn start_rescan(
&self,
desc: &descriptors::MultipathDescriptor,
desc: &descriptors::LianaDescriptor,
timestamp: u32,
) -> Result<(), String> {
self.lock().unwrap().start_rescan(desc, timestamp)

View File

@ -28,7 +28,7 @@ fn update_coins(
bit: &impl BitcoinInterface,
db_conn: &mut Box<dyn DatabaseConnection>,
previous_tip: &BlockChainTip,
descs: &[descriptors::InheritanceDescriptor],
descs: &[descriptors::SinglePathLianaDesc],
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> UpdatedCoins {
let curr_coins = db_conn.coins(CoinType::All);
@ -189,7 +189,7 @@ fn new_tip(bit: &impl BitcoinInterface, current_tip: &BlockChainTip) -> TipUpdat
fn updates(
bit: &impl BitcoinInterface,
db: &impl DatabaseInterface,
descs: &[descriptors::InheritanceDescriptor],
descs: &[descriptors::SinglePathLianaDesc],
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) {
let mut db_conn = db.connection();
@ -238,7 +238,7 @@ fn updates(
fn rescan_check(
bit: &impl BitcoinInterface,
db: &impl DatabaseInterface,
descs: &[descriptors::InheritanceDescriptor],
descs: &[descriptors::SinglePathLianaDesc],
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) {
log::debug!("Checking the state of an ongoing rescan if there is any");
@ -300,7 +300,7 @@ pub fn looper(
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
shutdown: sync::Arc<atomic::AtomicBool>,
poll_interval: time::Duration,
desc: descriptors::MultipathDescriptor,
desc: descriptors::LianaDescriptor,
) {
let mut last_poll = None;
let mut synced = false;

View File

@ -22,7 +22,7 @@ impl Poller {
bit: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
poll_interval: time::Duration,
desc: descriptors::MultipathDescriptor,
desc: descriptors::LianaDescriptor,
) -> Poller {
let shutdown = sync::Arc::from(atomic::AtomicBool::from(false));
let handle = thread::Builder::new()

View File

@ -155,7 +155,7 @@ pub enum InsaneFeeInfo {
// Apply some sanity checks on a created transaction's PSBT.
// TODO: add more sanity checks from revault_tx
fn sanity_check_psbt(
spent_desc: &descriptors::MultipathDescriptor,
spent_desc: &descriptors::LianaDescriptor,
psbt: &Psbt,
) -> Result<(), CommandError> {
let tx = &psbt.unsigned_tx;
@ -216,7 +216,7 @@ fn serializable_size<T: bitcoin::consensus::Encodable + ?Sized>(t: &T) -> u64 {
impl DaemonControl {
// Get the derived descriptor for this coin
fn derived_desc(&self, coin: &Coin) -> descriptors::DerivedInheritanceDescriptor {
fn derived_desc(&self, coin: &Coin) -> descriptors::DerivedSinglePathLianaDesc {
let desc = if coin.is_change {
self.config.main_descriptor.change_descriptor()
} else {
@ -783,7 +783,7 @@ impl DaemonControl {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetInfoDescriptors {
pub main: descriptors::MultipathDescriptor,
pub main: descriptors::LianaDescriptor,
}
/// Information about the daemon

View File

@ -1,4 +1,4 @@
use crate::descriptors::MultipathDescriptor;
use crate::descriptors::LianaDescriptor;
use std::{net::SocketAddr, path::PathBuf, str::FromStr, time::Duration};
@ -91,7 +91,7 @@ pub struct Config {
deserialize_with = "deserialize_fromstr",
serialize_with = "serialize_to_string"
)]
pub main_descriptor: MultipathDescriptor,
pub main_descriptor: LianaDescriptor,
/// Settings for the Bitcoin interface
pub bitcoin_config: BitcoinConfig,
/// Settings specific to bitcoind as the Bitcoin interface
@ -112,7 +112,7 @@ pub enum ConfigError {
DatadirNotFound,
FileNotFound,
ReadingFile(String),
UnexpectedDescriptor(Box<MultipathDescriptor>),
UnexpectedDescriptor(Box<LianaDescriptor>),
Unexpected(String),
}

View File

@ -18,7 +18,7 @@ use crate::{
},
Coin, CoinType,
},
descriptors::MultipathDescriptor,
descriptors::LianaDescriptor,
};
use std::{cmp, convert::TryInto, fmt, io, path};
@ -39,7 +39,7 @@ pub enum SqliteDbError {
FileNotFound(path::PathBuf),
UnsupportedVersion(i64),
InvalidNetwork(bitcoin::Network),
DescriptorMismatch(Box<MultipathDescriptor>),
DescriptorMismatch(Box<LianaDescriptor>),
Rusqlite(rusqlite::Error),
}
@ -83,7 +83,7 @@ impl From<rusqlite::Error> for SqliteDbError {
#[derive(Debug, Clone)]
pub struct FreshDbOptions {
pub bitcoind_network: bitcoin::Network,
pub main_descriptor: MultipathDescriptor,
pub main_descriptor: LianaDescriptor,
}
#[derive(Debug, Clone)]
@ -122,7 +122,7 @@ impl SqliteDb {
pub fn sanity_check(
&self,
bitcoind_network: bitcoin::Network,
main_descriptor: &MultipathDescriptor,
main_descriptor: &LianaDescriptor,
) -> Result<(), SqliteDbError> {
let mut conn = self.connection()?;
@ -597,7 +597,7 @@ mod tests {
fn dummy_options() -> FreshDbOptions {
let desc_str = "wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs";
let main_descriptor = MultipathDescriptor::from_str(desc_str).unwrap();
let main_descriptor = LianaDescriptor::from_str(desc_str).unwrap();
FreshDbOptions {
bitcoind_network: bitcoin::Network::Bitcoin,
main_descriptor,
@ -646,7 +646,7 @@ mod tests {
.contains("Database was created for network");
fs::remove_file(&db_path).unwrap();
let other_desc_str = "wsh(andor(pk([aabbccdd]tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))";
let other_desc = MultipathDescriptor::from_str(other_desc_str).unwrap();
let other_desc = LianaDescriptor::from_str(other_desc_str).unwrap();
let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap();
db.sanity_check(bitcoin::Network::Bitcoin, &other_desc)
.unwrap_err()

View File

@ -1,4 +1,4 @@
use crate::descriptors::MultipathDescriptor;
use crate::descriptors::LianaDescriptor;
use std::{convert::TryFrom, str::FromStr};
@ -112,7 +112,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbTip {
pub struct DbWallet {
pub id: i64,
pub timestamp: u32,
pub main_descriptor: MultipathDescriptor,
pub main_descriptor: LianaDescriptor,
pub deposit_derivation_index: bip32::ChildNumber,
pub change_derivation_index: bip32::ChildNumber,
pub rescan_timestamp: Option<u32>,
@ -126,7 +126,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet {
let timestamp = row.get(1)?;
let desc_str: String = row.get(2)?;
let main_descriptor = MultipathDescriptor::from_str(&desc_str)
let main_descriptor = LianaDescriptor::from_str(&desc_str)
.expect("Insane database: can't parse deposit descriptor");
let der_idx: u32 = row.get(3)?;

553
src/descriptors/analysis.rs Normal file
View File

@ -0,0 +1,553 @@
use miniscript::{
bitcoin::{util::bip32, Sequence},
descriptor,
policy::{Liftable, Semantic as SemanticPolicy},
Miniscript, ScriptContext, Terminal,
};
use std::{
collections::{HashMap, HashSet},
convert::TryFrom,
error, fmt, sync,
};
#[derive(Debug)]
pub enum LianaPolicyError {
InsaneTimelock(u32),
InvalidKey(Box<descriptor::DescriptorPublicKey>),
DuplicateKey(Box<descriptor::DescriptorPublicKey>),
InvalidMultiThresh(usize),
InvalidMultiKeys(usize),
IncompatibleDesc,
}
impl std::fmt::Display for LianaPolicyError {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
match self {
Self::InsaneTimelock(tl) => {
write!(f, "Timelock value '{}' isn't valid or safe to use", tl)
}
Self::InvalidKey(key) => {
write!(
f,
"Invalid key '{}'. Need a wildcard ('ranged') xpub with an origin and a multipath for (and only for) deriving change addresses. That is, an xpub of the form '[aaff0099]xpub.../<0;1>/*'.",
key
)
}
Self::InvalidMultiThresh(thresh) => write!(f, "Invalid multisig threshold value '{}'. The threshold must be > to 0 and <= to the number of keys.", thresh),
Self::InvalidMultiKeys(n_keys) => write!(f, "Invalid number of keys '{}'. Between 2 and 20 keys must be given to use multiple keys in a specific path.", n_keys),
Self::DuplicateKey(key) => {
write!(f, "Duplicate key '{}'.", key)
}
Self::IncompatibleDesc => write!(
f,
"Descriptor is not compatible with a Liana spending policy."
),
}
}
}
impl error::Error for LianaPolicyError {}
// Whether a Miniscript policy node represents a key check (or several of them).
fn is_single_key_or_multisig(policy: &SemanticPolicy<descriptor::DescriptorPublicKey>) -> bool {
match policy {
SemanticPolicy::Key(..) => true,
SemanticPolicy::Threshold(_, subs) => {
subs.iter().all(|sub| matches!(sub, SemanticPolicy::Key(_)))
}
_ => false,
}
}
// We require the descriptor key to:
// - Be deriveable (to contain a wildcard)
// - Be multipath (to contain a step in the derivation path with multiple indexes)
// - The multipath step to only contain two indexes, 0 and 1.
// - Be 'signable' by an external signer (to contain an origin)
fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool {
match *key {
descriptor::DescriptorPublicKey::Single(..) | descriptor::DescriptorPublicKey::XPub(..) => {
false
}
descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => {
let der_paths = xpub.derivation_paths.paths();
// Rust-miniscript enforces BIP389 which states that all paths must have the same len.
let len = der_paths.get(0).expect("Cannot be empty").len();
// Technically the xpub could be for the master xpub and not have an origin. But it's
// no unlikely (and easily fixable) while users shooting themselves in the foot by
// forgetting to provide the origin is so likely that it's worth ruling out xpubs
// without origin entirely.
xpub.origin.is_some()
&& xpub.wildcard == descriptor::Wildcard::Unhardened
&& der_paths.len() == 2
&& der_paths[0][len - 1] == 0.into()
&& der_paths[1][len - 1] == 1.into()
}
}
}
// We require the locktime to:
// - not be disabled
// - be in number of blocks
// - be 'clean' / minimal, ie all bits without consensus meaning should be 0
//
// All this is achieved simply through asking for a 16-bit integer, since all the
// above are signaled in leftmost bits.
fn csv_check(csv_value: u32) -> Result<u16, LianaPolicyError> {
if csv_value > 0 {
u16::try_from(csv_value).map_err(|_| LianaPolicyError::InsaneTimelock(csv_value))
} else {
Err(LianaPolicyError::InsaneTimelock(csv_value))
}
}
// Get the origin of a key in a multipath descriptors.
// Returns None if the given key isn't a multixpub.
fn key_origin(
key: &descriptor::DescriptorPublicKey,
) -> Option<&(bip32::Fingerprint, bip32::DerivationPath)> {
match key {
descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => xpub.origin.as_ref(),
_ => None,
}
}
/// Information about a single spending path in the descriptor.
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
pub enum PathInfo {
Single(descriptor::DescriptorPublicKey),
Multi(usize, Vec<descriptor::DescriptorPublicKey>),
}
impl PathInfo {
/// Get the information about the primary spending path.
/// Returns None if the policy does not describe the primary spending path of a Liana
/// descriptor (that is, a set of keys).
pub fn from_primary_path(
policy: SemanticPolicy<descriptor::DescriptorPublicKey>,
) -> Result<PathInfo, LianaPolicyError> {
match policy {
SemanticPolicy::Key(key) => Ok(PathInfo::Single(key)),
SemanticPolicy::Threshold(k, subs) => {
let keys: Result<_, LianaPolicyError> = subs
.into_iter()
.map(|sub| match sub {
SemanticPolicy::Key(key) => Ok(key),
_ => Err(LianaPolicyError::IncompatibleDesc),
})
.collect();
Ok(PathInfo::Multi(k, keys?))
}
_ => Err(LianaPolicyError::IncompatibleDesc),
}
}
/// Get the information about the recovery spending path.
/// Returns None if the policy does not describe the recovery spending path of a Liana
/// descriptor (that is, a set of keys after a timelock).
pub fn from_recovery_path(
policy: SemanticPolicy<descriptor::DescriptorPublicKey>,
) -> Result<(u16, PathInfo), LianaPolicyError> {
// The recovery spending path must always be a policy of type `thresh(2, older(x), thresh(n, key1,
// key2, ..))`. In the special case n == 1, it is only `thresh(2, older(x), key)`. In the
// special case n == len(keys) (i.e. it's an N-of-N multisig), it is normalized as
// `thresh(n+1, older(x), key1, key2, ...)`.
let (k, subs) = match policy {
SemanticPolicy::Threshold(k, subs) => (k, subs),
_ => return Err(LianaPolicyError::IncompatibleDesc),
};
if k == 2 && subs.len() == 2 {
// The general case (as well as the n == 1 case). The sub that is not the timelock is
// of the same form as a primary path.
let tl_value = subs
.iter()
.find_map(|s| match s {
SemanticPolicy::Older(val) => Some(csv_check(val.0)),
_ => None,
})
.ok_or(LianaPolicyError::IncompatibleDesc)??;
let keys_sub = subs
.into_iter()
.find(is_single_key_or_multisig)
.ok_or(LianaPolicyError::IncompatibleDesc)?;
PathInfo::from_primary_path(keys_sub).map(|info| (tl_value, info))
} else if k == subs.len() && subs.len() > 2 {
// The N-of-N case. All subs but the threshold must be keys (if one had been thresh()
// of keys it would have been normalized).
let mut tl_value = None;
let mut keys = Vec::with_capacity(subs.len());
for sub in subs {
match sub {
SemanticPolicy::Key(key) => keys.push(key),
SemanticPolicy::Older(val) => {
if tl_value.is_some() {
return Err(LianaPolicyError::IncompatibleDesc);
}
tl_value = Some(csv_check(val.0)?);
}
_ => return Err(LianaPolicyError::IncompatibleDesc),
}
}
assert!(keys.len() > 1); // At least 3 subs, only one of which may be older().
Ok((
tl_value.ok_or(LianaPolicyError::IncompatibleDesc)?,
PathInfo::Multi(k - 1, keys),
))
} else {
// If there is less than 2 subs, there can't be both a timelock and keys. If the
// threshold is not equal to the number of subs, the timelock can't be mandatory.
Err(LianaPolicyError::IncompatibleDesc)
}
}
/// Add another available key to this `PathInfo`. Note this doesn't change the threshold.
pub fn with_added_key(mut self, key: descriptor::DescriptorPublicKey) -> Self {
match self {
Self::Single(curr_key) => Self::Multi(1, vec![curr_key, key]),
Self::Multi(_, ref mut keys) => {
keys.push(key);
self
}
}
}
/// Get the required number of keys for spending through this path, and the set of keys
/// that can be used to provide a signature for this path.
pub fn thresh_origins(&self) -> (usize, HashSet<(bip32::Fingerprint, bip32::DerivationPath)>) {
match self {
PathInfo::Single(key) => {
let mut origins = HashSet::with_capacity(1);
origins.insert(
key_origin(key)
.expect("Must be a multixpub with an origin.")
.clone(),
);
(1, origins)
}
PathInfo::Multi(k, keys) => (
*k,
keys.iter()
.map(|key| {
key_origin(key)
.expect("Must be a multixpub with an origin.")
.clone()
})
.collect(),
),
}
}
/// Get the spend information for this descriptor based from the list of all pubkeys that
/// signed the transaction.
pub fn spend_info<'a>(
&self,
all_pubkeys_signed: impl Iterator<Item = &'a (bip32::Fingerprint, bip32::DerivationPath)>,
) -> PathSpendInfo {
let mut signed_pubkeys = HashMap::new();
let mut sigs_count = 0;
let (threshold, origins) = self.thresh_origins();
// For all existing signatures, pick those that are from one of our pubkeys.
for (fg, der_path) in all_pubkeys_signed {
// For all xpubs in the descriptor, we derive at /0/* or /1/*, so the xpub's origin's
// derivation path is the key's one without the last two derivation indexes.
if der_path.len() < 2 {
continue;
}
let parent_der_path: bip32::DerivationPath = der_path[..der_path.len() - 2].into();
let parent_origin = (*fg, parent_der_path);
// Now if the origin of this key without the two final derivation indexes is part of
// the set of our keys, count it as a signature for it. Note it won't mixup keys
// between spending paths, since we can't have duplicate xpubs in the descriptor and
// the (fingerprint, der_path) tuple is a UID for an xpub.
if origins.contains(&parent_origin) {
sigs_count += 1;
if let Some(count) = signed_pubkeys.get_mut(&parent_origin) {
*count += 1;
} else {
signed_pubkeys.insert(parent_origin, 1);
}
}
}
PathSpendInfo {
threshold,
sigs_count,
signed_pubkeys,
}
}
// TODO: avoid using a vec...
/// Get the keys contained in this spending path.
pub fn keys(&self) -> Vec<descriptor::DescriptorPublicKey> {
match self {
PathInfo::Single(ref key) => vec![key.clone()],
PathInfo::Multi(_, keys) => keys.clone(),
}
}
/// Returns `None` if it is a multisig that does not fit inside a CHECKMULTISIG.
pub fn into_miniscript(
self,
as_hash: bool,
) -> Option<Miniscript<descriptor::DescriptorPublicKey, miniscript::Segwitv0>> {
match self {
PathInfo::Single(key) => Some(
Miniscript::from_ast(Terminal::Check(sync::Arc::from(
Miniscript::from_ast(if as_hash {
Terminal::PkH(key)
} else {
Terminal::PkK(key)
})
.expect("pk_k is a valid Miniscript"),
)))
.expect("Well typed"),
),
PathInfo::Multi(thresh, keys) => {
if thresh < 1 || keys.len() > 20 || thresh > keys.len() {
None
} else {
Some(
Miniscript::from_ast(Terminal::Multi(thresh, keys))
.expect("multi is a valid Miniscript"),
)
}
}
}
}
}
/// A Liana spending policy. Can be created from some settings (the primary and recovery keys, the
/// timelock(s)) and be used to derive a descriptor. It can also be inferred from a descriptor and
/// be used to retrieve the settings.
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
pub struct LianaPolicy {
pub(super) primary_path: PathInfo,
pub(super) recovery_path: (u16, PathInfo),
}
impl LianaPolicy {
/// Create a new Liana policy from a given configuration.
pub fn new(
primary_path: PathInfo,
recovery_path: PathInfo,
recovery_timelock: u16,
) -> Result<LianaPolicy, LianaPolicyError> {
// We require the locktime to:
// - not be disabled
// - be in number of blocks
// - be 'clean' / minimal, ie all bits without consensus meaning should be 0
// - be positive (Miniscript requires it not to be 0)
//
// All this is achieved through asking for a 16-bit integer.
if recovery_timelock == 0 {
return Err(LianaPolicyError::InsaneTimelock(recovery_timelock as u32));
}
// If any of the paths is a multisig, make sure they are within the CHECKMULTISIG bounds.
for path_info in &[&primary_path, &recovery_path] {
if let PathInfo::Multi(thresh, keys) = path_info {
if keys.len() < 2 || keys.len() > 20 {
return Err(LianaPolicyError::InvalidMultiKeys(keys.len()));
}
if thresh == &0 || thresh > &keys.len() {
return Err(LianaPolicyError::InvalidMultiThresh(*thresh));
}
}
}
// Check all keys are valid according to our standard (this checks all are multipath keys).
let (prim_keys, rec_keys) = (primary_path.keys(), recovery_path.keys());
let all_keys = prim_keys.iter().chain(rec_keys.iter());
if let Some(key) = all_keys.clone().find(|k| !is_valid_desc_key(k)) {
return Err(LianaPolicyError::InvalidKey((*key).clone().into()));
}
// Check for key duplicates. They are invalid in (nonmalleable) miniscripts.
let mut key_set = HashSet::new();
for key in all_keys {
let xpub = match key {
descriptor::DescriptorPublicKey::MultiXPub(ref multi_xpub) => multi_xpub.xkey,
_ => unreachable!("Just checked it was a multixpub above"),
};
if key_set.contains(&xpub) {
return Err(LianaPolicyError::DuplicateKey(key.clone().into()));
}
key_set.insert(xpub);
}
assert!(!key_set.is_empty());
Ok(LianaPolicy {
primary_path,
recovery_path: (recovery_timelock, recovery_path),
})
}
/// Create a Liana policy from a descriptor. This will check the descriptor is correctly formed
/// (P2WSH, multipath, ..) and has a valid Liana semantic.
pub fn from_multipath_descriptor(
desc: &descriptor::Descriptor<descriptor::DescriptorPublicKey>,
) -> Result<LianaPolicy, LianaPolicyError> {
// For now we only allow P2WSH descriptors.
let wsh_desc = match &desc {
descriptor::Descriptor::Wsh(desc) => desc,
_ => return Err(LianaPolicyError::IncompatibleDesc),
};
// Get the Miniscript from the descriptor and make sure it only contains valid multipath
// descriptor keys.
let ms = match wsh_desc.as_inner() {
descriptor::WshInner::Ms(ms) => ms,
_ => return Err(LianaPolicyError::IncompatibleDesc),
};
let invalid_key = ms.iter_pk().find_map(|pk| {
if is_valid_desc_key(&pk) {
None
} else {
Some(pk)
}
});
if let Some(key) = invalid_key {
return Err(LianaPolicyError::InvalidKey(key.into()));
}
// Now lift a semantic policy out of this Miniscript and normalize it to make sure we
// compare apples to apples below.
let policy = ms
.lift()
.expect("Lifting can't fail on a Miniscript")
.normalized();
// The policy must always be "1 of N spending paths" with at least an always-available
// primary path with at least one key, and at least one timelocked recovery path with at
// least one key.
let subs = match policy {
SemanticPolicy::Threshold(1, subs) => Some(subs),
_ => None,
}
.ok_or(LianaPolicyError::IncompatibleDesc)?;
// Fetch the two spending paths' semantic policies. The primary path is identified as the
// only one that isn't timelocked.
let (mut primary_path, mut recovery_path) = (None::<PathInfo>, None);
for sub in subs {
// This is a (multi)key check. It must be the primary path.
if is_single_key_or_multisig(&sub) {
// We only support a single primary path. But it may be that the primary path is a
// 1-of-N multisig. In this case the policy is normalized from `thresh(1, thresh(1,
// pk(A), pk(B)), thresh(2, older(42), pk(C)))` to `thresh(1, pk(A), pk(B),
// thresh(2, older(42), pk(C)))`.
if let Some(prim_path) = primary_path {
if let SemanticPolicy::Key(key) = sub {
primary_path = Some(prim_path.with_added_key(key));
} else {
return Err(LianaPolicyError::IncompatibleDesc);
}
} else {
primary_path = Some(PathInfo::from_primary_path(sub)?);
}
} else {
// If it's not a simple (multi)key check, it must be the timelocked recovery path.
// For now, we only support a single recovery path.
if recovery_path.is_some() {
return Err(LianaPolicyError::IncompatibleDesc);
}
recovery_path = Some(PathInfo::from_recovery_path(sub)?);
}
}
Ok(LianaPolicy {
primary_path: primary_path.ok_or(LianaPolicyError::IncompatibleDesc)?,
recovery_path: recovery_path.ok_or(LianaPolicyError::IncompatibleDesc)?,
})
}
pub fn primary_path(&self) -> &PathInfo {
&self.primary_path
}
/// Timelock and path info for the recovery path.
pub fn recovery_path(&self) -> (u16, &PathInfo) {
(self.recovery_path.0, &self.recovery_path.1)
}
/// Create a descriptor from this spending policy with multipath key expressions.
///
/// Although for now this function is deterministic, it **will not** be in the future.
pub fn into_multipath_descriptor(
self,
) -> descriptor::Descriptor<descriptor::DescriptorPublicKey> {
let LianaPolicy {
primary_path,
recovery_path: (timelock, recovery_path),
} = self;
// Create the timelocked spending path. If there is a single key we make it a pk_h() in
// order to save on the script size (since we assume the timelocked recovery path will
// seldom be used).
let recovery_timelock = Terminal::Older(Sequence::from_height(timelock));
let recovery_keys = recovery_path
.into_miniscript(true)
.expect("We check the multisig never overflows in our constructors.");
let recovery_branch = Miniscript::from_ast(Terminal::AndV(
Miniscript::from_ast(Terminal::Verify(recovery_keys.into()))
.expect("Well typed")
.into(),
Miniscript::from_ast(recovery_timelock)
.expect("Well typed")
.into(),
))
.expect("Well typed");
// Combine the timelocked spending path with the simple "primary" path. For the primary key
// we don't use a pkh since it's the one that will likely always be used.
let primary_keys = primary_path
.into_miniscript(false)
.expect("We check the multisig never overflows in our constructors.");
let tl_miniscript =
Miniscript::from_ast(Terminal::OrD(primary_keys.into(), recovery_branch.into()))
.expect("Well typed");
miniscript::Segwitv0::check_local_validity(&tl_miniscript)
.expect("Miniscript must be sane");
descriptor::Descriptor::Wsh(
descriptor::Wsh::new(tl_miniscript).expect("Must pass sanity checks"),
)
}
}
/// Partial spend information for a specific spending path within a descriptor.
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct PathSpendInfo {
/// The required number of signatures to provide to spend through this path.
pub threshold: usize,
/// The number of signatures provided.
pub sigs_count: usize,
/// The keys for which a signature was provided and the number (always >=1) of
/// signatures provided for this key.
pub signed_pubkeys: HashMap<(bip32::Fingerprint, bip32::DerivationPath), usize>,
}
/// Information about a partial spend of Liana coins
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct PartialSpendInfo {
/// Number of signatures present for the primary path
pub(super) primary_path: PathSpendInfo,
/// Number of signatures present for the recovery path, only present if the path is available
/// in the first place.
pub(super) recovery_path: Option<PathSpendInfo>,
}
impl PartialSpendInfo {
/// Get the number of signatures present for the primary path
pub fn primary_path(&self) -> &PathSpendInfo {
&self.primary_path
}
/// Get the number of signatures present for the recovery path. Only present if the path is
/// available in the first place.
pub fn recovery_path(&self) -> &Option<PathSpendInfo> {
&self.recovery_path
}
}

137
src/descriptors/keys.rs Normal file
View File

@ -0,0 +1,137 @@
use miniscript::{
bitcoin::{
self,
hashes::{hash160, ripemd160, sha256},
util::bip32,
},
hash256, MiniscriptKey, ToPublicKey,
};
use std::{error, fmt, str};
#[derive(Debug)]
pub enum DescKeyError {
DerivedKeyParsing,
}
impl std::fmt::Display for DescKeyError {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
match self {
DescKeyError::DerivedKeyParsing => write!(f, "Parsing derived key"),
}
}
}
impl error::Error for DescKeyError {}
/// A public key used in derived descriptors
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
pub struct DerivedPublicKey {
/// Fingerprint of the master xpub and the derivation index used. We don't use a path
/// since we never derive at more than one depth.
pub origin: (bip32::Fingerprint, bip32::DerivationPath),
/// The actual key
pub key: bitcoin::PublicKey,
}
impl fmt::Display for DerivedPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (fingerprint, deriv_path) = &self.origin;
write!(f, "[")?;
for byte in fingerprint.as_bytes().iter() {
write!(f, "{:02x}", byte)?;
}
write!(f, "/{}", deriv_path)?;
write!(f, "]{}", self.key)
}
}
impl str::FromStr for DerivedPublicKey {
type Err = DescKeyError;
fn from_str(s: &str) -> Result<DerivedPublicKey, Self::Err> {
// The key is always of the form:
// [ fingerprint / index ]<key>
// 1 + 8 + 1 + 1 + 1 + 66 minimum
if s.len() < 78 {
return Err(DescKeyError::DerivedKeyParsing);
}
// Non-ASCII?
for ch in s.as_bytes() {
if *ch < 20 || *ch > 127 {
return Err(DescKeyError::DerivedKeyParsing);
}
}
if s.chars().next().expect("Size checked above") != '[' {
return Err(DescKeyError::DerivedKeyParsing);
}
let mut parts = s[1..].split(']');
let fg_deriv = parts.next().ok_or(DescKeyError::DerivedKeyParsing)?;
let key_str = parts.next().ok_or(DescKeyError::DerivedKeyParsing)?;
if fg_deriv.len() < 10 {
return Err(DescKeyError::DerivedKeyParsing);
}
let fingerprint = bip32::Fingerprint::from_str(&fg_deriv[..8])
.map_err(|_| DescKeyError::DerivedKeyParsing)?;
let deriv_path = bip32::DerivationPath::from_str(&fg_deriv[9..])
.map_err(|_| DescKeyError::DerivedKeyParsing)?;
if deriv_path.into_iter().any(bip32::ChildNumber::is_hardened) {
return Err(DescKeyError::DerivedKeyParsing);
}
let key =
bitcoin::PublicKey::from_str(key_str).map_err(|_| DescKeyError::DerivedKeyParsing)?;
Ok(DerivedPublicKey {
key,
origin: (fingerprint, deriv_path),
})
}
}
impl MiniscriptKey for DerivedPublicKey {
type Sha256 = sha256::Hash;
type Hash256 = hash256::Hash;
type Ripemd160 = ripemd160::Hash;
type Hash160 = hash160::Hash;
fn is_uncompressed(&self) -> bool {
self.key.is_uncompressed()
}
fn is_x_only_key(&self) -> bool {
false
}
fn num_der_paths(&self) -> usize {
0
}
}
impl ToPublicKey for DerivedPublicKey {
fn to_public_key(&self) -> bitcoin::PublicKey {
self.key
}
fn to_sha256(hash: &sha256::Hash) -> sha256::Hash {
*hash
}
fn to_hash256(hash: &hash256::Hash) -> hash256::Hash {
*hash
}
fn to_ripemd160(hash: &ripemd160::Hash) -> ripemd160::Hash {
*hash
}
fn to_hash160(hash: &hash160::Hash) -> hash160::Hash {
*hash
}
}

File diff suppressed because it is too large Load Diff

View File

@ -464,7 +464,7 @@ mod tests {
use super::*;
use crate::{
config::{BitcoinConfig, BitcoindConfig},
descriptors::MultipathDescriptor,
descriptors::LianaDescriptor,
testutils::*,
};
@ -686,7 +686,7 @@ mod tests {
// Create a dummy config with this bitcoind
let desc_str = "wsh(andor(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#3xh8xmhn";
let desc = MultipathDescriptor::from_str(desc_str).unwrap();
let desc = LianaDescriptor::from_str(desc_str).unwrap();
let receive_desc = desc.receive_descriptor().clone();
let change_desc = desc.change_descriptor().clone();
let config = Config {

View File

@ -409,9 +409,7 @@ mod tests {
.unwrap(),
wildcard: Wildcard::Unhardened,
});
let prim_keys =
descriptors::LianaDescKeys::from_multi(2, vec![prim_key_a, prim_key_b, prim_key_c])
.unwrap();
let prim_keys = descriptors::PathInfo::Multi(2, vec![prim_key_a, prim_key_b, prim_key_c]);
let origin_der = bip32::DerivationPath::from_str("m/1/2'/3/4'").unwrap();
let xkey = recov_signer.xpub_at(&origin_der, &secp);
let recov_key = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
@ -424,8 +422,9 @@ mod tests {
.unwrap(),
wildcard: Wildcard::Unhardened,
});
let recov_keys = descriptors::LianaDescKeys::from_single(recov_key);
let desc = descriptors::MultipathDescriptor::new(prim_keys, recov_keys, 42).unwrap();
let recov_keys = descriptors::PathInfo::Single(recov_key);
let policy = descriptors::LianaPolicy::new(prim_keys, recov_keys, 42).unwrap();
let desc = descriptors::LianaDescriptor::new(policy);
// Create a dummy PSBT spending a coin from this descriptor with a single input and single
// (external) output. We'll be modifying it as we go.

View File

@ -60,7 +60,7 @@ impl BitcoinInterface for DummyBitcoind {
fn received_coins(
&self,
_: &BlockChainTip,
_: &[descriptors::InheritanceDescriptor],
_: &[descriptors::SinglePathLianaDesc],
) -> Vec<UTxO> {
Vec::new()
}
@ -91,7 +91,7 @@ impl BitcoinInterface for DummyBitcoind {
todo!()
}
fn start_rescan(&self, _: &descriptors::MultipathDescriptor, _: u32) -> Result<(), String> {
fn start_rescan(&self, _: &descriptors::LianaDescriptor, _: u32) -> Result<(), String> {
todo!()
}
@ -399,9 +399,10 @@ impl DummyLiana {
poll_interval_secs: time::Duration::from_secs(2),
};
let owner_key = descriptors::LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap());
let heir_key = descriptors::LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap());
let desc = descriptors::MultipathDescriptor::new(owner_key, heir_key, 10_000).unwrap();
let owner_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap());
let heir_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap());
let policy = descriptors::LianaPolicy::new(owner_key, heir_key, 10_000).unwrap();
let desc = descriptors::LianaDescriptor::new(policy);
let config = Config {
bitcoin_config,
bitcoind_config: None,