From 68f65485053aca5e23892c826e50d7fead6cd9da Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Thu, 15 Feb 2018 14:02:53 +0100 Subject: [PATCH] Implement native segwit addresses in new SegwitAddress class. This commit establishes a base class for legacy and segwit addresses. Uses Bech32 code from https://github.com/sipa/bech32/pull/40. --- .../org/bitcoinj/core/AbstractAddress.java | 83 ++++++ .../main/java/org/bitcoinj/core/Address.java | 47 +++- .../main/java/org/bitcoinj/core/Bech32.java | 152 +++++++++++ .../org/bitcoinj/core/DumpedPrivateKey.java | 15 +- .../org/bitcoinj/core/NetworkParameters.java | 6 + .../java/org/bitcoinj/core/SegwitAddress.java | 255 ++++++++++++++++++ .../core/VersionedChecksummedBytes.java | 15 -- .../bitcoinj/core/WrongNetworkException.java | 9 +- .../org/bitcoinj/crypto/BIP38PrivateKey.java | 15 +- .../org/bitcoinj/params/MainNetParams.java | 1 + .../org/bitcoinj/params/TestNet2Params.java | 1 + .../org/bitcoinj/params/TestNet3Params.java | 1 + .../main/java/org/bitcoinj/script/Script.java | 11 +- .../org/bitcoinj/script/ScriptBuilder.java | 47 ++-- .../java/org/bitcoinj/core/AddressTest.java | 1 - .../java/org/bitcoinj/core/Bech32Test.java | 78 ++++++ .../org/bitcoinj/core/SegwitAddressTest.java | 175 ++++++++++++ .../core/VersionedChecksummedBytesTest.java | 4 +- 18 files changed, 864 insertions(+), 52 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..d8a5560e211 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/AbstractAddress.java @@ -0,0 +1,83 @@ +/* + * 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; + +import org.bitcoinj.script.Script.ScriptType; + +/** + *

+ * Base class for addresses, e.g. native segwit addresses ({@link SegwitAddress}) or legacy addresses ({@link Address}). + *

+ * + *

+ * Use {@link #fromString(NetworkParameters, String)} to conveniently construct any kind of address from its textual + * form. + *

+ */ +public abstract class AbstractAddress extends VersionedChecksummedBytes { + public AbstractAddress(NetworkParameters params, byte[] bytes) { + super(params, bytes); + } + + /** + * Construct an address from its textual form. + * + * @param params + * the expected network this address is valid for, or null if the network should be derived from the + * textual form + * @param str + * the textual form of the address, such as "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL" or + * "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" + * @return constructed address + * @throws AddressFormatException + * if the given string doesn't parse or the checksum is invalid + * @throws WrongNetworkException + * if the given string is valid but not for the expected network (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); + } + } + } + + /** + * Get either the public key hash or script hash that is encoded in the address. + * + * @return hash that is encoded in the address + */ + public abstract byte[] getHash(); + + /** + * Get the type of output script that will be used for sending to the address. + * + * @return type of output script + */ + public abstract ScriptType getOutputScriptType(); +} diff --git a/core/src/main/java/org/bitcoinj/core/Address.java b/core/src/main/java/org/bitcoinj/core/Address.java index 6e6a8870325..75d80723c02 100644 --- a/core/src/main/java/org/bitcoinj/core/Address.java +++ b/core/src/main/java/org/bitcoinj/core/Address.java @@ -26,6 +26,7 @@ import org.bitcoinj.params.Networks; import org.bitcoinj.script.Script; +import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.script.ScriptPattern; import com.google.common.base.Objects; @@ -41,7 +42,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. */ @@ -167,20 +168,53 @@ public Address(NetworkParameters params, byte[] hash160) { this(params, false, hash160); } - @Override - protected int getVersion() { + /** + * Get the version header of an address. This is the first byte of a base58 encoded address. + * + * @return version header as one byte + */ + public int getVersion() { return p2sh ? params.getP2SHHeader() : params.getAddressHeader(); } - /** The (big endian) 20 byte hash that is the core of a Bitcoin address. */ + /** + * Returns the base58-encoded textual form, including version and checksum bytes. + * + * @return textual form + */ + public String toBase58() { + return Base58.encodeChecked(getVersion(), bytes); + } + + /** @deprecated use {@link #getHash()} */ + @Deprecated public byte[] getHash160() { + return getHash(); + } + + /** The (big endian) 20 byte hash that is the core of a Bitcoin address. */ + @Override + public byte[] getHash() { return bytes; } + /** + * Get the type of output script that will be used for sending to the address. This is either + * {@link ScriptType#P2PKH} or {@link ScriptType#P2SH}. + * + * @return type of output script + */ + @Override + public ScriptType getOutputScriptType() { + return p2sh ? ScriptType.P2SH : ScriptType.P2PKH; + } + /** * Returns true if this address is a Pay-To-Script-Hash (P2SH) address. * See also https://github.com/bitcoin/bips/blob/master/bip-0013.mediawiki: Address Format for pay-to-script-hash + * @deprecated Use {@link #getOutputScriptType()} */ + @Deprecated public boolean isP2SHAddress() { return p2sh; } @@ -216,6 +250,11 @@ public int hashCode() { return Objects.hashCode(super.hashCode(), p2sh); } + @Override + public String toString() { + return toBase58(); + } + @Override public Address clone() throws CloneNotSupportedException { return (Address) super.clone(); 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..22b5ec37a71 --- /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[] data; + + private Bech32Data(final String hrp, final byte[] data) { + this.hrp = hrp; + this.data = data; + } + } + + /** 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.data); + } + + /** 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..77bc241c9f9 100644 --- a/core/src/main/java/org/bitcoinj/core/DumpedPrivateKey.java +++ b/core/src/main/java/org/bitcoinj/core/DumpedPrivateKey.java @@ -71,9 +71,13 @@ private DumpedPrivateKey(NetworkParameters params, byte[] bytes) { this(params, encode(keyBytes, compressed)); } - @Override - protected int getVersion() { - return params.getDumpedPrivateKeyHeader(); + /** + * Returns the base58-encoded textual form, including version and checksum bytes. + * + * @return textual form + */ + public String toBase58() { + return Base58.encodeChecked(params.getDumpedPrivateKeyHeader(), bytes); } private static byte[] encode(byte[] keyBytes, boolean compressed) { @@ -102,4 +106,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..92368e894de --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/SegwitAddress.java @@ -0,0 +1,255 @@ +/* + * 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 javax.annotation.Nullable; + +import org.bitcoinj.params.Networks; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.Script.ScriptType; + +/** + *

+ * Implementation of native segwit addresses. They are composed of two parts: + *

+ * See BIP173 for details. + *

+ * + *

+ * 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 + * segwit address. + *

+ */ +public class SegwitAddress extends AbstractAddress { + public static final int WITNESS_PROGRAM_LENGTH_PKH = 20; + public static final int WITNESS_PROGRAM_LENGTH_SH = 32; + public static final int WITNESS_PROGRAM_MIN_LENGTH = 2; + public static final int WITNESS_PROGRAM_MAX_LENGTH = 40; + + /** + * Private constructor. Use {@link #fromBech32(NetworkParameters, String)}, + * {@link #fromHash(NetworkParameters, byte[])} or {@link #fromKey(NetworkParameters, ECKey)}. + * + * @param params + * network this address is valid for + * @param witnessVersion + * version number between 0 and 16 + * @param witnessProgram + * hash of pubkey or script (for version 0) + */ + private SegwitAddress(NetworkParameters params, int witnessVersion, byte[] witnessProgram) + throws AddressFormatException { + this(params, encode(witnessVersion, witnessProgram)); + } + + /** + * Helper for the above constructor. + */ + private static byte[] encode(int witnessVersion, byte[] witnessProgram) throws AddressFormatException { + byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true); + byte[] bytes = new byte[1 + convertedProgram.length]; + bytes[0] = (byte) (Script.encodeToOpN(witnessVersion) & 0xff); + System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length); + return bytes; + } + + /** + * Private constructor. Use {@link #fromBech32(NetworkParameters, String)}, + * {@link #fromHash(NetworkParameters, byte[])} or {@link #fromKey(NetworkParameters, ECKey)}. + * + * @param params + * network this address is valid for + * @param data + * in segwit address format, before bit re-arranging and bech32 encoding + * @throws AddressFormatException + * if any of the sanity checks fail + */ + private SegwitAddress(NetworkParameters params, byte[] data) throws AddressFormatException { + super(params, data); + if (data.length < 1) + throw new AddressFormatException("Zero data found"); + final int witnessVersion = getWitnessVersion(); + if (witnessVersion < 0 || witnessVersion > 16) + throw new AddressFormatException("Invalid script version: " + witnessVersion); + byte[] witnessProgram = getWitnessProgram(); + if (witnessProgram.length < WITNESS_PROGRAM_MIN_LENGTH || witnessProgram.length > WITNESS_PROGRAM_MAX_LENGTH) + throw new AddressFormatException("Invalid length: " + witnessProgram.length); + // Check script length for version 0 + if (witnessVersion == 0 && witnessProgram.length != WITNESS_PROGRAM_LENGTH_PKH + && witnessProgram.length != WITNESS_PROGRAM_LENGTH_SH) + throw new AddressFormatException("Invalid length for address version 0: " + witnessProgram.length); + } + + /** + * Returns the witness version in decoded form. Only version 0 is in use right now. + * + * @return witness version, between 0 and 16 + */ + public int getWitnessVersion() { + return bytes[0] & 0xff; + } + + /** + * Returns the witness program in decoded form. + * + * @return witness program + */ + public byte[] getWitnessProgram() { + // skip version byte + return convertBits(bytes, 1, bytes.length - 1, 5, 8, false); + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] getHash() { + return getWitnessProgram(); + } + + /** + * Get the type of output script that will be used for sending to the address. This is either + * {@link ScriptType#P2WPKH} or {@link ScriptType#P2WSH}. + * + * @return type of output script + */ + @Override + public ScriptType getOutputScriptType() { + int version = getWitnessVersion(); + if (version != 0) + return ScriptType.NO_TYPE; + int programLength = getWitnessProgram().length; + if (programLength == WITNESS_PROGRAM_LENGTH_PKH) + return ScriptType.P2WPKH; + else if (programLength == WITNESS_PROGRAM_LENGTH_SH) + return ScriptType.P2WSH; + else + return ScriptType.NO_TYPE; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return toBech32(); + } + + /** + * Construct a {@link SegwitAddress} from its textual form. + * + * @param params + * expected network this address is valid for, or null if the network should be derived from the bech32 + * @param bech32 + * bech32-encoded textual form of the address + * @return constructed address + * @throws AddressFormatException + * if something about the given bech32 address isn't right + */ + public static SegwitAddress fromBech32(@Nullable NetworkParameters params, String bech32) + throws AddressFormatException { + Bech32.Bech32Data bechData = Bech32.decode(bech32); + if (params == null) { + for (NetworkParameters p : Networks.get()) { + if (bechData.hrp.equals(p.getSegwitAddressHrp())) + return new SegwitAddress(p, bechData.data); + } + throw new AddressFormatException("No network found for " + bech32); + } else { + if (bechData.hrp.equals(params.getSegwitAddressHrp())) + return new SegwitAddress(params, bechData.data); + throw new WrongNetworkException(bechData.hrp); + } + } + + /** + * 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. + * + * @param params + * network this address is valid for + * @param hash + * 20-byte pubkey hash or 32-byte script hash + * @return constructed address + */ + public static SegwitAddress fromHash(NetworkParameters params, byte[] hash) { + return new SegwitAddress(params, 0, hash); + } + + /** + * Construct a {@link SegwitAddress} that represents the public part of the given {@link ECKey}. Note that an + * address is derived from a hash of the public key and is not the public key itself. + * + * @param params + * network this address is valid for + * @param key + * only the public part is used + * @return constructed address + */ + public static SegwitAddress fromKey(NetworkParameters params, ECKey key) { + return fromHash(params, key.getPubKeyHash()); + } + + /** + * Returns the textual form of the address. + * + * @return textual form encoded in bech32 + */ + public String toBech32() { + return Bech32.encode(params.getSegwitAddressHrp(), bytes); + } + + /** + * Helper for re-arranging bits into groups. + */ + 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 3973827306a..3b0f8a76fb8 100644 --- a/core/src/main/java/org/bitcoinj/core/VersionedChecksummedBytes.java +++ b/core/src/main/java/org/bitcoinj/core/VersionedChecksummedBytes.java @@ -57,21 +57,6 @@ public final NetworkParameters getParameters() { return params; } - /** - * Returns the base-58 encoded String representation of this - * object, including version and checksum bytes. - */ - public final String toBase58() { - return Base58.encodeChecked(getVersion(), bytes); - } - - 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 a875ba4f029..22b60bb2ec6 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 b4dc55b31aa..1f942a3ef32 100644 --- a/core/src/main/java/org/bitcoinj/crypto/BIP38PrivateKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/BIP38PrivateKey.java @@ -105,9 +105,13 @@ private BIP38PrivateKey(NetworkParameters params, byte[] bytes, boolean ecMultip this.content = content; } - @Override - protected int getVersion() { - return 1; + /** + * Returns the base58-encoded textual form, including version and checksum bytes. + * + * @return textual form + */ + public String toBase58() { + return Base58.encodeChecked(1, bytes); } public ECKey decrypt(String passphrase) throws BadPassphraseException { @@ -186,4 +190,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/main/java/org/bitcoinj/script/Script.java b/core/src/main/java/org/bitcoinj/script/Script.java index d08e316041f..a32b72a930f 100644 --- a/core/src/main/java/org/bitcoinj/script/Script.java +++ b/core/src/main/java/org/bitcoinj/script/Script.java @@ -60,7 +60,9 @@ public enum ScriptType { NO_TYPE, P2PKH, PUB_KEY, - P2SH + P2SH, + P2WPKH, + P2WSH } /** Flags to pass to {@link Script#correctlySpends(Transaction, long, Script, Set)}. @@ -507,8 +509,9 @@ private static int getSigOpCount(List chunks, boolean accurate) thr return sigOps; } - static int decodeFromOpN(int opcode) { - checkArgument((opcode == OP_0 || opcode == OP_1NEGATE) || (opcode >= OP_1 && opcode <= OP_16), "decodeFromOpN called on non OP_N opcode"); + public static int decodeFromOpN(int opcode) { + checkArgument((opcode == OP_0 || opcode == OP_1NEGATE) || (opcode >= OP_1 && opcode <= OP_16), + "decodeFromOpN called on non OP_N opcode: %s", ScriptOpCodes.getOpCodeName(opcode)); if (opcode == OP_0) return 0; else if (opcode == OP_1NEGATE) @@ -517,7 +520,7 @@ else if (opcode == OP_1NEGATE) return opcode + 1 - OP_1; } - static int encodeToOpN(int value) { + public static int encodeToOpN(int value) { checkArgument(value >= -1 && value <= 16, "encodeToOpN called for " + value + " which we cannot encode in an opcode."); if (value == 0) return OP_0; diff --git a/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java b/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java index efca88cb227..e847835c33f 100644 --- a/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java +++ b/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java @@ -18,10 +18,14 @@ package org.bitcoinj.script; import com.google.common.collect.Lists; + +import org.bitcoinj.core.AbstractAddress; import org.bitcoinj.core.Address; import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Utils; import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script.ScriptType; import javax.annotation.Nullable; import java.math.BigInteger; @@ -248,24 +252,35 @@ public Script build() { } /** Creates a scriptPubKey that encodes payment to the given address. */ - public static Script createOutputScript(Address to) { - if (to.isP2SHAddress()) { - // OP_HASH160 OP_EQUAL - return new ScriptBuilder() - .op(OP_HASH160) - .data(to.getHash160()) - .op(OP_EQUAL) - .build(); + public static Script createOutputScript(AbstractAddress to) { + ScriptBuilder builder = new ScriptBuilder(); + if (to instanceof Address) { + Address toLegacy = (Address) to; + ScriptType scriptType = toLegacy.getOutputScriptType(); + if (scriptType == ScriptType.P2PKH) { + // OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG + builder.op(OP_DUP); + builder.op(OP_HASH160); + builder.data(toLegacy.getHash()); + builder.op(OP_EQUALVERIFY); + builder.op(OP_CHECKSIG); + } else if (scriptType == ScriptType.P2SH) { + // OP_HASH160 OP_EQUAL + builder.op(OP_HASH160); + builder.data(toLegacy.getHash()); + builder.op(OP_EQUAL); + } else { + throw new IllegalStateException("Cannot handle " + scriptType); + } + } else if (to instanceof SegwitAddress) { + // OP_0 + SegwitAddress toSegwit = (SegwitAddress) to; + builder.smallNum(toSegwit.getWitnessVersion()); + builder.data(toSegwit.getWitnessProgram()); } else { - // OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG - return new ScriptBuilder() - .op(OP_DUP) - .op(OP_HASH160) - .data(to.getHash160()) - .op(OP_EQUALVERIFY) - .op(OP_CHECKSIG) - .build(); + throw new IllegalStateException("Cannot handle " + to); } + return builder.build(); } /** Creates a scriptPubKey that encodes payment to the given raw public key. */ diff --git a/core/src/test/java/org/bitcoinj/core/AddressTest.java b/core/src/test/java/org/bitcoinj/core/AddressTest.java index 54c5e67de56..21381d34eac 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..02fd331c553 --- /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 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)); + } + } + + @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..070577102fa --- /dev/null +++ b/core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java @@ -0,0 +1,175 @@ +/* + * 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.bitcoinj.script.Script.ScriptType; +import org.bitcoinj.script.ScriptBuilder; +import org.junit.Test; + +import com.google.common.base.MoreObjects; + +public class SegwitAddressTest { + private static final MainNetParams MAINNET = MainNetParams.get(); + private static final TestNet3Params TESTNET = TestNet3Params.get(); + + @Test + public void example_p2wpkh_mainnet() { + String bech32 = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"; + + SegwitAddress address = SegwitAddress.fromBech32(MAINNET, bech32); + + assertEquals(MAINNET, address.params); + assertEquals("0014751e76e8199196d454941c45d1b3a323f1433bd6", + Utils.HEX.encode(ScriptBuilder.createOutputScript(address).getProgram())); + assertEquals(ScriptType.P2WPKH, address.getOutputScriptType()); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toBech32()); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toString()); + } + + @Test + public void example_p2wsh_mainnet() { + String bech32 = "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"; + + SegwitAddress address = SegwitAddress.fromBech32(MAINNET, bech32); + + assertEquals(MAINNET, address.params); + assertEquals("00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + Utils.HEX.encode(ScriptBuilder.createOutputScript(address).getProgram())); + assertEquals(ScriptType.P2WSH, address.getOutputScriptType()); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toBech32()); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toString()); + } + + @Test + public void example_p2wpkh_testnet() { + String bech32 = "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"; + + SegwitAddress address = SegwitAddress.fromBech32(TESTNET, bech32); + + assertEquals(TESTNET, address.params); + assertEquals("0014751e76e8199196d454941c45d1b3a323f1433bd6", + Utils.HEX.encode(ScriptBuilder.createOutputScript(address).getProgram())); + assertEquals(ScriptType.P2WPKH, address.getOutputScriptType()); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toBech32()); + assertEquals(bech32.toLowerCase(Locale.ROOT), address.toString()); + } + + @Test + public void example_p2wsh_testnet() { + String bech32 = "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"; + + SegwitAddress address = SegwitAddress.fromBech32(TESTNET, bech32); + + assertEquals(TESTNET, address.params); + assertEquals("00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + Utils.HEX.encode(ScriptBuilder.createOutputScript(address).getProgram())); + assertEquals(ScriptType.P2WSH, address.getOutputScriptType()); + 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.expectedParams, address.params); + assertEquals(valid.expectedScriptPubKey, + Utils.HEX.encode(ScriptBuilder.createOutputScript(address).getProgram())); + assertEquals(valid.address.toLowerCase(Locale.ROOT), address.toBech32()); + assertEquals(valid.expectedWitnessVersion, address.getWitnessVersion()); + } + } + + private static class AddressData { + final String address; + final NetworkParameters expectedParams; + final String expectedScriptPubKey; + final int expectedWitnessVersion; + + AddressData(String address, NetworkParameters expectedParams, String expectedScriptPubKey, + int expectedWitnessVersion) { + this.address = address; + this.expectedParams = expectedParams; + this.expectedScriptPubKey = expectedScriptPubKey; + this.expectedWitnessVersion = expectedWitnessVersion; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("address", address).add("params", expectedParams.getId()) + .add("scriptPubKey", expectedScriptPubKey).add("witnessVersion", expectedWitnessVersion).toString(); + } + } + + private static AddressData[] VALID_ADDRESSES = { + new AddressData("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", MAINNET, + "0014751e76e8199196d454941c45d1b3a323f1433bd6", 0), + new AddressData("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", TESTNET, + "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", 0), + new AddressData("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", MAINNET, + "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", 1), + new AddressData("BC1SW50QA3JX3S", MAINNET, "6002751e", 16), + new AddressData("bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", MAINNET, "5210751e76e8199196d454941c45d1b3a323", 2), + new AddressData("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", TESTNET, + "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", 0) }; + + @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 4159a03873d..e9560ea5496 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 Base58.encodeChecked(params.getAddressHeader(), bytes); } }