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:
parent
780be05260
commit
5638387d3a
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,3 +4,6 @@ target
|
|||||||
.settings
|
.settings
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
|
*.chain
|
||||||
|
*.spvchain
|
||||||
|
*.wallet
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
@ -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) + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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.
|
||||||
|
@ -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())
|
||||||
|
@ -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);
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
498
core/src/main/java/com/google/bitcoin/wallet/BasicKeyChain.java
Normal file
498
core/src/main/java/com/google/bitcoin/wallet/BasicKeyChain.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
104
core/src/main/java/com/google/bitcoin/wallet/KeyChain.java
Normal file
104
core/src/main/java/com/google/bitcoin/wallet/KeyChain.java
Normal 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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
405
core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java
Normal file
405
core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java
Normal 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
@ -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() {
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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());
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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];
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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 {
|
@ -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());
|
||||||
|
@ -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();
|
||||||
|
@ -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.");
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user