Skip to content

Commit a95a61a

Browse files
author
bg002h
committed
descriptor: expose WalletPolicy template + key_info accessors
The internal `template: Descriptor<KeyExpression>` and `key_info: Vec<DescriptorPublicKey>` fields are needed by external consumers that need to inspect a `WalletPolicy`'s placeholder structure without consuming the policy via `into_descriptor()`. The motivating case is multipath-shared `@N` placeholders — e.g. `sh(multi(1,@0/**,@0/<2;3>/*))`, where the same placeholder appears at two distinct AST positions. After `into_descriptor()` the placeholder labels are erased and the materialized descriptor surfaces two separate keys; the template preserves placeholder identity via the public `KeyExpression::index` field. Encoders and other consumers that need the `(AST position) -> (placeholder index)` mapping have no public way to obtain it today. Adds two read accessors on `WalletPolicy`: - `pub fn template(&self) -> &Descriptor<KeyExpression>` — yields the template; iterating gives `(AST position, KeyExpression)` pairs and the placeholder index is `ke.index.0`. - `pub fn key_info(&self) -> &[DescriptorPublicKey]` — yields the key-information vector (symmetric with the existing `set_key_info(&mut self, ...)`). Re-exports `KeyExpression` and `KeyIndex` at `miniscript::descriptor::*` so the new return type is namable from outside the crate, and adds rustdoc to `KeyIndex` and `KeyExpression::is_disjoint` (now reachable via the re-exports; required by the crate's missing_docs lint). Internal `validate()` already uses `self.template.iter_pk()` the same way external callers will; this commit just makes the access pattern available on the public API.
1 parent f7f1689 commit a95a61a

3 files changed

Lines changed: 77 additions & 2 deletions

File tree

src/descriptor/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ pub use self::key::{
5959
NonDefiniteKeyError, SinglePriv, SinglePub, SinglePubKey, Wildcard, XKeyNetwork,
6060
};
6161
pub use self::key_map::KeyMap;
62-
pub use self::wallet_policy::{WalletPolicy, WalletPolicyError};
62+
pub use self::wallet_policy::{KeyExpression, KeyIndex, WalletPolicy, WalletPolicyError};
6363

6464
/// Script descriptor
6565
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]

src/descriptor/wallet_policy/key_expression.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ pub struct KeyExpression {
2323
pub wildcard: Wildcard,
2424
}
2525

26+
/// The numeric index of a BIP-388 key placeholder (the `N` in `@N`).
2627
#[derive(Debug, Clone, Copy, Hash, PartialOrd, Ord, PartialEq, Eq)]
2728
pub struct KeyIndex(pub u32);
2829

2930
impl KeyExpression {
31+
/// Returns `true` if the multipath derivation suffixes of `self` and
32+
/// `other` share no concrete child-derivation step.
3033
pub fn is_disjoint(&self, other: &KeyExpression) -> bool {
3134
let lhs: BTreeSet<_> = self
3235
.derivation_paths

src/descriptor/wallet_policy/mod.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{Descriptor, DescriptorPublicKey, String, Translator, Vec};
99

1010
mod key_expression;
1111

12-
use key_expression::{KeyExpression, KeyIndex};
12+
pub use key_expression::{KeyExpression, KeyIndex};
1313

1414
/// A wallet policy as described in BIP-388
1515
///
@@ -108,6 +108,26 @@ impl WalletPolicy {
108108
WalletPolicy::from_descriptor_unchecked(descriptor).and_then(WalletPolicy::validate)
109109
}
110110

111+
/// Returns a reference to the template descriptor.
112+
///
113+
/// The template uses [`KeyExpression`] in place of concrete keys.
114+
/// Each `KeyExpression` carries a public `index` field identifying
115+
/// which entry in the key-information vector is referenced at that
116+
/// AST position. Callers needing the
117+
/// `(AST position) -> (placeholder index)` mapping can iterate
118+
/// `policy.template().iter_pk()` and read `ke.index.0` for each
119+
/// yielded `KeyExpression`. For the resolved descriptor with
120+
/// concrete keys, see [`Self::into_descriptor`] (consumes `self`).
121+
pub fn template(&self) -> &Descriptor<KeyExpression> { &self.template }
122+
123+
/// Returns the key-information vector.
124+
///
125+
/// `key_info()[i]` is the concrete key at AST position `i`,
126+
/// matching the order of `self.template().iter_pk()`. The
127+
/// corresponding placeholder index is
128+
/// `self.template().iter_pk().nth(i).unwrap().index.0`.
129+
pub fn key_info(&self) -> &[DescriptorPublicKey] { &self.key_info }
130+
111131
/// Convert a `WalletPolicy` into a `Descriptor<DescriptorPublicKey>` using
112132
/// the underlying template and key information.
113133
pub fn into_descriptor(self) -> Result<Descriptor<DescriptorPublicKey>, WalletPolicyError> {
@@ -377,4 +397,56 @@ mod tests {
377397
template_only.set_key_info(&keys).unwrap();
378398
assert!(template_only.clone().into_descriptor().is_ok());
379399
}
400+
401+
#[test]
402+
fn template_accessor_yields_keyexpressions_with_placeholder_indices() {
403+
// Template-only parse: key_info is empty until set_key_info() is called.
404+
let template_only =
405+
WalletPolicy::from_str("wsh(sortedmulti(2,@0/**,@1/**))").expect("parse template");
406+
let indices: Vec<u32> = template_only
407+
.template()
408+
.iter_pk()
409+
.map(|ke| ke.index.0)
410+
.collect();
411+
assert_eq!(indices, vec![0, 1]);
412+
assert!(template_only.key_info().is_empty());
413+
assert_eq!(format!("{:#}", template_only.template()), "wsh(sortedmulti(2,@0/**,@1/**))");
414+
}
415+
416+
#[test]
417+
fn template_accessor_distinguishes_ast_position_from_placeholder_index() {
418+
// Multipath-shared `@0`: the same placeholder appears at two distinct
419+
// AST positions. After `into_descriptor()` the placeholder labels are
420+
// erased — the materialized descriptor would surface two separate
421+
// (but equal) keys. The template preserves the placeholder identity
422+
// via `KeyExpression::index`.
423+
let policy =
424+
WalletPolicy::from_str("sh(multi(1,@0/**,@0/<2;3>/*))").expect("parse template");
425+
let pairs: Vec<(usize, u32)> = policy
426+
.template()
427+
.iter_pk()
428+
.enumerate()
429+
.map(|(ast_pos, ke)| (ast_pos, ke.index.0))
430+
.collect();
431+
assert_eq!(pairs, vec![(0, 0), (1, 0)]);
432+
}
433+
434+
#[test]
435+
fn key_info_accessor_yields_concrete_keys_in_ast_order() {
436+
// Full concrete-key parse populates key_info in AST order.
437+
let descriptor_str = "wsh(sortedmulti(2,[6738736c/48'/0'/0'/2']xpub6FC1fXFP1GXLX5TKtcjHGT4q89SDRehkQLtbKJ2PzWcvbBHtyDsJPLtpLtkGqYNYZdVVAjRQ5kug9CsapegmmeRutpP7PW4u4wVF9JfkDhw/<0;1>/*,[b2b1f0cf/48'/0'/0'/2']xpub6EWhjpPa6FqrcaPBuGBZRJVjzGJ1ZsMygRF26RwN932Vfkn1gyCiTbECVitBjRCkexEvetLdiqzTcYimmzYxyR1BZ79KNevgt61PDcukmC7/<0;1>/*))";
438+
let policy = WalletPolicy::from_str(descriptor_str).expect("parse descriptor");
439+
assert_eq!(policy.key_info().len(), 2);
440+
441+
// Template iter_pk yields placeholder indices in AST order; the
442+
// (AST position) -> (placeholder index) mapping lets external callers
443+
// reconstruct placeholder-indexed views from key_info[i].
444+
let pairs: Vec<(usize, u32)> = policy
445+
.template()
446+
.iter_pk()
447+
.enumerate()
448+
.map(|(ast_pos, ke)| (ast_pos, ke.index.0))
449+
.collect();
450+
assert_eq!(pairs, vec![(0, 0), (1, 1)]);
451+
}
380452
}

0 commit comments

Comments
 (0)