From 06c84c2c23e853bb24a9415af4345c7d9082c10d Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 24 May 2011 21:42:08 +0000 Subject: [PATCH] Fix a bug in Base58 decoding. Refactor how it is handled and introduce a new DumpedPrivateKey class that can be used to load keys generated by the dumpprivkey RPC. Use a new VersionedChecksummedBytes class to share the code. --- src/com/google/bitcoin/core/Address.java | 64 +++------------ src/com/google/bitcoin/core/Base58.java | 15 ++-- .../google/bitcoin/core/DumpedPrivateKey.java | 58 ++++++++++++++ .../bitcoin/core/NetworkParameters.java | 12 ++- .../core/VersionedChecksummedBytes.java | 78 +++++++++++++++++++ .../google/bitcoin/examples/PrivateKeys.java | 14 +++- .../com/google/bitcoin/core/AddressTest.java | 6 +- 7 files changed, 177 insertions(+), 70 deletions(-) create mode 100644 src/com/google/bitcoin/core/DumpedPrivateKey.java create mode 100644 src/com/google/bitcoin/core/VersionedChecksummedBytes.java diff --git a/src/com/google/bitcoin/core/Address.java b/src/com/google/bitcoin/core/Address.java index 35110b55..93d460a6 100644 --- a/src/com/google/bitcoin/core/Address.java +++ b/src/com/google/bitcoin/core/Address.java @@ -34,19 +34,16 @@ import java.util.Arrays; * * Note that an address is specific to a network because the first byte is a discriminator value. */ -public class Address { - private byte[] hash160; - private NetworkParameters params; - +public class Address extends VersionedChecksummedBytes { /** * Construct an address from parameters and the hash160 form. Example:

* *

new Address(NetworkParameters.prodNet(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));
*/ public Address(NetworkParameters params, byte[] hash160) { - assert hash160.length == 20; - this.hash160 = hash160; - this.params = params; + super(params.addressHeader, hash160); + if (hash160.length != 20) // 160 = 8 * 20 + throw new RuntimeException("Addresses are 160-bit hashes, so you must provide 20 bytes"); } /** @@ -55,57 +52,14 @@ public class Address { *
new Address(NetworkParameters.prodNet(), "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");
*/ public Address(NetworkParameters params, String address) throws AddressFormatException { - this.params = params; - this.hash160 = strToHash160(address); + super(address); + if (version != params.addressHeader) + throw new AddressFormatException("Mismatched version number, trying to cross networks? " + version + + " vs " + params.addressHeader); } /** The (big endian) 20 byte hash that is the core of a BitCoin address. */ public byte[] getHash160() { - assert hash160 != null; - return hash160; - } - - // TODO: Make this use Base58.decodeChecked - private byte[] strToHash160(String address) throws AddressFormatException { - byte[] bytes = Base58.decode(address); - if (bytes.length != 25) { - // Zero pad the result. - byte[] tmp = new byte[25]; - System.arraycopy(bytes, 0, tmp, tmp.length - bytes.length, bytes.length); - bytes = tmp; - } - if (bytes[0] != params.addressHeader) - throw new AddressFormatException("Address header incorrect: from a different network?"); - byte[] check = Utils.doubleDigest(bytes, 0, 21); - if (check[0] != bytes[21] || check[1] != bytes[22] || check[2] != bytes[23] || check[3] != bytes[24]) - throw new AddressFormatException("Checksum failed: check the address for typos"); - byte[] hash160 = new byte[20]; - System.arraycopy(bytes, 1, hash160, 0, 20); - return hash160; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Address)) return false; - Address a = (Address) o; - return Arrays.equals(a.getHash160(), getHash160()); - } - - @Override - public int hashCode() { - return Arrays.hashCode(getHash160()); - } - - @Override - public String toString() { - byte[] input = hash160; - // A stringified address is: - // 1 byte version + 20 bytes hash + 4 bytes check code (itself a truncated hash) - byte[] addressBytes = new byte[1 + 20 + 4]; - addressBytes[0] = params.addressHeader; - System.arraycopy(input, 0, addressBytes, 1, 20); - byte[] check = Utils.doubleDigest(addressBytes, 0, 21); - System.arraycopy(check, 0, addressBytes, 21, 4); - return Base58.encode(addressBytes); + return bytes; } } diff --git a/src/com/google/bitcoin/core/Base58.java b/src/com/google/bitcoin/core/Base58.java index aaed4bd4..a195eb94 100644 --- a/src/com/google/bitcoin/core/Base58.java +++ b/src/com/google/bitcoin/core/Base58.java @@ -63,13 +63,16 @@ public class Base58 { // is because BigIntegers are represented with twos-compliment notation, thus if the high bit of the last // byte happens to be 1 another 8 zero bits will be added to ensure the number parses as positive. Detect // that case here and chop it off. - if ((bytes.length > 1) && (bytes[0] == 0) && (bytes[1] < 0)) { - // Java 6 has a convenience for this, but Android can't use it. - byte[] tmp = new byte[bytes.length - 1]; - System.arraycopy(bytes, 1, tmp, 0, bytes.length - 1); - bytes = tmp; + boolean stripSignByte = bytes.length > 1 && bytes[0] == 0 && bytes[1] < 0; + // Count the leading zeros, if any. + int leadingZeros = 0; + for (int i = 0; input.charAt(i) == ALPHABET.charAt(0); i++) { + leadingZeros++; } - return bytes; + // Now cut/pad correctly. Java 6 has a convenience for this, but Android can't use it. + byte[] tmp = new byte[bytes.length - (stripSignByte ? 1 : 0) + leadingZeros]; + System.arraycopy(bytes, stripSignByte ? 1 : 0, tmp, leadingZeros, tmp.length - leadingZeros); + return tmp; } public static BigInteger decodeToBigInteger(String input) throws AddressFormatException { diff --git a/src/com/google/bitcoin/core/DumpedPrivateKey.java b/src/com/google/bitcoin/core/DumpedPrivateKey.java new file mode 100644 index 00000000..5b90ff20 --- /dev/null +++ b/src/com/google/bitcoin/core/DumpedPrivateKey.java @@ -0,0 +1,58 @@ +/** + * Copyright 2011 Google Inc. + * + * 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 com.google.bitcoin.core; + +import java.math.BigInteger; + +/** + * Parses and generates private keys in the form used by the Bitcoin "dumpprivkey" command. This is the private key + * bytes with a header byte and 4 checksum bytes at the end. + */ +public class DumpedPrivateKey extends VersionedChecksummedBytes { + /** + * Allows the output of a private key in versioned, checksummed form. + * + * @param params The network parameters of this key, needed for the version byte. + * @param keyBytes The 256-bit private key. + */ + public DumpedPrivateKey(NetworkParameters params, byte[] keyBytes) { + super(params.dumpedPrivateKeyHeader, keyBytes); + if (keyBytes.length != 32) // 256 bit keys + throw new RuntimeException("Keys are 256 bits, so you must provide 32 bytes."); + } + + /** + * Parses the given private key as created by the "dumpprivkey" Bitcoin C++ RPC. + * + * @param params The expected network parameters of the key. If you don't care, provide null. + * @param encoded The base58 encoded string. + * @throws AddressFormatException If the string is invalid or the header byte doesn't match the network params. + */ + public DumpedPrivateKey(NetworkParameters params, String encoded) throws AddressFormatException { + super(encoded); + if (params != null && version != params.dumpedPrivateKeyHeader) + throw new AddressFormatException("Mismatched version number, trying to cross networks? " + version + + " vs " + params.dumpedPrivateKeyHeader); + } + + /** + * Returns an ECKey created from this encoded private key. + */ + public ECKey getKey() { + return new ECKey(new BigInteger(1, bytes)); + } +} diff --git a/src/com/google/bitcoin/core/NetworkParameters.java b/src/com/google/bitcoin/core/NetworkParameters.java index 4a0ac72b..b8cb44b8 100644 --- a/src/com/google/bitcoin/core/NetworkParameters.java +++ b/src/com/google/bitcoin/core/NetworkParameters.java @@ -29,13 +29,13 @@ import java.math.BigInteger; * evolves there may be more. You can create your own as long as they don't conflict. */ public class NetworkParameters implements Serializable { + private static final long serialVersionUID = 3L; + /** * The protocol version this library implements. A value of 31800 means 0.3.18.00. */ public static final int PROTOCOL_VERSION = 31800; - private static final long serialVersionUID = 2579833727976661964L; - // TODO: Seed nodes and checkpoint values should be here as well. /** @@ -56,8 +56,10 @@ public class NetworkParameters implements Serializable { public int port; /** The header bytes that identify the start of a packet on this network. */ public long packetMagic; - /** First byte of a base58 encoded address. */ - public byte addressHeader; + /** First byte of a base58 encoded address. See {@link Address}*/ + public int addressHeader; + /** First byte of a base58 encoded dumped private key. See {@link DumpedPrivateKey}. */ + public int dumpedPrivateKeyHeader; /** How many blocks pass between difficulty adjustment periods. BitCoin standardises this to be 2015. */ public int interval; /** @@ -101,6 +103,7 @@ public class NetworkParameters implements Serializable { n.packetMagic = 0xfabfb5daL; n.port = 18333; n.addressHeader = 111; + n.dumpedPrivateKeyHeader = 239; n.interval = INTERVAL; n.targetTimespan = TARGET_TIMESPAN; n.genesisBlock = createGenesis(n); @@ -125,6 +128,7 @@ public class NetworkParameters implements Serializable { n.port = 8333; n.packetMagic = 0xf9beb4d9L; n.addressHeader = 0; + n.dumpedPrivateKeyHeader = 128; n.interval = INTERVAL; n.targetTimespan = TARGET_TIMESPAN; n.genesisBlock = createGenesis(n); diff --git a/src/com/google/bitcoin/core/VersionedChecksummedBytes.java b/src/com/google/bitcoin/core/VersionedChecksummedBytes.java new file mode 100644 index 00000000..84764cd9 --- /dev/null +++ b/src/com/google/bitcoin/core/VersionedChecksummedBytes.java @@ -0,0 +1,78 @@ +/** + * Copyright 2011 Google Inc. + * + * 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 com.google.bitcoin.core; + +import java.util.Arrays; + +/** + *

In Bitcoin the following format is often used to represent some type of key:

+ * + *
[one version byte] [data bytes] [4 checksum bytes]
+ * + *

and the result is then Base58 encoded. This format is used for addresses, and private keys exported using the + * dumpprivkey command.

+ */ +public class VersionedChecksummedBytes { + protected int version; + protected byte[] bytes; + + protected VersionedChecksummedBytes(String encoded) throws AddressFormatException { + byte[] tmp = Base58.decodeChecked(encoded); + version = tmp[0] & 0xFF; + bytes = new byte[tmp.length - 1]; + System.arraycopy(tmp, 1, bytes, 0, tmp.length - 1); + } + + protected VersionedChecksummedBytes(int version, byte[] bytes) { + assert version < 256 && version >= 0; + this.version = version; + this.bytes = bytes; + } + + @Override + public String toString() { + // A stringified address is: + // 1 byte version + 20 bytes hash + 4 bytes check code (itself a truncated hash) + byte[] addressBytes = new byte[1 + 20 + 4]; + addressBytes[0] = (byte)version; + System.arraycopy(bytes, 0, addressBytes, 1, 20); + byte[] check = Utils.doubleDigest(addressBytes, 0, 21); + System.arraycopy(check, 0, addressBytes, 21, 4); + return Base58.encode(addressBytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VersionedChecksummedBytes)) return false; + VersionedChecksummedBytes vcb = (VersionedChecksummedBytes) o; + return Arrays.equals(vcb.bytes, bytes); + } + + /** + * Returns the "version" or "header" byte: the first byte of the data. This is used to disambiguate what the + * contents apply to, for example, which network the key or address is valid on. + * @return A positive number between 0 and 255. + */ + public int getVersion() { + return version; + } +} diff --git a/src/com/google/bitcoin/examples/PrivateKeys.java b/src/com/google/bitcoin/examples/PrivateKeys.java index 19290e35..d4aa4196 100644 --- a/src/com/google/bitcoin/examples/PrivateKeys.java +++ b/src/com/google/bitcoin/examples/PrivateKeys.java @@ -32,11 +32,19 @@ import java.net.InetAddress; */ public class PrivateKeys { public static void main(String[] args) throws Exception { + // TODO: Assumes production network not testnet. Make it selectable. NetworkParameters params = NetworkParameters.prodNet(); try { - // Decode the private key from Satoshis Base58 variant. - BigInteger privKey = Base58.decodeToBigInteger(args[0]); - ECKey key = new ECKey(privKey); + // Decode the private key from Satoshis Base58 variant. If 51 characters long then it's from Bitcoins + // dumpprivkey command and includes a version byte and checksum. Otherwise assume it's a raw key. + ECKey key; + if (args[0].length() == 51) { + DumpedPrivateKey dumpedPrivateKey = new DumpedPrivateKey(params, args[0]); + key = dumpedPrivateKey.getKey(); + } else { + BigInteger privKey = Base58.decodeToBigInteger(args[0]); + key = new ECKey(privKey); + } System.out.println("Address from private key is: " + key.toAddress(params).toString()); // And the address ... Address destination = new Address(params, args[1]); diff --git a/tests/com/google/bitcoin/core/AddressTest.java b/tests/com/google/bitcoin/core/AddressTest.java index ec8bcfe2..475a870e 100644 --- a/tests/com/google/bitcoin/core/AddressTest.java +++ b/tests/com/google/bitcoin/core/AddressTest.java @@ -25,7 +25,8 @@ public class AddressTest { static final NetworkParameters testParams = NetworkParameters.testNet(); static final NetworkParameters prodParams = NetworkParameters.prodNet(); - @Test public void testStringification() throws Exception { + @Test + public void testStringification() throws Exception { // Test a testnet address. Address a = new Address(testParams, Hex.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc")); assertEquals("n4eA2nbYqErp7H6jebchxAN59DmNpksexv", a.toString()); @@ -34,7 +35,8 @@ public class AddressTest { assertEquals("17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL", b.toString()); } - @Test public void testDecoding() throws Exception { + @Test + public void testDecoding() throws Exception { Address a = new Address(testParams, "n4eA2nbYqErp7H6jebchxAN59DmNpksexv"); assertEquals("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc", Utils.bytesToHexString(a.getHash160()));