643 lines
19 KiB
Rust
643 lines
19 KiB
Rust
//! 助记词管理模块
|
||
//!
|
||
//! 实现BIP39助记词导入、验证、密钥派生和地址生成
|
||
|
||
use crate::WalletError;
|
||
use sha2::{Sha256, Sha512, Digest};
|
||
use std::collections::HashMap;
|
||
|
||
/// BIP39助记词语言
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum Language {
|
||
/// 英语
|
||
English,
|
||
/// 简体中文
|
||
SimplifiedChinese,
|
||
/// 繁体中文
|
||
TraditionalChinese,
|
||
/// 日语
|
||
Japanese,
|
||
/// 韩语
|
||
Korean,
|
||
/// 法语
|
||
French,
|
||
/// 意大利语
|
||
Italian,
|
||
/// 西班牙语
|
||
Spanish,
|
||
}
|
||
|
||
/// 助记词长度
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum MnemonicLength {
|
||
/// 12个单词 (128位熵)
|
||
Words12 = 12,
|
||
/// 15个单词 (160位熵)
|
||
Words15 = 15,
|
||
/// 18个单词 (192位熵)
|
||
Words18 = 18,
|
||
/// 21个单词 (224位熵)
|
||
Words21 = 21,
|
||
/// 24个单词 (256位熵)
|
||
Words24 = 24,
|
||
}
|
||
|
||
impl MnemonicLength {
|
||
/// 获取熵长度(位)
|
||
pub fn entropy_bits(&self) -> usize {
|
||
match self {
|
||
MnemonicLength::Words12 => 128,
|
||
MnemonicLength::Words15 => 160,
|
||
MnemonicLength::Words18 => 192,
|
||
MnemonicLength::Words21 => 224,
|
||
MnemonicLength::Words24 => 256,
|
||
}
|
||
}
|
||
|
||
/// 获取熵长度(字节)
|
||
pub fn entropy_bytes(&self) -> usize {
|
||
self.entropy_bits() / 8
|
||
}
|
||
}
|
||
|
||
/// 助记词
|
||
#[derive(Debug, Clone)]
|
||
pub struct Mnemonic {
|
||
/// 助记词单词列表
|
||
words: Vec<String>,
|
||
/// 语言
|
||
language: Language,
|
||
/// 熵
|
||
entropy: Vec<u8>,
|
||
}
|
||
|
||
impl Mnemonic {
|
||
/// 从单词列表创建助记词
|
||
pub fn from_words(words: Vec<String>, language: Language) -> Result<Self, WalletError> {
|
||
// 验证单词数量
|
||
let word_count = words.len();
|
||
let valid_counts = [12, 15, 18, 21, 24];
|
||
if !valid_counts.contains(&word_count) {
|
||
return Err(WalletError::KeyError(
|
||
format!("Invalid word count: {}, expected one of {:?}", word_count, valid_counts)
|
||
));
|
||
}
|
||
|
||
// 验证每个单词
|
||
let wordlist = get_wordlist(language);
|
||
for word in &words {
|
||
if !wordlist.contains_key(word.as_str()) {
|
||
return Err(WalletError::KeyError(
|
||
format!("Invalid word: {}", word)
|
||
));
|
||
}
|
||
}
|
||
|
||
// 将单词转换为索引
|
||
let indices: Vec<u16> = words.iter()
|
||
.map(|w| *wordlist.get(w.as_str()).unwrap())
|
||
.collect();
|
||
|
||
// 从索引恢复熵和校验和
|
||
let entropy_bits = word_count * 11;
|
||
let checksum_bits = entropy_bits / 33;
|
||
let entropy_length = (entropy_bits - checksum_bits) / 8;
|
||
|
||
let mut bits = Vec::new();
|
||
for index in indices {
|
||
for i in (0..11).rev() {
|
||
bits.push((index >> i) & 1 == 1);
|
||
}
|
||
}
|
||
|
||
// 提取熵
|
||
let mut entropy = vec![0u8; entropy_length];
|
||
for (i, bit) in bits.iter().take(entropy_length * 8).enumerate() {
|
||
if *bit {
|
||
entropy[i / 8] |= 1 << (7 - (i % 8));
|
||
}
|
||
}
|
||
|
||
// 验证校验和
|
||
let mut hasher = Sha256::new();
|
||
hasher.update(&entropy);
|
||
let hash = hasher.finalize();
|
||
|
||
let mut checksum_bits_actual = Vec::new();
|
||
for i in 0..checksum_bits {
|
||
checksum_bits_actual.push((hash[i / 8] >> (7 - (i % 8))) & 1 == 1);
|
||
}
|
||
|
||
let checksum_bits_expected: Vec<bool> = bits.iter()
|
||
.skip(entropy_length * 8)
|
||
.copied()
|
||
.collect();
|
||
|
||
if checksum_bits_actual != checksum_bits_expected {
|
||
return Err(WalletError::KeyError(
|
||
"Invalid checksum".to_string()
|
||
));
|
||
}
|
||
|
||
Ok(Mnemonic {
|
||
words,
|
||
language,
|
||
entropy,
|
||
})
|
||
}
|
||
|
||
/// 从熵创建助记词
|
||
pub fn from_entropy(entropy: &[u8], language: Language) -> Result<Self, WalletError> {
|
||
// 验证熵长度
|
||
let entropy_bits = entropy.len() * 8;
|
||
let valid_bits = [128, 160, 192, 224, 256];
|
||
if !valid_bits.contains(&entropy_bits) {
|
||
return Err(WalletError::KeyError(
|
||
format!("Invalid entropy length: {} bits", entropy_bits)
|
||
));
|
||
}
|
||
|
||
// 计算校验和
|
||
let mut hasher = Sha256::new();
|
||
hasher.update(entropy);
|
||
let hash = hasher.finalize();
|
||
|
||
let checksum_bits = entropy_bits / 32;
|
||
|
||
// 组合熵和校验和
|
||
let mut bits = Vec::new();
|
||
for byte in entropy {
|
||
for i in (0..8).rev() {
|
||
bits.push((byte >> i) & 1 == 1);
|
||
}
|
||
}
|
||
|
||
for i in 0..checksum_bits {
|
||
bits.push((hash[i / 8] >> (7 - (i % 8))) & 1 == 1);
|
||
}
|
||
|
||
// 将11位分组转换为单词索引
|
||
let wordlist_array = get_wordlist_array(language);
|
||
let mut words = Vec::new();
|
||
|
||
for chunk in bits.chunks(11) {
|
||
let mut index = 0u16;
|
||
for (i, bit) in chunk.iter().enumerate() {
|
||
if *bit {
|
||
index |= 1 << (10 - i);
|
||
}
|
||
}
|
||
words.push(wordlist_array[index as usize].to_string());
|
||
}
|
||
|
||
Ok(Mnemonic {
|
||
words,
|
||
language,
|
||
entropy: entropy.to_vec(),
|
||
})
|
||
}
|
||
|
||
/// 获取助记词单词列表
|
||
pub fn words(&self) -> &[String] {
|
||
&self.words
|
||
}
|
||
|
||
/// 获取助记词字符串
|
||
pub fn phrase(&self) -> String {
|
||
self.words.join(" ")
|
||
}
|
||
|
||
/// 获取熵
|
||
pub fn entropy(&self) -> &[u8] {
|
||
&self.entropy
|
||
}
|
||
|
||
/// 获取语言
|
||
pub fn language(&self) -> Language {
|
||
self.language
|
||
}
|
||
|
||
/// 派生种子(BIP39)
|
||
pub fn to_seed(&self, passphrase: &str) -> Vec<u8> {
|
||
let mnemonic = self.phrase();
|
||
let salt = format!("mnemonic{}", passphrase);
|
||
|
||
// PBKDF2-HMAC-SHA512
|
||
pbkdf2_hmac_sha512(
|
||
mnemonic.as_bytes(),
|
||
salt.as_bytes(),
|
||
2048,
|
||
64
|
||
)
|
||
}
|
||
}
|
||
|
||
/// PBKDF2-HMAC-SHA512密钥派生
|
||
fn pbkdf2_hmac_sha512(password: &[u8], salt: &[u8], iterations: usize, output_len: usize) -> Vec<u8> {
|
||
use hmac::{Hmac, Mac};
|
||
type HmacSha512 = Hmac<Sha512>;
|
||
|
||
let mut output = vec![0u8; output_len];
|
||
let hlen = 64; // SHA512输出长度
|
||
let blocks = (output_len + hlen - 1) / hlen;
|
||
|
||
for i in 1..=blocks {
|
||
let mut mac = HmacSha512::new_from_slice(password).unwrap();
|
||
mac.update(salt);
|
||
mac.update(&(i as u32).to_be_bytes());
|
||
let mut u = mac.finalize().into_bytes().to_vec();
|
||
let mut f = u.clone();
|
||
|
||
for _ in 1..iterations {
|
||
let mut mac = HmacSha512::new_from_slice(password).unwrap();
|
||
mac.update(&u);
|
||
u = mac.finalize().into_bytes().to_vec();
|
||
|
||
for (f_byte, u_byte) in f.iter_mut().zip(u.iter()) {
|
||
*f_byte ^= u_byte;
|
||
}
|
||
}
|
||
|
||
let start = (i - 1) * hlen;
|
||
let end = std::cmp::min(start + hlen, output_len);
|
||
output[start..end].copy_from_slice(&f[..end - start]);
|
||
}
|
||
|
||
output
|
||
}
|
||
|
||
/// 密钥派生路径(BIP32/BIP44)
|
||
#[derive(Debug, Clone)]
|
||
pub struct DerivationPath {
|
||
/// 路径组件
|
||
components: Vec<u32>,
|
||
}
|
||
|
||
impl DerivationPath {
|
||
/// 从字符串解析派生路径
|
||
/// 格式: m/44'/60'/0'/0/0
|
||
pub fn from_str(path: &str) -> Result<Self, WalletError> {
|
||
if !path.starts_with("m/") && !path.starts_with("M/") {
|
||
return Err(WalletError::KeyError(
|
||
"Derivation path must start with m/ or M/".to_string()
|
||
));
|
||
}
|
||
|
||
let parts: Vec<&str> = path[2..].split('/').collect();
|
||
let mut components = Vec::new();
|
||
|
||
for part in parts {
|
||
if part.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
let (index_str, hardened) = if part.ends_with('\'') || part.ends_with('h') {
|
||
(&part[..part.len() - 1], true)
|
||
} else {
|
||
(part, false)
|
||
};
|
||
|
||
let index: u32 = index_str.parse()
|
||
.map_err(|_| WalletError::KeyError(
|
||
format!("Invalid index in derivation path: {}", part)
|
||
))?;
|
||
|
||
let component = if hardened {
|
||
index | 0x80000000
|
||
} else {
|
||
index
|
||
};
|
||
|
||
components.push(component);
|
||
}
|
||
|
||
Ok(DerivationPath { components })
|
||
}
|
||
|
||
/// 创建标准BIP44路径
|
||
/// m/44'/coin_type'/account'/change/address_index
|
||
pub fn bip44(coin_type: u32, account: u32, change: u32, address_index: u32) -> Self {
|
||
DerivationPath {
|
||
components: vec![
|
||
44 | 0x80000000, // purpose: 44' (BIP44)
|
||
coin_type | 0x80000000, // coin_type: hardened
|
||
account | 0x80000000, // account: hardened
|
||
change, // change: 0=external, 1=internal
|
||
address_index, // address_index
|
||
],
|
||
}
|
||
}
|
||
|
||
/// 获取路径组件
|
||
pub fn components(&self) -> &[u32] {
|
||
&self.components
|
||
}
|
||
|
||
/// 转换为字符串
|
||
pub fn to_string(&self) -> String {
|
||
let mut path = String::from("m");
|
||
for component in &self.components {
|
||
let hardened = component & 0x80000000 != 0;
|
||
let index = component & 0x7fffffff;
|
||
path.push('/');
|
||
path.push_str(&index.to_string());
|
||
if hardened {
|
||
path.push('\'');
|
||
}
|
||
}
|
||
path
|
||
}
|
||
}
|
||
|
||
/// 扩展密钥
|
||
#[derive(Debug, Clone)]
|
||
pub struct ExtendedKey {
|
||
/// 私钥
|
||
private_key: Vec<u8>,
|
||
/// 链码
|
||
chain_code: Vec<u8>,
|
||
/// 深度
|
||
depth: u8,
|
||
/// 父指纹
|
||
parent_fingerprint: [u8; 4],
|
||
/// 子索引
|
||
child_index: u32,
|
||
}
|
||
|
||
impl ExtendedKey {
|
||
/// 从种子创建主密钥
|
||
pub fn from_seed(seed: &[u8]) -> Result<Self, WalletError> {
|
||
use hmac::{Hmac, Mac};
|
||
type HmacSha512 = Hmac<Sha512>;
|
||
|
||
let mut mac = HmacSha512::new_from_slice(b"Bitcoin seed")
|
||
.map_err(|e| WalletError::KeyError(e.to_string()))?;
|
||
mac.update(seed);
|
||
let result = mac.finalize().into_bytes();
|
||
|
||
let private_key = result[..32].to_vec();
|
||
let chain_code = result[32..].to_vec();
|
||
|
||
Ok(ExtendedKey {
|
||
private_key,
|
||
chain_code,
|
||
depth: 0,
|
||
parent_fingerprint: [0; 4],
|
||
child_index: 0,
|
||
})
|
||
}
|
||
|
||
/// 派生子密钥
|
||
pub fn derive(&self, index: u32) -> Result<Self, WalletError> {
|
||
use hmac::{Hmac, Mac};
|
||
type HmacSha512 = Hmac<Sha512>;
|
||
|
||
let hardened = index & 0x80000000 != 0;
|
||
|
||
let mut mac = HmacSha512::new_from_slice(&self.chain_code)
|
||
.map_err(|e| WalletError::KeyError(e.to_string()))?;
|
||
|
||
if hardened {
|
||
// 硬化派生: HMAC-SHA512(Key = cpar, Data = 0x00 || ser256(kpar) || ser32(i))
|
||
mac.update(&[0]);
|
||
mac.update(&self.private_key);
|
||
} else {
|
||
// 普通派生: HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) || ser32(i))
|
||
// 简化实现:使用私钥
|
||
mac.update(&self.private_key);
|
||
}
|
||
mac.update(&index.to_be_bytes());
|
||
|
||
let result = mac.finalize().into_bytes();
|
||
let child_key = result[..32].to_vec();
|
||
let child_chain = result[32..].to_vec();
|
||
|
||
// 计算父指纹(简化实现)
|
||
let mut hasher = Sha256::new();
|
||
hasher.update(&self.private_key);
|
||
let hash = hasher.finalize();
|
||
let mut parent_fingerprint = [0u8; 4];
|
||
parent_fingerprint.copy_from_slice(&hash[..4]);
|
||
|
||
Ok(ExtendedKey {
|
||
private_key: child_key,
|
||
chain_code: child_chain,
|
||
depth: self.depth + 1,
|
||
parent_fingerprint,
|
||
child_index: index,
|
||
})
|
||
}
|
||
|
||
/// 按路径派生
|
||
pub fn derive_path(&self, path: &DerivationPath) -> Result<Self, WalletError> {
|
||
let mut key = self.clone();
|
||
for &component in path.components() {
|
||
key = key.derive(component)?;
|
||
}
|
||
Ok(key)
|
||
}
|
||
|
||
/// 获取私钥
|
||
pub fn private_key(&self) -> &[u8] {
|
||
&self.private_key
|
||
}
|
||
|
||
/// 获取链码
|
||
pub fn chain_code(&self) -> &[u8] {
|
||
&self.chain_code
|
||
}
|
||
}
|
||
|
||
/// 地址生成器
|
||
pub struct AddressGenerator;
|
||
|
||
impl AddressGenerator {
|
||
/// 从扩展密钥生成地址
|
||
pub fn from_extended_key(key: &ExtendedKey) -> String {
|
||
// 使用私钥生成地址(简化实现)
|
||
let mut hasher = Sha256::new();
|
||
hasher.update(key.private_key());
|
||
let hash = hasher.finalize();
|
||
|
||
// 转换为十六进制字符串
|
||
format!("0x{}", hex::encode(&hash[..20]))
|
||
}
|
||
|
||
/// 从助记词和路径生成地址
|
||
pub fn from_mnemonic(mnemonic: &Mnemonic, path: &DerivationPath, passphrase: &str) -> Result<String, WalletError> {
|
||
let seed = mnemonic.to_seed(passphrase);
|
||
let master_key = ExtendedKey::from_seed(&seed)?;
|
||
let derived_key = master_key.derive_path(path)?;
|
||
Ok(Self::from_extended_key(&derived_key))
|
||
}
|
||
|
||
/// 批量生成地址
|
||
pub fn generate_addresses(
|
||
mnemonic: &Mnemonic,
|
||
coin_type: u32,
|
||
account: u32,
|
||
count: usize,
|
||
passphrase: &str
|
||
) -> Result<Vec<(String, DerivationPath)>, WalletError> {
|
||
let mut addresses = Vec::new();
|
||
let seed = mnemonic.to_seed(passphrase);
|
||
let master_key = ExtendedKey::from_seed(&seed)?;
|
||
|
||
for i in 0..count {
|
||
let path = DerivationPath::bip44(coin_type, account, 0, i as u32);
|
||
let derived_key = master_key.derive_path(&path)?;
|
||
let address = Self::from_extended_key(&derived_key);
|
||
addresses.push((address, path));
|
||
}
|
||
|
||
Ok(addresses)
|
||
}
|
||
}
|
||
|
||
/// 获取单词列表(单词 -> 索引)
|
||
fn get_wordlist(language: Language) -> HashMap<&'static str, u16> {
|
||
let words = get_wordlist_array(language);
|
||
words.iter()
|
||
.enumerate()
|
||
.map(|(i, &word)| (word, i as u16))
|
||
.collect()
|
||
}
|
||
|
||
/// 获取单词列表数组(索引 -> 单词)
|
||
fn get_wordlist_array(language: Language) -> &'static [&'static str] {
|
||
match language {
|
||
Language::English => &ENGLISH_WORDLIST,
|
||
Language::SimplifiedChinese => &CHINESE_SIMPLIFIED_WORDLIST,
|
||
_ => &ENGLISH_WORDLIST, // 其他语言暂时使用英语
|
||
}
|
||
}
|
||
|
||
/// 英语单词列表(BIP39标准,2048个单词)
|
||
/// 这里只列出前20个作为示例,实际应包含全部2048个单词
|
||
const ENGLISH_WORDLIST: &[&str] = &[
|
||
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract",
|
||
"absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
|
||
"acoustic", "acquire", "across", "act",
|
||
// ... 省略其余2028个单词
|
||
// 实际使用时应包含完整的2048个BIP39英语单词
|
||
];
|
||
|
||
/// 简体中文单词列表(BIP39标准,2048个单词)
|
||
const CHINESE_SIMPLIFIED_WORDLIST: &[&str] = &[
|
||
"的", "一", "是", "在", "不", "了", "有", "和",
|
||
"人", "这", "中", "大", "为", "上", "个", "国",
|
||
"我", "以", "要", "他",
|
||
// ... 省略其余2028个单词
|
||
];
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_mnemonic_from_entropy() {
|
||
let entropy = vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0];
|
||
let mnemonic = Mnemonic::from_entropy(&entropy, Language::English).unwrap();
|
||
assert_eq!(mnemonic.words().len(), 12);
|
||
assert_eq!(mnemonic.entropy(), &entropy);
|
||
}
|
||
|
||
#[test]
|
||
fn test_mnemonic_to_seed() {
|
||
let entropy = vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0];
|
||
let mnemonic = Mnemonic::from_entropy(&entropy, Language::English).unwrap();
|
||
let seed = mnemonic.to_seed("");
|
||
assert_eq!(seed.len(), 64);
|
||
}
|
||
|
||
#[test]
|
||
fn test_derivation_path_parse() {
|
||
let path = DerivationPath::from_str("m/44'/60'/0'/0/0").unwrap();
|
||
assert_eq!(path.components().len(), 5);
|
||
assert_eq!(path.components()[0], 44 | 0x80000000);
|
||
assert_eq!(path.components()[1], 60 | 0x80000000);
|
||
assert_eq!(path.components()[4], 0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_derivation_path_bip44() {
|
||
let path = DerivationPath::bip44(60, 0, 0, 0);
|
||
assert_eq!(path.components().len(), 5);
|
||
assert_eq!(path.to_string(), "m/44'/60'/0'/0/0");
|
||
}
|
||
|
||
#[test]
|
||
fn test_extended_key_from_seed() {
|
||
let seed = vec![0u8; 64];
|
||
let key = ExtendedKey::from_seed(&seed).unwrap();
|
||
assert_eq!(key.private_key().len(), 32);
|
||
assert_eq!(key.chain_code().len(), 32);
|
||
assert_eq!(key.depth, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_extended_key_derive() {
|
||
let seed = vec![0u8; 64];
|
||
let master = ExtendedKey::from_seed(&seed).unwrap();
|
||
let child = master.derive(0).unwrap();
|
||
assert_eq!(child.depth, 1);
|
||
assert_eq!(child.child_index, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_extended_key_derive_path() {
|
||
let seed = vec![0u8; 64];
|
||
let master = ExtendedKey::from_seed(&seed).unwrap();
|
||
let path = DerivationPath::bip44(60, 0, 0, 0);
|
||
let derived = master.derive_path(&path).unwrap();
|
||
assert_eq!(derived.depth, 5);
|
||
}
|
||
|
||
#[test]
|
||
fn test_address_from_extended_key() {
|
||
let seed = vec![0u8; 64];
|
||
let key = ExtendedKey::from_seed(&seed).unwrap();
|
||
let address = AddressGenerator::from_extended_key(&key);
|
||
assert!(address.starts_with("0x"));
|
||
assert_eq!(address.len(), 42); // 0x + 40 hex chars
|
||
}
|
||
|
||
#[test]
|
||
fn test_address_from_mnemonic() {
|
||
let entropy = vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0];
|
||
let mnemonic = Mnemonic::from_entropy(&entropy, Language::English).unwrap();
|
||
let path = DerivationPath::bip44(60, 0, 0, 0);
|
||
let address = AddressGenerator::from_mnemonic(&mnemonic, &path, "").unwrap();
|
||
assert!(address.starts_with("0x"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_generate_addresses() {
|
||
let entropy = vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0];
|
||
let mnemonic = Mnemonic::from_entropy(&entropy, Language::English).unwrap();
|
||
let addresses = AddressGenerator::generate_addresses(&mnemonic, 60, 0, 5, "").unwrap();
|
||
assert_eq!(addresses.len(), 5);
|
||
for (address, _path) in addresses {
|
||
assert!(address.starts_with("0x"));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_invalid_word_count() {
|
||
let words = vec!["abandon".to_string(); 11]; // 无效数量
|
||
let result = Mnemonic::from_words(words, Language::English);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_invalid_entropy_length() {
|
||
let entropy = vec![0u8; 15]; // 无效长度
|
||
let result = Mnemonic::from_entropy(&entropy, Language::English);
|
||
assert!(result.is_err());
|
||
}
|
||
}
|