From 61bf092e314eaf90be3f0352d796b70a3d83e910 Mon Sep 17 00:00:00 2001 From: Ferdinando Ametrano Date: Wed, 5 Aug 2026 23:17:31 +0200 Subject: [PATCH 1/2] fix: read the ideographic space back, not only write it to_mnemonic joins a japanese mnemonic with U+3000 while to_entropy, check and expand split on " ", so the library cannot read the sentences it writes: to_entropy raises on all 24 japanese vectors of vectors.json and expand returns the sentence unchanged. The separator is read after NFKD, which maps U+3000 to U+0020, so it cannot be self.delimiter -- that was #110, reverted in df3e150 for failing CI. What is left to split on is any run of whitespace, which is what detect_language already reads. to_seed follows the same rule, so that a sentence check refuses no longer stretches into a different seed in silence. Co-Authored-By: Claude Opus 5 (1M context) --- src/mnemonic/mnemonic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mnemonic/mnemonic.py b/src/mnemonic/mnemonic.py index caa4ffb..3613bdd 100644 --- a/src/mnemonic/mnemonic.py +++ b/src/mnemonic/mnemonic.py @@ -152,7 +152,7 @@ def generate(self, strength: int = 128) -> str: # Adapted from def to_entropy(self, words: list[str] | str) -> bytearray: if not isinstance(words, list): - words = words.split(" ") + words = self.normalize_string(words).split() if len(words) not in [12, 15, 18, 21, 24]: raise ValueError( "Number of words must be one of the following: [12, 15, 18, 21, 24], but it is not (%d)." @@ -210,7 +210,7 @@ def to_mnemonic(self, data: bytes) -> str: return self.delimiter.join(result) def check(self, mnemonic: str) -> bool: - mnemonic_list = self.normalize_string(mnemonic).split(" ") + mnemonic_list = self.normalize_string(mnemonic).split() # list of valid mnemonic lengths if len(mnemonic_list) not in [12, 15, 18, 21, 24]: return False @@ -241,11 +241,11 @@ def expand_word(self, prefix: str) -> str: return prefix def expand(self, mnemonic: str) -> str: - return " ".join(map(self.expand_word, mnemonic.split(" "))) + return " ".join(map(self.expand_word, mnemonic.split())) @classmethod def to_seed(cls, mnemonic: str, passphrase: str = "") -> bytes: - mnemonic = cls.normalize_string(mnemonic) + mnemonic = " ".join(cls.normalize_string(mnemonic).split()) passphrase = cls.normalize_string(passphrase) passphrase = "mnemonic" + passphrase mnemonic_bytes = mnemonic.encode("utf-8") From 21b214a4a40a595829610234ed84762e983e9eb6 Mon Sep 17 00:00:00 2001 From: Ferdinando Ametrano Date: Wed, 5 Aug 2026 23:18:42 +0200 Subject: [PATCH 2/2] test: cover the japanese round trip and the whitespace runs Both fail without the previous commit: to_entropy raises on every japanese vector, and check refuses a sentence separated by anything but one space. The existing round trip hides the first by splitting the sentence itself before handing it over. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_mnemonic.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_mnemonic.py b/tests/test_mnemonic.py index 641cf16..3d13c0a 100755 --- a/tests/test_mnemonic.py +++ b/tests/test_mnemonic.py @@ -141,6 +141,38 @@ def test_expand_word(self) -> None: "action", m.expand_word("acti") ) # unique prefix expanded to word in list + def test_japanese_vectors_to_entropy(self) -> None: + # the ideographic space separates words on every path, not only in + # to_mnemonic; see #110, whose fix split on self.delimiter and was + # reverted because NFKD had already turned U+3000 into a space + with open("vectors.json", "r") as f: + vectors = json.load(f) + m = Mnemonic("japanese") + for v in vectors["japanese"]: + self.assertIn("\u3000", v[1]) + entropy = bytes(m.to_entropy(v[1])) + self.assertEqual(v[0], entropy.hex()) + self.assertEqual(v[1], m.to_mnemonic(entropy)) + + def test_whitespace_runs(self) -> None: + # check(), to_entropy() and to_seed() read the same separator: any + # run of unicode whitespace, once NFKD has been applied + m = Mnemonic("english") + canonical = "abandon " * 11 + "about" + entropy = m.to_entropy(canonical) + seed = Mnemonic.to_seed(canonical) + for variant in ( + " " + canonical, + canonical + "\n", + canonical.replace("abandon about", "abandon about"), + canonical.replace(" ", "\t"), + canonical.replace(" ", "\u3000"), + canonical.replace(" ", "\u00a0"), + ): + self.assertIs(m.check(variant), True) + self.assertEqual(entropy, m.to_entropy(variant)) + self.assertEqual(seed, Mnemonic.to_seed(variant)) + def test_expand(self) -> None: m = Mnemonic("english") self.assertEqual("access", m.expand("access"))