mirror of https://github.com/m2049r/xmrwallet.git
allow bech32 segwit btc addresses (#508)
This commit is contained in:
parent
dba6cb057e
commit
c4e361a873
|
@ -16,12 +16,13 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.util;
|
package com.m2049r.xmrwallet.util;
|
||||||
|
|
||||||
// based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java
|
// mostly based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.model.NetworkType;
|
import com.m2049r.xmrwallet.model.NetworkType;
|
||||||
import com.m2049r.xmrwallet.model.WalletManager;
|
import com.m2049r.xmrwallet.model.WalletManager;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -31,8 +32,9 @@ public class BitcoinAddressValidator {
|
||||||
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
|
||||||
public static boolean validate(String addrress) {
|
public static boolean validate(String addrress) {
|
||||||
return validate(addrress,
|
boolean testnet = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet;
|
||||||
WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet);
|
if (validate(addrress, testnet)) return true;
|
||||||
|
return validateBech32Segwit(addrress, testnet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean validate(String addrress, boolean testnet) {
|
public static boolean validate(String addrress, boolean testnet) {
|
||||||
|
@ -85,4 +87,112 @@ public class BitcoinAddressValidator {
|
||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// validate Bech32 segwit
|
||||||
|
// see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki for spec
|
||||||
|
//
|
||||||
|
|
||||||
|
private static final String DATA_CHARS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
|
public static boolean validateBech32Segwit(String bech32, boolean testnet) {
|
||||||
|
if (!bech32.equals(bech32.toLowerCase()) && !bech32.equals(bech32.toUpperCase())) {
|
||||||
|
return false; // mixing upper and lower case not allowed
|
||||||
|
}
|
||||||
|
bech32 = bech32.toLowerCase();
|
||||||
|
|
||||||
|
if (testnet && !bech32.startsWith("tb1")) return false;
|
||||||
|
if (!testnet && !bech32.startsWith("bc1")) return false;
|
||||||
|
|
||||||
|
if ((bech32.length() < 14) || (bech32.length() > 74)) return false;
|
||||||
|
int mod = bech32.length() % 8;
|
||||||
|
if ((mod == 0) || (mod == 3) || (mod == 5)) return false;
|
||||||
|
|
||||||
|
int sep = -1;
|
||||||
|
final byte[] bytes = bech32.getBytes(StandardCharsets.US_ASCII);
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
if ((bytes[i] < 33) || (bytes[i] > 126)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (bytes[i] == 49) sep = i; // 49 := '1' in ASCII
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sep != 2) return false; // bech32 always has len(hrp)==2
|
||||||
|
if (sep > bytes.length - 7) {
|
||||||
|
return false; // min 6 bytes data
|
||||||
|
}
|
||||||
|
if (bytes.length < 8) { // hrp{min}=1 + sep=1 + data{min}=6 := 8
|
||||||
|
return false; // too short
|
||||||
|
}
|
||||||
|
if (bytes.length > 90) {
|
||||||
|
return false; // too long
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] hrp = Arrays.copyOfRange(bytes, 0, sep);
|
||||||
|
|
||||||
|
final byte[] data = Arrays.copyOfRange(bytes, sep + 1, bytes.length);
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
int b = DATA_CHARS.indexOf(data[i]);
|
||||||
|
if (b < 0) return false; // invalid character
|
||||||
|
data[i] = (byte) b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateBech32Data(data)) return false;
|
||||||
|
|
||||||
|
return verifyChecksum(hrp, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int polymod(byte[] values) {
|
||||||
|
final int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
|
||||||
|
int chk = 1;
|
||||||
|
for (byte v : values) {
|
||||||
|
byte b = (byte) (chk >> 25);
|
||||||
|
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
chk ^= ((b >> i) & 1) == 1 ? GEN[i] : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] hrpExpand(byte[] hrp) {
|
||||||
|
final byte[] expanded = new byte[(2 * hrp.length) + 1];
|
||||||
|
int i = 0;
|
||||||
|
for (int j = 0; j < hrp.length; j++) {
|
||||||
|
expanded[i++] = (byte) (hrp[j] >> 5);
|
||||||
|
}
|
||||||
|
expanded[i++] = 0;
|
||||||
|
for (int j = 0; j < hrp.length; j++) {
|
||||||
|
expanded[i++] = (byte) (hrp[j] & 0x1f);
|
||||||
|
}
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean verifyChecksum(byte[] hrp, byte[] data) {
|
||||||
|
final byte[] hrpExpanded = hrpExpand(hrp);
|
||||||
|
final byte[] values = new byte[hrpExpanded.length + data.length];
|
||||||
|
System.arraycopy(hrpExpanded, 0, values, 0, hrpExpanded.length);
|
||||||
|
System.arraycopy(data, 0, values, hrpExpanded.length, data.length);
|
||||||
|
return (polymod(values) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean validateBech32Data(final byte[] data) {
|
||||||
|
if ((data[0] < 0) || (data[0] > 16)) return false; // witness version
|
||||||
|
final int programLength = data.length - 1 - 6; // 1-byte version at beginning & 6-byte checksum at end
|
||||||
|
|
||||||
|
// since we are coming from our own decoder, we don't need to verify data is 5-bit bytes
|
||||||
|
|
||||||
|
final int convertedSize = programLength * 5 / 8;
|
||||||
|
final int remainderSize = programLength * 5 % 8;
|
||||||
|
|
||||||
|
if ((convertedSize < 2) || (convertedSize > 40)) return false;
|
||||||
|
|
||||||
|
if ((data[0] == 0) && (convertedSize != 20) && (convertedSize != 32)) return false;
|
||||||
|
|
||||||
|
if (remainderSize >= 5) return false;
|
||||||
|
// ignore checksum at end and get last byte of program
|
||||||
|
if ((data[data.length - 1 - 6] & ((1 << remainderSize) - 1)) != 0) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -18,6 +18,7 @@ package com.m2049r.xmrwallet.util;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,4 +55,38 @@ public class BitcoinAddressValidatorTest {
|
||||||
assertTrue(!BitcoinAddressValidator.validate("3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU ", false));
|
assertTrue(!BitcoinAddressValidator.validate("3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU ", false));
|
||||||
assertTrue(!BitcoinAddressValidator.validate(" 3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU ", false));
|
assertTrue(!BitcoinAddressValidator.validate(" 3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU ", false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validSegwit() {
|
||||||
|
// see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", false));
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", true));
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", false));
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", true));
|
||||||
|
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", false));
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", true));
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", false));
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("BC1SW50QA3JX3S", false));
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", false));
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", true));
|
||||||
|
|
||||||
|
assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1q76awjp3nmklgnf0yyu0qncsekktf4e3qj248t4", false)); // electrum blog
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidSegwit() {
|
||||||
|
// see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", true)); // Invalid human-readable part
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", true)); // Invalid checksum
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", true)); // Invalid witness version
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc1rw5uspcuh", true)); // Invalid program length
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", true)); // Invalid program length
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", true)); // Invalid program length for witness version 0 (per BIP141)
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", true)); // Mixed case
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", true)); // zero padding of more than 4 bits
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", true)); // Non-zero padding in 8-to-5 conversion
|
||||||
|
assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc1gmk9yu", true)); // Empty data section
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue