add initial support for codex32

* Refactor Bech32 to separate validation, decoding, and checksum

Also make the charset and some things public, and add a new flag
to convertBits whether to enforce the last padded bits are zeros.
This will be used to support Codex32/BIP93.

* Add initial BIP93 support

This adds initial support for Codex32, specifically support for
importing secret shares and deriving BIP32 secrets from them.
Deriving shares is not yet implemented.
This commit is contained in:
Ian McKenzie 2025-11-26 21:37:38 -08:00 committed by GitHub
parent cb6fc2a066
commit a378e1a33b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 571 additions and 21 deletions

View File

@ -22,10 +22,10 @@ import java.util.Locale;
public class Bech32 {
/** The Bech32 character set for encoding. */
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
public static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
/** The Bech32 character set for decoding. */
private static final byte[] CHARSET_REV = {
public static final byte[] CHARSET_REV = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
@ -36,6 +36,9 @@ public class Bech32 {
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
};
private static final int BECH32_CHECKSUM_LEN = 6;
public static final char BECH32_SEPARATOR = '1';
public static class Bech32Data {
public final String hrp;
public final byte[] data;
@ -102,12 +105,12 @@ public class Bech32 {
/** Create a checksum. */
private static byte[] createChecksum(final String hrp, Encoding encoding, final byte[] values) {
byte[] hrpExpanded = expandHrp(hrp);
byte[] enc = new byte[hrpExpanded.length + values.length + 6];
byte[] enc = new byte[hrpExpanded.length + values.length + BECH32_CHECKSUM_LEN];
System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length);
System.arraycopy(values, 0, enc, hrpExpanded.length, values.length);
int mod = polymod(enc) ^ encoding.checksumConstant;
byte[] ret = new byte[6];
for (int i = 0; i < 6; ++i) {
byte[] ret = new byte[BECH32_CHECKSUM_LEN];
for (int i = 0; i < BECH32_CHECKSUM_LEN; ++i) {
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
}
return ret;
@ -146,7 +149,7 @@ public class Bech32 {
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
StringBuilder sb = new StringBuilder(hrp.length() + 1 + combined.length);
sb.append(hrp);
sb.append('1');
sb.append(BECH32_SEPARATOR);
for (byte b : combined) {
sb.append(CHARSET.charAt(b));
}
@ -159,6 +162,21 @@ public class Bech32 {
}
public static Bech32Data decode(final String str, int limit) {
final int separatorPos = str.lastIndexOf(BECH32_SEPARATOR);
validate(str, limit, separatorPos, BECH32_CHECKSUM_LEN);
byte[] values = rawDecode(str, separatorPos);
String hrp = str.substring(0, separatorPos).toLowerCase(Locale.ROOT);
Encoding encoding = verifyChecksum(hrp, values);
if(encoding == null) {
throw new ProtocolException("Invalid checksum");
}
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - BECH32_CHECKSUM_LEN), encoding);
}
/** Helper for validating the basic string correctness */
public static void validate(final String str, int limit, int separatorPos, int checksumLen) {
if (separatorPos < 1) throw new ProtocolException("Missing human-readable part");
boolean lower = false, upper = false;
if (str.length() < 8)
throw new ProtocolException("Input too short: " + str.length());
@ -178,23 +196,23 @@ public class Bech32 {
upper = true;
}
}
final int pos = str.lastIndexOf('1');
if (pos < 1) throw new ProtocolException("Missing human-readable part");
final int dataPartLength = str.length() - 1 - pos;
if (dataPartLength < 6) throw new ProtocolException("Data part too short: " + dataPartLength);
final int dataPartLength = str.length() - 1 - separatorPos;
if (dataPartLength < checksumLen) throw new ProtocolException("Data part too short: " + dataPartLength);
for (int i = 0; i < dataPartLength; ++i) {
char c = str.charAt(i + separatorPos + 1);
if (CHARSET_REV[c] == -1) throw new ProtocolException("Invalid character " + c + " at position " + i);
}
}
public static byte[] rawDecode(final String str, int separator_pos) {
final int dataPartLength = str.length() - 1 - separator_pos;
byte[] values = new byte[dataPartLength];
for (int i = 0; i < dataPartLength; ++i) {
char c = str.charAt(i + pos + 1);
if (CHARSET_REV[c] == -1) throw new ProtocolException("Invalid character " + c + " at position " + i);
char c = str.charAt(i + separator_pos + 1);
values[i] = CHARSET_REV[c];
}
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
Encoding encoding = verifyChecksum(hrp, values);
if(encoding == null) {
throw new ProtocolException("Invalid checksum");
}
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6), encoding);
return values;
}
private static byte[] encode(int witnessVersion, byte[] witnessProgram) {
@ -209,7 +227,12 @@ public class Bech32 {
* Helper for re-arranging bits into groups.
*/
public static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits,
final int toBits, final boolean pad) {
final int toBits, final boolean pad) {
return convertBits(in, inStart, inLen, fromBits,toBits, pad, true);
}
public static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits,
final int toBits, final boolean pad, final boolean enforcePaddingZero) {
int acc = 0;
int bits = 0;
ByteArrayOutputStream out = new ByteArrayOutputStream(64);
@ -231,7 +254,11 @@ public class Bech32 {
if (pad) {
if (bits > 0)
out.write((acc << (toBits - bits)) & maxv);
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
} else if (bits >= fromBits) {
// Incomplete group at end must be less than fromBits
throw new ProtocolException("Could not convert bits, invalid padding");
} else if (enforcePaddingZero && (((acc << (toBits - bits)) & maxv) != 0)) {
// Incomplete group at end must be all zeros
throw new ProtocolException("Could not convert bits, invalid padding");
}
return out.toByteArray();

View File

@ -0,0 +1,227 @@
package com.sparrowwallet.drongo.wallet.bip93;
import com.sparrowwallet.drongo.protocol.Bech32;
import com.sparrowwallet.drongo.protocol.ProtocolException;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Locale;
public class Codex32 {
private static final String HRP = "ms";
private static final char SPECIAL_SHARE_INDEX = 's';
private static final int CODEX32_ID_LEN = 4;
public static class Codex32Data {
public final byte[] rawData;
public final ChecksumType checksumType;
public Codex32Data(byte[] rawData, ChecksumType checksumType) {
this.rawData = rawData;
this.checksumType = checksumType;
}
public byte getThreshold() {
return rawData[0];
}
public int getThresholdAsInt() {
return Bech32.CHARSET.charAt(getThreshold()) - '0';
}
public byte[] getIdentifier() {
return Arrays.copyOfRange(rawData, 1, 5);
}
public String identifierAsString() {
byte[] id = getIdentifier();
StringBuilder sb = new StringBuilder(CODEX32_ID_LEN);
for(byte b : id) {
sb.append(Bech32.CHARSET.charAt(b));
}
return sb.toString();
}
public byte getShareIndex() {
return rawData[5];
}
public boolean isUnsharedSecret() {
return getShareIndex() == Bech32.CHARSET_REV[SPECIAL_SHARE_INDEX];
}
public byte[] getPayload() {
return Arrays.copyOfRange(rawData, 6, rawData.length);
}
public byte[] payloadToBip32Secret() throws MnemonicException {
if(!isUnsharedSecret()) {
throw new MnemonicException("Trying to get secret from non-secret share");
}
byte[] payload = getPayload();
return Bech32.convertBits(payload, 0, payload.length, 5, 8, false, false);
}
}
public static String encode(Codex32Data data) throws MnemonicException {
byte[] checksum = createChecksum(data.rawData, data.checksumType);
StringBuilder sb = new StringBuilder();
sb.append(Codex32.HRP);
sb.append(Bech32.BECH32_SEPARATOR);
for(byte b : data.rawData) {
sb.append(Bech32.CHARSET.charAt(b));
}
for(byte b : checksum) {
sb.append(Bech32.CHARSET.charAt(b));
}
String result = sb.toString();
validate(result, 2);
return result;
}
public static Codex32Data decode(String str) throws MnemonicException {
final int separatorPos = str.lastIndexOf(Bech32.BECH32_SEPARATOR);
validate(str, separatorPos);
byte[] rawData = Bech32.rawDecode(str, separatorPos);
int dataLen = rawData.length;
ChecksumType checksumType = verifyChecksum(rawData);
byte[] dataPart = new byte[dataLen - checksumType.length];
System.arraycopy(rawData, 0, dataPart, 0, dataLen - checksumType.length);
return new Codex32Data(dataPart, checksumType);
}
public static void validate(String str, int separatorPos) throws MnemonicException {
if(str.length() < 48 || str.length() > 127) {
throw new MnemonicException("Input invalid length: " + str.length());
}
try {
// Can set checksum len to zero, since we validate if string is too short above
Bech32.validate(str, 127, separatorPos, 0);
} catch(ProtocolException e) {
throw new MnemonicException("Input is not valid Bech32 string: " + e.getMessage());
}
String hrp = str.substring(0, separatorPos).toLowerCase(Locale.ROOT);
if(!HRP.equals(hrp)) {
throw new MnemonicException("Input does not have Codex32 \"ms\" human-readable part: " + hrp);
}
if(str.charAt(3) == '0' && str.toLowerCase(Locale.ROOT).charAt(8) != SPECIAL_SHARE_INDEX) {
throw new MnemonicException("Non zero threshold with unshared secret share: " + str.charAt(8));
}
int threshold = str.charAt(3) - '0';
if(!((threshold == 0) || (2 <= threshold && threshold <= 9))) {
throw new MnemonicException("Threshold not in range: " + threshold);
}
}
private static ChecksumType verifyChecksum(final byte[] values) throws MnemonicException {
int dataLen = values.length;
ChecksumType checksumType;
if(dataLen <= 92) {
checksumType = ChecksumType.CODEX32;
} else if(dataLen >= 96 && dataLen <= 124) {
checksumType = ChecksumType.CODEX32_LONG;
} else {
throw new MnemonicException("Data part invalid length: " + dataLen);
}
int payloadLen = dataLen - 6 - checksumType.length;
if((payloadLen * 5) % 8 > 4) {
throw new MnemonicException("Payload invalid length, incomplete group greater than 4 bits");
}
boolean verified = checksumType.polymod(values).equals(checksumType.constant);
if(!verified) {
throw new MnemonicException("Invalid Checksum");
}
return checksumType;
}
private static byte[] createChecksum(final byte[] values, ChecksumType checksumType) {
BigInteger polymodInt = checksumType.polymod(Arrays.copyOf(values, values.length + checksumType.length));
polymodInt = polymodInt.xor(checksumType.constant);
byte[] buffer = new byte[checksumType.length];
for(int i = 0; i < checksumType.length; i++) {
byte[] intermediate = polymodInt.shiftRight(5 * (checksumType.length - 1 - i)).toByteArray();
buffer[i] = (byte) (intermediate[intermediate.length - 1] & (byte) 31);
}
return buffer;
}
public enum ChecksumType {
CODEX32(new BigInteger("10ce0795c2fd1e62a", 16), 13) {
@Override
public BigInteger polymod(final byte[] values) {
BigInteger gen0 = new BigInteger("19dc500ce73fde210", 16);
BigInteger gen1 = new BigInteger("1bfae00def77fe529", 16);
BigInteger gen2 = new BigInteger("1fbd920fffe7bee52", 16);
BigInteger gen3 = new BigInteger("1739640bdeee3fdad", 16);
BigInteger gen4 = new BigInteger("07729a039cfc75f5a", 16);
BigInteger sixtyOnes = new BigInteger("0fffffffffffffff", 16);
BigInteger residue = new BigInteger("23181b3", 16);
for(byte v_i : values) {
BigInteger b = residue.shiftRight(60);
residue = residue.and(sixtyOnes).shiftLeft(5);
residue = residue.xor(BigInteger.valueOf(v_i));
if(b.shiftRight(0).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen0);
if(b.shiftRight(1).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen1);
if(b.shiftRight(2).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen2);
if(b.shiftRight(3).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen3);
if(b.shiftRight(4).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen4);
}
return residue;
}
},
CODEX32_LONG(new BigInteger("43381e570bf4798ab26", 16), 15) {
public BigInteger polymod(final byte[] values) {
BigInteger gen0 = new BigInteger("3d59d273535ea62d897", 16);
BigInteger gen1 = new BigInteger("7a9becb6361c6c51507", 16);
BigInteger gen2 = new BigInteger("543f9b7e6c38d8a2a0e", 16);
BigInteger gen3 = new BigInteger("0c577eaeccf1990d13c", 16);
BigInteger gen4 = new BigInteger("1887f74f8dc71b10651", 16);
BigInteger seventyOnes = new BigInteger("3fffffffffffffffff", 16);
BigInteger residue = new BigInteger("23181b3", 16);
for(byte v_i : values) {
BigInteger b = residue.shiftRight(70);
residue = residue.and(seventyOnes).shiftLeft(5);
residue = residue.xor(BigInteger.valueOf(v_i));
if(b.shiftRight(0).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen0);
if(b.shiftRight(1).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen1);
if(b.shiftRight(2).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen2);
if(b.shiftRight(3).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen3);
if(b.shiftRight(4).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen4);
}
return residue;
}
};
public final BigInteger constant;
public final int length;
ChecksumType(BigInteger constant, int length) {
this.constant = constant;
this.length = length;
}
public BigInteger polymod(final byte[] values) {
return BigInteger.ZERO;
}
}
}

View File

@ -4,10 +4,13 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.bip93.Codex32;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.List;
public class KeystoreTest {
@Test
@ -127,4 +130,67 @@ public class KeystoreTest {
Assertions.assertEquals("xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt", keystore0h1h.getExtendedPublicKey().toString());
Assertions.assertEquals("xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1", keystore0h1h.getExtendedPrivateKey(false).toString());
}
@Test
void bip93TestVectors() throws MnemonicException {
// xprv derivation parts of the BIP93 test vectors, rest of the BIP93 tests are in Codex32Test.java
byte[] testVector1Secret = Codex32.decode("ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw").payloadToBip32Secret();
DeterministicKey testVector1Key = HDKeyDerivation.createMasterPrivateKey(testVector1Secret);
MasterPrivateExtendedKey testVector1MPEK = new MasterPrivateExtendedKey(testVector1Key);
Keystore keystore1 = Keystore.fromMasterPrivateExtendedKey(testVector1MPEK, KeyDerivation.parsePath("m"));
Assertions.assertEquals("xprv9s21ZrQH143K3taPNekMd9oV5K6szJ8ND7vVh6fxicRUMDcChr3bFFzuxY8qP3xFFBL6DWc2uEYCfBFZ2nFWbAqKPhtCLRjgv78EZJDEfpL", keystore1.getExtendedMasterPrivateKey().toString());
byte[] testVector2Secret = Codex32.decode("MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW").payloadToBip32Secret();
DeterministicKey testVector2Key = HDKeyDerivation.createMasterPrivateKey(testVector2Secret);
MasterPrivateExtendedKey testVector2MPEK = new MasterPrivateExtendedKey(testVector2Key);
Keystore keystore2 = Keystore.fromMasterPrivateExtendedKey(testVector2MPEK, KeyDerivation.parsePath("m"));
Assertions.assertEquals("xprv9s21ZrQH143K2NkobdHxXeyFDqE44nJYvzLFtsriatJNWMNKznGoGgW5UMTL4fyWtajnMYb5gEc2CgaKhmsKeskoi9eTimpRv2N11THhPTU", keystore2.getExtendedMasterPrivateKey().toString());
List<String> testVector3SecretShares = Arrays.asList(
"ms13cashsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nln",
"ms13cashsllhdmn9m42vcsamx24zrxgs3qpte35dvzkjpt0r",
"ms13cashsllhdmn9m42vcsamx24zrxgs3qzfatvdwq5692k6",
"ms13cashsllhdmn9m42vcsamx24zrxgs3qrsx6ydhed97jx2"
);
for(String secretString : testVector3SecretShares) {
byte[] testVector3Secret = Codex32.decode(secretString).payloadToBip32Secret();
DeterministicKey testVector3Key = HDKeyDerivation.createMasterPrivateKey(testVector3Secret);
MasterPrivateExtendedKey testVector3MPEK = new MasterPrivateExtendedKey(testVector3Key);
Keystore keystore3 = Keystore.fromMasterPrivateExtendedKey(testVector3MPEK, KeyDerivation.parsePath("m"));
Assertions.assertEquals("xprv9s21ZrQH143K266qUcrDyYJrSG7KA3A7sE5UHndYRkFzsPQ6xwUhEGK1rNuyyA57Vkc1Ma6a8boVqcKqGNximmAe9L65WsYNcNitKRPnABd", keystore3.getExtendedMasterPrivateKey().toString());
}
List<String> testVector4SecretShares = Arrays.asList(
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqpj82dp34u6lqtd",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqzsrs4pnh7jmpj5",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqrfcpap2w8dqezy",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqy5tdvphn6znrf0",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq9dsuypw2ragmel",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqx05xupvgp4v6qx",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq8k0h5p43c2hzsk",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqgum7hplmjtr8ks",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqf9q0lpxzt5clxq",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq28y48pyqfuu7le",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqt7ly0paesr8x0f",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqvrvg7pqydv5uyz",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqd6hekpea5n0y5j",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqwcnrwpmlkmt9dt",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq0pgjxpzx0ysaam"
);
for(String secretString : testVector4SecretShares) {
byte[] testVector4Secret = Codex32.decode(secretString).payloadToBip32Secret();
DeterministicKey testVector4Key = HDKeyDerivation.createMasterPrivateKey(testVector4Secret);
MasterPrivateExtendedKey testVector4MPEK = new MasterPrivateExtendedKey(testVector4Key);
Keystore keystore4 = Keystore.fromMasterPrivateExtendedKey(testVector4MPEK, KeyDerivation.parsePath("m"));
Assertions.assertEquals("xprv9s21ZrQH143K3s41UCWxXTsU4TRrhkpD1t21QJETan3hjo8DP5LFdFcB5eaFtV8x6Y9aZotQyP8KByUjgLTbXCUjfu2iosTbMv98g8EQoqr", keystore4.getExtendedMasterPrivateKey().toString());
}
byte[] testVector5Secret = Codex32.decode("MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK").payloadToBip32Secret();
DeterministicKey testVector5Key = HDKeyDerivation.createMasterPrivateKey(testVector5Secret);
MasterPrivateExtendedKey testVector5MPEK = new MasterPrivateExtendedKey(testVector5Key);
Keystore keystore5 = Keystore.fromMasterPrivateExtendedKey(testVector5MPEK, KeyDerivation.parsePath("m"));
Assertions.assertEquals("xprv9s21ZrQH143K4UYT4rP3TZVKKbmRVmfRqTx9mG2xCy2JYipZbkLV8rwvBXsUbEv9KQiUD7oED1Wyi9evZzUn2rqK9skRgPkNaAzyw3YrpJN", keystore5.getExtendedMasterPrivateKey().toString());
}
}

View File

@ -0,0 +1,230 @@
package com.sparrowwallet.drongo.wallet.bip93;
import com.sparrowwallet.drongo.protocol.Bech32;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
public class Codex32Test {
// Codex32 -> BIP32 secret testing, and eventually share derivations testing. xprv derivations testing lives in KeystoreTest.java
@Test
public void bip93TestVector1() throws MnemonicException {
Codex32.Codex32Data data = Codex32.decode("ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw");
Assertions.assertEquals(0, data.getThresholdAsInt());
Assertions.assertEquals("test", data.identifierAsString());
Assertions.assertTrue(data.isUnsharedSecret());
Assertions.assertEquals("318c6318c6318c6318c6318c6318c631", HexFormat.of().formatHex(data.payloadToBip32Secret()));
Assertions.assertEquals("ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw", Codex32.encode(data));
}
@Test
public void bip93TestVector2() throws MnemonicException {
Codex32.Codex32Data aShare = Codex32.decode("MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM");
Assertions.assertEquals(2, aShare.getThresholdAsInt());
Assertions.assertEquals("name", aShare.identifierAsString());
Assertions.assertEquals(Bech32.CHARSET_REV['a'], aShare.getShareIndex());
Assertions.assertFalse(aShare.isUnsharedSecret());
Codex32.Codex32Data cShare = Codex32.decode("MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN");
Assertions.assertEquals(2, cShare.getThresholdAsInt());
Assertions.assertEquals("name", cShare.identifierAsString());
Assertions.assertEquals(Bech32.CHARSET_REV['c'], cShare.getShareIndex());
Assertions.assertFalse(cShare.isUnsharedSecret());
// TODO: Test derivations once that's built
Codex32.Codex32Data dShare = Codex32.decode("MS12NAMEDLL4F8JLH4E5VDVULDLFXU2JHDNLSM97XVENRXEG");
Assertions.assertEquals(2, dShare.getThresholdAsInt());
Assertions.assertEquals("name", dShare.identifierAsString());
Assertions.assertEquals(Bech32.CHARSET_REV['d'], dShare.getShareIndex());
Assertions.assertFalse(dShare.isUnsharedSecret());
Codex32.Codex32Data data = Codex32.decode("MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW");
Assertions.assertEquals(2, data.getThresholdAsInt());
Assertions.assertEquals("name", data.identifierAsString());
Assertions.assertTrue(data.isUnsharedSecret());
Assertions.assertEquals("d1808e096b35b209ca12132b264662a5", HexFormat.of().formatHex(data.payloadToBip32Secret()));
Assertions.assertEquals("MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW".toLowerCase(Locale.ROOT), Codex32.encode(data));
}
@Test
public void bip93TestVector3() throws MnemonicException {
Codex32.Codex32Data data = Codex32.decode("ms13cashsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nln");
Assertions.assertEquals(3, data.getThresholdAsInt());
Assertions.assertEquals("cash", data.identifierAsString());
Assertions.assertTrue(data.isUnsharedSecret());
Assertions.assertEquals("ffeeddccbbaa99887766554433221100", HexFormat.of().formatHex(data.payloadToBip32Secret()));
Assertions.assertEquals("ms13cashsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nln", Codex32.encode(data));
Codex32.Codex32Data aShare = Codex32.decode("ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t");
Assertions.assertEquals(3, aShare.getThresholdAsInt());
Assertions.assertEquals("cash", aShare.identifierAsString());
Assertions.assertEquals(Bech32.CHARSET_REV['a'], aShare.getShareIndex());
Assertions.assertFalse(aShare.isUnsharedSecret());
Assertions.assertEquals("ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t", Codex32.encode(aShare));
Codex32.Codex32Data cShare = Codex32.decode("ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr");
Assertions.assertEquals(3, cShare.getThresholdAsInt());
Assertions.assertEquals("cash", cShare.identifierAsString());
Assertions.assertEquals(Bech32.CHARSET_REV['c'], cShare.getShareIndex());
Assertions.assertFalse(cShare.isUnsharedSecret());
Assertions.assertEquals("ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr", Codex32.encode(cShare));
// TODO: Test derivations once that exists
Codex32.Codex32Data dShare = Codex32.decode("ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm");
Assertions.assertEquals(3, dShare.getThresholdAsInt());
Assertions.assertEquals("cash", dShare.identifierAsString());
Assertions.assertEquals(Bech32.CHARSET_REV['d'], dShare.getShareIndex());
Assertions.assertFalse(dShare.isUnsharedSecret());
Assertions.assertEquals("ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm", Codex32.encode(dShare));
Codex32.Codex32Data eShare = Codex32.decode("ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9");
Assertions.assertEquals(3, eShare.getThresholdAsInt());
Assertions.assertEquals("cash", eShare.identifierAsString());
Assertions.assertEquals(Bech32.CHARSET_REV['e'], eShare.getShareIndex());
Assertions.assertFalse(eShare.isUnsharedSecret());
Assertions.assertEquals("ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9", Codex32.encode(eShare));
Codex32.Codex32Data fShare = Codex32.decode("ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704");
Assertions.assertEquals(3, fShare.getThresholdAsInt());
Assertions.assertEquals("cash", fShare.identifierAsString());
Assertions.assertEquals(Bech32.CHARSET_REV['f'], fShare.getShareIndex());
Assertions.assertFalse(fShare.isUnsharedSecret());
Assertions.assertEquals("ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704", Codex32.encode(fShare));
}
@Test
public void bip93TestVector4() throws MnemonicException {
Codex32.Codex32Data data = Codex32.decode("ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma");
Assertions.assertEquals(0, data.getThresholdAsInt());
Assertions.assertEquals("leet", data.identifierAsString());
Assertions.assertTrue(data.isUnsharedSecret());
Assertions.assertEquals("ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100", HexFormat.of().formatHex(data.payloadToBip32Secret()));
Assertions.assertEquals("ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma", Codex32.encode(data));
}
@Test
public void bip93TestVector5() throws MnemonicException {
Codex32.Codex32Data data = Codex32.decode("MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK");
Assertions.assertEquals(0, data.getThresholdAsInt());
Assertions.assertEquals("0c8v", data.identifierAsString());
Assertions.assertTrue(data.isUnsharedSecret());
Assertions.assertEquals("dc5423251cb87175ff8110c8531d0952d8d73e1194e95b5f19d6f9df7c01111104c9baecdfea8cccc677fb9ddc8aec5553b86e528bcadfdcc201c17c638c47e9", HexFormat.of().formatHex(data.payloadToBip32Secret()));
Assertions.assertEquals("ms100c8vsm32zxfguhpchtlupzry9x8gf2tvdw0s3jn54khce6mua7lqpzygsfjd6an074rxvcemlh8wu3tk925acdefghjklmnpqrstuvwxy06fhpv80undvarhrak", Codex32.encode(data));
}
@Test
public void bip93InvalidShares() {
// Incorrect checksums
List<String> invalidChecksum = Arrays.asList(
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxve740yyge2ghq",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxve740yyge2ghp",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxlk3yepcstwr",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx6pgnv7jnpcsp",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxx0cpvr7n4geq",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxm5252y7d3lr",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxrd9sukzl05ej",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxc55srw5jrm0",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxgc7rwhtudwc",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx4gy22afwghvs",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxe8yfm0",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxvm597d",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxme084q0vpht7pe0",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxme084q0vpht7pew",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxqyadsp3nywm8a",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzvg7ar4hgaejk",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcznau0advgxqe",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxch3jrc6j5040j",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx52gxl6ppv40mcv",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx7g4g2nhhle8fk",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx63m45uj8ss4x8",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxy4r708q7kg65x"
);
for(String invalid : invalidChecksum) {
Assertions.assertThrows(MnemonicException.class, () -> Codex32.decode(invalid));
}
// Wrong checksum for their given data sizes
List<String> wrongChecksumForSize = Arrays.asList(
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxurfvwmdcmymdufv",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxcsyppjkd8lz4hx3",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxu6hwvl5p0l9xf3c",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwqey9rfs6smenxa",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxv70wkzrjr4ntqet",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3hmlrmpa4zl0v",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxrfggf88znkaup",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpt7l4aycv9qzj",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxus27z9xtyxyw3",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcwm4re8fs78vn"
);
for(String invalid : wrongChecksumForSize) {
Assertions.assertThrows(MnemonicException.class, () -> Codex32.decode(invalid));
}
// These examples have improper lengths. They are either too short, too long,
// or would decode to byte sequence with an incomplete group greater than 4 bits
List<String> improperLength = Arrays.asList(
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxw0a4c70rfefn4",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxk4pavy5n46nea",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx9lrwar5zwng4w",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxr335l5tv88js3",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxvu7q9nz8p7dj68v",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpq6k542scdxndq3",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxkmfw6jm270mz6ej",
"ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxzhddxw99w7xws",
"ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxx42cux6um92rz",
"ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxarja5kqukdhy9",
"ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxky0ua3ha84qk8",
"ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx9eheesxadh2n2n9",
"ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx9llwmgesfulcj2z",
"ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx02ev7caq6n9fgkf"
);
for(String invalid : improperLength) {
Assertions.assertThrows(MnemonicException.class, () -> Codex32.decode(invalid));
}
// A "0" threshold with a non-"s" index
Assertions.assertThrows(MnemonicException.class, () -> Codex32.decode("ms10fauxxxxxxxxxxxxxxxxxxxxxxxxxxxx0z26tfn0ulw3p"));
// A non-digit threshold
Assertions.assertThrows(MnemonicException.class, () -> Codex32.decode("ms1fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxda3kr3s0s2swg"));
// Do not begin with the required "ms" or "MS" prefix and/or are missing the "1" separator
List<String> invalidPrefix = Arrays.asList(
"0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"ms0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"m10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"s10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxhkd4f70m8lgws",
"10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxhkd4f70m8lgws",
"m10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxx8t28z74x8hs4l",
"s10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxh9d0fhnvfyx3x"
);
for(String invalid : invalidPrefix) {
Assertions.assertThrows(MnemonicException.class, () -> Codex32.decode(invalid));
}
// Incorrectly mix upper and lower case characters
List<String> caseMix = Arrays.asList(
"Ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"mS10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"MS10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"ms10FAUXsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"ms10fauxSxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2",
"ms10fauxsXXXXXXXXXXXXXXXXXXXXXXXXXXuqxkk05lyf3x2",
"ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxUQXKK05LYF3X2"
);
for(String invalid : caseMix) {
Assertions.assertThrows(MnemonicException.class, () -> Codex32.decode(invalid));
}
}
}