3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-01 07:42:17 +00:00

HD wallets alpha preview

This commit is contained in:
Mike Hearn 2014-03-26 20:05:33 +01:00
parent 780be05260
commit 5638387d3a
71 changed files with 6582 additions and 1601 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ target
.settings .settings
.idea .idea
*.iml *.iml
*.chain
*.spvchain
*.wallet

View File

@ -17,6 +17,7 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import com.google.bitcoin.script.Script; import com.google.bitcoin.script.Script;
import com.google.bitcoin.wallet.AbstractKeyChainEventListener;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.List; import java.util.List;
@ -24,7 +25,7 @@ import java.util.List;
/** /**
* Convenience implementation of {@link WalletEventListener}. * Convenience implementation of {@link WalletEventListener}.
*/ */
public abstract class AbstractWalletEventListener implements WalletEventListener { public abstract class AbstractWalletEventListener extends AbstractKeyChainEventListener implements WalletEventListener {
@Override @Override
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
onChange(); onChange();
@ -46,7 +47,7 @@ public abstract class AbstractWalletEventListener implements WalletEventListener
} }
@Override @Override
public void onKeysAdded(Wallet wallet, List<ECKey> keys) { public void onKeysAdded(List<ECKey> keys) {
onChange(); onChange();
} }

View File

@ -964,7 +964,7 @@ public class Block extends Message {
// counter in the scriptSig so every transaction has a different hash. // counter in the scriptSig so every transaction has a different hash.
coinbase.addInput(new TransactionInput(params, coinbase, new byte[]{(byte) txCounter, (byte) (txCounter++ >> 8)})); coinbase.addInput(new TransactionInput(params, coinbase, new byte[]{(byte) txCounter, (byte) (txCounter++ >> 8)}));
coinbase.addOutput(new TransactionOutput(params, coinbase, value, coinbase.addOutput(new TransactionOutput(params, coinbase, value,
ScriptBuilder.createOutputScript(new ECKey(null, pubKeyTo)).getProgram())); ScriptBuilder.createOutputScript(ECKey.fromPublicOnly(pubKeyTo)).getProgram()));
transactions.add(coinbase); transactions.add(coinbase);
coinbase.setParent(this); coinbase.setParent(this);
coinbase.length = coinbase.bitcoinSerialize().length; coinbase.length = coinbase.bitcoinSerialize().length;
@ -973,12 +973,15 @@ public class Block extends Message {
static final byte[] EMPTY_BYTES = new byte[32]; static final byte[] EMPTY_BYTES = new byte[32];
// It's pretty weak to have this around at runtime: fix later.
private static final byte[] pubkeyForTesting = new ECKey().getPubKey();
/** /**
* Returns a solved block that builds on top of this one. This exists for unit tests. * Returns a solved block that builds on top of this one. This exists for unit tests.
*/ */
@VisibleForTesting @VisibleForTesting
public Block createNextBlock(Address to, long time) { public Block createNextBlock(Address to, long time) {
return createNextBlock(to, null, time, EMPTY_BYTES, Utils.toNanoCoins(50, 0)); return createNextBlock(to, null, time, pubkeyForTesting, Utils.toNanoCoins(50, 0));
} }
/** /**
@ -1029,12 +1032,12 @@ public class Block extends Message {
@VisibleForTesting @VisibleForTesting
public Block createNextBlock(@Nullable Address to, TransactionOutPoint prevOut) { public Block createNextBlock(@Nullable Address to, TransactionOutPoint prevOut) {
return createNextBlock(to, prevOut, Utils.currentTimeSeconds(), EMPTY_BYTES, Utils.toNanoCoins(50, 0)); return createNextBlock(to, prevOut, Utils.currentTimeSeconds(), pubkeyForTesting, Utils.toNanoCoins(50, 0));
} }
@VisibleForTesting @VisibleForTesting
public Block createNextBlock(@Nullable Address to, BigInteger value) { public Block createNextBlock(@Nullable Address to, BigInteger value) {
return createNextBlock(to, null, Utils.currentTimeSeconds(), EMPTY_BYTES, value); return createNextBlock(to, null, Utils.currentTimeSeconds(), pubkeyForTesting, value);
} }
@VisibleForTesting @VisibleForTesting

View File

@ -19,7 +19,6 @@ package com.google.bitcoin.core;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
/** /**
@ -75,7 +74,8 @@ public class DumpedPrivateKey extends VersionedChecksummedBytes {
* Returns an ECKey created from this encoded private key. * Returns an ECKey created from this encoded private key.
*/ */
public ECKey getKey() { public ECKey getKey() {
return new ECKey(new BigInteger(1, bytes), null, compressed); final ECKey key = ECKey.fromPrivate(bytes);
return compressed ? key : key.decompress();
} }
@Override @Override

View File

@ -16,13 +16,11 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import com.google.bitcoin.crypto.EncryptedPrivateKey; import com.google.bitcoin.crypto.*;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.TransactionSignature;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.bitcoin.NativeSecp256k1; import org.bitcoin.NativeSecp256k1;
import org.bitcoinj.wallet.Protos;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.asn1.*; import org.spongycastle.asn1.*;
@ -51,26 +49,38 @@ import java.security.SignatureException;
import java.util.Arrays; import java.util.Arrays;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
// TODO: This class is quite a mess by now. Once users are migrated away from Java serialization for the wallets,
// refactor this to have better internal layout and a more consistent API.
/** /**
* <p>Represents an elliptic curve public and (optionally) private key, usable for digital signatures but not encryption. * <p>Represents an elliptic curve public and (optionally) private key, usable for digital signatures but not encryption.
* Creating a new ECKey with the empty constructor will generate a new random keypair. Other constructors can be used * Creating a new ECKey with the empty constructor will generate a new random keypair. Other static methods can be used
* when you already have the public or private parts. If you create a key with only the public part, you can check * when you already have the public or private parts. If you create a key with only the public part, you can check
* signatures but not create them.</p> * signatures but not create them.</p>
* *
* <p>ECKey also provides access to Bitcoin-Qt compatible text message signing, as accessible via the UI or JSON-RPC. * <p>ECKey also provides access to Bitcoin Core compatible text message signing, as accessible via the UI or JSON-RPC.
* This is slightly different to signing raw bytes - if you want to sign your own data and it won't be exposed as * This is slightly different to signing raw bytes - if you want to sign your own data and it won't be exposed as
* text to people, you don't want to use this. If in doubt, ask on the mailing list.</p> * text to people, you don't want to use this. If in doubt, ask on the mailing list.</p>
* *
* <p>The ECDSA algorithm supports <i>key recovery</i> in which a signature plus a couple of discriminator bits can * <p>The ECDSA algorithm supports <i>key recovery</i> in which a signature plus a couple of discriminator bits can
* be reversed to find the public key used to calculate it. This can be convenient when you have a message and a * be reversed to find the public key used to calculate it. This can be convenient when you have a message and a
* signature and want to find out who signed it, rather than requiring the user to provide the expected identity.</p> * signature and want to find out who signed it, rather than requiring the user to provide the expected identity.</p>
*
* <p>This class supports a variety of serialization forms. The methods that accept/return byte arrays serialize
* private keys as raw byte arrays and public keys using the SEC standard byte encoding for public keys. Signatures
* are encoded using ASN.1/DER inside the Bitcoin protocol.</p>
*
* <p>A key can be <i>compressed</i> or <i>uncompressed</i>. This refers to whether the public key is represented
* when encoded into bytes as an (x, y) coordinate on the elliptic curve, or whether it's represented as just an X
* co-ordinate and an extra byte that carries a sign bit. With the latter form the Y coordinate can be calculated
* dynamically, however, <b>because the binary serialization is different the address of a key changes if its
* compression status is changed</b>. If you deviate from the defaults it's important to understand this: money sent
* to a compressed version of the key will have a different address to the same key in uncompressed form. Whether
* a public key is compressed or not is recorded in the SEC binary serialisation format, and preserved in a flag in
* this class so round-tripping preserves state. Unless you're working with old software or doing unusual things, you
* can usually ignore the compressed/uncompressed distinction.</p>
*/ */
public class ECKey implements Serializable { public class ECKey implements EncryptableItem, Serializable {
private static final Logger log = LoggerFactory.getLogger(ECKey.class); private static final Logger log = LoggerFactory.getLogger(ECKey.class);
/** The parameters of the secp256k1 curve that Bitcoin uses. */ /** The parameters of the secp256k1 curve that Bitcoin uses. */
@ -95,31 +105,32 @@ public class ECKey implements Serializable {
// The two parts of the key. If "priv" is set, "pub" can always be calculated. If "pub" is set but not "priv", we // The two parts of the key. If "priv" is set, "pub" can always be calculated. If "pub" is set but not "priv", we
// can only verify signatures not make them. // can only verify signatures not make them.
// TODO: Redesign this class to use consistent internals and more efficient serialization. protected final BigInteger priv; // A field element.
private BigInteger priv; protected final ECPoint pub;
private byte[] pub;
// Creation time of the key in seconds since the epoch, or zero if the key was deserialized from a version that did // Creation time of the key in seconds since the epoch, or zero if the key was deserialized from a version that did
// not have this field. // not have this field.
private long creationTimeSeconds; protected long creationTimeSeconds;
/** protected KeyCrypter keyCrypter;
* Instance of the KeyCrypter interface to use for encrypting and decrypting the key. protected EncryptedData encryptedPrivateKey;
*/
transient private KeyCrypter keyCrypter;
/** // Transient because it's calculated on demand/cached.
* The encrypted private key information. private transient byte[] pubKeyHash;
*/
private EncryptedPrivateKey encryptedPrivateKey;
// Transient because it's calculated on demand.
transient private byte[] pubKeyHash;
/** /**
* Generates an entirely new keypair. Point compression is used so the resulting public key will be 33 bytes * Generates an entirely new keypair. Point compression is used so the resulting public key will be 33 bytes
* (32 for the co-ordinate and 1 byte to represent the y bit). * (32 for the co-ordinate and 1 byte to represent the y bit).
*/ */
public ECKey() { public ECKey() {
this(secureRandom);
}
/**
* Generates an entirely new keypair with the given {@link SecureRandom} object. Point compression is used so the
* resulting public key will be 33 bytes (32 for the co-ordinate and 1 byte to represent the y bit).
*/
public ECKey(SecureRandom secureRandom) {
ECKeyPairGenerator generator = new ECKeyPairGenerator(); ECKeyPairGenerator generator = new ECKeyPairGenerator();
ECKeyGenerationParameters keygenParams = new ECKeyGenerationParameters(CURVE, secureRandom); ECKeyGenerationParameters keygenParams = new ECKeyGenerationParameters(CURVE, secureRandom);
generator.init(keygenParams); generator.init(keygenParams);
@ -127,11 +138,31 @@ public class ECKey implements Serializable {
ECPrivateKeyParameters privParams = (ECPrivateKeyParameters) keypair.getPrivate(); ECPrivateKeyParameters privParams = (ECPrivateKeyParameters) keypair.getPrivate();
ECPublicKeyParameters pubParams = (ECPublicKeyParameters) keypair.getPublic(); ECPublicKeyParameters pubParams = (ECPublicKeyParameters) keypair.getPublic();
priv = privParams.getD(); priv = privParams.getD();
pub = pubParams.getQ().getEncoded(true); pub = CURVE.getCurve().decodePoint(pubParams.getQ().getEncoded(true));
creationTimeSeconds = Utils.currentTimeSeconds(); creationTimeSeconds = Utils.currentTimeSeconds();
} }
protected ECKey(@Nullable BigInteger priv, ECPoint pub) {
this.priv = priv;
this.pub = checkNotNull(pub);
}
/**
* Utility for compressing an elliptic curve point. Returns the same point if it's already compressed.
* See the ECKey class docs for a discussion of point compression.
*/
public static ECPoint.Fp compressPoint(ECPoint uncompressed) {
return new ECPoint.Fp(CURVE.getCurve(), uncompressed.getX(), uncompressed.getY(), true);
}
/**
* Utility for decompressing an elliptic curve point. Returns the same point if it's already compressed.
* See the ECKey class docs for a discussion of point compression.
*/
public static ECPoint.Fp decompressPoint(ECPoint compressed) {
return new ECPoint.Fp(CURVE.getCurve(), compressed.getX(), compressed.getY(), false);
}
/** /**
* Construct an ECKey from an ASN.1 encoded private key. These are produced by OpenSSL and stored by the Bitcoin * Construct an ECKey from an ASN.1 encoded private key. These are produced by OpenSSL and stored by the Bitcoin
* reference implementation in its wallet. Note that this is slow because it requires an EC point multiply. * reference implementation in its wallet. Note that this is slow because it requires an EC point multiply.
@ -140,14 +171,67 @@ public class ECKey implements Serializable {
return extractKeyFromASN1(asn1privkey); return extractKeyFromASN1(asn1privkey);
} }
/** Creates an ECKey given the private key only. The public key is calculated from it (this is slow) */ /**
public ECKey(BigInteger privKey) { * Creates an ECKey given the private key only. The public key is calculated from it (this is slow). Note that
this(privKey, (byte[])null); * the resulting public key is compressed.
*/
public static ECKey fromPrivate(BigInteger privKey) {
return new ECKey(privKey, compressPoint(CURVE.getG().multiply(privKey)));
} }
/** A constructor variant with BigInteger pubkey. See {@link ECKey#ECKey(BigInteger, byte[])}. */ /**
public ECKey(BigInteger privKey, BigInteger pubKey) { * Creates an ECKey given the private key only. The public key is calculated from it (this is slow). The resulting
this(privKey, Utils.bigIntegerToBytes(pubKey, 65)); * public key is compressed.
*/
public static ECKey fromPrivate(byte[] privKeyBytes) {
return fromPrivate(new BigInteger(1, privKeyBytes));
}
/**
* Creates an ECKey that simply trusts the caller to ensure that point is really the result of multiplying the
* generator point by the private key. This is used to speed things up when you know you have the right values
* already. The compression state of pub will be preserved.
*/
public static ECKey fromPrivateAndPrecalculatedPublic(BigInteger priv, ECPoint pub) {
return new ECKey(priv, pub);
}
/**
* Creates an ECKey that simply trusts the caller to ensure that point is really the result of multiplying the
* generator point by the private key. This is used to speed things up when you know you have the right values
* already. The compression state of the point will be preserved.
*/
public static ECKey fromPrivateAndPrecalculatedPublic(byte[] priv, byte[] pub) {
checkNotNull(priv);
checkNotNull(pub);
return new ECKey(new BigInteger(1, priv), CURVE.getCurve().decodePoint(pub));
}
/**
* Creates an ECKey that cannot be used for signing, only verifying signatures, from the given point. The
* compression state of pub will be preserved.
*/
public static ECKey fromPublicOnly(ECPoint pub) {
return new ECKey(null, pub);
}
/**
* Creates an ECKey that cannot be used for signing, only verifying signatures, from the given encoded point.
* The compression state of pub will be preserved.
*/
public static ECKey fromPublicOnly(byte[] pub) {
return new ECKey(null, CURVE.getCurve().decodePoint(pub));
}
/**
* Returns a copy of this key, but with the public point represented in uncompressed form. Normally you would
* never need this: it's for specialised scenarios or when backwards compatibility in encoded form is necessary.
*/
public ECKey decompress() {
if (!pub.isCompressed())
return this;
else
return new ECKey(priv, decompressPoint(pub));
} }
/** /**
@ -155,6 +239,7 @@ public class ECKey implements Serializable {
* is more convenient if you are importing a key from elsewhere. The public key will be automatically derived * is more convenient if you are importing a key from elsewhere. The public key will be automatically derived
* from the private key. * from the private key.
*/ */
@Deprecated
public ECKey(@Nullable byte[] privKeyBytes, @Nullable byte[] pubKey) { public ECKey(@Nullable byte[] privKeyBytes, @Nullable byte[] pubKey) {
this(privKeyBytes == null ? null : new BigInteger(1, privKeyBytes), pubKey); this(privKeyBytes == null ? null : new BigInteger(1, privKeyBytes), pubKey);
} }
@ -166,33 +251,49 @@ public class ECKey implements Serializable {
* @param pubKey The keys public key * @param pubKey The keys public key
* @param keyCrypter The KeyCrypter that will be used, with an AES key, to encrypt and decrypt the private key * @param keyCrypter The KeyCrypter that will be used, with an AES key, to encrypt and decrypt the private key
*/ */
public ECKey(@Nullable EncryptedPrivateKey encryptedPrivateKey, @Nullable byte[] pubKey, KeyCrypter keyCrypter) { @Deprecated
public ECKey(EncryptedData encryptedPrivateKey, byte[] pubKey, KeyCrypter keyCrypter) {
this((byte[])null, pubKey); this((byte[])null, pubKey);
this.keyCrypter = Preconditions.checkNotNull(keyCrypter); this.keyCrypter = checkNotNull(keyCrypter);
this.encryptedPrivateKey = encryptedPrivateKey; this.encryptedPrivateKey = encryptedPrivateKey;
} }
/** /**
* Creates an ECKey given either the private key only, the public key only, or both. If only the private key * Constructs a key that has an encrypted private component. The given object wraps encrypted bytes and an
* initialization vector. Note that the key will not be decrypted during this call: the returned ECKey is
* unusable for signing unless a decryption key is supplied.
*/
public static ECKey fromEncrypted(EncryptedData encryptedPrivateKey, KeyCrypter crypter, byte[] pubKey) {
ECKey key = fromPublicOnly(pubKey);
key.encryptedPrivateKey = checkNotNull(encryptedPrivateKey);
key.keyCrypter = checkNotNull(crypter);
return key;
}
/**
* Creates an ECKey given either the private key only, the public key only, or both. If only the private key
* is supplied, the public key will be calculated from it (this is slow). If both are supplied, it's assumed * is supplied, the public key will be calculated from it (this is slow). If both are supplied, it's assumed
* the public key already correctly matches the public key. If only the public key is supplied, this ECKey cannot * the public key already correctly matches the public key. If only the public key is supplied, this ECKey cannot
* be used for signing. * be used for signing.
* @param compressed If set to true and pubKey is null, the derived public key will be in compressed form. * @param compressed If set to true and pubKey is null, the derived public key will be in compressed form.
*/ */
@Deprecated
public ECKey(@Nullable BigInteger privKey, @Nullable byte[] pubKey, boolean compressed) { public ECKey(@Nullable BigInteger privKey, @Nullable byte[] pubKey, boolean compressed) {
if (privKey == null && pubKey == null) if (privKey == null && pubKey == null)
throw new IllegalArgumentException("ECKey requires at least private or public key"); throw new IllegalArgumentException("ECKey requires at least private or public key");
this.priv = privKey; this.priv = privKey;
this.pub = null;
if (pubKey == null) { if (pubKey == null) {
// Derive public from private. // Derive public from private.
this.pub = publicKeyFromPrivate(privKey, compressed); ECPoint point = CURVE.getG().multiply(privKey);
if (compressed)
point = compressPoint(point);
this.pub = point;
} else { } else {
// We expect the pubkey to be in regular encoded form, just as a BigInteger. Therefore the first byte is // We expect the pubkey to be in regular encoded form, just as a BigInteger. Therefore the first byte is
// a special marker byte. // a special marker byte.
// TODO: This is probably not a useful API and may be confusing. // TODO: This is probably not a useful API and may be confusing.
this.pub = pubKey; this.pub = CURVE.getCurve().decodePoint(pubKey);
} }
} }
@ -202,24 +303,36 @@ public class ECKey implements Serializable {
* the public key already correctly matches the public key. If only the public key is supplied, this ECKey cannot * the public key already correctly matches the public key. If only the public key is supplied, this ECKey cannot
* be used for signing. * be used for signing.
*/ */
@Deprecated
private ECKey(@Nullable BigInteger privKey, @Nullable byte[] pubKey) { private ECKey(@Nullable BigInteger privKey, @Nullable byte[] pubKey) {
this(privKey, pubKey, false); this(privKey, pubKey, false);
} }
/**
* Returns true if this key doesn't have unencrypted access to private key bytes. This may be because it was never
* given any private key bytes to begin with (a watching key), or because the key is encrypted. You can use
* {@link #isEncrypted()} to tell the cases apart.
*/
public boolean isPubKeyOnly() { public boolean isPubKeyOnly() {
return priv == null; return priv == null;
} }
/**
* Returns true if this key has unencrypted access to private key bytes. Does the opposite of
* {@link #isPubKeyOnly()}.
*/
public boolean hasPrivKey() { public boolean hasPrivKey() {
return priv != null; return priv != null;
} }
/** /**
* Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by the BitCoin reference * Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by the Bitcoin reference
* implementation in its wallet storage format. * implementation in its wallet storage format.
* @throws com.google.bitcoin.core.ECKey.MissingPrivateKeyException if the private key is missing or encrypted.
*/ */
public byte[] toASN1() { public byte[] toASN1() {
try { try {
byte[] privKeyBytes = getPrivKeyBytes();
ByteArrayOutputStream baos = new ByteArrayOutputStream(400); ByteArrayOutputStream baos = new ByteArrayOutputStream(400);
// ASN1_SEQUENCE(EC_PRIVATEKEY) = { // ASN1_SEQUENCE(EC_PRIVATEKEY) = {
@ -230,7 +343,7 @@ public class ECKey implements Serializable {
// } ASN1_SEQUENCE_END(EC_PRIVATEKEY) // } ASN1_SEQUENCE_END(EC_PRIVATEKEY)
DERSequenceGenerator seq = new DERSequenceGenerator(baos); DERSequenceGenerator seq = new DERSequenceGenerator(baos);
seq.addObject(new ASN1Integer(1)); // version seq.addObject(new ASN1Integer(1)); // version
seq.addObject(new DEROctetString(priv.toByteArray())); seq.addObject(new DEROctetString(privKeyBytes));
seq.addObject(new DERTaggedObject(0, SECNamedCurves.getByName("secp256k1").toASN1Primitive())); seq.addObject(new DERTaggedObject(0, SECNamedCurves.getByName("secp256k1").toASN1Primitive()));
seq.addObject(new DERTaggedObject(1, new DERBitString(getPubKey()))); seq.addObject(new DERTaggedObject(1, new DERBitString(getPubKey())));
seq.close(); seq.close();
@ -252,7 +365,7 @@ public class ECKey implements Serializable {
/** Gets the hash160 form of the public key (as seen in addresses). */ /** Gets the hash160 form of the public key (as seen in addresses). */
public byte[] getPubKeyHash() { public byte[] getPubKeyHash() {
if (pubKeyHash == null) if (pubKeyHash == null)
pubKeyHash = Utils.sha256hash160(this.pub); pubKeyHash = Utils.sha256hash160(this.pub.getEncoded());
return pubKeyHash; return pubKeyHash;
} }
@ -261,19 +374,36 @@ public class ECKey implements Serializable {
* as the pubKeyHash/address. * as the pubKeyHash/address.
*/ */
public byte[] getPubKey() { public byte[] getPubKey() {
return pub.getEncoded();
}
/** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */
public ECPoint getPubKeyPoint() {
return pub; return pub;
} }
/**
* Gets the private key in the form of an integer field element. The public key is derived by performing EC
* point addition this number of times (i.e. point multiplying).
*
* @throws java.lang.IllegalStateException if the private key bytes are not available.
*/
public BigInteger getPrivKey() {
if (priv == null)
throw new MissingPrivateKeyException();
return priv;
}
/** /**
* Returns whether this key is using the compressed form or not. Compressed pubkeys are only 33 bytes, not 64. * Returns whether this key is using the compressed form or not. Compressed pubkeys are only 33 bytes, not 64.
*/ */
public boolean isCompressed() { public boolean isCompressed() {
return pub.length == 33; return pub.isCompressed();
} }
public String toString() { public String toString() {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
b.append("pub:").append(Utils.bytesToHexString(pub)); b.append("pub:").append(Utils.bytesToHexString(pub.getEncoded()));
if (creationTimeSeconds != 0) { if (creationTimeSeconds != 0) {
b.append(" timestamp:").append(creationTimeSeconds); b.append(" timestamp:").append(creationTimeSeconds);
} }
@ -301,21 +431,7 @@ public class ECKey implements Serializable {
* the RIPEMD-160 hash of the public key and is not the public key itself (which is too large to be convenient). * the RIPEMD-160 hash of the public key and is not the public key itself (which is too large to be convenient).
*/ */
public Address toAddress(NetworkParameters params) { public Address toAddress(NetworkParameters params) {
byte[] hash160 = Utils.sha256hash160(pub); return new Address(params, getPubKeyHash());
return new Address(params, hash160);
}
/**
* Clears all the ECKey private key contents from memory.
* WARNING - this method irreversibly deletes the private key information.
* It turns the ECKEy into a watch only key.
*/
public void clearPrivateKey() {
priv = BigInteger.ZERO;
if (encryptedPrivateKey != null) {
encryptedPrivateKey.clear();
}
} }
/** /**
@ -417,8 +533,8 @@ public class ECKey implements Serializable {
/** /**
* Signs the given hash and returns the R and S components as BigIntegers. In the Bitcoin protocol, they are * Signs the given hash and returns the R and S components as BigIntegers. In the Bitcoin protocol, they are
* usually encoded using DER format, so you want {@link com.google.bitcoin.core.ECKey.ECDSASignature#toASN1()} * usually encoded using ASN.1 format, so you want {@link com.google.bitcoin.core.ECKey.ECDSASignature#toASN1()}
* instead. However sometimes the independent components can be useful, for instance, if you're doing to do * instead. However sometimes the independent components can be useful, for instance, if you're going to do
* further EC maths on them. * further EC maths on them.
* @throws KeyCrypterException if this ECKey doesn't have a private part. * @throws KeyCrypterException if this ECKey doesn't have a private part.
*/ */
@ -444,35 +560,23 @@ public class ECKey implements Serializable {
* @throws KeyCrypterException if this ECKey doesn't have a private part. * @throws KeyCrypterException if this ECKey doesn't have a private part.
*/ */
public ECDSASignature sign(Sha256Hash input, @Nullable KeyParameter aesKey) throws KeyCrypterException { public ECDSASignature sign(Sha256Hash input, @Nullable KeyParameter aesKey) throws KeyCrypterException {
if (FAKE_SIGNATURES) KeyCrypter crypter = getKeyCrypter();
return TransactionSignature.dummy(); if (crypter != null) {
if (aesKey == null)
// The private key bytes to use for signing. throw new KeyIsEncryptedException();
BigInteger privateKeyForSigning; return decrypt(crypter, aesKey).sign(input);
if (isEncrypted()) {
// The private key needs decrypting before use.
if (aesKey == null) {
throw new KeyCrypterException("This ECKey is encrypted but no decryption key has been supplied.");
}
if (keyCrypter == null) {
throw new KeyCrypterException("There is no KeyCrypter to decrypt the private key for signing.");
}
privateKeyForSigning = new BigInteger(1, keyCrypter.decrypt(encryptedPrivateKey, aesKey));
// Check encryption was correct.
if (!Arrays.equals(pub, publicKeyFromPrivate(privateKeyForSigning, isCompressed())))
throw new KeyCrypterException("Could not decrypt bytes");
} else { } else {
// No decryption of private key required. // No decryption of private key required.
if (priv == null) { if (priv == null)
throw new KeyCrypterException("This ECKey does not have the private key necessary for signing."); throw new MissingPrivateKeyException();
} else {
privateKeyForSigning = priv;
}
} }
return doSign(input, priv);
}
protected ECDSASignature doSign(Sha256Hash input, BigInteger privateKeyForSigning) {
if (FAKE_SIGNATURES)
return TransactionSignature.dummy();
checkNotNull(privateKeyForSigning);
ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()));
ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKeyForSigning, CURVE); ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKeyForSigning, CURVE);
signer.init(true, privKey); signer.init(true, privKey);
@ -541,13 +645,6 @@ public class ECKey implements Serializable {
return ECKey.verify(sigHash.getBytes(), signature, getPubKey()); return ECKey.verify(sigHash.getBytes(), signature, getPubKey());
} }
/**
* Returns true if this pubkey is canonical, i.e. the correct length taking into account compression.
*/
public boolean isPubKeyCanonical() {
return isPubKeyCanonical(pub);
}
/** /**
* Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression. * Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression.
*/ */
@ -622,7 +719,7 @@ public class ECKey implements Serializable {
*/ */
public String signMessage(String message, @Nullable KeyParameter aesKey) throws KeyCrypterException { public String signMessage(String message, @Nullable KeyParameter aesKey) throws KeyCrypterException {
if (priv == null) if (priv == null)
throw new IllegalStateException("This ECKey does not have the private key necessary for signing."); throw new MissingPrivateKeyException();
byte[] data = Utils.formatMessageForSigning(message); byte[] data = Utils.formatMessageForSigning(message);
Sha256Hash hash = Sha256Hash.createDouble(data); Sha256Hash hash = Sha256Hash.createDouble(data);
ECDSASignature sig = sign(hash, aesKey); ECDSASignature sig = sign(hash, aesKey);
@ -630,7 +727,7 @@ public class ECKey implements Serializable {
int recId = -1; int recId = -1;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
ECKey k = ECKey.recoverFromSignature(i, sig, hash, isCompressed()); ECKey k = ECKey.recoverFromSignature(i, sig, hash, isCompressed());
if (k != null && Arrays.equals(k.pub, pub)) { if (k != null && k.pub.equals(pub)) {
recId = i; recId = i;
break; break;
} }
@ -697,7 +794,7 @@ public class ECKey implements Serializable {
*/ */
public void verifyMessage(String message, String signatureBase64) throws SignatureException { public void verifyMessage(String message, String signatureBase64) throws SignatureException {
ECKey key = ECKey.signedMessageToKey(message, signatureBase64); ECKey key = ECKey.signedMessageToKey(message, signatureBase64);
if (!Arrays.equals(key.getPubKey(), pub)) if (!key.pub.equals(pub))
throw new SignatureException("Signature did not match for message"); throw new SignatureException("Signature did not match for message");
} }
@ -769,7 +866,7 @@ public class ECKey implements Serializable {
BigInteger srInv = rInv.multiply(sig.s).mod(n); BigInteger srInv = rInv.multiply(sig.s).mod(n);
BigInteger eInvrInv = rInv.multiply(eInv).mod(n); BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
ECPoint.Fp q = (ECPoint.Fp) ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv); ECPoint.Fp q = (ECPoint.Fp) ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv);
return new ECKey((byte[])null, q.getEncoded(compressed)); return ECKey.fromPublicOnly(q.getEncoded(true));
} }
/** Decompress a compressed public key (x co-ord and low-bit of y-coord). */ /** Decompress a compressed public key (x co-ord and low-bit of y-coord). */
@ -781,11 +878,11 @@ public class ECKey implements Serializable {
} }
/** /**
* Returns a 32 byte array containing the private key, or null if the key is encrypted or public only * Returns a 32 byte array containing the private key.
* @throws com.google.bitcoin.core.ECKey.MissingPrivateKeyException if the private key bytes are missing/encrypted.
*/ */
@Nullable
public byte[] getPrivKeyBytes() { public byte[] getPrivKeyBytes() {
return Utils.bigIntegerToBytes(priv, 32); return Utils.bigIntegerToBytes(getPrivKey(), 32);
} }
/** /**
@ -797,15 +894,14 @@ public class ECKey implements Serializable {
* @throws IllegalStateException if the private key is not available. * @throws IllegalStateException if the private key is not available.
*/ */
public DumpedPrivateKey getPrivateKeyEncoded(NetworkParameters params) { public DumpedPrivateKey getPrivateKeyEncoded(NetworkParameters params) {
final byte[] privKeyBytes = getPrivKeyBytes(); return new DumpedPrivateKey(params, getPrivKeyBytes(), isCompressed());
checkState(privKeyBytes != null, "Private key is not available");
return new DumpedPrivateKey(params, privKeyBytes, isCompressed());
} }
/** /**
* Returns the creation time of this key or zero if the key was deserialized from a version that did not store * Returns the creation time of this key or zero if the key was deserialized from a version that did not store
* that data. * that data.
*/ */
@Override
public long getCreationTimeSeconds() { public long getCreationTimeSeconds() {
return creationTimeSeconds; return creationTimeSeconds;
} }
@ -823,33 +919,39 @@ public class ECKey implements Serializable {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || !(o instanceof ECKey)) return false;
ECKey other = (ECKey) o;
return Arrays.equals(pub, other.pub); ECKey ecKey = (ECKey) o;
if (creationTimeSeconds != ecKey.creationTimeSeconds) return false;
if (keyCrypter != null ? !keyCrypter.equals(ecKey.keyCrypter) : ecKey.keyCrypter != null) return false;
if (priv != null && !priv.equals(ecKey.priv)) return false;
if (pub != null && !pub.equals(ecKey.pub)) return false;
return true;
} }
@Override @Override
public int hashCode() { public int hashCode() {
// Public keys are random already so we can just use a part of them as the hashcode. Read from the start to // Public keys are random already so we can just use a part of them as the hashcode. Read from the start to
// avoid picking up the type code (compressed vs uncompressed) which is tacked on the end. // avoid picking up the type code (compressed vs uncompressed) which is tacked on the end.
return (pub[0] & 0xFF) | ((pub[1] & 0xFF) << 8) | ((pub[2] & 0xFF) << 16) | ((pub[3] & 0xFF) << 24); byte[] bits = getPubKey();
return (bits[0] & 0xFF) | ((bits[1] & 0xFF) << 8) | ((bits[2] & 0xFF) << 16) | ((bits[3] & 0xFF) << 24);
} }
/** /**
* Create an encrypted private key with the keyCrypter and the AES key supplied. * Create an encrypted private key with the keyCrypter and the AES key supplied.
* This method returns a new encrypted key and leaves the original unchanged. * This method returns a new encrypted key and leaves the original unchanged.
* To be secure you need to clear the original, unencrypted private key bytes.
* *
* @param keyCrypter The keyCrypter that specifies exactly how the encrypted bytes are created. * @param keyCrypter The keyCrypter that specifies exactly how the encrypted bytes are created.
* @param aesKey The KeyParameter with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached as it is slow to create). * @param aesKey The KeyParameter with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached as it is slow to create).
* @return encryptedKey * @return encryptedKey
*/ */
public ECKey encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException { public ECKey encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
Preconditions.checkNotNull(keyCrypter); checkNotNull(keyCrypter);
final byte[] privKeyBytes = getPrivKeyBytes(); final byte[] privKeyBytes = getPrivKeyBytes();
checkState(privKeyBytes != null, "Private key is not available"); EncryptedData encryptedPrivateKey = keyCrypter.encrypt(privKeyBytes, aesKey);
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(privKeyBytes, aesKey); ECKey result = ECKey.fromEncrypted(encryptedPrivateKey, keyCrypter, getPubKey());
ECKey result = new ECKey(encryptedPrivateKey, getPubKey(), keyCrypter);
result.setCreationTimeSeconds(creationTimeSeconds); result.setCreationTimeSeconds(creationTimeSeconds);
return result; return result;
} }
@ -864,13 +966,15 @@ public class ECKey implements Serializable {
* @return unencryptedKey * @return unencryptedKey
*/ */
public ECKey decrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException { public ECKey decrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
Preconditions.checkNotNull(keyCrypter); checkNotNull(keyCrypter);
// Check that the keyCrypter matches the one used to encrypt the keys, if set. // Check that the keyCrypter matches the one used to encrypt the keys, if set.
if (this.keyCrypter != null && !this.keyCrypter.equals(keyCrypter)) { if (this.keyCrypter != null && !this.keyCrypter.equals(keyCrypter))
throw new KeyCrypterException("The keyCrypter being used to decrypt the key is different to the one that was used to encrypt it"); throw new KeyCrypterException("The keyCrypter being used to decrypt the key is different to the one that was used to encrypt it");
} checkState(encryptedPrivateKey != null, "This key is not encrypted");
byte[] unencryptedPrivateKey = keyCrypter.decrypt(encryptedPrivateKey, aesKey); byte[] unencryptedPrivateKey = keyCrypter.decrypt(encryptedPrivateKey, aesKey);
ECKey key = new ECKey(new BigInteger(1, unencryptedPrivateKey), null, isCompressed()); ECKey key = ECKey.fromPrivate(unencryptedPrivateKey);
if (!isCompressed())
key = key.decompress();
if (!Arrays.equals(key.getPubKey(), getPubKey())) if (!Arrays.equals(key.getPubKey(), getPubKey()))
throw new KeyCrypterException("Provided AES key is wrong"); throw new KeyCrypterException("Provided AES key is wrong");
key.setCreationTimeSeconds(creationTimeSeconds); key.setCreationTimeSeconds(creationTimeSeconds);
@ -878,49 +982,30 @@ public class ECKey implements Serializable {
} }
/** /**
* Check that it is possible to decrypt the key with the keyCrypter and that the original key is returned. * <p>Check that it is possible to decrypt the key with the keyCrypter and that the original key is returned.</p>
* *
* Because it is a critical failure if the private keys cannot be decrypted successfully (resulting of loss of all bitcoins controlled * <p>Because it is a critical failure if the private keys cannot be decrypted successfully (resulting of loss of all
* by the private key) you can use this method to check when you *encrypt* a wallet that it can definitely be decrypted successfully. * bitcoins controlled by the private key) you can use this method to check when you *encrypt* a wallet that
* See {@link Wallet#encrypt(KeyCrypter keyCrypter, KeyParameter aesKey)} for example usage. * it can definitely be decrypted successfully.</p>
*
* <p>See {@link Wallet#encrypt(KeyCrypter keyCrypter, KeyParameter aesKey)} for example usage.</p>
* *
* @return true if the encrypted key can be decrypted back to the original key successfully. * @return true if the encrypted key can be decrypted back to the original key successfully.
*/ */
public static boolean encryptionIsReversible(ECKey originalKey, ECKey encryptedKey, KeyCrypter keyCrypter, KeyParameter aesKey) { public static boolean encryptionIsReversible(ECKey originalKey, ECKey encryptedKey, KeyCrypter keyCrypter, KeyParameter aesKey) {
String genericErrorText = "The check that encryption could be reversed failed for key " + originalKey.toString() + ". ";
try { try {
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, aesKey); ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, aesKey);
if (rebornUnencryptedKey == null) { byte[] originalPrivateKeyBytes = originalKey.getPrivKeyBytes();
log.error(genericErrorText + "The test decrypted key was missing."); byte[] rebornKeyBytes = rebornUnencryptedKey.getPrivKeyBytes();
if (!Arrays.equals(originalPrivateKeyBytes, rebornKeyBytes)) {
log.error("The check that encryption could be reversed failed for {}", originalKey);
return false; return false;
} }
return true;
byte[] originalPrivateKeyBytes = originalKey.getPrivKeyBytes();
if (originalPrivateKeyBytes != null) {
if (rebornUnencryptedKey.getPrivKeyBytes() == null) {
log.error(genericErrorText + "The test decrypted key was missing.");
return false;
} else {
if (originalPrivateKeyBytes.length != rebornUnencryptedKey.getPrivKeyBytes().length) {
log.error(genericErrorText + "The test decrypted private key was a different length to the original.");
return false;
} else {
for (int i = 0; i < originalPrivateKeyBytes.length; i++) {
if (originalPrivateKeyBytes[i] != rebornUnencryptedKey.getPrivKeyBytes()[i]) {
log.error(genericErrorText + "Byte " + i + " of the private key did not match the original.");
return false;
}
}
}
}
}
} catch (KeyCrypterException kce) { } catch (KeyCrypterException kce) {
log.error(kce.getMessage()); log.error(kce.getMessage());
return false; return false;
} }
// Key can successfully be decrypted.
return true;
} }
/** /**
@ -928,26 +1013,55 @@ public class ECKey implements Serializable {
* A private key is deemed to be encrypted when there is both a KeyCrypter and the encryptedPrivateKey is non-zero. * A private key is deemed to be encrypted when there is both a KeyCrypter and the encryptedPrivateKey is non-zero.
*/ */
public boolean isEncrypted() { public boolean isEncrypted() {
return keyCrypter != null && encryptedPrivateKey != null && encryptedPrivateKey.getEncryptedBytes() != null && encryptedPrivateKey.getEncryptedBytes().length > 0; return keyCrypter != null && encryptedPrivateKey != null && encryptedPrivateKey.encryptedBytes.length > 0;
}
@Nullable
@Override
public Protos.Wallet.EncryptionType getEncryptionType() {
return keyCrypter != null ? keyCrypter.getUnderstoodEncryptionType() : Protos.Wallet.EncryptionType.UNENCRYPTED;
} }
/** /**
* @return The encryptedPrivateKey (containing the encrypted private key bytes and initialisation vector) for this ECKey, * A wrapper for {@link #getPrivKeyBytes()} that returns null if the private key bytes are missing or would have
* or null if the ECKey is not encrypted. * to be derived (for the HD key case).
*/
@Override
@Nullable
public byte[] getSecretBytes() {
if (hasPrivKey())
return getPrivKeyBytes();
else
return null;
}
/** An alias for {@link #getEncryptedPrivateKey()} */
@Nullable
@Override
public EncryptedData getEncryptedData() {
return getEncryptedPrivateKey();
}
/**
* Returns the the encrypted private key bytes and initialisation vector for this ECKey, or null if the ECKey
* is not encrypted.
*/ */
@Nullable @Nullable
public EncryptedPrivateKey getEncryptedPrivateKey() { public EncryptedData getEncryptedPrivateKey() {
if (encryptedPrivateKey == null) { return encryptedPrivateKey;
return null;
} else {
return encryptedPrivateKey.clone();
}
} }
/** /**
* @return The KeyCrypter that was used to encrypt to encrypt this ECKey. You need this to decrypt the ECKey. * Returns the KeyCrypter that was used to encrypt to encrypt this ECKey. You need this to decrypt the ECKey.
*/ */
@Nullable
public KeyCrypter getKeyCrypter() { public KeyCrypter getKeyCrypter() {
return keyCrypter; return keyCrypter;
} }
public static class MissingPrivateKeyException extends RuntimeException {
}
public static class KeyIsEncryptedException extends MissingPrivateKeyException {
}
} }

View File

@ -174,7 +174,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
queueRecalc(true); queueRecalc(true);
} }
@Override public void onKeysAdded(Wallet wallet, List<ECKey> keys) { @Override public void onKeysAdded(List<ECKey> keys) {
queueRecalc(true); queueRecalc(true);
} }

View File

@ -893,13 +893,16 @@ public class Transaction extends ChildMessage implements Serializable {
// The anyoneCanPay feature isn't used at the moment. // The anyoneCanPay feature isn't used at the moment.
boolean anyoneCanPay = false; boolean anyoneCanPay = false;
byte[] connectedPubKeyScript = input.getOutpoint().getConnectedPubKeyScript(); byte[] connectedPubKeyScript = input.getOutpoint().getConnectedPubKeyScript();
if (key.hasPrivKey() || key.isEncrypted()) { try {
signatures[i] = calculateSignature(i, key, aesKey, connectedPubKeyScript, hashType, anyoneCanPay); signatures[i] = calculateSignature(i, key, aesKey, connectedPubKeyScript, hashType, anyoneCanPay);
} else { } catch (ECKey.KeyIsEncryptedException e) {
throw e;
} catch (ECKey.MissingPrivateKeyException e) {
// Create a dummy signature to ensure the transaction is of the correct size when we try to ensure // Create a dummy signature to ensure the transaction is of the correct size when we try to ensure
// the right fee-per-kb is attached. If the wallet doesn't have the privkey, the user is assumed to // the right fee-per-kb is attached. If the wallet doesn't have the privkey, the user is assumed to
// be doing something special and that they will replace the dummy signature with a real one later. // be doing something special and that they will replace the dummy signature with a real one later.
signatures[i] = TransactionSignature.dummy(); signatures[i] = TransactionSignature.dummy();
log.info("Used dummy signature for input {} due to failure during signing (most likely missing privkey)", i);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/** /**
* Copyright 2011 Google Inc. * Copyright 2013 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import com.google.bitcoin.script.Script; import com.google.bitcoin.script.Script;
import com.google.bitcoin.wallet.KeyChainEventListener;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.List; import java.util.List;
@ -25,7 +26,7 @@ import java.util.List;
* <p>Implementors are called when the contents of the wallet changes, for instance due to receiving/sending money * <p>Implementors are called when the contents of the wallet changes, for instance due to receiving/sending money
* or a block chain re-organize. It may be convenient to derive from {@link AbstractWalletEventListener} instead.</p> * or a block chain re-organize. It may be convenient to derive from {@link AbstractWalletEventListener} instead.</p>
*/ */
public interface WalletEventListener { public interface WalletEventListener extends KeyChainEventListener {
/** /**
* This is called when a transaction is seen that sends coins <b>to</b> this wallet, either because it * This is called when a transaction is seen that sends coins <b>to</b> this wallet, either because it
* was broadcast across the network or because a block was received. If a transaction is seen when it was broadcast, * was broadcast across the network or because a block was received. If a transaction is seen when it was broadcast,
@ -116,12 +117,6 @@ public interface WalletEventListener {
*/ */
void onWalletChanged(Wallet wallet); void onWalletChanged(Wallet wallet);
/**
* Called whenever a new key is added to the wallet, whether that be via {@link Wallet#addKeys(java.util.List)}
* or due to some other automatic derivation.
*/
void onKeysAdded(Wallet wallet, List<ECKey> keys);
/** Called whenever a new watched script is added to the wallet. */ /** Called whenever a new watched script is added to the wallet. */
void onScriptsAdded(Wallet wallet, List<Script> scripts); void onScriptsAdded(Wallet wallet, List<Script> scripts);
} }

View File

@ -17,49 +17,60 @@
package com.google.bitcoin.crypto; package com.google.bitcoin.crypto;
/** /**
* <p>This is just a wrapper for the i (child number) as per BIP 32 with a boolean getter for the first bit and a getter * <p>This is just a wrapper for the i (child number) as per BIP 32 with a boolean getter for the most significant bit
* for the actual 0-based child number. A {@link java.util.List} of these forms a <i>path</i> through a * and a getter for the actual 0-based child number. A {@link List} of these forms a <i>path</i> through a
* {@link DeterministicHierarchy}. This class is immutable. * {@link DeterministicHierarchy}. This class is immutable.
*/ */
public class ChildNumber { public class ChildNumber {
public static final int PRIV_BIT = 0x80000000; /**
* The bit that's set in the child number to indicate whether this key is "hardened". Given a hardened key, it is
* not possible to derive a child public key if you know only the hardened public key. With a non-hardened key this
* is possible, so you can derive trees of public keys given only a public parent, but the downside is that it's
* possible to leak private keys if you disclose a parent public key and a child private key (elliptic curve maths
* allows you to work upwards).
*/
public static final int HARDENED_BIT = 0x80000000;
public static final ChildNumber ZERO = new ChildNumber(0); public static final ChildNumber ZERO = new ChildNumber(0);
public static final ChildNumber ONE = new ChildNumber(1);
public static final ChildNumber ZERO_HARDENED = new ChildNumber(0, true);
/** Integer i as per BIP 32 spec, including the MSB denoting derivation type (0 = public, 1 = private) **/ /** Integer i as per BIP 32 spec, including the MSB denoting derivation type (0 = public, 1 = private) **/
private final int i; private final int i;
public ChildNumber(int childNumber, boolean isPrivate) { public ChildNumber(int childNumber, boolean isHardened) {
if (hasPrivateBit(childNumber)) { if (hasHardenedBit(childNumber))
throw new IllegalArgumentException("Most significant bit is reserved and shouldn't be set: " + childNumber); throw new IllegalArgumentException("Most significant bit is reserved and shouldn't be set: " + childNumber);
} i = isHardened ? (childNumber | HARDENED_BIT) : childNumber;
i = isPrivate ? (childNumber | PRIV_BIT) : childNumber;
} }
public ChildNumber(int i) { public ChildNumber(int i) {
this.i = i; this.i = i;
} }
/** Returns the uint32 encoded form of the path element, including the most significant bit. */
public int getI() { public int getI() {
return i; return i;
} }
public boolean isPrivateDerivation() { /** Returns the uint32 encoded form of the path element, including the most significant bit. */
return hasPrivateBit(i); public int i() { return i; }
public boolean isHardened() {
return hasHardenedBit(i);
} }
private static boolean hasPrivateBit(int a) { private static boolean hasHardenedBit(int a) {
return (a & PRIV_BIT) != 0; return (a & HARDENED_BIT) != 0;
} }
/** /** Returns the child number without the hardening bit set (i.e. index in that part of the tree). */
* @return the child number without the private/public derivation bit set. public int num() {
*/ return i & (~HARDENED_BIT);
public int getChildNumber() {
return i & (~PRIV_BIT);
} }
public String toString() { public String toString() {
return String.format("%d%s", getChildNumber(), isPrivateDerivation() ? "'" : ""); return String.format("%d%s", num(), isHardened() ? "'" : "");
} }
@Override @Override

View File

@ -25,6 +25,9 @@ import java.util.Map;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
// TODO: This whole API feels a bit object heavy. Do we really need ChildNumber and so many maps, etc?
// TODO: Should we be representing this using an actual tree arrangement in memory instead of a bunch of hashmaps?
/** /**
* <p>A DeterministicHierarchy calculates and keeps a whole tree (hierarchy) of keys originating from a single * <p>A DeterministicHierarchy calculates and keeps a whole tree (hierarchy) of keys originating from a single
* root key. This implements part of the BIP 32 specification. A deterministic key tree is useful because * root key. This implements part of the BIP 32 specification. A deterministic key tree is useful because
@ -46,8 +49,8 @@ public class DeterministicHierarchy implements Serializable {
private final Map<ImmutableList<ChildNumber>, DeterministicKey> keys = Maps.newHashMap(); private final Map<ImmutableList<ChildNumber>, DeterministicKey> keys = Maps.newHashMap();
private final ImmutableList<ChildNumber> rootPath; private final ImmutableList<ChildNumber> rootPath;
private final Map<ImmutableList<ChildNumber>, ChildNumber> lastPrivDerivedNumbers = Maps.newHashMap(); // Keep track of how many child keys each node has. This is kind of weak.
private final Map<ImmutableList<ChildNumber>, ChildNumber> lastPubDerivedNumbers = Maps.newHashMap(); private final Map<ImmutableList<ChildNumber>, ChildNumber> lastChildNumbers = Maps.newHashMap();
/** /**
* Constructs a new hierarchy rooted at the given key. Note that this does not have to be the top of the tree. * Constructs a new hierarchy rooted at the given key. Note that this does not have to be the top of the tree.
@ -55,11 +58,21 @@ public class DeterministicHierarchy implements Serializable {
*/ */
public DeterministicHierarchy(DeterministicKey rootKey) { public DeterministicHierarchy(DeterministicKey rootKey) {
putKey(rootKey); putKey(rootKey);
rootPath = rootKey.getChildNumberPath(); rootPath = rootKey.getPath();
} }
private void putKey(DeterministicKey key) { /**
keys.put(key.getChildNumberPath(), key); * Inserts a key into the heirarchy. Used during deserialization: you normally don't need this. Keys must be
* inserted in order.
*/
public void putKey(DeterministicKey key) {
ImmutableList<ChildNumber> path = key.getPath();
// Update our tracking of what the next child in each branch of the tree should be. Just assume that keys are
// inserted in order here.
final DeterministicKey parent = key.getParent();
if (parent != null)
lastChildNumbers.put(parent.getPath(), key.getChildNumber());
keys.put(path, key);
} }
/** /**
@ -76,7 +89,9 @@ public class DeterministicHierarchy implements Serializable {
? ImmutableList.<ChildNumber>builder().addAll(rootPath).addAll(path).build() ? ImmutableList.<ChildNumber>builder().addAll(rootPath).addAll(path).build()
: ImmutableList.copyOf(path); : ImmutableList.copyOf(path);
if (!keys.containsKey(absolutePath)) { if (!keys.containsKey(absolutePath)) {
checkArgument(create, "No key found for {} path {}.", relativePath ? "relative" : "absolute", path); if (!create)
throw new IllegalArgumentException(String.format("No key found for %s path %s.",
relativePath ? "relative" : "absolute", HDUtils.formatPath(path)));
checkArgument(absolutePath.size() > 0, "Can't derive the master key: nothing to derive from."); checkArgument(absolutePath.size() > 0, "Can't derive the master key: nothing to derive from.");
DeterministicKey parent = get(absolutePath.subList(0, absolutePath.size() - 1), relativePath, true); DeterministicKey parent = get(absolutePath.subList(0, absolutePath.size() - 1), relativePath, true);
putKey(HDKeyDerivation.deriveChildKey(parent, absolutePath.get(absolutePath.size() - 1))); putKey(HDKeyDerivation.deriveChildKey(parent, absolutePath.get(absolutePath.size() - 1)));
@ -100,7 +115,7 @@ public class DeterministicHierarchy implements Serializable {
int nAttempts = 0; int nAttempts = 0;
while (nAttempts++ < MAX_CHILD_DERIVATION_ATTEMPTS) { while (nAttempts++ < MAX_CHILD_DERIVATION_ATTEMPTS) {
try { try {
ChildNumber createChildNumber = getNextChildNumberToDerive(parent.getChildNumberPath(), privateDerivation); ChildNumber createChildNumber = getNextChildNumberToDerive(parent.getPath(), privateDerivation);
return deriveChild(parent, createChildNumber); return deriveChild(parent, createChildNumber);
} catch (HDDerivationException ignore) { } } catch (HDDerivationException ignore) { }
} }
@ -108,13 +123,20 @@ public class DeterministicHierarchy implements Serializable {
} }
private ChildNumber getNextChildNumberToDerive(ImmutableList<ChildNumber> path, boolean privateDerivation) { private ChildNumber getNextChildNumberToDerive(ImmutableList<ChildNumber> path, boolean privateDerivation) {
Map<ImmutableList<ChildNumber>, ChildNumber> lastDerivedNumbers = getLastDerivedNumbers(privateDerivation); ChildNumber lastChildNumber = lastChildNumbers.get(path);
ChildNumber lastChildNumber = lastDerivedNumbers.get(path); ChildNumber nextChildNumber = new ChildNumber(lastChildNumber != null ? lastChildNumber.num() + 1 : 0, privateDerivation);
ChildNumber nextChildNumber = new ChildNumber(lastChildNumber != null ? lastChildNumber.getChildNumber() + 1 : 0, privateDerivation); lastChildNumbers.put(path, nextChildNumber);
lastDerivedNumbers.put(path, nextChildNumber);
return nextChildNumber; return nextChildNumber;
} }
public int getNumChildren(ImmutableList<ChildNumber> path) {
final ChildNumber cn = lastChildNumbers.get(path);
if (cn == null)
return 0;
else
return cn.num() + 1; // children start with zero based childnumbers
}
/** /**
* Extends the tree by calculating the requested child for the given path. For example, to get the key at position * Extends the tree by calculating the requested child for the given path. For example, to get the key at position
* 1/2/3 you would pass 1/2 as the parent path and 3 as the child number. * 1/2/3 you would pass 1/2 as the parent path and 3 as the child number.
@ -141,8 +163,4 @@ public class DeterministicHierarchy implements Serializable {
public DeterministicKey getRootKey() { public DeterministicKey getRootKey() {
return get(rootPath, false, false); return get(rootPath, false, false);
} }
private Map<ImmutableList<ChildNumber>, ChildNumber> getLastDerivedNumbers(boolean privateDerivation) {
return privateDerivation ? lastPrivDerivedNumbers : lastPubDerivedNumbers;
}
} }

View File

@ -15,54 +15,80 @@
*/ */
package com.google.bitcoin.crypto; package com.google.bitcoin.crypto;
import com.google.bitcoin.core.Base58; import com.google.bitcoin.core.*;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Utils;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.math.ec.ECPoint; import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.util.encoders.Hex; import org.spongycastle.util.encoders.Hex;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.Serializable;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.text.MessageFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.*;
import static com.google.common.base.Preconditions.checkNotNull;
// TODO: Merge this with a redesigned ECKey class.
/** /**
* A deterministic key is a node in a {@link DeterministicHierarchy}. As per * A deterministic key is a node in a {@link DeterministicHierarchy}. As per
* <a href="https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki">the BIP 32 specification</a> it is a pair (key, chaincode). If you * <a href="https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki">the BIP 32 specification</a> it is a pair
* know its path in the tree you can derive more keys from this. * (key, chaincode). If you know its path in the tree and its chain code you can derive more keys from this. To obtain
* one of these, you can call {@link HDKeyDerivation#createMasterPrivateKey(byte[])}.
*/ */
public class DeterministicKey implements Serializable { public class DeterministicKey extends ECKey {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final Joiner PATH_JOINER = Joiner.on("/");
private final DeterministicKey parent; private final DeterministicKey parent;
private ECPoint publicAsPoint;
private final BigInteger privateAsFieldElement;
private final ImmutableList<ChildNumber> childNumberPath; private final ImmutableList<ChildNumber> childNumberPath;
/** 32 bytes */ /** 32 bytes */
private final byte[] chainCode; private final byte[] chainCode;
DeterministicKey(ImmutableList<ChildNumber> childNumberPath, byte[] chainCode, /** The 4 byte header that serializes in base58 to "xpub" */
@Nullable ECPoint publicAsPoint, @Nullable BigInteger privateKeyFieldElt, public static final int HEADER_PUB = 0x0488B21E;
@Nullable DeterministicKey parent) { /** The 4 byte header that serializes in base58 to "xprv" */
public static final int HEADER_PRIV = 0x0488ADE4;
/** Constructs a key from its components. This is not normally something you should use. */
public DeterministicKey(ImmutableList<ChildNumber> childNumberPath,
byte[] chainCode,
ECPoint publicAsPoint,
@Nullable BigInteger priv,
@Nullable DeterministicKey parent) {
super(priv, compressPoint(checkNotNull(publicAsPoint)));
checkArgument(chainCode.length == 32); checkArgument(chainCode.length == 32);
this.parent = parent; this.parent = parent;
this.childNumberPath = childNumberPath; this.childNumberPath = checkNotNull(childNumberPath);
this.chainCode = Arrays.copyOf(chainCode, chainCode.length); this.chainCode = Arrays.copyOf(chainCode, chainCode.length);
this.publicAsPoint = publicAsPoint == null ? null : publicAsPoint.normalize(); }
this.privateAsFieldElement = privateKeyFieldElt;
/** Constructs a key from its components. This is not normally something you should use. */
public DeterministicKey(ImmutableList<ChildNumber> childNumberPath,
byte[] chainCode,
BigInteger priv,
@Nullable DeterministicKey parent) {
super(priv, compressPoint(ECKey.CURVE.getG().multiply(priv)));
checkArgument(chainCode.length == 32);
this.parent = parent;
this.childNumberPath = checkNotNull(childNumberPath);
this.chainCode = Arrays.copyOf(chainCode, chainCode.length);
}
/** Constructs a key from its components. This is not normally something you should use. */
public DeterministicKey(ImmutableList<ChildNumber> childNumberPath,
byte[] chainCode,
KeyCrypter crypter, ECPoint pub, EncryptedData priv, @Nullable DeterministicKey parent) {
this(childNumberPath, chainCode, pub, null, parent);
this.encryptedPrivateKey = checkNotNull(priv);
this.keyCrypter = checkNotNull(crypter);
}
/** Clones the key */
public DeterministicKey(DeterministicKey keyToClone, DeterministicKey newParent) {
super(keyToClone.priv, keyToClone.pub);
this.parent = newParent;
this.childNumberPath = keyToClone.childNumberPath;
this.chainCode = keyToClone.chainCode;
this.encryptedPrivateKey = keyToClone.encryptedPrivateKey;
} }
/** /**
@ -70,17 +96,22 @@ public class DeterministicKey implements Serializable {
* A path can be written as 1/2/1 which means the first child of the root, the second child of that node, then * A path can be written as 1/2/1 which means the first child of the root, the second child of that node, then
* the first child of that node. * the first child of that node.
*/ */
public ImmutableList<ChildNumber> getChildNumberPath() { public ImmutableList<ChildNumber> getPath() {
return childNumberPath; return childNumberPath;
} }
/**
* Returns the path of this key as a human readable string starting with M to indicate the master key.
*/
public String getPathAsString() {
return HDUtils.formatPath(getPath());
}
private int getDepth() { private int getDepth() {
return childNumberPath.size(); return childNumberPath.size();
} }
/** /** Returns the last element of the path returned by {@link DeterministicKey#getPath()} */
* Returns the last element of the path returned by {@link DeterministicKey#getChildNumberPath()}
*/
public ChildNumber getChildNumber() { public ChildNumber getChildNumber() {
return getDepth() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1); return getDepth() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1);
} }
@ -92,63 +123,31 @@ public class DeterministicKey implements Serializable {
return chainCode; return chainCode;
} }
/**
* Returns the path of this key as a human readable string starting with M to indicate the master key.
*/
public String getPath() {
return PATH_JOINER.join(Iterables.concat(Collections.singleton("M"), getChildNumberPath()));
}
/** /**
* Returns RIPE-MD160(SHA256(pub key bytes)). * Returns RIPE-MD160(SHA256(pub key bytes)).
*/ */
public byte[] getIdentifier() { public byte[] getIdentifier() {
return Utils.sha256hash160(getPubKeyBytes()); return Utils.sha256hash160(getPubKey());
} }
ECPoint getPubPoint() {
if (publicAsPoint == null) {
checkNotNull(privateAsFieldElement);
publicAsPoint = ECKey.CURVE.getG().multiply(privateAsFieldElement).normalize();
}
return publicAsPoint;
}
public byte[] getPubKeyBytes() {
return getPubPoint().getEncoded(true);
}
/** Returns the first 32 bits of the result of {@link #getIdentifier()}. */ /** Returns the first 32 bits of the result of {@link #getIdentifier()}. */
public byte[] getFingerprint() { public byte[] getFingerprint() {
// TODO: why is this different than armory's fingerprint? BIP 32: "The first 32 bits of the identifier are called the fingerprint." // TODO: why is this different than armory's fingerprint? BIP 32: "The first 32 bits of the identifier are called the fingerprint."
return Arrays.copyOfRange(getIdentifier(), 0, 4); return Arrays.copyOfRange(getIdentifier(), 0, 4);
} }
@Nullable
public BigInteger getPrivAsFieldElement() {
return privateAsFieldElement;
}
@Nullable @Nullable
public DeterministicKey getParent() { public DeterministicKey getParent() {
return parent; return parent;
} }
/** /**
* Returns the private key bytes, if they were provided during construction. * Returns private key bytes, padded with zeros to 33 bytes.
*/ * @throws java.lang.IllegalStateException if the private key bytes are missing.
@Nullable
public byte[] getPrivKeyBytes() {
return privateAsFieldElement == null ? null : privateAsFieldElement.toByteArray();
}
/**
* @return private key bytes, padded with zeros to 33 bytes.
*/ */
public byte[] getPrivKeyBytes33() { public byte[] getPrivKeyBytes33() {
byte[] bytes33 = new byte[33]; byte[] bytes33 = new byte[33];
byte[] priv = checkNotNull(getPrivKeyBytes(), "Private key missing"); byte[] priv = getPrivKeyBytes();
System.arraycopy(priv, 0, bytes33, 33 - priv.length, priv.length); System.arraycopy(priv, 0, bytes33, 33 - priv.length, priv.length);
return bytes33; return bytes33;
} }
@ -157,17 +156,169 @@ public class DeterministicKey implements Serializable {
* Returns the same key with the private part removed. May return the same instance. * Returns the same key with the private part removed. May return the same instance.
*/ */
public DeterministicKey getPubOnly() { public DeterministicKey getPubOnly() {
if (!hasPrivate()) return this; if (isPubKeyOnly()) return this;
final DeterministicKey parentPub = getParent() == null ? null : getParent().getPubOnly(); final DeterministicKey parentPub = getParent() == null ? null : getParent().getPubOnly();
return new DeterministicKey(getChildNumberPath(), getChainCode(), getPubPoint(), null, parentPub); return new DeterministicKey(getPath(), getChainCode(), getPubKeyPoint(), null, parentPub);
} }
public boolean hasPrivate() {
return privateAsFieldElement != null; static byte[] addChecksum(byte[] input) {
int inputLength = input.length;
byte[] checksummed = new byte[inputLength + 4];
System.arraycopy(input, 0, checksummed, 0, inputLength);
byte[] checksum = Utils.doubleDigest(input);
System.arraycopy(checksum, 0, checksummed, inputLength, 4);
return checksummed;
} }
public ECKey toECKey() { @Override
return new ECKey(getPrivKeyBytes(), getPubKeyBytes()); public DeterministicKey encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
throw new UnsupportedOperationException("Must supply a new parent for encryption");
}
public DeterministicKey encrypt(KeyCrypter keyCrypter, KeyParameter aesKey, @Nullable DeterministicKey newParent) throws KeyCrypterException {
// Same as the parent code, except we construct a DeterministicKey instead of an ECKey.
checkNotNull(keyCrypter);
if (newParent != null)
checkArgument(newParent.isEncrypted());
final byte[] privKeyBytes = getPrivKeyBytes();
checkState(privKeyBytes != null, "Private key is not available");
EncryptedData encryptedPrivateKey = keyCrypter.encrypt(privKeyBytes, aesKey);
return new DeterministicKey(childNumberPath, chainCode, keyCrypter, pub, encryptedPrivateKey, newParent);
}
/**
* A deterministic key is considered to be encrypted if it has access to encrypted private key bytes, OR if its
* parent does. The reason is because the parent would be encrypted under the same key and this key knows how to
* rederive its own private key bytes from the parent, if needed.
*/
@Override
public boolean isEncrypted() {
return priv == null && (super.isEncrypted() || (parent != null && parent.isEncrypted()));
}
/**
* Returns this keys {@link com.google.bitcoin.crypto.KeyCrypter} <b>or</b> the keycrypter of its parent key.
*/
@Override @Nullable
public KeyCrypter getKeyCrypter() {
if (keyCrypter != null)
return keyCrypter;
else if (parent != null)
return parent.getKeyCrypter();
else
return null;
}
@Override
public ECDSASignature sign(Sha256Hash input, @Nullable KeyParameter aesKey) throws KeyCrypterException {
if (isEncrypted()) {
// If the key is encrypted, ECKey.sign will decrypt it first before rerunning sign. Decryption walks the
// key heirarchy to find the private key (see below), so, we can just run the inherited method.
return super.sign(input, aesKey);
} else {
// If it's not encrypted, derive the private via the parents.
final BigInteger privateKey = findOrDerivePrivateKey();
checkState(privateKey != null, "This key is a part of a public-key only heirarchy and cannot be used for signing");
return super.doSign(input, privateKey);
}
}
@Override
public DeterministicKey decrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
checkNotNull(keyCrypter);
// Check that the keyCrypter matches the one used to encrypt the keys, if set.
if (this.keyCrypter != null && !this.keyCrypter.equals(keyCrypter))
throw new KeyCrypterException("The keyCrypter being used to decrypt the key is different to the one that was used to encrypt it");
BigInteger privKey = findOrDeriveEncryptedPrivateKey(keyCrypter, aesKey);
DeterministicKey key = new DeterministicKey(childNumberPath, chainCode, privKey, parent);
if (!Arrays.equals(key.getPubKey(), getPubKey()))
throw new KeyCrypterException("Provided AES key is wrong");
return key;
}
// For when a key is encrypted, either decrypt our encrypted private key bytes, or work up the tree asking parents
// to decrypt and re-derive.
private BigInteger findOrDeriveEncryptedPrivateKey(KeyCrypter keyCrypter, KeyParameter aesKey) {
if (encryptedPrivateKey != null)
return new BigInteger(1, keyCrypter.decrypt(encryptedPrivateKey, aesKey));
// Otherwise we don't have it, but maybe we can figure it out from our parents. Walk up the tree looking for
// the first key that has some encrypted private key data.
DeterministicKey cursor = parent;
while (cursor != null) {
if (cursor.encryptedPrivateKey != null) break;
cursor = cursor.parent;
}
if (cursor == null)
throw new KeyCrypterException("Neither this key nor its parents have an encrypted private key");
byte[] parentalPrivateKeyBytes = keyCrypter.decrypt(cursor.encryptedPrivateKey, aesKey);
return derivePrivateKeyDownwards(cursor, parentalPrivateKeyBytes);
}
@Nullable
private BigInteger findOrDerivePrivateKey() {
DeterministicKey cursor = this;
while (cursor != null) {
if (cursor.priv != null) break;
cursor = cursor.parent;
}
if (cursor == null)
return null;
return derivePrivateKeyDownwards(cursor, cursor.priv.toByteArray());
}
private BigInteger derivePrivateKeyDownwards(DeterministicKey cursor, byte[] parentalPrivateKeyBytes) {
DeterministicKey downCursor = new DeterministicKey(cursor.childNumberPath, cursor.chainCode,
cursor.pub, new BigInteger(1, parentalPrivateKeyBytes), cursor.parent);
// Now we have to rederive the keys along the path back to ourselves. That path can be found by just truncating
// our path with the length of the parents path.
ImmutableList<ChildNumber> path = childNumberPath.subList(cursor.getDepth(), childNumberPath.size());
for (ChildNumber num : path) {
downCursor = HDKeyDerivation.deriveChildKey(downCursor, num);
}
// downCursor is now the same key as us, but with private key bytes.
checkState(downCursor.pub.equals(pub));
return checkNotNull(downCursor.priv);
}
public DeterministicKey derive(int child) {
return HDKeyDerivation.deriveChildKey(this, new ChildNumber(child, true));
}
/**
* Returns the private key of this deterministic key. Even if this object isn't storing the private key,
* it can be re-derived by walking up to the parents if necessary and this is what will happen.
* @throws java.lang.IllegalStateException if the parents are encrypted or a watching chain.
*/
@Override
public BigInteger getPrivKey() {
final BigInteger key = findOrDerivePrivateKey();
checkState(key != null, "Private key bytes not available");
return key;
}
public byte[] serializePublic() {
return serialize(true);
}
public byte[] serializePrivate() {
return serialize(false);
}
private byte[] serialize(boolean pub) {
ByteBuffer ser = ByteBuffer.allocate(78);
ser.putInt(pub ? HEADER_PUB : HEADER_PRIV);
ser.put((byte) getDepth());
if (parent == null) {
ser.putInt(0);
} else {
ser.put(parent.getFingerprint());
}
ser.putInt(getChildNumber().i());
ser.put(getChainCode());
ser.put(pub ? getPubKey() : getPrivKeyBytes33());
checkState(ser.position() == 78);
return ser.array();
} }
public String serializePubB58() { public String serializePubB58() {
@ -182,42 +333,85 @@ public class DeterministicKey implements Serializable {
return Base58.encode(addChecksum(ser)); return Base58.encode(addChecksum(ser));
} }
static byte[] addChecksum(byte[] input) { public static DeterministicKey deserializeB58(@Nullable DeterministicKey parent, String base58) {
int inputLength = input.length; try {
byte[] checksummed = new byte[inputLength + 4]; ByteBuffer buffer = ByteBuffer.wrap(Base58.decodeChecked(base58));
System.arraycopy(input, 0, checksummed, 0, inputLength); int header = buffer.getInt();
byte[] checksum = Utils.doubleDigest(input); if (header != HEADER_PRIV && header != HEADER_PUB)
System.arraycopy(checksum, 0, checksummed, inputLength, 4); throw new IllegalArgumentException("Unknown header bytes: " + base58.substring(0, 4));
return checksummed; boolean pub = header == HEADER_PUB;
} byte depth = buffer.get();
byte[] parentFingerprint = new byte[4];
public byte[] serializePublic() { buffer.get(parentFingerprint);
return serialize(true); final int i = buffer.getInt();
} final ChildNumber childNumber = new ChildNumber(i);
ImmutableList<ChildNumber> path;
public byte[] serializePrivate() { if (parent != null) {
return serialize(false); if (Arrays.equals(parentFingerprint, HDUtils.longTo4ByteArray(0)))
} throw new IllegalArgumentException("Parent was provided but this key doesn't have one");
if (!Arrays.equals(parent.getFingerprint(), parentFingerprint))
private byte[] serialize(boolean pub) { throw new IllegalArgumentException("Parent fingerprints don't match");
ByteBuffer ser = ByteBuffer.allocate(78); path = HDUtils.append(parent.getPath(), childNumber);
ser.putInt(pub ? 0x0488B21E : 0x0488ADE4); if (path.size() != depth)
ser.put((byte) getDepth()); throw new IllegalArgumentException("Depth does not match");
if (parent == null) { } else {
ser.putInt(0); if (depth == 0) {
} else { path = ImmutableList.of();
ser.put(parent.getFingerprint()); } else if (depth == 1) {
// We have been given a key that is not a root key, yet we also don't have any object representing
// the parent. This can happen when deserializing an account key for a watching wallet. In this case,
// we assume that the parent has a path of zero.
path = ImmutableList.of(childNumber);
} else {
throw new IllegalArgumentException("Depth is " + depth + " and no parent key was provided, so we " +
"cannot reconstruct the key path from the provided data.");
}
}
byte[] chainCode = new byte[32];
buffer.get(chainCode);
byte[] data = new byte[33];
buffer.get(data);
checkArgument(!buffer.hasRemaining(), "Found unexpected data in key");
if (pub) {
ECPoint point = ECKey.CURVE.getCurve().decodePoint(data);
return new DeterministicKey(path, chainCode, point, null, parent);
} else {
return new DeterministicKey(path, chainCode, new BigInteger(1, data), parent);
}
} catch (AddressFormatException e) {
throw new IllegalArgumentException(e);
} }
ser.putInt(getChildNumber().getI()); }
ser.put(getChainCode());
ser.put(pub ? getPubKeyBytes() : getPrivKeyBytes33());
assert ser.position() == 78;
return ser.array(); /**
* Verifies equality of all fields but NOT the parent pointer (thus the same key derived in two separate heirarchy
* objects will equal each other.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
DeterministicKey key = (DeterministicKey) o;
if (!Arrays.equals(chainCode, key.chainCode)) return false;
if (!childNumberPath.equals(key.childNumberPath)) return false;
return true;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + childNumberPath.hashCode();
result = 31 * result + Arrays.hashCode(chainCode);
return result;
} }
@Override @Override
public String toString() { public String toString() {
return MessageFormat.format("ExtendedHierarchicKey[pub: {0}]", new String(Hex.encode(getPubKeyBytes()))); return String.format("pub:%s chaincode:%s path:%s", new String(Hex.encode(getPubKey())),
new String(Hex.encode(getChainCode())), getPathAsString());
} }
} }

View File

@ -0,0 +1,43 @@
/**
* Copyright 2014 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.crypto;
import org.bitcoinj.wallet.Protos;
import javax.annotation.Nullable;
/**
* Provides a uniform way to access something that can be optionally encrypted with a
* {@link com.google.bitcoin.crypto.KeyCrypter}, yielding an {@link com.google.bitcoin.crypto.EncryptedData}, and
* which can have a creation time associated with it.
*/
public interface EncryptableItem {
/** Returns whether the item is encrypted or not. If it is, then {@link #getSecretBytes()} will return null. */
public boolean isEncrypted();
/** Returns the raw bytes of the item, if not encrypted, or null if encrypted or the secret is missing. */
@Nullable public byte[] getSecretBytes();
/** Returns the initialization vector and encrypted secret bytes, or null if not encrypted. */
@Nullable public EncryptedData getEncryptedData();
/** Returns an enum constant describing what algorithm was used to encrypt the key or UNENCRYPTED. */
public Protos.Wallet.EncryptionType getEncryptionType();
/** Returns the time in seconds since the UNIX epoch at which this encryptable item was first created/derived. */
public long getCreationTimeSeconds();
}

View File

@ -0,0 +1,56 @@
/**
* Copyright 2013 Jim Burton.
*
* Licensed under the MIT license (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.php
*
* 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.crypto;
import java.util.Arrays;
/**
* <p>An instance of EncryptedData is a holder for an initialization vector and encrypted bytes. It is typically
* used to hold encrypted private key bytes.</p>
*
* <p>The initialisation vector is random data that is used to initialise the AES block cipher when the
* private key bytes were encrypted. You need these for decryption.</p>
*/
public final class EncryptedData {
public final byte[] initialisationVector;
public final byte[] encryptedBytes;
public EncryptedData(byte[] initialisationVector, byte[] encryptedBytes) {
this.initialisationVector = Arrays.copyOf(initialisationVector, initialisationVector.length);
this.encryptedBytes = Arrays.copyOf(encryptedBytes, encryptedBytes.length);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EncryptedData that = (EncryptedData) o;
return Arrays.equals(encryptedBytes, that.encryptedBytes) && Arrays.equals(initialisationVector, that.initialisationVector);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(initialisationVector);
result = 31 * result + Arrays.hashCode(encryptedBytes);
return result;
}
@Override
public String toString() {
return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector) + ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes) + "]";
}
}

View File

@ -29,7 +29,8 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
/** /**
* Implementation of the (public derivation version) deterministic wallet child key generation algorithm. * Implementation of the <a href="https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki">BIP 32</a>
* deterministic wallet child key generation algorithm.
*/ */
public final class HDKeyDerivation { public final class HDKeyDerivation {
@ -65,12 +66,11 @@ public final class HDKeyDerivation {
/** /**
* @throws HDDerivationException if privKeyBytes is invalid (0 or >= n). * @throws HDDerivationException if privKeyBytes is invalid (0 or >= n).
*/ */
public static DeterministicKey createMasterPrivKeyFromBytes( public static DeterministicKey createMasterPrivKeyFromBytes(byte[] privKeyBytes, byte[] chainCode) throws HDDerivationException {
byte[] privKeyBytes, byte[] chainCode) throws HDDerivationException { BigInteger priv = new BigInteger(1, privKeyBytes);
BigInteger privateKeyFieldElt = new BigInteger(1, privKeyBytes); assertNonZero(priv, "Generated master key is invalid.");
assertNonZero(privateKeyFieldElt, "Generated master key is invalid."); assertLessThanN(priv, "Generated master key is invalid.");
assertLessThanN(privateKeyFieldElt, "Generated master key is invalid."); return new DeterministicKey(ImmutableList.<ChildNumber>of(), chainCode, priv, null);
return new DeterministicKey(ImmutableList.<ChildNumber>of(), chainCode, null, privateKeyFieldElt, null);
} }
public static DeterministicKey createMasterPubKeyFromBytes(byte[] pubKeyBytes, byte[] chainCode) { public static DeterministicKey createMasterPubKeyFromBytes(byte[] pubKeyBytes, byte[] chainCode) {
@ -78,7 +78,8 @@ public final class HDKeyDerivation {
} }
/** /**
* @param childNumber the "extended" child number, ie. with the 0x80000000 bit specifying private/public derivation. * Derives a key given the "extended" child number, ie. with the 0x80000000 bit specifying whether to use hardened
* derivation or not.
*/ */
public static DeterministicKey deriveChildKey(DeterministicKey parent, int childNumber) { public static DeterministicKey deriveChildKey(DeterministicKey parent, int childNumber) {
return deriveChildKey(parent, new ChildNumber(childNumber)); return deriveChildKey(parent, new ChildNumber(childNumber));
@ -88,49 +89,67 @@ public final class HDKeyDerivation {
* @throws HDDerivationException if private derivation is attempted for a public-only parent key, or * @throws HDDerivationException if private derivation is attempted for a public-only parent key, or
* if the resulting derived key is invalid (eg. private key == 0). * if the resulting derived key is invalid (eg. private key == 0).
*/ */
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
throws HDDerivationException { if (parent.isPubKeyOnly()) {
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);
RawKeyBytes rawKey = deriveChildKeyBytes(parent, childNumber); return new DeterministicKey(
return new DeterministicKey( HDUtils.append(parent.getPath(), childNumber),
HDUtils.append(parent.getChildNumberPath(), childNumber), rawKey.chainCode,
rawKey.chainCode, ECKey.CURVE.getCurve().decodePoint(rawKey.keyBytes), // c'tor will compress
parent.hasPrivate() ? null : ECKey.CURVE.getCurve().decodePoint(rawKey.keyBytes), null,
parent.hasPrivate() ? new BigInteger(1, rawKey.keyBytes) : null, parent);
parent); } else {
RawKeyBytes rawKey = deriveChildKeyBytesFromPrivate(parent, childNumber);
return new DeterministicKey(
HDUtils.append(parent.getPath(), childNumber),
rawKey.chainCode,
new BigInteger(1, rawKey.keyBytes),
parent);
}
} }
private static RawKeyBytes deriveChildKeyBytes(DeterministicKey parent, ChildNumber childNumber) private static RawKeyBytes deriveChildKeyBytesFromPrivate(DeterministicKey parent,
throws HDDerivationException { ChildNumber childNumber) throws HDDerivationException {
checkArgument(parent.hasPrivKey(), "Parent key must have private key bytes for this method.");
byte[] parentPublicKey = HDUtils.getBytes(parent.getPubPoint()); byte[] parentPublicKey = ECKey.compressPoint(parent.getPubKeyPoint()).getEncoded();
assert parentPublicKey.length == 33 : parentPublicKey.length; assert parentPublicKey.length == 33 : parentPublicKey.length;
ByteBuffer data = ByteBuffer.allocate(37); ByteBuffer data = ByteBuffer.allocate(37);
if (childNumber.isPrivateDerivation()) { if (childNumber.isHardened()) {
data.put(parent.getPrivKeyBytes33()); data.put(parent.getPrivKeyBytes33());
} else { } else {
data.put(parentPublicKey); data.put(parentPublicKey);
} }
data.putInt(childNumber.getI()); data.putInt(childNumber.i());
byte[] i = HDUtils.hmacSha512(parent.getChainCode(), data.array()); byte[] i = HDUtils.hmacSha512(parent.getChainCode(), data.array());
assert i.length == 64 : i.length; assert i.length == 64 : i.length;
byte[] il = Arrays.copyOfRange(i, 0, 32); byte[] il = Arrays.copyOfRange(i, 0, 32);
byte[] chainCode = Arrays.copyOfRange(i, 32, 64); byte[] chainCode = Arrays.copyOfRange(i, 32, 64);
BigInteger ilInt = new BigInteger(1, il); BigInteger ilInt = new BigInteger(1, il);
assertLessThanN(ilInt, "Illegal derived key: I_L >= n"); assertLessThanN(ilInt, "Illegal derived key: I_L >= n");
byte[] keyBytes; final BigInteger priv = parent.getPrivKey();
final BigInteger privAsFieldElement = parent.getPrivAsFieldElement(); BigInteger ki = priv.add(ilInt).mod(ECKey.CURVE.getN());
if (privAsFieldElement != null) { assertNonZero(ki, "Illegal derived key: derived private key equals 0.");
BigInteger ki = privAsFieldElement.add(ilInt).mod(ECKey.CURVE.getN()); return new RawKeyBytes(ki.toByteArray(), chainCode);
assertNonZero(ki, "Illegal derived key: derived private key equals 0."); }
keyBytes = ki.toByteArray();
} else { private static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
checkArgument(!childNumber.isPrivateDerivation(), "Can't use private derivation with public keys only."); checkArgument(!childNumber.isHardened(), "Can't use private derivation with public keys only.");
ECPoint Ki = ECKey.CURVE.getG().multiply(ilInt).add(parent.getPubPoint()); byte[] parentPublicKey = ECKey.compressPoint(parent.getPubKeyPoint()).getEncoded();
checkArgument(!Ki.isInfinity(), "Illegal derived key: derived public key equals infinity."); assert parentPublicKey.length == 33 : parentPublicKey.length;
keyBytes = Ki.getEncoded(true); ByteBuffer data = ByteBuffer.allocate(37);
} data.put(parentPublicKey);
return new RawKeyBytes(keyBytes, chainCode); data.putInt(childNumber.i());
byte[] i = HDUtils.hmacSha512(parent.getChainCode(), data.array());
assert i.length == 64 : i.length;
byte[] il = Arrays.copyOfRange(i, 0, 32);
byte[] chainCode = Arrays.copyOfRange(i, 32, 64);
BigInteger ilInt = new BigInteger(1, il);
// TODO: Throw a specific exception here to make sure the caller iterates.
assertLessThanN(ilInt, "Illegal derived key: I_L >= n");
ECPoint Ki = ECKey.CURVE.getG().multiply(ilInt).add(parent.getPubKeyPoint());
checkArgument(!Ki.equals(ECKey.CURVE.getCurve().getInfinity()),
"Illegal derived key: derived public key equals infinity.");
return new RawKeyBytes(Ki.getEncoded(true), chainCode);
} }
private static void assertNonZero(BigInteger integer, String errorMessage) { private static void assertNonZero(BigInteger integer, String errorMessage) {

View File

@ -17,21 +17,23 @@
package com.google.bitcoin.crypto; package com.google.bitcoin.crypto;
import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.ECKey;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import org.spongycastle.crypto.digests.SHA512Digest; import org.spongycastle.crypto.digests.SHA512Digest;
import org.spongycastle.crypto.macs.HMac; import org.spongycastle.crypto.macs.HMac;
import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.math.ec.ECPoint;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** /**
* Static utilities used in BIP 32 Hierarchical Deterministic Wallets (HDW). * Static utilities used in BIP 32 Hierarchical Deterministic Wallets (HDW).
*/ */
public final class HDUtils { public final class HDUtils {
private static final Joiner PATH_JOINER = Joiner.on("/");
private HDUtils() { }
static HMac createHmacSha512Digest(byte[] key) { static HMac createHmacSha512Digest(byte[] key) {
SHA512Digest digest = new SHA512Digest(); SHA512Digest digest = new SHA512Digest();
@ -62,11 +64,11 @@ public final class HDUtils {
return bytes; return bytes;
} }
static byte[] getBytes(ECPoint pubKPoint) {
return pubKPoint.getEncoded(true);
}
static ImmutableList<ChildNumber> append(ImmutableList<ChildNumber> path, ChildNumber childNumber) { static ImmutableList<ChildNumber> append(ImmutableList<ChildNumber> path, ChildNumber childNumber) {
return ImmutableList.<ChildNumber>builder().addAll(path).add(childNumber).build(); return ImmutableList.<ChildNumber>builder().addAll(path).add(childNumber).build();
} }
public static String formatPath(List<ChildNumber> path) {
return PATH_JOINER.join(Iterables.concat(Collections.singleton("M"), path));
}
} }

View File

@ -15,23 +15,23 @@
*/ */
package com.google.bitcoin.crypto; package com.google.bitcoin.crypto;
import java.io.Serializable;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType; import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.KeyParameter;
import java.io.Serializable;
/** /**
* <p>A KeyCrypter can be used to encrypt and decrypt a message. The sequence of events to encrypt and then decrypt * <p>A KeyCrypter can be used to encrypt and decrypt a message. The sequence of events to encrypt and then decrypt
* a message are as follows:</p> * a message are as follows:</p>
* *
*<p>(1) Ask the user for a password. deriveKey() is then called to create an KeyParameter. This contains the AES * <p>(1) Ask the user for a password. deriveKey() is then called to create an KeyParameter. This contains the AES
*key that will be used for encryption.</p> * key that will be used for encryption.</p>
*<p>(2) Encrypt the message using encrypt(), providing the message bytes and the KeyParameter from (1). This returns * <p>(2) Encrypt the message using encrypt(), providing the message bytes and the KeyParameter from (1). This returns
*an EncryptedPrivateKey which contains the encryptedPrivateKey bytes and an initialisation vector.</p> * an EncryptedData which contains the encryptedPrivateKey bytes and an initialisation vector.</p>
*<p>(3) To decrypt an EncryptedPrivateKey, repeat step (1) to get a KeyParameter, then call decrypt().</p> * <p>(3) To decrypt an EncryptedData, repeat step (1) to get a KeyParameter, then call decrypt().</p>
* *
*<p>There can be different algorithms used for encryption/ decryption so the getUnderstoodEncryptionType is used * <p>There can be different algorithms used for encryption/ decryption so the getUnderstoodEncryptionType is used
*to determine whether any given KeyCrypter can understand the type of encrypted data you have.</p> * to determine whether any given KeyCrypter can understand the type of encrypted data you have.</p>
*/ */
public interface KeyCrypter extends Serializable { public interface KeyCrypter extends Serializable {
@ -54,7 +54,7 @@ public interface KeyCrypter extends Serializable {
* *
* @throws KeyCrypterException if decryption was unsuccessful. * @throws KeyCrypterException if decryption was unsuccessful.
*/ */
public byte[] decrypt(EncryptedPrivateKey encryptedBytesToDecode, KeyParameter aesKey) throws KeyCrypterException; public byte[] decrypt(EncryptedData encryptedBytesToDecode, KeyParameter aesKey) throws KeyCrypterException;
/** /**
* Encrypt the supplied bytes, converting them into ciphertext. * Encrypt the supplied bytes, converting them into ciphertext.
@ -62,5 +62,5 @@ public interface KeyCrypter extends Serializable {
* @return encryptedPrivateKey An encryptedPrivateKey containing the encrypted bytes and an initialisation vector. * @return encryptedPrivateKey An encryptedPrivateKey containing the encrypted bytes and an initialisation vector.
* @throws KeyCrypterException if encryption was unsuccessful * @throws KeyCrypterException if encryption was unsuccessful
*/ */
public EncryptedPrivateKey encrypt(byte[] plainBytes, KeyParameter aesKey) throws KeyCrypterException; public EncryptedData encrypt(byte[] plainBytes, KeyParameter aesKey) throws KeyCrypterException;
} }

View File

@ -160,7 +160,7 @@ public class KeyCrypterScrypt implements KeyCrypter, Serializable {
* Password based encryption using AES - CBC 256 bits. * Password based encryption using AES - CBC 256 bits.
*/ */
@Override @Override
public EncryptedPrivateKey encrypt(byte[] plainBytes, KeyParameter aesKey) throws KeyCrypterException { public EncryptedData encrypt(byte[] plainBytes, KeyParameter aesKey) throws KeyCrypterException {
checkNotNull(plainBytes); checkNotNull(plainBytes);
checkNotNull(aesKey); checkNotNull(aesKey);
@ -178,7 +178,7 @@ public class KeyCrypterScrypt implements KeyCrypter, Serializable {
final int length1 = cipher.processBytes(plainBytes, 0, plainBytes.length, encryptedBytes, 0); final int length1 = cipher.processBytes(plainBytes, 0, plainBytes.length, encryptedBytes, 0);
final int length2 = cipher.doFinal(encryptedBytes, length1); final int length2 = cipher.doFinal(encryptedBytes, length1);
return new EncryptedPrivateKey(iv, Arrays.copyOf(encryptedBytes, length1 + length2)); return new EncryptedData(iv, Arrays.copyOf(encryptedBytes, length1 + length2));
} catch (Exception e) { } catch (Exception e) {
throw new KeyCrypterException("Could not encrypt bytes.", e); throw new KeyCrypterException("Could not encrypt bytes.", e);
} }
@ -193,18 +193,18 @@ public class KeyCrypterScrypt implements KeyCrypter, Serializable {
* @throws KeyCrypterException if bytes could not be decoded to a valid key * @throws KeyCrypterException if bytes could not be decoded to a valid key
*/ */
@Override @Override
public byte[] decrypt(EncryptedPrivateKey privateKeyToDecode, KeyParameter aesKey) throws KeyCrypterException { public byte[] decrypt(EncryptedData privateKeyToDecode, KeyParameter aesKey) throws KeyCrypterException {
checkNotNull(privateKeyToDecode); checkNotNull(privateKeyToDecode);
checkNotNull(aesKey); checkNotNull(aesKey);
try { try {
ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKey()), privateKeyToDecode.getInitialisationVector()); ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKey()), privateKeyToDecode.initialisationVector);
// Decrypt the message. // Decrypt the message.
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine()));
cipher.init(false, keyWithIv); cipher.init(false, keyWithIv);
byte[] cipherBytes = privateKeyToDecode.getEncryptedBytes(); byte[] cipherBytes = privateKeyToDecode.encryptedBytes;
byte[] decryptedBytes = new byte[cipher.getOutputSize(cipherBytes.length)]; byte[] decryptedBytes = new byte[cipher.getOutputSize(cipherBytes.length)];
final int length1 = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decryptedBytes, 0); final int length1 = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decryptedBytes, 0);
final int length2 = cipher.doFinal(decryptedBytes, length1); final int length2 = cipher.doFinal(decryptedBytes, length1);

View File

@ -49,7 +49,7 @@ public class NativeWalletEventListener implements WalletEventListener {
public native void onWalletChanged(Wallet wallet); public native void onWalletChanged(Wallet wallet);
@Override @Override
public native void onKeysAdded(Wallet wallet, List<ECKey> keys); public native void onKeysAdded(List<ECKey> keys);
@Override @Override
public native void onScriptsAdded(Wallet wallet, List<Script> scripts); public native void onScriptsAdded(Wallet wallet, List<Script> scripts);

View File

@ -22,10 +22,12 @@ import com.google.bitcoin.net.discovery.DnsDiscovery;
import com.google.bitcoin.store.BlockStoreException; import com.google.bitcoin.store.BlockStoreException;
import com.google.bitcoin.store.SPVBlockStore; import com.google.bitcoin.store.SPVBlockStore;
import com.google.bitcoin.store.WalletProtobufSerializer; import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.AbstractIdleService; import com.google.common.util.concurrent.AbstractIdleService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Service; import com.google.common.util.concurrent.Service;
import com.subgraph.orchid.TorClient; import com.subgraph.orchid.TorClient;
import org.bitcoinj.wallet.Protos;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -33,6 +35,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -168,12 +171,14 @@ public class WalletAppKit extends AbstractIdleService {
} }
/** /**
* <p>Override this to load all wallet extensions if any are necessary.</p> * <p>Override this to return wallet extensions if any are necessary.</p>
* *
* <p>When this is called, chain(), store(), and peerGroup() will return the created objects, however they are not * <p>When this is called, chain(), store(), and peerGroup() will return the created objects, however they are not
* initialized/started</p> * initialized/started.</p>
*/ */
protected void addWalletExtensions() throws Exception { } protected List<WalletExtension> provideWalletExtensions() throws Exception {
return ImmutableList.of();
}
/** /**
* This method is invoked on a background thread after all objects are initialised, but before the peer group * This method is invoked on a background thread after all objects are initialised, but before the peer group
@ -203,9 +208,9 @@ public class WalletAppKit extends AbstractIdleService {
// object. // object.
long time = Long.MAX_VALUE; long time = Long.MAX_VALUE;
if (vWalletFile.exists()) { if (vWalletFile.exists()) {
Wallet wallet = new Wallet(params);
FileInputStream stream = new FileInputStream(vWalletFile); FileInputStream stream = new FileInputStream(vWalletFile);
new WalletProtobufSerializer().readWallet(WalletProtobufSerializer.parseToProto(stream), wallet); final WalletProtobufSerializer serializer = new WalletProtobufSerializer();
final Wallet wallet = serializer.readWallet(params, null, WalletProtobufSerializer.parseToProto(stream));
time = wallet.getEarliestKeyCreationTime(); time = wallet.getEarliestKeyCreationTime();
} }
CheckpointManager.checkpoint(params, checkpoints, vStore, time); CheckpointManager.checkpoint(params, checkpoints, vStore, time);
@ -217,9 +222,10 @@ public class WalletAppKit extends AbstractIdleService {
if (vWalletFile.exists()) { if (vWalletFile.exists()) {
FileInputStream walletStream = new FileInputStream(vWalletFile); FileInputStream walletStream = new FileInputStream(vWalletFile);
try { try {
List<WalletExtension> extensions = provideWalletExtensions();
vWallet = new Wallet(params); vWallet = new Wallet(params);
addWalletExtensions(); // All extensions must be present before we deserialize Protos.Wallet proto = WalletProtobufSerializer.parseToProto(walletStream);
new WalletProtobufSerializer().readWallet(WalletProtobufSerializer.parseToProto(walletStream), vWallet); new WalletProtobufSerializer().readWallet(params, (WalletExtension[]) extensions.toArray(), proto);
if (shouldReplayWallet) if (shouldReplayWallet)
vWallet.clearTransactions(0); vWallet.clearTransactions(0);
} finally { } finally {
@ -227,13 +233,13 @@ public class WalletAppKit extends AbstractIdleService {
} }
} else { } else {
vWallet = new Wallet(params); vWallet = new Wallet(params);
vWallet.addKey(new ECKey()); vWallet.freshReceiveKey();
addWalletExtensions(); for (WalletExtension e : provideWalletExtensions()) {
vWallet.addExtension(e);
}
vWallet.saveToFile(vWalletFile); vWallet.saveToFile(vWalletFile);
} }
if (useAutoSave) { if (useAutoSave) vWallet.autosaveToFile(vWalletFile, 200, TimeUnit.MILLISECONDS, null);
vWallet.autosaveToFile(vWalletFile, 200, TimeUnit.MILLISECONDS, null);
}
// Set up peer addresses or discovery first, so if wallet extensions try to broadcast a transaction // Set up peer addresses or discovery first, so if wallet extensions try to broadcast a transaction
// before we're actually connected the broadcast waits for an appropriate number of connections. // before we're actually connected the broadcast waits for an appropriate number of connections.
if (peerAddresses != null) { if (peerAddresses != null) {

View File

@ -175,8 +175,10 @@ public class PaymentChannelClient implements IPaymentChannelClient {
return CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE; return CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE;
} }
state = new PaymentChannelClientState(wallet, myKey, final byte[] pubKeyBytes = initiate.getMultisigKey().toByteArray();
new ECKey(null, initiate.getMultisigKey().toByteArray()), if (!ECKey.isPubKeyCanonical(pubKeyBytes))
throw new VerificationException("Server gave us a non-canonical public key, protocol error.");
state = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes),
contractValue, initiate.getExpireTimeSecs()); contractValue, initiate.getExpireTimeSecs());
try { try {
state.initiate(); state.initiate();

View File

@ -161,8 +161,6 @@ public class PaymentChannelClientState {
this.wallet = checkNotNull(wallet); this.wallet = checkNotNull(wallet);
initWalletListeners(); initWalletListeners();
this.serverMultisigKey = checkNotNull(serverMultisigKey); this.serverMultisigKey = checkNotNull(serverMultisigKey);
if (!myKey.isPubKeyCanonical() || !serverMultisigKey.isPubKeyCanonical())
throw new VerificationException("Pubkey was not canonical (ie non-standard)");
this.myKey = checkNotNull(myKey); this.myKey = checkNotNull(myKey);
this.valueToMe = this.totalValue = checkNotNull(value); this.valueToMe = this.totalValue = checkNotNull(value);
this.expiryTime = expiryTimeInSeconds; this.expiryTime = expiryTimeInSeconds;

View File

@ -217,7 +217,7 @@ public class PaymentChannelServer {
minAcceptedChannelSize.longValue()); minAcceptedChannelSize.longValue());
myKey = new ECKey(); myKey = new ECKey();
wallet.addKey(myKey); wallet.freshReceiveKey();
expireTime = Utils.currentTimeSeconds() + timeWindow; expireTime = Utils.currentTimeSeconds() + timeWindow;
step = InitStep.WAITING_ON_UNSIGNED_REFUND; step = InitStep.WAITING_ON_UNSIGNED_REFUND;

View File

@ -119,7 +119,7 @@ public class PaymentChannelServerState {
this.broadcaster = checkNotNull(broadcaster); this.broadcaster = checkNotNull(broadcaster);
this.multisigContract = checkNotNull(storedServerChannel.contract); this.multisigContract = checkNotNull(storedServerChannel.contract);
this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); this.multisigScript = multisigContract.getOutput(0).getScriptPubKey();
this.clientKey = new ECKey(null, multisigScript.getChunks().get(1).data); this.clientKey = ECKey.fromPublicOnly(multisigScript.getChunks().get(1).data);
this.clientOutput = checkNotNull(storedServerChannel.clientOutput); this.clientOutput = checkNotNull(storedServerChannel.clientOutput);
this.refundTransactionUnlockTimeSecs = storedServerChannel.refundTransactionUnlockTimeSecs; this.refundTransactionUnlockTimeSecs = storedServerChannel.refundTransactionUnlockTimeSecs;
this.serverKey = checkNotNull(storedServerChannel.myKey); this.serverKey = checkNotNull(storedServerChannel.myKey);
@ -192,7 +192,7 @@ public class PaymentChannelServerState {
// Sign the refund tx with the scriptPubKey and return the signature. We don't have the spending transaction // Sign the refund tx with the scriptPubKey and return the signature. We don't have the spending transaction
// so do the steps individually. // so do the steps individually.
clientKey = new ECKey(null, clientMultiSigPubKey); clientKey = ECKey.fromPublicOnly(clientMultiSigPubKey);
Script multisigPubKey = ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.of(clientKey, serverKey)); Script multisigPubKey = ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.of(clientKey, serverKey));
// We are really only signing the fact that the transaction has a proper lock time and don't care about anything // We are really only signing the fact that the transaction has a proper lock time and don't care about anything
// else, so we sign SIGHASH_NONE and SIGHASH_ANYONECANPAY. // else, so we sign SIGHASH_NONE and SIGHASH_ANYONECANPAY.

View File

@ -57,9 +57,9 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
* {@link TransactionBroadcaster} which are used to complete and announce contract and refund * {@link TransactionBroadcaster} which are used to complete and announce contract and refund
* transactions. * transactions.
*/ */
public StoredPaymentChannelClientStates(Wallet containingWallet, TransactionBroadcaster announcePeerGroup) { public StoredPaymentChannelClientStates(@Nullable Wallet containingWallet, TransactionBroadcaster announcePeerGroup) {
this.announcePeerGroup = checkNotNull(announcePeerGroup); this.announcePeerGroup = checkNotNull(announcePeerGroup);
this.containingWallet = checkNotNull(containingWallet); this.containingWallet = containingWallet;
} }
/** Returns this extension from the given wallet, or null if no such extension was added. */ /** Returns this extension from the given wallet, or null if no such extension was added. */
@ -255,7 +255,7 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
StoredClientChannel channel = new StoredClientChannel(new Sha256Hash(storedState.getId().toByteArray()), StoredClientChannel channel = new StoredClientChannel(new Sha256Hash(storedState.getId().toByteArray()),
new Transaction(params, storedState.getContractTransaction().toByteArray()), new Transaction(params, storedState.getContractTransaction().toByteArray()),
refundTransaction, refundTransaction,
new ECKey(new BigInteger(1, storedState.getMyKey().toByteArray()), null, true), ECKey.fromPrivate(storedState.getMyKey().toByteArray()),
BigInteger.valueOf(storedState.getValueToMe()), BigInteger.valueOf(storedState.getValueToMe()),
BigInteger.valueOf(storedState.getRefundFees()), false); BigInteger.valueOf(storedState.getRefundFees()), false);
if (storedState.hasCloseTransactionHash()) if (storedState.hasCloseTransactionHash())

View File

@ -23,6 +23,7 @@ import com.google.protobuf.ByteString;
import net.jcip.annotations.GuardedBy; import net.jcip.annotations.GuardedBy;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.*; import java.util.*;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@ -39,7 +40,7 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
static final String EXTENSION_ID = StoredPaymentChannelServerStates.class.getName(); static final String EXTENSION_ID = StoredPaymentChannelServerStates.class.getName();
@GuardedBy("lock") @VisibleForTesting final Map<Sha256Hash, StoredServerChannel> mapChannels = new HashMap<Sha256Hash, StoredServerChannel>(); @GuardedBy("lock") @VisibleForTesting final Map<Sha256Hash, StoredServerChannel> mapChannels = new HashMap<Sha256Hash, StoredServerChannel>();
private final Wallet wallet; private Wallet wallet;
private final TransactionBroadcaster broadcaster; private final TransactionBroadcaster broadcaster;
private final Timer channelTimeoutHandler = new Timer(true); private final Timer channelTimeoutHandler = new Timer(true);
@ -59,8 +60,8 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
* Creates a new PaymentChannelServerStateManager and associates it with the given {@link Wallet} and * Creates a new PaymentChannelServerStateManager and associates it with the given {@link Wallet} and
* {@link TransactionBroadcaster} which are used to complete and announce payment transactions. * {@link TransactionBroadcaster} which are used to complete and announce payment transactions.
*/ */
public StoredPaymentChannelServerStates(Wallet wallet, TransactionBroadcaster broadcaster) { public StoredPaymentChannelServerStates(@Nullable Wallet wallet, TransactionBroadcaster broadcaster) {
this.wallet = checkNotNull(wallet); this.wallet = wallet;
this.broadcaster = checkNotNull(broadcaster); this.broadcaster = checkNotNull(broadcaster);
} }
@ -173,7 +174,7 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
public void deserializeWalletExtension(Wallet containingWallet, byte[] data) throws Exception { public void deserializeWalletExtension(Wallet containingWallet, byte[] data) throws Exception {
lock.lock(); lock.lock();
try { try {
checkArgument(containingWallet == wallet); this.wallet = containingWallet;
ServerState.StoredServerPaymentChannels states = ServerState.StoredServerPaymentChannels.parseFrom(data); ServerState.StoredServerPaymentChannels states = ServerState.StoredServerPaymentChannels.parseFrom(data);
NetworkParameters params = containingWallet.getParams(); NetworkParameters params = containingWallet.getParams();
for (ServerState.StoredServerPaymentChannel storedState : states.getChannelsList()) { for (ServerState.StoredServerPaymentChannel storedState : states.getChannelsList()) {
@ -181,7 +182,7 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
new Transaction(params, storedState.getContractTransaction().toByteArray()), new Transaction(params, storedState.getContractTransaction().toByteArray()),
new TransactionOutput(params, null, storedState.getClientOutput().toByteArray(), 0), new TransactionOutput(params, null, storedState.getClientOutput().toByteArray(), 0),
storedState.getRefundTransactionUnlockTimeSecs(), storedState.getRefundTransactionUnlockTimeSecs(),
new ECKey(storedState.getMyKey().toByteArray(), null), ECKey.fromPrivate(storedState.getMyKey().toByteArray()),
BigInteger.valueOf(storedState.getBestValueToMe()), BigInteger.valueOf(storedState.getBestValueToMe()),
storedState.hasBestValueSignature() ? storedState.getBestValueSignature().toByteArray() : null); storedState.hasBestValueSignature() ? storedState.getBestValueSignature().toByteArray() : null);
putChannel(channel); putChannel(channel);

View File

@ -12,4 +12,10 @@ public class UnreadableWalletException extends Exception {
public UnreadableWalletException(String s, Throwable t) { public UnreadableWalletException(String s, Throwable t) {
super(s, t); super(s, t);
} }
public static class BadPassword extends UnreadableWalletException {
public BadPassword() {
super("Password incorrect");
}
}
} }

View File

@ -19,10 +19,10 @@ package com.google.bitcoin.store;
import com.google.bitcoin.core.*; import com.google.bitcoin.core.*;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.crypto.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterScrypt; import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.script.Script; import com.google.bitcoin.script.Script;
import com.google.bitcoin.wallet.KeyChainGroup;
import com.google.bitcoin.wallet.WalletTransaction; import com.google.bitcoin.wallet.WalletTransaction;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
@ -35,17 +35,14 @@ import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Date; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -55,7 +52,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
* a data interchange format developed by Google with an efficient binary representation, a type safe specification * a data interchange format developed by Google with an efficient binary representation, a type safe specification
* language and compilers that generate code to work with those data structures for many languages. Protocol buffers * language and compilers that generate code to work with those data structures for many languages. Protocol buffers
* can have their format evolved over time: conceptually they represent data using (tag, length, value) tuples. The * can have their format evolved over time: conceptually they represent data using (tag, length, value) tuples. The
* format is defined by the <tt>bitcoin.proto</tt> file in the bitcoinj source distribution.<p> * format is defined by the <tt>wallet.proto</tt> file in the bitcoinj source distribution.<p>
* *
* This class is used through its static methods. The most common operations are writeWallet and readWallet, which do * This class is used through its static methods. The most common operations are writeWallet and readWallet, which do
* the obvious operations on Output/InputStreams. You can use a {@link java.io.ByteArrayInputStream} and equivalent * the obvious operations on Output/InputStreams. You can use a {@link java.io.ByteArrayInputStream} and equivalent
@ -128,40 +125,7 @@ public class WalletProtobufSerializer {
walletBuilder.addTransaction(txProto); walletBuilder.addTransaction(txProto);
} }
for (ECKey key : wallet.getKeys()) { walletBuilder.addAllKey(wallet.serializeKeychainToProtobuf());
Protos.Key.Builder keyBuilder = Protos.Key.newBuilder().setCreationTimestamp(key.getCreationTimeSeconds() * 1000)
// .setLabel() TODO
.setType(Protos.Key.Type.ORIGINAL);
if (key.getPrivKeyBytes() != null)
keyBuilder.setPrivateKey(ByteString.copyFrom(key.getPrivKeyBytes()));
EncryptedPrivateKey encryptedPrivateKey = key.getEncryptedPrivateKey();
if (encryptedPrivateKey != null) {
// Key is encrypted.
Protos.EncryptedPrivateKey.Builder encryptedKeyBuilder = Protos.EncryptedPrivateKey.newBuilder()
.setEncryptedPrivateKey(ByteString.copyFrom(encryptedPrivateKey.getEncryptedBytes()))
.setInitialisationVector(ByteString.copyFrom(encryptedPrivateKey.getInitialisationVector()));
if (key.getKeyCrypter() == null) {
throw new IllegalStateException("The encrypted key " + key.toString() + " has no KeyCrypter.");
} else {
// If it is a Scrypt + AES encrypted key, set the persisted key type.
if (key.getKeyCrypter().getUnderstoodEncryptionType() == Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES) {
keyBuilder.setType(Protos.Key.Type.ENCRYPTED_SCRYPT_AES);
} else {
throw new IllegalArgumentException("The key " + key.toString() + " is encrypted with a KeyCrypter of type " + key.getKeyCrypter().getUnderstoodEncryptionType() +
". This WalletProtobufSerialiser does not understand that type of encryption.");
}
}
keyBuilder.setEncryptedPrivateKey(encryptedKeyBuilder);
}
// We serialize the public key even if the private key is present for speed reasons: we don't want to do
// lots of slow EC math to load the wallet, we prefer to store the redundant data instead. It matters more
// on mobile platforms.
keyBuilder.setPublicKey(ByteString.copyFrom(key.getPubKey()));
walletBuilder.addKey(keyBuilder);
}
for (Script script : wallet.getWatchedScripts()) { for (Script script : wallet.getWatchedScripts()) {
Protos.Script protoScript = Protos.Script protoScript =
@ -379,9 +343,7 @@ public class WalletProtobufSerializer {
NetworkParameters params = NetworkParameters.fromID(paramsID); NetworkParameters params = NetworkParameters.fromID(paramsID);
if (params == null) if (params == null)
throw new UnreadableWalletException("Unknown network parameters ID " + paramsID); throw new UnreadableWalletException("Unknown network parameters ID " + paramsID);
Wallet wallet = new Wallet(params); return readWallet(params, null, walletProto);
readWallet(walletProto, wallet);
return wallet;
} catch (IOException e) { } catch (IOException e) {
throw new UnreadableWalletException("Could not parse input stream to protobuf", e); throw new UnreadableWalletException("Could not parse input stream to protobuf", e);
} }
@ -398,45 +360,18 @@ public class WalletProtobufSerializer {
* *
* @throws UnreadableWalletException thrown in various error conditions (see description). * @throws UnreadableWalletException thrown in various error conditions (see description).
*/ */
public void readWallet(Protos.Wallet walletProto, Wallet wallet) throws UnreadableWalletException { public Wallet readWallet(NetworkParameters params, @Nullable WalletExtension[] extensions,
Protos.Wallet walletProto) throws UnreadableWalletException {
// Read the scrypt parameters that specify how encryption and decryption is performed. // Read the scrypt parameters that specify how encryption and decryption is performed.
KeyChainGroup chain;
if (walletProto.hasEncryptionParameters()) { if (walletProto.hasEncryptionParameters()) {
Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters(); Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
wallet.setKeyCrypter(new KeyCrypterScrypt(encryptionParameters)); final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(encryptionParameters);
} chain = KeyChainGroup.fromProtobufEncrypted(walletProto.getKeyList(), keyCrypter);
} else {
if (walletProto.hasDescription()) { chain = KeyChainGroup.fromProtobufUnencrypted(walletProto.getKeyList());
wallet.setDescription(walletProto.getDescription());
}
// Read all keys
for (Protos.Key keyProto : walletProto.getKeyList()) {
if (!(keyProto.getType() == Protos.Key.Type.ORIGINAL || keyProto.getType() == Protos.Key.Type.ENCRYPTED_SCRYPT_AES)) {
throw new UnreadableWalletException("Unknown key type in wallet, type = " + keyProto.getType());
}
byte[] privKey = keyProto.hasPrivateKey() ? keyProto.getPrivateKey().toByteArray() : null;
EncryptedPrivateKey encryptedPrivateKey = null;
if (keyProto.hasEncryptedPrivateKey()) {
Protos.EncryptedPrivateKey encryptedPrivateKeyProto = keyProto.getEncryptedPrivateKey();
encryptedPrivateKey = new EncryptedPrivateKey(encryptedPrivateKeyProto.getInitialisationVector().toByteArray(),
encryptedPrivateKeyProto.getEncryptedPrivateKey().toByteArray());
}
byte[] pubKey = keyProto.hasPublicKey() ? keyProto.getPublicKey().toByteArray() : null;
ECKey ecKey;
final KeyCrypter keyCrypter = wallet.getKeyCrypter();
if (keyCrypter != null && keyCrypter.getUnderstoodEncryptionType() != EncryptionType.UNENCRYPTED) {
// If the key is encrypted construct an ECKey using the encrypted private key bytes.
ecKey = new ECKey(encryptedPrivateKey, pubKey, keyCrypter);
} else {
// Construct an unencrypted private key.
ecKey = new ECKey(privKey, pubKey);
}
ecKey.setCreationTimeSeconds((keyProto.getCreationTimestamp() + 500) / 1000);
wallet.addKey(ecKey);
} }
Wallet wallet = new Wallet(params, chain);
List<Script> scripts = Lists.newArrayList(); List<Script> scripts = Lists.newArrayList();
for (Protos.Script protoScript : walletProto.getWatchedScriptList()) { for (Protos.Script protoScript : walletProto.getWatchedScriptList()) {
@ -452,6 +387,10 @@ public class WalletProtobufSerializer {
wallet.addWatchedScripts(scripts); wallet.addWatchedScripts(scripts);
if (walletProto.hasDescription()) {
wallet.setDescription(walletProto.getDescription());
}
// Read all transactions and insert into the txMap. // Read all transactions and insert into the txMap.
for (Protos.Transaction txProto : walletProto.getTransactionList()) { for (Protos.Transaction txProto : walletProto.getTransactionList()) {
readTransaction(txProto, wallet.getParams()); readTransaction(txProto, wallet.getParams());
@ -481,7 +420,7 @@ public class WalletProtobufSerializer {
wallet.setKeyRotationTime(new Date(walletProto.getKeyRotationTime() * 1000)); wallet.setKeyRotationTime(new Date(walletProto.getKeyRotationTime() * 1000));
} }
loadExtensions(wallet, walletProto); loadExtensions(wallet, extensions != null ? extensions : new WalletExtension[0], walletProto);
for (Protos.Tag tag : walletProto.getTagsList()) { for (Protos.Tag tag : walletProto.getTagsList()) {
wallet.setTag(tag.getTag(), tag.getData()); wallet.setTag(tag.getTag(), tag.getData());
@ -493,10 +432,14 @@ public class WalletProtobufSerializer {
// Make sure the object can be re-used to read another wallet without corruption. // Make sure the object can be re-used to read another wallet without corruption.
txMap.clear(); txMap.clear();
return wallet;
} }
private void loadExtensions(Wallet wallet, Protos.Wallet walletProto) throws UnreadableWalletException { private void loadExtensions(Wallet wallet, WalletExtension[] extensionsList, Protos.Wallet walletProto) throws UnreadableWalletException {
final Map<String, WalletExtension> extensions = wallet.getExtensions(); final Map<String, WalletExtension> extensions = new HashMap<String, WalletExtension>();
for (WalletExtension e : extensionsList)
extensions.put(e.getWalletExtensionID(), e);
for (Protos.Extension extProto : walletProto.getExtensionList()) { for (Protos.Extension extProto : walletProto.getExtensionList()) {
String id = extProto.getId(); String id = extProto.getId();
WalletExtension extension = extensions.get(id); WalletExtension extension = extensions.get(id);
@ -511,6 +454,7 @@ public class WalletProtobufSerializer {
log.info("Loading wallet extension {}", id); log.info("Loading wallet extension {}", id);
try { try {
extension.deserializeWalletExtension(wallet, extProto.getData().toByteArray()); extension.deserializeWalletExtension(wallet, extProto.getData().toByteArray());
wallet.addExtension(extension);
} catch (Exception e) { } catch (Exception e) {
if (extProto.getMandatory() && requireMandatoryExtensions) if (extProto.getMandatory() && requireMandatoryExtensions)
throw new UnreadableWalletException("Could not parse mandatory extension in wallet: " + id); throw new UnreadableWalletException("Could not parse mandatory extension in wallet: " + id);

View File

@ -85,9 +85,9 @@ public class TestWithNetworkConnections {
Wallet.SendRequest.DEFAULT_FEE_PER_KB = BigInteger.ZERO; Wallet.SendRequest.DEFAULT_FEE_PER_KB = BigInteger.ZERO;
this.blockStore = blockStore; this.blockStore = blockStore;
wallet = new Wallet(unitTestParams); wallet = new Wallet(unitTestParams);
key = new ECKey(); wallet.setKeychainLookaheadSize(5); // Make tests faster by reducing the number of keys derived ahead of time.
key = wallet.freshReceiveKey();
address = key.toAddress(unitTestParams); address = key.toAddress(unitTestParams);
wallet.addKey(key);
blockChain = new BlockChain(unitTestParams, wallet, blockStore); blockChain = new BlockChain(unitTestParams, wallet, blockStore);
startPeerServers(); startPeerServers();

View File

@ -54,7 +54,8 @@ public class TestWithWallet {
myKey = new ECKey(); myKey = new ECKey();
myAddress = myKey.toAddress(params); myAddress = myKey.toAddress(params);
wallet = new Wallet(params); wallet = new Wallet(params);
wallet.addKey(myKey); wallet.setKeychainLookaheadSize(5);
wallet.importKey(myKey);
blockStore = new MemoryBlockStore(params); blockStore = new MemoryBlockStore(params);
chain = new BlockChain(params, wallet, blockStore); chain = new BlockChain(params, wallet, blockStore);
} }

View File

@ -0,0 +1,11 @@
package com.google.bitcoin.wallet;
import com.google.bitcoin.core.ECKey;
import java.util.List;
public class AbstractKeyChainEventListener implements KeyChainEventListener {
@Override
public void onKeysAdded(List<ECKey> keys) {
}
}

View File

@ -0,0 +1,498 @@
/**
* Copyright 2013 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.wallet;
import com.google.bitcoin.core.BloomFilter;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.crypto.*;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.utils.ListenerRegistration;
import com.google.bitcoin.utils.Threading;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.*;
/**
* A {@link KeyChain} that implements the simplest model possible: it can have keys imported into it, and just acts as
* a dumb bag of keys. It will, left to its own devices, always return the same key for usage by the wallet, although
* it will automatically add one to itself if it's empty or if encryption is requested.
*/
public class BasicKeyChain implements EncryptableKeyChain {
private final ReentrantLock lock = Threading.lock("BasicKeyChain");
// Maps used to let us quickly look up a key given data we find in transcations or the block chain.
private final LinkedHashMap<ByteString, ECKey> hashToKeys;
private final LinkedHashMap<ByteString, ECKey> pubkeyToKeys;
@Nullable private final KeyCrypter keyCrypter;
private final CopyOnWriteArrayList<ListenerRegistration<KeyChainEventListener>> listeners;
public BasicKeyChain() {
this(null);
}
BasicKeyChain(@Nullable KeyCrypter crypter) {
this.keyCrypter = crypter;
hashToKeys = new LinkedHashMap<ByteString, ECKey>();
pubkeyToKeys = new LinkedHashMap<ByteString, ECKey>();
listeners = new CopyOnWriteArrayList<ListenerRegistration<KeyChainEventListener>>();
}
/** Returns the {@link KeyCrypter} in use or null if the key chain is not encrypted. */
@Nullable
public KeyCrypter getKeyCrypter() {
lock.lock();
try {
return keyCrypter;
} finally {
lock.unlock();
}
}
@Override
public ECKey getKey(@Nullable KeyPurpose ignored) {
lock.lock();
try {
if (hashToKeys.isEmpty()) {
checkState(keyCrypter == null); // We will refuse to encrypt an empty key chain.
final ECKey key = new ECKey();
importKeyLocked(key);
queueOnKeysAdded(ImmutableList.of(key));
}
return hashToKeys.values().iterator().next();
} finally {
lock.unlock();
}
}
/** Returns a copy of the list of keys that this chain is managing. */
public List<ECKey> getKeys() {
lock.lock();
try {
return new ArrayList<ECKey>(hashToKeys.values());
} finally {
lock.unlock();
}
}
public int importKeys(ECKey... keys) {
return importKeys(ImmutableList.copyOf(keys));
}
public int importKeys(List<? extends ECKey> keys) {
lock.lock();
try {
// Check that if we're encrypted, the keys are all encrypted, and if we're not, that none are.
// We are NOT checking that the actual password matches here because we don't have access to the password at
// this point: if you screw up and import keys with mismatched passwords, you lose! So make sure the
// password is checked first.
for (ECKey key : keys) {
checkKeyEncryptionStateMatches(key);
}
List<ECKey> actuallyAdded = new ArrayList<ECKey>(keys.size());
for (final ECKey key : keys) {
if (hasKey(key)) continue;
actuallyAdded.add(key);
importKeyLocked(key);
}
if (actuallyAdded.size() > 0)
queueOnKeysAdded(actuallyAdded);
return actuallyAdded.size();
} finally {
lock.unlock();
}
}
private void checkKeyEncryptionStateMatches(ECKey key) {
if (keyCrypter == null && key.isEncrypted())
throw new KeyCrypterException("Key is encrypted but chain is not");
else if (keyCrypter != null && !key.isEncrypted())
throw new KeyCrypterException("Key is not encrypted but chain is");
else if (keyCrypter != null && key.getKeyCrypter() != null && !key.getKeyCrypter().equals(keyCrypter))
throw new KeyCrypterException("Key encrypted under different parameters to chain");
}
private void importKeyLocked(ECKey key) {
pubkeyToKeys.put(ByteString.copyFrom(key.getPubKey()), key);
hashToKeys.put(ByteString.copyFrom(key.getPubKeyHash()), key);
}
/* package */ void importKey(ECKey key) {
lock.lock();
try {
importKeyLocked(key);
queueOnKeysAdded(ImmutableList.of(key));
} finally {
lock.unlock();
}
}
@Override
public ECKey findKeyFromPubHash(byte[] pubkeyHash) {
lock.lock();
try {
return hashToKeys.get(ByteString.copyFrom(pubkeyHash));
} finally {
lock.unlock();
}
}
@Override
public ECKey findKeyFromPubKey(byte[] pubkey) {
lock.lock();
try {
return pubkeyToKeys.get(ByteString.copyFrom(pubkey));
} finally {
lock.unlock();
}
}
@Override
public boolean hasKey(ECKey key) {
return findKeyFromPubKey(key.getPubKey()) != null;
}
@Override
public int numKeys() {
return pubkeyToKeys.size();
}
/**
* Removes the given key from the keychain. Be very careful with this - losing a private key <b>destroys the
* money associated with it</b>.
* @return Whether the key was removed or not.
*/
public boolean removeKey(ECKey key) {
lock.lock();
try {
boolean a = hashToKeys.remove(ByteString.copyFrom(key.getPubKeyHash())) != null;
boolean b = pubkeyToKeys.remove(ByteString.copyFrom(key.getPubKey())) != null;
checkState(a == b); // Should be in both maps or neither.
return a;
} finally {
lock.unlock();
}
}
@Override
public long getEarliestKeyCreationTime() {
lock.lock();
try {
long time = Long.MAX_VALUE;
for (ECKey key : hashToKeys.values())
time = Math.min(key.getCreationTimeSeconds(), time);
return time;
} finally {
lock.unlock();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Serialization support
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Map<ECKey, Protos.Key.Builder> serializeToEditableProtobufs() {
Map<ECKey, Protos.Key.Builder> result = new LinkedHashMap<ECKey, Protos.Key.Builder>();
for (ECKey ecKey : hashToKeys.values()) {
Protos.Key.Builder protoKey = serializeEncryptableItem(ecKey);
protoKey.setPublicKey(ByteString.copyFrom(ecKey.getPubKey()));
result.put(ecKey, protoKey);
}
return result;
}
@Override
public List<Protos.Key> serializeToProtobuf() {
Collection<Protos.Key.Builder> builders = serializeToEditableProtobufs().values();
List<Protos.Key> result = new ArrayList<Protos.Key>(builders.size());
for (Protos.Key.Builder builder : builders) result.add(builder.build());
return result;
}
/*package*/ static Protos.Key.Builder serializeEncryptableItem(EncryptableItem item) {
Protos.Key.Builder proto = Protos.Key.newBuilder();
proto.setCreationTimestamp(item.getCreationTimeSeconds() * 1000);
if (item.isEncrypted() && item.getEncryptedData() != null) {
// The encrypted data can be missing for an "encrypted" key in the case of a deterministic wallet for
// which the leaf keys chain to an encrypted parent and rederive their private keys on the fly. In that
// case the caller in DeterministicKeyChain will take care of setting the type.
EncryptedData data = item.getEncryptedData();
proto.getEncryptedDataBuilder()
.setEncryptedPrivateKey(ByteString.copyFrom(data.encryptedBytes))
.setInitialisationVector(ByteString.copyFrom(data.initialisationVector));
// We don't allow mixing of encryption types at the moment.
checkState(item.getEncryptionType() == Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES);
proto.setType(Protos.Key.Type.ENCRYPTED_SCRYPT_AES);
} else {
final byte[] secret = item.getSecretBytes();
// The secret might be missing in the case of a watching wallet, or a key for which the private key
// is expected to be rederived on the fly from its parent.
if (secret != null)
proto.setSecretBytes(ByteString.copyFrom(secret));
proto.setType(Protos.Key.Type.ORIGINAL);
}
return proto;
}
/**
* Returns a new BasicKeyChain that contains all basic, ORIGINAL type keys extracted from the list. Unrecognised
* key types are ignored.
*/
public static BasicKeyChain fromProtobufUnencrypted(List<Protos.Key> keys) throws UnreadableWalletException {
BasicKeyChain chain = new BasicKeyChain();
chain.deserializeFromProtobuf(keys);
return chain;
}
/**
* Returns a new BasicKeyChain that contains all basic, ORIGINAL type keys and also any encrypted keys extracted
* from the list. Unrecognised key types are ignored.
* @throws com.google.bitcoin.store.UnreadableWalletException.BadPassword if the password doesn't seem to match
* @throws com.google.bitcoin.store.UnreadableWalletException if the data structures are corrupted/inconsistent
*/
public static BasicKeyChain fromProtobufEncrypted(List<Protos.Key> keys, KeyCrypter crypter) throws UnreadableWalletException {
BasicKeyChain chain = new BasicKeyChain(checkNotNull(crypter));
chain.deserializeFromProtobuf(keys);
return chain;
}
private void deserializeFromProtobuf(List<Protos.Key> keys) throws UnreadableWalletException {
lock.lock();
try {
checkState(hashToKeys.isEmpty(), "Tried to deserialize into a non-empty chain");
for (Protos.Key key : keys) {
if (key.getType() != Protos.Key.Type.ORIGINAL && key.getType() != Protos.Key.Type.ENCRYPTED_SCRYPT_AES)
continue;
boolean encrypted = key.getType() == Protos.Key.Type.ENCRYPTED_SCRYPT_AES;
byte[] priv = key.hasSecretBytes() ? key.getSecretBytes().toByteArray() : null;
if (!key.hasPublicKey())
throw new UnreadableWalletException("Public key missing");
byte[] pub = key.getPublicKey().toByteArray();
ECKey ecKey;
if (encrypted) {
checkState(keyCrypter != null, "This wallet is encrypted but encrypt() was not called prior to deserialization");
if (!key.hasEncryptedData())
throw new UnreadableWalletException("Encrypted private key data missing");
Protos.EncryptedData proto = key.getEncryptedData();
EncryptedData e = new EncryptedData(proto.getInitialisationVector().toByteArray(),
proto.getEncryptedPrivateKey().toByteArray());
ecKey = ECKey.fromEncrypted(e, keyCrypter, pub);
} else {
if (priv != null)
ecKey = ECKey.fromPrivateAndPrecalculatedPublic(priv, pub);
else
ecKey = ECKey.fromPublicOnly(pub);
}
ecKey.setCreationTimeSeconds((key.getCreationTimestamp() + 500) / 1000);
importKeyLocked(ecKey);
}
} finally {
lock.unlock();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Event listener support
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addEventListener(KeyChainEventListener listener) {
addEventListener(listener, Threading.USER_THREAD);
}
@Override
public void addEventListener(KeyChainEventListener listener, Executor executor) {
listeners.add(new ListenerRegistration<KeyChainEventListener>(listener, executor));
}
@Override
public boolean removeEventListener(KeyChainEventListener listener) {
return ListenerRegistration.removeFromList(listener, listeners);
}
private void queueOnKeysAdded(final List<ECKey> keys) {
checkState(lock.isHeldByCurrentThread());
for (final ListenerRegistration<KeyChainEventListener> registration : listeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onKeysAdded(keys);
}
});
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Encryption support
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Convenience wrapper around {@link #toEncrypted(com.google.bitcoin.crypto.KeyCrypter,
* org.spongycastle.crypto.params.KeyParameter)} which uses the default Scrypt key derivation algorithm and
* parameters, derives a key from the given password and returns the created key.
*/
@Override
public BasicKeyChain toEncrypted(CharSequence password) {
checkNotNull(password);
checkArgument(password.length() > 0);
KeyCrypter scrypt = new KeyCrypterScrypt();
KeyParameter derivedKey = scrypt.deriveKey(password);
return toEncrypted(scrypt, derivedKey);
}
/**
* Encrypt the wallet using the KeyCrypter and the AES key. A good default KeyCrypter to use is
* {@link com.google.bitcoin.crypto.KeyCrypterScrypt}.
*
* @param keyCrypter The KeyCrypter that specifies how to encrypt/ decrypt a key
* @param aesKey AES key to use (normally created using KeyCrypter#deriveKey and cached as it is time consuming
* to create from a password)
* @throws KeyCrypterException Thrown if the wallet encryption fails. If so, the wallet state is unchanged.
*/
@Override
public BasicKeyChain toEncrypted(KeyCrypter keyCrypter, KeyParameter aesKey) {
lock.lock();
try {
checkNotNull(keyCrypter);
checkState(this.keyCrypter == null, "Key chain is already encrypted");
BasicKeyChain encrypted = new BasicKeyChain(keyCrypter);
for (ECKey key : hashToKeys.values()) {
ECKey encryptedKey = key.encrypt(keyCrypter, aesKey);
// Check that the encrypted key can be successfully decrypted.
// This is done as it is a critical failure if the private key cannot be decrypted successfully
// (all bitcoin controlled by that private key is lost forever).
// For a correctly constructed keyCrypter the encryption should always be reversible so it is just
// being as cautious as possible.
if (!ECKey.encryptionIsReversible(key, encryptedKey, keyCrypter, aesKey))
throw new KeyCrypterException("The key " + key.toString() + " cannot be successfully decrypted after encryption so aborting wallet encryption.");
encrypted.importKeyLocked(encryptedKey);
}
return encrypted;
} finally {
lock.unlock();
}
}
@Override
public BasicKeyChain toDecrypted(CharSequence password) {
checkNotNull(keyCrypter, "Wallet is already decrypted");
KeyParameter aesKey = keyCrypter.deriveKey(password);
return toDecrypted(aesKey);
}
@Override
public BasicKeyChain toDecrypted(KeyParameter aesKey) {
lock.lock();
try {
checkState(keyCrypter != null, "Wallet is already decrypted");
// Do an up-front check.
if (numKeys() > 0 && !checkAESKey(aesKey))
throw new KeyCrypterException("Password/key was incorrect.");
BasicKeyChain decrypted = new BasicKeyChain();
for (ECKey key : hashToKeys.values()) {
decrypted.importKeyLocked(key.decrypt(keyCrypter, aesKey));
}
return decrypted;
} finally {
lock.unlock();
}
}
/**
* Returns whether the given password is correct for this key chain.
* @throws IllegalStateException if the chain is not encrypted at all.
*/
@Override
public boolean checkPassword(CharSequence password) {
checkNotNull(password);
checkState(keyCrypter != null, "Key chain not encrypted");
return checkAESKey(keyCrypter.deriveKey(password));
}
/**
* Check whether the AES key can decrypt the first encrypted key in the wallet.
*
* @return true if AES key supplied can decrypt the first encrypted private key in the wallet, false otherwise.
*/
@Override
public boolean checkAESKey(KeyParameter aesKey) {
lock.lock();
try {
// If no keys then cannot decrypt.
if (hashToKeys.isEmpty()) return false;
checkState(keyCrypter != null, "Key chain is not encrypted");
// Find the first encrypted key in the wallet.
ECKey first = null;
for (ECKey key : hashToKeys.values()) {
if (key.isEncrypted()) {
first = key;
break;
}
}
checkState(first != null, "No encrypted keys in the wallet");
try {
ECKey rebornKey = first.decrypt(keyCrypter, aesKey);
return Arrays.equals(first.getPubKey(), rebornKey.getPubKey());
} catch (KeyCrypterException e) {
// The AES key supplied is incorrect.
return false;
}
} finally {
lock.unlock();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Bloom filtering support
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public BloomFilter getFilter(int size, double falsePositiveRate, long tweak) {
BloomFilter filter = new BloomFilter(size, falsePositiveRate, tweak);
for (ECKey key : hashToKeys.values()) {
filter.insert(key.getPubKey());
filter.insert(key.getPubKeyHash());
}
return filter;
}
@Override
public int numBloomFilterEntries() {
return numKeys() * 2;
}
}

View File

@ -0,0 +1,690 @@
/**
* Copyright 2013 The bitcoinj developers.
*
* 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.wallet;
import com.google.bitcoin.core.BloomFilter;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.crypto.*;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.utils.Threading;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.math.ec.ECPoint;
import javax.annotation.Nullable;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.*;
import static com.google.common.collect.Lists.newLinkedList;
/**
* <p>A deterministic key chain is a {@link KeyChain} that uses the
* <a href="https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki">BIP 32 standard</a>, as implemented by
* {@link com.google.bitcoin.crypto.DeterministicHierarchy}, to derive all the keys in the keychain from a master seed.
* This type of wallet is extremely convenient and flexible. Although backing up full wallet files is always a good
* idea, to recover money only the root seed needs to be preserved and that is a number small enough that it can be
* written down on paper or, when represented using a BIP 39 {@link com.google.bitcoin.crypto.MnemonicCode},
* dictated over the phone (possibly even memorized).</p>
*
* <p>Deterministic key chains have other advantages: parts of the key tree can be selectively revealed to allow
* for auditing, and new public keys can be generated without access to the private keys, yielding a highly secure
* configuration for web servers which can accept payments into a wallet but not spend from them. This does not work
* quite how you would expect due to a quirk of elliptic curve mathematics and the techniques used to deal with it.
* A watching wallet is not instantiated using the public part of the master key as you may imagine. Instead, you
* need to take the account key (first child of the master key) and provide the public part of that to the watching
* wallet instead. You can do this by calling {@link #getWatchingKey()} and then serializing it with
* {@link com.google.bitcoin.crypto.DeterministicKey#serializePubB58()}. The resulting "xpub..." string encodes
* sufficient information about the account key to create a watching chain via
* {@link com.google.bitcoin.crypto.DeterministicKey#deserializeB58(com.google.bitcoin.crypto.DeterministicKey, String)}
* (with null as the first parameter) and then {@link #watch(com.google.bitcoin.crypto.DeterministicKey)}.</p>
*
* <p>This class builds on {@link com.google.bitcoin.crypto.DeterministicHierarchy} and
* {@link com.google.bitcoin.crypto.DeterministicKey} by adding support for serialization to and from protobufs,
* and encryption of parts of the key tree. Internally it arranges itself as per the BIP 32 spec, with the seed being
* used to derive a master key, which is then used to derive an account key, the account key is used to derive two
* child keys called the <i>internal</i> and <i>external</i> keys (for change and handing out addresses respectively)
* and finally the actual leaf keys that users use hanging off the end. The leaf keys are special in that they don't
* internally store the private part at all, instead choosing to rederive the private key from the parent when
* needed for signing. This simplifies the design for encrypted key chains.</p>
*/
public class DeterministicKeyChain implements EncryptableKeyChain {
private static final Logger log = LoggerFactory.getLogger(DeterministicKeyChain.class);
private final ReentrantLock lock = Threading.lock("DeterministicKeyChain");
private DeterministicHierarchy hierarchy;
private DeterministicKey rootKey;
private DeterministicSeed seed;
// Paths through the key tree. External keys are ones that are communicated to other parties. Internal keys are
// keys created for change addresses, coinbases, mixing, etc - anything that isn't communicated. The distinction
// is somewhat arbitrary but can be useful for audits. The first number is the "account number" but we don't use
// that feature yet. In future we might hand out different accounts for cases where we wish to hand payers
// a payment request that can generate lots of addresses independently.
public static final ImmutableList<ChildNumber> ACCOUNT_ZERO_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED);
public static final ImmutableList<ChildNumber> EXTERNAL_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED, ChildNumber.ZERO);
public static final ImmutableList<ChildNumber> INTERNAL_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED, new ChildNumber(1, false));
// We try to ensure we have at least this many keys ready and waiting to be handed out via getKey().
// See docs for getLookaheadSize() for more info on what this is for. The -1 value means it hasn't been calculated
// yet. For new chains it's set to whatever the default is, unless overridden by setLookaheadSize. For deserialized
// chains, it will be calculated on demand from the number of loaded keys.
private static final int LAZY_CALCULATE_LOOKAHEAD = -1;
private int lookaheadSize = 100;
// The parent keys for external keys (handed out to other people) and internal keys (used for change addresses).
private DeterministicKey externalKey, internalKey;
// How many keys on each path have actually been used. This may be fewer than the number that have been deserialized
// or held in memory, because of the lookahead zone.
private int issuedExternalKeys, issuedInternalKeys;
// We simplify by wrapping a basic key chain and that way we get some functionality like key lookup and event
// listeners "for free". All keys in the key tree appear here, even if they aren't meant to be used for receiving
// money.
private final BasicKeyChain basicKeyChain;
/**
* Generates a new key chain with a 128 bit seed selected randomly from the given {@link java.security.SecureRandom}
* object.
*/
public DeterministicKeyChain(SecureRandom random) {
this(getRandomSeed(random), Utils.currentTimeMillis() / 1000);
}
private static byte[] getRandomSeed(SecureRandom random) {
byte[] seed = new byte[128 / 8];
random.nextBytes(seed);
return seed;
}
/**
* Creates a deterministic key chain starting from the given seed. All keys yielded by this chain will be the same
* if the starting seed is the same. You should provide the creation time in seconds since the UNIX epoch for the
* seed: this lets us know from what part of the chain we can expect to see derived keys appear.
*/
public DeterministicKeyChain(byte[] seed, long seedCreationTimeSecs) {
this(new DeterministicSeed(seed, seedCreationTimeSecs));
}
public DeterministicKeyChain(DeterministicSeed seed) {
this(seed, null);
}
// c'tor for building watching chains, we keep it private and give it a static name to make the purpose clear.
private DeterministicKeyChain(DeterministicKey accountKey) {
checkArgument(accountKey.isPubKeyOnly(), "Private subtrees not currently supported");
checkArgument(accountKey.getPath().size() == 1, "You can only watch an account key currently");
basicKeyChain = new BasicKeyChain();
initializeHierarchyUnencrypted(accountKey);
}
/**
* Creates a deterministic key chain that watches the given (public only) root key. You can use this to calculate
* balances and generally follow along, but spending is not possible with such a chain. Currently you can't use
* this method to watch an arbitrary fragment of some other tree, this limitation may be removed in future.
*/
public static DeterministicKeyChain watch(DeterministicKey accountKey) {
return new DeterministicKeyChain(accountKey);
}
DeterministicKeyChain(DeterministicSeed seed, @Nullable KeyCrypter crypter) {
this.seed = seed;
basicKeyChain = new BasicKeyChain(crypter);
if (!seed.isEncrypted()) {
rootKey = HDKeyDerivation.createMasterPrivateKey(checkNotNull(seed.getSecretBytes()));
initializeHierarchyUnencrypted(rootKey);
} else {
// We can't initialize ourselves with just an encrypted seed, so we expected deserialization code to do the
// rest of the setup (loading the root key).
}
}
// For use in encryption.
private DeterministicKeyChain(KeyCrypter crypter, KeyParameter aesKey, DeterministicKeyChain chain) {
checkArgument(!chain.rootKey.isEncrypted(), "Chain already encrypted");
this.issuedExternalKeys = chain.issuedExternalKeys;
this.issuedInternalKeys = chain.issuedInternalKeys;
this.lookaheadSize = chain.lookaheadSize;
this.seed = chain.seed.encrypt(crypter, aesKey);
basicKeyChain = new BasicKeyChain(crypter);
// The first number is the "account number" but we don't use that feature.
rootKey = chain.rootKey.encrypt(crypter, aesKey, null);
hierarchy = new DeterministicHierarchy(rootKey);
basicKeyChain.importKey(rootKey);
DeterministicKey account = encryptNonLeaf(aesKey, chain, rootKey, ACCOUNT_ZERO_PATH);
externalKey = encryptNonLeaf(aesKey, chain, account, EXTERNAL_PATH);
internalKey = encryptNonLeaf(aesKey, chain, account, INTERNAL_PATH);
// Now copy the (pubkey only) leaf keys across to avoid rederiving them. The private key bytes are missing
// anyway so there's nothing to encrypt.
for (ECKey eckey : chain.basicKeyChain.getKeys()) {
DeterministicKey key = (DeterministicKey) eckey;
if (key.getPath().size() != 3) continue; // Not a leaf key.
DeterministicKey parent = hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false);
// Clone the key to the new encrypted hierarchy.
key = new DeterministicKey(key.getPubOnly(), parent);
hierarchy.putKey(key);
basicKeyChain.importKey(key);
}
}
private DeterministicKey encryptNonLeaf(KeyParameter aesKey, DeterministicKeyChain chain,
DeterministicKey parent, ImmutableList<ChildNumber> path) {
DeterministicKey key = chain.hierarchy.get(path, false, false);
key = key.encrypt(checkNotNull(basicKeyChain.getKeyCrypter()), aesKey, parent);
hierarchy.putKey(key);
basicKeyChain.importKey(key);
return key;
}
// Derives the account path keys and inserts them into the basic key chain. This is important to preserve their
// order for serialization, amongst other things.
private void initializeHierarchyUnencrypted(DeterministicKey baseKey) {
if (baseKey.getPath().isEmpty()) {
// baseKey is a master/root key derived directly from a seed.
addToBasicChain(rootKey);
hierarchy = new DeterministicHierarchy(rootKey);
addToBasicChain(hierarchy.get(ACCOUNT_ZERO_PATH, false, true));
} else if (baseKey.getPath().size() == 1) {
// baseKey is a "watching key" that we were given so we could follow along with this account.
rootKey = null;
addToBasicChain(baseKey);
hierarchy = new DeterministicHierarchy(baseKey);
} else {
throw new IllegalArgumentException();
}
externalKey = hierarchy.deriveChild(ACCOUNT_ZERO_PATH, false, false, ChildNumber.ZERO);
internalKey = hierarchy.deriveChild(ACCOUNT_ZERO_PATH, false, false, ChildNumber.ONE);
addToBasicChain(externalKey);
addToBasicChain(internalKey);
}
/** Returns a freshly derived key that has not been returned by this method before. */
@Override
public DeterministicKey getKey(KeyPurpose purpose) {
lock.lock();
try {
DeterministicKey key;
List<DeterministicKey> lookahead;
if (purpose == KeyPurpose.RECEIVE_FUNDS) {
issuedExternalKeys++;
lookahead = maybeLookAhead(externalKey, issuedExternalKeys);
// TODO: Handle the case where the derived key is >= curve order.
key = HDKeyDerivation.deriveChildKey(externalKey, issuedExternalKeys - 1);
} else if (purpose == KeyPurpose.CHANGE) {
issuedInternalKeys++;
lookahead = maybeLookAhead(internalKey, issuedInternalKeys);
// TODO: Handle the case where the derived key is >= curve order.
key = HDKeyDerivation.deriveChildKey(internalKey, issuedInternalKeys - 1);
} else {
throw new IllegalArgumentException("Unknown key purpose " + purpose);
}
basicKeyChain.importKeys(lookahead);
return key;
} finally {
lock.unlock();
}
}
private void addToBasicChain(DeterministicKey key) {
basicKeyChain.importKeys(ImmutableList.of(key));
}
@Override
public DeterministicKey findKeyFromPubHash(byte[] pubkeyHash) {
lock.lock();
try {
return (DeterministicKey) basicKeyChain.findKeyFromPubHash(pubkeyHash);
} finally {
lock.unlock();
}
}
@Override
public DeterministicKey findKeyFromPubKey(byte[] pubkey) {
lock.lock();
try {
return (DeterministicKey) basicKeyChain.findKeyFromPubKey(pubkey);
} finally {
lock.unlock();
}
}
@Override
public boolean hasKey(ECKey key) {
lock.lock();
try {
return basicKeyChain.hasKey(key);
} finally {
lock.unlock();
}
}
/** Returns the deterministic key for the given absolute path in the hierarchy. */
protected DeterministicKey getKeyByPath(ChildNumber... path) {
return getKeyByPath(ImmutableList.<ChildNumber>copyOf(path));
}
/** Returns the deterministic key for the given absolute path in the hierarchy. */
protected DeterministicKey getKeyByPath(ImmutableList<ChildNumber> path) {
return hierarchy.get(path, false, false);
}
/**
* <p>An alias for <code>getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH).getPubOnly()</code>.
* Use this when you would like to create a watching key chain that follows this one, but can't spend money from it.
* The returned key can be serialized and then passed into {@link #watch(com.google.bitcoin.crypto.DeterministicKey)}
* on another system to watch the hierarchy.</p>
*/
public DeterministicKey getWatchingKey() {
return getKeyByPath(ACCOUNT_ZERO_PATH).getPubOnly();
}
@Override
public int numKeys() {
// We need to return here the total number of keys including the lookahead zone, not the number of keys we
// have issued via getKey/freshReceiveKey.
return basicKeyChain.numKeys();
}
@Override
public long getEarliestKeyCreationTime() {
return seed.getCreationTimeSeconds();
}
@Override
public void addEventListener(KeyChainEventListener listener) {
basicKeyChain.addEventListener(listener);
}
@Override
public void addEventListener(KeyChainEventListener listener, Executor executor) {
basicKeyChain.addEventListener(listener, executor);
}
@Override
public boolean removeEventListener(KeyChainEventListener listener) {
return basicKeyChain.removeEventListener(listener);
}
/** Returns a list of words that represent the seed. */
public List<String> toMnemonicCode() {
lock.lock();
try {
return seed.toMnemonicCode();
} finally {
lock.unlock();
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Serialization support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public List<Protos.Key> serializeToProtobuf() {
lock.lock();
try {
// Most of the serialization work is delegated to the basic key chain, which will serialize the bulk of the
// data (handling encryption along the way), and letting us patch it up with the extra data we care about.
LinkedList<Protos.Key> entries = newLinkedList();
if (seed != null) {
Protos.Key.Builder seedEntry = BasicKeyChain.serializeEncryptableItem(seed);
seedEntry.setType(Protos.Key.Type.DETERMINISTIC_ROOT_SEED);
entries.add(seedEntry.build());
}
Map<ECKey, Protos.Key.Builder> keys = basicKeyChain.serializeToEditableProtobufs();
for (Map.Entry<ECKey, Protos.Key.Builder> entry : keys.entrySet()) {
DeterministicKey key = (DeterministicKey) entry.getKey();
Protos.Key.Builder proto = entry.getValue();
proto.setType(Protos.Key.Type.DETERMINISTIC_KEY);
final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKeyBuilder();
detKey.setChainCode(ByteString.copyFrom(key.getChainCode()));
for (ChildNumber num : key.getPath())
detKey.addPath(num.i());
if (key.equals(externalKey)) {
detKey.setIssuedSubkeys(issuedExternalKeys);
detKey.setLookaheadSize(lookaheadSize);
} else if (key.equals(internalKey)) {
detKey.setIssuedSubkeys(issuedInternalKeys);
detKey.setLookaheadSize(lookaheadSize);
}
entries.add(proto.build());
}
return entries;
} finally {
lock.unlock();
}
}
/**
* Returns all the key chains found in the given list of keys. Typically there will only be one, but in the case of
* key rotation it can happen that there are multiple chains found.
*/
public static List<DeterministicKeyChain> fromProtobuf(List<Protos.Key> keys, @Nullable KeyCrypter crypter) throws UnreadableWalletException {
List<DeterministicKeyChain> chains = newLinkedList();
DeterministicSeed seed = null;
DeterministicKeyChain chain = null;
int lookaheadSize = -1;
for (Protos.Key key : keys) {
final Protos.Key.Type t = key.getType();
if (t == Protos.Key.Type.DETERMINISTIC_ROOT_SEED) {
if (chain != null) {
checkState(lookaheadSize >= 0);
chain.setLookaheadSize(lookaheadSize);
chain.maybeLookAhead();
chains.add(chain);
chain = null;
}
long timestamp = key.getCreationTimestamp() / 1000;
if (key.hasSecretBytes()) {
seed = new DeterministicSeed(key.getSecretBytes().toByteArray(), timestamp);
} else if (key.hasEncryptedData()) {
EncryptedData data = new EncryptedData(key.getEncryptedData().getInitialisationVector().toByteArray(),
key.getEncryptedData().getEncryptedPrivateKey().toByteArray());
seed = new DeterministicSeed(data, timestamp);
} else {
throw new UnreadableWalletException("Malformed key proto: " + key.toString());
}
if (log.isDebugEnabled())
log.debug("Deserializing: DETERMINISTIC_ROOT_SEED: {}", seed);
} else if (t == Protos.Key.Type.DETERMINISTIC_KEY) {
if (!key.hasDeterministicKey())
throw new UnreadableWalletException("Deterministic key missing extra data: " + key.toString());
byte[] chainCode = key.getDeterministicKey().getChainCode().toByteArray();
// Deserialize the path through the tree.
LinkedList<ChildNumber> path = newLinkedList();
for (int i : key.getDeterministicKey().getPathList())
path.add(new ChildNumber(i));
// Deserialize the public key and path.
ECPoint pubkey = ECKey.CURVE.getCurve().decodePoint(key.getPublicKey().toByteArray());
final ImmutableList<ChildNumber> immutablePath = ImmutableList.copyOf(path);
// Possibly create the chain, if we didn't already do so yet.
boolean isWatchingAccountKey = false;
if (chain == null) {
if (seed == null) {
DeterministicKey accountKey = new DeterministicKey(immutablePath, chainCode, pubkey, null, null);
if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH))
throw new UnreadableWalletException("Expecting account key but found key with path: " +
HDUtils.formatPath(accountKey.getPath()));
chain = DeterministicKeyChain.watch(accountKey);
isWatchingAccountKey = true;
} else {
chain = new DeterministicKeyChain(seed, crypter);
chain.lookaheadSize = LAZY_CALCULATE_LOOKAHEAD;
// If the seed is encrypted, then the chain is incomplete at this point. However, we will load
// it up below as we parse in the keys. We just need to check at the end that we've loaded
// everything afterwards.
}
}
// Find the parent key assuming this is not the root key, and not an account key for a watching chain.
DeterministicKey parent = null;
if (!path.isEmpty() && !isWatchingAccountKey) {
ChildNumber index = path.removeLast();
parent = chain.hierarchy.get(path, false, false);
path.add(index);
}
DeterministicKey detkey;
if (key.hasSecretBytes()) {
// Not encrypted: private key is available.
final BigInteger priv = new BigInteger(1, key.getSecretBytes().toByteArray());
detkey = new DeterministicKey(immutablePath, chainCode, pubkey, priv, parent);
} else {
if (key.hasEncryptedData()) {
Protos.EncryptedData proto = key.getEncryptedData();
EncryptedData data = new EncryptedData(proto.getInitialisationVector().toByteArray(),
proto.getEncryptedPrivateKey().toByteArray());
checkNotNull(crypter, "Encountered an encrypted key but no key crypter provided");
detkey = new DeterministicKey(immutablePath, chainCode, crypter, pubkey, data, parent);
} else {
// No secret key bytes and key is not encrypted: either a watching key or private key bytes
// will be rederived on the fly from the parent.
detkey = new DeterministicKey(immutablePath, chainCode, pubkey, null, parent);
}
}
if (log.isDebugEnabled())
log.debug("Deserializing: DETERMINISTIC_KEY: {}", detkey);
if (!isWatchingAccountKey) {
// If the non-encrypted case, the non-leaf keys (account, internal, external) have already been
// rederived and inserted at this point and the two lines below are just a no-op. In the encrypted
// case though, we can't rederive and we must reinsert, potentially building the heirarchy object
// if need be.
if (path.size() == 0) {
// Master key.
chain.rootKey = detkey;
chain.hierarchy = new DeterministicHierarchy(detkey);
} else if (path.size() == 2) {
if (detkey.getChildNumber().num() == 0) {
chain.externalKey = detkey;
chain.issuedExternalKeys = key.getDeterministicKey().getIssuedSubkeys();
lookaheadSize = Math.max(lookaheadSize, key.getDeterministicKey().getLookaheadSize());
} else if (detkey.getChildNumber().num() == 1) {
chain.internalKey = detkey;
chain.issuedInternalKeys = key.getDeterministicKey().getIssuedSubkeys();
}
}
}
chain.hierarchy.putKey(detkey);
chain.basicKeyChain.importKey(detkey);
}
}
if (chain != null) {
checkState(lookaheadSize >= 0);
chain.setLookaheadSize(lookaheadSize);
chain.maybeLookAhead();
chains.add(chain);
}
return chains;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Encryption support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public DeterministicKeyChain toEncrypted(CharSequence password) {
checkNotNull(password);
checkArgument(password.length() > 0);
checkState(seed != null, "Attempt to encrypt a watching chain.");
checkState(!seed.isEncrypted());
KeyCrypter scrypt = new KeyCrypterScrypt();
KeyParameter derivedKey = scrypt.deriveKey(password);
return toEncrypted(scrypt, derivedKey);
}
@Override
public DeterministicKeyChain toEncrypted(KeyCrypter keyCrypter, KeyParameter aesKey) {
return new DeterministicKeyChain(keyCrypter, aesKey, this);
}
@Override
public DeterministicKeyChain toDecrypted(CharSequence password) {
checkNotNull(password);
checkArgument(password.length() > 0);
KeyCrypter crypter = getKeyCrypter();
checkState(crypter != null, "Chain not encrypted");
KeyParameter derivedKey = crypter.deriveKey(password);
return toDecrypted(derivedKey);
}
@Override
public DeterministicKeyChain toDecrypted(KeyParameter aesKey) {
checkState(getKeyCrypter() != null, "Key chain not encrypted");
checkState(seed.isEncrypted());
DeterministicSeed decSeed = seed.decrypt(getKeyCrypter(), aesKey);
DeterministicKeyChain chain = new DeterministicKeyChain(decSeed);
chain.lookaheadSize = lookaheadSize;
// Now copy the (pubkey only) leaf keys across to avoid rederiving them. The private key bytes are missing
// anyway so there's nothing to decrypt.
for (ECKey eckey : basicKeyChain.getKeys()) {
DeterministicKey key = (DeterministicKey) eckey;
if (key.getPath().size() != 3) continue; // Not a leaf key.
checkState(key.isEncrypted());
DeterministicKey parent = chain.hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false);
// Clone the key to the new decrypted hierarchy.
key = new DeterministicKey(key.getPubOnly(), parent);
chain.hierarchy.putKey(key);
chain.basicKeyChain.importKey(key);
}
chain.issuedExternalKeys = issuedExternalKeys;
chain.issuedInternalKeys = issuedInternalKeys;
return chain;
}
@Override
public boolean checkPassword(CharSequence password) {
checkNotNull(password);
checkState(getKeyCrypter() != null, "Key chain not encrypted");
return checkAESKey(getKeyCrypter().deriveKey(password));
}
@Override
public boolean checkAESKey(KeyParameter aesKey) {
checkNotNull(aesKey);
checkState(getKeyCrypter() != null, "Key chain not encrypted");
try {
return rootKey.decrypt(getKeyCrypter(), aesKey).getPubKeyPoint().equals(rootKey.getPubKeyPoint());
} catch (KeyCrypterException e) {
return false;
}
}
@Nullable
@Override
public KeyCrypter getKeyCrypter() {
return basicKeyChain.getKeyCrypter();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Bloom filtering support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int numBloomFilterEntries() {
return numKeys() * 2;
}
@Override
public BloomFilter getFilter(int size, double falsePositiveRate, long tweak) {
checkArgument(size >= numBloomFilterEntries());
return basicKeyChain.getFilter(size, falsePositiveRate, tweak);
}
/**
* <p>The number of public keys we should pre-generate on each path before they are requested by the app. This is
* required so that when scanning through the chain given only a seed, we can give enough keys to the remote node
* via the Bloom filter such that we see transactions that are "from the future", for example transactions created
* by a different app that's sharing the same seed, or transactions we made before but we're replaying the chain
* given just the seed. The default is 100.</p>
*/
public int getLookaheadSize() {
lock.lock();
try {
return lookaheadSize;
} finally {
lock.unlock();
}
}
/**
* Sets a new lookahead size. See {@link #getLookaheadSize()} for details on what this is. Setting a new size
* that's larger than the current size will return immediately and the new size will only take effect next time
* a fresh filter is requested (e.g. due to a new peer being connected). So you should set this before starting
* to sync the chain, if you want to modify it.
*/
public void setLookaheadSize(int lookaheadSize) {
lock.lock();
try {
this.lookaheadSize = lookaheadSize;
} finally {
lock.unlock();
}
}
// Pre-generate enough keys to reach the lookahead size.
private void maybeLookAhead() {
lock.lock();
try {
List<DeterministicKey> keys = maybeLookAhead(externalKey, issuedExternalKeys);
keys.addAll(maybeLookAhead(internalKey, issuedInternalKeys));
// Batch add all keys at once so there's only one event listener invocation, as this will be listened to
// by the wallet and used to rebuild/broadcast the Bloom filter. That's expensive so we don't want to do
// it more often than necessary.
basicKeyChain.importKeys(keys);
} finally {
lock.unlock();
}
}
// Returned keys must be inserted into the basic key chain.
private List<DeterministicKey> maybeLookAhead(DeterministicKey parent, int issued) {
checkState(lock.isHeldByCurrentThread());
final int numChildren = hierarchy.getNumChildren(parent.getPath());
int needed = issued + getLookaheadSize() - numChildren;
checkState(needed >= 0, "needed = " + needed);
List<DeterministicKey> result = new ArrayList<DeterministicKey>(needed);
if (needed == 0) return result;
long now = System.currentTimeMillis();
log.info("Pre-generating {} keys for {}", needed, parent.getPathAsString());
for (int i = 0; i < needed; i++) {
// TODO: Handle the case where the derived key is >= curve order.
DeterministicKey key = HDKeyDerivation.deriveChildKey(parent, numChildren + i);
hierarchy.putKey(key);
result.add(key);
}
log.info("Took {} msec", System.currentTimeMillis() - now);
return result;
}
/** Returns the seed or null if this chain is encrypted or watching. */
@Nullable
public DeterministicSeed getSeed() {
lock.lock();
try {
return seed;
} finally {
lock.unlock();
}
}
// For internal usage only (for printing keys in KeyChainGroup).
/* package */ List<ECKey> getKeys() {
return basicKeyChain.getKeys();
}
}

View File

@ -0,0 +1,151 @@
/**
* Copyright 2014 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.wallet;
import com.google.bitcoin.crypto.*;
import org.bitcoinj.wallet.Protos;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.encoders.Hex;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Holds the seed bytes for the BIP32 deterministic wallet algorithm, inside a
* {@link com.google.bitcoin.wallet.DeterministicKeyChain}. The purpose of this wrapper is to simplify the encryption
* code.
*/
public class DeterministicSeed implements EncryptableItem {
@Nullable private final byte[] unencryptedSeed;
@Nullable private final EncryptedData encryptedSeed;
private final long creationTimeSeconds;
private static MnemonicCode MNEMONIC_CODE;
private static synchronized MnemonicCode getCachedMnemonicCode() {
try {
// This object can be large and has to load the word list from disk, so we lazy cache it.
if (MNEMONIC_CODE == null) {
MNEMONIC_CODE = new MnemonicCode();
}
return MNEMONIC_CODE;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public DeterministicSeed(byte[] unencryptedSeed, long creationTimeSeconds) {
this.unencryptedSeed = checkNotNull(unencryptedSeed);
this.encryptedSeed = null;
this.creationTimeSeconds = creationTimeSeconds;
}
public DeterministicSeed(EncryptedData encryptedSeed, long creationTimeSeconds) {
this.unencryptedSeed = null;
this.encryptedSeed = checkNotNull(encryptedSeed);
this.creationTimeSeconds = creationTimeSeconds;
}
/**
* Constructs a seed from a BIP 39 mnemonic code. See {@link com.google.bitcoin.crypto.MnemonicCode} for more
* details on this scheme.
* @param words A list of 12 words.
* @param creationTimeSeconds When the seed was originally created, UNIX time.
* @throws MnemonicException if there is a problem decoding the words.
*/
public DeterministicSeed(List<String> words, long creationTimeSeconds) throws MnemonicException.MnemonicChecksumException, MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
this(getCachedMnemonicCode().toEntropy(words), creationTimeSeconds);
}
@Override
public boolean isEncrypted() {
checkState(unencryptedSeed != null || encryptedSeed != null);
return encryptedSeed != null;
}
@Override
public String toString() {
if (isEncrypted())
return "DeterministicSeed [encrypted]";
else
return "DeterministicSeed " + toHexString();
}
/** Returns the seed as hex or null if encrypted. */
@Nullable
public String toHexString() {
if (unencryptedSeed != null)
return new String(Hex.encode(unencryptedSeed));
else
return null;
}
@Nullable
@Override
public byte[] getSecretBytes() {
return unencryptedSeed;
}
@Nullable
@Override
public EncryptedData getEncryptedData() {
return encryptedSeed;
}
@Override
public Protos.Wallet.EncryptionType getEncryptionType() {
return Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES;
}
@Override
public long getCreationTimeSeconds() {
return creationTimeSeconds;
}
public DeterministicSeed encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
checkState(encryptedSeed == null, "Trying to encrypt seed twice");
checkState(unencryptedSeed != null, "Seed bytes missing so cannot encrypt");
EncryptedData data = keyCrypter.encrypt(unencryptedSeed, aesKey);
return new DeterministicSeed(data, creationTimeSeconds);
}
public DeterministicSeed decrypt(KeyCrypter crypter, KeyParameter aesKey) {
checkState(isEncrypted());
checkNotNull(encryptedSeed);
return new DeterministicSeed(crypter.decrypt(encryptedSeed, aesKey), creationTimeSeconds);
}
/** Returns a list of words that represent the seed, or IllegalStateException if the seed is encrypted or missing. */
public List<String> toMnemonicCode(MnemonicCode code) {
try {
if (isEncrypted())
throw new IllegalStateException("The seed is encrypted");
final byte[] seed = checkNotNull(getSecretBytes());
return code.toMnemonic(seed);
} catch (MnemonicException.MnemonicLengthException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
/** Returns a list of words that represent the seed, or IllegalStateException if the seed is encrypted or missing. */
public List<String> toMnemonicCode() {
return toMnemonicCode(getCachedMnemonicCode());
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright 2013 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.wallet;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
/**
* An encryptable key chain is a key-chain that can be encrypted with a user-provided password or AES key.
*/
public interface EncryptableKeyChain extends KeyChain {
/**
* Takes the given password, which should be strong, derives a key from it and then invokes
* {@link #toEncrypted(com.google.bitcoin.crypto.KeyCrypter, org.spongycastle.crypto.params.KeyParameter)} with
* {@link com.google.bitcoin.crypto.KeyCrypterScrypt} as the crypter.
*
* @return The derived key, in case you wish to cache it for future use.
*/
public EncryptableKeyChain toEncrypted(CharSequence password);
/**
* Returns a new keychain holding identical/cloned keys to this chain, but encrypted under the given key.
* Old keys and keychains remain valid and so you should ensure you don't accidentally hold references to them.
*/
public EncryptableKeyChain toEncrypted(KeyCrypter keyCrypter, KeyParameter aesKey);
/**
* Decrypts the key chain with the given password. See {@link #toDecrypted(org.spongycastle.crypto.params.KeyParameter)}
* for details.
*/
public EncryptableKeyChain toDecrypted(CharSequence password);
/**
* Decrypt the key chain with the given AES key and whatever {@link KeyCrypter} is already set. Note that if you
* just want to spend money from an encrypted wallet, don't decrypt the whole thing first. Instead, set the
* {@link com.google.bitcoin.core.Wallet.SendRequest#aesKey} field before asking the wallet to build the send.
*
* @param aesKey AES key to use (normally created using KeyCrypter#deriveKey and cached as it is time consuming to
* create from a password)
* @throws KeyCrypterException Thrown if the wallet decryption fails. If so, the wallet state is unchanged.
*/
public EncryptableKeyChain toDecrypted(KeyParameter aesKey);
public boolean checkPassword(CharSequence password);
public boolean checkAESKey(KeyParameter aesKey);
/** Returns the key crypter used by this key chain, or null if it's not encrypted. */
@Nullable
public KeyCrypter getKeyCrypter();
}

View File

@ -0,0 +1,104 @@
/**
* Copyright 2013 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.wallet;
import com.google.bitcoin.core.BloomFilter;
import com.google.bitcoin.core.ECKey;
import org.bitcoinj.wallet.Protos;
import java.util.List;
import java.util.concurrent.Executor;
/**
* <p>A KeyChain is a class that stores a collection of keys for a {@link com.google.bitcoin.core.Wallet}. Key chains
* are expected to be able to look up keys given a hash (i.e. address) or pubkey bytes, and provide keys on request
* for a given purpose. They can inform event listeners about new keys being added.</p>
*
* <p>However it is important to understand what this interface does <i>not</i> provide. It cannot encrypt or decrypt
* keys, for instance you need an implementor of {@link EncryptableKeyChain}. It cannot have keys imported into it,
* that you to use a method of a specific key chain instance, such as {@link BasicKeyChain}. The reason for these
* restrictions is to support key chains that may be handled by external hardware or software, or which are derived
* deterministically from a seed (and thus the notion of importing a key is meaningless).</p>
*/
public interface KeyChain {
/**
* Locates a keypair from the keychain given the hash of the public key. This is needed when finding out which
* key we need to use to redeem a transaction output.
*
* @return ECKey object or null if no such key was found.
*/
public ECKey findKeyFromPubHash(byte[] pubkeyHash);
/**
* Locates a keypair from the keychain given the raw public key bytes.
* @return ECKey or null if no such key was found.
*/
public ECKey findKeyFromPubKey(byte[] pubkey);
/** Returns true if the given key is in the chain. */
public boolean hasKey(ECKey key);
enum KeyPurpose {
RECEIVE_FUNDS,
CHANGE
}
/** Obtains a key intended for the given purpose. The chain may create a new key, derive one, or re-use an old one. */
public ECKey getKey(KeyPurpose purpose);
/** Returns a list of keys serialized to the bitcoinj protobuf format. */
public List<Protos.Key> serializeToProtobuf();
/** Adds a listener for events that are run when keys are added, on the user thread. */
public void addEventListener(KeyChainEventListener listener);
/** Adds a listener for events that are run when keys are added, on the given executor. */
public void addEventListener(KeyChainEventListener listener, Executor executor);
/** Removes a listener for events that are run when keys are added. */
public boolean removeEventListener(KeyChainEventListener listener);
/** Returns the number of keys this key chain manages. */
public int numKeys();
/**
* Returns the number of elements this chain wishes to insert into the Bloom filter. The size passed to
* {@link #getFilter(int, double, long)} should be at least this large.
*/
public int numBloomFilterEntries();
/**
* <p>Returns the earliest creation time of keys in this chain, in seconds since the epoch. This can return zero
* if at least one key does not have that data (was created before key timestamping was implemented). If there
* are no keys in the wallet, {@link Long#MAX_VALUE} is returned.</p>
*/
public long getEarliestKeyCreationTime();
/**
* <p>Gets a bloom filter that contains all of the public keys from this chain, and which will provide the given
* false-positive rate if it has size elements. Keep in mind that you will get 2 elements in the bloom filter for
* each key in the key chain, for the public key and the hash of the public key (address form). For this reason
* size should be <i>at least</i> 2x the result of {@link #numKeys()}.</p>
*
* <p>This is used to generate a {@link BloomFilter} which can be {@link BloomFilter#merge(BloomFilter)}d with
* another. It could also be used if you have a specific target for the filter's size.</p>
*
* <p>See the docs for {@link com.google.bitcoin.core.BloomFilter#BloomFilter(int, double, long)} for a brief
* explanation of anonymity when using bloom filters, and for the meaning of these parameters.</p>
*/
public BloomFilter getFilter(int size, double falsePositiveRate, long tweak);
}

View File

@ -0,0 +1,29 @@
/**
* Copyright 2013 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.wallet;
import com.google.bitcoin.core.ECKey;
import java.util.List;
public interface KeyChainEventListener {
/**
* Called whenever a new key is added to the key chain, whether that be via an explicit addition or due to some
* other automatic derivation. See the documentation for your {@link KeyChain} implementation for details on what
* can trigger this event.
*/
void onKeysAdded(List<ECKey> keys);
}

View File

@ -0,0 +1,405 @@
/**
* Copyright 2014 Mike Hearn
*
* 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.wallet;
import com.google.bitcoin.core.*;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.utils.Threading;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.bitcoinj.wallet.Protos;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import static com.google.common.base.Preconditions.*;
/**
* <p>A KeyChainGroup is used by the {@link com.google.bitcoin.core.Wallet} and
* manages: a {@link com.google.bitcoin.wallet.BasicKeyChain} object (which will normally be empty), and zero or more
* {@link com.google.bitcoin.wallet.DeterministicKeyChain}s. A deterministic key chain will be created lazily/on demand
* when a fresh or current key is requested, possibly being initialized from the private key bytes of the earliest non
* rotating key in the basic key chain if one is available, or from a fresh random seed if not.</p>
*
* <p>If a key rotation time is set, it may be necessary to add a new DeterministicKeyChain with a fresh seed
* and also preserve the old one, so funds can be swept from the rotating keys. In this case, there may be
* more than one deterministic chain. The latest chain is called the active chain and is where new keys are served
* from.</p>
*
* <p>The wallet delegates most key management tasks to this class. It is <b>not</b> thread safe and requires external
* locking, i.e. by the wallet lock. The group then in turn delegates most operations to the key chain objects,
* combining their responses together when necessary.</p>
*/
public class KeyChainGroup {
private BasicKeyChain basic;
private final List<DeterministicKeyChain> chains;
private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys;
@Nullable private KeyCrypter keyCrypter;
private int lookaheadSize = -1;
/** Creates a keychain group with no basic chain, and a single randomly initialized HD chain. */
public KeyChainGroup() {
this(null, new ArrayList<DeterministicKeyChain>(1), null);
}
/** Creates a keychain group with no basic chain, and an HD chain initialized from the given seed. */
public KeyChainGroup(DeterministicSeed seed) {
this(null, ImmutableList.of(new DeterministicKeyChain(seed)), null);
}
// Used for deserialization.
private KeyChainGroup(@Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains, @Nullable KeyCrypter crypter) {
this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
this.chains = checkNotNull(chains);
this.keyCrypter = crypter;
this.currentKeys = new EnumMap<KeyChain.KeyPurpose, DeterministicKey>(KeyChain.KeyPurpose.class);
}
private void createAndActivateNewHDChain() {
final DeterministicKeyChain chain = new DeterministicKeyChain(new SecureRandom());
if (lookaheadSize >= 0)
chain.setLookaheadSize(lookaheadSize);
chains.add(chain);
}
/**
* Returns a key that hasn't been seen in a transaction yet, and which is suitable for displaying in a wallet
* user interface as "a convenient key to receive funds on" when the purpose parameter is
* {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS}. The returned key is stable until
* it's actually seen in a pending or confirmed transaction, at which point this method will start returning
* a different key (for each purpose independently).
*/
public ECKey currentKey(KeyChain.KeyPurpose purpose) {
final DeterministicKey current = currentKeys.get(purpose);
return current != null ? current : freshKey(purpose);
}
/**
* Returns a key that has not been returned by this method before (fresh). You can think of this as being
* a newly created key, although the notion of "create" is not really valid for a
* {@link com.google.bitcoin.wallet.DeterministicKeyChain}. When the parameter is
* {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} the returned key is suitable for being put
* into a receive coins wizard type UI. You should use this when the user is definitely going to hand this key out
* to someone who wishes to send money.
*/
public ECKey freshKey(KeyChain.KeyPurpose purpose) {
DeterministicKeyChain chain = getActiveKeyChain();
DeterministicKey key = chain.getKey(purpose); // Always returns the next key along the key chain.
currentKeys.put(purpose, key);
return key;
}
/** Returns the key chain that's used for generation of fresh/current keys. This is always the newest HD chain. */
public DeterministicKeyChain getActiveKeyChain() {
if (chains.isEmpty())
createAndActivateNewHDChain();
return chains.get(chains.size() - 1);
}
/**
* Sets the lookahead buffer size for ALL deterministic key chains, see
* {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)}
* for more information.
*/
public void setLookaheadSize(int lookaheadSize) {
this.lookaheadSize = lookaheadSize;
for (DeterministicKeyChain chain : chains) {
chain.setLookaheadSize(lookaheadSize);
}
}
/**
* Gets the current lookahead size being used for ALL deterministic key chains. See
* {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)}
* for more information.
*/
public int getLookaheadSize() {
return lookaheadSize;
}
/** Imports the given keys into the basic chain, creating it if necessary. */
public int importKeys(List<ECKey> keys) {
return basic.importKeys(keys);
}
/** Imports the given keys into the basic chain, creating it if necessary. */
public int importKeys(ECKey... keys) {
return importKeys(ImmutableList.copyOf(keys));
}
public boolean checkPassword(CharSequence password) {
checkState(keyCrypter != null, "Not encrypted");
return checkAESKey(keyCrypter.deriveKey(password));
}
public boolean checkAESKey(KeyParameter aesKey) {
checkState(keyCrypter != null, "Not encrypted");
if (basic.numKeys() > 0)
return basic.checkAESKey(aesKey) && getActiveKeyChain().checkAESKey(aesKey);
return getActiveKeyChain().checkAESKey(aesKey);
}
/** Imports the given unencrypted keys into the basic chain, encrypting them along the way with the given key. */
public int importKeysAndEncrypt(final List<ECKey> keys, KeyParameter aesKey) {
// TODO: Firstly check if the aes key can decrypt any of the existing keys successfully.
checkState(keyCrypter != null, "Not encrypted");
LinkedList<ECKey> encryptedKeys = Lists.newLinkedList();
for (ECKey key : keys) {
if (key.isEncrypted())
throw new IllegalArgumentException("Cannot provide already encrypted keys");
encryptedKeys.add(key.encrypt(keyCrypter, aesKey));
}
return importKeys(encryptedKeys);
}
@Nullable
public ECKey findKeyFromPubHash(byte[] pubkeyHash) {
ECKey result;
if ((result = basic.findKeyFromPubHash(pubkeyHash)) != null)
return result;
for (DeterministicKeyChain chain : chains) {
if ((result = chain.findKeyFromPubHash(pubkeyHash)) != null)
return result;
}
return null;
}
public boolean hasKey(ECKey key) {
if (basic.hasKey(key))
return true;
for (DeterministicKeyChain chain : chains)
if (chain.hasKey(key))
return true;
return false;
}
@Nullable
public ECKey findKeyFromPubKey(byte[] pubkey) {
ECKey result;
if ((result = basic.findKeyFromPubKey(pubkey)) != null)
return result;
for (DeterministicKeyChain chain : chains) {
if ((result = chain.findKeyFromPubKey(pubkey)) != null)
return result;
}
return null;
}
/** Returns the number of keys managed by this group, including the lookahead buffers. */
public int numKeys() {
int result = basic.numKeys();
for (DeterministicKeyChain chain : chains)
result += chain.numKeys();
return result;
}
/**
* Removes a key that was imported into the basic key chain. You cannot remove deterministic keys.
* @throws java.lang.IllegalArgumentException if the key is deterministic.
*/
public boolean removeImportedKey(ECKey key) {
checkNotNull(key);
checkArgument(!(key instanceof DeterministicKey));
return basic.removeKey(key);
}
/**
* Encrypt the keys in the group using the KeyCrypter and the AES key. A good default KeyCrypter to use is
* {@link com.google.bitcoin.crypto.KeyCrypterScrypt}.
*
* @throws com.google.bitcoin.crypto.KeyCrypterException Thrown if the wallet encryption fails for some reason, leaving the group unchanged.
*/
public void encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
checkNotNull(keyCrypter);
checkNotNull(aesKey);
// This code must be exception safe.
BasicKeyChain newBasic = basic.toEncrypted(keyCrypter, aesKey);
List<DeterministicKeyChain> newChains = new ArrayList<DeterministicKeyChain>(chains.size());
// If the user is trying to encrypt us before ever asking for a key, we might not have lazy created an HD chain
// yet. So do it now.
if (chains.isEmpty())
createAndActivateNewHDChain();
for (DeterministicKeyChain chain : chains)
newChains.add(chain.toEncrypted(keyCrypter, aesKey));
this.keyCrypter = keyCrypter;
basic = newBasic;
chains.clear();
chains.addAll(newChains);
}
/**
* Decrypt the keys in the group using the previously given key crypter and the AES key. A good default
* KeyCrypter to use is {@link com.google.bitcoin.crypto.KeyCrypterScrypt}.
*
* @throws com.google.bitcoin.crypto.KeyCrypterException Thrown if the wallet decryption fails for some reason, leaving the group unchanged.
*/
public void decrypt(KeyParameter aesKey) {
// This code must be exception safe.
checkNotNull(aesKey);
BasicKeyChain newBasic = basic.toDecrypted(aesKey);
List<DeterministicKeyChain> newChains = new ArrayList<DeterministicKeyChain>(chains.size());
for (DeterministicKeyChain chain : chains)
newChains.add(chain.toDecrypted(aesKey));
this.keyCrypter = null;
basic = newBasic;
chains.clear();
chains.addAll(newChains);
}
/** Returns true if the group is encrypted. */
public boolean isEncrypted() {
return keyCrypter != null;
}
/** Returns the key crypter or null if the group is not encrypted. */
@Nullable public KeyCrypter getKeyCrypter() { return keyCrypter; }
public long getEarliestKeyCreationTime() {
long time = basic.getEarliestKeyCreationTime(); // Long.MAX_VALUE if empty.
for (DeterministicKeyChain chain : chains)
time = Math.min(time, chain.getEarliestKeyCreationTime());
return time;
}
public int getBloomFilterElementCount() {
int result = basic.numBloomFilterEntries();
for (DeterministicKeyChain chain : chains)
result += chain.numBloomFilterEntries();
return result;
}
public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak) {
BloomFilter filter = new BloomFilter(size, falsePositiveRate, nTweak);
if (basic.numKeys() > 0)
filter.merge(basic.getFilter(size, falsePositiveRate, nTweak));
for (DeterministicKeyChain chain : chains)
filter.merge(chain.getFilter(size, falsePositiveRate, nTweak));
return filter;
}
/** {@inheritDoc} */
public boolean isRequiringUpdateAllBloomFilter() {
throw new UnsupportedOperationException(); // Unused.
}
/** Adds a listener for events that are run when keys are added, on the user thread. */
public void addEventListener(KeyChainEventListener listener) {
addEventListener(listener, Threading.USER_THREAD);
}
/** Adds a listener for events that are run when keys are added, on the given executor. */
public void addEventListener(KeyChainEventListener listener, Executor executor) {
checkNotNull(listener);
checkNotNull(executor);
basic.addEventListener(listener, executor);
for (DeterministicKeyChain chain : chains)
chain.addEventListener(listener, executor);
}
/** Removes a listener for events that are run when keys are added. */
public boolean removeEventListener(KeyChainEventListener listener) {
checkNotNull(listener);
for (DeterministicKeyChain chain : chains)
chain.removeEventListener(listener);
return basic.removeEventListener(listener);
}
/** Returns a list of key protobufs obtained by merging the chains. */
public List<Protos.Key> serializeToProtobuf() {
List<Protos.Key> result;
if (basic != null)
result = basic.serializeToProtobuf();
else
result = Lists.newArrayList();
for (DeterministicKeyChain chain : chains) {
List<Protos.Key> protos = chain.serializeToProtobuf();
result.addAll(protos);
}
return result;
}
public static KeyChainGroup fromProtobufUnencrypted(List<Protos.Key> keys) throws UnreadableWalletException {
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufUnencrypted(keys);
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, null);
if (chains.isEmpty()) {
// TODO: Old bag-of-keys style wallet only! Auto-upgrade time!
}
return new KeyChainGroup(basicKeyChain, chains, null);
}
public static KeyChainGroup fromProtobufEncrypted(List<Protos.Key> keys, KeyCrypter crypter) throws UnreadableWalletException {
checkNotNull(crypter);
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufEncrypted(keys, crypter);
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, crypter);
if (chains.isEmpty()) {
// TODO: Old bag-of-keys style wallet only! Auto-upgrade time!
}
return new KeyChainGroup(basicKeyChain, chains, crypter);
}
public String toString(@Nullable NetworkParameters params, boolean includePrivateKeys) {
final StringBuilder builder = new StringBuilder();
if (basic != null) {
for (ECKey key : basic.getKeys())
formatKeyWithAddress(params, includePrivateKeys, key, builder);
}
final String newline = String.format("%n");
for (DeterministicKeyChain chain : chains) {
DeterministicSeed seed = chain.getSeed();
if (seed != null && !seed.isEncrypted()) {
final List<String> words = seed.toMnemonicCode();
builder.append("Seed as words: ");
builder.append(Joiner.on(' ').join(words));
builder.append(newline);
builder.append("Seed as hex: ");
builder.append(seed.toHexString());
builder.append(newline);
} else {
builder.append("Seed is encrypted");
builder.append(newline);
}
for (ECKey key : chain.getKeys())
formatKeyWithAddress(params, includePrivateKeys, key, builder);
}
return builder.toString();
}
private void formatKeyWithAddress(@Nullable NetworkParameters params, boolean includePrivateKeys,
ECKey key, StringBuilder builder) {
if (params != null) {
final Address address = key.toAddress(params);
builder.append(" addr:");
builder.append(address.toString());
}
builder.append(" hash160:");
builder.append(Utils.bytesToHexString(key.getPubKeyHash()));
builder.append(" ");
builder.append(includePrivateKeys ? key.toStringWithPrivate() : key.toString());
builder.append("\n");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@ public class AlertMessageTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
final ECKey key = new ECKey(TEST_KEY_PRIV, null); final ECKey key = ECKey.fromPrivate(TEST_KEY_PRIV);
params = new UnitTestParams() { params = new UnitTestParams() {
@Override @Override
public byte[] getAlertSigningKey() { public byte[] getAlertSigningKey() {

View File

@ -79,12 +79,12 @@ public class BlockChainTest {
} }
} }
}; };
wallet.addKey(new ECKey()); wallet.freshReceiveKey();
resetBlockStore(); resetBlockStore();
chain = new BlockChain(unitTestParams, wallet, blockStore); chain = new BlockChain(unitTestParams, wallet, blockStore);
coinbaseTo = wallet.getKeys().get(0).toAddress(unitTestParams); coinbaseTo = wallet.currentReceiveKey().toAddress(unitTestParams);
} }
@After @After
@ -124,7 +124,7 @@ public class BlockChainTest {
// Quick check that we can actually receive coins. // Quick check that we can actually receive coins.
Transaction tx1 = createFakeTx(unitTestParams, Transaction tx1 = createFakeTx(unitTestParams,
Utils.toNanoCoins(1, 0), Utils.toNanoCoins(1, 0),
wallet.getKeys().get(0).toAddress(unitTestParams)); wallet.currentReceiveKey().toAddress(unitTestParams));
Block b1 = createFakeBlock(blockStore, tx1).block; Block b1 = createFakeBlock(blockStore, tx1).block;
chain.add(b1); chain.add(b1);
assertTrue(wallet.getBalance().signum() > 0); assertTrue(wallet.getBalance().signum() > 0);
@ -136,7 +136,7 @@ public class BlockChainTest {
// there isn't any such tx present (as an optimization). // there isn't any such tx present (as an optimization).
Transaction tx1 = createFakeTx(unitTestParams, Transaction tx1 = createFakeTx(unitTestParams,
Utils.toNanoCoins(1, 0), Utils.toNanoCoins(1, 0),
wallet.getKeys().get(0).toAddress(unitTestParams)); wallet.currentReceiveKey().toAddress(unitTestParams));
Block b1 = createFakeBlock(blockStore, tx1).block; Block b1 = createFakeBlock(blockStore, tx1).block;
chain.add(b1); chain.add(b1);
resetBlockStore(); resetBlockStore();
@ -267,8 +267,7 @@ public class BlockChainTest {
// considered relevant. // considered relevant.
Address somebodyElse = new ECKey().toAddress(unitTestParams); Address somebodyElse = new ECKey().toAddress(unitTestParams);
Block b1 = unitTestParams.getGenesisBlock().createNextBlock(somebodyElse); Block b1 = unitTestParams.getGenesisBlock().createNextBlock(somebodyElse);
ECKey key = new ECKey(); ECKey key = wallet.freshReceiveKey();
wallet.addKey(key);
Address addr = key.toAddress(unitTestParams); Address addr = key.toAddress(unitTestParams);
// Create a tx that gives us some coins, and another that spends it to someone else in the same block. // Create a tx that gives us some coins, and another that spends it to someone else in the same block.
Transaction t1 = FakeTxBuilder.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), addr); Transaction t1 = FakeTxBuilder.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), addr);
@ -288,14 +287,13 @@ public class BlockChainTest {
// Create a second wallet to receive the coinbase spend. // Create a second wallet to receive the coinbase spend.
Wallet wallet2 = new Wallet(unitTestParams); Wallet wallet2 = new Wallet(unitTestParams);
ECKey receiveKey = new ECKey(); ECKey receiveKey = wallet2.freshReceiveKey();
wallet2.addKey(receiveKey);
chain.addWallet(wallet2); chain.addWallet(wallet2);
Address addressToSendTo = receiveKey.toAddress(unitTestParams); Address addressToSendTo = receiveKey.toAddress(unitTestParams);
// Create a block, sending the coinbase to the coinbaseTo address (which is in the wallet). // Create a block, sending the coinbase to the coinbaseTo address (which is in the wallet).
Block b1 = unitTestParams.getGenesisBlock().createNextBlockWithCoinbase(wallet.getKeys().get(0).getPubKey()); Block b1 = unitTestParams.getGenesisBlock().createNextBlockWithCoinbase(wallet.currentReceiveKey().getPubKey());
chain.add(b1); chain.add(b1);
// Check a transaction has been received. // Check a transaction has been received.

View File

@ -57,17 +57,13 @@ public class BloomFilterTest {
assertTrue(addr.toString().equals("17Wx1GQfyPTNWpQMHrTwRSMTCAonSiZx9e")); assertTrue(addr.toString().equals("17Wx1GQfyPTNWpQMHrTwRSMTCAonSiZx9e"));
Wallet wallet = new Wallet(params); Wallet wallet = new Wallet(params);
// Check that the wallet was created with no keys wallet.importKey(privKey.getKey());
// If wallets ever get created with keys, this test needs redone.
for (ECKey key : wallet.getKeys())
fail();
wallet.addKey(privKey.getKey());
// Add a random key which happens to have been used in a recent generation // Add a random key which happens to have been used in a recent generation
wallet.addKey(new ECKey(null, Hex.decode("03cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99"))); wallet.importKey(ECKey.fromPublicOnly(Hex.decode("03cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99")));
wallet.commitTx(new Transaction(params, Hex.decode("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d038754030114062f503253482fffffffff01c05e559500000000232103cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99ac00000000"))); wallet.commitTx(new Transaction(params, Hex.decode("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d038754030114062f503253482fffffffff01c05e559500000000232103cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99ac00000000")));
// We should have 2 per pubkey, and one for the pay-2-pubkey output we have // We should have 2 per pubkey, and one for the pay-2-pubkey output we have
assertTrue(wallet.getBloomFilterElementCount() == 5); assertEquals(5, wallet.getBloomFilterElementCount());
BloomFilter filter = wallet.getBloomFilter(wallet.getBloomFilterElementCount(), 0.001, 0); BloomFilter filter = wallet.getBloomFilter(wallet.getBloomFilterElementCount(), 0.001, 0);

View File

@ -56,12 +56,13 @@ public class ChainSplitTest {
Wallet.SendRequest.DEFAULT_FEE_PER_KB = BigInteger.ZERO; Wallet.SendRequest.DEFAULT_FEE_PER_KB = BigInteger.ZERO;
unitTestParams = UnitTestParams.get(); unitTestParams = UnitTestParams.get();
wallet = new Wallet(unitTestParams); wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey()); wallet.setKeychainLookaheadSize(5); // Make tests faster.
wallet.addKey(new ECKey()); ECKey key1 = wallet.freshReceiveKey();
ECKey key2 = wallet.freshReceiveKey();
blockStore = new MemoryBlockStore(unitTestParams); blockStore = new MemoryBlockStore(unitTestParams);
chain = new BlockChain(unitTestParams, wallet, blockStore); chain = new BlockChain(unitTestParams, wallet, blockStore);
coinsTo = wallet.getKeys().get(0).toAddress(unitTestParams); coinsTo = key1.toAddress(unitTestParams);
coinsTo2 = wallet.getKeys().get(1).toAddress(unitTestParams); coinsTo2 = key2.toAddress(unitTestParams);
someOtherGuy = new ECKey().toAddress(unitTestParams); someOtherGuy = new ECKey().toAddress(unitTestParams);
} }
@ -583,7 +584,7 @@ public class ChainSplitTest {
}, Threading.SAME_THREAD); }, Threading.SAME_THREAD);
Block b1 = unitTestParams.getGenesisBlock().createNextBlock(someOtherGuy); Block b1 = unitTestParams.getGenesisBlock().createNextBlock(someOtherGuy);
final ECKey coinsTo2 = wallet.getKeys().get(1); final ECKey coinsTo2 = wallet.freshReceiveKey();
Block b2 = b1.createNextBlockWithCoinbase(coinsTo2.getPubKey()); Block b2 = b1.createNextBlockWithCoinbase(coinsTo2.getPubKey());
Block b3 = b2.createNextBlock(someOtherGuy); Block b3 = b2.createNextBlock(someOtherGuy);

View File

@ -67,7 +67,7 @@ public class CoinbaseBlockTest {
assertNotNull(miningKey); assertNotNull(miningKey);
Wallet wallet = new Wallet(params); Wallet wallet = new Wallet(params);
wallet.addKey(miningKey); wallet.importKey(miningKey);
// Initial balance should be zero by construction. // Initial balance should be zero by construction.
assertEquals(BigInteger.ZERO, wallet.getBalance()); assertEquals(BigInteger.ZERO, wallet.getBalance());

View File

@ -16,7 +16,7 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import com.google.bitcoin.crypto.EncryptedPrivateKey; import com.google.bitcoin.crypto.EncryptedData;
import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterScrypt; import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.crypto.TransactionSignature; import com.google.bitcoin.crypto.TransactionSignature;
@ -32,12 +32,16 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos; import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters; import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.encoders.DecoderException; import org.spongycastle.util.encoders.DecoderException;
import org.spongycastle.math.ec.ECCurve;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.util.encoders.Hex; import org.spongycastle.util.encoders.Hex;
import java.io.InputStream; import java.io.InputStream;
@ -108,7 +112,7 @@ public class ECKeyTest {
// Test that we can construct an ECKey from a private key (deriving the public from the private), then signing // Test that we can construct an ECKey from a private key (deriving the public from the private), then signing
// a message with it. // a message with it.
BigInteger privkey = new BigInteger(1, Hex.decode("180cb41c7c600be951b5d3d0a7334acc7506173875834f7a6c4c786a28fcbb19")); BigInteger privkey = new BigInteger(1, Hex.decode("180cb41c7c600be951b5d3d0a7334acc7506173875834f7a6c4c786a28fcbb19"));
ECKey key = new ECKey(privkey); ECKey key = ECKey.fromPrivate(privkey);
byte[] output = key.sign(Sha256Hash.ZERO_HASH).encodeToDER(); byte[] output = key.sign(Sha256Hash.ZERO_HASH).encodeToDER();
assertTrue(key.verify(Sha256Hash.ZERO_HASH.getBytes(), output)); assertTrue(key.verify(Sha256Hash.ZERO_HASH.getBytes(), output));
@ -157,7 +161,7 @@ public class ECKeyTest {
// Now re-encode and decode the ASN.1 to see if it is equivalent (it does not produce the exact same byte // Now re-encode and decode the ASN.1 to see if it is equivalent (it does not produce the exact same byte
// sequence, some integers are padded now). // sequence, some integers are padded now).
ECKey roundtripKey = ECKey roundtripKey =
new ECKey(decodedKey.getPrivKeyBytes(), decodedKey.getPubKey()); ECKey.fromPrivateAndPrecalculatedPublic(decodedKey.getPrivKey(), decodedKey.getPubKeyPoint());
for (ECKey key : new ECKey[] {decodedKey, roundtripKey}) { for (ECKey key : new ECKey[] {decodedKey, roundtripKey}) {
byte[] message = reverseBytes(Hex.decode( byte[] message = reverseBytes(Hex.decode(
@ -244,10 +248,11 @@ public class ECKeyTest {
String message = "Hello World!"; String message = "Hello World!";
Sha256Hash hash = Sha256Hash.create(message.getBytes()); Sha256Hash hash = Sha256Hash.create(message.getBytes());
ECKey.ECDSASignature sig = key.sign(hash); ECKey.ECDSASignature sig = key.sign(hash);
key = new ECKey(null, key.getPubKey()); key = ECKey.fromPublicOnly(key.getPubKeyPoint());
boolean found = false; boolean found = false;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
ECKey key2 = ECKey.recoverFromSignature(i, sig, hash, true); ECKey key2 = ECKey.recoverFromSignature(i, sig, hash, true);
checkNotNull(key2);
if (key.equals(key2)) { if (key.equals(key2)) {
found = true; found = true;
break; break;
@ -267,7 +272,7 @@ public class ECKeyTest {
ECKey encryptedKey = key.encrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1)); ECKey encryptedKey = key.encrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1));
assertEquals(time, encryptedKey.getCreationTimeSeconds()); assertEquals(time, encryptedKey.getCreationTimeSeconds());
assertTrue(encryptedKey.isEncrypted()); assertTrue(encryptedKey.isEncrypted());
assertNull(encryptedKey.getPrivKeyBytes()); assertNull(encryptedKey.getSecretBytes());
key = encryptedKey.decrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1)); key = encryptedKey.decrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1));
assertTrue(!key.isEncrypted()); assertTrue(!key.isEncrypted());
assertArrayEquals(originalPrivateKeyBytes, key.getPrivKeyBytes()); assertArrayEquals(originalPrivateKeyBytes, key.getPrivKeyBytes());
@ -278,22 +283,10 @@ public class ECKeyTest {
ECKey unencryptedKey = new ECKey(); ECKey unencryptedKey = new ECKey();
byte[] originalPrivateKeyBytes = checkNotNull(unencryptedKey.getPrivKeyBytes()); byte[] originalPrivateKeyBytes = checkNotNull(unencryptedKey.getPrivKeyBytes());
log.info("Original private key = " + Utils.bytesToHexString(originalPrivateKeyBytes)); log.info("Original private key = " + Utils.bytesToHexString(originalPrivateKeyBytes));
EncryptedData encryptedPrivateKey = keyCrypter.encrypt(unencryptedKey.getPrivKeyBytes(), keyCrypter.deriveKey(PASSWORD1));
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(unencryptedKey.getPrivKeyBytes(), keyCrypter.deriveKey(PASSWORD1)); ECKey encryptedKey = ECKey.fromEncrypted(encryptedPrivateKey, keyCrypter, unencryptedKey.getPubKey());
ECKey encryptedKey = new ECKey(encryptedPrivateKey, unencryptedKey.getPubKey(), keyCrypter); assertTrue(encryptedKey.isEncrypted());
assertNull(encryptedKey.getSecretBytes());
// The key should initially be encrypted
assertTrue("Key not encrypted at start", encryptedKey.isEncrypted());
// The unencrypted private key bytes of the encrypted keychain should all be blank.
byte[] privateKeyBytes = encryptedKey.getPrivKeyBytes();
if (privateKeyBytes != null) {
for (int i = 0; i < privateKeyBytes.length; i++) {
assertEquals("Byte " + i + " of the private key was not zero but should be", 0, privateKeyBytes[i]);
}
}
// Decrypt the key.
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1)); ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1));
assertTrue(!rebornUnencryptedKey.isEncrypted()); assertTrue(!rebornUnencryptedKey.isEncrypted());
assertArrayEquals(originalPrivateKeyBytes, rebornUnencryptedKey.getPrivKeyBytes()); assertArrayEquals(originalPrivateKeyBytes, rebornUnencryptedKey.getPrivKeyBytes());
@ -302,8 +295,8 @@ public class ECKeyTest {
@Test @Test
public void testEncryptionIsReversible() throws Exception { public void testEncryptionIsReversible() throws Exception {
ECKey originalUnencryptedKey = new ECKey(); ECKey originalUnencryptedKey = new ECKey();
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(originalUnencryptedKey.getPrivKeyBytes(), keyCrypter.deriveKey(PASSWORD1)); EncryptedData encryptedPrivateKey = keyCrypter.encrypt(originalUnencryptedKey.getPrivKeyBytes(), keyCrypter.deriveKey(PASSWORD1));
ECKey encryptedKey = new ECKey(encryptedPrivateKey, originalUnencryptedKey.getPubKey(), keyCrypter); ECKey encryptedKey = ECKey.fromEncrypted(encryptedPrivateKey, keyCrypter, originalUnencryptedKey.getPubKey());
// The key should be encrypted // The key should be encrypted
assertTrue("Key not encrypted at start", encryptedKey.isEncrypted()); assertTrue("Key not encrypted at start", encryptedKey.isEncrypted());
@ -316,18 +309,18 @@ public class ECKeyTest {
// Change one of the encrypted key bytes (this is to simulate a faulty keyCrypter). // Change one of the encrypted key bytes (this is to simulate a faulty keyCrypter).
// Encryption should not be reversible // Encryption should not be reversible
byte[] goodEncryptedPrivateKeyBytes = encryptedPrivateKey.getEncryptedBytes(); byte[] goodEncryptedPrivateKeyBytes = encryptedPrivateKey.encryptedBytes;
// Break the encrypted private key and check it is broken. // Break the encrypted private key and check it is broken.
byte[] badEncryptedPrivateKeyBytes = new byte[goodEncryptedPrivateKeyBytes.length]; byte[] badEncryptedPrivateKeyBytes = new byte[goodEncryptedPrivateKeyBytes.length];
encryptedPrivateKey.setEncryptedPrivateBytes(badEncryptedPrivateKeyBytes); encryptedPrivateKey = new EncryptedData(encryptedPrivateKey.initialisationVector, badEncryptedPrivateKeyBytes);
ECKey badEncryptedKey = new ECKey(encryptedPrivateKey, originalUnencryptedKey.getPubKey(), keyCrypter); ECKey badEncryptedKey = ECKey.fromEncrypted(encryptedPrivateKey, keyCrypter, originalUnencryptedKey.getPubKey());
assertTrue("Key encryption is reversible with faulty encrypted bytes", !ECKey.encryptionIsReversible(originalUnencryptedKey, badEncryptedKey, keyCrypter, keyCrypter.deriveKey(PASSWORD1))); assertTrue("Key encryption is reversible with faulty encrypted bytes", !ECKey.encryptionIsReversible(originalUnencryptedKey, badEncryptedKey, keyCrypter, keyCrypter.deriveKey(PASSWORD1)));
} }
@Test @Test
public void testToString() throws Exception { public void testToString() throws Exception {
ECKey key = new ECKey(BigInteger.TEN); // An example private key. ECKey key = ECKey.fromPrivate(BigInteger.TEN).decompress(); // An example private key.
assertEquals("pub:04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7", key.toString()); assertEquals("pub:04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7", key.toString());
assertEquals("pub:04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7 priv:0a", key.toStringWithPrivate()); assertEquals("pub:04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7 priv:0a", key.toStringWithPrivate());
@ -342,10 +335,11 @@ public class ECKeyTest {
String message = "Goodbye Jupiter!"; String message = "Goodbye Jupiter!";
Sha256Hash hash = Sha256Hash.create(message.getBytes()); Sha256Hash hash = Sha256Hash.create(message.getBytes());
ECKey.ECDSASignature sig = encryptedKey.sign(hash, aesKey); ECKey.ECDSASignature sig = encryptedKey.sign(hash, aesKey);
unencryptedKey = new ECKey(null, unencryptedKey.getPubKey()); unencryptedKey = ECKey.fromPublicOnly(unencryptedKey.getPubKeyPoint());
boolean found = false; boolean found = false;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
ECKey key2 = ECKey.recoverFromSignature(i, sig, hash, true); ECKey key2 = ECKey.recoverFromSignature(i, sig, hash, true);
checkNotNull(key2);
if (unencryptedKey.equals(key2)) { if (unencryptedKey.equals(key2)) {
found = true; found = true;
break; break;
@ -372,19 +366,13 @@ public class ECKeyTest {
ECKey encryptedKey = (new ECKey()).encrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1)); ECKey encryptedKey = (new ECKey()).encrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1));
checkSomeBytesAreNonZero(unencryptedKey.getPrivKeyBytes()); checkSomeBytesAreNonZero(unencryptedKey.getPrivKeyBytes());
unencryptedKey.clearPrivateKey();
checkAllBytesAreZero(unencryptedKey.getPrivKeyBytes());
// The encryptedPrivateKey should be null in an unencrypted ECKey anyhow but check all the same. // The encryptedPrivateKey should be null in an unencrypted ECKey anyhow but check all the same.
assertTrue(unencryptedKey.getEncryptedPrivateKey() == null); assertTrue(unencryptedKey.getEncryptedPrivateKey() == null);
checkSomeBytesAreNonZero(encryptedKey.getPrivKeyBytes()); checkSomeBytesAreNonZero(encryptedKey.getSecretBytes());
checkSomeBytesAreNonZero(encryptedKey.getEncryptedPrivateKey().getEncryptedBytes()); checkSomeBytesAreNonZero(encryptedKey.getEncryptedPrivateKey().encryptedBytes);
checkSomeBytesAreNonZero(encryptedKey.getEncryptedPrivateKey().getInitialisationVector()); checkSomeBytesAreNonZero(encryptedKey.getEncryptedPrivateKey().initialisationVector);
encryptedKey.clearPrivateKey();
checkAllBytesAreZero(encryptedKey.getPrivKeyBytes());
checkAllBytesAreZero(encryptedKey.getEncryptedPrivateKey().getEncryptedBytes());
checkAllBytesAreZero(encryptedKey.getEncryptedPrivateKey().getInitialisationVector());
} }
@Test @Test
@ -460,10 +448,4 @@ public class ECKeyTest {
for (byte b : bytes) if (b != 0) return true; for (byte b : bytes) if (b != 0) return true;
return false; return false;
} }
private static boolean checkAllBytesAreZero(byte[] bytes) {
if (bytes == null) return true;
for (byte b : bytes) if (b != 0) return false;
return true;
}
} }

View File

@ -101,10 +101,10 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
// A wallet which contains a pubkey used in each transaction from above // A wallet which contains a pubkey used in each transaction from above
Wallet wallet = new Wallet(unitTestParams); Wallet wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey(null, Hex.decode("04b27f7e9475ccf5d9a431cb86d665b8302c140144ec2397fce792f4a4e7765fecf8128534eaa71df04f93c74676ae8279195128a1506ebf7379d23dab8fca0f63"))); wallet.importKey(ECKey.fromPublicOnly(Hex.decode("04b27f7e9475ccf5d9a431cb86d665b8302c140144ec2397fce792f4a4e7765fecf8128534eaa71df04f93c74676ae8279195128a1506ebf7379d23dab8fca0f63")));
wallet.addKey(new ECKey(null, Hex.decode("04732012cb962afa90d31b25d8fb0e32c94e513ab7a17805c14ca4c3423e18b4fb5d0e676841733cb83abaf975845c9f6f2a8097b7d04f4908b18368d6fc2d68ec"))); wallet.importKey(ECKey.fromPublicOnly(Hex.decode("04732012cb962afa90d31b25d8fb0e32c94e513ab7a17805c14ca4c3423e18b4fb5d0e676841733cb83abaf975845c9f6f2a8097b7d04f4908b18368d6fc2d68ec")));
wallet.addKey(new ECKey(null, Hex.decode("04cfb4113b3387637131ebec76871fd2760fc430dd16de0110f0eb07bb31ffac85e2607c189cb8582ea1ccaeb64ffd655409106589778f3000fdfe3263440b0350"))); wallet.importKey(ECKey.fromPublicOnly(Hex.decode("04cfb4113b3387637131ebec76871fd2760fc430dd16de0110f0eb07bb31ffac85e2607c189cb8582ea1ccaeb64ffd655409106589778f3000fdfe3263440b0350")));
wallet.addKey(new ECKey(null, Hex.decode("04b2f30018908a59e829c1534bfa5010d7ef7f79994159bba0f534d863ef9e4e973af6a8de20dc41dbea50bc622263ec8a770b2c9406599d39e4c9afe61f8b1613"))); wallet.importKey(ECKey.fromPublicOnly(Hex.decode("04b2f30018908a59e829c1534bfa5010d7ef7f79994159bba0f534d863ef9e4e973af6a8de20dc41dbea50bc622263ec8a770b2c9406599d39e4c9afe61f8b1613")));
BloomFilter filter = wallet.getBloomFilter(wallet.getKeychainSize()*2, 0.001, 0xDEADBEEF); BloomFilter filter = wallet.getBloomFilter(wallet.getKeychainSize()*2, 0.001, 0xDEADBEEF);
// Compare the serialized bloom filter to a known-good value // Compare the serialized bloom filter to a known-good value

View File

@ -882,7 +882,7 @@ public class FullBlockTestGenerator {
// A valid block created exactly like b44 to make sure the creation itself works // A valid block created exactly like b44 to make sure the creation itself works
Block b44 = new Block(params); Block b44 = new Block(params);
byte[] outScriptBytes = ScriptBuilder.createOutputScript(new ECKey(null, coinbaseOutKeyPubKey)).getProgram(); byte[] outScriptBytes = ScriptBuilder.createOutputScript(ECKey.fromPublicOnly(coinbaseOutKeyPubKey)).getProgram();
{ {
b44.setDifficultyTarget(b43.getDifficultyTarget()); b44.setDifficultyTarget(b43.getDifficultyTarget());
b44.addCoinbaseTransaction(coinbaseOutKeyPubKey, BigInteger.ZERO); b44.addCoinbaseTransaction(coinbaseOutKeyPubKey, BigInteger.ZERO);
@ -1633,7 +1633,7 @@ public class FullBlockTestGenerator {
// Entirely invalid scriptPubKey to ensure we aren't pre-verifying too much // Entirely invalid scriptPubKey to ensure we aren't pre-verifying too much
t.addOutput(new TransactionOutput(params, t, BigInteger.valueOf(0), new byte[] {OP_PUSHDATA1 - 1 })); t.addOutput(new TransactionOutput(params, t, BigInteger.valueOf(0), new byte[] {OP_PUSHDATA1 - 1 }));
t.addOutput(new TransactionOutput(params, t, BigInteger.valueOf(1), t.addOutput(new TransactionOutput(params, t, BigInteger.valueOf(1),
ScriptBuilder.createOutputScript(new ECKey(null, coinbaseOutKeyPubKey)).getProgram())); ScriptBuilder.createOutputScript(ECKey.fromPublicOnly(coinbaseOutKeyPubKey)).getProgram()));
// Spendable output // Spendable output
t.addOutput(new TransactionOutput(params, t, BigInteger.ZERO, new byte[] {OP_1, uniquenessCounter++})); t.addOutput(new TransactionOutput(params, t, BigInteger.ZERO, new byte[] {OP_1, uniquenessCounter++}));
addOnlyInputToTransaction(t, prevOut); addOnlyInputToTransaction(t, prevOut);

View File

@ -81,17 +81,17 @@ public class LazyParseByteCacheTest {
public void setUp() throws Exception { public void setUp() throws Exception {
unitTestParams = UnitTestParams.get(); unitTestParams = UnitTestParams.get();
wallet = new Wallet(unitTestParams); wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey()); wallet.freshReceiveKey();
resetBlockStore(); resetBlockStore();
Transaction tx1 = createFakeTx(unitTestParams, Transaction tx1 = createFakeTx(unitTestParams,
Utils.toNanoCoins(2, 0), Utils.toNanoCoins(2, 0),
wallet.getKeys().get(0).toAddress(unitTestParams)); wallet.currentReceiveKey().toAddress(unitTestParams));
//add a second input so can test granularity of byte cache. //add a second input so can test granularity of byte cache.
Transaction prevTx = new Transaction(unitTestParams); Transaction prevTx = new Transaction(unitTestParams);
TransactionOutput prevOut = new TransactionOutput(unitTestParams, prevTx, Utils.toNanoCoins(1, 0), wallet.getKeys().get(0).toAddress(unitTestParams)); TransactionOutput prevOut = new TransactionOutput(unitTestParams, prevTx, Utils.toNanoCoins(1, 0), wallet.currentReceiveKey().toAddress(unitTestParams));
prevTx.addOutput(prevOut); prevTx.addOutput(prevOut);
// Connect it. // Connect it.
tx1.addInput(prevOut); tx1.addInput(prevOut);

View File

@ -44,6 +44,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -355,7 +356,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
Wallet w2 = new Wallet(params); Wallet w2 = new Wallet(params);
ECKey key1 = new ECKey(); ECKey key1 = new ECKey();
key1.setCreationTimeSeconds(now - 86400); // One day ago. key1.setCreationTimeSeconds(now - 86400); // One day ago.
w2.addKey(key1); w2.importKey(key1);
peerGroup.addWallet(w2); peerGroup.addWallet(w2);
peerGroup.waitForJobQueue(); peerGroup.waitForJobQueue();
assertEquals(peerGroup.getFastCatchupTimeSecs(), now - 86400 - WEEK); assertEquals(peerGroup.getFastCatchupTimeSecs(), now - 86400 - WEEK);
@ -363,7 +364,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
// due to the need to avoid complicated lock inversions. // due to the need to avoid complicated lock inversions.
ECKey key2 = new ECKey(); ECKey key2 = new ECKey();
key2.setCreationTimeSeconds(now - 100000); key2.setCreationTimeSeconds(now - 100000);
w2.addKey(key2); w2.importKey(key2);
peerGroup.waitForJobQueue(); peerGroup.waitForJobQueue();
assertEquals(peerGroup.getFastCatchupTimeSecs(), now - WEEK - 100000); assertEquals(peerGroup.getFastCatchupTimeSecs(), now - WEEK - 100000);
} }
@ -442,10 +443,13 @@ public class PeerGroupTest extends TestWithPeerGroup {
final SettableFuture<Void> peerConnectedFuture = SettableFuture.create(); final SettableFuture<Void> peerConnectedFuture = SettableFuture.create();
final SettableFuture<Void> peerDisconnectedFuture = SettableFuture.create(); final SettableFuture<Void> peerDisconnectedFuture = SettableFuture.create();
peerGroup.addEventListener(new AbstractPeerEventListener() { peerGroup.addEventListener(new AbstractPeerEventListener() {
@Override public void onPeerConnected(Peer peer, int peerCount) { @Override
public void onPeerConnected(Peer peer, int peerCount) {
peerConnectedFuture.set(null); peerConnectedFuture.set(null);
} }
@Override public void onPeerDisconnected(Peer peer, int peerCount) {
@Override
public void onPeerDisconnected(Peer peer, int peerCount) {
peerDisconnectedFuture.set(null); peerDisconnectedFuture.set(null);
} }
}, Threading.SAME_THREAD); }, Threading.SAME_THREAD);
@ -531,7 +535,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
// the same procedure. However a new node that's connected should get the fresh filter. // the same procedure. However a new node that's connected should get the fresh filter.
peerGroup.startAsync(); peerGroup.startAsync();
peerGroup.awaitRunning(); peerGroup.awaitRunning();
final ECKey key = wallet.getKeys().get(0); final ECKey key = wallet.currentReceiveKey();
// Create a couple of peers. // Create a couple of peers.
InboundMessageQueuer p1 = connectPeer(1); InboundMessageQueuer p1 = connectPeer(1);
InboundMessageQueuer p2 = connectPeer(2); InboundMessageQueuer p2 = connectPeer(2);
@ -565,15 +569,35 @@ public class PeerGroupTest extends TestWithPeerGroup {
// Create a couple of peers. // Create a couple of peers.
InboundMessageQueuer p1 = connectPeer(1); InboundMessageQueuer p1 = connectPeer(1);
InboundMessageQueuer p2 = connectPeer(2); InboundMessageQueuer p2 = connectPeer(2);
peerGroup.waitForJobQueue();
BloomFilter f1 = p1.lastReceivedFilter; BloomFilter f1 = p1.lastReceivedFilter;
BloomFilter f2 = p2.lastReceivedFilter; BloomFilter f2 = p2.lastReceivedFilter;
final ECKey key = new ECKey(); ECKey key = null;
wallet.addKey(key); // We have to run ahead of the lookahead zone for this test.
peerGroup.waitForJobQueue(); for (int i = 0; i < wallet.getKeychainLookaheadSize() + 1; i++) {
BloomFilter f3 = (BloomFilter) outbound(p1); key = wallet.freshReceiveKey();
BloomFilter f4 = (BloomFilter) outbound(p2); // Wait here. Bloom filters are recalculated asynchronously so if we didn't wait, we might not pass the
assertTrue(outbound(p1) instanceof MemoryPoolMessage); // test below where we expect each key to generate a new filter because this thread could generate all
assertTrue(outbound(p2) instanceof MemoryPoolMessage); // the keys before the peergroup thread does the recalculation, causing only one filter to be sent.
peerGroup.waitForJobQueue();
}
BloomFilter f3 = null;
BloomFilter f4 = null;
// Each time we request a fresh key, a new filter is sent. That's because the lookahead buffer is NOT an
// optimisation (currently), but rather is intended to try and ensure we don't miss transactions when
// catching up through the chain.
for (int i = 0; i < wallet.getKeychainLookaheadSize(); i++) {
f3 = (BloomFilter) outbound(p1);
assertNotNull(f3);
assertEquals(MemoryPoolMessage.class, outbound(p1).getClass());
f4 = (BloomFilter) outbound(p2);
assertNotNull(f4);
assertEquals(MemoryPoolMessage.class, outbound(p2).getClass());
}
checkNotNull(f3);
checkNotNull(f4);
checkNotNull(key);
// Check the last filter received.
assertNotEquals(f1, f3); assertNotEquals(f1, f3);
assertNotEquals(f2, f4); assertNotEquals(f2, f4);
assertEquals(f3, f4); assertEquals(f3, f4);

View File

@ -653,9 +653,8 @@ public class PeerTest extends TestWithNetworkConnections {
connectWithVersion(useNotFound ? 70001 : 60001); connectWithVersion(useNotFound ? 70001 : 60001);
// Test that if we receive a relevant transaction that has a lock time, it doesn't result in a notification // Test that if we receive a relevant transaction that has a lock time, it doesn't result in a notification
// until we explicitly opt in to seeing those. // until we explicitly opt in to seeing those.
ECKey key = new ECKey();
Wallet wallet = new Wallet(unitTestParams); Wallet wallet = new Wallet(unitTestParams);
wallet.addKey(key); ECKey key = wallet.freshReceiveKey();
peer.addWallet(wallet); peer.addWallet(wallet);
final Transaction[] vtx = new Transaction[1]; final Transaction[] vtx = new Transaction[1];
wallet.addEventListener(new AbstractWalletEventListener() { wallet.addEventListener(new AbstractWalletEventListener() {
@ -726,9 +725,8 @@ public class PeerTest extends TestWithNetworkConnections {
private void checkTimeLockedDependency(boolean shouldAccept, boolean useNotFound) throws Exception { private void checkTimeLockedDependency(boolean shouldAccept, boolean useNotFound) throws Exception {
// Initial setup. // Initial setup.
connectWithVersion(useNotFound ? 70001 : 60001); connectWithVersion(useNotFound ? 70001 : 60001);
ECKey key = new ECKey();
Wallet wallet = new Wallet(unitTestParams); Wallet wallet = new Wallet(unitTestParams);
wallet.addKey(key); ECKey key = wallet.freshReceiveKey();
wallet.setAcceptRiskyTransactions(shouldAccept); wallet.setAcceptRiskyTransactions(shouldAccept);
peer.addWallet(wallet); peer.addWallet(wallet);
final Transaction[] vtx = new Transaction[1]; final Transaction[] vtx = new Transaction[1];

View File

@ -113,7 +113,12 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
Address dest = new ECKey().toAddress(params); Address dest = new ECKey().toAddress(params);
Wallet.SendResult sendResult = wallet.sendCoins(peerGroup, dest, Utils.toNanoCoins(1, 0)); Wallet.SendResult sendResult = wallet.sendCoins(peerGroup, dest, Utils.toNanoCoins(1, 0));
assertFalse(sendResult.broadcastComplete.isDone()); assertFalse(sendResult.broadcastComplete.isDone());
Transaction t1 = (Transaction) outbound(p1); Transaction t1;
{
Message m;
while (!((m = outbound(p1)) instanceof Transaction));
t1 = (Transaction) m;
}
assertFalse(sendResult.broadcastComplete.isDone()); assertFalse(sendResult.broadcastComplete.isDone());
// p1 eats it :( A bit later the PeerGroup is taken down. // p1 eats it :( A bit later the PeerGroup is taken down.
@ -162,7 +167,15 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
assertEquals(transactions[0], sendResult.tx); assertEquals(transactions[0], sendResult.tx);
assertEquals(0, transactions[0].getConfidence().numBroadcastPeers()); assertEquals(0, transactions[0].getConfidence().numBroadcastPeers());
transactions[0] = null; transactions[0] = null;
Transaction t1 = (Transaction) outbound(p1); Transaction t1;
{
peerGroup.waitForJobQueue();
Message m = outbound(p1);
// Hack: bloom filters are recalculated asynchronously to sending transactions to avoid lock
// inversion, so we might or might not get the filter/mempool message first or second.
while (!(m instanceof Transaction)) m = outbound(p1);
t1 = (Transaction) m;
}
assertNotNull(t1); assertNotNull(t1);
// 49 BTC in change. // 49 BTC in change.
assertEquals(Utils.toNanoCoins(49, 0), t1.getValueSentToMe(wallet)); assertEquals(Utils.toNanoCoins(49, 0), t1.getValueSentToMe(wallet));
@ -189,7 +202,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
assertNull(outbound(p1)); // Nothing sent. assertNull(outbound(p1)); // Nothing sent.
// Add the wallet to the peer group (simulate initialization). Transactions should be announced. // Add the wallet to the peer group (simulate initialization). Transactions should be announced.
peerGroup.addWallet(wallet); peerGroup.addWallet(wallet);
// Transaction announced to the first peer. // Transaction announced to the first peer. No extra Bloom filter because no change address was needed.
assertEquals(t3.getHash(), ((Transaction) outbound(p1)).getHash()); assertEquals(t3.getHash(), ((Transaction) outbound(p1)).getHash());
} }
} }

View File

@ -30,14 +30,15 @@ import com.google.bitcoin.testing.TestWithWallet;
import com.google.bitcoin.utils.Threading; import com.google.bitcoin.utils.Threading;
import com.google.bitcoin.wallet.*; import com.google.bitcoin.wallet.*;
import com.google.bitcoin.wallet.WalletTransaction.Pool; import com.google.bitcoin.wallet.WalletTransaction.Pool;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos; import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType; import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -45,10 +46,14 @@ import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.encoders.Hex; import org.spongycastle.util.encoders.Hex;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.InetAddress; import java.net.InetAddress;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.*; import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -63,7 +68,6 @@ public class WalletTest extends TestWithWallet {
private static final Logger log = LoggerFactory.getLogger(WalletTest.class); private static final Logger log = LoggerFactory.getLogger(WalletTest.class);
private Address myEncryptedAddress; private Address myEncryptedAddress;
private Address myEncryptedAddress2;
private Wallet encryptedWallet; private Wallet encryptedWallet;
@ -79,18 +83,13 @@ public class WalletTest extends TestWithWallet {
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH]; encryptedWallet = new Wallet(params);
secureRandom.nextBytes(salt); encryptedWallet.setKeychainLookaheadSize(5); // For speed.
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt)); myEncryptedAddress = encryptedWallet.freshReceiveKey().toAddress(params);
ScryptParameters scryptParameters = scryptParametersBuilder.build(); encryptedWallet.encrypt(PASSWORD1);
keyCrypter = new KeyCrypterScrypt(scryptParameters); keyCrypter = encryptedWallet.getKeyCrypter();
encryptedWallet = new Wallet(params, keyCrypter);
aesKey = keyCrypter.deriveKey(PASSWORD1); aesKey = keyCrypter.deriveKey(PASSWORD1);
wrongAesKey = keyCrypter.deriveKey(WRONG_PASSWORD); wrongAesKey = keyCrypter.deriveKey(WRONG_PASSWORD);
ECKey myEncryptedKey = encryptedWallet.addNewEncryptedKey(keyCrypter, aesKey);
myEncryptedAddress = myEncryptedKey.toAddress(params);
} }
@After @After
@ -253,9 +252,8 @@ public class WalletTest extends TestWithWallet {
try { try {
req.ensureMinRequiredFee = false; req.ensureMinRequiredFee = false;
wallet.completeTx(req); wallet.completeTx(req);
fail("No exception was thrown trying to sign an encrypted key with no password supplied."); fail();
} catch (KeyCrypterException kce) { } catch (ECKey.MissingPrivateKeyException kce) {
assertEquals("This ECKey is encrypted but no decryption key has been supplied.", kce.getMessage());
} }
assertEquals("Wrong number of UNSPENT.1", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); assertEquals("Wrong number of UNSPENT.1", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL.1", 1, wallet.getTransactions(true).size()); assertEquals("Wrong number of ALL.1", 1, wallet.getTransactions(true).size());
@ -339,9 +337,9 @@ public class WalletTest extends TestWithWallet {
// Having a test for deprecated method getFromAddress() is no evil so we suppress the warning here. // Having a test for deprecated method getFromAddress() is no evil so we suppress the warning here.
private void basicSanityChecks(Wallet wallet, Transaction t, Address fromAddress, Address destination) throws VerificationException { private void basicSanityChecks(Wallet wallet, Transaction t, Address fromAddress, Address destination) throws VerificationException {
assertEquals("Wrong number of tx inputs", 1, t.getInputs().size()); assertEquals("Wrong number of tx inputs", 1, t.getInputs().size());
assertEquals(fromAddress, t.getInputs().get(0).getScriptSig().getFromAddress(params)); assertEquals(fromAddress, t.getInput(0).getScriptSig().getFromAddress(params));
assertEquals("Wrong number of tx outputs",2, t.getOutputs().size()); assertEquals("Wrong number of tx outputs",2, t.getOutputs().size());
assertEquals(destination, t.getOutputs().get(0).getScriptPubKey().getToAddress(params)); assertEquals(destination, t.getOutput(0).getScriptPubKey().getToAddress(params));
assertEquals(wallet.getChangeAddress(), t.getOutputs().get(1).getScriptPubKey().getToAddress(params)); assertEquals(wallet.getChangeAddress(), t.getOutputs().get(1).getScriptPubKey().getToAddress(params));
assertEquals(toNanoCoins(0, 49), t.getOutputs().get(1).getValue()); assertEquals(toNanoCoins(0, 49), t.getOutputs().get(1).getValue());
// Check the script runs and signatures verify. // Check the script runs and signatures verify.
@ -417,7 +415,7 @@ public class WalletTest extends TestWithWallet {
// Do some basic sanity checks. // Do some basic sanity checks.
assertEquals(1, t2.getInputs().size()); assertEquals(1, t2.getInputs().size());
assertEquals(myAddress, t2.getInputs().get(0).getScriptSig().getFromAddress(params)); assertEquals(myAddress, t2.getInput(0).getScriptSig().getFromAddress(params));
assertEquals(TransactionConfidence.ConfidenceType.UNKNOWN, t2.getConfidence().getConfidenceType()); assertEquals(TransactionConfidence.ConfidenceType.UNKNOWN, t2.getConfidence().getConfidenceType());
// We have NOT proven that the signature is correct! // We have NOT proven that the signature is correct!
@ -989,21 +987,23 @@ public class WalletTest extends TestWithWallet {
@Test @Test
public void keyCreationTime() throws Exception { public void keyCreationTime() throws Exception {
wallet = new Wallet(params); wallet = new Wallet(params);
wallet.setKeychainLookaheadSize(5); // For speed.
Utils.setMockClock(); Utils.setMockClock();
long now = Utils.currentTimeSeconds(); long now = Utils.currentTimeSeconds();
// No keys returns current time. // No keys returns current time.
assertEquals(now, wallet.getEarliestKeyCreationTime()); assertEquals(now, wallet.getEarliestKeyCreationTime());
Utils.rollMockClock(60); Utils.rollMockClock(60);
wallet.addKey(new ECKey()); wallet.freshReceiveKey();
assertEquals(now + 60, wallet.getEarliestKeyCreationTime()); assertEquals(now + 60, wallet.getEarliestKeyCreationTime());
Utils.rollMockClock(60); Utils.rollMockClock(60);
wallet.addKey(new ECKey()); wallet.freshReceiveKey();
assertEquals(now + 60, wallet.getEarliestKeyCreationTime()); assertEquals(now + 60, wallet.getEarliestKeyCreationTime());
} }
@Test @Test
public void scriptCreationTime() throws Exception { public void scriptCreationTime() throws Exception {
wallet = new Wallet(params); wallet = new Wallet(params);
wallet.setKeychainLookaheadSize(5); // For speed.
Utils.setMockClock(); Utils.setMockClock();
long now = Utils.currentTimeSeconds(); long now = Utils.currentTimeSeconds();
// No keys returns current time. // No keys returns current time.
@ -1012,7 +1012,7 @@ public class WalletTest extends TestWithWallet {
wallet.addWatchedAddress(new ECKey().toAddress(params)); wallet.addWatchedAddress(new ECKey().toAddress(params));
Utils.rollMockClock(60); Utils.rollMockClock(60);
wallet.addKey(new ECKey()); wallet.freshReceiveKey();
assertEquals(now + 60, wallet.getEarliestKeyCreationTime()); assertEquals(now + 60, wallet.getEarliestKeyCreationTime());
} }
@ -1071,8 +1071,7 @@ public class WalletTest extends TestWithWallet {
@Test @Test
public void pubkeyOnlyScripts() throws Exception { public void pubkeyOnlyScripts() throws Exception {
// Verify that we support outputs like OP_PUBKEY and the corresponding inputs. // Verify that we support outputs like OP_PUBKEY and the corresponding inputs.
ECKey key1 = new ECKey(); ECKey key1 = wallet.freshReceiveKey();
wallet.addKey(key1);
BigInteger value = toNanoCoins(5, 0); BigInteger value = toNanoCoins(5, 0);
Transaction t1 = createFakeTx(params, value, key1); Transaction t1 = createFakeTx(params, value, key1);
if (wallet.isPendingTransactionRelevant(t1)) if (wallet.isPendingTransactionRelevant(t1))
@ -1173,10 +1172,9 @@ public class WalletTest extends TestWithWallet {
Sha256Hash hash1 = Sha256Hash.hashFileContents(f); Sha256Hash hash1 = Sha256Hash.hashFileContents(f);
// Start with zero delay and ensure the wallet file changes after adding a key. // Start with zero delay and ensure the wallet file changes after adding a key.
wallet.autosaveToFile(f, 0, TimeUnit.SECONDS, null); wallet.autosaveToFile(f, 0, TimeUnit.SECONDS, null);
ECKey key = new ECKey(); ECKey key = wallet.freshReceiveKey();
wallet.addKey(key);
Sha256Hash hash2 = Sha256Hash.hashFileContents(f); Sha256Hash hash2 = Sha256Hash.hashFileContents(f);
assertFalse("Wallet not saved after addKey", hash1.equals(hash2)); // File has changed. assertFalse("Wallet not saved after generating fresh key", hash1.equals(hash2)); // File has changed.
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key); Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
if (wallet.isPendingTransactionRelevant(t1)) if (wallet.isPendingTransactionRelevant(t1))
@ -1206,8 +1204,7 @@ public class WalletTest extends TestWithWallet {
} }
} }
); );
ECKey key = new ECKey(); ECKey key = wallet.freshReceiveKey();
wallet.addKey(key);
Sha256Hash hash2 = Sha256Hash.hashFileContents(f); Sha256Hash hash2 = Sha256Hash.hashFileContents(f);
assertFalse(hash1.equals(hash2)); // File has changed immediately despite the delay, as keys are important. assertFalse(hash1.equals(hash2)); // File has changed immediately despite the delay, as keys are important.
assertNotNull(results[0]); assertNotNull(results[0]);
@ -1246,7 +1243,7 @@ public class WalletTest extends TestWithWallet {
wallet.shutdownAutosaveAndWait(); wallet.shutdownAutosaveAndWait();
results[0] = results[1] = null; results[0] = results[1] = null;
ECKey key2 = new ECKey(); ECKey key2 = new ECKey();
wallet.addKey(key2); wallet.importKey(key2);
assertEquals(hash5, Sha256Hash.hashFileContents(f)); // File has NOT changed. assertEquals(hash5, Sha256Hash.hashFileContents(f)); // File has NOT changed.
Transaction t2 = createFakeTx(params, toNanoCoins(5, 0), key2); Transaction t2 = createFakeTx(params, toNanoCoins(5, 0), key2);
Block b3 = createFakeBlock(blockStore, t2).block; Block b3 = createFakeBlock(blockStore, t2).block;
@ -1263,8 +1260,7 @@ public class WalletTest extends TestWithWallet {
BigInteger v1 = Utils.toNanoCoins(1, 0); BigInteger v1 = Utils.toNanoCoins(1, 0);
sendMoneyToWallet(v1, AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(v1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
// First create our current transaction // First create our current transaction
ECKey k2 = new ECKey(); ECKey k2 = wallet.freshReceiveKey();
wallet.addKey(k2);
BigInteger v2 = toNanoCoins(0, 50); BigInteger v2 = toNanoCoins(0, 50);
Transaction t2 = new Transaction(params); Transaction t2 = new Transaction(params);
TransactionOutput o2 = new TransactionOutput(params, t2, v2, k2.toAddress(params)); TransactionOutput o2 = new TransactionOutput(params, t2, v2, k2.toAddress(params));
@ -1321,33 +1317,19 @@ public class WalletTest extends TestWithWallet {
@Test @Test
public void encryptionDecryptionBasic() throws Exception { public void encryptionDecryptionBasic() throws Exception {
// Check the wallet is initially of WalletType ENCRYPTED. assertEquals(EncryptionType.ENCRYPTED_SCRYPT_AES, encryptedWallet.getEncryptionType());
assertTrue("Wallet is not an encrypted wallet", encryptedWallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES); assertTrue(encryptedWallet.checkPassword(PASSWORD1));
assertFalse(encryptedWallet.checkPassword(WRONG_PASSWORD));
// Correct password should decrypt first encrypted private key.
assertTrue("checkPassword result is wrong with correct password.2", encryptedWallet.checkPassword(PASSWORD1));
// Incorrect password should not decrypt first encrypted private key.
assertFalse("checkPassword result is wrong with incorrect password.3", encryptedWallet.checkPassword(WRONG_PASSWORD));
// Decrypt wallet.
assertTrue("The keyCrypter is missing but should not be", keyCrypter != null); assertTrue("The keyCrypter is missing but should not be", keyCrypter != null);
encryptedWallet.decrypt(aesKey); encryptedWallet.decrypt(aesKey);
// Wallet should now be unencrypted. // Wallet should now be unencrypted.
assertTrue("Wallet is not an unencrypted wallet", encryptedWallet.getKeyCrypter() == null); assertTrue("Wallet is not an unencrypted wallet", encryptedWallet.getKeyCrypter() == null);
try {
// Correct password should not decrypt first encrypted private key as wallet is unencrypted. encryptedWallet.checkPassword(PASSWORD1);
assertTrue("checkPassword result is wrong with correct password", !encryptedWallet.checkPassword(PASSWORD1)); fail();
} catch (IllegalStateException e) {
// Incorrect password should not decrypt first encrypted private key as wallet is unencrypted. }
assertTrue("checkPassword result is wrong with incorrect password", !encryptedWallet.checkPassword(WRONG_PASSWORD));
// Encrypt wallet.
encryptedWallet.encrypt(keyCrypter, aesKey);
// Wallet should now be of type WalletType.ENCRYPTED_SCRYPT_AES.
assertTrue("Wallet is not an encrypted wallet", encryptedWallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
} }
@Test @Test
@ -1401,52 +1383,39 @@ public class WalletTest extends TestWithWallet {
@Test(expected = KeyCrypterException.class) @Test(expected = KeyCrypterException.class)
public void addUnencryptedKeyToEncryptedWallet() throws Exception { public void addUnencryptedKeyToEncryptedWallet() throws Exception {
ECKey key1 = new ECKey(); ECKey key1 = new ECKey();
encryptedWallet.addKey(key1); encryptedWallet.importKey(key1);
} }
@Test(expected = KeyCrypterException.class) @Test(expected = KeyCrypterException.class)
public void addEncryptedKeyToUnencryptedWallet() throws Exception { public void addEncryptedKeyToUnencryptedWallet() throws Exception {
ECKey key1 = new ECKey(); ECKey key1 = new ECKey();
key1 = key1.encrypt(keyCrypter, keyCrypter.deriveKey("PASSWORD!")); key1 = key1.encrypt(keyCrypter, keyCrypter.deriveKey("PASSWORD!"));
wallet.addKey(key1); wallet.importKey(key1);
} }
@Test @Test(expected = KeyCrypterException.class)
public void encryptionDecryptionHomogenousKeys() throws Exception { public void mismatchedCrypter() throws Exception {
// Check the wallet is currently encrypted
assertTrue("Wallet is not an encrypted wallet", encryptedWallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
// Try added an ECKey that was encrypted with a differenct ScryptParameters (i.e. a non-homogenous key). // Try added an ECKey that was encrypted with a differenct ScryptParameters (i.e. a non-homogenous key).
// This is not allowed as the ScryptParameters is stored at the Wallet level. // This is not allowed as the ScryptParameters is stored at the Wallet level.
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH]; byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH];
secureRandom.nextBytes(salt); secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt)); Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));
ScryptParameters scryptParameters = scryptParametersBuilder.build(); Protos.ScryptParameters scryptParameters = scryptParametersBuilder.build();
KeyCrypter keyCrypterDifferent = new KeyCrypterScrypt(scryptParameters); KeyCrypter keyCrypterDifferent = new KeyCrypterScrypt(scryptParameters);
ECKey ecKeyDifferent = new ECKey(); ECKey ecKeyDifferent = new ECKey();
ecKeyDifferent = ecKeyDifferent.encrypt(keyCrypterDifferent, aesKey); ecKeyDifferent = ecKeyDifferent.encrypt(keyCrypterDifferent, aesKey);
encryptedWallet.importKey(ecKeyDifferent);
}
Iterable<ECKey> keys = encryptedWallet.getKeys(); @Test
Iterator iterator = keys.iterator(); public void importAndEncrypt() throws IOException, InsufficientMoneyException {
boolean oneKey = iterator.hasNext(); final ECKey key = new ECKey();
iterator.next(); encryptedWallet.importKeysAndEncrypt(ImmutableList.of(key), PASSWORD1);
assertTrue("Wrong number of keys in wallet before key addition", oneKey && !iterator.hasNext()); sendMoneyToWallet(encryptedWallet, Utils.COIN, key.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertEquals(Utils.COIN, encryptedWallet.getBalance());
try { SendRequest req = Wallet.SendRequest.emptyWallet(new ECKey().toAddress(params));
encryptedWallet.addKey(ecKeyDifferent); req.aesKey = checkNotNull(encryptedWallet.getKeyCrypter()).deriveKey(PASSWORD1);
fail("AddKey should have thrown an EncrypterDecrypterException but did not."); encryptedWallet.sendCoinsOffline(req);
} catch (KeyCrypterException ede) {
// Expected behaviour.
}
keys = encryptedWallet.getKeys();
iterator = keys.iterator();
oneKey = iterator.hasNext();
iterator.next();
assertTrue("Wrong number of keys in wallet after key addition", oneKey && !iterator.hasNext());
} }
@Test @Test
@ -2209,6 +2178,7 @@ public class WalletTest extends TestWithWallet {
assertEquals(outputValue, request.tx.getOutput(0).getValue()); assertEquals(outputValue, request.tx.getOutput(0).getValue());
} }
@Ignore("Key rotation temporarily disabled during HD wallet migration")
@Test @Test
public void keyRotation() throws Exception { public void keyRotation() throws Exception {
Utils.setMockClock(); Utils.setMockClock();
@ -2216,10 +2186,8 @@ public class WalletTest extends TestWithWallet {
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet); MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
wallet.setKeyRotationEnabled(true); wallet.setKeyRotationEnabled(true);
// Send three cents to two different keys, then add a key and mark the initial keys as compromised. // Send three cents to two different keys, then add a key and mark the initial keys as compromised.
ECKey key1 = new ECKey(); ECKey key1 = wallet.freshReceiveKey();
ECKey key2 = new ECKey(); ECKey key2 = wallet.freshReceiveKey();
wallet.addKey(key1);
wallet.addKey(key2);
sendMoneyToWallet(wallet, CENT, key1.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(wallet, CENT, key1.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
sendMoneyToWallet(wallet, CENT, key2.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(wallet, CENT, key2.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
sendMoneyToWallet(wallet, CENT, key2.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(wallet, CENT, key2.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
@ -2229,8 +2197,7 @@ public class WalletTest extends TestWithWallet {
assertFalse(wallet.isKeyRotating(key1)); assertFalse(wallet.isKeyRotating(key1));
// Rotate the wallet. // Rotate the wallet.
ECKey key3 = new ECKey(); ECKey key3 = wallet.freshReceiveKey();
wallet.addKey(key3);
// We see a broadcast triggered by setting the rotation time. // We see a broadcast triggered by setting the rotation time.
wallet.setKeyRotationTime(compromiseTime); wallet.setKeyRotationTime(compromiseTime);
assertTrue(wallet.isKeyRotating(key1)); assertTrue(wallet.isKeyRotating(key1));
@ -2262,8 +2229,7 @@ public class WalletTest extends TestWithWallet {
// Now round-trip the wallet and check the protobufs are storing the data correctly. // Now round-trip the wallet and check the protobufs are storing the data correctly.
Protos.Wallet protos = new WalletProtobufSerializer().walletToProto(wallet); Protos.Wallet protos = new WalletProtobufSerializer().walletToProto(wallet);
wallet = new Wallet(params); wallet = new WalletProtobufSerializer().readWallet(params, null, protos);
new WalletProtobufSerializer().readWallet(protos, wallet);
tx = wallet.getTransaction(tx.getHash()); tx = wallet.getTransaction(tx.getHash());
assertEquals(Transaction.Purpose.KEY_ROTATION, tx.getPurpose()); assertEquals(Transaction.Purpose.KEY_ROTATION, tx.getPurpose());
@ -2280,8 +2246,7 @@ public class WalletTest extends TestWithWallet {
//@Test //- this test is slow, disable for now. //@Test //- this test is slow, disable for now.
public void fragmentedReKeying() throws Exception { public void fragmentedReKeying() throws Exception {
// Send lots of small coins and check the fee is correct. // Send lots of small coins and check the fee is correct.
ECKey key = new ECKey(); ECKey key = wallet.freshReceiveKey();
wallet.addKey(key);
Address address = key.toAddress(params); Address address = key.toAddress(params);
Utils.setMockClock(); Utils.setMockClock();
Utils.rollMockClock(86400); Utils.rollMockClock(86400);
@ -2294,7 +2259,7 @@ public class WalletTest extends TestWithWallet {
Date compromise = Utils.now(); Date compromise = Utils.now();
Utils.rollMockClock(86400); Utils.rollMockClock(86400);
wallet.addKey(new ECKey()); wallet.freshReceiveKey();
wallet.setKeyRotationTime(compromise); wallet.setKeyRotationTime(compromise);
Transaction tx = broadcaster.waitForTransaction(); Transaction tx = broadcaster.waitForTransaction();
@ -2314,10 +2279,9 @@ public class WalletTest extends TestWithWallet {
public void completeTxPartiallySigned() throws Exception { public void completeTxPartiallySigned() throws Exception {
// Check the wallet will write dummy scriptSigs for inputs that we have only pubkeys for without the privkey. // Check the wallet will write dummy scriptSigs for inputs that we have only pubkeys for without the privkey.
ECKey priv = new ECKey(); ECKey priv = new ECKey();
ECKey pub = new ECKey(null, priv.getPubKey()); ECKey pub = ECKey.fromPublicOnly(priv.getPubKeyPoint());
wallet.addKey(pub); wallet.importKey(pub);
ECKey priv2 = new ECKey(); ECKey priv2 = wallet.freshReceiveKey();
wallet.addKey(priv2);
// Send three transactions, with one being an address type and the other being a raw CHECKSIG type pubkey only, // Send three transactions, with one being an address type and the other being a raw CHECKSIG type pubkey only,
// and the final one being a key we do have. We expect the first two inputs to be dummy values and the last // and the final one being a key we do have. We expect the first two inputs to be dummy values and the last
// to be signed correctly. // to be signed correctly.

View File

@ -18,14 +18,9 @@ package com.google.bitcoin.crypto;
import com.google.bitcoin.core.AddressFormatException; import com.google.bitcoin.core.AddressFormatException;
import com.google.bitcoin.core.Base58; import com.google.bitcoin.core.Base58;
import com.google.bitcoin.crypto.ChildNumber;
import com.google.bitcoin.crypto.DeterministicHierarchy;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.crypto.HDKeyDerivation;
import com.google.common.base.Functions; import com.google.common.base.Functions;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -34,6 +29,8 @@ import org.spongycastle.util.encoders.Hex;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals;
/** /**
* A test with test vectors as per BIP 32 spec: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Test_Vectors * A test with test vectors as per BIP 32 spec: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Test_Vectors
*/ */
@ -131,17 +128,17 @@ public class BIP32Test {
log.info("======= Test vector {}", testCase); log.info("======= Test vector {}", testCase);
HDWTestVector tv = tvs[testCase]; HDWTestVector tv = tvs[testCase];
DeterministicKey masterPrivateKey = HDKeyDerivation.createMasterPrivateKey(Hex.decode(tv.seed)); DeterministicKey masterPrivateKey = HDKeyDerivation.createMasterPrivateKey(Hex.decode(tv.seed));
Assert.assertEquals(testEncode(tv.priv), testEncode(masterPrivateKey.serializePrivB58())); assertEquals(testEncode(tv.priv), testEncode(masterPrivateKey.serializePrivB58()));
Assert.assertEquals(testEncode(tv.pub), testEncode(masterPrivateKey.serializePubB58())); assertEquals(testEncode(tv.pub), testEncode(masterPrivateKey.serializePubB58()));
DeterministicHierarchy dh = new DeterministicHierarchy(masterPrivateKey); DeterministicHierarchy dh = new DeterministicHierarchy(masterPrivateKey);
for (int i = 0; i < tv.derived.size(); i++) { for (int i = 0; i < tv.derived.size(); i++) {
HDWTestVector.DerivedTestCase tc = tv.derived.get(i); HDWTestVector.DerivedTestCase tc = tv.derived.get(i);
log.info("{}", tc.name); log.info("{}", tc.name);
Assert.assertEquals(tc.name, String.format("Test%d %s", testCase + 1, tc.getPathDescription())); assertEquals(tc.name, String.format("Test%d %s", testCase + 1, tc.getPathDescription()));
int depth = tc.path.length - 1; int depth = tc.path.length - 1;
DeterministicKey ehkey = dh.deriveChild(Arrays.asList(tc.path).subList(0, depth), false, true, tc.path[depth]); DeterministicKey ehkey = dh.deriveChild(Arrays.asList(tc.path).subList(0, depth), false, true, tc.path[depth]);
Assert.assertEquals(testEncode(tc.priv), testEncode(ehkey.serializePrivB58())); assertEquals(testEncode(tc.priv), testEncode(ehkey.serializePrivB58()));
Assert.assertEquals(testEncode(tc.pub), testEncode(ehkey.serializePubB58())); assertEquals(testEncode(tc.pub), testEncode(ehkey.serializePubB58()));
} }
} }

View File

@ -16,10 +16,13 @@
package com.google.bitcoin.crypto; package com.google.bitcoin.crypto;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Sha256Hash;
import org.junit.Test; import org.junit.Test;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.encoders.Hex; import org.spongycastle.util.encoders.Hex;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
/** /**
* This test is adapted from Armory's BIP 32 tests. * This test is adapted from Armory's BIP 32 tests.
@ -121,8 +124,67 @@ public class ChildKeyDerivationTest {
} }
} }
@Test
public void encryptedDerivation() throws Exception {
// Check that encrypting a parent key in the heirarchy and then deriving from it yields a DeterministicKey
// with no private key component, and that the private key bytes are derived on demand.
KeyCrypter scrypter = new KeyCrypterScrypt();
KeyParameter aesKey = scrypter.deriveKey("we never went to the moon");
DeterministicKey key1 = HDKeyDerivation.createMasterPrivateKey("it was all a hoax".getBytes());
DeterministicKey encryptedKey1 = key1.encrypt(scrypter, aesKey, null);
DeterministicKey decryptedKey1 = encryptedKey1.decrypt(scrypter, aesKey);
assertEquals(key1, decryptedKey1);
DeterministicKey key2 = HDKeyDerivation.deriveChildKey(key1, ChildNumber.ZERO);
DeterministicKey derivedKey2 = HDKeyDerivation.deriveChildKey(encryptedKey1, ChildNumber.ZERO);
assertTrue(derivedKey2.isEncrypted()); // parent is encrypted.
DeterministicKey decryptedKey2 = derivedKey2.decrypt(scrypter, aesKey);
assertFalse(decryptedKey2.isEncrypted());
assertEquals(key2, decryptedKey2);
Sha256Hash hash = Sha256Hash.create("the mainstream media won't cover it. why is that?".getBytes());
try {
derivedKey2.sign(hash);
fail();
} catch (ECKey.KeyIsEncryptedException e) {
// Ignored.
}
ECKey.ECDSASignature signature = derivedKey2.sign(hash, aesKey);
assertTrue(derivedKey2.verify(hash, signature));
}
@Test
public void pubOnlyDerivation() throws Exception {
DeterministicKey key1 = HDKeyDerivation.createMasterPrivateKey("satoshi lives!".getBytes());
DeterministicKey key2 = HDKeyDerivation.deriveChildKey(key1, ChildNumber.ZERO_HARDENED);
DeterministicKey key3 = HDKeyDerivation.deriveChildKey(key2, ChildNumber.ZERO);
DeterministicKey pubkey3 = HDKeyDerivation.deriveChildKey(key2.getPubOnly(), ChildNumber.ZERO);
assertEquals(key3.getPubKeyPoint(), pubkey3.getPubKeyPoint());
}
@Test
public void serializeToText() {
DeterministicKey key1 = HDKeyDerivation.createMasterPrivateKey("satoshi lives!".getBytes());
DeterministicKey key2 = HDKeyDerivation.deriveChildKey(key1, ChildNumber.ZERO_HARDENED);
{
final String pub58 = key1.serializePubB58();
final String priv58 = key1.serializePrivB58();
assertEquals("xpub661MyMwAqRbcF7mq7Aejj5xZNzFfgi3ABamE9FedDHVmViSzSxYTgAQGcATDo2J821q7Y9EAagjg5EP3L7uBZk11PxZU3hikL59dexfLkz3", pub58);
assertEquals("xprv9s21ZrQH143K2dhN197jMx1ppxRBHFKJpMqdLsF1ewxncv7quRED8N5nksxphju3W7naj1arF56L5PUEWfuSk8h73Sb2uh7bSwyXNrjzhAZ", priv58);
assertEquals(DeterministicKey.deserializeB58(null, priv58), key1);
assertEquals(DeterministicKey.deserializeB58(null, pub58).getPubKeyPoint(), key1.getPubKeyPoint());
}
{
final String pub58 = key2.serializePubB58();
final String priv58 = key2.serializePrivB58();
assertEquals(DeterministicKey.deserializeB58(key1, priv58), key2);
assertEquals(DeterministicKey.deserializeB58(key1, pub58).getPubKeyPoint(), key2.getPubKeyPoint());
}
}
private static String hexEncodePub(DeterministicKey pubKey) { private static String hexEncodePub(DeterministicKey pubKey) {
return hexEncode(pubKey.getPubKeyBytes()); return hexEncode(pubKey.getPubKey());
} }
private static String hexEncode(byte[] bytes) { private static String hexEncode(byte[] bytes) {

View File

@ -1,13 +1,8 @@
package com.google.bitcoin.crypto; package com.google.bitcoin.crypto;
import com.google.bitcoin.core.ECKey;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.ECDomainParameters; import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.math.ec.ECCurve;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.util.encoders.Hex; import org.spongycastle.util.encoders.Hex;
import java.util.Arrays; import java.util.Arrays;
@ -17,8 +12,6 @@ import java.util.List;
* @author Matija Mazi <br/> * @author Matija Mazi <br/>
*/ */
public class HDUtilsTest { public class HDUtilsTest {
private static final Logger log = LoggerFactory.getLogger(HDUtilsTest.class);
@Test @Test
public void testHmac() throws Exception { public void testHmac() throws Exception {
String tv[] = { String tv[] = {
@ -112,52 +105,6 @@ public class HDUtilsTest {
return Hex.decode(hmacTestVectors[i]); return Hex.decode(hmacTestVectors[i]);
} }
@Test
public void testPointCompression() {
List<String> testPubKey = Arrays.asList(
"044f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa385b6b1b8ead809ca67454d9683fcf2ba03456d6fe2c4abe2b07f0fbdbb2f1c1",
"04ed83704c95d829046f1ac27806211132102c34e9ac7ffa1b71110658e5b9d1bdedc416f5cefc1db0625cd0c75de8192d2b592d7e3b00bcfb4a0e860d880fd1fc",
"042596957532fc37e40486b910802ff45eeaa924548c0e1c080ef804e523ec3ed3ed0a9004acf927666eee18b7f5e8ad72ff100a3bb710a577256fd7ec81eb1cb3");
ECDomainParameters ecp = ECKey.CURVE;
ECCurve curve = ecp.getCurve();
for (String testpkStr : testPubKey) {
byte[] testpk = Hex.decode(testpkStr);
ECPoint orig = curve.decodePoint(testpk);
ECPoint ptFlat = curve.decodePoint(orig.getEncoded(false));
ECPoint ptComp = curve.decodePoint(ptFlat.getEncoded(true));
ECPoint uncompressed = curve.decodePoint(ptComp.getEncoded(false));
ECPoint recompressed = curve.decodePoint(uncompressed.getEncoded(true));
log.info("====================");
log.info("Flat: {}", asHexStr(ptFlat));
log.info("Compressed: {}", asHexStr(ptComp));
log.info("Uncompressed: {}", asHexStr(uncompressed));
log.info("Recompressed: {}", asHexStr(recompressed));
log.info("Original (uncomp): {}", asHexStr(orig));
// assert point equality:
Assert.assertEquals(ptFlat, uncompressed);
Assert.assertEquals(ptFlat, ptComp);
Assert.assertEquals(ptComp, recompressed);
Assert.assertEquals(ptComp, orig);
// assert bytes equality:
Assert.assertArrayEquals(ptFlat.getEncoded(), uncompressed.getEncoded());
Assert.assertArrayEquals(ptComp.getEncoded(), recompressed.getEncoded());
Assert.assertArrayEquals(ptFlat.getEncoded(), orig.getEncoded());
Assert.assertFalse(Arrays.equals(ptFlat.getEncoded(), ptComp.getEncoded()));
// todo: assert header byte
}
}
private String asHexStr(ECPoint ptFlat) {
return new String(Hex.encode(ptFlat.getEncoded()));
}
@Test @Test
public void testLongToByteArray() throws Exception { public void testLongToByteArray() throws Exception {
byte[] bytes = HDUtils.longTo4ByteArray(1026); byte[] bytes = HDUtils.longTo4ByteArray(1026);

View File

@ -62,7 +62,7 @@ public class KeyCrypterScryptTest {
KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters); KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters);
// Encrypt. // Encrypt.
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(TEST_BYTES1, keyCrypter.deriveKey(PASSWORD1)); EncryptedData encryptedPrivateKey = keyCrypter.encrypt(TEST_BYTES1, keyCrypter.deriveKey(PASSWORD1));
assertNotNull(encryptedPrivateKey); assertNotNull(encryptedPrivateKey);
// Decrypt. // Decrypt.
@ -89,7 +89,7 @@ public class KeyCrypterScryptTest {
String plainText = UUID.randomUUID().toString(); String plainText = UUID.randomUUID().toString();
CharSequence password = UUID.randomUUID().toString(); CharSequence password = UUID.randomUUID().toString();
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(plainText.getBytes(), keyCrypter.deriveKey(password)); EncryptedData encryptedPrivateKey = keyCrypter.encrypt(plainText.getBytes(), keyCrypter.deriveKey(password));
assertNotNull(encryptedPrivateKey); assertNotNull(encryptedPrivateKey);
@ -110,7 +110,7 @@ public class KeyCrypterScryptTest {
stringBuffer.append(i).append(" ").append("The quick brown fox"); stringBuffer.append(i).append(" ").append("The quick brown fox");
} }
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(stringBuffer.toString().getBytes(), keyCrypter.deriveKey(PASSWORD2)); EncryptedData encryptedPrivateKey = keyCrypter.encrypt(stringBuffer.toString().getBytes(), keyCrypter.deriveKey(PASSWORD2));
assertNotNull(encryptedPrivateKey); assertNotNull(encryptedPrivateKey);
try { try {
@ -127,9 +127,9 @@ public class KeyCrypterScryptTest {
KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters); KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters);
// Encrypt bytes. // Encrypt bytes.
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(TEST_BYTES1, keyCrypter.deriveKey(PASSWORD1)); EncryptedData encryptedPrivateKey = keyCrypter.encrypt(TEST_BYTES1, keyCrypter.deriveKey(PASSWORD1));
assertNotNull(encryptedPrivateKey); assertNotNull(encryptedPrivateKey);
log.debug("\nEncrypterDecrypterTest: cipherBytes = \nlength = " + encryptedPrivateKey.getEncryptedBytes().length + "\n---------------\n" + Utils.bytesToHexString(encryptedPrivateKey.getEncryptedBytes()) + "\n---------------\n"); log.debug("\nEncrypterDecrypterTest: cipherBytes = \nlength = " + encryptedPrivateKey.encryptedBytes.length + "\n---------------\n" + Utils.bytesToHexString(encryptedPrivateKey.encryptedBytes) + "\n---------------\n");
byte[] rebornPlainBytes = keyCrypter.decrypt(encryptedPrivateKey, keyCrypter.deriveKey(PASSWORD1)); byte[] rebornPlainBytes = keyCrypter.decrypt(encryptedPrivateKey, keyCrypter.deriveKey(PASSWORD1));
@ -149,7 +149,7 @@ public class KeyCrypterScryptTest {
byte[] plainBytes = new byte[i]; byte[] plainBytes = new byte[i];
random.nextBytes(plainBytes); random.nextBytes(plainBytes);
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(plainBytes, keyCrypter.deriveKey(PASSWORD1)); EncryptedData encryptedPrivateKey = keyCrypter.encrypt(plainBytes, keyCrypter.deriveKey(PASSWORD1));
assertNotNull(encryptedPrivateKey); assertNotNull(encryptedPrivateKey);
//log.debug("\nEncrypterDecrypterTest: cipherBytes = \nlength = " + cipherBytes.length + "\n---------------\n" + Utils.bytesToHexString(cipherBytes) + "\n---------------\n"); //log.debug("\nEncrypterDecrypterTest: cipherBytes = \nlength = " + cipherBytes.length + "\n---------------\n" + Utils.bytesToHexString(cipherBytes) + "\n---------------\n");

View File

@ -36,6 +36,7 @@ import java.io.File;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.Arrays;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -68,8 +69,9 @@ public class ChannelConnectionTest extends TestWithWallet {
sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN);
wallet.addExtension(new StoredPaymentChannelClientStates(wallet, failBroadcaster)); wallet.addExtension(new StoredPaymentChannelClientStates(wallet, failBroadcaster));
serverWallet = new Wallet(params); serverWallet = new Wallet(params);
serverWallet.setKeychainLookaheadSize(5);
serverWallet.addExtension(new StoredPaymentChannelServerStates(serverWallet, failBroadcaster)); serverWallet.addExtension(new StoredPaymentChannelServerStates(serverWallet, failBroadcaster));
serverWallet.addKey(new ECKey()); serverWallet.freshReceiveKey();
serverChain = new BlockChain(params, serverWallet, blockStore); serverChain = new BlockChain(params, serverWallet, blockStore);
// Use an atomic boolean to indicate failure because fail()/assert*() dont work in network threads // Use an atomic boolean to indicate failure because fail()/assert*() dont work in network threads
fail = new AtomicBoolean(false); fail = new AtomicBoolean(false);
@ -416,19 +418,17 @@ public class ChannelConnectionTest extends TestWithWallet {
private static Wallet roundTripClientWallet(Wallet wallet) throws Exception { private static Wallet roundTripClientWallet(Wallet wallet) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
new WalletProtobufSerializer().writeWallet(wallet, bos); new WalletProtobufSerializer().writeWallet(wallet, bos);
Wallet wallet2 = new Wallet(wallet.getParams()); org.bitcoinj.wallet.Protos.Wallet proto = WalletProtobufSerializer.parseToProto(new ByteArrayInputStream(bos.toByteArray()));
wallet2.addExtension(new StoredPaymentChannelClientStates(wallet2, failBroadcaster)); StoredPaymentChannelClientStates state = new StoredPaymentChannelClientStates(null, failBroadcaster);
new WalletProtobufSerializer().readWallet(WalletProtobufSerializer.parseToProto(new ByteArrayInputStream(bos.toByteArray())), wallet2); return new WalletProtobufSerializer().readWallet(wallet.getParams(), new WalletExtension[] { state }, proto);
return wallet2;
} }
private static Wallet roundTripServerWallet(Wallet wallet) throws Exception { private static Wallet roundTripServerWallet(Wallet wallet) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
new WalletProtobufSerializer().writeWallet(wallet, bos); new WalletProtobufSerializer().writeWallet(wallet, bos);
Wallet wallet2 = new Wallet(wallet.getParams()); StoredPaymentChannelServerStates state = new StoredPaymentChannelServerStates(null, failBroadcaster);
wallet2.addExtension(new StoredPaymentChannelServerStates(wallet2, failBroadcaster)); org.bitcoinj.wallet.Protos.Wallet proto = WalletProtobufSerializer.parseToProto(new ByteArrayInputStream(bos.toByteArray()));
new WalletProtobufSerializer().readWallet(WalletProtobufSerializer.parseToProto(new ByteArrayInputStream(bos.toByteArray())), wallet2); return new WalletProtobufSerializer().readWallet(wallet.getParams(), new WalletExtension[] { state }, proto);
return wallet2;
} }
@Test @Test
@ -544,7 +544,8 @@ public class ChannelConnectionTest extends TestWithWallet {
@Test @Test
public void testEmptyWallet() throws Exception { public void testEmptyWallet() throws Exception {
Wallet emptyWallet = new Wallet(params); Wallet emptyWallet = new Wallet(params);
emptyWallet.addKey(new ECKey()); emptyWallet.setKeychainLookaheadSize(5);
emptyWallet.freshReceiveKey();
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server; PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
@ -565,6 +566,24 @@ public class ChannelConnectionTest extends TestWithWallet {
} }
} }
@Test
public void testClientRefusesNonCanonicalKey() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
Protos.TwoWayChannelMessage.Builder initiateMsg = Protos.TwoWayChannelMessage.newBuilder(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
ByteString brokenKey = initiateMsg.getInitiate().getMultisigKey();
brokenKey = ByteString.copyFrom(Arrays.copyOf(brokenKey.toByteArray(), brokenKey.size() + 1));
initiateMsg.getInitiateBuilder().setMultisigKey(brokenKey);
client.receiveMessage(initiateMsg.build());
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(CloseReason.REMOTE_SENT_INVALID_MESSAGE, pair.clientRecorder.q.take());
}
@Test @Test
public void testClientResumeNothing() throws Exception { public void testClientResumeNothing() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);

View File

@ -69,9 +69,8 @@ public class PaymentChannelStateTest extends TestWithWallet {
})); }));
sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN);
chain = new BlockChain(params, wallet, blockStore); // Recreate chain as sendMoneyToWallet will confuse it chain = new BlockChain(params, wallet, blockStore); // Recreate chain as sendMoneyToWallet will confuse it
serverKey = new ECKey();
serverWallet = new Wallet(params); serverWallet = new Wallet(params);
serverWallet.addKey(serverKey); serverKey = serverWallet.freshReceiveKey();
chain.addWallet(serverWallet); chain.addWallet(serverWallet);
halfCoin = Utils.toNanoCoins(0, 50); halfCoin = Utils.toNanoCoins(0, 50);
@ -119,7 +118,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), halfCoin, EXPIRE_TIME); clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); clientState.initiate();
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
@ -233,7 +232,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
Utils.CENT.divide(BigInteger.valueOf(2)), EXPIRE_TIME); Utils.CENT.divide(BigInteger.valueOf(2)), EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
assertEquals(Utils.CENT.divide(BigInteger.valueOf(2)), clientState.getTotalValue()); assertEquals(Utils.CENT.divide(BigInteger.valueOf(2)), clientState.getTotalValue());
@ -334,14 +333,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
try { clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null,
Arrays.copyOf(serverKey.getPubKey(), serverKey.getPubKey().length + 1)), halfCoin, EXPIRE_TIME);
} catch (VerificationException e) {
assertTrue(e.getMessage().contains("not canonical"));
}
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); clientState.initiate();
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
@ -546,14 +538,14 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
// Clearly ONE is far too small to be useful // Clearly ONE is far too small to be useful
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), BigInteger.ONE, EXPIRE_TIME); clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), BigInteger.ONE, EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
try { try {
clientState.initiate(); clientState.initiate();
fail(); fail();
} catch (ValueOutOfRangeException e) {} } catch (ValueOutOfRangeException e) {}
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
Transaction.MIN_NONDUST_OUTPUT.subtract(BigInteger.ONE).add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), Transaction.MIN_NONDUST_OUTPUT.subtract(BigInteger.ONE).add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE),
EXPIRE_TIME); EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
@ -563,7 +555,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
} catch (ValueOutOfRangeException e) {} } catch (ValueOutOfRangeException e) {}
// Verify that MIN_NONDUST_OUTPUT + MIN_TX_FEE is accepted // Verify that MIN_NONDUST_OUTPUT + MIN_TX_FEE is accepted
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), EXPIRE_TIME); Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
// We'll have to pay REFERENCE_DEFAULT_MIN_TX_FEE twice (multisig+refund), and we'll end up getting back nearly nothing... // We'll have to pay REFERENCE_DEFAULT_MIN_TX_FEE twice (multisig+refund), and we'll end up getting back nearly nothing...
@ -572,7 +564,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
// Now actually use a more useful CENT // Now actually use a more useful CENT
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), Utils.CENT, EXPIRE_TIME); clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), Utils.CENT, EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); clientState.initiate();
assertEquals(clientState.getRefundTxFees(), BigInteger.ZERO); assertEquals(clientState.getRefundTxFees(), BigInteger.ZERO);
@ -648,7 +640,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), Utils.CENT, EXPIRE_TIME) { clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), Utils.CENT, EXPIRE_TIME) {
@Override @Override
protected void editContractSendRequest(Wallet.SendRequest req) { protected void editContractSendRequest(Wallet.SendRequest req) {
req.coinSelector = wallet.getCoinSelector(); req.coinSelector = wallet.getCoinSelector();
@ -734,7 +726,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), halfCoin, EXPIRE_TIME); clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); clientState.initiate();
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());

View File

@ -57,11 +57,13 @@ public class WalletProtobufSerializerTest {
public void setUp() throws Exception { public void setUp() throws Exception {
BriefLogFormatter.initVerbose(); BriefLogFormatter.initVerbose();
myWatchedKey = new ECKey(); myWatchedKey = new ECKey();
myWallet = new Wallet(params);
myKey = new ECKey(); myKey = new ECKey();
myKey.setCreationTimeSeconds(123456789L); myKey.setCreationTimeSeconds(123456789L);
myWallet.importKey(myKey);
myAddress = myKey.toAddress(params); myAddress = myKey.toAddress(params);
myWallet = new Wallet(params); myWallet = new Wallet(params);
myWallet.addKey(myKey); myWallet.importKey(myKey);
mScriptCreationTime = new Date().getTime() / 1000 - 1234; mScriptCreationTime = new Date().getTime() / 1000 - 1234;
myWallet.addWatchedAddress(myWatchedKey.toAddress(params), mScriptCreationTime); myWallet.addWatchedAddress(myWatchedKey.toAddress(params), mScriptCreationTime);
myWallet.setDescription(WALLET_DESCRIPTION); myWallet.setDescription(WALLET_DESCRIPTION);
@ -146,7 +148,7 @@ public class WalletProtobufSerializerTest {
myKey = new ECKey(); myKey = new ECKey();
myAddress = myKey.toAddress(params); myAddress = myKey.toAddress(params);
myWallet = new Wallet(params); myWallet = new Wallet(params);
myWallet.addKey(myKey); myWallet.importKey(myKey);
Wallet wallet1 = roundTrip(myWallet); Wallet wallet1 = roundTrip(myWallet);
assertArrayEquals(myKey.getPubKey(), wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPubKey()); assertArrayEquals(myKey.getPubKey(), wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPubKey());
assertArrayEquals(myKey.getPrivKeyBytes(), wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPrivKeyBytes()); assertArrayEquals(myKey.getPrivKeyBytes(), wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPrivKeyBytes());
@ -310,27 +312,23 @@ public class WalletProtobufSerializerTest {
public void testExtensions() throws Exception { public void testExtensions() throws Exception {
myWallet.addExtension(new SomeFooExtension("com.whatever.required", true)); myWallet.addExtension(new SomeFooExtension("com.whatever.required", true));
Protos.Wallet proto = new WalletProtobufSerializer().walletToProto(myWallet); Protos.Wallet proto = new WalletProtobufSerializer().walletToProto(myWallet);
Wallet wallet2 = new Wallet(params);
// Initial extension is mandatory: try to read it back into a wallet that doesn't know about it. // Initial extension is mandatory: try to read it back into a wallet that doesn't know about it.
try { try {
new WalletProtobufSerializer().readWallet(proto, wallet2); new WalletProtobufSerializer().readWallet(params, null, proto);
fail(); fail();
} catch (UnreadableWalletException e) { } catch (UnreadableWalletException e) {
assertTrue(e.getMessage().contains("mandatory")); assertTrue(e.getMessage().contains("mandatory"));
} }
Wallet wallet3 = new Wallet(params); Wallet wallet = new WalletProtobufSerializer().readWallet(params,
// This time it works. new WalletExtension[]{ new SomeFooExtension("com.whatever.required", true) },
wallet3.addExtension(new SomeFooExtension("com.whatever.required", true)); proto);
new WalletProtobufSerializer().readWallet(proto, wallet3); assertTrue(wallet.getExtensions().containsKey("com.whatever.required"));
assertTrue(wallet3.getExtensions().containsKey("com.whatever.required"));
// Non-mandatory extensions are ignored if the wallet doesn't know how to read them. // Non-mandatory extensions are ignored if the wallet doesn't know how to read them.
Wallet wallet4 = new Wallet(params); Wallet wallet2 = new Wallet(params);
wallet4.addExtension(new SomeFooExtension("com.whatever.optional", false)); wallet2.addExtension(new SomeFooExtension("com.whatever.optional", false));
Protos.Wallet proto4 = new WalletProtobufSerializer().walletToProto(wallet4); Protos.Wallet proto2 = new WalletProtobufSerializer().walletToProto(wallet2);
Wallet wallet5 = new Wallet(params); Wallet wallet5 = new WalletProtobufSerializer().readWallet(params, null, proto2);
new WalletProtobufSerializer().readWallet(proto4, wallet5);
assertEquals(0, wallet5.getExtensions().size()); assertEquals(0, wallet5.getExtensions().size());
} }

View File

@ -0,0 +1,256 @@
/**
* Copyright 2013 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.wallet;
import com.google.bitcoin.core.BloomFilter;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.utils.Threading;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.bitcoinj.wallet.Protos;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.junit.Assert.*;
public class BasicKeyChainTest {
private BasicKeyChain chain;
private AtomicReference<List<ECKey>> onKeysAdded;
private AtomicBoolean onKeysAddedRan;
@Before
public void setup() {
chain = new BasicKeyChain();
onKeysAdded = new AtomicReference<List<ECKey>>();
onKeysAddedRan = new AtomicBoolean();
chain.addEventListener(new AbstractKeyChainEventListener() {
@Override
public void onKeysAdded(List<ECKey> keys2) {
onKeysAdded.set(keys2);
onKeysAddedRan.set(true);
}
}, Threading.SAME_THREAD);
}
@Test
public void importKeys() {
long now = Utils.currentTimeSeconds();
Utils.rollMockClock(0);
final ECKey key1 = new ECKey();
Utils.rollMockClock(86400);
final ECKey key2 = new ECKey();
final ArrayList<ECKey> keys = Lists.newArrayList(key1, key2);
// Import two keys, check the event is correct.
assertEquals(2, chain.importKeys(keys));
assertEquals(2, chain.numKeys());
assertTrue(onKeysAddedRan.getAndSet(false));
assertArrayEquals(keys.toArray(), onKeysAdded.get().toArray());
assertEquals(now, chain.getEarliestKeyCreationTime());
// Check we ignore duplicates.
final ECKey newKey = new ECKey();
keys.add(newKey);
assertEquals(1, chain.importKeys(keys));
assertTrue(onKeysAddedRan.getAndSet(false));
assertEquals(newKey, onKeysAdded.getAndSet(null).get(0));
assertEquals(0, chain.importKeys(keys));
assertFalse(onKeysAddedRan.getAndSet(false));
assertNull(onKeysAdded.get());
assertTrue(chain.hasKey(key1));
assertTrue(chain.hasKey(key2));
assertEquals(key1, chain.findKeyFromPubHash(key1.getPubKeyHash()));
assertEquals(key2, chain.findKeyFromPubKey(key2.getPubKey()));
assertNull(chain.findKeyFromPubKey(key2.getPubKeyHash()));
}
@Test
public void removeKey() {
ECKey key = new ECKey();
chain.importKeys(key);
assertEquals(1, chain.numKeys());
assertTrue(chain.removeKey(key));
assertEquals(0, chain.numKeys());
assertFalse(chain.removeKey(key));
}
@Test
public void getKey() {
ECKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertTrue(onKeysAddedRan.getAndSet(false));
assertEquals(key1, onKeysAdded.getAndSet(null).get(0));
ECKey key2 = chain.getKey(KeyChain.KeyPurpose.CHANGE);
assertFalse(onKeysAddedRan.getAndSet(false));
assertEquals(key2, key1);
}
@Test(expected = IllegalStateException.class)
public void checkPasswordNoKeys() {
chain.checkPassword("test");
}
@Test(expected = IllegalStateException.class)
public void checkPasswordNotEncrypted() {
final ArrayList<ECKey> keys = Lists.newArrayList(new ECKey(), new ECKey());
chain.importKeys(keys);
chain.checkPassword("test");
}
@Test(expected = IllegalStateException.class)
public void doubleEncryptFails() {
final ArrayList<ECKey> keys = Lists.newArrayList(new ECKey(), new ECKey());
chain.importKeys(keys);
chain = chain.toEncrypted("foo");
chain.toEncrypted("foo");
}
@Test
public void encryptDecrypt() {
final ECKey key1 = new ECKey();
chain.importKeys(key1, new ECKey());
final String PASSWORD = "foobar";
chain = chain.toEncrypted(PASSWORD);
final KeyCrypter keyCrypter = chain.getKeyCrypter();
assertNotNull(keyCrypter);
assertTrue(keyCrypter instanceof KeyCrypterScrypt);
assertTrue(chain.checkPassword(PASSWORD));
assertFalse(chain.checkPassword("wrong"));
ECKey key = chain.findKeyFromPubKey(key1.getPubKey());
assertTrue(key.isEncrypted());
assertNull(key.getSecretBytes());
try {
// Don't allow import of an unencrypted key.
chain.importKeys(new ECKey());
fail();
} catch (KeyCrypterException e) {
}
try {
chain.toDecrypted(keyCrypter.deriveKey("wrong"));
fail();
} catch (KeyCrypterException e) {}
chain = chain.toDecrypted(PASSWORD);
key = chain.findKeyFromPubKey(key1.getPubKey());
assertFalse(key.isEncrypted());
key.getPrivKeyBytes();
}
@Test(expected = KeyCrypterException.class)
public void cannotImportEncryptedKey() {
final ECKey key1 = new ECKey();
chain.importKeys(ImmutableList.of(key1));
chain = chain.toEncrypted("foobar");
ECKey encryptedKey = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertTrue(encryptedKey.isEncrypted());
BasicKeyChain chain2 = new BasicKeyChain();
chain2.importKeys(ImmutableList.of(encryptedKey));
}
@Test(expected = KeyCrypterException.class)
public void cannotMixParams() throws Exception {
chain = chain.toEncrypted("foobar");
KeyCrypterScrypt scrypter = new KeyCrypterScrypt(2); // Some bogus params.
ECKey key1 = new ECKey().encrypt(scrypter, scrypter.deriveKey("other stuff"));
chain.importKeys(key1);
}
@Test
public void serializationUnencrypted() throws UnreadableWalletException {
Utils.rollMockClock(0);
Date now = Utils.now();
final ECKey key1 = new ECKey();
Utils.rollMockClock(5000);
final ECKey key2 = new ECKey();
chain.importKeys(ImmutableList.of(key1, key2));
List<Protos.Key> keys = chain.serializeToProtobuf();
assertEquals(2, keys.size());
assertArrayEquals(key1.getPubKey(), keys.get(0).getPublicKey().toByteArray());
assertArrayEquals(key2.getPubKey(), keys.get(1).getPublicKey().toByteArray());
assertArrayEquals(key1.getPrivKeyBytes(), keys.get(0).getSecretBytes().toByteArray());
assertArrayEquals(key2.getPrivKeyBytes(), keys.get(1).getSecretBytes().toByteArray());
long normTime = (long) (Math.floor(now.getTime() / 1000) * 1000);
assertEquals(normTime, keys.get(0).getCreationTimestamp());
assertEquals(normTime + 5000 * 1000, keys.get(1).getCreationTimestamp());
chain = BasicKeyChain.fromProtobufUnencrypted(keys);
assertEquals(2, chain.getKeys().size());
assertEquals(key1, chain.getKeys().get(0));
assertEquals(key2, chain.getKeys().get(1));
}
@Test
public void serializationEncrypted() throws UnreadableWalletException {
ECKey key1 = new ECKey();
chain.importKeys(key1);
chain = chain.toEncrypted("foo bar");
key1 = chain.getKeys().get(0);
List<Protos.Key> keys = chain.serializeToProtobuf();
assertEquals(1, keys.size());
assertArrayEquals(key1.getPubKey(), keys.get(0).getPublicKey().toByteArray());
assertFalse(keys.get(0).hasSecretBytes());
assertTrue(keys.get(0).hasEncryptedData());
chain = BasicKeyChain.fromProtobufEncrypted(keys, checkNotNull(chain.getKeyCrypter()));
assertEquals(key1.getEncryptedPrivateKey(), chain.getKeys().get(0).getEncryptedPrivateKey());
assertTrue(chain.checkPassword("foo bar"));
}
@Test
public void watching() throws UnreadableWalletException {
ECKey key1 = new ECKey();
ECKey pub = ECKey.fromPublicOnly(key1.getPubKeyPoint());
chain.importKeys(pub);
assertEquals(1, chain.numKeys());
List<Protos.Key> keys = chain.serializeToProtobuf();
assertEquals(1, keys.size());
assertTrue(keys.get(0).hasPublicKey());
assertFalse(keys.get(0).hasSecretBytes());
chain = BasicKeyChain.fromProtobufUnencrypted(keys);
assertEquals(1, chain.numKeys());
assertFalse(chain.findKeyFromPubKey(pub.getPubKey()).hasPrivKey());
}
@Test
public void bloom() throws Exception {
ECKey key1 = new ECKey();
ECKey key2 = new ECKey();
chain.importKeys(key1, key2);
assertEquals(2, chain.numKeys());
assertEquals(4, chain.numBloomFilterEntries());
BloomFilter filter = chain.getFilter(4, 0.001, 100);
assertTrue(filter.contains(key1.getPubKey()));
assertTrue(filter.contains(key1.getPubKeyHash()));
assertTrue(filter.contains(key2.getPubKey()));
assertTrue(filter.contains(key2.getPubKeyHash()));
ECKey key3 = new ECKey();
assertFalse(filter.contains(key3.getPubKey()));
}
}

View File

@ -0,0 +1,294 @@
/**
* Copyright 2013 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.wallet;
import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.BloomFilter;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.bitcoin.utils.Threading;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import org.bitcoinj.wallet.Protos;
import org.junit.Before;
import org.junit.Test;
import org.spongycastle.crypto.params.KeyParameter;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.junit.Assert.*;
public class DeterministicKeyChainTest {
private DeterministicKeyChain chain;
private final byte[] SEED = Sha256Hash.create("don't use a string seed like this in real life".getBytes()).getBytes();
@Before
public void setup() {
BriefLogFormatter.init();
// You should use a random seed instead. The secs constant comes from the unit test file, so we can compare
// serialized data properly.
long secs = 1389353062L;
chain = new DeterministicKeyChain(SEED, secs);
chain.setLookaheadSize(10);
assertEquals(secs, checkNotNull(chain.getSeed()).getCreationTimeSeconds());
}
@Test
public void mnemonicCode() throws Exception {
final List<String> words = chain.toMnemonicCode();
assertEquals("aerobic toe save section draw warm cute upon raccoon mother priority pilot taste sweet next traffic fatal sword dentist original crisp team caution rebel",
Joiner.on(" ").join(words));
DeterministicSeed seed = new DeterministicSeed(words, checkNotNull(chain.getSeed()).getCreationTimeSeconds());
}
@Test
public void derive() throws Exception {
ECKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
ECKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
final Address address = new Address(UnitTestParams.get(), "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa");
assertEquals(address, key1.toAddress(UnitTestParams.get()));
assertEquals("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X", key2.toAddress(UnitTestParams.get()).toString());
assertEquals(key1, chain.findKeyFromPubHash(address.getHash160()));
assertEquals(key2, chain.findKeyFromPubKey(key2.getPubKey()));
key1.sign(Sha256Hash.ZERO_HASH);
ECKey key3 = chain.getKey(KeyChain.KeyPurpose.CHANGE);
assertEquals("mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn", key3.toAddress(UnitTestParams.get()).toString());
key3.sign(Sha256Hash.ZERO_HASH);
}
@Test
public void events() throws Exception {
// Check that we get the right events at the right time.
final List<List<ECKey>> listenerKeys = Lists.newArrayList();
long secs = 1389353062L;
chain = new DeterministicKeyChain(SEED, secs);
chain.addEventListener(new AbstractKeyChainEventListener() {
@Override
public void onKeysAdded(List<ECKey> keys) {
listenerKeys.add(keys);
}
}, Threading.SAME_THREAD);
assertEquals(0, listenerKeys.size());
chain.setLookaheadSize(5);
assertEquals(0, listenerKeys.size());
ECKey key = chain.getKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(1, listenerKeys.size()); // 1 event
final List<ECKey> firstEvent = listenerKeys.get(0);
assertEquals(6, firstEvent.size()); // 5 lookahead keys and 1 to satisfy the request.
assertTrue(firstEvent.contains(key)); // order is not specified.
listenerKeys.clear();
key = chain.getKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(1, listenerKeys.size()); // 1 event
assertEquals(1, listenerKeys.get(0).size()); // 1 key.
DeterministicKey eventKey = (DeterministicKey) listenerKeys.get(0).get(0);
assertNotEquals(key, eventKey); // The key added is not the one that's served.
assertEquals(6, eventKey.getChildNumber().i());
listenerKeys.clear();
key = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertEquals(1, listenerKeys.size()); // 1 event
assertEquals(6, listenerKeys.get(0).size()); // 1 key.
eventKey = (DeterministicKey) listenerKeys.get(0).get(0);
// The key added IS the one that's served because we did not previously request any RECEIVE_FUNDS keys.
assertEquals(key, eventKey);
assertEquals(0, eventKey.getChildNumber().i());
}
@Test
public void random() {
// Can't test much here but verify the constructor worked and the class is functional. The other tests rely on
// a fixed seed to be deterministic.
chain = new DeterministicKeyChain(new SecureRandom());
chain.setLookaheadSize(10);
chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).sign(Sha256Hash.ZERO_HASH);
chain.getKey(KeyChain.KeyPurpose.CHANGE).sign(Sha256Hash.ZERO_HASH);
}
@Test
public void serializeUnencrypted() throws UnreadableWalletException {
DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key3 = chain.getKey(KeyChain.KeyPurpose.CHANGE);
List<Protos.Key> keys = chain.serializeToProtobuf();
// 1 root seed, 1 master key, 1 account key, 2 internal keys, 3 derived and 20 lookahead.
assertEquals(28, keys.size());
// Get another key that will be lost during round-tripping, to ensure we can derive it again.
DeterministicKey key4 = chain.getKey(KeyChain.KeyPurpose.CHANGE);
final String EXPECTED_SERIALIZATION = checkSerialization(keys, "deterministic-wallet-serialization.txt");
// Round trip the data back and forth to check it is preserved.
int oldLookaheadSize = chain.getLookaheadSize();
chain = DeterministicKeyChain.fromProtobuf(keys, null).get(0);
assertEquals(EXPECTED_SERIALIZATION, protoToString(chain.serializeToProtobuf()));
assertEquals(key1, chain.findKeyFromPubHash(key1.getPubKeyHash()));
assertEquals(key2, chain.findKeyFromPubHash(key2.getPubKeyHash()));
assertEquals(key3, chain.findKeyFromPubHash(key3.getPubKeyHash()));
assertEquals(key4, chain.getKey(KeyChain.KeyPurpose.CHANGE));
key1.sign(Sha256Hash.ZERO_HASH);
key2.sign(Sha256Hash.ZERO_HASH);
key3.sign(Sha256Hash.ZERO_HASH);
key4.sign(Sha256Hash.ZERO_HASH);
assertEquals(oldLookaheadSize, chain.getLookaheadSize());
}
@Test(expected = IllegalStateException.class)
public void notEncrypted() {
chain.toDecrypted("fail");
}
@Test(expected = IllegalStateException.class)
public void encryptTwice() {
chain = chain.toEncrypted("once");
chain = chain.toEncrypted("twice");
}
private void checkEncryptedKeyChain(DeterministicKeyChain encChain, DeterministicKey key1) {
// Check we can look keys up and extend the chain without the AES key being provided.
DeterministicKey encKey1 = encChain.findKeyFromPubKey(key1.getPubKey());
DeterministicKey encKey2 = encChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertFalse(key1.isEncrypted());
assertTrue(encKey1.isEncrypted());
assertEquals(encKey1.getPubKeyPoint(), key1.getPubKeyPoint());
final KeyParameter aesKey = checkNotNull(encChain.getKeyCrypter()).deriveKey("open secret");
encKey1.sign(Sha256Hash.ZERO_HASH, aesKey);
encKey2.sign(Sha256Hash.ZERO_HASH, aesKey);
assertTrue(encChain.checkAESKey(aesKey));
assertFalse(encChain.checkPassword("access denied"));
assertTrue(encChain.checkPassword("open secret"));
}
@Test
public void encryption() throws UnreadableWalletException {
DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKeyChain encChain = chain.toEncrypted("open secret");
DeterministicKey encKey1 = encChain.findKeyFromPubKey(key1.getPubKey());
checkEncryptedKeyChain(encChain, key1);
// Round-trip to ensure de/serialization works and that we can store two chains and they both deserialize.
List<Protos.Key> serialized = encChain.serializeToProtobuf();
List<Protos.Key> doubled = Lists.newArrayListWithExpectedSize(serialized.size() * 2);
doubled.addAll(serialized);
doubled.addAll(serialized);
final List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(doubled, encChain.getKeyCrypter());
assertEquals(2, chains.size());
encChain = chains.get(0);
checkEncryptedKeyChain(encChain, chain.findKeyFromPubKey(key1.getPubKey()));
encChain = chains.get(1);
checkEncryptedKeyChain(encChain, chain.findKeyFromPubKey(key1.getPubKey()));
DeterministicKey encKey2 = encChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
// Decrypt and check the keys match.
DeterministicKeyChain decChain = encChain.toDecrypted("open secret");
DeterministicKey decKey1 = decChain.findKeyFromPubHash(encKey1.getPubKeyHash());
DeterministicKey decKey2 = decChain.findKeyFromPubHash(encKey2.getPubKeyHash());
assertEquals(decKey1.getPubKeyPoint(), encKey1.getPubKeyPoint());
assertEquals(decKey2.getPubKeyPoint(), encKey2.getPubKeyPoint());
assertFalse(decKey1.isEncrypted());
assertFalse(decKey2.isEncrypted());
assertNotEquals(encKey1.getParent(), decKey1.getParent()); // parts of a different hierarchy
// Check we can once again derive keys from the decrypted chain.
decChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).sign(Sha256Hash.ZERO_HASH);
decChain.getKey(KeyChain.KeyPurpose.CHANGE).sign(Sha256Hash.ZERO_HASH);
}
@Test
public void watchingChain() throws UnreadableWalletException {
DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key3 = chain.getKey(KeyChain.KeyPurpose.CHANGE);
DeterministicKey key4 = chain.getKey(KeyChain.KeyPurpose.CHANGE);
DeterministicKey watchingKey = chain.getWatchingKey();
final String pub58 = watchingKey.serializePubB58();
assertEquals("xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi", pub58);
watchingKey = DeterministicKey.deserializeB58(null, pub58);
chain = DeterministicKeyChain.watch(watchingKey);
chain.setLookaheadSize(10);
assertEquals(key1.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint());
assertEquals(key2.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint());
final DeterministicKey key = chain.getKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(key3.getPubKeyPoint(), key.getPubKeyPoint());
try {
// Can't sign with a key from a watching chain.
key.sign(Sha256Hash.ZERO_HASH);
fail();
} catch (IllegalStateException e) {
}
// Test we can serialize and deserialize a watching chain OK.
List<Protos.Key> serialization = chain.serializeToProtobuf();
checkSerialization(serialization, "watching-wallet-serialization.txt");
chain = DeterministicKeyChain.fromProtobuf(serialization, null).get(0);
final DeterministicKey rekey4 = chain.getKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(key4.getPubKeyPoint(), rekey4.getPubKeyPoint());
}
@Test(expected = IllegalStateException.class)
public void watchingCannotEncrypt() throws Exception {
final DeterministicKey accountKey = chain.getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH);
chain = DeterministicKeyChain.watch(accountKey.getPubOnly());
chain = chain.toEncrypted("this doesn't make any sense");
}
@Test
public void bloom() {
DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
// The filter includes the internal keys as well (for now), although I'm not sure if we should allow funds to
// be received on them or not ....
assertEquals(32, chain.numBloomFilterEntries());
BloomFilter filter = chain.getFilter(32, 0.001, 1);
assertTrue(filter.contains(key1.getPubKey()));
assertTrue(filter.contains(key1.getPubKeyHash()));
assertTrue(filter.contains(key2.getPubKey()));
assertTrue(filter.contains(key2.getPubKeyHash()));
}
private String protoToString(List<Protos.Key> keys) {
StringBuilder sb = new StringBuilder();
for (Protos.Key key : keys) {
sb.append(key.toString());
sb.append("\n");
}
return sb.toString().trim();
}
private String checkSerialization(List<Protos.Key> keys, String filename) {
try {
String sb = protoToString(keys);
String expected = Resources.toString(getClass().getResource(filename), Charsets.UTF_8);
assertEquals(expected, sb);
return expected;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,325 @@
/**
* Copyright 2014 Mike Hearn
*
* 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.wallet;
import com.google.bitcoin.core.BloomFilter;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.bitcoin.utils.Threading;
import com.google.common.collect.ImmutableList;
import org.bitcoinj.wallet.Protos;
import org.junit.Test;
import org.spongycastle.crypto.params.KeyParameter;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
public class KeyChainGroupTest {
// Number of initial keys in this tests HD wallet, including interior keys.
private static final int INITIAL_KEYS = 4;
private static final int LOOKAHEAD_SIZE = 5;
private KeyChainGroup group;
public KeyChainGroupTest() {
BriefLogFormatter.init();
Utils.rollMockClock(0);
group = new KeyChainGroup();
group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests.
group.getActiveKeyChain(); // Force create a chain.
}
@Test
public void freshCurrentKeys() throws Exception {
assertEquals(INITIAL_KEYS, group.numKeys());
assertEquals(2 * INITIAL_KEYS, group.getBloomFilterElementCount());
ECKey r1 = group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
final int keys = INITIAL_KEYS + LOOKAHEAD_SIZE + 1;
assertEquals(keys, group.numKeys());
assertEquals(2 * keys, group.getBloomFilterElementCount());
ECKey i1 = new ECKey();
group.importKeys(i1);
assertEquals(keys + 1, group.numKeys());
assertEquals(2 * (keys + 1), group.getBloomFilterElementCount());
ECKey r2 = group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertEquals(r1, r2);
ECKey c1 = group.currentKey(KeyChain.KeyPurpose.CHANGE);
assertNotEquals(r1, c1);
ECKey r3 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertNotEquals(r1, r3);
ECKey c2 = group.freshKey(KeyChain.KeyPurpose.CHANGE);
assertNotEquals(r3, c2);
ECKey r4 = group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertEquals(r3, r4);
ECKey c3 = group.currentKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(c2, c3);
}
@Test
public void imports() throws Exception {
ECKey key1 = new ECKey();
assertFalse(group.removeImportedKey(key1));
assertEquals(1, group.importKeys(ImmutableList.of(key1)));
assertEquals(INITIAL_KEYS + 1, group.numKeys()); // Lookahead is triggered by requesting a key, so none yet.
group.removeImportedKey(key1);
assertEquals(INITIAL_KEYS, group.numKeys());
}
@Test
public void findKey() throws Exception {
ECKey a = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertTrue(a instanceof DeterministicKey);
ECKey b = group.freshKey(KeyChain.KeyPurpose.CHANGE);
ECKey c = new ECKey();
ECKey d = new ECKey(); // Not imported.
group.importKeys(c);
assertTrue(group.hasKey(a));
assertTrue(group.hasKey(b));
assertTrue(group.hasKey(c));
assertFalse(group.hasKey(d));
ECKey result = group.findKeyFromPubKey(a.getPubKey());
assertEquals(a, result);
result = group.findKeyFromPubKey(b.getPubKey());
assertEquals(b, result);
result = group.findKeyFromPubHash(a.getPubKeyHash());
assertEquals(a, result);
result = group.findKeyFromPubHash(b.getPubKeyHash());
assertEquals(b, result);
result = group.findKeyFromPubKey(c.getPubKey());
assertEquals(c, result);
result = group.findKeyFromPubHash(c.getPubKeyHash());
assertEquals(c, result);
assertNull(group.findKeyFromPubKey(d.getPubKey()));
assertNull(group.findKeyFromPubHash(d.getPubKeyHash()));
}
// Check encryption with and without a basic keychain.
@Test
public void encryptionWithoutImported() throws Exception {
encryption(false);
}
@Test
public void encryptionWithImported() throws Exception {
encryption(true);
}
public void encryption(boolean withImported) throws Exception {
Utils.rollMockClock(0);
long now = Utils.currentTimeSeconds();
ECKey a = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertEquals(now, group.getEarliestKeyCreationTime());
Utils.rollMockClock(-86400);
long yesterday = Utils.currentTimeSeconds();
ECKey b = new ECKey();
assertFalse(group.isEncrypted());
try {
group.checkPassword("foo"); // Cannot check password of an unencrypted group.
fail();
} catch (IllegalStateException e) {
}
if (withImported) {
assertEquals(now, group.getEarliestKeyCreationTime());
group.importKeys(b);
assertEquals(yesterday, group.getEarliestKeyCreationTime());
}
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(2);
final KeyParameter aesKey = scrypt.deriveKey("password");
group.encrypt(scrypt, aesKey);
assertTrue(group.isEncrypted());
assertTrue(group.checkPassword("password"));
assertFalse(group.checkPassword("wrong password"));
final ECKey ea = group.findKeyFromPubKey(a.getPubKey());
assertTrue(checkNotNull(ea).isEncrypted());
if (withImported) {
assertTrue(checkNotNull(group.findKeyFromPubKey(b.getPubKey())).isEncrypted());
assertEquals(yesterday, group.getEarliestKeyCreationTime());
} else {
assertEquals(now, group.getEarliestKeyCreationTime());
}
try {
ea.sign(Sha256Hash.ZERO_HASH);
fail();
} catch (ECKey.KeyIsEncryptedException e) {
// Ignored.
}
if (withImported) {
ECKey c = new ECKey();
try {
group.importKeys(c);
fail();
} catch (KeyCrypterException e) {
}
group.importKeysAndEncrypt(ImmutableList.of(c), aesKey);
ECKey ec = group.findKeyFromPubKey(c.getPubKey());
try {
group.importKeysAndEncrypt(ImmutableList.of(ec), aesKey);
fail();
} catch (IllegalArgumentException e) {
}
}
try {
group.decrypt(scrypt.deriveKey("WRONG PASSWORD"));
fail();
} catch (KeyCrypterException e) {
}
group.decrypt(aesKey);
assertFalse(group.isEncrypted());
assertFalse(checkNotNull(group.findKeyFromPubKey(a.getPubKey())).isEncrypted());
if (withImported) {
assertFalse(checkNotNull(group.findKeyFromPubKey(b.getPubKey())).isEncrypted());
assertEquals(yesterday, group.getEarliestKeyCreationTime());
} else {
assertEquals(now, group.getEarliestKeyCreationTime());
}
}
@Test
public void encryptionWhilstEmpty() throws Exception {
group = new KeyChainGroup();
group.setLookaheadSize(5);
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(2);
final KeyParameter aesKey = scrypt.deriveKey("password");
group.encrypt(scrypt, aesKey);
assertEquals(4, group.numKeys());
assertTrue(group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).isEncrypted());
final ECKey key = group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
group.decrypt(aesKey);
assertFalse(checkNotNull(group.findKeyFromPubKey(key.getPubKey())).isEncrypted());
}
@Test
public void bloom() throws Exception {
assertEquals(INITIAL_KEYS * 2, group.getBloomFilterElementCount());
ECKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
ECKey key2 = new ECKey();
final int size = (INITIAL_KEYS + LOOKAHEAD_SIZE + 1 /* for the just created key */) * 2;
assertEquals(size, group.getBloomFilterElementCount());
BloomFilter filter = group.getBloomFilter(size, 0.001, (long)(Math.random() * Long.MAX_VALUE));
assertTrue(filter.contains(key1.getPubKeyHash()));
assertTrue(filter.contains(key1.getPubKey()));
assertFalse(filter.contains(key2.getPubKey()));
// Check that the filter contains the lookahead buffer.
for (int i = 0; i < LOOKAHEAD_SIZE; i++) {
ECKey k = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertTrue(filter.contains(k.getPubKeyHash()));
}
// We ran ahead of the lookahead buffer.
assertFalse(filter.contains(group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKey()));
group.importKeys(key2);
filter = group.getBloomFilter(group.getBloomFilterElementCount(), 0.001, (long)(Math.random() * Long.MAX_VALUE));
assertTrue(filter.contains(key1.getPubKeyHash()));
assertTrue(filter.contains(key1.getPubKey()));
assertTrue(filter.contains(key2.getPubKey()));
}
@Test
public void earliestKeyTime() throws Exception {
long now = Utils.currentTimeSeconds(); // mock
long yesterday = now - 86400;
assertEquals(now, group.getEarliestKeyCreationTime());
Utils.rollMockClock(10000);
group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
Utils.rollMockClock(10000);
group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
// Check that all keys are assumed to be created at the same instant the seed is.
assertEquals(now, group.getEarliestKeyCreationTime());
ECKey key = new ECKey();
key.setCreationTimeSeconds(yesterday);
group.importKeys(key);
assertEquals(yesterday, group.getEarliestKeyCreationTime());
}
@Test
public void events() throws Exception {
// Check that events are registered with the right chains and that if a chain is added, it gets the event
// listeners attached properly even post-hoc.
final AtomicReference<ECKey> ran = new AtomicReference<ECKey>(null);
final KeyChainEventListener listener = new KeyChainEventListener() {
@Override
public void onKeysAdded(List<ECKey> keys) {
ran.set(keys.get(0));
}
};
group.addEventListener(listener, Threading.SAME_THREAD);
ECKey key = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertEquals(key, ran.getAndSet(null));
ECKey key2 = new ECKey();
group.importKeys(key2);
assertEquals(key2, ran.getAndSet(null));
group.removeEventListener(listener);
ECKey key3 = new ECKey();
group.importKeys(key3);
assertNull(ran.get());
}
@Test
public void serialization() throws Exception {
assertEquals(INITIAL_KEYS + 1 /* for the seed */, group.serializeToProtobuf().size());
DeterministicKey key1 = (DeterministicKey) group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key2 = (DeterministicKey) group.freshKey(KeyChain.KeyPurpose.CHANGE);
List<Protos.Key> protoKeys1 = group.serializeToProtobuf();
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */, protoKeys1.size());
group.importKeys(new ECKey());
List<Protos.Key> protoKeys2 = group.serializeToProtobuf();
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys2.size());
group = KeyChainGroup.fromProtobufUnencrypted(protoKeys1);
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */, protoKeys1.size());
assertTrue(group.hasKey(key1));
assertTrue(group.hasKey(key2));
group = KeyChainGroup.fromProtobufUnencrypted(protoKeys2);
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys2.size());
assertTrue(group.hasKey(key1));
assertTrue(group.hasKey(key2));
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(2);
final KeyParameter aesKey = scrypt.deriveKey("password");
group.encrypt(scrypt, aesKey);
List<Protos.Key> protoKeys3 = group.serializeToProtobuf();
group = KeyChainGroup.fromProtobufEncrypted(protoKeys3, scrypt);
assertTrue(group.isEncrypted());
assertTrue(group.checkPassword("password"));
group.decrypt(aesKey);
// No need for extensive contents testing here, as that's done in the keychain class tests.
}
@Test
public void constructFromSeed() throws Exception {
ECKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
final DeterministicSeed seed = checkNotNull(group.getActiveKeyChain().getSeed());
KeyChainGroup group2 = new KeyChainGroup(seed);
group2.setLookaheadSize(5);
ECKey key2 = group2.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertEquals(key1, key2);
}
}

View File

@ -0,0 +1,297 @@
type: DETERMINISTIC_ROOT_SEED
secret_bytes: "\004<r\377aT\'\356M\257w\260R\006\253\322}\343\267eW6S\233\210\352NS;\275I-"
creation_timestamp: 1389353062000
type: DETERMINISTIC_KEY
secret_bytes: "\241\346\003RF\t\027\367\a\f\333^C&\302-\335\314\353t&h\335{ \032\364\267\335\235\345\276"
public_key: "\003\361\245l\225\304\247X]\256,_hn\362\031\243\220\220\237z0\\<\022-O\ts\244\250\344A"
creation_timestamp: 0
deterministic_key {
chain_code: ",\263\335\024\031\221c;~\325\326\272\367*\r+\032H\270\026\234\226\357\222i_VxP\200x\252"
}
type: DETERMINISTIC_KEY
secret_bytes: "\364\r-u\037\337Sl\272\314\221L\306\356?\3505d\330\321_3W\234R#\035\273z\341x\275"
public_key: "\002LOV+\260\017\024lxz\021\236Xv\000X\324e\244\037\243\325\325f\003vs*]\260\340\035"
creation_timestamp: 0
deterministic_key {
chain_code: "\377\340\2459\230\210\367\361\362\205\267\244#\t#\360\215\221_\302v\315{\200Y\210\224\"\243\272\256\301"
path: 2147483648
}
type: DETERMINISTIC_KEY
secret_bytes: "~\301G\035\221\b\024\023\275\211s\273\270\205/4\233ai\366~\006\341$lc\000\272\336\021\347\305"
public_key: "\002\264\r,\017e\213\016W\372\024W\215z\022C@+A\2720G\016\034\353\202\312\372\251\206\035r"
creation_timestamp: 0
deterministic_key {
chain_code: "\000Mm\373e\255\363\373\'\265A\003\247\320U\305\340\342\233\033\034\312zTR\006\347Yu\362b\366"
path: 2147483648
path: 0
issued_subkeys: 2
lookahead_size: 10
}
type: DETERMINISTIC_KEY
secret_bytes: "\"\273\024]\271iR\237\335\343HN\353\352\v\220\241\006\022\302\244W\033~\260HTtz\005\376F"
public_key: "\002\330b\034\023\320\217|!\271p\034\017p\330!\245\233j\376\b\316\373\231D\324\271d\217h\217\016^"
creation_timestamp: 0
deterministic_key {
chain_code: "\367j\245\025U\265\346\v\234\275\343\rd\214q\004\232\253\312\222Hi\305\201\370`^\304\210\034p*"
path: 2147483648
path: 1
issued_subkeys: 1
lookahead_size: 10
}
type: DETERMINISTIC_KEY
secret_bytes: "gA\025\241\366\211\023k\267B \326\365\025\v\214X\001c\364\200\247\246#o\307J\357\353h\242N"
public_key: "\003\306\337\017!F\303;\257Z\320:w\353\304\021L#\250\255\345X\023k\233\323\273\253\331s\352\362\024"
creation_timestamp: 0
deterministic_key {
chain_code: "RS\20672^\r\265\fNCd\305\235\266\a\232\033\303\316\230\376FK\322\314\300}\335zk\016"
path: 2147483648
path: 0
path: 0
}
type: DETERMINISTIC_KEY
secret_bytes: "&\371/\\\237WO\250\260\366\252\245$\360\203\020\310o\3472T\204_\306\371\3547\352M\310:i"
public_key: "\003\341#\025-h\212\273XE\211\266\224|\222\251\335\375?\275A\350rU\341\212\361\221\267\303\313I\t"
creation_timestamp: 0
deterministic_key {
chain_code: ":JV\362\341\275\220\370r\031@\272\225,\307B\v\023\017\277\b\02000\261\225\026\355J\b\316G"
path: 2147483648
path: 0
path: 1
}
type: DETERMINISTIC_KEY
secret_bytes: "\360\242\254\001/\301\370\314!NV(j\370C\254\225\034\022!\035h\350i\0057\376\223\027\224i\266"
public_key: "\002\375\317\3177\306\272\204\344\210\367\203\326\tn\306\376\322\004\264\r36W\262/!\t>FN\215\302"
creation_timestamp: 0
deterministic_key {
chain_code: "\005\2717\377\3625\362\017`\270\370k\301B\241C[\350\213\244m{L&C\244\250\200$\f\025\357"
path: 2147483648
path: 0
path: 2
}
type: DETERMINISTIC_KEY
secret_bytes: "\241\262\266\022\252\\\372\271\005\356\367\241\270\307\372]\253\370\310Dj\bT&y\311~[\023\340\0355"
public_key: "\003\031\256\332?\356\255\270o\001\232\327\262\207@\275\315\355\336]\002\020\v\302)\361\037U\223\372\233\266e"
creation_timestamp: 0
deterministic_key {
chain_code: "$\211\377\t\276\033I!*\320\003\316\260Bl\r^ w\276\300\025\251\ak\317\342\034@9\204\374"
path: 2147483648
path: 0
path: 3
}
type: DETERMINISTIC_KEY
secret_bytes: "n\t_\254\221%L\355XYc\353\ae\355{5j\342\272\026\277\341\t~\325\374-WR."
public_key: "\003\273\222i0kH\005\313e\373\306c\021\340u\275\353\231\224i\333\357\017r\372\200\036PW\311\356,"
creation_timestamp: 0
deterministic_key {
chain_code: "f\315\357Y0\037\033\377)|\234\273\267\234\324\000\251\263#&\\\255tZ\313+\0003Hn\022"
path: 2147483648
path: 0
path: 4
}
type: DETERMINISTIC_KEY
secret_bytes: ">7a\'?=\203(\247c\345\242\274.w\\\006\365\337\256m\224\001a6\306\321\034\222r g"
public_key: "\003\300\022\330\270/Jy2\246\226\266\310t\344\241Q\342r\275\027\a\326:\377\230\343\037t\032\351V\207"
creation_timestamp: 0
deterministic_key {
chain_code: "\372\232\306\242\340P\251\037\227\222z\311\260\f\350 \2627@\223\247\333= \2118\331\344\006\236\362m"
path: 2147483648
path: 0
path: 5
}
type: DETERMINISTIC_KEY
secret_bytes: "\205y\v\327@\220D\247\2703\n\342O`\245\252\203\304\347\3532\216u\255g\236}\224\a\345\251\205"
public_key: "\002\350\257\214\317@\262\314sC\021[\000\201;(\000\253\326\275\335\233\'1\206\252\242@B/Fl\266"
creation_timestamp: 0
deterministic_key {
chain_code: "\2714Rw\317\230\001\356h\203\216z\230xL~[lR\032\275\247\277\362r\333q\220\242`\206\275"
path: 2147483648
path: 0
path: 6
}
type: DETERMINISTIC_KEY
secret_bytes: "\037\252\207l\370\342\336\347)\254\340\215\337\000\272o\265\3705\261\2443>\035S\323\317\035\230Y\223\356"
public_key: "\002\361z\203\341\345\350\214L\272\262\301-8/\246xX\'\r\027\026#^M\a\313\277\356\354B\022"
creation_timestamp: 0
deterministic_key {
chain_code: "\242\3452\270\275\321\363\206#\310\206\222\2359%tH\364\343\271\266\372I\204U\031Y\325CIbY"
path: 2147483648
path: 0
path: 7
}
type: DETERMINISTIC_KEY
secret_bytes: "M\000\3201\320;\247RQP\305\224\322\300Z5G\242\265\366K\000l\\\304\207~\265\n\326\033\""
public_key: "\003l\021W\350G\026kc\225\213\307Rv1[p\270P\r\266T\275\021\b\270\335\'\270\254\307\242:"
creation_timestamp: 0
deterministic_key {
chain_code: "\364E\240q\263\227Y\200\361q/\212X\343i\234\226\235\036+\n\036&\203(\341\002\235\270\021U\342"
path: 2147483648
path: 0
path: 8
}
type: DETERMINISTIC_KEY
secret_bytes: "\024\370\267\224\2021T\212`\245\v&\207^V\2670[h\321\327\035\\\361lU)h%\257\222\250"
public_key: "\003KaP\210J\320\354\202\024#*#\323\276i\\\004\341\225\253qw_\235\371\370\316\315N\rZ\031"
creation_timestamp: 0
deterministic_key {
chain_code: "\2561\251Rw\2434%\304\v]\020\220d\370\234<\217\214\306\363\361\033\262\204\265}#\224\333\255\031"
path: 2147483648
path: 0
path: 9
}
type: DETERMINISTIC_KEY
secret_bytes: "\263\364\244\214\361\267Z\254y65\024f<F\303\350\001\304\300\322,Y\024`3\"-#\217\v"
public_key: "\003\250\371\347\214\240t\242\355\277\231\3351\227g\222\375\363[\326p9\244\032\305^}\003)\000\035\252E"
creation_timestamp: 0
deterministic_key {
chain_code: "\362\203\2555\335\3013\037\361\200-\245i\225\024\322\274V#;\3157`$\360\206\332?/P]\034"
path: 2147483648
path: 0
path: 10
}
type: DETERMINISTIC_KEY
secret_bytes: "\302Rn\016_\251\377\375\036D\277\3430E\214k\323v}\361\370\375\305\310\222\335\364X\277\\\006\n"
public_key: "\002\263\220\a\311h`\216?m\375\232V\205\025zi,`\203\252{\323,&\247\304\263\006K\035j\261"
creation_timestamp: 0
deterministic_key {
chain_code: "\251K\330\274\360\254q\267\005\3331\2716\277M\3544\352S\006\243\317\223\305Y\304\317\r#\233\362H"
path: 2147483648
path: 0
path: 11
}
type: DETERMINISTIC_KEY
secret_bytes: "1dL\321\363\250\324\265\254i\270n\337\356O\230\362\237E\275\271-NTJ!\224\331\314\260a<"
public_key: "\002\336\201guA0\314$\016\335\016xTY\237I\327Zx\217\365K,k\334g\211\202\3770\247X"
creation_timestamp: 0
deterministic_key {
chain_code: "WV#4$\027\034m\023\353\235U\021,\327\303\327[\b\003\255$\024\243v\2306\276\230/\273\021"
path: 2147483648
path: 1
path: 0
}
type: DETERMINISTIC_KEY
secret_bytes: "\216\3363\303M\313wH\206\226\332\304]\027\236k\211\343dfR\fY\006\260E\277~\2346w\234"
public_key: "\002\221\322\332>W&\00475\374\317jP\021\332[[\276\363\016\2636\322:\321\361\032!?q-\320"
creation_timestamp: 0
deterministic_key {
chain_code: "\036\364!\212\223\235\037\333\346\215o\344MD4\303\206\215\327M\354.\210\201c\353\267\254\245\250\257\273"
path: 2147483648
path: 1
path: 1
}
type: DETERMINISTIC_KEY
secret_bytes: "\301\335\371\036\b\235k\352\324\356\037\345\223x8\267\347\220o\357\265\246V\223\251\3325\212\036\a\320\367"
public_key: "\003\204\311|\002}\002\201IQ\003c\253\335Ay\220@3\210\001~\345L\216u\030\217\232\262m{\371"
creation_timestamp: 0
deterministic_key {
chain_code: "\036\026\334\313\264\227\025Y\n\367X8\b\355F,\262h\2246\373\203M1\355\254>\320\r5M\r"
path: 2147483648
path: 1
path: 2
}
type: DETERMINISTIC_KEY
secret_bytes: "\256Lyzha\250\220w\022\0231\036\316\'\313\321\324\313\306\313\032{\2773\273\227\351<\266\223\036"
public_key: "\003\376WS\201\255et\362H\372\261\233\332\265\250\266Y\344\336y\240\'\025\374\222\274\261\351\032\212\313"
creation_timestamp: 0
deterministic_key {
chain_code: "N_\376\r\372n\000\263\353\v\220Z\254\023\307z)M\243g\200L\305\tU\n\n\354+C\016\277"
path: 2147483648
path: 1
path: 3
}
type: DETERMINISTIC_KEY
secret_bytes: "\'X\227U\337=\273\336\211r{\241\\c\257\341zp\341\224\353p\322Pt\254\365\206<S\366]"
public_key: "\003\000\344v\304py\001-]ut\245\32027\265\367R\331\026o\v\372|\213b1\343\356\250#"
creation_timestamp: 0
deterministic_key {
chain_code: "\030@\276\337|\300\303\255\262\374\001\222\023\240%\220\274\306=\242$\213\356\355URv2\210\257\350\201"
path: 2147483648
path: 1
path: 4
}
type: DETERMINISTIC_KEY
secret_bytes: "@\304L\023j\343\034:_\022\276\270\035\240R\202\273N\'\343(T\256\233\236\304af\215\254\300\035"
public_key: "\002P\254\2652\374\234\312\250<h\b/&\223\022\343<*\266\317\372\305\373\320J\246\324\321\357y\2736"
creation_timestamp: 0
deterministic_key {
chain_code: "\327\343\333W\312\005\321\a\271`\354\265>\325\300\212\367\217F\275~\370\200\270T\260\323\317\030\200\240\242"
path: 2147483648
path: 1
path: 5
}
type: DETERMINISTIC_KEY
secret_bytes: "\002\246\357\f\226m\2066\265c\016\r\201\023\264\230s\341\364\322\031f9\330\211\347&\333r\003E?"
public_key: "\003\000\323\225^\225\032\275R\267\347\213\256\033-\252\302\322`%fL\326\bo\337\367c\232\241\310\354\330"
creation_timestamp: 0
deterministic_key {
chain_code: "z\335|\370>\237\231\311ML3\360/\371\203\243#\037\3555\a%\231]4\213\310=\332\316\002\232"
path: 2147483648
path: 1
path: 6
}
type: DETERMINISTIC_KEY
secret_bytes: ":\357,R\200\n\fW\002\030\261t\202\006W\303\037\265\203\270\306\201\342\361\206\365\326%\263Vl\340"
public_key: "\003\310\264dd\275\026\211\247\324\221\207edi\353\036\246\350\366\370\264\213\266\357_\332x}gI\367-"
creation_timestamp: 0
deterministic_key {
chain_code: "\031\\\223*!\004\361\353|\347.\274\032P\275\337\n\224\233\230\216\2660\246\241\311\t>\255\016\313\204"
path: 2147483648
path: 1
path: 7
}
type: DETERMINISTIC_KEY
secret_bytes: "\001w\035\023\315\231\336\314\204\270&o\353\353\350\243-\375\357\344\345\337g>q\235\210\005\2323F\351"
public_key: "\003\226\253\321\200\001\346u\020_C9\nj\001} \212\027\341-f\f=\320~\311\200ck@\361\341"
creation_timestamp: 0
deterministic_key {
chain_code: "\253\201\367\346\275\232\320\314\276\005\373\031\316\355\270\276(D\220\364\343\310\370\347\272\2759\232s\300\2218"
path: 2147483648
path: 1
path: 8
}
type: DETERMINISTIC_KEY
secret_bytes: "\031\223\347\022q\261\345\032\367+\201\022\f6\242F\032\303t\361;O\372\275rp\246X\254\022\314\272"
public_key: "\0036\201\035\327\312\220\'\360\325B\276\372\347\aFp\265\252\vs\243\245\210\004y\236\250>\353[U"
creation_timestamp: 0
deterministic_key {
chain_code: "\306#\361\242\241\016-\346\341\330\325\331\352\231\220X\267y\207\302\020\353#\345\0033{\345\353\244\3362"
path: 2147483648
path: 1
path: 9
}
type: DETERMINISTIC_KEY
secret_bytes: "2\003\fr4\335b\001#\204O\231\310\274\324\037\243\335\243A\003\017\325\344\rnXl\006R)\367"
public_key: "\003J\371[{Vs\232A\260\343\376!\265\a\031`\0239\277=jd\n\230\270\034\350#\302}x\334"
creation_timestamp: 0
deterministic_key {
chain_code: "c\330\261\301\001\215\307\v\374M\231B7!/x/\215\341\265\312\027b+%\032\304\322z\304`\254"
path: 2147483648
path: 1
path: 10
}

View File

@ -0,0 +1,259 @@
type: DETERMINISTIC_KEY
public_key: "\002LOV+\260\017\024lxz\021\236Xv\000X\324e\244\037\243\325\325f\003vs*]\260\340\035"
creation_timestamp: 0
deterministic_key {
chain_code: "\377\340\2459\230\210\367\361\362\205\267\244#\t#\360\215\221_\302v\315{\200Y\210\224\"\243\272\256\301"
path: 2147483648
}
type: DETERMINISTIC_KEY
public_key: "\002\264\r,\017e\213\016W\372\024W\215z\022C@+A\2720G\016\034\353\202\312\372\251\206\035r"
creation_timestamp: 0
deterministic_key {
chain_code: "\000Mm\373e\255\363\373\'\265A\003\247\320U\305\340\342\233\033\034\312zTR\006\347Yu\362b\366"
path: 2147483648
path: 0
issued_subkeys: 2
lookahead_size: 10
}
type: DETERMINISTIC_KEY
public_key: "\002\330b\034\023\320\217|!\271p\034\017p\330!\245\233j\376\b\316\373\231D\324\271d\217h\217\016^"
creation_timestamp: 0
deterministic_key {
chain_code: "\367j\245\025U\265\346\v\234\275\343\rd\214q\004\232\253\312\222Hi\305\201\370`^\304\210\034p*"
path: 2147483648
path: 1
issued_subkeys: 1
lookahead_size: 10
}
type: DETERMINISTIC_KEY
public_key: "\003\306\337\017!F\303;\257Z\320:w\353\304\021L#\250\255\345X\023k\233\323\273\253\331s\352\362\024"
creation_timestamp: 0
deterministic_key {
chain_code: "RS\20672^\r\265\fNCd\305\235\266\a\232\033\303\316\230\376FK\322\314\300}\335zk\016"
path: 2147483648
path: 0
path: 0
}
type: DETERMINISTIC_KEY
public_key: "\003\341#\025-h\212\273XE\211\266\224|\222\251\335\375?\275A\350rU\341\212\361\221\267\303\313I\t"
creation_timestamp: 0
deterministic_key {
chain_code: ":JV\362\341\275\220\370r\031@\272\225,\307B\v\023\017\277\b\02000\261\225\026\355J\b\316G"
path: 2147483648
path: 0
path: 1
}
type: DETERMINISTIC_KEY
public_key: "\002\375\317\3177\306\272\204\344\210\367\203\326\tn\306\376\322\004\264\r36W\262/!\t>FN\215\302"
creation_timestamp: 0
deterministic_key {
chain_code: "\005\2717\377\3625\362\017`\270\370k\301B\241C[\350\213\244m{L&C\244\250\200$\f\025\357"
path: 2147483648
path: 0
path: 2
}
type: DETERMINISTIC_KEY
public_key: "\003\031\256\332?\356\255\270o\001\232\327\262\207@\275\315\355\336]\002\020\v\302)\361\037U\223\372\233\266e"
creation_timestamp: 0
deterministic_key {
chain_code: "$\211\377\t\276\033I!*\320\003\316\260Bl\r^ w\276\300\025\251\ak\317\342\034@9\204\374"
path: 2147483648
path: 0
path: 3
}
type: DETERMINISTIC_KEY
public_key: "\003\273\222i0kH\005\313e\373\306c\021\340u\275\353\231\224i\333\357\017r\372\200\036PW\311\356,"
creation_timestamp: 0
deterministic_key {
chain_code: "f\315\357Y0\037\033\377)|\234\273\267\234\324\000\251\263#&\\\255tZ\313+\0003Hn\022"
path: 2147483648
path: 0
path: 4
}
type: DETERMINISTIC_KEY
public_key: "\003\300\022\330\270/Jy2\246\226\266\310t\344\241Q\342r\275\027\a\326:\377\230\343\037t\032\351V\207"
creation_timestamp: 0
deterministic_key {
chain_code: "\372\232\306\242\340P\251\037\227\222z\311\260\f\350 \2627@\223\247\333= \2118\331\344\006\236\362m"
path: 2147483648
path: 0
path: 5
}
type: DETERMINISTIC_KEY
public_key: "\002\350\257\214\317@\262\314sC\021[\000\201;(\000\253\326\275\335\233\'1\206\252\242@B/Fl\266"
creation_timestamp: 0
deterministic_key {
chain_code: "\2714Rw\317\230\001\356h\203\216z\230xL~[lR\032\275\247\277\362r\333q\220\242`\206\275"
path: 2147483648
path: 0
path: 6
}
type: DETERMINISTIC_KEY
public_key: "\002\361z\203\341\345\350\214L\272\262\301-8/\246xX\'\r\027\026#^M\a\313\277\356\354B\022"
creation_timestamp: 0
deterministic_key {
chain_code: "\242\3452\270\275\321\363\206#\310\206\222\2359%tH\364\343\271\266\372I\204U\031Y\325CIbY"
path: 2147483648
path: 0
path: 7
}
type: DETERMINISTIC_KEY
public_key: "\003l\021W\350G\026kc\225\213\307Rv1[p\270P\r\266T\275\021\b\270\335\'\270\254\307\242:"
creation_timestamp: 0
deterministic_key {
chain_code: "\364E\240q\263\227Y\200\361q/\212X\343i\234\226\235\036+\n\036&\203(\341\002\235\270\021U\342"
path: 2147483648
path: 0
path: 8
}
type: DETERMINISTIC_KEY
public_key: "\003KaP\210J\320\354\202\024#*#\323\276i\\\004\341\225\253qw_\235\371\370\316\315N\rZ\031"
creation_timestamp: 0
deterministic_key {
chain_code: "\2561\251Rw\2434%\304\v]\020\220d\370\234<\217\214\306\363\361\033\262\204\265}#\224\333\255\031"
path: 2147483648
path: 0
path: 9
}
type: DETERMINISTIC_KEY
public_key: "\003\250\371\347\214\240t\242\355\277\231\3351\227g\222\375\363[\326p9\244\032\305^}\003)\000\035\252E"
creation_timestamp: 0
deterministic_key {
chain_code: "\362\203\2555\335\3013\037\361\200-\245i\225\024\322\274V#;\3157`$\360\206\332?/P]\034"
path: 2147483648
path: 0
path: 10
}
type: DETERMINISTIC_KEY
public_key: "\002\263\220\a\311h`\216?m\375\232V\205\025zi,`\203\252{\323,&\247\304\263\006K\035j\261"
creation_timestamp: 0
deterministic_key {
chain_code: "\251K\330\274\360\254q\267\005\3331\2716\277M\3544\352S\006\243\317\223\305Y\304\317\r#\233\362H"
path: 2147483648
path: 0
path: 11
}
type: DETERMINISTIC_KEY
public_key: "\002\336\201guA0\314$\016\335\016xTY\237I\327Zx\217\365K,k\334g\211\202\3770\247X"
creation_timestamp: 0
deterministic_key {
chain_code: "WV#4$\027\034m\023\353\235U\021,\327\303\327[\b\003\255$\024\243v\2306\276\230/\273\021"
path: 2147483648
path: 1
path: 0
}
type: DETERMINISTIC_KEY
public_key: "\002\221\322\332>W&\00475\374\317jP\021\332[[\276\363\016\2636\322:\321\361\032!?q-\320"
creation_timestamp: 0
deterministic_key {
chain_code: "\036\364!\212\223\235\037\333\346\215o\344MD4\303\206\215\327M\354.\210\201c\353\267\254\245\250\257\273"
path: 2147483648
path: 1
path: 1
}
type: DETERMINISTIC_KEY
public_key: "\003\204\311|\002}\002\201IQ\003c\253\335Ay\220@3\210\001~\345L\216u\030\217\232\262m{\371"
creation_timestamp: 0
deterministic_key {
chain_code: "\036\026\334\313\264\227\025Y\n\367X8\b\355F,\262h\2246\373\203M1\355\254>\320\r5M\r"
path: 2147483648
path: 1
path: 2
}
type: DETERMINISTIC_KEY
public_key: "\003\376WS\201\255et\362H\372\261\233\332\265\250\266Y\344\336y\240\'\025\374\222\274\261\351\032\212\313"
creation_timestamp: 0
deterministic_key {
chain_code: "N_\376\r\372n\000\263\353\v\220Z\254\023\307z)M\243g\200L\305\tU\n\n\354+C\016\277"
path: 2147483648
path: 1
path: 3
}
type: DETERMINISTIC_KEY
public_key: "\003\000\344v\304py\001-]ut\245\32027\265\367R\331\026o\v\372|\213b1\343\356\250#"
creation_timestamp: 0
deterministic_key {
chain_code: "\030@\276\337|\300\303\255\262\374\001\222\023\240%\220\274\306=\242$\213\356\355URv2\210\257\350\201"
path: 2147483648
path: 1
path: 4
}
type: DETERMINISTIC_KEY
public_key: "\002P\254\2652\374\234\312\250<h\b/&\223\022\343<*\266\317\372\305\373\320J\246\324\321\357y\2736"
creation_timestamp: 0
deterministic_key {
chain_code: "\327\343\333W\312\005\321\a\271`\354\265>\325\300\212\367\217F\275~\370\200\270T\260\323\317\030\200\240\242"
path: 2147483648
path: 1
path: 5
}
type: DETERMINISTIC_KEY
public_key: "\003\000\323\225^\225\032\275R\267\347\213\256\033-\252\302\322`%fL\326\bo\337\367c\232\241\310\354\330"
creation_timestamp: 0
deterministic_key {
chain_code: "z\335|\370>\237\231\311ML3\360/\371\203\243#\037\3555\a%\231]4\213\310=\332\316\002\232"
path: 2147483648
path: 1
path: 6
}
type: DETERMINISTIC_KEY
public_key: "\003\310\264dd\275\026\211\247\324\221\207edi\353\036\246\350\366\370\264\213\266\357_\332x}gI\367-"
creation_timestamp: 0
deterministic_key {
chain_code: "\031\\\223*!\004\361\353|\347.\274\032P\275\337\n\224\233\230\216\2660\246\241\311\t>\255\016\313\204"
path: 2147483648
path: 1
path: 7
}
type: DETERMINISTIC_KEY
public_key: "\003\226\253\321\200\001\346u\020_C9\nj\001} \212\027\341-f\f=\320~\311\200ck@\361\341"
creation_timestamp: 0
deterministic_key {
chain_code: "\253\201\367\346\275\232\320\314\276\005\373\031\316\355\270\276(D\220\364\343\310\370\347\272\2759\232s\300\2218"
path: 2147483648
path: 1
path: 8
}
type: DETERMINISTIC_KEY
public_key: "\0036\201\035\327\312\220\'\360\325B\276\372\347\aFp\265\252\vs\243\245\210\004y\236\250>\353[U"
creation_timestamp: 0
deterministic_key {
chain_code: "\306#\361\242\241\016-\346\341\330\325\331\352\231\220X\267y\207\302\020\353#\345\0033{\345\353\244\3362"
path: 2147483648
path: 1
path: 9
}
type: DETERMINISTIC_KEY
public_key: "\003J\371[{Vs\232A\260\343\376!\265\a\031`\0239\277=jd\n\230\270\034\350#\302}x\334"
creation_timestamp: 0
deterministic_key {
chain_code: "c\330\261\301\001\215\307\v\374M\231B7!/x/\215\341\265\312\027b+%\032\304\322z\304`\254"
path: 2147483648
path: 1
path: 10
}

View File

@ -34,15 +34,33 @@ message PeerAddress {
required uint64 services = 3; required uint64 services = 3;
} }
/** message EncryptedData {
* The data to store a private key encrypted with Scrypt and AES
*
*/
message EncryptedPrivateKey {
required bytes initialisation_vector = 1; // The initialisation vector for the AES encryption (16 bytes) required bytes initialisation_vector = 1; // The initialisation vector for the AES encryption (16 bytes)
required bytes encrypted_private_key = 2; // The encrypted private key required bytes encrypted_private_key = 2; // The encrypted private key
} }
/**
* Data attached to a Key message that defines the data needed by the BIP32 deterministic key hierarchy algorithm.
*/
message DeterministicKey {
// Random data that allows us to extend a key. Without this, we can't figure out the next key in the chain and
// should just treat it as a regular ORIGINAL type key.
required bytes chain_code = 1;
// The path through the key tree. Each number is encoded in the standard form: high bit set for private derivation
// and high bit unset for public derivation.
repeated uint32 path = 2;
// How many children of this key have been issued, that is, given to the user when they requested a fresh key?
// For the parents of keys being handed out, this is always less than the true number of children: the difference is
// called the lookahead zone. These keys are put into Bloom filters so we can spot transactions made by clones of
// this wallet - for instance when restoring from backup or if the seed was shared between devices.
//
// If this field is missing it means we're not issuing subkeys of this key to users.
optional uint32 issued_subkeys = 3;
optional uint32 lookahead_size = 4;
}
/** /**
* A key used to control Bitcoin spending. * A key used to control Bitcoin spending.
* *
@ -54,36 +72,55 @@ message EncryptedPrivateKey {
*/ */
message Key { message Key {
enum Type { enum Type {
ORIGINAL = 1; // Unencrypted - Original bitcoin secp256k1 curve /** Unencrypted - Original bitcoin secp256k1 curve */
ENCRYPTED_SCRYPT_AES = 2; // Encrypted with Scrypt and AES - - Original bitcoin secp256k1 curve ORIGINAL = 1;
/** Encrypted with Scrypt and AES - Original bitcoin secp256k1 curve */
ENCRYPTED_SCRYPT_AES = 2;
/**
* Not really a key, but rather contains the seed for a deterministic key hierarchy in the private_key field.
* The label and public_key fields are missing. Creation timestamp will exist.
*/
DETERMINISTIC_ROOT_SEED = 3;
/**
* A key that was derived deterministically. Note that the root seed that created it may NOT be present in the
* wallet, for the case of watching wallets. A deterministic key may or may not have the private key bytes present.
* However the public key bytes and the deterministic_key field are guaranteed to exist. In a wallet where there
* is a path from this key up to a key that has (possibly encrypted) private bytes, it's expected that the private
* key can be rederived on the fly.
*/
DETERMINISTIC_KEY = 4;
} }
required Type type = 1; required Type type = 1;
// The private EC key bytes without any ASN.1 wrapping. // Either the private EC key bytes (without any ASN.1 wrapping), or the deterministic root seed.
optional bytes private_key = 2; // If the secret is encrypted, or this is a "watching entry" then this is missing.
optional bytes secret_bytes = 2;
// The message containing the encrypted private EC key information. // If the secret data is encrypted, then secret_bytes is missing and this field is set.
// When an EncryptedPrivateKey is present then the (unencrypted) private_key will be a zero length byte array or contain all zeroes. optional EncryptedData encrypted_data = 6;
// This is for security of the private key information.
optional EncryptedPrivateKey encrypted_private_key = 6;
// The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to // The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
// do lots of slow EC math on startup. // do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
optional bytes public_key = 3; optional bytes public_key = 3;
// User-provided label associated with the key. // User-provided label associated with the key.
optional string label = 4; optional string label = 4;
// Timestamp stored as millis since epoch. Useful for skipping block bodies before this point. // Timestamp stored as millis since epoch. Useful for skipping block bodies before this point. Only reason it's
// optional is that some very old wallets don't have this data.
optional int64 creation_timestamp = 5; optional int64 creation_timestamp = 5;
optional DeterministicKey deterministic_key = 7;
} }
message Script { message Script {
required bytes program = 1; required bytes program = 1;
// Timestamp stored as millis since epoch. Useful for skipping block bodies before this point // Timestamp stored as millis since epoch. Useful for skipping block bodies before this point
// when watching for scripts on the blockchain. // when watching for scripts on the blockchain.
required int64 creation_timestamp = 2; required int64 creation_timestamp = 2;
} }
message TransactionInput { message TransactionInput {

View File

@ -17,10 +17,7 @@
package com.google.bitcoin.examples; package com.google.bitcoin.examples;
import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.*;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.kits.WalletAppKit; import com.google.bitcoin.kits.WalletAppKit;
import com.google.bitcoin.params.RegTestParams; import com.google.bitcoin.params.RegTestParams;
import com.google.bitcoin.protocols.channels.PaymentChannelClientConnection; import com.google.bitcoin.protocols.channels.PaymentChannelClientConnection;
@ -28,6 +25,7 @@ import com.google.bitcoin.protocols.channels.StoredPaymentChannelClientStates;
import com.google.bitcoin.protocols.channels.ValueOutOfRangeException; import com.google.bitcoin.protocols.channels.ValueOutOfRangeException;
import com.google.bitcoin.utils.BriefLogFormatter; import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.bitcoin.utils.Threading; import com.google.bitcoin.utils.Threading;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -38,6 +36,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -72,11 +71,11 @@ public class ExamplePaymentChannelClient {
// the plugin that knows how to parse all the additional data is present during the load. // the plugin that knows how to parse all the additional data is present during the load.
appKit = new WalletAppKit(params, new File("."), "payment_channel_example_client") { appKit = new WalletAppKit(params, new File("."), "payment_channel_example_client") {
@Override @Override
protected void addWalletExtensions() { protected List<WalletExtension> provideWalletExtensions() {
// The StoredPaymentChannelClientStates object is responsible for, amongst other things, broadcasting // The StoredPaymentChannelClientStates object is responsible for, amongst other things, broadcasting
// the refund transaction if its lock time has expired. It also persists channels so we can resume them // the refund transaction if its lock time has expired. It also persists channels so we can resume them
// after a restart. // after a restart.
wallet().addExtension(new StoredPaymentChannelClientStates(wallet(), peerGroup())); return ImmutableList.<WalletExtension>of(new StoredPaymentChannelClientStates(null, peerGroup()));
} }
}; };
appKit.connectToLocalHost(); appKit.connectToLocalHost();
@ -84,7 +83,7 @@ public class ExamplePaymentChannelClient {
appKit.awaitRunning(); appKit.awaitRunning();
// We now have active network connections and a fully synced wallet. // We now have active network connections and a fully synced wallet.
// Add a new key which will be used for the multisig contract. // Add a new key which will be used for the multisig contract.
appKit.wallet().addKey(myKey); appKit.wallet().importKey(myKey);
appKit.wallet().allowSpendingUnconfirmedTransactions(); appKit.wallet().allowSpendingUnconfirmedTransactions();
System.out.println(appKit.wallet()); System.out.println(appKit.wallet());

View File

@ -20,15 +20,18 @@ package com.google.bitcoin.examples;
import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Sha256Hash; import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.VerificationException; import com.google.bitcoin.core.VerificationException;
import com.google.bitcoin.core.WalletExtension;
import com.google.bitcoin.kits.WalletAppKit; import com.google.bitcoin.kits.WalletAppKit;
import com.google.bitcoin.params.RegTestParams; import com.google.bitcoin.params.RegTestParams;
import com.google.bitcoin.protocols.channels.*; import com.google.bitcoin.protocols.channels.*;
import com.google.bitcoin.utils.BriefLogFormatter; import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.common.collect.ImmutableList;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.List;
/** /**
* Simple server that listens on port 4242 for incoming payment channels. * Simple server that listens on port 4242 for incoming payment channels.
@ -52,12 +55,11 @@ public class ExamplePaymentChannelServer implements PaymentChannelServerListener
// the plugin that knows how to parse all the additional data is present during the load. // the plugin that knows how to parse all the additional data is present during the load.
appKit = new WalletAppKit(params, new File("."), "payment_channel_example_server") { appKit = new WalletAppKit(params, new File("."), "payment_channel_example_server") {
@Override @Override
protected void addWalletExtensions() { protected List<WalletExtension> provideWalletExtensions() {
// The StoredPaymentChannelClientStates object is responsible for, amongst other things, broadcasting // The StoredPaymentChannelClientStates object is responsible for, amongst other things, broadcasting
// the refund transaction if its lock time has expired. It also persists channels so we can resume them // the refund transaction if its lock time has expired. It also persists channels so we can resume them
// after a restart. // after a restart.
storedStates = new StoredPaymentChannelServerStates(wallet(), peerGroup()); return ImmutableList.<WalletExtension>of(new StoredPaymentChannelServerStates(null, peerGroup()));
wallet().addExtension(storedStates);
} }
}; };
appKit.connectToLocalHost(); appKit.connectToLocalHost();

View File

@ -110,7 +110,7 @@ public class ForwardingService {
} }
}); });
Address sendToAddress = kit.wallet().getKeys().get(0).toAddress(params); Address sendToAddress = kit.wallet().currentReceiveKey().toAddress(params);
System.out.println("Send coins to: " + sendToAddress); System.out.println("Send coins to: " + sendToAddress);
System.out.println("Waiting for coins to arrive. Press Ctrl-C to quit."); System.out.println("Waiting for coins to arrive. Press Ctrl-C to quit.");

View File

@ -46,7 +46,7 @@ public class PrivateKeys {
key = dumpedPrivateKey.getKey(); key = dumpedPrivateKey.getKey();
} else { } else {
BigInteger privKey = Base58.decodeToBigInteger(args[0]); BigInteger privKey = Base58.decodeToBigInteger(args[0]);
key = new ECKey(privKey); key = ECKey.fromPrivate(privKey);
} }
System.out.println("Address from private key is: " + key.toAddress(params).toString()); System.out.println("Address from private key is: " + key.toAddress(params).toString());
// And the address ... // And the address ...
@ -54,7 +54,7 @@ public class PrivateKeys {
// Import the private key to a fresh wallet. // Import the private key to a fresh wallet.
Wallet wallet = new Wallet(params); Wallet wallet = new Wallet(params);
wallet.addKey(key); wallet.importKey(key);
// Find the transactions that involve those coins. // Find the transactions that involve those coins.
final MemoryBlockStore blockStore = new MemoryBlockStore(params); final MemoryBlockStore blockStore = new MemoryBlockStore(params);

View File

@ -148,7 +148,9 @@ public class WalletTool {
DELETE_KEY, DELETE_KEY,
SYNC, SYNC,
RESET, RESET,
SEND SEND,
ENCRYPT,
DECRYPT,
} }
public enum WaitForEnum { public enum WaitForEnum {
@ -215,7 +217,8 @@ public class WalletTool {
final String HELP_TEXT = Resources.toString(WalletTool.class.getResource("wallet-tool-help.txt"), Charsets.UTF_8); final String HELP_TEXT = Resources.toString(WalletTool.class.getResource("wallet-tool-help.txt"), Charsets.UTF_8);
if (args.length == 0 || options.has("help") || options.nonOptionArguments().size() < 1) { if (args.length == 0 || options.has("help") ||
options.nonOptionArguments().size() < 1 || options.nonOptionArguments().contains("help")) {
System.out.println(HELP_TEXT); System.out.println(HELP_TEXT);
return; return;
} }
@ -338,6 +341,8 @@ public class WalletTool {
return; return;
} }
break; break;
case ENCRYPT: encrypt(); break;
case DECRYPT: decrypt(); break;
} }
if (!wallet.isConsistent()) { if (!wallet.isConsistent()) {
@ -366,6 +371,34 @@ public class WalletTool {
shutdown(); shutdown();
} }
private static void encrypt() {
if (password == null) {
System.err.println("You must provide a --password");
return;
}
if (wallet.isEncrypted()) {
System.err.println("This wallet is already encrypted.");
return;
}
wallet.encrypt(password);
}
private static void decrypt() {
if (password == null) {
System.err.println("You must provide a --password");
return;
}
if (!wallet.isEncrypted()) {
System.err.println("This wallet is not encrypted.");
return;
}
try {
wallet.decrypt(password);
} catch (KeyCrypterException e) {
System.err.println("Password incorrect.");
}
}
private static void addAddr() { private static void addAddr() {
String addr = (String) options.valueOf("addr"); String addr = (String) options.valueOf("addr");
if (addr == null) { if (addr == null) {
@ -395,10 +428,9 @@ public class WalletTool {
try { try {
BigInteger value = Utils.toNanoCoins(parts[1]); BigInteger value = Utils.toNanoCoins(parts[1]);
if (destination.startsWith("0")) { if (destination.startsWith("0")) {
boolean compressed = destination.startsWith("02") || destination.startsWith("03");
// Treat as a raw public key. // Treat as a raw public key.
BigInteger pubKey = new BigInteger(destination, 16); byte[] pubKey = new BigInteger(destination, 16).toByteArray();
ECKey key = new ECKey(null, pubKey.toByteArray(), compressed); ECKey key = ECKey.fromPublicOnly(pubKey);
t.addOutput(value, key); t.addOutput(value, key);
} else { } else {
// Treat as an address. // Treat as an address.
@ -767,10 +799,8 @@ public class WalletTool {
return; return;
} }
wallet = new Wallet(params); wallet = new Wallet(params);
if (password != null) { if (password != null)
wallet.encrypt(password); wallet.encrypt(password);
wallet.addNewEncryptedKey(password);
}
wallet.saveToFile(walletFile); wallet.saveToFile(walletFile);
} }
@ -786,6 +816,16 @@ public class WalletTool {
} }
private static void addKey() { private static void addKey() {
// If we're being given precise details, we have to import the key.
if (options.has("privkey") || options.has("pubkey")) {
importKey();
} else {
ECKey key = wallet.freshReceiveKey();
System.out.println(key.toAddress(params) + " " + key);
}
}
private static void importKey() {
ECKey key; ECKey key;
long creationTimeSeconds = getCreationTimeSeconds(); long creationTimeSeconds = getCreationTimeSeconds();
if (options.has("privkey")) { if (options.has("privkey")) {
@ -805,7 +845,7 @@ public class WalletTool {
System.err.println("Could not understand --privkey as either hex or base58: " + data); System.err.println("Could not understand --privkey as either hex or base58: " + data);
return; return;
} }
key = new ECKey(new BigInteger(1, decode)); key = ECKey.fromPrivate(new BigInteger(1, decode));
} }
if (options.has("pubkey")) { if (options.has("pubkey")) {
// Give the user a hint. // Give the user a hint.
@ -814,13 +854,10 @@ public class WalletTool {
key.setCreationTimeSeconds(creationTimeSeconds); key.setCreationTimeSeconds(creationTimeSeconds);
} else if (options.has("pubkey")) { } else if (options.has("pubkey")) {
byte[] pubkey = Utils.parseAsHexOrBase58((String) options.valueOf("pubkey")); byte[] pubkey = Utils.parseAsHexOrBase58((String) options.valueOf("pubkey"));
key = new ECKey(null, pubkey); key = ECKey.fromPublicOnly(pubkey);
key.setCreationTimeSeconds(creationTimeSeconds); key.setCreationTimeSeconds(creationTimeSeconds);
} else { } else {
// Freshly generated key. throw new IllegalStateException();
key = new ECKey();
if (creationTimeSeconds > 0)
key.setCreationTimeSeconds(creationTimeSeconds);
} }
if (wallet.findKeyFromPubKey(key.getPubKey()) != null) { if (wallet.findKeyFromPubKey(key.getPubKey()) != null) {
System.err.println("That key already exists in this wallet."); System.err.println("That key already exists in this wallet.");
@ -834,11 +871,11 @@ public class WalletTool {
} }
key = key.encrypt(wallet.getKeyCrypter(), wallet.getKeyCrypter().deriveKey(password)); key = key.encrypt(wallet.getKeyCrypter(), wallet.getKeyCrypter().deriveKey(password));
} }
wallet.addKey(key); wallet.importKey(key);
System.out.println(key.toAddress(params) + " " + key);
} catch (KeyCrypterException kce) { } catch (KeyCrypterException kce) {
System.err.println("There was an encryption related error when adding the key. The error was '" + kce.getMessage() + "'."); System.err.println("There was an encryption related error when adding the key. The error was '" + kce.getMessage() + "'.");
} }
System.out.println(key.toAddress(params) + " " + key);
} }
private static long getCreationTimeSeconds() { private static long getCreationTimeSeconds() {

View File

@ -41,7 +41,7 @@ public class Controller {
public void onBitcoinSetup() { public void onBitcoinSetup() {
bitcoin.wallet().addEventListener(new BalanceUpdater()); bitcoin.wallet().addEventListener(new BalanceUpdater());
addressControl.setAddress(bitcoin.wallet().getKeys().get(0).toAddress(Main.params).toString()); addressControl.setAddress(bitcoin.wallet().currentReceiveKey().toAddress(Main.params).toString());
refreshBalanceLabel(); refreshBalanceLabel();
} }