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.

This commit is contained in:
Mike Hearn 2011-05-24 21:42:08 +00:00
parent ab7351ff78
commit 06c84c2c23
7 changed files with 177 additions and 70 deletions

View File

@ -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:<p>
*
* <pre>new Address(NetworkParameters.prodNet(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));</pre>
*/
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 {
* <pre>new Address(NetworkParameters.prodNet(), "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");</pre>
*/
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;
}
}

View File

@ -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 {

View File

@ -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));
}
}

View File

@ -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);

View File

@ -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;
/**
* <p>In Bitcoin the following format is often used to represent some type of key:</p>
*
* <pre>[one version byte] [data bytes] [4 checksum bytes]</pre>
*
* <p>and the result is then Base58 encoded. This format is used for addresses, and private keys exported using the
* dumpprivkey command.</p>
*/
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;
}
}

View File

@ -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]);

View File

@ -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()));