From 5cd98c09e8d08e375bcac97c9d5849bb5de482c1 Mon Sep 17 00:00:00 2001 From: itorod Date: Mon, 4 May 2026 13:47:20 +0000 Subject: [PATCH] feat: Add descriptor miniscript constructor method --- CHANGELOG.md | 2 + .../org/bitcoindevkit/DescriptorTest.kt | 63 +++++++++ bdk-ffi/src/descriptor.rs | 120 ++++++++++++++++++ 3 files changed, 185 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9396668c..00f14b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - New `WildcardType` enum [#853] - New `DescriptorPublicKey::add_wildcard` method, which adds an unhardened wildcard to the derivation path of the descriptor [#853] - New `DescriptorSecretKey::add_wildcard(wildcard_type: WildcardType)` method, which adds a wildcard to the derivation path of the descriptor [#853] +- Exposed `new_sh`, `new_wsh`,`new_bare` and `new_sh_wsh` methods on `Descriptor` type [#988] [#853]: https://github.com/bitcoindevkit/bdk-ffi/pull/853 [#853]: https://github.com/bitcoindevkit/bdk-ffi/pull/945 @@ -32,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), [#986]: https://github.com/bitcoindevkit/bdk-ffi/pull/971 [#986]: https://github.com/bitcoindevkit/bdk-ffi/pull/973 [#986]: https://github.com/bitcoindevkit/bdk-ffi/pull/986 +[#986]: https://github.com/bitcoindevkit/bdk-ffi/pull/988 ## [v2.3.0] diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/DescriptorTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/DescriptorTest.kt index b07eaf90..a1594c04 100644 --- a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/DescriptorTest.kt +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/DescriptorTest.kt @@ -74,6 +74,69 @@ class DescriptorTest { assertEquals(newShWpkhDescriptor.descType(), DescriptorType.SH_WPKH) } + @Test + fun createMiniscriptDescriptors() { + val newShDescriptor = Descriptor.newSh("pk(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)") + val newWshDescriptor = Descriptor.newWsh("pk(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)") + val newShWshDescriptor = Descriptor.newShWsh("pk(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)") + val newBareDescriptor = Descriptor.newBare("pk(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)") + + assertEquals(newShDescriptor.descType(), DescriptorType.SH) + assertEquals(newWshDescriptor.descType(), DescriptorType.WSH) + assertEquals(newShWshDescriptor.descType(), DescriptorType.SH_WSH) + assertEquals(newBareDescriptor.descType(), DescriptorType.BARE) + } + + @Test + fun miniscriptConstructorsRejectInvalidExpressions() { + val invalid = "not_a_valid_miniscript" + + assertFailsWith { Descriptor.newWsh(invalid) } + assertFailsWith { Descriptor.newSh(invalid) } + assertFailsWith { Descriptor.newShWsh(invalid) } + assertFailsWith { Descriptor.newBare(invalid) } + } + + // BareCtx only allows pk(), pkh(), and multi(k<=3,...) at the top level. A timelock + // conjunction is valid miniscript for new_wsh but is rejected by new_bare. + @Test + fun notAllMiniscriptsWorkEverywhere() { + val compressedPk = "02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737" + val timelockConjunction = "and_v(v:pk($compressedPk),after(1000))" + + // println("Complex miniscript: $timelockConjunction") + // println("Resulting descriptor: ${Descriptor.newWsh(timelockConjunction)}") + + // Can do + Descriptor.newSh(timelockConjunction) + Descriptor.newWsh(timelockConjunction) + + // No can do + assertFailsWith { + Descriptor.newBare(timelockConjunction) + Descriptor.newWpkh(timelockConjunction) + Descriptor.newWsh(timelockConjunction) + Descriptor.newPkh(timelockConjunction) + } + } + + // Segwitv0 (new_wsh) requires all keys to be compressed. An uncompressed key is valid + // in Legacy context (new_sh) but rejected by new_wsh. + @Test + fun newWshRejectsUncompressedKey() { + // Satoshi's genesis block public key — a well-known uncompressed key. + val uncompressedPk = "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f" + val expression = "pk($uncompressedPk)" + + // Can do + Descriptor.newSh(expression) + + // No can do + assertFailsWith { + Descriptor.newWsh(expression) + } + } + // Cannot create addr() descriptor. @Test fun cannotCreateAddrDescriptor() { diff --git a/bdk-ffi/src/descriptor.rs b/bdk-ffi/src/descriptor.rs index 7b75c9f9..07e6e459 100644 --- a/bdk-ffi/src/descriptor.rs +++ b/bdk-ffi/src/descriptor.rs @@ -15,6 +15,7 @@ use bdk_wallet::descriptor::{ExtendedDescriptor, IntoWalletDescriptor}; use bdk_wallet::keys::DescriptorPublicKey as BdkDescriptorPublicKey; use bdk_wallet::keys::{DescriptorSecretKey as BdkDescriptorSecretKey, KeyMap}; use bdk_wallet::miniscript::descriptor::ConversionError; +use bdk_wallet::miniscript::Miniscript as BDKMiniscript; use bdk_wallet::template::{ Bip44, Bip44Public, Bip49, Bip49Public, Bip84, Bip84Public, Bip86, Bip86Public, DescriptorTemplate, @@ -482,6 +483,125 @@ impl Descriptor { }) } + /// Create a new wsh descriptor from witness script Errors when miniscript exceeds resource limits + /// under p2sh context or does not type check at the top level + #[uniffi::constructor] + pub fn new_wsh(mini_script: String) -> Result { + let parsed_miniscript = match BDKMiniscript::from_str(&mini_script) { + Ok(miniscript) => miniscript, + Err(e) => { + return Err(DescriptorError::Miniscript { + error_message: e.to_string(), + }); + } + }; + + let miniscript_descriptor = + match bdk_wallet::miniscript::Descriptor::new_wsh(parsed_miniscript) { + Ok(descriptor) => descriptor, + Err(e) => { + return Err(DescriptorError::Miniscript { + error_message: e.to_string(), + }) + } + }; + let extended_descriptor = ExtendedDescriptor::from(miniscript_descriptor); + + Ok(Self { + extended_descriptor, + key_map: KeyMap::new(), + }) + } + + /// Create a new sh wrapped wsh descriptor with witness script Errors when miniscript exceeds resource limits + /// under wsh context or does not type check at the top level + #[uniffi::constructor] + pub fn new_sh_wsh(mini_script: String) -> Result { + let parsed_miniscript = match BDKMiniscript::from_str(&mini_script) { + Ok(miniscript) => miniscript, + Err(e) => { + return Err(DescriptorError::Miniscript { + error_message: e.to_string(), + }); + } + }; + + let miniscript_descriptor = + match bdk_wallet::miniscript::Descriptor::new_sh_wsh(parsed_miniscript) { + Ok(descriptor) => descriptor, + Err(e) => { + return Err(DescriptorError::Miniscript { + error_message: e.to_string(), + }) + } + }; + let extended_descriptor = ExtendedDescriptor::from(miniscript_descriptor); + + Ok(Self { + extended_descriptor, + key_map: KeyMap::new(), + }) + } + + /// Create a new sh for a given redeem script Errors when miniscript exceeds resource limits under p2sh context or does not type check at the top level + #[uniffi::constructor] + pub fn new_sh(mini_script: String) -> Result { + let parsed_miniscript = match BDKMiniscript::from_str(&mini_script) { + Ok(miniscript) => miniscript, + Err(e) => { + return Err(DescriptorError::Miniscript { + error_message: e.to_string(), + }); + } + }; + + let miniscript_descriptor = + match bdk_wallet::miniscript::Descriptor::new_sh(parsed_miniscript) { + Ok(descriptor) => descriptor, + Err(e) => { + return Err(DescriptorError::Miniscript { + error_message: e.to_string(), + }) + } + }; + let extended_descriptor = ExtendedDescriptor::from(miniscript_descriptor); + + Ok(Self { + extended_descriptor, + key_map: KeyMap::new(), + }) + } + + /// Create a new bare descriptor from witness script Errors when miniscript exceeds resource limits + /// under bare context or does not type check at the top level + #[uniffi::constructor] + pub fn new_bare(mini_script: String) -> Result { + let parsed_miniscript = match BDKMiniscript::from_str(&mini_script) { + Ok(miniscript) => miniscript, + Err(e) => { + return Err(DescriptorError::Miniscript { + error_message: e.to_string(), + }); + } + }; + + let miniscript_descriptor = + match bdk_wallet::miniscript::Descriptor::new_bare(parsed_miniscript) { + Ok(descriptor) => descriptor, + Err(e) => { + return Err(DescriptorError::Miniscript { + error_message: e.to_string(), + }) + } + }; + let extended_descriptor = ExtendedDescriptor::from(miniscript_descriptor); + + Ok(Self { + extended_descriptor, + key_map: KeyMap::new(), + }) + } + /// Dangerously convert the descriptor to a string. pub fn to_string_with_secret(&self) -> String { let descriptor = &self.extended_descriptor;