3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-12 10:15:52 +00:00

VersionedChecksummedBytes: Store network params rather than version, in preparation for native segwit addresses.

This commit is contained in:
Andreas Schildbach 2018-02-15 17:47:09 +01:00
parent f25840309b
commit 57f25d4c22
6 changed files with 171 additions and 191 deletions

View File

@ -18,18 +18,17 @@
package org.bitcoinj.core;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Arrays;
import javax.annotation.Nullable;
import org.bitcoinj.params.Networks;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptPattern;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Objects;
/**
* <p>A Bitcoin address looks like 1MsScoe2fTJoq4ZPdQgqyhgWeoNamYPevy and is derived from an elliptic curve public key
@ -48,7 +47,8 @@ public class Address extends VersionedChecksummedBytes {
*/
public static final int LENGTH = 20;
private transient NetworkParameters params;
/** True if P2SH, false if P2PKH. */
public final boolean p2sh;
/**
* Private constructor. Use {@link #fromBase58(NetworkParameters, String)},
@ -62,13 +62,10 @@ public class Address extends VersionedChecksummedBytes {
* @param hash160
* 20-byte hash of pubkey or script
*/
private Address(NetworkParameters params, int version, byte[] hash160) throws WrongNetworkException {
super(version, hash160);
checkNotNull(params);
private Address(NetworkParameters params, boolean p2sh, byte[] hash160) throws WrongNetworkException {
super(params, hash160);
checkArgument(hash160.length == 20, "Addresses are 160-bit hashes, so you must provide 20 bytes");
if (!isAcceptableVersion(params, version))
throw new WrongNetworkException(version);
this.params = params;
this.p2sh = p2sh;
}
/**
@ -82,7 +79,7 @@ public class Address extends VersionedChecksummedBytes {
* @return constructed address
*/
public static Address fromPubKeyHash(NetworkParameters params, byte[] hash160) {
return new Address(params, params.getAddressHeader(), hash160);
return new Address(params, false, hash160);
}
/**
@ -110,7 +107,7 @@ public class Address extends VersionedChecksummedBytes {
*/
public static Address fromP2SHHash(NetworkParameters params, byte[] hash160) {
try {
return new Address(params, params.getP2SHHeader(), hash160);
return new Address(params, true, hash160);
} catch (WrongNetworkException e) {
throw new RuntimeException(e); // Cannot happen.
}
@ -144,37 +141,35 @@ public class Address extends VersionedChecksummedBytes {
* if the given address is valid but for a different chain (eg testnet vs mainnet)
*/
public static Address fromBase58(@Nullable NetworkParameters params, String base58) throws AddressFormatException {
return new Address(params, base58);
byte[] versionAndDataBytes = Base58.decodeChecked(base58);
int version = versionAndDataBytes[0] & 0xFF;
byte[] bytes = Arrays.copyOfRange(versionAndDataBytes, 1, versionAndDataBytes.length);
if (params == null) {
for (NetworkParameters p : Networks.get()) {
if (version == p.getAddressHeader())
return new Address(p, false, bytes);
else if (version == p.getP2SHHeader())
return new Address(p, true, bytes);
}
throw new AddressFormatException("No network found for " + base58);
} else {
if (version == params.getAddressHeader())
return new Address(params, false, bytes);
else if (version == params.getP2SHHeader())
return new Address(params, true, bytes);
throw new WrongNetworkException(version);
}
}
/** @deprecated use {@link #fromPubKeyHash(NetworkParameters, byte[])} */
@Deprecated
public Address(NetworkParameters params, byte[] hash160) {
this(params, params.getAddressHeader(), hash160);
this(params, false, hash160);
}
/** @deprecated Use {@link #fromBase58(NetworkParameters, String)} */
@Deprecated
public Address(@Nullable NetworkParameters params, String address) throws AddressFormatException {
super(address);
if (params != null) {
if (!isAcceptableVersion(params, version)) {
throw new WrongNetworkException(version);
}
this.params = params;
} else {
NetworkParameters paramsFound = null;
for (NetworkParameters p : Networks.get()) {
if (isAcceptableVersion(p, version)) {
paramsFound = p;
break;
}
}
if (paramsFound == null)
throw new AddressFormatException("No network found for " + address);
this.params = paramsFound;
}
@Override
protected int getVersion() {
return p2sh ? params.getP2SHHeader() : params.getAddressHeader();
}
/** The (big endian) 20 byte hash that is the core of a Bitcoin address. */
@ -187,20 +182,7 @@ public class Address extends VersionedChecksummedBytes {
* See also https://github.com/bitcoin/bips/blob/master/bip-0013.mediawiki: Address Format for pay-to-script-hash
*/
public boolean isP2SHAddress() {
final NetworkParameters parameters = getParameters();
return parameters != null && this.version == parameters.p2shHeader;
}
/**
* Examines the version byte of the address and attempts to find a matching NetworkParameters. If you aren't sure
* which network the address is intended for (eg, it was provided by a user), you can use this to decide if it is
* compatible with the current wallet. You should be able to handle a null response from this method. Note that the
* parameters returned is not necessarily the same as the one the Address was created with.
*
* @return network the address is valid for
*/
public NetworkParameters getParameters() {
return params;
return p2sh;
}
/**
@ -219,31 +201,23 @@ public class Address extends VersionedChecksummedBytes {
}
}
/**
* Check if a given address version is valid given the NetworkParameters.
*/
private static boolean isAcceptableVersion(NetworkParameters params, int version) {
if (version == params.getAddressHeader())
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (version == params.getP2SHHeader())
return true;
return false;
if (o == null || getClass() != o.getClass())
return false;
Address other = (Address) o;
return super.equals(other) && this.p2sh == other.p2sh;
}
@Override
public int hashCode() {
return Objects.hashCode(super.hashCode(), p2sh);
}
@Override
public Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
// Java serialization
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeUTF(params.id);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
params = NetworkParameters.fromID(in.readUTF());
}
}

View File

@ -17,13 +17,14 @@
package org.bitcoinj.core;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import java.util.Arrays;
import javax.annotation.Nullable;
import org.bitcoinj.params.Networks;
/**
* 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. If there are 33 private key bytes instead of 32, then
@ -42,13 +43,37 @@ public class DumpedPrivateKey extends VersionedChecksummedBytes {
* @throws WrongNetworkException
* if the given private key is valid but for a different chain (eg testnet vs mainnet)
*/
public static DumpedPrivateKey fromBase58(@Nullable NetworkParameters params,String base58) throws AddressFormatException {
return new DumpedPrivateKey(params, base58);
public static DumpedPrivateKey fromBase58(@Nullable NetworkParameters params, String base58)
throws AddressFormatException {
byte[] versionAndDataBytes = Base58.decodeChecked(base58);
int version = versionAndDataBytes[0] & 0xFF;
byte[] bytes = Arrays.copyOfRange(versionAndDataBytes, 1, versionAndDataBytes.length);
if (params == null) {
for (NetworkParameters p : Networks.get())
if (version == p.getDumpedPrivateKeyHeader())
return new DumpedPrivateKey(p, bytes);
throw new AddressFormatException("No network found for " + base58);
} else {
if (version == params.getDumpedPrivateKeyHeader())
return new DumpedPrivateKey(params, bytes);
throw new WrongNetworkException(version);
}
}
private DumpedPrivateKey(NetworkParameters params, byte[] bytes) {
super(params, bytes);
if (bytes.length != 32 && bytes.length != 33)
throw new AddressFormatException("Wrong number of bytes for a private key, not 32 or 33");
}
// Used by ECKey.getPrivateKeyEncoded()
DumpedPrivateKey(NetworkParameters params, byte[] keyBytes, boolean compressed) {
super(params.getDumpedPrivateKeyHeader(), encode(keyBytes, compressed));
this(params, encode(keyBytes, compressed));
}
@Override
protected int getVersion() {
return params.getDumpedPrivateKeyHeader();
}
private static byte[] encode(byte[] keyBytes, boolean compressed) {
@ -64,17 +89,6 @@ public class DumpedPrivateKey extends VersionedChecksummedBytes {
}
}
/** @deprecated Use {@link #fromBase58(NetworkParameters, String)} */
@Deprecated
public DumpedPrivateKey(@Nullable NetworkParameters params, String encoded) throws AddressFormatException {
super(encoded);
if (params != null && version != params.getDumpedPrivateKeyHeader())
throw new WrongNetworkException(version);
if (bytes.length != 32 && bytes.length != 33) {
throw new AddressFormatException("Wrong number of bytes for a private key, not 32 or 33");
}
}
/**
* Returns an ECKey created from this encoded private key.
*/
@ -88,17 +102,4 @@ public class DumpedPrivateKey extends VersionedChecksummedBytes {
public boolean isPubKeyCompressed() {
return bytes.length == 33 && bytes[32] == 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DumpedPrivateKey other = (DumpedPrivateKey) o;
return version == other.version && Arrays.equals(bytes, other.bytes);
}
@Override
public int hashCode() {
return Objects.hashCode(version, Arrays.hashCode(bytes));
}
}

View File

@ -1,5 +1,6 @@
/*
* Copyright 2011 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.
@ -16,39 +17,44 @@
package org.bitcoinj.core;
import static com.google.common.base.Preconditions.checkArgument;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import com.google.common.base.Objects;
import com.google.common.primitives.Ints;
import com.google.common.primitives.UnsignedBytes;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* <p>In Bitcoin the following format is often used to represent some type of key:</p>
* <p/>
* <pre>[one version byte] [data bytes] [4 checksum bytes]</pre>
* <p/>
* <p>and the result is then Base58 encoded. This format is used for addresses, and private keys exported using the
* dumpprivkey command.</p>
* <p>
* The following format is often used to represent some type of data (e.g. key or hash of key):
* </p>
*
* <pre>
* [prefix] [data bytes] [checksum]
* </pre>
* <p>
* and the result is then encoded with some variant of base. This format is most commonly used for addresses and private
* keys exported using Bitcoin Core's dumpprivkey command.
* </p>
*/
public class VersionedChecksummedBytes implements Serializable, Cloneable, Comparable<VersionedChecksummedBytes> {
protected final int version;
protected byte[] bytes;
public abstract class VersionedChecksummedBytes implements Serializable, Cloneable, Comparable<VersionedChecksummedBytes> {
protected final transient NetworkParameters params;
protected final byte[] bytes;
protected VersionedChecksummedBytes(String encoded) throws AddressFormatException {
byte[] versionAndDataBytes = Base58.decodeChecked(encoded);
byte versionByte = versionAndDataBytes[0];
version = versionByte & 0xFF;
bytes = new byte[versionAndDataBytes.length - 1];
System.arraycopy(versionAndDataBytes, 1, bytes, 0, versionAndDataBytes.length - 1);
protected VersionedChecksummedBytes(NetworkParameters params, byte[] bytes) {
this.params = checkNotNull(params);
this.bytes = checkNotNull(bytes);
}
protected VersionedChecksummedBytes(int version, byte[] bytes) {
checkArgument(version >= 0 && version < 256);
this.version = version;
this.bytes = bytes;
/**
* @return network this data is valid for
*/
public final NetworkParameters getParameters() {
return params;
}
/**
@ -56,9 +62,11 @@ public class VersionedChecksummedBytes implements Serializable, Cloneable, Compa
* object, including version and checksum bytes.
*/
public final String toBase58() {
return Base58.encodeChecked(version, bytes);
return Base58.encodeChecked(getVersion(), bytes);
}
protected abstract int getVersion();
@Override
public String toString() {
return toBase58();
@ -66,7 +74,7 @@ public class VersionedChecksummedBytes implements Serializable, Cloneable, Compa
@Override
public int hashCode() {
return Objects.hashCode(version, Arrays.hashCode(bytes));
return Objects.hashCode(params, Arrays.hashCode(bytes));
}
@Override
@ -74,12 +82,10 @@ public class VersionedChecksummedBytes implements Serializable, Cloneable, Compa
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VersionedChecksummedBytes other = (VersionedChecksummedBytes) o;
return this.version == other.version && Arrays.equals(this.bytes, other.bytes);
return this.params.equals(other.params) && Arrays.equals(this.bytes, other.bytes);
}
/**
* {@inheritDoc}
*
* This implementation narrows the return type to <code>VersionedChecksummedBytes</code>
* and allows subclasses to throw <code>CloneNotSupportedException</code> even though it
* is never thrown by this implementation.
@ -90,23 +96,30 @@ public class VersionedChecksummedBytes implements Serializable, Cloneable, Compa
}
/**
* {@inheritDoc}
*
* This implementation uses an optimized Google Guava method to compare <code>bytes</code>.
*/
@Override
public int compareTo(VersionedChecksummedBytes o) {
int result = Ints.compare(this.version, o.version);
int result = this.params.getId().compareTo(o.params.getId());
return result != 0 ? result : UnsignedBytes.lexicographicalComparator().compare(this.bytes, o.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;
// Java serialization
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeUTF(params.getId());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
try {
Field paramsField = VersionedChecksummedBytes.class.getDeclaredField("params");
paramsField.setAccessible(true);
paramsField.set(this, checkNotNull(NetworkParameters.fromID(in.readUTF())));
paramsField.setAccessible(false);
} catch (NoSuchFieldException | IllegalAccessException x) {
throw new RuntimeException(x);
}
}
}

View File

@ -18,22 +18,17 @@ package org.bitcoinj.crypto;
import org.bitcoinj.core.*;
import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.primitives.Bytes;
import com.lambdaworks.crypto.SCrypt;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.text.Normalizer;
import java.util.Arrays;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
@ -41,8 +36,6 @@ import static com.google.common.base.Preconditions.checkState;
* passphrase-protected private keys. Currently, only decryption is supported.
*/
public class BIP38PrivateKey extends VersionedChecksummedBytes {
public transient NetworkParameters params;
public final boolean ecMultiply;
public final boolean compressed;
public final boolean hasLotAndSequence;
@ -62,20 +55,16 @@ public class BIP38PrivateKey extends VersionedChecksummedBytes {
* if the given base58 doesn't parse or the checksum is invalid
*/
public static BIP38PrivateKey fromBase58(NetworkParameters params, String base58) throws AddressFormatException {
return new BIP38PrivateKey(params, base58);
}
byte[] versionAndDataBytes = Base58.decodeChecked(base58);
int version = versionAndDataBytes[0] & 0xFF;
byte[] bytes = Arrays.copyOfRange(versionAndDataBytes, 1, versionAndDataBytes.length);
/** @deprecated Use {@link #fromBase58(NetworkParameters, String)} */
@Deprecated
public BIP38PrivateKey(NetworkParameters params, String encoded) throws AddressFormatException {
super(encoded);
this.params = checkNotNull(params);
if (version != 0x01)
throw new AddressFormatException("Mismatched version number: " + version);
if (bytes.length != 38)
throw new AddressFormatException("Wrong number of bytes, excluding version byte: " + bytes.length);
hasLotAndSequence = (bytes[1] & 0x04) != 0; // bit 2
compressed = (bytes[1] & 0x20) != 0; // bit 5
boolean hasLotAndSequence = (bytes[1] & 0x04) != 0; // bit 2
boolean compressed = (bytes[1] & 0x20) != 0; // bit 5
if ((bytes[1] & 0x01) != 0) // bit 0
throw new AddressFormatException("Bit 0x01 reserved for future use.");
if ((bytes[1] & 0x02) != 0) // bit 1
@ -85,6 +74,7 @@ public class BIP38PrivateKey extends VersionedChecksummedBytes {
if ((bytes[1] & 0x10) != 0) // bit 4
throw new AddressFormatException("Bit 0x10 reserved for future use.");
final int byte0 = bytes[0] & 0xff;
final boolean ecMultiply;
if (byte0 == 0x42) {
// Non-EC-multiplied key
if ((bytes[1] & 0xc0) != 0xc0) // bits 6+7
@ -100,8 +90,24 @@ public class BIP38PrivateKey extends VersionedChecksummedBytes {
} else {
throw new AddressFormatException("Second byte must by 0x42 or 0x43.");
}
addressHash = Arrays.copyOfRange(bytes, 2, 6);
content = Arrays.copyOfRange(bytes, 6, 38);
byte[] addressHash = Arrays.copyOfRange(bytes, 2, 6);
byte[] content = Arrays.copyOfRange(bytes, 6, 38);
return new BIP38PrivateKey(params, bytes, ecMultiply, compressed, hasLotAndSequence, addressHash, content);
}
private BIP38PrivateKey(NetworkParameters params, byte[] bytes, boolean ecMultiply, boolean compressed,
boolean hasLotAndSequence, byte[] addressHash, byte[] content) throws AddressFormatException {
super(params, bytes);
this.ecMultiply = ecMultiply;
this.compressed = compressed;
this.hasLotAndSequence = hasLotAndSequence;
this.addressHash = addressHash;
this.content = content;
}
@Override
protected int getVersion() {
return 1;
}
public ECKey decrypt(String passphrase) throws BadPassphraseException {
@ -180,29 +186,4 @@ public class BIP38PrivateKey extends VersionedChecksummedBytes {
throw new RuntimeException(x);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BIP38PrivateKey other = (BIP38PrivateKey) o;
return super.equals(other) && Objects.equal(this.params, other.params);
}
@Override
public int hashCode() {
return Objects.hashCode(super.hashCode(), params);
}
// Java serialization
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeUTF(params.getId());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
params = checkNotNull(NetworkParameters.fromID(in.readUTF()));
}
}

View File

@ -149,11 +149,11 @@ public class AddressTest {
@Test
public void p2shAddress() throws Exception {
// Test that we can construct P2SH addresses
Address mainNetP2SHAddress = Address.fromBase58(MAINNET, "35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU");
assertEquals(mainNetP2SHAddress.version, MAINNET.p2shHeader);
Address mainNetP2SHAddress = Address.fromBase58(MainNetParams.get(), "35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU");
assertEquals(mainNetP2SHAddress.getVersion(), MAINNET.p2shHeader);
assertTrue(mainNetP2SHAddress.isP2SHAddress());
Address testNetP2SHAddress = Address.fromBase58(TESTNET, "2MuVSxtfivPKJe93EC1Tb9UhJtGhsoWEHCe");
assertEquals(testNetP2SHAddress.version, TESTNET.p2shHeader);
Address testNetP2SHAddress = Address.fromBase58(TestNet3Params.get(), "2MuVSxtfivPKJe93EC1Tb9UhJtGhsoWEHCe");
assertEquals(testNetP2SHAddress.getVersion(), TESTNET.p2shHeader);
assertTrue(testNetP2SHAddress.isP2SHAddress());
// Test that we can determine what network a P2SH address belongs to

View File

@ -29,19 +29,30 @@ public class VersionedChecksummedBytesTest {
private static final NetworkParameters TESTNET = TestNet3Params.get();
private static final NetworkParameters MAINNET = MainNetParams.get();
private static class VersionedChecksummedBytesToTest extends VersionedChecksummedBytes {
public VersionedChecksummedBytesToTest(NetworkParameters params, byte[] bytes) {
super(params, bytes);
}
@Override
protected int getVersion() {
return params.getAddressHeader();
}
}
@Test
public void stringification() throws Exception {
// Test a testnet address.
VersionedChecksummedBytes a = new VersionedChecksummedBytes(TESTNET.getAddressHeader(), HEX.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc"));
VersionedChecksummedBytes a = new VersionedChecksummedBytesToTest(TESTNET, HEX.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc"));
assertEquals("n4eA2nbYqErp7H6jebchxAN59DmNpksexv", a.toString());
VersionedChecksummedBytes b = new VersionedChecksummedBytes(MAINNET.getAddressHeader(), HEX.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));
VersionedChecksummedBytes b = new VersionedChecksummedBytesToTest(MAINNET, HEX.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));
assertEquals("17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL", b.toString());
}
@Test
public void cloning() throws Exception {
VersionedChecksummedBytes a = new VersionedChecksummedBytes(TESTNET.getAddressHeader(), HEX.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc"));
VersionedChecksummedBytes a = new VersionedChecksummedBytesToTest(TESTNET, HEX.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc"));
VersionedChecksummedBytes b = a.clone();
assertEquals(a, b);
@ -50,7 +61,7 @@ public class VersionedChecksummedBytesTest {
@Test
public void comparisonCloneEqualTo() throws Exception {
VersionedChecksummedBytes a = new VersionedChecksummedBytes(TESTNET.getAddressHeader(), HEX.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc"));
VersionedChecksummedBytes a = new VersionedChecksummedBytesToTest(TESTNET, HEX.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc"));
VersionedChecksummedBytes b = a.clone();
assertTrue(a.compareTo(b) == 0);