Merge remote-tracking branch 'upstream/master' into rebase

Conflicts:
	core/src/main/java/com/dogecoin/dogecoinj/core/Transaction.java
	core/src/main/java/com/dogecoin/dogecoinj/core/Wallet.java
	core/src/main/java/com/dogecoin/dogecoinj/wallet/DeterministicSeed.java
This commit is contained in:
langerhans
2014-10-30 22:32:59 +01:00
17 changed files with 419 additions and 243 deletions

View File

@@ -1117,23 +1117,36 @@ public class ECKey implements EncryptableItem, Serializable {
@Override @Override
public String toString() { public String toString() {
return toString(false); return toString(false, null);
} }
/** /**
* Produce a string rendering of the ECKey INCLUDING the private key. * Produce a string rendering of the ECKey INCLUDING the private key.
* Unless you absolutely need the private key it is better for security reasons to just use {@link #toString()}. * Unless you absolutely need the private key it is better for security reasons to just use {@link #toString()}.
*/ */
public String toStringWithPrivate() { public String toStringWithPrivate(NetworkParameters params) {
return toString(true); return toString(true, params);
} }
private String toString(boolean includePrivate) { public String getPrivateKeyAsHex() {
return Utils.HEX.encode(getPrivKeyBytes());
}
public String getPublicKeyAsHex() {
return Utils.HEX.encode(pub.getEncoded());
}
public String getPrivateKeyAsWiF(NetworkParameters params) {
return getPrivateKeyEncoded(params).toString();
}
private String toString(boolean includePrivate, NetworkParameters params) {
final ToStringHelper helper = Objects.toStringHelper(this).omitNullValues(); final ToStringHelper helper = Objects.toStringHelper(this).omitNullValues();
helper.add("pub", Utils.HEX.encode(pub.getEncoded())); helper.add("pub HEX", getPublicKeyAsHex());
if (includePrivate) { if (includePrivate) {
try { try {
helper.add("priv", Utils.HEX.encode(getPrivKey().toByteArray())); helper.add("priv HEX", getPrivateKeyAsHex());
helper.add("priv WIF", getPrivateKeyAsWiF(params));
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// TODO: Make hasPrivKey() work for deterministic keys and fix this. // TODO: Make hasPrivKey() work for deterministic keys and fix this.
} }
@@ -1156,7 +1169,7 @@ public class ECKey implements EncryptableItem, Serializable {
builder.append("\n"); builder.append("\n");
if (includePrivateKeys) { if (includePrivateKeys) {
builder.append(" "); builder.append(" ");
builder.append(toStringWithPrivate()); builder.append(toStringWithPrivate(params));
builder.append("\n"); builder.append("\n");
} }
} }

View File

@@ -90,14 +90,14 @@ public class Transaction extends ChildMessage implements Serializable {
/** /**
* If fee is lower than this value (in satoshis), a default reference client will treat it as if there were no fee. * If fee is lower than this value (in satoshis), a default reference client will treat it as if there were no fee.
* Currently this is 10000 satoshis. * Currently this is 1000 satoshis.
*/ */
public static final Coin REFERENCE_DEFAULT_MIN_TX_FEE = Coin.COIN; // 1 DOGE min fee public static final Coin REFERENCE_DEFAULT_MIN_TX_FEE = Coin.COIN; // 1 DOGE min fee
/** /**
* Any standard (ie pay-to-address) output smaller than this value (in satoshis) will most likely be rejected by the network. * Any standard (ie pay-to-address) output smaller than this value (in satoshis) will most likely be rejected by the network.
* This is calculated by assuming a standard output will be 34 bytes, and then using the formula used in * This is calculated by assuming a standard output will be 34 bytes, and then using the formula used in
* {@link TransactionOutput#getMinNonDustValue(Coin)}. * {@link TransactionOutput#getMinNonDustValue(Coin)}. Currently it's 546 satoshis.
*/ */
public static final Coin MIN_NONDUST_OUTPUT = Coin.SATOSHI; //DOGE: We can send one "shibetoshi" but this will cost us extra fee! public static final Coin MIN_NONDUST_OUTPUT = Coin.SATOSHI; //DOGE: We can send one "shibetoshi" but this will cost us extra fee!

View File

@@ -47,6 +47,7 @@ import com.google.protobuf.ByteString;
import org.bitcoin.protocols.payments.Protos.PaymentDetails; import org.bitcoin.protocols.payments.Protos.PaymentDetails;
import com.dogecoin.dogecoinj.wallet.Protos.Wallet.EncryptionType; import com.dogecoin.dogecoinj.wallet.Protos.Wallet.EncryptionType;
import org.bitcoinj.wallet.AllRandomKeysRotating;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.KeyParameter;
@@ -58,7 +59,10 @@ import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static com.google.common.base.Preconditions.*; import static com.google.common.base.Preconditions.*;
@@ -106,7 +110,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
private static final long serialVersionUID = 2L; private static final long serialVersionUID = 2L;
private static final int MINIMUM_BLOOM_DATA_LENGTH = 8; private static final int MINIMUM_BLOOM_DATA_LENGTH = 8;
// Ordering: lock > keychainLock. Keychain is protected separately to allow fast querying of current receive address
// even if the wallet itself is busy e.g. saving or processing a big reorg. Useful for reducing UI latency.
protected final ReentrantLock lock = Threading.lock("wallet"); protected final ReentrantLock lock = Threading.lock("wallet");
protected final ReentrantReadWriteLock keychainLock = Threading.factory.newReentrantReadWriteLock("wallet-keychain");
// The various pools below give quick access to wallet-relevant transactions by the state they're in: // The various pools below give quick access to wallet-relevant transactions by the state they're in:
// //
@@ -151,7 +158,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// The key chain group is not thread safe, and generally the whole hierarchy of objects should not be mutated // The key chain group is not thread safe, and generally the whole hierarchy of objects should not be mutated
// outside the wallet lock. So don't expose this object directly via any accessors! // outside the wallet lock. So don't expose this object directly via any accessors!
@GuardedBy("lock") protected KeyChainGroup keychain; @GuardedBy("keychainLock") protected KeyChainGroup keychain;
// A list of scripts watched by this wallet. // A list of scripts watched by this wallet.
private Set<Script> watchedScripts; private Set<Script> watchedScripts;
@@ -187,7 +194,6 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// UNIX time in seconds. Money controlled by keys created before this time will be automatically respent to a key // UNIX time in seconds. Money controlled by keys created before this time will be automatically respent to a key
// that was created after it. Useful when you believe some keys have been compromised. // that was created after it. Useful when you believe some keys have been compromised.
private volatile long vKeyRotationTimestamp; private volatile long vKeyRotationTimestamp;
private volatile boolean vKeyRotationEnabled;
protected transient CoinSelector coinSelector = new DefaultCoinSelector(); protected transient CoinSelector coinSelector = new DefaultCoinSelector();
@@ -338,12 +344,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* a different key (for each purpose independently). * a different key (for each purpose independently).
*/ */
public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) { public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) {
lock.lock(); keychainLock.readLock().lock();
try { try {
maybeUpgradeToHD(); maybeUpgradeToHD();
return keychain.currentKey(purpose); return keychain.currentKey(purpose);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -359,12 +365,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* Returns address for a {@link #currentKey(com.dogecoin.dogecoinj.wallet.KeyChain.KeyPurpose)} * Returns address for a {@link #currentKey(com.dogecoin.dogecoinj.wallet.KeyChain.KeyPurpose)}
*/ */
public Address currentAddress(KeyChain.KeyPurpose purpose) { public Address currentAddress(KeyChain.KeyPurpose purpose) {
lock.lock(); keychainLock.readLock().lock();
try { try {
maybeUpgradeToHD(); maybeUpgradeToHD();
return keychain.currentAddress(purpose); return keychain.currentAddress(purpose);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -397,17 +403,18 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* to someone who wishes to send money. * to someone who wishes to send money.
*/ */
public List<DeterministicKey> freshKeys(KeyChain.KeyPurpose purpose, int numberOfKeys) { public List<DeterministicKey> freshKeys(KeyChain.KeyPurpose purpose, int numberOfKeys) {
lock.lock(); List<DeterministicKey> keys;
keychainLock.writeLock().lock();
try { try {
maybeUpgradeToHD(); maybeUpgradeToHD();
List<DeterministicKey> keys = keychain.freshKeys(purpose, numberOfKeys); keys = keychain.freshKeys(purpose, numberOfKeys);
// Do we really need an immediate hard save? Arguably all this is doing is saving the 'current' key
// and that's not quite so important, so we could coalesce for more performance.
saveNow();
return keys;
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
// Do we really need an immediate hard save? Arguably all this is doing is saving the 'current' key
// and that's not quite so important, so we could coalesce for more performance.
saveNow();
return keys;
} }
/** /**
@@ -422,14 +429,15 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* Returns address for a {@link #freshKey(com.dogecoin.dogecoinj.wallet.KeyChain.KeyPurpose)} * Returns address for a {@link #freshKey(com.dogecoin.dogecoinj.wallet.KeyChain.KeyPurpose)}
*/ */
public Address freshAddress(KeyChain.KeyPurpose purpose) { public Address freshAddress(KeyChain.KeyPurpose purpose) {
lock.lock(); Address key;
keychainLock.writeLock().lock();
try { try {
Address key = keychain.freshAddress(purpose); key = keychain.freshAddress(purpose);
saveNow();
return key;
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
saveNow();
return key;
} }
/** /**
@@ -449,11 +457,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* you automatically the first time a new key is requested (this happens when spending due to the change address). * you automatically the first time a new key is requested (this happens when spending due to the change address).
*/ */
public void upgradeToDeterministic(@Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword { public void upgradeToDeterministic(@Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword {
lock.lock(); keychainLock.writeLock().lock();
try { try {
keychain.upgradeToDeterministic(vKeyRotationEnabled ? vKeyRotationTimestamp : 0, aesKey); keychain.upgradeToDeterministic(vKeyRotationTimestamp, aesKey);
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
} }
@@ -463,11 +471,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* that would require a new address or key. * that would require a new address or key.
*/ */
public boolean isDeterministicUpgradeRequired() { public boolean isDeterministicUpgradeRequired() {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.isDeterministicUpgradeRequired(); return keychain.isDeterministicUpgradeRequired();
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -476,7 +484,6 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
private void maybeUpgradeToHD(@Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword { private void maybeUpgradeToHD(@Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword {
checkState(lock.isHeldByCurrentThread());
if (keychain.isDeterministicUpgradeRequired()) { if (keychain.isDeterministicUpgradeRequired()) {
log.info("Upgrade to HD wallets is required, attempting to do so."); log.info("Upgrade to HD wallets is required, attempting to do so.");
try { try {
@@ -493,11 +500,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* Returns a snapshot of the watched scripts. This view is not live. * Returns a snapshot of the watched scripts. This view is not live.
*/ */
public List<Script> getWatchedScripts() { public List<Script> getWatchedScripts() {
lock.lock(); keychainLock.readLock().lock();
try { try {
return new ArrayList<Script>(watchedScripts); return new ArrayList<Script>(watchedScripts);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -507,11 +514,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* @return Whether the key was removed or not. * @return Whether the key was removed or not.
*/ */
public boolean removeKey(ECKey key) { public boolean removeKey(ECKey key) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
return keychain.removeImportedKey(key); return keychain.removeImportedKey(key);
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
} }
@@ -519,11 +526,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* Returns the number of keys in the key chain, including lookahead keys. * Returns the number of keys in the key chain, including lookahead keys.
*/ */
public int getKeychainSize() { public int getKeychainSize() {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.numKeys(); return keychain.numKeys();
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -531,11 +538,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* Returns a list of the non-deterministic keys that have been imported into the wallet, or the empty list if none. * Returns a list of the non-deterministic keys that have been imported into the wallet, or the empty list if none.
*/ */
public List<ECKey> getImportedKeys() { public List<ECKey> getImportedKeys() {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.getImportedKeys(); return keychain.getImportedKeys();
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -579,16 +586,17 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* in the list that was not already present. * in the list that was not already present.
*/ */
public int importKeys(final List<ECKey> keys) { public int importKeys(final List<ECKey> keys) {
lock.lock(); // API usage check.
checkNoDeterministicKeys(keys);
int result;
keychainLock.writeLock().lock();
try { try {
// API usage check. result = keychain.importKeys(keys);
checkNoDeterministicKeys(keys);
int result = keychain.importKeys(keys);
saveNow();
return result;
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
saveNow();
return result;
} }
private void checkNoDeterministicKeys(List<ECKey> keys) { private void checkNoDeterministicKeys(List<ECKey> keys) {
@@ -600,23 +608,23 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
/** Takes a list of keys and a password, then encrypts and imports them in one step using the current keycrypter. */ /** Takes a list of keys and a password, then encrypts and imports them in one step using the current keycrypter. */
public int importKeysAndEncrypt(final List<ECKey> keys, CharSequence password) { public int importKeysAndEncrypt(final List<ECKey> keys, CharSequence password) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
checkNotNull(getKeyCrypter(), "Wallet is not encrypted"); checkNotNull(getKeyCrypter(), "Wallet is not encrypted");
return importKeysAndEncrypt(keys, getKeyCrypter().deriveKey(password)); return importKeysAndEncrypt(keys, getKeyCrypter().deriveKey(password));
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
} }
/** Takes a list of keys and an AES key, then encrypts and imports them in one step using the current keycrypter. */ /** Takes a list of keys and an AES key, then encrypts and imports them in one step using the current keycrypter. */
public int importKeysAndEncrypt(final List<ECKey> keys, KeyParameter aesKey) { public int importKeysAndEncrypt(final List<ECKey> keys, KeyParameter aesKey) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
checkNoDeterministicKeys(keys); checkNoDeterministicKeys(keys);
return keychain.importKeysAndEncrypt(keys, aesKey); return keychain.importKeysAndEncrypt(keys, aesKey);
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
} }
@@ -632,7 +640,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* </p> * </p>
*/ */
public void addAndActivateHDChain(DeterministicKeyChain chain) { public void addAndActivateHDChain(DeterministicKeyChain chain) {
keychain.addAndActivateHDChain(chain); keychainLock.writeLock().lock();
try {
keychain.addAndActivateHDChain(chain);
} finally {
keychainLock.writeLock().unlock();
}
} }
/** See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */ /** See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */
@@ -647,28 +660,33 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
/** See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */ /** See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */
public int getKeychainLookaheadSize() { public int getKeychainLookaheadSize() {
return keychain.getLookaheadSize(); keychainLock.readLock().lock();
try {
return keychain.getLookaheadSize();
} finally {
keychainLock.readLock().unlock();
}
} }
/** See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#setLookaheadThreshold(int)} for more info on this. */ /** See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#setLookaheadThreshold(int)} for more info on this. */
public void setKeychainLookaheadThreshold(int num) { public void setKeychainLookaheadThreshold(int num) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
maybeUpgradeToHD(); maybeUpgradeToHD();
keychain.setLookaheadThreshold(num); keychain.setLookaheadThreshold(num);
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
} }
/** See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#setLookaheadThreshold(int)} for more info on this. */ /** See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#setLookaheadThreshold(int)} for more info on this. */
public int getKeychainLookaheadThreshold() { public int getKeychainLookaheadThreshold() {
lock.lock(); keychainLock.readLock().lock();
try { try {
maybeUpgradeToHD(); maybeUpgradeToHD();
return keychain.getLookaheadThreshold(); return keychain.getLookaheadThreshold();
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -679,12 +697,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* zero key in the recommended BIP32 hierarchy. * zero key in the recommended BIP32 hierarchy.
*/ */
public DeterministicKey getWatchingKey() { public DeterministicKey getWatchingKey() {
lock.lock(); keychainLock.readLock().lock();
try { try {
maybeUpgradeToHD(); maybeUpgradeToHD();
return keychain.getActiveKeyChain().getWatchingKey(); return keychain.getActiveKeyChain().getWatchingKey();
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -739,29 +757,27 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* @return how many scripts were added successfully * @return how many scripts were added successfully
*/ */
public int addWatchedScripts(final List<Script> scripts) { public int addWatchedScripts(final List<Script> scripts) {
lock.lock(); int added = 0;
keychainLock.writeLock().lock();
try { try {
int added = 0;
for (final Script script : scripts) { for (final Script script : scripts) {
if (watchedScripts.contains(script)) continue; if (watchedScripts.contains(script)) continue;
watchedScripts.add(script); watchedScripts.add(script);
added++; added++;
} }
queueOnScriptsAdded(scripts);
saveNow();
return added;
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
queueOnScriptsAdded(scripts);
saveNow();
return added;
} }
/** /**
* Returns all addresses watched by this wallet. * Returns all addresses watched by this wallet.
*/ */
public List<Address> getWatchedAddresses() { public List<Address> getWatchedAddresses() {
lock.lock(); keychainLock.readLock().lock();
try { try {
List<Address> addresses = new LinkedList<Address>(); List<Address> addresses = new LinkedList<Address>();
for (Script script : watchedScripts) for (Script script : watchedScripts)
@@ -769,7 +785,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
addresses.add(script.getToAddress(params)); addresses.add(script.getToAddress(params));
return addresses; return addresses;
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -782,21 +798,21 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
@Override @Override
@Nullable @Nullable
public ECKey findKeyFromPubHash(byte[] pubkeyHash) { public ECKey findKeyFromPubHash(byte[] pubkeyHash) {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.findKeyFromPubHash(pubkeyHash); return keychain.findKeyFromPubHash(pubkeyHash);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
/** Returns true if the given key is in the wallet, false otherwise. Currently an O(N) operation. */ /** Returns true if the given key is in the wallet, false otherwise. Currently an O(N) operation. */
public boolean hasKey(ECKey key) { public boolean hasKey(ECKey key) {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.hasKey(key); return keychain.hasKey(key);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -809,11 +825,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public boolean isWatchedScript(Script script) { public boolean isWatchedScript(Script script) {
lock.lock(); keychainLock.readLock().lock();
try { try {
return watchedScripts.contains(script); return watchedScripts.contains(script);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -824,11 +840,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
@Override @Override
@Nullable @Nullable
public ECKey findKeyFromPubKey(byte[] pubkey) { public ECKey findKeyFromPubKey(byte[] pubkey) {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.findKeyFromPubKey(pubkey); return keychain.findKeyFromPubKey(pubkey);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -845,11 +861,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
@Nullable @Nullable
@Override @Override
public RedeemData findRedeemDataFromScriptHash(byte[] payToScriptHash) { public RedeemData findRedeemDataFromScriptHash(byte[] payToScriptHash) {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.findRedeemDataFromScriptHash(payToScriptHash); return keychain.findRedeemDataFromScriptHash(payToScriptHash);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -864,7 +880,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#markKeyAsUsed(DeterministicKey)} for more info on this. * See {@link com.dogecoin.dogecoinj.wallet.DeterministicKeyChain#markKeyAsUsed(DeterministicKey)} for more info on this.
*/ */
private void markKeysAsUsed(Transaction tx) { private void markKeysAsUsed(Transaction tx) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
for (TransactionOutput o : tx.getOutputs()) { for (TransactionOutput o : tx.getOutputs()) {
try { try {
@@ -882,7 +898,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
} }
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
} }
@@ -891,14 +907,14 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* @throws com.dogecoin.dogecoinj.core.ECKey.MissingPrivateKeyException if the seed is unavailable (watching wallet) * @throws com.dogecoin.dogecoinj.core.ECKey.MissingPrivateKeyException if the seed is unavailable (watching wallet)
*/ */
public DeterministicSeed getKeyChainSeed() { public DeterministicSeed getKeyChainSeed() {
lock.lock(); keychainLock.readLock().lock();
try { try {
DeterministicSeed seed = keychain.getActiveKeyChain().getSeed(); DeterministicSeed seed = keychain.getActiveKeyChain().getSeed();
if (seed == null) if (seed == null)
throw new ECKey.MissingPrivateKeyException(); throw new ECKey.MissingPrivateKeyException();
return seed; return seed;
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -907,12 +923,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* use currentReceiveKey/freshReceiveKey instead. * use currentReceiveKey/freshReceiveKey instead.
*/ */
public DeterministicKey getKeyByPath(List<ChildNumber> path) { public DeterministicKey getKeyByPath(List<ChildNumber> path) {
lock.lock(); keychainLock.readLock().lock();
try { try {
maybeUpgradeToHD(); maybeUpgradeToHD();
return keychain.getActiveKeyChain().getKeyByPath(path, false); return keychain.getActiveKeyChain().getKeyByPath(path, false);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -922,14 +938,14 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* parameters to derive a key from the given password. * parameters to derive a key from the given password.
*/ */
public void encrypt(CharSequence password) { public void encrypt(CharSequence password) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
final KeyCrypterScrypt scrypt = new KeyCrypterScrypt(); final KeyCrypterScrypt scrypt = new KeyCrypterScrypt();
keychain.encrypt(scrypt, scrypt.deriveKey(password)); keychain.encrypt(scrypt, scrypt.deriveKey(password));
saveNow();
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
saveNow();
} }
/** /**
@@ -941,13 +957,13 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* @throws KeyCrypterException Thrown if the wallet encryption fails. If so, the wallet state is unchanged. * @throws KeyCrypterException Thrown if the wallet encryption fails. If so, the wallet state is unchanged.
*/ */
public void encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) { public void encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
keychain.encrypt(keyCrypter, aesKey); keychain.encrypt(keyCrypter, aesKey);
saveNow();
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
saveNow();
} }
/** /**
@@ -955,15 +971,15 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* @throws KeyCrypterException Thrown if the wallet decryption fails. If so, the wallet state is unchanged. * @throws KeyCrypterException Thrown if the wallet decryption fails. If so, the wallet state is unchanged.
*/ */
public void decrypt(CharSequence password) { public void decrypt(CharSequence password) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
final KeyCrypter crypter = keychain.getKeyCrypter(); final KeyCrypter crypter = keychain.getKeyCrypter();
checkState(crypter != null, "Not encrypted"); checkState(crypter != null, "Not encrypted");
keychain.decrypt(crypter.deriveKey(password)); keychain.decrypt(crypter.deriveKey(password));
saveNow();
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
saveNow();
} }
/** /**
@@ -973,13 +989,13 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* @throws KeyCrypterException Thrown if the wallet decryption fails. If so, the wallet state is unchanged. * @throws KeyCrypterException Thrown if the wallet decryption fails. If so, the wallet state is unchanged.
*/ */
public void decrypt(KeyParameter aesKey) { public void decrypt(KeyParameter aesKey) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
keychain.decrypt(aesKey); keychain.decrypt(aesKey);
saveNow();
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
saveNow();
} }
/** /**
@@ -990,11 +1006,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* @throws IllegalStateException if the wallet is not encrypted. * @throws IllegalStateException if the wallet is not encrypted.
*/ */
public boolean checkPassword(CharSequence password) { public boolean checkPassword(CharSequence password) {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.checkPassword(password); return keychain.checkPassword(password);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -1004,11 +1020,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* @return boolean true if AES key supplied can decrypt the first encrypted private key in the wallet, false otherwise. * @return boolean true if AES key supplied can decrypt the first encrypted private key in the wallet, false otherwise.
*/ */
public boolean checkAESKey(KeyParameter aesKey) { public boolean checkAESKey(KeyParameter aesKey) {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.checkAESKey(aesKey); return keychain.checkAESKey(aesKey);
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -1018,11 +1034,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
*/ */
@Nullable @Nullable
public KeyCrypter getKeyCrypter() { public KeyCrypter getKeyCrypter() {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.getKeyCrypter(); return keychain.getKeyCrypter();
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -1032,7 +1048,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* (This is a convenience method - the encryption type is actually stored in the keyCrypter). * (This is a convenience method - the encryption type is actually stored in the keyCrypter).
*/ */
public EncryptionType getEncryptionType() { public EncryptionType getEncryptionType() {
lock.lock(); keychainLock.readLock().lock();
try { try {
KeyCrypter crypter = keychain.getKeyCrypter(); KeyCrypter crypter = keychain.getKeyCrypter();
if (crypter != null) if (crypter != null)
@@ -1040,7 +1056,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
else else
return EncryptionType.UNENCRYPTED; return EncryptionType.UNENCRYPTED;
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -1058,11 +1074,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// TODO: Make this package private once the classes finish moving around. // TODO: Make this package private once the classes finish moving around.
/** Internal use only. */ /** Internal use only. */
public List<Protos.Key> serializeKeychainToProtobuf() { public List<Protos.Key> serializeKeychainToProtobuf() {
lock.lock(); keychainLock.readLock().lock();
try { try {
return keychain.serializeToProtobuf(); return keychain.serializeToProtobuf();
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -1474,7 +1490,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
RiskAnalysis analysis = riskAnalyzer.create(this, tx, dependencies); RiskAnalysis analysis = riskAnalyzer.create(this, tx, dependencies);
RiskAnalysis.Result result = analysis.analyze(); RiskAnalysis.Result result = analysis.analyze();
if (result != RiskAnalysis.Result.OK) { if (result != RiskAnalysis.Result.OK) {
log.warn("Pending transaction {} was considered risky: {}", tx.getHashAsString(), analysis); log.warn("Pending transaction was considered risky: {}\n{}", analysis, tx);
return true; return true;
} }
return false; return false;
@@ -2078,13 +2094,9 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* like receiving money. The listener is executed by the given executor. * like receiving money. The listener is executed by the given executor.
*/ */
public void addEventListener(WalletEventListener listener, Executor executor) { public void addEventListener(WalletEventListener listener, Executor executor) {
lock.lock(); // This is thread safe, so we don't need to take the lock.
try { eventListeners.add(new ListenerRegistration<WalletEventListener>(listener, executor));
eventListeners.add(new ListenerRegistration<WalletEventListener>(listener, executor)); keychain.addEventListener(listener, executor);
keychain.addEventListener(listener, executor);
} finally {
lock.unlock();
}
} }
/** /**
@@ -2092,13 +2104,8 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* was never added. * was never added.
*/ */
public boolean removeEventListener(WalletEventListener listener) { public boolean removeEventListener(WalletEventListener listener) {
lock.lock(); keychain.removeEventListener(listener);
try { return ListenerRegistration.removeFromList(listener, eventListeners);
keychain.removeEventListener(listener);
return ListenerRegistration.removeFromList(listener, eventListeners);
} finally {
lock.unlock();
}
} }
private void queueOnTransactionConfidenceChanged(final Transaction tx) { private void queueOnTransactionConfidenceChanged(final Transaction tx) {
@@ -2171,7 +2178,6 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
protected void queueOnScriptsAdded(final List<Script> scripts) { protected void queueOnScriptsAdded(final List<Script> scripts) {
checkState(lock.isHeldByCurrentThread());
for (final ListenerRegistration<WalletEventListener> registration : eventListeners) { for (final ListenerRegistration<WalletEventListener> registration : eventListeners) {
registration.executor.execute(new Runnable() { registration.executor.execute(new Runnable() {
@Override @Override
@@ -2607,7 +2613,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
*/ */
@Override @Override
public long getEarliestKeyCreationTime() { public long getEarliestKeyCreationTime() {
lock.lock(); keychainLock.readLock().lock();
try { try {
long earliestTime = keychain.getEarliestKeyCreationTime(); long earliestTime = keychain.getEarliestKeyCreationTime();
for (Script script : watchedScripts) for (Script script : watchedScripts)
@@ -2616,7 +2622,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
return Utils.currentTimeSeconds(); return Utils.currentTimeSeconds();
return earliestTime; return earliestTime;
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -3787,29 +3793,27 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
*/ */
@Override @Override
public int getBloomFilterElementCount() { public int getBloomFilterElementCount() {
// This is typically called by the PeerGroup, in which case it will have already explicitly taken the lock int size = 0;
// before calling, but because this is public API we must still lock again regardless. for (Transaction tx : getTransactions(false)) {
lock.lock(); for (TransactionOutput out : tx.getOutputs()) {
try { try {
int size = keychain.getBloomFilterElementCount(); if (isTxOutputBloomFilterable(out))
for (Transaction tx : getTransactions(false)) { size++;
for (TransactionOutput out : tx.getOutputs()) { } catch (ScriptException e) {
try { // If it is ours, we parsed the script correctly, so this shouldn't happen.
if (isTxOutputBloomFilterable(out)) throw new RuntimeException(e);
size++;
} catch (ScriptException e) {
throw new RuntimeException(e); // If it is ours, we parsed the script correctly, so this shouldn't happen
}
} }
} }
}
// Some scripts may have more than one bloom element. That should normally be okay, keychainLock.readLock().lock();
// because under-counting just increases false-positive rate. try {
size += keychain.getBloomFilterElementCount();
// Some scripts may have more than one bloom element. That should normally be okay, because under-counting
// just increases false-positive rate.
size += watchedScripts.size(); size += watchedScripts.size();
return size; return size;
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -3822,11 +3826,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
public boolean isRequiringUpdateAllBloomFilter() { public boolean isRequiringUpdateAllBloomFilter() {
// This is typically called by the PeerGroup, in which case it will have already explicitly taken the lock // This is typically called by the PeerGroup, in which case it will have already explicitly taken the lock
// before calling, but because this is public API we must still lock again regardless. // before calling, but because this is public API we must still lock again regardless.
lock.lock(); keychainLock.readLock().lock();
try { try {
return !watchedScripts.isEmpty(); return !watchedScripts.isEmpty();
} finally { } finally {
lock.unlock(); keychainLock.readLock().unlock();
} }
} }
@@ -3854,6 +3858,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// This is typically called by the PeerGroup, in which case it will have already explicitly taken the lock // This is typically called by the PeerGroup, in which case it will have already explicitly taken the lock
// before calling, but because this is public API we must still lock again regardless. // before calling, but because this is public API we must still lock again regardless.
lock.lock(); lock.lock();
keychainLock.readLock().lock();
try { try {
BloomFilter filter = keychain.getBloomFilter(size, falsePositiveRate, nTweak); BloomFilter filter = keychain.getBloomFilter(size, falsePositiveRate, nTweak);
@@ -3882,12 +3887,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
return filter; return filter;
} finally { } finally {
keychainLock.readLock().unlock();
lock.unlock(); lock.unlock();
} }
} }
private boolean isTxOutputBloomFilterable(TransactionOutput out) { private boolean isTxOutputBloomFilterable(TransactionOutput out) {
checkState(lock.isHeldByCurrentThread());
boolean isScriptTypeSupported = out.getScriptPubKey().isSentToRawPubKey() || out.getScriptPubKey().isPayToScriptHash(); boolean isScriptTypeSupported = out.getScriptPubKey().isSentToRawPubKey() || out.getScriptPubKey().isPayToScriptHash();
return (out.isMine(this) && isScriptTypeSupported) || return (out.isMine(this) && isScriptTypeSupported) ||
out.isWatched(this); out.isWatched(this);
@@ -3899,7 +3904,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* sequence within it to reliably find relevant transactions. * sequence within it to reliably find relevant transactions.
*/ */
public boolean checkForFilterExhaustion(FilteredBlock block) { public boolean checkForFilterExhaustion(FilteredBlock block) {
lock.lock(); keychainLock.writeLock().lock();
try { try {
int epoch = keychain.getCombinedKeyLookaheadEpochs(); int epoch = keychain.getCombinedKeyLookaheadEpochs();
for (Transaction tx : block.getAssociatedTransactions().values()) { for (Transaction tx : block.getAssociatedTransactions().values()) {
@@ -3912,7 +3917,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// block at this point and await a new filter before restarting the download. // block at this point and await a new filter before restarting the download.
return newEpoch > epoch; return newEpoch > epoch;
} finally { } finally {
lock.unlock(); keychainLock.writeLock().unlock();
} }
} }
@@ -3984,6 +3989,26 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
} }
/**
* Deserialize the wallet extension with the supplied data and add
* it to the wallet, unless there exists an extension with the
* same id.
*/
public void deserializeAndAddExtension(WalletExtension extension, byte[] data) throws Exception {
String id = checkNotNull(extension).getWalletExtensionID();
lock.lock();
try {
if (extensions.containsKey(id)) {
return;
} else {
extension.deserializeWalletExtension(this, data);
addExtension(extension);
}
} finally {
lock.unlock();
}
}
@Override @Override
public synchronized void setTag(String tag, ByteString value) { public synchronized void setTag(String tag, ByteString value) {
super.setTag(tag, value); super.setTag(tag, value);
@@ -4295,15 +4320,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
/** /**
* <p>When a key rotation time is set, and money controlled by keys created before the given timestamp T will be * <p>When a key rotation time is set, any money controlled by keys created before the given timestamp T will be
* automatically respent to any key that was created after T. This can be used to recover from a situation where * automatically respent to any key that was created after T. This can be used to recover from a situation where
* a set of keys is believed to be compromised. You can stop key rotation by calling this method again with zero * a set of keys is believed to be compromised. You can stop key rotation by calling this method again with zero
* as the argument, or by using {@link #setKeyRotationEnabled(boolean)}. Once set up, calling * as the argument. Once set up, calling {@link #doMaintenance(org.spongycastle.crypto.params.KeyParameter, boolean)}
* {@link #maybeDoMaintenance(org.spongycastle.crypto.params.KeyParameter, boolean)} will create and possibly * will create and possibly send rotation transactions: but it won't be done automatically (because you might have
* send rotation transactions: but it won't be done automatically (because you might have to ask for the users * to ask for the users password).</p>
* password).</p>
*
* <p>Note that this method won't do anything unless you call {@link #setKeyRotationEnabled(boolean)} first.</p>
* *
* <p>The given time cannot be in the future.</p> * <p>The given time cannot be in the future.</p>
*/ */
@@ -4316,36 +4338,42 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
saveNow(); saveNow();
} }
/** Toggles key rotation on and off. Note that this state is not serialized. */
public void setKeyRotationEnabled(boolean enabled) {
vKeyRotationEnabled = enabled;
}
/** Returns whether the keys creation time is before the key rotation time, if one was set. */ /** Returns whether the keys creation time is before the key rotation time, if one was set. */
public boolean isKeyRotating(ECKey key) { public boolean isKeyRotating(ECKey key) {
long time = vKeyRotationTimestamp; long time = vKeyRotationTimestamp;
return time != 0 && key.getCreationTimeSeconds() < time; return time != 0 && key.getCreationTimeSeconds() < time;
} }
/** @deprecated Renamed to doMaintenance */
@Deprecated
public ListenableFuture<List<Transaction>> maybeDoMaintenance(@Nullable KeyParameter aesKey, boolean andSend) throws DeterministicUpgradeRequiresPassword {
return doMaintenance(aesKey, andSend);
}
/** /**
* A wallet app should call this from time to time if key rotation is enabled in order to let the wallet craft and * A wallet app should call this from time to time in order to let the wallet craft and send transactions needed
* send transactions needed to re-organise coins internally. A good time to call this would be after receiving coins * to re-organise coins internally. A good time to call this would be after receiving coins for an unencrypted
* for an unencrypted wallet, or after sending money for an encrypted wallet. If you have an encrypted wallet and * wallet, or after sending money for an encrypted wallet. If you have an encrypted wallet and just want to know
* just want to know if some maintenance needs doing, call this method with doSend set to false and look at the * if some maintenance needs doing, call this method with andSend set to false and look at the returned list of
* returned list of transactions. * transactions. Maintenance might also include internal changes that involve some processing or work but
* which don't require making transactions - these will happen automatically unless the password is required
* in which case an exception will be thrown.
* *
* @param aesKey the users password, if any. * @param aesKey the users password, if any.
* @param andSend if true, send the transactions via the tx broadcaster and return them, if false just return them. * @param signAndSend if true, send the transactions via the tx broadcaster and return them, if false just return them.
* @return A list of transactions that the wallet just made/will make for internal maintenance. Might be empty. * @return A list of transactions that the wallet just made/will make for internal maintenance. Might be empty.
* @throws com.dogecoin.dogecoinj.wallet.DeterministicUpgradeRequiresPassword if key rotation requires the users password.
*/ */
public ListenableFuture<List<Transaction>> maybeDoMaintenance(@Nullable KeyParameter aesKey, boolean andSend) { public ListenableFuture<List<Transaction>> doMaintenance(@Nullable KeyParameter aesKey, boolean signAndSend) throws DeterministicUpgradeRequiresPassword {
List<Transaction> txns; List<Transaction> txns;
lock.lock(); lock.lock();
keychainLock.writeLock().lock();
try { try {
txns = maybeRotateKeys(aesKey); txns = maybeRotateKeys(aesKey, signAndSend);
if (!andSend) if (!signAndSend)
return Futures.immediateFuture(txns); return Futures.immediateFuture(txns);
} finally { } finally {
keychainLock.writeLock().unlock();
lock.unlock(); lock.unlock();
} }
checkState(!lock.isHeldByCurrentThread()); checkState(!lock.isHeldByCurrentThread());
@@ -4374,26 +4402,37 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
// Checks to see if any coins are controlled by rotating keys and if so, spends them. // Checks to see if any coins are controlled by rotating keys and if so, spends them.
private List<Transaction> maybeRotateKeys(@Nullable KeyParameter aesKey) { private List<Transaction> maybeRotateKeys(@Nullable KeyParameter aesKey, boolean sign) throws DeterministicUpgradeRequiresPassword {
checkState(lock.isHeldByCurrentThread()); checkState(lock.isHeldByCurrentThread());
checkState(keychainLock.isWriteLockedByCurrentThread());
List<Transaction> results = Lists.newLinkedList(); List<Transaction> results = Lists.newLinkedList();
// TODO: Handle chain replays here. // TODO: Handle chain replays here.
if (!vKeyRotationEnabled) return results;
// Snapshot volatiles so this method has an atomic view.
long keyRotationTimestamp = vKeyRotationTimestamp; long keyRotationTimestamp = vKeyRotationTimestamp;
if (keyRotationTimestamp == 0) return results; // Nothing to do. if (keyRotationTimestamp == 0) return results; // Nothing to do.
// We might have to create a new HD hierarchy if the previous ones are now rotating. // We might have to create a new HD hierarchy if the previous ones are now rotating.
boolean allChainsRotating = true; boolean allChainsRotating = true;
for (DeterministicKeyChain chain : keychain.getDeterministicKeyChains()) { for (DeterministicKeyChain chain : keychain.getDeterministicKeyChains()) {
if (chain.getEarliestKeyCreationTime() > vKeyRotationTimestamp) { if (chain.getEarliestKeyCreationTime() >= vKeyRotationTimestamp) {
allChainsRotating = false; allChainsRotating = false;
break; break;
} }
} }
if (allChainsRotating) { if (allChainsRotating) {
log.info("All HD chains are currently rotating, creating a new one"); try {
keychain.createAndActivateNewHDChain(); if (keychain.getImportedKeys().isEmpty()) {
log.info("All HD chains are currently rotating and we have no random keys, creating fresh HD chain ...");
keychain.createAndActivateNewHDChain();
} else {
log.info("All HD chains are currently rotating, attempting to create a new one from the next oldest non-rotating key material ...");
keychain.upgradeToDeterministic(keyRotationTimestamp, aesKey);
log.info(" ... upgraded to HD again, based on next best oldest key.");
}
} catch (AllRandomKeysRotating rotating) {
log.info(" ... no non-rotating random keys available, generating entirely new HD tree: backup required after this.");
keychain.createAndActivateNewHDChain();
}
saveNow();
} }
// Because transactions are size limited, we might not be able to re-key the entire wallet in one go. So // Because transactions are size limited, we might not be able to re-key the entire wallet in one go. So
@@ -4401,14 +4440,14 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// fully done, at least for now (we may still get more transactions later and this method will be reinvoked). // fully done, at least for now (we may still get more transactions later and this method will be reinvoked).
Transaction tx; Transaction tx;
do { do {
tx = rekeyOneBatch(keyRotationTimestamp, aesKey, results); tx = rekeyOneBatch(keyRotationTimestamp, aesKey, results, sign);
if (tx != null) results.add(tx); if (tx != null) results.add(tx);
} while (tx != null && tx.getInputs().size() == KeyTimeCoinSelector.MAX_SIMULTANEOUS_INPUTS); } while (tx != null && tx.getInputs().size() == KeyTimeCoinSelector.MAX_SIMULTANEOUS_INPUTS);
return results; return results;
} }
@Nullable @Nullable
private Transaction rekeyOneBatch(long timeSecs, @Nullable KeyParameter aesKey, List<Transaction> others) { private Transaction rekeyOneBatch(long timeSecs, @Nullable KeyParameter aesKey, List<Transaction> others, boolean sign) {
lock.lock(); lock.lock();
try { try {
// Build the transaction using some custom logic for our special needs. Last parameter to // Build the transaction using some custom logic for our special needs. Last parameter to
@@ -4440,7 +4479,8 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
rekeyTx.setPurpose(Transaction.Purpose.KEY_ROTATION); rekeyTx.setPurpose(Transaction.Purpose.KEY_ROTATION);
SendRequest req = SendRequest.forTx(rekeyTx); SendRequest req = SendRequest.forTx(rekeyTx);
req.aesKey = aesKey; req.aesKey = aesKey;
signTransaction(req); if (sign)
signTransaction(req);
// KeyTimeCoinSelector should never select enough inputs to push us oversize. // KeyTimeCoinSelector should never select enough inputs to push us oversize.
checkState(rekeyTx.bitcoinSerialize().length < Transaction.MAX_STANDARD_TX_SIZE); checkState(rekeyTx.bitcoinSerialize().length < Transaction.MAX_STANDARD_TX_SIZE);
return rekeyTx; return rekeyTx;
@@ -4462,7 +4502,39 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* If not, redisplay the confirm window and try again. * If not, redisplay the confirm window and try again.
*/ */
@Override @Override
public ReentrantLock getLock() { public Lock getLock() {
return lock; return new Lock() {
@Override
public void lock() {
lock.lock();
keychainLock.readLock().lock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock() {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock(long l, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public void unlock() {
keychainLock.readLock().unlock();
lock.unlock();
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
};
} }
} }

View File

@@ -482,7 +482,7 @@ public class DeterministicKey extends ECKey {
builder.append("\n"); builder.append("\n");
if (includePrivateKeys) { if (includePrivateKeys) {
builder.append(" "); builder.append(" ");
builder.append(toStringWithPrivate()); builder.append(toStringWithPrivate(params));
builder.append("\n"); builder.append("\n");
} }
} }

View File

@@ -300,8 +300,10 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
ECKey.fromPrivate(storedState.getMyKey().toByteArray()), ECKey.fromPrivate(storedState.getMyKey().toByteArray()),
Coin.valueOf(storedState.getValueToMe()), Coin.valueOf(storedState.getValueToMe()),
Coin.valueOf(storedState.getRefundFees()), false); Coin.valueOf(storedState.getRefundFees()), false);
if (storedState.hasCloseTransactionHash()) if (storedState.hasCloseTransactionHash()) {
channel.close = containingWallet.getTransaction(new Sha256Hash(storedState.toByteArray())); Sha256Hash closeTxHash = new Sha256Hash(storedState.getCloseTransactionHash().toByteArray());
channel.close = containingWallet.getTransaction(closeTxHash);
}
putChannel(channel, false); putChannel(channel, false);
} }
} finally { } finally {

View File

@@ -513,13 +513,14 @@ public class WalletProtobufSerializer {
} else { } else {
log.info("Loading wallet extension {}", id); log.info("Loading wallet extension {}", id);
try { try {
extension.deserializeWalletExtension(wallet, extProto.getData().toByteArray()); wallet.deserializeAndAddExtension(extension, extProto.getData().toByteArray());
wallet.addOrGetExistingExtension(extension);
} catch (Exception e) { } catch (Exception e) {
if (extProto.getMandatory() && requireMandatoryExtensions) if (extProto.getMandatory() && requireMandatoryExtensions) {
log.error("Error whilst reading extension {}, failing to read wallet", id, e);
throw new UnreadableWalletException("Could not parse mandatory extension in wallet: " + id); throw new UnreadableWalletException("Could not parse mandatory extension in wallet: " + id);
else } else {
log.error("Error whilst reading extension {}, ignoring", id, e); log.error("Error whilst reading extension {}, ignoring", id, e);
}
} }
} }
} }

View File

@@ -151,7 +151,7 @@ public class DefaultRiskAnalysis implements RiskAnalysis {
* Checks the output to see if the script violates a standardness rule. Not complete. * Checks the output to see if the script violates a standardness rule. Not complete.
*/ */
public static RuleViolation isOutputStandard(TransactionOutput output) { public static RuleViolation isOutputStandard(TransactionOutput output) {
if (MIN_ANALYSIS_NONDUST_OUTPUT.compareTo(output.getValue()) > 0) if (output.getValue().compareTo(MIN_ANALYSIS_NONDUST_OUTPUT) < 0)
return RuleViolation.DUST; return RuleViolation.DUST;
for (ScriptChunk chunk : output.getScriptPubKey().getChunks()) { for (ScriptChunk chunk : output.getScriptPubKey().getChunks()) {
if (chunk.isPushData() && !chunk.isShortestPossiblePushData()) if (chunk.isPushData() && !chunk.isShortestPossiblePushData())

View File

@@ -214,7 +214,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
return self(); return self();
} }
/** The passphrase to use with the generated mnemonic, or null. Currently must be empty. */ /** The passphrase to use with the generated mnemonic, or null if you would like to use the default empty string. Currently must be the empty string. */
public T passphrase(String passphrase) { public T passphrase(String passphrase) {
// FIXME support non-empty passphrase // FIXME support non-empty passphrase
this.passphrase = passphrase; this.passphrase = passphrase;
@@ -228,15 +228,20 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
DeterministicKeyChain chain; DeterministicKeyChain chain;
if (random != null) { if (random != null) {
chain = new DeterministicKeyChain(random, bits, passphrase, seedCreationTimeSecs); // Default passphrase to "" if not specified
chain = new DeterministicKeyChain(random, bits, getPassphrase(), seedCreationTimeSecs);
} else if (entropy != null) { } else if (entropy != null) {
chain = new DeterministicKeyChain(entropy, passphrase, seedCreationTimeSecs); chain = new DeterministicKeyChain(entropy, getPassphrase(), seedCreationTimeSecs);
} else { } else {
chain = new DeterministicKeyChain(seed); chain = new DeterministicKeyChain(seed);
} }
return chain; return chain;
} }
protected String getPassphrase() {
return passphrase != null ? passphrase : DEFAULT_PASSPHRASE_FOR_MNEMONIC;
}
} }
public static Builder<?> builder() { public static Builder<?> builder() {
@@ -1186,8 +1191,8 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
DeterministicKey parent = detkey.getParent(); DeterministicKey parent = detkey.getParent();
if (parent == null) continue; if (parent == null) continue;
if (detkey.getPath().size() <= treeSize) continue; if (detkey.getPath().size() <= treeSize) continue;
if (parent.equals(internalKey) && detkey.getChildNumber().i() > issuedInternalKeys) continue; if (parent.equals(internalKey) && detkey.getChildNumber().i() >= issuedInternalKeys) continue;
if (parent.equals(externalKey) && detkey.getChildNumber().i() > issuedExternalKeys) continue; if (parent.equals(externalKey) && detkey.getChildNumber().i() >= issuedExternalKeys) continue;
issuedKeys.add(detkey); issuedKeys.add(detkey);
} }
return issuedKeys; return issuedKeys;

View File

@@ -30,6 +30,7 @@ import java.io.UnsupportedEncodingException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.List; import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
import static com.dogecoin.dogecoinj.core.Utils.HEX; import static com.dogecoin.dogecoinj.core.Utils.HEX;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
@@ -78,7 +79,7 @@ public class DeterministicSeed implements EncryptableItem {
* @param creationTimeSeconds When the seed was originally created, UNIX time. * @param creationTimeSeconds When the seed was originally created, UNIX time.
*/ */
public DeterministicSeed(List<String> mnemonicCode, @Nullable byte[] seed, String passphrase, long creationTimeSeconds) { public DeterministicSeed(List<String> mnemonicCode, @Nullable byte[] seed, String passphrase, long creationTimeSeconds) {
this((seed != null ? seed : MnemonicCode.toSeed(mnemonicCode, passphrase)), mnemonicCode, creationTimeSeconds); this((seed != null ? seed : MnemonicCode.toSeed(mnemonicCode, checkNotNull(passphrase))), mnemonicCode, creationTimeSeconds);
} }
/** /**
@@ -90,7 +91,7 @@ public class DeterministicSeed implements EncryptableItem {
* @param creationTimeSeconds When the seed was originally created, UNIX time. * @param creationTimeSeconds When the seed was originally created, UNIX time.
*/ */
public DeterministicSeed(SecureRandom random, int bits, String passphrase, long creationTimeSeconds) { public DeterministicSeed(SecureRandom random, int bits, String passphrase, long creationTimeSeconds) {
this(getEntropy(random, bits), passphrase, creationTimeSeconds); this(getEntropy(random, bits), checkNotNull(passphrase), creationTimeSeconds);
} }
/** /**
@@ -101,8 +102,9 @@ public class DeterministicSeed implements EncryptableItem {
* @param creationTimeSeconds When the seed was originally created, UNIX time. * @param creationTimeSeconds When the seed was originally created, UNIX time.
*/ */
public DeterministicSeed(byte[] entropy, String passphrase, long creationTimeSeconds) { public DeterministicSeed(byte[] entropy, String passphrase, long creationTimeSeconds) {
Preconditions.checkArgument(entropy.length % 4 == 0, "entropy size in bits not divisible by 32"); checkArgument(entropy.length % 4 == 0, "entropy size in bits not divisible by 32");
Preconditions.checkArgument(entropy.length * 8 >= DEFAULT_SEED_ENTROPY_BITS, "entropy size too small"); checkArgument(entropy.length * 8 >= DEFAULT_SEED_ENTROPY_BITS, "entropy size too small");
checkNotNull(passphrase);
try { try {
this.mnemonicCode = MnemonicCode.INSTANCE.toMnemonic(entropy); this.mnemonicCode = MnemonicCode.INSTANCE.toMnemonic(entropy);
@@ -116,7 +118,7 @@ public class DeterministicSeed implements EncryptableItem {
} }
private static byte[] getEntropy(SecureRandom random, int bits) { private static byte[] getEntropy(SecureRandom random, int bits) {
Preconditions.checkArgument(bits <= MAX_SEED_ENTROPY_BITS, "requested entropy size too large"); checkArgument(bits <= MAX_SEED_ENTROPY_BITS, "requested entropy size too large");
byte[] seed = new byte[bits / 8]; byte[] seed = new byte[bits / 8];
random.nextBytes(seed); random.nextBytes(seed);

View File

@@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.AllRandomKeysRotating;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.KeyParameter;
@@ -68,7 +69,7 @@ public class KeyChainGroup implements KeyBag {
private BasicKeyChain basic; private BasicKeyChain basic;
private NetworkParameters params; private NetworkParameters params;
private final LinkedList<DeterministicKeyChain> chains; protected final LinkedList<DeterministicKeyChain> chains;
private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys; private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys;
private EnumMap<KeyChain.KeyPurpose, Address> currentAddresses; private EnumMap<KeyChain.KeyPurpose, Address> currentAddresses;
@@ -630,12 +631,14 @@ public class KeyChainGroup implements KeyBag {
* and you should provide the users encryption key. * and you should provide the users encryption key.
* @return the DeterministicKeyChain that was created by the upgrade. * @return the DeterministicKeyChain that was created by the upgrade.
*/ */
public DeterministicKeyChain upgradeToDeterministic(long keyRotationTimeSecs, @Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword { public DeterministicKeyChain upgradeToDeterministic(long keyRotationTimeSecs, @Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword, AllRandomKeysRotating {
checkState(chains.isEmpty());
checkState(basic.numKeys() > 0); checkState(basic.numKeys() > 0);
checkArgument(keyRotationTimeSecs >= 0); checkArgument(keyRotationTimeSecs >= 0);
ECKey keyToUse = basic.findOldestKeyAfter(keyRotationTimeSecs); // Subtract one because the key rotation time might have been set to the creation time of the first known good
checkArgument(keyToUse != null, "All keys are considered rotating, so we cannot upgrade deterministically."); // key, in which case, that's the one we want to find.
ECKey keyToUse = basic.findOldestKeyAfter(keyRotationTimeSecs - 1);
if (keyToUse == null)
throw new AllRandomKeysRotating();
if (keyToUse.isEncrypted()) { if (keyToUse.isEncrypted()) {
if (aesKey == null) { if (aesKey == null) {
@@ -658,7 +661,12 @@ public class KeyChainGroup implements KeyBag {
throw new IllegalStateException("AES Key was provided but wallet is not encrypted."); throw new IllegalStateException("AES Key was provided but wallet is not encrypted.");
} }
log.info("Auto-upgrading pre-HD wallet using oldest non-rotating private key"); if (chains.isEmpty()) {
log.info("Auto-upgrading pre-HD wallet to HD!");
} else {
log.info("Wallet with existing HD chain is being re-upgraded due to change in key rotation time.");
}
log.info("Instantiating new HD chain using oldest non-rotating private key (address: {})", keyToUse.toAddress(params));
byte[] entropy = checkNotNull(keyToUse.getSecretBytes()); byte[] entropy = checkNotNull(keyToUse.getSecretBytes());
// Private keys should be at least 128 bits long. // Private keys should be at least 128 bits long.
checkState(entropy.length >= DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8); checkState(entropy.length >= DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);

View File

@@ -97,9 +97,9 @@ public class MarriedKeyChain extends DeterministicKeyChain {
if (threshold == 0) if (threshold == 0)
threshold = (followingKeys.size() + 1) / 2 + 1; threshold = (followingKeys.size() + 1) / 2 + 1;
if (random != null) { if (random != null) {
chain = new MarriedKeyChain(random, bits, passphrase, seedCreationTimeSecs); chain = new MarriedKeyChain(random, bits, getPassphrase(), seedCreationTimeSecs);
} else if (entropy != null) { } else if (entropy != null) {
chain = new MarriedKeyChain(entropy, passphrase, seedCreationTimeSecs); chain = new MarriedKeyChain(entropy, getPassphrase(), seedCreationTimeSecs);
} else { } else {
chain = new MarriedKeyChain(seed); chain = new MarriedKeyChain(seed);
} }

View File

@@ -0,0 +1,7 @@
package org.bitcoinj.wallet;
/**
* Indicates that an attempt was made to upgrade a random wallet to deterministic, but there were no non-rotating
* random keys to use as source material for the seed. Add a non-compromised key first!
*/
public class AllRandomKeysRotating extends RuntimeException {}

View File

@@ -317,9 +317,21 @@ public class ECKeyTest {
@Test @Test
public void testToString() throws Exception { public void testToString() throws Exception {
ECKey key = ECKey.fromPrivate(BigInteger.TEN).decompress(); // An example private key. ECKey key = ECKey.fromPrivate(BigInteger.TEN).decompress(); // An example private key.
NetworkParameters params = MainNetParams.get();
assertEquals("ECKey{pub HEX=04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7, isEncrypted=false}", key.toString());
assertEquals("ECKey{pub HEX=04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7, priv HEX=000000000000000000000000000000000000000000000000000000000000000a, priv WIF=5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreBoNWTw6, isEncrypted=false}", key.toStringWithPrivate(params));
}
assertEquals("ECKey{pub=04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7, isEncrypted=false}", key.toString()); @Test
assertEquals("ECKey{pub=04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7, priv=0a, isEncrypted=false}", key.toStringWithPrivate()); public void testGetPrivateKeyAsHex() throws Exception {
ECKey key = ECKey.fromPrivate(BigInteger.TEN).decompress(); // An example private key.
assertEquals("000000000000000000000000000000000000000000000000000000000000000a", key.getPrivateKeyAsHex());
}
@Test
public void testGetPublicKeyAsHex() throws Exception {
ECKey key = ECKey.fromPrivate(BigInteger.TEN).decompress(); // An example private key.
assertEquals("04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7", key.getPublicKeyAsHex());
} }
@Test @Test

View File

@@ -52,6 +52,7 @@ 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;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static com.dogecoin.dogecoinj.core.Coin.*; import static com.dogecoin.dogecoinj.core.Coin.*;
import static com.dogecoin.dogecoinj.core.Utils.HEX; import static com.dogecoin.dogecoinj.core.Utils.HEX;
@@ -78,6 +79,7 @@ public class WalletTest extends TestWithWallet {
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
// TODO: Move these fields into the right tests so we don't create two wallets for every test case.
encryptedWallet = new Wallet(params); encryptedWallet = new Wallet(params);
myEncryptedAddress = encryptedWallet.freshReceiveKey().toAddress(params); myEncryptedAddress = encryptedWallet.freshReceiveKey().toAddress(params);
encryptedWallet.encrypt(PASSWORD1); encryptedWallet.encrypt(PASSWORD1);
@@ -96,7 +98,6 @@ public class WalletTest extends TestWithWallet {
createMarriedWallet(threshold, numKeys, true); createMarriedWallet(threshold, numKeys, true);
} }
private void createMarriedWallet(int threshold, int numKeys, boolean addSigners) throws BlockStoreException { private void createMarriedWallet(int threshold, int numKeys, boolean addSigners) throws BlockStoreException {
wallet = new Wallet(params); wallet = new Wallet(params);
blockStore = new MemoryBlockStore(params); blockStore = new MemoryBlockStore(params);
@@ -1956,43 +1957,45 @@ public class WalletTest extends TestWithWallet {
// Generate a ton of small outputs // Generate a ton of small outputs
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1); StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1);
int i = 0; int i = 0;
while (i <= CENT.divide(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE)) { Coin tenThousand = Coin.valueOf(10000);
Transaction tx = createFakeTxWithChangeAddress(params, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, myAddress, notMyAddr); while (i <= 100) {
Transaction tx = createFakeTxWithChangeAddress(params, tenThousand, myAddress, notMyAddr);
tx.getInput(0).setSequenceNumber(i++); // Keep every transaction unique tx.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i); wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
} }
Coin balance = wallet.getBalance();
// Create a spend that will throw away change (category 3 type 2 in which the change causes fee which is worth more than change) // Create a spend that will throw away change (category 3 type 2 in which the change causes fee which is worth more than change)
SendRequest request1 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(SATOSHI)); SendRequest request1 = SendRequest.to(notMyAddr, balance.subtract(SATOSHI));
wallet.completeTx(request1); wallet.completeTx(request1);
assertEquals(SATOSHI, request1.tx.getFee()); assertEquals(SATOSHI, request1.tx.getFee());
assertEquals(request1.tx.getInputs().size(), i); // We should have spent all inputs assertEquals(request1.tx.getInputs().size(), i); // We should have spent all inputs
// Give us one more input... // Give us one more input...
Transaction tx1 = createFakeTxWithChangeAddress(params, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, myAddress, notMyAddr); Transaction tx1 = createFakeTxWithChangeAddress(params, tenThousand, myAddress, notMyAddr);
tx1.getInput(0).setSequenceNumber(i++); // Keep every transaction unique tx1.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx1, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i); wallet.receiveFromBlock(tx1, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
// ... and create a spend that will throw away change (category 3 type 1 in which the change causes dust output) // ... and create a spend that will throw away change (category 3 type 1 in which the change causes dust output)
SendRequest request2 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(SATOSHI)); SendRequest request2 = SendRequest.to(notMyAddr, balance.subtract(SATOSHI));
wallet.completeTx(request2); wallet.completeTx(request2);
assertEquals(SATOSHI, request2.tx.getFee()); assertEquals(SATOSHI, request2.tx.getFee());
assertEquals(request2.tx.getInputs().size(), i - 1); // We should have spent all inputs - 1 assertEquals(request2.tx.getInputs().size(), i - 1); // We should have spent all inputs - 1
// Give us one more input... // Give us one more input...
Transaction tx2 = createFakeTxWithChangeAddress(params, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, myAddress, notMyAddr); Transaction tx2 = createFakeTxWithChangeAddress(params, tenThousand, myAddress, notMyAddr);
tx2.getInput(0).setSequenceNumber(i++); // Keep every transaction unique tx2.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx2, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i); wallet.receiveFromBlock(tx2, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
// ... and create a spend that will throw away change (category 3 type 1 in which the change causes dust output) // ... and create a spend that will throw away change (category 3 type 1 in which the change causes dust output)
// but that also could have been category 2 if it wanted // but that also could have been category 2 if it wanted
SendRequest request3 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(SATOSHI)); SendRequest request3 = SendRequest.to(notMyAddr, CENT.add(tenThousand).subtract(SATOSHI));
wallet.completeTx(request3); wallet.completeTx(request3);
assertEquals(SATOSHI, request3.tx.getFee()); assertEquals(SATOSHI, request3.tx.getFee());
assertEquals(request3.tx.getInputs().size(), i - 2); // We should have spent all inputs - 2 assertEquals(request3.tx.getInputs().size(), i - 2); // We should have spent all inputs - 2
// //
SendRequest request4 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(SATOSHI)); SendRequest request4 = SendRequest.to(notMyAddr, balance.subtract(SATOSHI));
request4.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.divide(request3.tx.bitcoinSerialize().length); request4.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.divide(request3.tx.bitcoinSerialize().length);
wallet.completeTx(request4); wallet.completeTx(request4);
assertEquals(SATOSHI, request4.tx.getFee()); assertEquals(SATOSHI, request4.tx.getFee());
@@ -2000,24 +2003,24 @@ public class WalletTest extends TestWithWallet {
// Give us a few more inputs... // Give us a few more inputs...
while (wallet.getBalance().compareTo(CENT.multiply(2)) < 0) { while (wallet.getBalance().compareTo(CENT.multiply(2)) < 0) {
Transaction tx3 = createFakeTxWithChangeAddress(params, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, myAddress, notMyAddr); Transaction tx3 = createFakeTxWithChangeAddress(params, tenThousand, myAddress, notMyAddr);
tx3.getInput(0).setSequenceNumber(i++); // Keep every transaction unique tx3.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx3, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i); wallet.receiveFromBlock(tx3, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
} }
// ...that is just slightly less than is needed for category 1 // ...that is just slightly less than is needed for category 1
SendRequest request5 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(SATOSHI)); SendRequest request5 = SendRequest.to(notMyAddr, CENT.add(tenThousand).subtract(SATOSHI));
wallet.completeTx(request5); wallet.completeTx(request5);
assertEquals(SATOSHI, request5.tx.getFee()); assertEquals(SATOSHI, request5.tx.getFee());
assertEquals(1, request5.tx.getOutputs().size()); // We should have no change output assertEquals(1, request5.tx.getOutputs().size()); // We should have no change output
// Give us one more input... // Give us one more input...
Transaction tx4 = createFakeTxWithChangeAddress(params, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, myAddress, notMyAddr); Transaction tx4 = createFakeTxWithChangeAddress(params, tenThousand, myAddress, notMyAddr);
tx4.getInput(0).setSequenceNumber(i); // Keep every transaction unique tx4.getInput(0).setSequenceNumber(i); // Keep every transaction unique
wallet.receiveFromBlock(tx4, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i); wallet.receiveFromBlock(tx4, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
// ... that puts us in category 1 (no fee!) // ... that puts us in category 1 (no fee!)
SendRequest request6 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(SATOSHI)); SendRequest request6 = SendRequest.to(notMyAddr, CENT.add(tenThousand).subtract(SATOSHI));
wallet.completeTx(request6); wallet.completeTx(request6);
assertEquals(ZERO, request6.tx.getFee()); assertEquals(ZERO, request6.tx.getFee());
assertEquals(2, request6.tx.getOutputs().size()); // We should have a change output assertEquals(2, request6.tx.getOutputs().size()); // We should have a change output
@@ -2288,7 +2291,6 @@ public class WalletTest extends TestWithWallet {
wallet = new Wallet(params); wallet = new Wallet(params);
// Watch out for wallet-initiated broadcasts. // Watch out for wallet-initiated broadcasts.
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet); MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
wallet.setKeyRotationEnabled(true);
// Send three cents to two different random keys, then add a key and mark the initial keys as compromised. // Send three cents to two different random keys, then add a key and mark the initial keys as compromised.
ECKey key1 = new ECKey(); ECKey key1 = new ECKey();
key1.setCreationTimeSeconds(Utils.currentTimeSeconds() - (86400 * 2)); key1.setCreationTimeSeconds(Utils.currentTimeSeconds() - (86400 * 2));
@@ -2303,14 +2305,11 @@ public class WalletTest extends TestWithWallet {
assertEquals(0, broadcaster.size()); assertEquals(0, broadcaster.size());
assertFalse(wallet.isKeyRotating(key1)); assertFalse(wallet.isKeyRotating(key1));
// We got compromised! We have an old style random-only wallet. So let's upgrade to HD: for that we need a fresh // We got compromised!
// random key that's not rotating as the wallet won't create a new seed for us, it'll just refuse to upgrade.
Utils.rollMockClock(1); Utils.rollMockClock(1);
ECKey key3 = new ECKey();
wallet.importKey(key3);
wallet.setKeyRotationTime(compromiseTime); wallet.setKeyRotationTime(compromiseTime);
assertTrue(wallet.isKeyRotating(key1)); assertTrue(wallet.isKeyRotating(key1));
wallet.maybeDoMaintenance(null, true); wallet.doMaintenance(null, true);
Transaction tx = broadcaster.waitForTransactionAndSucceed(); Transaction tx = broadcaster.waitForTransactionAndSucceed();
final Coin THREE_CENTS = CENT.add(CENT).add(CENT); final Coin THREE_CENTS = CENT.add(CENT).add(CENT);
@@ -2327,12 +2326,12 @@ public class WalletTest extends TestWithWallet {
// Now receive some more money to the newly derived address via a new block and check that nothing happens. // Now receive some more money to the newly derived address via a new block and check that nothing happens.
sendMoneyToWallet(wallet, CENT, toAddress, AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(wallet, CENT, toAddress, AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertTrue(wallet.maybeDoMaintenance(null, true).get().isEmpty()); assertTrue(wallet.doMaintenance(null, true).get().isEmpty());
assertEquals(0, broadcaster.size()); assertEquals(0, broadcaster.size());
// Receive money via a new block on key1 and ensure it shows up as a maintenance task. // Receive money via a new block on key1 and ensure it shows up as a maintenance task.
sendMoneyToWallet(wallet, CENT, key1.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(wallet, CENT, key1.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
wallet.maybeDoMaintenance(null, true); wallet.doMaintenance(null, true);
tx = broadcaster.waitForTransactionAndSucceed(); tx = broadcaster.waitForTransactionAndSucceed();
assertNotNull(wallet.findKeyFromPubHash(tx.getOutput(0).getScriptPubKey().getPubKeyHash())); assertNotNull(wallet.findKeyFromPubHash(tx.getOutput(0).getScriptPubKey().getPubKeyHash()));
log.info("Unexpected thing: {}", tx); log.info("Unexpected thing: {}", tx);
@@ -2375,14 +2374,68 @@ public class WalletTest extends TestWithWallet {
// A day later, we get compromised. // A day later, we get compromised.
Utils.rollMockClock(86400); Utils.rollMockClock(86400);
wallet.setKeyRotationTime(Utils.currentTimeSeconds()); wallet.setKeyRotationTime(Utils.currentTimeSeconds());
wallet.setKeyRotationEnabled(true);
List<Transaction> txns = wallet.maybeDoMaintenance(null, false).get(); List<Transaction> txns = wallet.doMaintenance(null, false).get();
assertEquals(1, txns.size()); assertEquals(1, txns.size());
DeterministicKey watchKey2 = wallet.getWatchingKey(); DeterministicKey watchKey2 = wallet.getWatchingKey();
assertNotEquals(watchKey1, watchKey2); assertNotEquals(watchKey1, watchKey2);
} }
@SuppressWarnings("ConstantConditions")
@Test
public void keyRotationHD2() throws Exception {
// Check we handle the following scenario: a weak random key is created, then some good random keys are created
// but the weakness of the first isn't known yet. The wallet is upgraded to HD based on the weak key. Later, we
// find out about the weakness and set the rotation time to after the bad key's creation date. A new HD chain
// should be created based on the oldest known good key and the old chain + bad random key should rotate to it.
// We fix the private keys just to make the test deterministic (last byte differs).
Utils.setMockClock();
ECKey badKey = ECKey.fromPrivate(Utils.HEX.decode("00905b93f990267f4104f316261fc10f9f983551f9ef160854f40102eb71cffdbb"));
badKey.setCreationTimeSeconds(Utils.currentTimeSeconds());
Utils.rollMockClock(86400);
ECKey goodKey = ECKey.fromPrivate(Utils.HEX.decode("00905b93f990267f4104f316261fc10f9f983551f9ef160854f40102eb71cffdcc"));
goodKey.setCreationTimeSeconds(Utils.currentTimeSeconds());
// Do an upgrade based on the bad key.
final AtomicReference<List<DeterministicKeyChain>> fChains = new AtomicReference<List<DeterministicKeyChain>>();
KeyChainGroup kcg = new KeyChainGroup(params) {
{
fChains.set(chains);
}
};
kcg.importKeys(badKey, goodKey);
Utils.rollMockClock(86400);
wallet = new Wallet(params, kcg); // This avoids the automatic HD initialisation
assertTrue(fChains.get().isEmpty());
wallet.upgradeToDeterministic(null);
DeterministicKey badWatchingKey = wallet.getWatchingKey();
assertEquals(badKey.getCreationTimeSeconds(), badWatchingKey.getCreationTimeSeconds());
sendMoneyToWallet(wallet, CENT, badWatchingKey.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
// Now we set the rotation time to the time we started making good keys. This should create a new HD chain.
wallet.setKeyRotationTime(goodKey.getCreationTimeSeconds());
List<Transaction> txns = wallet.doMaintenance(null, false).get();
assertEquals(1, txns.size());
Address output = txns.get(0).getOutput(0).getAddressFromP2PKHScript(params);
ECKey usedKey = wallet.findKeyFromPubHash(output.getHash160());
assertEquals(goodKey.getCreationTimeSeconds(), usedKey.getCreationTimeSeconds());
assertEquals(goodKey.getCreationTimeSeconds(), wallet.freshReceiveKey().getCreationTimeSeconds());
assertEquals("mrM3TpCnav5YQuVA1xLercCGJH4DXujMtv", usedKey.toAddress(params).toString());
DeterministicKeyChain c = fChains.get().get(1);
assertEquals(c.getEarliestKeyCreationTime(), goodKey.getCreationTimeSeconds());
assertEquals(2, fChains.get().size());
// Commit the maint txns.
wallet.commitTx(txns.get(0));
// Check next maintenance does nothing.
assertTrue(wallet.doMaintenance(null, false).get().isEmpty());
assertEquals(c, fChains.get().get(1));
assertEquals(2, fChains.get().size());
}
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void importOfHDKeyForbidden() throws Exception { public void importOfHDKeyForbidden() throws Exception {
wallet.importKey(wallet.freshReceiveKey()); wallet.importKey(wallet.freshReceiveKey());
@@ -2400,13 +2453,12 @@ public class WalletTest extends TestWithWallet {
} }
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet); MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
wallet.setKeyRotationEnabled(true);
Date compromise = Utils.now(); Date compromise = Utils.now();
Utils.rollMockClock(86400); Utils.rollMockClock(86400);
wallet.freshReceiveKey(); wallet.freshReceiveKey();
wallet.setKeyRotationTime(compromise); wallet.setKeyRotationTime(compromise);
wallet.maybeDoMaintenance(null, true); wallet.doMaintenance(null, true);
Transaction tx = broadcaster.waitForTransactionAndSucceed(); Transaction tx = broadcaster.waitForTransactionAndSucceed();
final Coin valueSentToMe = tx.getValueSentToMe(wallet); final Coin valueSentToMe = tx.getValueSentToMe(wallet);

View File

@@ -68,6 +68,14 @@ public class DeterministicKeyChainTest {
key3.sign(Sha256Hash.ZERO_HASH); key3.sign(Sha256Hash.ZERO_HASH);
} }
@Test
public void getKeys() throws Exception {
chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
chain.getKey(KeyChain.KeyPurpose.CHANGE);
chain.maybeLookAhead();
assertEquals(2, chain.getKeys(false).size());
}
@Test @Test
public void signMessage() throws Exception { public void signMessage() throws Exception {
ECKey key = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); ECKey key = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);

View File

@@ -446,9 +446,10 @@ public class WalletTool {
long rotationTimeSecs = Utils.currentTimeSeconds(); long rotationTimeSecs = Utils.currentTimeSeconds();
if (options.has(dateFlag)) { if (options.has(dateFlag)) {
rotationTimeSecs = options.valueOf(dateFlag).getTime() / 1000; rotationTimeSecs = options.valueOf(dateFlag).getTime() / 1000;
} else if (options.has(unixtimeFlag)) {
rotationTimeSecs = options.valueOf(unixtimeFlag);
} }
log.info("Setting wallet key rotation time to {}", rotationTimeSecs); log.info("Setting wallet key rotation time to {}", rotationTimeSecs);
wallet.setKeyRotationEnabled(true);
wallet.setKeyRotationTime(rotationTimeSecs); wallet.setKeyRotationTime(rotationTimeSecs);
KeyParameter aesKey = null; KeyParameter aesKey = null;
if (wallet.isEncrypted()) { if (wallet.isEncrypted()) {
@@ -456,7 +457,7 @@ public class WalletTool {
if (aesKey == null) if (aesKey == null)
return; return;
} }
Futures.getUnchecked(wallet.maybeDoMaintenance(aesKey, true)); Futures.getUnchecked(wallet.doMaintenance(aesKey, true));
} }
private static void encrypt() { private static void encrypt() {

View File

@@ -117,14 +117,7 @@ public class Main extends Application {
// or progress widget to keep the user engaged whilst we initialise, but we don't. // or progress widget to keep the user engaged whilst we initialise, but we don't.
if (params == RegTestParams.get()) { if (params == RegTestParams.get()) {
bitcoin.connectToLocalHost(); // You should run a regtest mode bitcoind locally. bitcoin.connectToLocalHost(); // You should run a regtest mode bitcoind locally.
} else if (params == MainNetParams.get()) {
// Checkpoints are block headers that ship inside our app: for a new user, we pick the last header
// in the checkpoints file and then download the rest from the network. It makes things much faster.
// Checkpoint files are made using the BuildCheckpoints tool and usually we have to download the
// last months worth or more (takes a few seconds).
bitcoin.setCheckpoints(getClass().getResourceAsStream("checkpoints"));
} else if (params == TestNet3Params.get()) { } else if (params == TestNet3Params.get()) {
bitcoin.setCheckpoints(getClass().getResourceAsStream("org.bitcoin.test.checkpoints"));
// As an example! // As an example!
bitcoin.useTor(); bitcoin.useTor();
} }