Skip to content

Commit

Permalink
Implement native segwit addresses in new SegwitAddress class.
Browse files Browse the repository at this point in the history
This commit establishes a base class for legacy and segwit addresses.

Uses Bech32 code from sipa/bech32#40.
  • Loading branch information
Andreas Schildbach committed Feb 27, 2018
1 parent afcdc81 commit 68f6548
Show file tree
Hide file tree
Showing 18 changed files with 864 additions and 52 deletions.
83 changes: 83 additions & 0 deletions core/src/main/java/org/bitcoinj/core/AbstractAddress.java
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>
* Base class for addresses, e.g. native segwit addresses ({@link SegwitAddress}) or legacy addresses ({@link Address}).
* </p>
*
* <p>
* Use {@link #fromString(NetworkParameters, String)} to conveniently construct any kind of address from its textual
* form.
* </p>
*/
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();
}
47 changes: 43 additions & 4 deletions core/src/main/java/org/bitcoinj/core/Address.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.</p>
*/
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.
*/
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down
152 changes: 152 additions & 0 deletions core/src/main/java/org/bitcoinj/core/Bech32.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
15 changes: 12 additions & 3 deletions core/src/main/java/org/bitcoinj/core/DumpedPrivateKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -102,4 +106,9 @@ public ECKey getKey() {
public boolean isPubKeyCompressed() {
return bytes.length == 33 && bytes[32] == 1;
}

@Override
public String toString() {
return toBase58();
}
}
6 changes: 6 additions & 0 deletions core/src/main/java/org/bitcoinj/core/NetworkParameters.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 68f6548

Please sign in to comment.