def mnemonic_from_entropy(entropy: Entropy, version_str: str = "standard", lang: str = "en") -> Mnemonic: """Convert input entropy to Electrum versioned mnemonic sentence. Input entropy can be expressed as binary 0/1 string, bytes-like, or integer. In the case of binary 0/1 string and bytes-like, leading zeros are considered redundant padding. """ if version_str not in _MNEMONIC_VERSIONS: err_msg = f"unknown electrum mnemonic version: '{version_str}'; " err_msg += f"not in {list(_MNEMONIC_VERSIONS.keys())}" raise BTClibValueError(err_msg) version = _MNEMONIC_VERSIONS[version_str] bin_str_entropy = bin_str_entropy_from_entropy(entropy) int_entropy = int(bin_str_entropy, 2) base = WORDLISTS.language_length(lang) while True: # electrum considers entropy as integer, losing any leading zero # so the value of bin_str_entropy before the while must be updated nbits = int_entropy.bit_length() bin_str_entropy = bin_str_entropy_from_entropy(int_entropy, nbits) indexes = wordlist_indexes_from_bin_str_entropy(bin_str_entropy, base) mnemonic = mnemonic_from_indexes(indexes, lang) # version validity check s = hmac.new(b"Seed version", mnemonic.encode(), sha512).hexdigest() if s.startswith(version): return mnemonic # next trial int_entropy += 1
def test_conversions() -> None: test_vectors = [ "10101011" * 32, "00101011" * 32, "00000000" + "10101011" * 31, ] for raw in test_vectors: assert bin_str_entropy_from_str(raw) == raw i = int(raw, 2) assert bin_str_entropy_from_int(i) == raw assert bin_str_entropy_from_int(bin(i).upper()) == raw assert bin_str_entropy_from_int(hex(i).upper()) == raw b = i.to_bytes(32, byteorder="big", signed=False) assert bin_str_entropy_from_bytes(b) == raw assert bin_str_entropy_from_bytes(b.hex()) == raw assert bin_str_entropy_from_entropy(raw) == raw assert bin_str_entropy_from_entropy(i) == raw assert bin_str_entropy_from_entropy(b) == raw max_bits = max(_bits) raw = "10" + "11111111" * (max_bits // 8) assert bin_str_entropy_from_entropy(raw) == bin_str_entropy_from_entropy( raw[:-2]) # entr integer has its leftmost bit set to 0 i = 1 << max_bits - 1 bin_str_entropy = bin_str_entropy_from_entropy(i) assert len(bin_str_entropy) == max_bits # entr integer has its leftmost bit set to 1 i = 1 << max_bits bin_str_entropy = bin_str_entropy_from_entropy(i) assert len(bin_str_entropy) == max_bits exp_i = i >> 1 i = int(bin_str_entropy, 2) assert i == exp_i i = secrets.randbits(255) raw = bin_str_entropy_from_int(i) assert int(raw, 2) == i assert len(raw) == 256 assert bin_str_entropy_from_str(raw) == raw assert bin_str_entropy_from_int(hex(i).upper()) == raw b = i.to_bytes(32, byteorder="big", signed=False) assert bin_str_entropy_from_bytes(b) == raw raw2 = bin_str_entropy_from_int(i, 255) assert int(raw2, 2) == i assert len(raw2) == 255 assert bin_str_entropy_from_str("0" + raw2) == raw raw2 = bin_str_entropy_from_str(raw, 128) assert len(raw2) == 128 assert raw2 == raw[:128]
def _entropy_checksum(entropy: Entropy) -> Tuple[BinStr, BinStr]: """Return the checksum of the binary string input entropy. Entropy must be expressed as binary 0/1 string and must be 128, 160, 192, 224, or 256 bits. Leading zeros are considered genuine entropy, not redundant padding. """ bin_str_entropy = bin_str_entropy_from_entropy(entropy) bytes_entropy = bytes_entropy_from_str(bin_str_entropy) # 256-bit checksum bytes_checksum = sha256(bytes_entropy).digest() # integer checksum (leading zeros are lost) int_checksum = int.from_bytes(bytes_checksum, byteorder="big", signed=False) # convert checksum to binary '01' string checksum = bin(int_checksum)[2:] # remove '0b' checksum = checksum.zfill(256) # pad with leading lost zeros # leftmost bits checksum_bits = len(bytes_entropy) // 4 return bin_str_entropy, checksum[:checksum_bits]
def test_exceptions() -> None: bin_str_entropy216 = "00011010" * 27 # 216 bits bin_str_entropy214 = bin_str_entropy216[:-2] # 214 bits entropy = bin_str_entropy_from_entropy(bin_str_entropy214, 214) assert entropy == bin_str_entropy214 # 214 is not in [128, 160, 192, 224, 256, 512] err_msg = "invalid number of bits: " with pytest.raises(BTClibValueError, match=err_msg): bin_str_entropy_from_entropy(bin_str_entropy214) # 214 is not in [216] err_msg = "invalid number of bits: " with pytest.raises(BTClibValueError, match=err_msg): bin_str_entropy_from_entropy(bin_str_entropy214, 216) int_entropy211 = int(bin_str_entropy214, 2) # 211 bits assert int_entropy211.bit_length() == 211 entropy = bin_str_entropy_from_entropy(int_entropy211, 214) assert entropy == bin_str_entropy214 entropy = bin_str_entropy_from_entropy(int_entropy211, 256) assert len(entropy) == 256 assert int(entropy, 2) == int_entropy211 entropy = bin_str_entropy_from_entropy(int_entropy211) assert len(entropy) == 224 assert int(entropy, 2) == int_entropy211 err_msg = "Negative entropy: " with pytest.raises(BTClibValueError, match=err_msg): bin_str_entropy_from_entropy(-1 * int_entropy211) bytes_entropy216 = int_entropy211.to_bytes(27, byteorder="big", signed=False) entropy = bin_str_entropy_from_entropy(bytes_entropy216, 214) assert entropy == bin_str_entropy214 entropy = bin_str_entropy_from_entropy(bytes_entropy216, 216) assert entropy != bin_str_entropy216 err_msg = "invalid number of bits: " with pytest.raises(BTClibValueError, match=err_msg): bin_str_entropy_from_entropy(bytes_entropy216, 224) with pytest.raises(BTClibValueError, match=err_msg): bin_str_entropy_from_entropy(tuple()) # type: ignore with pytest.raises(ValueError): bin_str_entropy_from_int("not an int") # type: ignore with pytest.raises(TypeError): bin_str_entropy_from_str(3) # type: ignore err_msg = "invalid number of bits: " with pytest.raises(BTClibValueError, match=err_msg): bin_str_entropy = "01" * 65 # 130 bits bytes_entropy_from_str(bin_str_entropy)