Skip to content

Commit b9eb100

Browse files
Improve mnemonic logging, add multi-language support, and enhance BIP39 test coverage
1 parent 25f0c28 commit b9eb100

File tree

19 files changed

+24951
-22
lines changed

19 files changed

+24951
-22
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// @formatter:off
2+
/**
3+
* Copyright 2025 Bernard Ladenthin [email protected]
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
// @formatter:on
19+
package net.ladenthin.bitcoinaddressfinder;
20+
21+
import java.io.InputStream;
22+
23+
public enum BIP39Wordlist {
24+
25+
CHINESE_SIMPLIFIED("chinese_simplified.txt"),
26+
CHINESE_TRADITIONAL("chinese_traditional.txt"),
27+
CZECH("czech.txt"),
28+
ENGLISH("english.txt"),
29+
FRENCH("french.txt"),
30+
ITALIAN("italian.txt"),
31+
JAPANESE("japanese.txt"),
32+
KOREAN("korean.txt"),
33+
PORTUGUESE("portuguese.txt"),
34+
RUSSIAN("russian.txt"),
35+
SPANISH("spanish.txt"),
36+
TURKISH("turkish.txt");
37+
38+
/**
39+
* Unicode character for an ideographic space (U+3000), used in East Asian languages such as Japanese.
40+
* This space is wider than a standard space and is required for correct formatting of Japanese mnemonics.
41+
*/
42+
public static final String IDEOGRAPHIC_SPACE = "\u3000";
43+
44+
/**
45+
* Standard ASCII space character (U+0020).
46+
* Used as the default separator between words in most BIP39 wordlists, such as English.
47+
*/
48+
public static final String NORMAL_SPACE = " ";
49+
50+
/**
51+
* The name of the wordlist file associated with this BIP39 language.
52+
* <p>
53+
* This filename is used to locate the corresponding wordlist resource file in the
54+
* {@code /mnemonic/wordlist/} directory of the classpath (e.g., {@code english.txt}).
55+
*/
56+
private final String fileName;
57+
58+
/**
59+
* Constructs a BIP39 wordlist enum constant with the associated filename.
60+
*
61+
* @param fileName the name of the wordlist file (e.g., {@code "english.txt"}), which is used
62+
* to load the corresponding wordlist resource from the classpath
63+
*/
64+
BIP39Wordlist(String fileName) {
65+
this.fileName = fileName;
66+
}
67+
68+
/**
69+
* Loads the BIP39 wordlist file as an {@link InputStream} for this language.
70+
* <p>
71+
* The wordlist file is expected to be located in the resource path:
72+
* {@code /mnemonic/wordlist/{fileName}}, where {@code fileName} corresponds
73+
* to the file name associated with the enum constant (e.g. {@code english.txt}).
74+
* <p>
75+
* This method is used to initialize a {@link MnemonicCode} instance with the correct
76+
* wordlist for a given language.
77+
*
78+
* @return the input stream of the wordlist file for this language, or {@code null}
79+
* if the resource is not found.
80+
*/
81+
public InputStream getWordListStream() {
82+
return BIP39Wordlist.class.getResourceAsStream("/mnemonic/wordlist/" + fileName);
83+
}
84+
85+
/**
86+
* Converts a filename-like language name into the corresponding {@link BIP39Wordlist} enum constant.
87+
* <p>
88+
* The input typically comes from wordlist filenames in the format {@code mnemonic/wordlist/{language}.txt},
89+
* for example:
90+
* <ul>
91+
* <li>{@code chinese_simplified.txt} → {@code CHINESE_SIMPLIFIED}</li>
92+
* <li>{@code english.txt} → {@code ENGLISH}</li>
93+
* </ul>
94+
* <p>
95+
* To perform the conversion, this method:
96+
* <ul>
97+
* <li>Converts the name to upper case</li>
98+
* <li>Replaces hyphens with underscores (if any)</li>
99+
* </ul>
100+
* This enables consistent mapping between file-based wordlist identifiers and enum values.
101+
*
102+
* @param name the lowercase filename-based language identifier, e.g. {@code "english"} or {@code "chinese_simplified"}
103+
* @return the corresponding {@link BIP39Wordlist} enum constant
104+
* @throws IllegalArgumentException if no matching enum exists
105+
*/
106+
public static BIP39Wordlist fromLanguageName(String name) {
107+
return valueOf(name.toUpperCase().replace('-', '_'));
108+
}
109+
110+
/**
111+
* Returns the word separator used in the mnemonic phrase for the given language.
112+
* <p>
113+
* Most languages use a single space (" ") as the separator between words.
114+
* However, Japanese uses the IDEOGRAPHIC SPACE (U+3000) to conform with the official BIP39 specification.
115+
*
116+
* @return the word separator specific to the language
117+
*/
118+
public String getSeparator() {
119+
if (this == JAPANESE) {
120+
return IDEOGRAPHIC_SPACE;
121+
}
122+
return NORMAL_SPACE;
123+
}
124+
}

src/main/java/net/ladenthin/bitcoinaddressfinder/ConsumerJava.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,23 @@ public boolean containsAddress(ByteBuffer threadLocalReuseableByteBuffer, byte[]
261261
}
262262

263263
/**
264-
* Try to log safe informations which may not thrown an exception.
264+
* Logs key information in a safe and robust way to avoid losing critical data
265+
* in case of a runtime exception.
266+
* <p>
267+
* The primary goal of this method is to ensure that if a valid secret key (i.e., a hit)
268+
* is found, its corresponding BigInteger value is immediately logged. Since logging a
269+
* BigInteger is unlikely to fail, this is the first and most essential piece of information.
270+
* <p>
271+
* Logging additional details such as the uncompressed/compressed public keys and their
272+
* hash160 values may theoretically trigger runtime exceptions (e.g., due to malformed data
273+
* or encoding issues). To mitigate the risk of losing the crucial secret value in such rare
274+
* cases, it is logged first.
275+
* <p>
276+
* All logs are prefixed consistently with {@code HIT_SAFE_PREFIX} to make hits easily searchable.
277+
*
278+
* @param publicKeyBytes the public key bytes wrapper
279+
* @param hash160Uncompressed the hash160 of the uncompressed public key
280+
* @param hash160Compressed the hash160 of the compressed public key
265281
*/
266282
private void safeLog(PublicKeyBytes publicKeyBytes, byte[] hash160Uncompressed, byte[] hash160Compressed) {
267283
logger.info(HIT_SAFE_PREFIX +"publicKeyBytes.getSecretKey(): " + publicKeyBytes.getSecretKey());

src/main/java/net/ladenthin/bitcoinaddressfinder/KeyUtility.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// @formatter:on
1919
package net.ladenthin.bitcoinaddressfinder;
2020

21+
import java.io.IOException;
2122
import java.math.BigInteger;
2223
import java.nio.ByteBuffer;
2324
import java.util.Arrays;
@@ -43,8 +44,6 @@ public class KeyUtility {
4344
@NonNull
4445
public final Network network;
4546
public final ByteBufferUtility byteBufferUtility;
46-
47-
private final MnemonicCode mnemonicCode = MnemonicCode.INSTANCE;
4847

4948
public KeyUtility(Network network, ByteBufferUtility byteBufferUtility) {
5049
this.network = network;
@@ -148,13 +147,37 @@ public String createKeyDetails(ECKey key) throws MnemonicException.MnemonicLengt
148147
String logPublicKeyHash160 = "publicKeyHash160Hex: [" + publicKeyHash160Hex + "]";
149148
String logPublicKeyHash160Base58 = "publicKeyHash160Base58: [" + publicKeyHash160Base58 + "]";
150149
String logCompressed = "Compressed: [" + key.isCompressed() + "]";
151-
List<String> mnemonic = mnemonicCode.toMnemonic(privateKeyBytes);
152-
String logMnemonic = "Mnemonic: " + mnemonic.toString();
150+
String logMnemonic = createMnemonics(privateKeyBytes);
153151

154152
String space = " ";
155153
return logprivateKeyBigInteger + space + logprivateKeyBytes + space + logprivateKeyHex + space + logWiF + space + logPublicKeyAsHex + space + logPublicKeyHash160 + space + logPublicKeyHash160Base58 + space + logCompressed + space + logMnemonic;
156154
}
157155

156+
public String createMnemonics(byte[] privateKeyBytes) {
157+
StringBuilder logMnemonic = new StringBuilder("Mnemonic:");
158+
for (BIP39Wordlist wordList : BIP39Wordlist.values()) {
159+
try {
160+
MnemonicCode mnemonicCode = new MnemonicCode(wordList.getWordListStream(), null);
161+
List<String> mnemonics = mnemonicCode.toMnemonic(privateKeyBytes);
162+
logMnemonic.append(" ");
163+
logMnemonic.append(wordList.name());
164+
logMnemonic.append(": [");
165+
boolean first = true;
166+
for(String mnemonic : mnemonics) {
167+
if (!first) {
168+
logMnemonic.append(wordList.getSeparator());
169+
}
170+
logMnemonic.append(mnemonic);
171+
first = false;
172+
}
173+
logMnemonic.append("]");
174+
} catch (IOException | IllegalArgumentException ex) {
175+
throw new RuntimeException(ex);
176+
}
177+
}
178+
return logMnemonic.toString();
179+
}
180+
158181
// <editor-fold defaultstate="collapsed" desc="ByteBuffer LegacyAddress conversion">
159182
public ByteBuffer addressToByteBuffer(LegacyAddress address) {
160183
ByteBuffer byteBuffer = byteBufferUtility.byteArrayToByteBuffer(address.getHash());

0 commit comments

Comments
 (0)