Skip to content

Commit 95fdd1c

Browse files
committed
Merge #932: wallet_policy: fix set_key_info silently dropping keys for repeated placeholders
1f655a9 wallet_policy: add regression test for repeated placeholder key drop (Sanket Kanjalkar) 716f0ce wallet_policy: fix set_key_info accepting extra keys for repeated placeholders (Sanket Kanjalkar) Pull request description: `set_key_info()` counted key-expression occurrences instead of unique placeholder indices. A template reusing `@0` with disjoint paths (e.g. `@0/**,@0/<2;3>/*`) would accept extra keys that are silently dropped during descriptor translation. Fix by counting unique placeholder indices via `BTreeSet`. I'm not super familiar with wallet_policy/BIP-388, but this looks like a bug to me. Credit to jmeccom for reporting this. ACKs for top commit: apoelstra: ACK 1f655a9; successfully ran local tests Tree-SHA512: c1399f6c3225482d796bd32c457b096a2b69ed75129a4ea7c6250ceef2b760a8e583e52c1e0e15e0874dabeca8adf7f15c0c4ec0ea2491b1611d886b5229ffad
2 parents a6faf09 + 1f655a9 commit 95fdd1c

1 file changed

Lines changed: 27 additions & 2 deletions

File tree

  • src/descriptor/wallet_policy

src/descriptor/wallet_policy/mod.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use core::str::FromStr;
55

66
use super::key::XKeyParseError;
77
use super::{DerivPaths, DescriptorKeyParseError, Wildcard};
8-
use crate::{Descriptor, DescriptorPublicKey, String, Translator, Vec};
8+
use crate::{BTreeSet, Descriptor, DescriptorPublicKey, String, Translator, Vec};
99

1010
mod key_expression;
1111

@@ -119,7 +119,9 @@ impl WalletPolicy {
119119
/// Sets the key information so that `WalletPolicy::into_descriptor` can be
120120
/// called successfully. Errors when there are not enough keys for the template.
121121
pub fn set_key_info(&mut self, keys: &[DescriptorPublicKey]) -> Result<(), WalletPolicyError> {
122-
if keys.len() != self.template.iter_pk().count() {
122+
let unique_placeholders: BTreeSet<u32> =
123+
self.template.iter_pk().map(|k| k.index.0).collect();
124+
if keys.len() != unique_placeholders.len() {
123125
return Err(WalletPolicyError::WalletPolicyInvalidKeyInfo);
124126
}
125127
self.key_info = keys.to_vec();
@@ -377,4 +379,27 @@ mod tests {
377379
template_only.set_key_info(&keys).unwrap();
378380
assert!(template_only.clone().into_descriptor().is_ok());
379381
}
382+
383+
// Regression test for a bug where set_key_info() counted key-expression
384+
// occurrences instead of unique placeholder indices. A template reusing @0
385+
// with disjoint paths (e.g. @0/**,@0/<2;3>/*) has 2 occurrences but only 1
386+
// unique placeholder. The old code accepted 2 keys, silently dropping the
387+
// second one during descriptor translation since both @0 resolve to
388+
// key_info[0].
389+
// Reported by jm@squareup.com
390+
#[test]
391+
fn set_key_info_rejects_extra_keys_for_repeated_placeholder() {
392+
let mut policy = WalletPolicy::from_str("wsh(sortedmulti(2,@0/**,@0/<2;3>/*))").unwrap();
393+
let attacker_key: DescriptorPublicKey = "[6738736c/48'/0'/0'/2']xpub6FC1fXFP1GXLX5TKtcjHGT4q89SDRehkQLtbKJ2PzWcvbBHtyDsJPLtpLtkGqYNYZdVVAjRQ5kug9CsapegmmeRutpP7PW4u4wVF9JfkDhw".parse().unwrap();
394+
let victim_key: DescriptorPublicKey = "[b2b1f0cf/48'/0'/0'/2']xpub6EWhjpPa6FqrcaPBuGBZRJVjzGJ1ZsMygRF26RwN932Vfkn1gyCiTbECVitBjRCkexEvetLdiqzTcYimmzYxyR1BZ79KNevgt61PDcukmC7".parse().unwrap();
395+
396+
// Must reject: 2 keys provided but only 1 unique placeholder (@0)
397+
assert_eq!(
398+
policy.set_key_info(&[attacker_key.clone(), victim_key]),
399+
Err(WalletPolicyError::WalletPolicyInvalidKeyInfo),
400+
);
401+
402+
// Must accept: 1 key for 1 unique placeholder
403+
assert!(policy.set_key_info(&[attacker_key]).is_ok());
404+
}
380405
}

0 commit comments

Comments
 (0)