]> Untitled Git - bdk/commitdiff
[wallet] Take both spending policies into account in create_tx
authorAlekos Filini <alekos.filini@gmail.com>
Tue, 10 Nov 2020 14:06:14 +0000 (15:06 +0100)
committerAlekos Filini <alekos.filini@gmail.com>
Fri, 13 Nov 2020 11:55:42 +0000 (12:55 +0100)
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.

src/descriptor/policy.rs
src/error.rs
src/wallet/mod.rs
src/wallet/tx_builder.rs

index 961b1ba7a68b95d47248eaf3572a2c0a5fabae2d..fe0056cd9e3c90f9f28d90abbf1039dc4cb4375e 100644 (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,
index f4ecd65de1a0c5c0b6cd41a16e8248cb6621c651..925217e9e010f5f896e893543bbc7bcfd49b2fcc 100644 (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),
index 05e3787e8b29c0c02bcc7a8a1c3604619ac5e9fd..79ce77894b68529b01541f992ca75c037fb9b48e 100644 (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() {
index 5d46e3e37dabb2df7909e3b59ea408bdb29ebb01..672602f770e68c3df0a53574ba331cfbab46211e 100644 (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,