mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-01-19 05:33:44 +01:00
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:
parent
ab7351ff78
commit
06c84c2c23
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
58
src/com/google/bitcoin/core/DumpedPrivateKey.java
Normal file
58
src/com/google/bitcoin/core/DumpedPrivateKey.java
Normal 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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
78
src/com/google/bitcoin/core/VersionedChecksummedBytes.java
Normal file
78
src/com/google/bitcoin/core/VersionedChecksummedBytes.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
// 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]);
|
||||
ECKey key = new ECKey(privKey);
|
||||
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]);
|
||||
|
@ -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()));
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user