Skip to content

Commit

Permalink
Bech32, SegwitAddress: Implement Bech32m format for v1+ witness addre…
Browse files Browse the repository at this point in the history
…sses.
  • Loading branch information
Andreas Schildbach committed Sep 29, 2021
1 parent 4dc4cf7 commit 183986c
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 42 deletions.
14 changes: 14 additions & 0 deletions core/src/main/java/org/bitcoinj/core/AddressFormatException.java
Expand Up @@ -73,6 +73,20 @@ public InvalidChecksum(String message) {
}
}

/**
* This exception is thrown by {@link SegwitAddress} when you try to decode data and the witness version doesn't
* match the Bech32 encoding as per BIP350. You shouldn't allow the user to proceed in this case.
*/
public static class UnexpectedWitnessVersion extends AddressFormatException {
public UnexpectedWitnessVersion() {
super("Unexpected witness version");
}

public UnexpectedWitnessVersion(String message) {
super(message);
}
}

/**
* This exception is thrown by the {@link PrefixedChecksummedBytes} hierarchy of classes when you try and decode an
* address or private key with an invalid prefix (version header or human-readable part). You shouldn't allow the
Expand Down
43 changes: 33 additions & 10 deletions core/src/main/java/org/bitcoinj/core/Bech32.java
Expand Up @@ -16,11 +16,19 @@

package org.bitcoinj.core;

import javax.annotation.Nullable;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.Arrays;
import java.util.Locale;

/**
* <p>Implementation of the Bech32 encoding.</p>
*
* <p>See <a href="https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki">BIP350</a> and
* <a href="https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki">BIP173</a> for details.</p>
*/
public class Bech32 {
/** The Bech32 character set for encoding. */
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
Expand All @@ -37,11 +45,18 @@ public class Bech32 {
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
};

private static final int BECH32_CONST = 1;
private static final int BECH32M_CONST = 0x2bc830a3;

public enum Encoding { BECH32, BECH32M }

public static class Bech32Data {
public final Encoding encoding;
public final String hrp;
public final byte[] data;

private Bech32Data(final String hrp, final byte[] data) {
private Bech32Data(final Encoding encoding, final String hrp, final byte[] data) {
this.encoding = encoding;
this.hrp = hrp;
this.data = data;
}
Expand Down Expand Up @@ -76,21 +91,28 @@ private static byte[] expandHrp(final String hrp) {
}

/** Verify a checksum. */
private static boolean verifyChecksum(final String hrp, final byte[] values) {
private static @Nullable
Encoding verifyChecksum(final String hrp, final byte[] values) {
byte[] hrpExpanded = expandHrp(hrp);
byte[] combined = new byte[hrpExpanded.length + values.length];
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length);
System.arraycopy(values, 0, combined, hrpExpanded.length, values.length);
return polymod(combined) == 1;
final int check = polymod(combined);
if (check == BECH32_CONST)
return Encoding.BECH32;
else if (check == BECH32M_CONST)
return Encoding.BECH32M;
else
return null;
}

/** Create a checksum. */
private static byte[] createChecksum(final String hrp, final byte[] values) {
private static byte[] createChecksum(final Encoding encoding, final String hrp, final byte[] values) {
byte[] hrpExpanded = expandHrp(hrp);
byte[] enc = new byte[hrpExpanded.length + values.length + 6];
System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length);
System.arraycopy(values, 0, enc, hrpExpanded.length, values.length);
int mod = polymod(enc) ^ 1;
int mod = polymod(enc) ^ (encoding == Encoding.BECH32 ? BECH32_CONST : BECH32M_CONST);
byte[] ret = new byte[6];
for (int i = 0; i < 6; ++i) {
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
Expand All @@ -100,15 +122,15 @@ private static byte[] createChecksum(final String hrp, final byte[] values) {

/** Encode a Bech32 string. */
public static String encode(final Bech32Data bech32) {
return encode(bech32.hrp, bech32.data);
return encode(bech32.encoding, bech32.hrp, bech32.data);
}

/** Encode a Bech32 string. */
public static String encode(String hrp, final byte[] values) {
public static String encode(Encoding encoding, String hrp, final byte[] values) {
checkArgument(hrp.length() >= 1, "Human-readable part is too short");
checkArgument(hrp.length() <= 83, "Human-readable part is too long");
hrp = hrp.toLowerCase(Locale.ROOT);
byte[] checksum = createChecksum(hrp, values);
byte[] checksum = createChecksum(encoding, hrp, values);
byte[] combined = new byte[values.length + checksum.length];
System.arraycopy(values, 0, combined, 0, values.length);
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
Expand Down Expand Up @@ -153,7 +175,8 @@ public static Bech32Data decode(final String str) throws AddressFormatException
values[i] = CHARSET_REV[c];
}
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
if (!verifyChecksum(hrp, values)) throw new AddressFormatException.InvalidChecksum();
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6));
Encoding encoding = verifyChecksum(hrp, values);
if (encoding == null) throw new AddressFormatException.InvalidChecksum();
return new Bech32Data(encoding, hrp, Arrays.copyOfRange(values, 0, values.length - 6));
}
}
23 changes: 18 additions & 5 deletions core/src/main/java/org/bitcoinj/core/SegwitAddress.java
Expand Up @@ -37,7 +37,8 @@
* bits into groups of 5).</li>
* </ul>
*
* <p>See <a href="https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki">BIP173</a> for details.</p>
* <p>See <a href="https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki">BIP350</a> and
* <a href="https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki">BIP173</a> for details.</p>
*
* <p>However, you don't need to care about the internals. Use {@link #fromBech32(NetworkParameters, String)},
* {@link #fromHash(NetworkParameters, byte[])} or {@link #fromKey(NetworkParameters, ECKey)} to construct a native
Expand Down Expand Up @@ -105,7 +106,7 @@ private SegwitAddress(NetworkParameters params, byte[] data) throws AddressForma
}

/**
* Returns the witness version in decoded form. Only version 0 is in use right now.
* Returns the witness version in decoded form. Only versions 0 and 1 are in use right now.
*
* @return witness version, between 0 and 16
*/
Expand Down Expand Up @@ -168,16 +169,25 @@ public static SegwitAddress fromBech32(@Nullable NetworkParameters params, Strin
if (params == null) {
for (NetworkParameters p : Networks.get()) {
if (bechData.hrp.equals(p.getSegwitAddressHrp()))
return new SegwitAddress(p, bechData.data);
return fromBechData(p, bechData);
}
throw new AddressFormatException.InvalidPrefix("No network found for " + bech32);
} else {
if (bechData.hrp.equals(params.getSegwitAddressHrp()))
return new SegwitAddress(params, bechData.data);
return fromBechData(params, bechData);
throw new AddressFormatException.WrongNetwork(bechData.hrp);
}
}

private static SegwitAddress fromBechData(NetworkParameters params, Bech32.Bech32Data bechData) {
final SegwitAddress address = new SegwitAddress(params, bechData.data);
final int witnessVersion = address.getWitnessVersion();
if ((witnessVersion == 0 && bechData.encoding != Bech32.Encoding.BECH32) ||
(witnessVersion != 0 && bechData.encoding != Bech32.Encoding.BECH32M))
throw new AddressFormatException.UnexpectedWitnessVersion("Unexpected witness version: " + witnessVersion);
return address;
}

/**
* Construct a {@link SegwitAddress} that represents the given hash, which is either a pubkey hash or a script hash.
* The resulting address will be either a P2WPKH or a P2WSH type of address.
Expand Down Expand Up @@ -213,7 +223,10 @@ public static SegwitAddress fromKey(NetworkParameters params, ECKey key) {
* @return textual form encoded in bech32
*/
public String toBech32() {
return Bech32.encode(params.getSegwitAddressHrp(), bytes);
if (getWitnessVersion() == 0)
return Bech32.encode(Bech32.Encoding.BECH32, params.getSegwitAddressHrp(), bytes);
else
return Bech32.encode(Bech32.Encoding.BECH32M, params.getSegwitAddressHrp(), bytes);
}

/**
Expand Down
86 changes: 65 additions & 21 deletions core/src/test/java/org/bitcoinj/core/Bech32Test.java
Expand Up @@ -25,20 +25,29 @@

public class Bech32Test {
@Test
public void valid() {
for (String valid : VALID) {
Bech32.Bech32Data bechData = Bech32.decode(valid);
String recode = Bech32.encode(bechData);
assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode),
valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT));
// Test encoding with an uppercase HRP
recode = Bech32.encode(bechData.hrp.toUpperCase(Locale.ROOT), bechData.data);
assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode),
valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT));
}
public void valid_bech32() {
for (String valid : VALID_BECH32)
valid(valid);
}

@Test
public void valid_bech32m() {
for (String valid : VALID_BECH32M)
valid(valid);
}

private static final String[] VALID = {
private void valid(String valid) {
Bech32.Bech32Data bechData = Bech32.decode(valid);
String recode = Bech32.encode(bechData);
assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode),
valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT));
// Test encoding with an uppercase HRP
recode = Bech32.encode(bechData.encoding, bechData.hrp.toUpperCase(Locale.ROOT), bechData.data);
assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode),
valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT));
}

private static final String[] VALID_BECH32 = {
"A12UEL5L",
"a12uel5l",
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
Expand All @@ -47,20 +56,38 @@ public void valid() {
"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w",
"?1ezyfcl",
};
private static final String[] VALID_BECH32M = {
"A1LQFN3A",
"a1lqfn3a",
"an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6",
"abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx",
"11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8",
"split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
"?1v759aa"
};

@Test
public void invalid() {
for (String invalid : INVALID) {
try {
Bech32.decode(invalid);
fail(String.format("Parsed an invalid code: '%s'", invalid));
} catch (AddressFormatException x) {
/* expected */
}
public void invalid_bech32() {
for (String invalid : INVALID_BECH32)
invalid(invalid);
}

@Test
public void invalid_bech32m() {
for (String invalid : INVALID_BECH32M)
invalid(invalid);
}

private void invalid(String invalid) {
try {
Bech32.decode(invalid);
fail(String.format("Parsed an invalid code: '%s'", invalid));
} catch (AddressFormatException x) {
/* expected */
}
}

private static final String[] INVALID = {
private static final String[] INVALID_BECH32 = {
" 1nwldj5", // HRP character out of range
new String(new char[] { 0x7f }) + "1axkwrx", // HRP character out of range
new String(new char[] { 0x80 }) + "1eym55h", // HRP character out of range
Expand All @@ -75,6 +102,23 @@ public void invalid() {
"1qzzfhee", // empty HRP
};

private static final String[] INVALID_BECH32M = {
" 1xj0phk", // HRP character out of range
new String(new char[] { 0x7f }) + "1g6xzxy", // HRP character out of range
new String(new char[] { 0x80 }) + "1vctc34", // HRP character out of range
"an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4", // overall max length exceeded
"qyrz8wqd2c9m", // No separator character
"1qyrz8wqd2c9m", // Empty HRP
"y1b0jsk6g", // Invalid data character
"lt1igcx5c0", // Invalid data character
"in1muywd", // Too short checksum
"mm1crxm3i", // Invalid character in checksum
"au1s5cgom", // Invalid character in checksum
"M1VUXWEZ", // checksum calculated with uppercase form of HRP
"16plkw9", // empty HRP
"1p2gdwpf", // empty HRP
};

@Test(expected = AddressFormatException.InvalidCharacter.class)
public void decode_invalidCharacter_notInAlphabet() {
Bech32.decode("A12OUEL5X");
Expand Down
31 changes: 25 additions & 6 deletions core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java
Expand Up @@ -148,16 +148,22 @@ public String toString() {
}

private static AddressData[] VALID_ADDRESSES = {
// from BIP350 (includes the corrected BIP173 vectors):
new AddressData("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", MAINNET,
"0014751e76e8199196d454941c45d1b3a323f1433bd6", 0),
new AddressData("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", TESTNET,
"00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", 0),
new AddressData("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", MAINNET,
new AddressData("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", MAINNET,
"5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", 1),
new AddressData("BC1SW50QA3JX3S", MAINNET, "6002751e", 16),
new AddressData("bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", MAINNET, "5210751e76e8199196d454941c45d1b3a323", 2),
new AddressData("BC1SW50QGDZ25J", MAINNET, "6002751e", 16),
new AddressData("bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", MAINNET, "5210751e76e8199196d454941c45d1b3a323", 2),
new AddressData("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", TESTNET,
"0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", 0) };
"0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", 0),
new AddressData("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", TESTNET,
"5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", 1),
new AddressData("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", MAINNET,
"512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 1),
};

@Test
public void invalidAddresses() {
Expand All @@ -171,7 +177,8 @@ public void invalidAddresses() {
}
}

private static String[] INVALID_ADDRESSES = { //
private static String[] INVALID_ADDRESSES = {
// from BIP173:
"tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", // Invalid human-readable part
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", // Invalid checksum
"BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", // Invalid witness version
Expand All @@ -182,6 +189,18 @@ public void invalidAddresses() {
"bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", // Zero padding of more than 4 bits
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", // Non-zero padding in 8-to-5 conversion
"bc1gmk9yu", // Empty data section

// from BIP350:
"tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", // Invalid human-readable part
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", // Invalid checksum
"BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", // Invalid witness version
"bc1rw5uspcuh", // Invalid program length
"bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", // Invalid program length
"BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", // Invalid program length for witness version 0 (per BIP141)
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", // Mixed case
"bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", // zero padding of more than 4 bits
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", // Non-zero padding in 8-to-5 conversion
"bc1gmk9yu", // Empty data section
};

@Test(expected = AddressFormatException.InvalidDataLength.class)
Expand Down Expand Up @@ -211,7 +230,7 @@ public void fromBech32_wrongNetwork() {

@Test
public void testJavaSerialization() throws Exception {
SegwitAddress address = SegwitAddress.fromBech32(null, "BC1SW50QA3JX3S");
SegwitAddress address = SegwitAddress.fromBech32(null, "BC1SW50QGDZ25J");

ByteArrayOutputStream os = new ByteArrayOutputStream();
new ObjectOutputStream(os).writeObject(address);
Expand Down

0 comments on commit 183986c

Please sign in to comment.