[wallet] Take both spending policies into account in create_tx

This allows specifying different "policy paths" for the internal and external
descriptors, and adds additional checks to make sure they are compatibile (i.e.
the timelocks are expressed in the same unit).

It's still suboptimal, since the `n_sequence`s are per-input and not per-transaction,
so it should be possibile to spend different inputs with different, otherwise
incompatible, `CSV` timelocks, but that requires a larger refactor that
can be done in a future patch.

This commit also tries to clarify how the "policy path" should be used by adding
a fairly detailed example to the docs.
This commit is contained in:
Alekos Filini 2020-11-10 15:06:14 +01:00
parent 9f31ad1bc8
commit 7c80aec454
No known key found for this signature in database
GPG Key ID: 5E8AFC3034FDFA4F
4 changed files with 181 additions and 16 deletions

View File

@ -433,7 +433,7 @@ impl Condition {
}
}
fn merge(mut self, other: &Condition) -> Result<Self, PolicyError> {
pub(crate) fn merge(mut self, other: &Condition) -> Result<Self, PolicyError> {
match (self.csv, other.csv) {
(Some(a), Some(b)) => self.csv = Some(Self::merge_timelock(a, b)?),
(None, any) => self.csv = any,

View File

@ -60,7 +60,7 @@ pub enum Error {
ChecksumMismatch,
DifferentDescriptorStructure,
SpendingPolicyRequired,
SpendingPolicyRequired(crate::types::ScriptType),
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
Signer(crate::wallet::signer::SignerError),

View File

@ -244,17 +244,62 @@ where
&self,
builder: TxBuilder<D, Cs, CreateTx>,
) -> Result<(PSBT, TransactionDetails), Error> {
// TODO: fetch both internal and external policies
let policy = self
let external_policy = self
.descriptor
.extract_policy(Arc::clone(&self.signers))?
.unwrap();
if policy.requires_path() && builder.policy_path.is_none() {
return Err(Error::SpendingPolicyRequired);
let internal_policy = self
.change_descriptor
.as_ref()
.map(|desc| {
Ok::<_, Error>(
desc.extract_policy(Arc::clone(&self.change_signers))?
.unwrap(),
)
})
.transpose()?;
// The policy allows spending external outputs, but it requires a policy path that hasn't been
// provided
if builder.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange
&& external_policy.requires_path()
&& builder.external_policy_path.is_none()
{
return Err(Error::SpendingPolicyRequired(ScriptType::External));
};
// Same for the internal_policy path, if present
if let Some(internal_policy) = &internal_policy {
if builder.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden
&& internal_policy.requires_path()
&& builder.internal_policy_path.is_none()
{
return Err(Error::SpendingPolicyRequired(ScriptType::Internal));
};
}
let requirements =
policy.get_condition(builder.policy_path.as_ref().unwrap_or(&BTreeMap::new()))?;
debug!("requirements: {:?}", requirements);
let external_requirements = external_policy.get_condition(
builder
.external_policy_path
.as_ref()
.unwrap_or(&BTreeMap::new()),
)?;
let internal_requirements = internal_policy
.map(|policy| {
Ok::<_, Error>(
policy.get_condition(
builder
.internal_policy_path
.as_ref()
.unwrap_or(&BTreeMap::new()),
)?,
)
})
.transpose()?;
let requirements = external_requirements
.clone()
.merge(&internal_requirements.unwrap_or_default())?;
debug!("Policy requirements: {:?}", requirements);
let version = match builder.version {
Some(tx_builder::Version(0)) => {
@ -1393,6 +1438,11 @@ mod test {
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
}
pub(crate) fn get_test_a_or_b_plus_csv() -> &'static str {
// or(pk(Alice),and(pk(Bob),older(144)))
"wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))"
}
pub(crate) fn get_test_single_sig_cltv() -> &'static str {
// and(pk(Alice),after(100000))
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))"
@ -2134,6 +2184,60 @@ mod test {
.unwrap();
}
#[test]
#[should_panic(expected = "SpendingPolicyRequired(External)")]
fn test_create_tx_policy_path_required() {
let (wallet, _, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
wallet
.create_tx(TxBuilder::with_recipients(vec![(
addr.script_pubkey(),
30_000,
)]))
.unwrap();
}
#[test]
fn test_create_tx_policy_path_no_csv() {
let (wallet, _, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
let external_policy = wallet.policies(ScriptType::External).unwrap().unwrap();
let root_id = external_policy.id;
// child #0 is just the key "A"
let path = vec![(root_id, vec![0])].into_iter().collect();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let (psbt, _) = wallet
.create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)])
.policy_path(path, ScriptType::External),
)
.unwrap();
assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFF);
}
#[test]
fn test_create_tx_policy_path_use_csv() {
let (wallet, _, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
let external_policy = wallet.policies(ScriptType::External).unwrap().unwrap();
let root_id = external_policy.id;
// child #1 is or(pk(B),older(144))
let path = vec![(root_id, vec![1])].into_iter().collect();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let (psbt, _) = wallet
.create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)])
.policy_path(path, ScriptType::External),
)
.unwrap();
assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 144);
}
#[test]
#[should_panic(expected = "IrreplaceableTransaction")]
fn test_bump_fee_irreplaceable_tx() {

View File

@ -51,7 +51,7 @@ use bitcoin::{OutPoint, Script, SigHashType, Transaction};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use crate::database::Database;
use crate::types::{FeeRate, UTXO};
use crate::types::{FeeRate, ScriptType, UTXO};
/// Context in which the [`TxBuilder`] is valid
pub trait TxBuilderContext: std::fmt::Debug + Default + Clone {}
@ -77,7 +77,8 @@ pub struct TxBuilder<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderC
pub(crate) drain_wallet: bool,
pub(crate) single_recipient: Option<Script>,
pub(crate) fee_policy: Option<FeePolicy>,
pub(crate) policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) utxos: Vec<OutPoint>,
pub(crate) unspendable: HashSet<OutPoint>,
pub(crate) manually_selected_only: bool,
@ -117,7 +118,8 @@ where
drain_wallet: Default::default(),
single_recipient: Default::default(),
fee_policy: Default::default(),
policy_path: Default::default(),
internal_policy_path: Default::default(),
external_policy_path: Default::default(),
utxos: Default::default(),
unspendable: Default::default(),
manually_selected_only: Default::default(),
@ -157,14 +159,72 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilde
self
}
/// Set the policy path to use while creating the transaction
/// Set the policy path to use while creating the transaction for a given script type
///
/// This method accepts a map where the key is the policy node id (see
/// [`Policy::id`](crate::descriptor::Policy::id)) and the value is the list of the indexes of
/// the items that are intended to be satisfied from the policy node (see
/// [`SatisfiableItem::Thresh::items`](crate::descriptor::policy::SatisfiableItem::Thresh::items)).
pub fn policy_path(mut self, policy_path: BTreeMap<String, Vec<usize>>) -> Self {
self.policy_path = Some(policy_path);
///
/// ## Example
///
/// An example of when the policy path is needed is the following descriptor:
/// `wsh(thresh(2,pk(A),sj:and_v(v:pk(B),n:older(6)),snj:and_v(v:pk(C),after(630000))))`,
/// derived from the miniscript policy `thresh(2,pk(A),and(pk(B),older(6)),and(pk(C),after(630000)))`.
/// It declares three descriptor fragments, and at the top level it uses `thresh()` to
/// ensure that at least two of them are satisfied. The individual fragments are:
///
/// 1. `pk(A)`
/// 2. `and(pk(B),older(6))`
/// 3. `and(pk(C),after(630000))`
///
/// When those conditions are combined in pairs, it's clear that the transaction needs to be created
/// differently depending on how the user intends to satisfy the policy afterwards:
///
/// * If fragments `1` and `2` are used, the transaction will need to use a specific
/// `n_sequence` in order to spend an `OP_CSV` branch.
/// * If fragments `1` and `3` are used, the transaction will need to use a specific `locktime`
/// in order to spend an `OP_CLTV` branch.
/// * If fragments `2` and `3` are used, the transaction will need both.
///
/// When the spending policy is represented as a tree (see
/// [`Wallet::policies`](super::Wallet::policies)), every node
/// is assigned a unique identifier that can be used in the policy path to specify which of
/// the node's children the user intends to satisfy: for instance, assuming the `thresh()`
/// root node of this example has an id of `aabbccdd`, the policy path map would look like:
///
/// `{ "aabbccdd" => [0, 1] }`
///
/// where the key is the node's id, and the value is a list of the children that should be
/// used, in no particular order.
///
/// If a particularly complex descriptor has multiple ambiguous thresholds in its structure,
/// multiple entries can be added to the map, one for each node that requires an explicit path.
///
/// ```
/// # use std::str::FromStr;
/// # use std::collections::BTreeMap;
/// # use bitcoin::*;
/// # use bdk::*;
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// let mut path = BTreeMap::new();
/// path.insert("aabbccdd".to_string(), vec![0, 1]);
///
/// let builder = TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)])
/// .policy_path(path, ScriptType::External);
/// # let builder: TxBuilder<bdk::database::MemoryDatabase, _, _> = builder;
/// ```
pub fn policy_path(
mut self,
policy_path: BTreeMap<String, Vec<usize>>,
script_type: ScriptType,
) -> Self {
let to_update = match script_type {
ScriptType::Internal => &mut self.internal_policy_path,
ScriptType::External => &mut self.external_policy_path,
};
*to_update = Some(policy_path);
self
}
@ -301,7 +361,8 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilde
drain_wallet: self.drain_wallet,
single_recipient: self.single_recipient,
fee_policy: self.fee_policy,
policy_path: self.policy_path,
internal_policy_path: self.internal_policy_path,
external_policy_path: self.external_policy_path,
utxos: self.utxos,
unspendable: self.unspendable,
manually_selected_only: self.manually_selected_only,