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:
commit
2b76180cdf
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
553
src/descriptors/analysis.rs
Normal 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
137
src/descriptors/keys.rs
Normal 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
@ -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 {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user