]> Untitled Git - bdk-cli/commitdiff
ref(compile):compile tr desc with rand unspend key
authorVihiga Tyonum <withtvpeter@gmail.com>
Sat, 4 Jul 2026 11:01:20 +0000 (12:01 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Sat, 4 Jul 2026 11:01:20 +0000 (12:01 +0100)
- add compile taproot descriptor with randomized
unspendable internal key

src/handlers/descriptor.rs
src/utils/descriptors.rs
src/utils/types.rs

index 1396c8887920666d7897a2ed0bb2ec3676d08d85..1c949cdf283c0877b21984e18f3a39f222b22179 100644 (file)
@@ -15,7 +15,11 @@ use clap::Parser;
 #[cfg(feature = "compiler")]
 use {
     bdk_wallet::{
-        bitcoin::XOnlyPublicKey,
+        bitcoin::{
+            XOnlyPublicKey,
+            key::{Parity, rand},
+            secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey},
+        },
         miniscript::{Descriptor, Miniscript, descriptor::TapTree, policy::Concrete},
     },
     std::{str::FromStr, sync::Arc},
@@ -89,15 +93,32 @@ impl AppCommand<AppContext<Init>> for CompileCommand {
             .compile()
             .map_err(|e| Error::Generic(e.to_string()))?;
 
+        let mut r = None;
+
         let descriptor = match self.script_type.as_str() {
             "sh" => Descriptor::new_sh(legacy_policy),
             "wsh" => Descriptor::new_wsh(segwit_policy),
             "sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
             "tr" => {
-                let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
+                // Use a randomized unspendable internal key (H + rG) instead of a fixed NUMS
+                // point. This improves privacy by preventing observers from determining whether
+                // key-path spending is disabled. `r` is returned so the user can verify the key
+                // is derived from the NUMS point. See BIP-341.
+                let secp = Secp256k1::new();
+                let r_secret = SecretKey::new(&mut rand::thread_rng());
+                r = Some(r_secret.display_secret().to_string());
+
+                let nums_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
                     .map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?;
+                let nums_point = PublicKey::from_x_only_public_key(nums_key, Parity::Even);
+
+                let internal_key_point = nums_point
+                    .add_exp_tweak(&secp, &Scalar::from(r_secret))
+                    .map_err(|e| Error::Generic(format!("Failed to tweak NUMS key: {e}")))?;
+                let (xonly_internal_key, _) = internal_key_point.x_only_public_key();
+
                 let tree = TapTree::Leaf(Arc::new(taproot_policy));
-                Descriptor::new_tr(xonly_public_key.to_string(), Some(tree))
+                Descriptor::new_tr(xonly_internal_key.to_string(), Some(tree))
             }
             _ => {
                 return Err(Error::Generic(
@@ -113,6 +134,7 @@ impl AppCommand<AppContext<Init>> for CompileCommand {
             public_descriptors: None,
             private_descriptors: None,
             fingerprint: None,
+            r,
         })
     }
 }
index 04391e503214acaa184f55eb110323b5745f79b7..44e795f154942c759f9b1197187635d729290fa0 100644 (file)
@@ -102,6 +102,7 @@ fn generate_private_descriptors(
         }),
         mnemonic: None,
         fingerprint: Some(fingerprint.to_string()),
+        r: None,
     })
 }
 
@@ -139,6 +140,7 @@ pub fn generate_public_descriptors(
         private_descriptors: None,
         mnemonic: None,
         fingerprint: Some(fingerprint.to_string()),
+        r: None,
     })
 }
 
index a8e29984c1f2af500d18dca1fe6cd0470b64b9b8..6f8cf0913ebd5213c2e8afb9cf47752e2d08ada9 100644 (file)
@@ -170,6 +170,10 @@ pub struct DescriptorResult {
 
     #[serde(skip_serializing_if = "Option::is_none")]
     pub fingerprint: Option<String>,
+
+    /// Randomness factor `r` used to derive the taproot unspendable internal key (H + rG).
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r: Option<String>,
 }
 
 #[derive(Serialize)]