]> Untitled Git - bdk-cli/commitdiff
fix(key): reject word counts other than 12 and 24 in key generate
authorOlorunshogo Moses BAMTEFA <shownzy001@gmail.com>
Sat, 24 Oct 2026 10:39:18 +0000 (11:39 +0100)
committerOlorunshogo Moses BAMTEFA <shownzy001@gmail.com>
Fri, 6 Nov 2026 07:12:28 +0000 (08:12 +0100)
CHANGELOG.md
src/handlers/key.rs
tests/integration/init.rs

index 7d0bdf59cc0e4199b91430bb13f546e647e4f6d8..ccc29bdb284ce02afb8b9baf47d4f0f5733d6225 100644 (file)
@@ -21,6 +21,7 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
 - Fixed `compile` panicking instead of returning an error when a key is valid on its own but the wrong shape for the requested script type (e.g. an x-only key under `sh`/`wsh`/`sh-wsh`, or an uncompressed key under `tr`)
 - Updated dependencies to resolve RUSTSEC advisories in rustls, rustls-webpki, webpki-roots, quinn-proto, h2 and zstd-sys; bumped async-hwi to 0.0.35.
 - Fixed `create_tx`, `create_dns_tx`, `create_sp_tx` and `bump_fee` building transactions over Bitcoin Core's 400,000 wu (100,000 vB) standard weight limit, which no default node relays
+- Fixed `key generate` silently producing a 24-word mnemonic for any `--entropy` value other than 12; values other than 12 and 24 are now rejected
 
 ## [4.0.0]
 
index e7bb6457b6c40eacb428f2f7dc2f2b80348f0fd9..4abf3cdfe0f8f4bcbcdd525c68902cedeb4f46dc 100644 (file)
@@ -9,7 +9,7 @@ use bdk_wallet::keys::{
     DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, bip39::WordCount,
 };
 use bdk_wallet::miniscript::{self, Segwitv0};
-use clap::Parser;
+use clap::{Parser, ValueEnum};
 
 impl KeySubCommand {
     pub fn execute(&self, ctx: &mut AppContext<Init>) -> Result<(), Error> {
@@ -26,6 +26,26 @@ impl KeySubCommand {
         }
     }
 }
+/// Mnemonic lengths supported by `key generate`: 12 and 24, the lengths hardware
+/// wallets and most BIP39 wallets generate. A local mirror of `WordCount`, which
+/// doesn't derive `Clone` and so can't be parsed by clap directly.
+#[derive(Clone, Copy, ValueEnum, Debug, Eq, PartialEq)]
+pub enum MnemonicWordCount {
+    #[value(name = "12")]
+    Words12,
+    #[value(name = "24")]
+    Words24,
+}
+
+impl From<MnemonicWordCount> for WordCount {
+    fn from(word_count: MnemonicWordCount) -> Self {
+        match word_count {
+            MnemonicWordCount::Words12 => WordCount::Words12,
+            MnemonicWordCount::Words24 => WordCount::Words24,
+        }
+    }
+}
+
 #[derive(Parser, Debug, Clone, PartialEq, Eq)]
 pub struct GenerateKeyCommand {
     /// Entropy level based on number of random seed mnemonic words.
@@ -33,9 +53,10 @@ pub struct GenerateKeyCommand {
         env = "WORD_COUNT",
         short = 'e',
         long = "entropy",
-        default_value = "12"
+        value_enum,
+        default_value_t = MnemonicWordCount::Words12
     )]
-    word_count: usize,
+    word_count: MnemonicWordCount,
     /// Seed password.
     #[arg(env = "PASSWORD", short = 'p', long = "password")]
     password: Option<String>,
@@ -46,13 +67,8 @@ impl AppCommand<AppContext<Init>> for GenerateKeyCommand {
 
     fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> {
         let secp = Secp256k1::new();
-        let mnemonic_type = match self.word_count {
-            12 => WordCount::Words12,
-            _ => WordCount::Words24,
-        };
-
         let mnemonic: GeneratedKey<_, miniscript::BareCtx> =
-            Mnemonic::generate((mnemonic_type, Language::English))
+            Mnemonic::generate((self.word_count.into(), Language::English))
                 .map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?;
         let mnemonic = mnemonic.into_key();
         let xkey: ExtendedKey = (mnemonic.clone(), self.password.clone()).into_extended_key()?;
index e0fcea2fd9e8427f5ccc8fee58d0f84450b9d0fc..3454882829f106579581f97733fd529e60de5942 100644 (file)
@@ -19,6 +19,37 @@ mod test_key {
             .stdout(predicate::str::contains("\"fingerprint\":"));
     }
 
+    #[test]
+    fn test_cli_key_generate_word_counts() {
+        let temp_dir = TempDir::new().unwrap();
+        let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf()));
+
+        for word_count in [12, 24] {
+            let output = cli
+                .key_cmd(&["generate", "--entropy", &word_count.to_string()])
+                .output()
+                .expect("Failed to execute generate command");
+            assert!(output.status.success(), "Generate command failed");
+
+            let json: Value = serde_json::from_slice(&output.stdout).expect("Invalid JSON");
+            let mnemonic = json["mnemonic"].as_str().expect("Missing mnemonic");
+            assert_eq!(mnemonic.split_whitespace().count(), word_count);
+        }
+    }
+
+    #[test]
+    fn test_cli_key_generate_rejects_invalid_word_count() {
+        let temp_dir = TempDir::new().unwrap();
+        let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf()));
+
+        for word_count in ["0", "13", "15", "18", "21", "999"] {
+            cli.key_cmd(&["generate", "--entropy", word_count])
+                .assert()
+                .failure()
+                .stderr(predicate::str::contains("possible values"));
+        }
+    }
+
     #[test]
     fn test_cli_key_derive() {
         let temp_dir = TempDir::new().unwrap();