From ca4da5cde5256b60bcdebe41cdbba2ac32f67b9b Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Thu, 15 Feb 2018 14:02:53 +0100 Subject: [PATCH] WIP Add SegwitAddress as a subclass of AbstractAddress (parent of Address too). Note the class names a subject to change, e.g. AbstractAddress > Address Address > LegacyAddress Uses Bech32 code from https://github.com/sipa/bech32/pull/40. --- .../org/bitcoinj/core/AbstractAddress.java | 54 +++++++ .../main/java/org/bitcoinj/core/Address.java | 15 +- .../main/java/org/bitcoinj/core/Bech32.java | 152 ++++++++++++++++++ .../org/bitcoinj/core/DumpedPrivateKey.java | 13 +- .../org/bitcoinj/core/NetworkParameters.java | 6 + .../java/org/bitcoinj/core/SegwitAddress.java | 133 +++++++++++++++ .../core/VersionedChecksummedBytes.java | 11 +- .../bitcoinj/core/WrongNetworkException.java | 9 +- .../org/bitcoinj/crypto/BIP38PrivateKey.java | 13 +- .../org/bitcoinj/params/MainNetParams.java | 1 + .../org/bitcoinj/params/TestNet2Params.java | 1 + .../org/bitcoinj/params/TestNet3Params.java | 1 + .../java/org/bitcoinj/core/AddressTest.java | 1 - .../java/org/bitcoinj/core/Bech32Test.java | 78 +++++++++ .../org/bitcoinj/core/SegwitAddressTest.java | 126 +++++++++++++++ .../core/VersionedChecksummedBytesTest.java | 4 +- 16 files changed, 592 insertions(+), 26 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/core/AbstractAddress.java create mode 100644 core/src/main/java/org/bitcoinj/core/Bech32.java create mode 100644 core/src/main/java/org/bitcoinj/core/SegwitAddress.java create mode 100644 core/src/test/java/org/bitcoinj/core/Bech32Test.java create mode 100644 core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java diff --git a/core/src/main/java/org/bitcoinj/core/AbstractAddress.java b/core/src/main/java/org/bitcoinj/core/AbstractAddress.java new file mode 100644 index 00000000000..8122aa02ff3 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/AbstractAddress.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.core; + +import javax.annotation.Nullable; + +public class AbstractAddress extends VersionedChecksummedBytes { + public AbstractAddress(NetworkParameters params, byte[] bytes) { + super(params, bytes); + } + + /** + * Construct an address from its textual representation. + * + * @param params + * The expected NetworkParameters or null if you don't want validation. + * @param str + * The textual form of the address, such as "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL". + * @throws AddressFormatException + * if the given string doesn't parse or the checksum is invalid + * @throws WrongNetworkException + * if the given string is valid but for a different chain (eg testnet vs mainnet) + */ + public static AbstractAddress fromString(@Nullable NetworkParameters params, String str) + throws AddressFormatException { + try { + return Address.fromBase58(params, str); + } catch (WrongNetworkException x) { + throw x; + } catch (AddressFormatException x) { + try { + return SegwitAddress.fromBech32(params, str); + } catch (WrongNetworkException x2) { + throw x; + } catch (AddressFormatException x2) { + throw new AddressFormatException(str); + } + } + } +} diff --git a/core/src/main/java/org/bitcoinj/core/Address.java b/core/src/main/java/org/bitcoinj/core/Address.java index d7e14e00b9a..c9183cee178 100644 --- a/core/src/main/java/org/bitcoinj/core/Address.java +++ b/core/src/main/java/org/bitcoinj/core/Address.java @@ -40,7 +40,7 @@ * should be interpreted. Whilst almost all addresses today are hashes of public keys, another (currently unsupported * type) can contain a hash of a script instead.

*/ -public class Address extends VersionedChecksummedBytes { +public class Address extends AbstractAddress { /** * An address is a RIPEMD160 hash of a public key, therefore is always 160 bits or 20 bytes. */ @@ -114,9 +114,11 @@ else if (version == params.getP2SHHeader()) } } - @Override - protected int getVersion() { - return p2sh ? params.getP2SHHeader() : params.getAddressHeader(); + /** + * Returns the base58-encoded textual representation, including version and checksum bytes. + */ + public String toBase58() { + return toBase58(p2sh ? params.getP2SHHeader() : params.getAddressHeader(), bytes); } /** The (big endian) 20 byte hash that is the core of a Bitcoin address. */ @@ -174,6 +176,11 @@ public int hashCode() { return Objects.hashCode(super.hashCode(), p2sh); } + @Override + public String toString() { + return toBase58(); + } + /** * This implementation narrows the return type to Address. */ diff --git a/core/src/main/java/org/bitcoinj/core/Bech32.java b/core/src/main/java/org/bitcoinj/core/Bech32.java new file mode 100644 index 00000000000..dc23b92a06e --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/Bech32.java @@ -0,0 +1,152 @@ +/* Copyright (c) 2018 Coinomi Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.bitcoinj.core; + +import java.util.Arrays; +import java.util.Locale; + +public class Bech32 { + /** The Bech32 character set for encoding. */ + private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + /** The Bech32 character set for decoding. */ + private 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, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 + }; + + public static class Bech32Data { + final String hrp; + final byte[] values; + + private Bech32Data(final String hrp, final byte[] values) { + this.hrp = hrp; + this.values = values; + } + } + + /** Find the polynomial with value coefficients mod the generator as 30-bit. */ + private static int polymod(final byte[] values) { + int c = 1; + for (byte v_i: values) { + int c0 = (c >>> 25) & 0xff; + c = ((c & 0x1ffffff) << 5) ^ (v_i & 0xff); + if ((c0 & 1) != 0) c ^= 0x3b6a57b2; + if ((c0 & 2) != 0) c ^= 0x26508e6d; + if ((c0 & 4) != 0) c ^= 0x1ea119fa; + if ((c0 & 8) != 0) c ^= 0x3d4233dd; + if ((c0 & 16) != 0) c ^= 0x2a1462b3; + } + return c; + } + + /** Expand a HRP for use in checksum computation. */ + private static byte[] expandHrp(final String hrp) { + int hrpLength = hrp.length(); + byte ret[] = new byte[hrpLength * 2 + 1]; + for (int i = 0; i < hrpLength; ++i) { + int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII + ret[i] = (byte) ((c >>> 5) & 0x07); + ret[i + hrpLength + 1] = (byte) (c & 0x1f); + } + ret[hrpLength] = 0; + return ret; + } + + /** Verify a checksum. */ + private static boolean 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; + } + + /** Create a checksum. */ + private static byte[] createChecksum(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; + byte[] ret = new byte[6]; + for (int i = 0; i < 6; ++i) { + ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31); + } + return ret; + } + + /** Encode a Bech32 string. */ + public static String encode(final Bech32Data bech32) throws AddressFormatException { + return encode(bech32.hrp, bech32.values); + } + + /** Encode a Bech32 string. */ + public static String encode(String hrp, final byte[] values) throws AddressFormatException { + if (hrp.length() < 1) throw new AddressFormatException("Human-readable part is too short"); + if (hrp.length() > 83) throw new AddressFormatException("Human-readable part is too long"); + hrp = hrp.toLowerCase(Locale.ROOT); + byte[] checksum = createChecksum(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); + StringBuilder sb = new StringBuilder(hrp.length() + 1 + combined.length); + sb.append(hrp); + sb.append('1'); + for (byte b : combined) { + sb.append(CHARSET.charAt(b)); + } + return sb.toString(); + } + + /** Decode a Bech32 string. */ + public static Bech32Data decode(final String str) throws AddressFormatException { + boolean lower = false, upper = false; + if (str.length() < 8) throw new AddressFormatException("Input too short"); + if (str.length() > 90) throw new AddressFormatException("Input too long"); + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + if (c < 33 || c > 126) throw new AddressFormatException("Characters out of range"); + if (c >= 'a' && c <= 'z') lower = true; + if (c >= 'A' && c <= 'Z') upper = true; + } + if (lower && upper) throw new AddressFormatException("Cannot mix upper and lower cases"); + int pos = str.lastIndexOf('1'); + if (pos < 1) throw new AddressFormatException("Missing human-readable part"); + if (pos + 7 > str.length()) throw new AddressFormatException("Data part too short"); + byte[] values = new byte[str.length() - 1 - pos]; + for (int i = 0; i < str.length() - 1 - pos; ++i) { + char c = str.charAt(i + pos + 1); + if (CHARSET_REV[c] == -1) throw new AddressFormatException("Characters out of range"); + values[i] = CHARSET_REV[c]; + } + String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT); + if (!verifyChecksum(hrp, values)) throw new AddressFormatException("Invalid checksum"); + return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6)); + } +} diff --git a/core/src/main/java/org/bitcoinj/core/DumpedPrivateKey.java b/core/src/main/java/org/bitcoinj/core/DumpedPrivateKey.java index 25a6726017b..7a5e7004a69 100644 --- a/core/src/main/java/org/bitcoinj/core/DumpedPrivateKey.java +++ b/core/src/main/java/org/bitcoinj/core/DumpedPrivateKey.java @@ -71,9 +71,11 @@ private DumpedPrivateKey(NetworkParameters params, byte[] bytes) { this(params, encode(keyBytes, compressed)); } - @Override - protected int getVersion() { - return params.getDumpedPrivateKeyHeader(); + /** + * Returns the base58-encoded textual representation, including version and checksum bytes. + */ + public String toBase58() { + return toBase58(params.getDumpedPrivateKeyHeader(), bytes); } private static byte[] encode(byte[] keyBytes, boolean compressed) { @@ -102,4 +104,9 @@ public ECKey getKey() { public boolean isPubKeyCompressed() { return bytes.length == 33 && bytes[32] == 1; } + + @Override + public String toString() { + return toBase58(); + } } diff --git a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java index 10eb323f8f6..ca2810f4f2f 100644 --- a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java +++ b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java @@ -77,6 +77,7 @@ public abstract class NetworkParameters { protected int addressHeader; protected int p2shHeader; protected int dumpedPrivateKeyHeader; + protected String segwitAddressHrp; protected int interval; protected int targetTimespan; protected byte[] alertSigningKey; @@ -336,6 +337,11 @@ public int getDumpedPrivateKeyHeader() { return dumpedPrivateKeyHeader; } + /** Human readable part of bech32 encoded segwit address. */ + public String getSegwitAddressHrp() { + return segwitAddressHrp; + } + /** * How much time in seconds is supposed to pass between "interval" blocks. If the actual elapsed time is * significantly different from this value, the network difficulty formula will produce a different value. Both diff --git a/core/src/main/java/org/bitcoinj/core/SegwitAddress.java b/core/src/main/java/org/bitcoinj/core/SegwitAddress.java new file mode 100644 index 00000000000..704905d8abc --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/SegwitAddress.java @@ -0,0 +1,133 @@ +/* + * Copyright 2018 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.core; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.annotation.Nullable; + +import org.bitcoinj.params.Networks; + +public class SegwitAddress extends AbstractAddress { + public SegwitAddress(NetworkParameters params, int witnessVersion, byte[] witnessProgram) + throws AddressFormatException { + this(params, encode(witnessVersion, witnessProgram)); + } + + private static byte[] encode(int witnessVersion, byte[] witnessProgram) throws AddressFormatException { + try { + ByteArrayOutputStream enc = new ByteArrayOutputStream(64 + 1); + enc.write(witnessVersion); + enc.write(convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true)); + return enc.toByteArray(); + } catch (IOException x) { + throw new RuntimeException(x); + } + } + + private SegwitAddress(NetworkParameters params, byte[] bytes) { + super(params, bytes); + if (bytes.length < 1) + throw new AddressFormatException("Zero data found"); + final int witnessVersion = witnessVersion(); + if (witnessVersion > 16) + throw new AddressFormatException("Invalid script version"); + byte[] witnessProgram = witnessProgram(); + if (witnessProgram.length < 2 || witnessProgram.length > 40) + throw new AddressFormatException("Invalid length"); + // check script length for version 0 + if (witnessVersion == 0 && witnessProgram.length != 20 && witnessProgram.length != 32) + throw new AddressFormatException("Invalid length for address version 0"); + } + + private int witnessVersion() { + return bytes[0] & 0xff; + } + + private byte[] witnessProgram() { + // skip version byte + return convertBits(bytes, 1, bytes.length - 1, 5, 8, false); + } + + public byte[] toScriptPubKey() { + ByteArrayOutputStream pubkey = new ByteArrayOutputStream(40 + 1); + int v = witnessVersion(); + // OP_0 is encoded as 0x00, but OP_1 through OP_16 are encoded as 0x51 though 0x60 + if (v > 0) + v += 0x50; + pubkey.write(v); + byte[] program = witnessProgram(); + pubkey.write(program.length); + pubkey.write(program, 0, program.length); + return pubkey.toByteArray(); + } + + @Override + public String toString() { + return toBech32(); + } + + public static SegwitAddress fromBech32(@Nullable NetworkParameters params, String bech32) + throws AddressFormatException { + Bech32.Bech32Data data = Bech32.decode(bech32); + if (params == null) { + for (NetworkParameters p : Networks.get()) { + if (data.hrp.equals(p.getSegwitAddressHrp())) + return new SegwitAddress(p, data.values); + } + throw new AddressFormatException("No network found for " + bech32); + } else { + if (data.hrp.equals(params.getSegwitAddressHrp())) + return new SegwitAddress(params, data.values); + throw new WrongNetworkException(0 /* dec.hrp */); + } + } + + public String toBech32() { + return Bech32.encode(params.getSegwitAddressHrp(), bytes); + } + + private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits, + final int toBits, final boolean pad) throws AddressFormatException { + int acc = 0; + int bits = 0; + ByteArrayOutputStream out = new ByteArrayOutputStream(64); + final int maxv = (1 << toBits) - 1; + final int max_acc = (1 << (fromBits + toBits - 1)) - 1; + for (int i = 0; i < inLen; i++) { + int value = in[i + inStart] & 0xff; + if ((value >>> fromBits) != 0) { + throw new AddressFormatException( + String.format("Input value '%X' exceeds '%d' bit size", value, fromBits)); + } + acc = ((acc << fromBits) | value) & max_acc; + bits += fromBits; + while (bits >= toBits) { + bits -= toBits; + out.write((acc >>> bits) & maxv); + } + } + if (pad) { + if (bits > 0) + out.write((acc << (toBits - bits)) & maxv); + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) { + throw new AddressFormatException("Could not convert bits, invalid padding"); + } + return out.toByteArray(); + } +} diff --git a/core/src/main/java/org/bitcoinj/core/VersionedChecksummedBytes.java b/core/src/main/java/org/bitcoinj/core/VersionedChecksummedBytes.java index 350b28d6090..b1a5d19bbfa 100644 --- a/core/src/main/java/org/bitcoinj/core/VersionedChecksummedBytes.java +++ b/core/src/main/java/org/bitcoinj/core/VersionedChecksummedBytes.java @@ -49,24 +49,17 @@ protected VersionedChecksummedBytes(NetworkParameters params, byte[] bytes) { * Returns the base-58 encoded String representation of this * object, including version and checksum bytes. */ - public final String toBase58() { + protected static String toBase58(int version, byte[] bytes) { // A stringified buffer is: // 1 byte version + data bytes + 4 bytes check code (a truncated hash) byte[] addressBytes = new byte[1 + bytes.length + 4]; - addressBytes[0] = (byte) getVersion(); + addressBytes[0] = (byte) version; System.arraycopy(bytes, 0, addressBytes, 1, bytes.length); byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, bytes.length + 1); System.arraycopy(checksum, 0, addressBytes, bytes.length + 1, 4); return Base58.encode(addressBytes); } - protected abstract int getVersion(); - - @Override - public String toString() { - return toBase58(); - } - @Override public int hashCode() { return Objects.hashCode(params, Arrays.hashCode(bytes)); diff --git a/core/src/main/java/org/bitcoinj/core/WrongNetworkException.java b/core/src/main/java/org/bitcoinj/core/WrongNetworkException.java index 3a545cac49f..1f2e486d0bb 100644 --- a/core/src/main/java/org/bitcoinj/core/WrongNetworkException.java +++ b/core/src/main/java/org/bitcoinj/core/WrongNetworkException.java @@ -1,5 +1,6 @@ /* * Copyright 2012 Google Inc. + * Copyright 2018 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,11 +23,11 @@ * different chains, an operation that is guaranteed to destroy the money. */ public class WrongNetworkException extends AddressFormatException { - /** The version code that was provided in the address. */ - public int verCode; - public WrongNetworkException(int verCode) { super("Version code of address did not match acceptable versions for network: " + verCode); - this.verCode = verCode; + } + + public WrongNetworkException(String hrp) { + super("Human readable part of address did not match acceptable HRPs for network: " + hrp); } } diff --git a/core/src/main/java/org/bitcoinj/crypto/BIP38PrivateKey.java b/core/src/main/java/org/bitcoinj/crypto/BIP38PrivateKey.java index 2c5ab80474a..96da14fa405 100644 --- a/core/src/main/java/org/bitcoinj/crypto/BIP38PrivateKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/BIP38PrivateKey.java @@ -105,9 +105,11 @@ private BIP38PrivateKey(NetworkParameters params, byte[] bytes, boolean ecMultip this.content = content; } - @Override - protected int getVersion() { - return 1; + /** + * Returns the base58-encoded textual representation, including version and checksum bytes. + */ + public String toBase58() { + return toBase58(1, bytes); } public ECKey decrypt(String passphrase) throws BadPassphraseException { @@ -186,4 +188,9 @@ private ECKey decryptEC(String normalizedPassphrase) { throw new RuntimeException(x); } } + + @Override + public String toString() { + return toBase58(); + } } diff --git a/core/src/main/java/org/bitcoinj/params/MainNetParams.java b/core/src/main/java/org/bitcoinj/params/MainNetParams.java index 194ff7738cd..738c50df589 100644 --- a/core/src/main/java/org/bitcoinj/params/MainNetParams.java +++ b/core/src/main/java/org/bitcoinj/params/MainNetParams.java @@ -40,6 +40,7 @@ public MainNetParams() { dumpedPrivateKeyHeader = 128; addressHeader = 0; p2shHeader = 5; + segwitAddressHrp = "bc"; port = 8333; packetMagic = 0xf9beb4d9L; bip32HeaderPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub". diff --git a/core/src/main/java/org/bitcoinj/params/TestNet2Params.java b/core/src/main/java/org/bitcoinj/params/TestNet2Params.java index 4b1755e3cca..cf1ee44089b 100644 --- a/core/src/main/java/org/bitcoinj/params/TestNet2Params.java +++ b/core/src/main/java/org/bitcoinj/params/TestNet2Params.java @@ -40,6 +40,7 @@ public TestNet2Params() { targetTimespan = TARGET_TIMESPAN; maxTarget = Utils.decodeCompactBits(0x1d0fffffL); dumpedPrivateKeyHeader = 239; + segwitAddressHrp = "tb"; genesisBlock.setTime(1296688602L); genesisBlock.setDifficultyTarget(0x1d07fff8L); genesisBlock.setNonce(384568319); diff --git a/core/src/main/java/org/bitcoinj/params/TestNet3Params.java b/core/src/main/java/org/bitcoinj/params/TestNet3Params.java index e9a3472c7d1..051f6002dc2 100644 --- a/core/src/main/java/org/bitcoinj/params/TestNet3Params.java +++ b/core/src/main/java/org/bitcoinj/params/TestNet3Params.java @@ -47,6 +47,7 @@ public TestNet3Params() { addressHeader = 111; p2shHeader = 196; dumpedPrivateKeyHeader = 239; + segwitAddressHrp = "tb"; genesisBlock.setTime(1296688602L); genesisBlock.setDifficultyTarget(0x1d00ffffL); genesisBlock.setNonce(414098458); diff --git a/core/src/test/java/org/bitcoinj/core/AddressTest.java b/core/src/test/java/org/bitcoinj/core/AddressTest.java index 6e3b3a0715a..a6118816809 100644 --- a/core/src/test/java/org/bitcoinj/core/AddressTest.java +++ b/core/src/test/java/org/bitcoinj/core/AddressTest.java @@ -104,7 +104,6 @@ public void errorPaths() { fail(); } catch (WrongNetworkException e) { // Success. - assertEquals(e.verCode, MainNetParams.get().getAddressHeader()); } catch (AddressFormatException e) { fail(); } diff --git a/core/src/test/java/org/bitcoinj/core/Bech32Test.java b/core/src/test/java/org/bitcoinj/core/Bech32Test.java new file mode 100644 index 00000000000..5b7f1e165bb --- /dev/null +++ b/core/src/test/java/org/bitcoinj/core/Bech32Test.java @@ -0,0 +1,78 @@ +/* Copyright (c) 2018 Coinomi Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.bitcoinj.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.Locale; + +import org.junit.Test; + +public class Bech32Test { + @Test + public void validChecksum() { + for (String valid : VALID_CHECKSUMS) { + Bech32.Bech32Data dec = Bech32.decode(valid); + String recode = Bech32.encode(dec); + 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(dec.hrp.toUpperCase(Locale.ROOT), dec.values); + assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode), + valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT)); + } + } + + @Test + public void invalidChecksum() { + for (String invalid : INVALID_CHECKSUMS) { + try { + Bech32.decode(invalid); + fail(String.format("Parsed an invalid code: '%s'", invalid)); + } catch (AddressFormatException x) { + /* expected */ + } + } + } + + // test vectors + + private static String[] VALID_CHECKSUMS = { + "A12UEL5L", + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", + "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w" + }; + + private static String[] INVALID_CHECKSUMS = { + " 1nwldj5", + new String(new char[] { 0x7f }) + "1axkwrx", + "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", + "pzry9x0s0muk", + "1pzry9x0s0muk", + "x1b4n0q5v", + "li1dgmt3", + "de1lg7wt" + new String(new char[] { 0xff }), + }; +} diff --git a/core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java b/core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java new file mode 100644 index 00000000000..67863c23c08 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Locale; + +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.TestNet3Params; +import org.junit.Test; + +public class SegwitAddressTest { + private static final MainNetParams MAINNET = MainNetParams.get(); + private static final TestNet3Params TESTNET = TestNet3Params.get(); + + @Test + public void fromBech32_mainnet() { + String bech32 = "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"; + + SegwitAddress address = SegwitAddress.fromBech32(MAINNET, bech32); + + assertEquals(MAINNET, address.params); + assertEquals("0014751e76e8199196d454941c45d1b3a323f1433bd6", Utils.HEX.encode(address.toScriptPubKey())); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toBech32()); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toString()); + } + + @Test + public void fromBech32_testnet() { + String bech32 = "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"; + + SegwitAddress address = SegwitAddress.fromBech32(TESTNET, bech32); + + assertEquals(TESTNET, address.params); + assertEquals("00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + Utils.HEX.encode(address.toScriptPubKey())); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toBech32()); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toString()); + } + + @Test + public void validAddresses() { + for (AddressData valid : VALID_ADDRESSES) { + SegwitAddress address = SegwitAddress.fromBech32(null, valid.address); + + assertEquals(valid.scriptPubKey, Utils.HEX.encode(address.toScriptPubKey())); + assertEquals(valid.address.toLowerCase(Locale.ROOT), address.toBech32()); + } + } + + private static class AddressData { + final String address; + final String scriptPubKey; + + AddressData(String address, String scriptPubKey) { + this.address = address; + this.scriptPubKey = scriptPubKey; + } + } + + private static AddressData[] VALID_ADDRESSES = { + new AddressData("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "0014751e76e8199196d454941c45d1b3a323f1433bd6"), + new AddressData("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"), + new AddressData("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"), + new AddressData("BC1SW50QA3JX3S", "6002751e"), + new AddressData("bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"), + new AddressData("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433") }; + + @Test + public void invalidAddresses() { + for (String invalid : INVALID_ADDRESSES) { + try { + SegwitAddress.fromBech32(null, invalid); + fail(invalid); + } catch (AddressFormatException x) { + // expected + } + } + } + + private static String[] INVALID_ADDRESSES = { "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", "bc1rw5uspcuh", + "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + "bc1gmk9yu" }; + + @Test + public void testJavaSerialization() throws Exception { + SegwitAddress address = SegwitAddress.fromBech32(null, "BC1SW50QA3JX3S"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + new ObjectOutputStream(os).writeObject(address); + VersionedChecksummedBytes addressCopy = (VersionedChecksummedBytes) new ObjectInputStream( + new ByteArrayInputStream(os.toByteArray())).readObject(); + + assertEquals(address, addressCopy); + assertEquals(address.params, addressCopy.params); + assertArrayEquals(address.bytes, addressCopy.bytes); + } +} diff --git a/core/src/test/java/org/bitcoinj/core/VersionedChecksummedBytesTest.java b/core/src/test/java/org/bitcoinj/core/VersionedChecksummedBytesTest.java index 96057b6c1d7..9c90e37fae0 100644 --- a/core/src/test/java/org/bitcoinj/core/VersionedChecksummedBytesTest.java +++ b/core/src/test/java/org/bitcoinj/core/VersionedChecksummedBytesTest.java @@ -35,8 +35,8 @@ public VersionedChecksummedBytesToTest(NetworkParameters params, byte[] bytes) { } @Override - protected int getVersion() { - return params.getAddressHeader(); + public String toString() { + return toBase58(params.getAddressHeader(), bytes); } }