&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)) => {
"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)))"
.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() {
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 {}
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,
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(),
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
}
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,