HD Wallets: redo key rotation, it's no longer automatic and expects the wallet app to poll for maintenance transactions. Deterministic keys now inherit the creation time of their parent.

This commit is contained in:
Mike Hearn 2014-06-24 19:14:40 +02:00
parent 7b337680bf
commit dbd6004f1b
17 changed files with 330 additions and 173 deletions

View file

@ -1,7 +1,6 @@
- Switch to the tree format agreed on with slush/stick.
- Store the account key creation time for an HD hierarchy.
- Support seeds up to 512 bits in size. Test compatibility with greenaddress, at least for some keys.
- Support for key rotation
- Calculate lookahead keys on a background thread.
- Redo internals of DKC to support arbitrary tree structures.
- Add a REFUND key purpose and map to the receive tree (for now).

View file

@ -38,6 +38,8 @@ import com.google.common.base.Objects;
import com.google.common.base.Objects.ToStringHelper;
import com.google.common.collect.*;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
@ -420,6 +422,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
}
private void maybeUpgradeToHD() throws DeterministicUpgradeRequiresPassword {
maybeUpgradeToHD(null);
}
private void maybeUpgradeToHD(@Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword {
checkState(lock.isHeldByCurrentThread());
if (keychain.isDeterministicUpgradeRequired()) {
log.info("Upgrade to HD wallets is required, attempting to do so.");
@ -1158,15 +1164,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
}
}
receive(tx, block, blockType, relativityOffset);
return true;
} finally {
lock.unlock();
}
if (blockType == AbstractBlockChain.NewBlockType.BEST_CHAIN) {
// If some keys are considered to be bad, possibly move money assigned to them now.
// This has to run outside the wallet lock as it may trigger broadcasting of new transactions.
maybeRotateKeys();
}
return true;
}
/**
@ -1374,11 +1375,6 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} finally {
lock.unlock();
}
if (blockType == AbstractBlockChain.NewBlockType.BEST_CHAIN) {
// If some keys are considered to be bad, possibly move money assigned to them now.
// This has to run outside the wallet lock as it may trigger broadcasting of new transactions.
maybeRotateKeys();
}
}
private void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType,
@ -3918,7 +3914,13 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Managing wallet-triggered transaction broadcast and key rotation.
// Wallet maintenance transactions. These transactions may not be directly connected to a payment the user is
// making. They may be instead key rotation transactions for when old keys are suspected to be compromised,
// de/re-fragmentation transactions for when our output sizes are inappropriate or suboptimal, privacy transactions
// and so on. Because these transactions may require user intervention in some way (e.g. entering their password)
// the wallet application is expected to poll the Wallet class to get SendRequests. Ideally security systems like
// hardware wallets or risk analysis providers are programmed to auto-approve transactions that send from our own
// keys back to our own keys.
/**
* <p>Specifies that the given {@link TransactionBroadcaster}, typically a {@link PeerGroup}, should be used for
@ -3981,27 +3983,28 @@ 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
* 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. Once the time is set transactions will be created and broadcast
* immediately. New coins that come in after calling this method will be automatically respent immediately. The
* rotation time is persisted to the wallet. You can stop key rotation by calling this method again with zero
* as the argument, or by using {@link #setKeyRotationEnabled(boolean)}.</p>
* 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
* {@link #maybeDoMaintenance(org.spongycastle.crypto.params.KeyParameter, boolean)} will create and possibly
* send rotation transactions: but it won't be done automatically (because you might have to ask for the users
* 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>
*/
public void setKeyRotationTime(long unixTimeSeconds) {
checkArgument(unixTimeSeconds <= Utils.currentTimeSeconds());
vKeyRotationTimestamp = unixTimeSeconds;
if (unixTimeSeconds > 0) {
log.info("Key rotation time set: {}", unixTimeSeconds);
maybeRotateKeys();
}
saveNow();
}
/** Toggles key rotation on and off. Note that this state is not serialized. Activating it can trigger tx sends. */
/** Toggles key rotation on and off. Note that this state is not serialized. */
public void setKeyRotationEnabled(boolean enabled) {
vKeyRotationEnabled = enabled;
if (enabled)
maybeRotateKeys();
}
/** Returns whether the keys creation time is before the key rotation time, if one was set. */
@ -4010,48 +4013,90 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
return time != 0 && key.getCreationTimeSeconds() < time;
}
// Checks to see if any coins are controlled by rotating keys and if so, spends them.
private void maybeRotateKeys() {
/**
* A wallet app should call this from time to time if key rotation is enabled in order to let the wallet craft and
* send transactions needed to re-organise coins internally. A good time to call this would be after receiving coins
* for an unencrypted wallet, or after sending money for an encrypted wallet. If you have an encrypted wallet and
* just want to know if some maintenance needs doing, call this method with doSend set to false and look at the
* returned list of transactions.
*
* @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.
* @return A list of transactions that the wallet just made/will make for internal maintenance. Might be empty.
*/
public ListenableFuture<List<Transaction>> maybeDoMaintenance(@Nullable KeyParameter aesKey, boolean andSend) {
List<Transaction> txns;
lock.lock();
try {
txns = maybeRotateKeys(aesKey);
if (!andSend)
return Futures.immediateFuture(txns);
} finally {
lock.unlock();
}
checkState(!lock.isHeldByCurrentThread());
// TODO: Handle chain replays and encrypted wallets here.
if (!vKeyRotationEnabled) return;
ArrayList<ListenableFuture<Transaction>> futures = new ArrayList<ListenableFuture<Transaction>>(txns.size());
TransactionBroadcaster broadcaster = vTransactionBroadcaster;
for (Transaction tx : txns) {
try {
final ListenableFuture<Transaction> future = broadcaster.broadcastTransaction(tx);
futures.add(future);
Futures.addCallback(future, new FutureCallback<Transaction>() {
@Override
public void onSuccess(Transaction transaction) {
log.info("Successfully broadcast key rotation tx: {}", transaction);
}
@Override
public void onFailure(Throwable throwable) {
log.error("Failed to broadcast key rotation tx", throwable);
}
});
} catch (Exception e) {
log.error("Failed to broadcast rekey tx", e);
}
}
return Futures.allAsList(futures);
}
// Checks to see if any coins are controlled by rotating keys and if so, spends them.
private List<Transaction> maybeRotateKeys(@Nullable KeyParameter aesKey) {
checkState(lock.isHeldByCurrentThread());
List<Transaction> results = Lists.newLinkedList();
// TODO: Handle chain replays here.
if (!vKeyRotationEnabled) return results;
// Snapshot volatiles so this method has an atomic view.
long keyRotationTimestamp = vKeyRotationTimestamp;
if (keyRotationTimestamp == 0) return; // Nothing to do.
TransactionBroadcaster broadcaster = vTransactionBroadcaster;
if (keyRotationTimestamp == 0) return results; // Nothing to do.
// We might have to create a new HD hierarchy if the previous ones are now rotating.
boolean allChainsRotating = true;
for (DeterministicKeyChain chain : keychain.getDeterministicKeyChains()) {
if (chain.getEarliestKeyCreationTime() > vKeyRotationTimestamp) {
allChainsRotating = false;
break;
}
}
if (allChainsRotating) {
log.info("All HD chains are currently rotating, creating a new one");
keychain.createAndActivateNewHDChain();
}
// Because transactions are size limited, we might not be able to re-key the entire wallet in one go. So
// loop around here until we no longer produce transactions with the max number of inputs. That means we're
// fully done, at least for now (we may still get more transactions later and this method will be reinvoked).
Transaction tx;
do {
tx = rekeyOneBatch(keyRotationTimestamp, broadcaster);
tx = rekeyOneBatch(keyRotationTimestamp, aesKey, results);
if (tx != null) results.add(tx);
} while (tx != null && tx.getInputs().size() == KeyTimeCoinSelector.MAX_SIMULTANEOUS_INPUTS);
return results;
}
@Nullable
private Transaction rekeyOneBatch(long keyRotationTimestamp, final TransactionBroadcaster broadcaster) {
/*final Transaction rekeyTx;
private Transaction rekeyOneBatch(long timeSecs, @Nullable KeyParameter aesKey, List<Transaction> others) {
lock.lock();
try {
// Firstly, see if we have any keys that are beyond the rotation time, and any before.
ECKey safeKey = null;
boolean haveRotatingKeys = false;
for (ECKey key : basicKeyChain) {
final long t = key.getCreationTimeSeconds();
if (t < keyRotationTimestamp) {
haveRotatingKeys = true;
} else {
safeKey = key;
}
}
if (!haveRotatingKeys)
return null;
if (safeKey == null) {
log.warn("Key rotation requested but no keys newer than the timestamp are available.");
return null;
}
// Build the transaction using some custom logic for our special needs. Last parameter to
// KeyTimeCoinSelector is whether to ignore pending transactions or not.
//
@ -4060,59 +4105,34 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// have already got stuck double spends in their wallet due to the Bloom-filtering block reordering
// bug that was fixed in 0.10, thus, making a re-key transaction depend on those would cause it to
// never confirm at all.
CoinSelector selector = new KeyTimeCoinSelector(this, keyRotationTimestamp, true);
CoinSelector keyTimeSelector = new KeyTimeCoinSelector(this, timeSecs, true);
FilteringCoinSelector selector = new FilteringCoinSelector(keyTimeSelector);
for (Transaction other : others)
selector.excludeOutputsSpentBy(other);
// TODO: Make this use the standard SendRequest.
CoinSelection toMove = selector.select(Coin.ZERO, calculateAllSpendCandidates(true));
if (toMove.valueGathered.equals(Coin.ZERO)) return null; // Nothing to do.
rekeyTx = new Transaction(params);
maybeUpgradeToHD(aesKey);
Transaction rekeyTx = new Transaction(params);
for (TransactionOutput output : toMove.gathered) {
rekeyTx.addInput(output);
}
rekeyTx.addOutput(toMove.valueGathered, safeKey);
rekeyTx.addOutput(toMove.valueGathered, freshReceiveAddress());
if (!adjustOutputDownwardsForFee(rekeyTx, toMove, Coin.ZERO, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE)) {
log.error("Failed to adjust rekey tx for fees.");
return null;
}
rekeyTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
rekeyTx.setPurpose(Transaction.Purpose.KEY_ROTATION);
rekeyTx.signInputs(Transaction.SigHash.ALL, this);
rekeyTx.signInputs(Transaction.SigHash.ALL, this, aesKey);
// KeyTimeCoinSelector should never select enough inputs to push us oversize.
checkState(rekeyTx.bitcoinSerialize().length < Transaction.MAX_STANDARD_TX_SIZE);
commitTx(rekeyTx);
return rekeyTx;
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen.
} finally {
lock.unlock();
}
if (broadcaster == null)
return rekeyTx;
log.info("Attempting to send key rotation tx: {}", rekeyTx);
// We must broadcast the tx in a separate thread to avoid inverting any locks. Otherwise we may be running
// with the blockchain lock held (whilst receiving a block) and thus re-entering the peerGroup would invert
// blockchain <-> peergroup.
new Thread() {
@Override
public void run() {
// Handle the future results just for logging.
try {
Futures.addCallback(broadcaster.broadcastTransaction(rekeyTx), new FutureCallback<Transaction>() {
@Override
public void onSuccess(Transaction transaction) {
log.info("Successfully broadcast key rotation tx: {}", transaction);
}
@Override
public void onFailure(Throwable throwable) {
log.error("Failed to broadcast key rotation tx", throwable);
}
});
} catch (Exception e) {
log.error("Failed to broadcast rekey tx, will try again later", e);
}
}
}.start();
return rekeyTx;*/
throw new RuntimeException("FIXME");
}
/**

View file

@ -185,7 +185,10 @@ public class DeterministicKey extends ECKey {
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);
DeterministicKey key = new DeterministicKey(childNumberPath, chainCode, keyCrypter, pub, encryptedPrivateKey, newParent);
if (newParent == null)
key.setCreationTimeSeconds(getCreationTimeSeconds());
return key;
}
/**
@ -238,6 +241,8 @@ public class DeterministicKey extends ECKey {
DeterministicKey key = new DeterministicKey(childNumberPath, chainCode, privKey, parent);
if (!Arrays.equals(key.getPubKey(), getPubKey()))
throw new KeyCrypterException("Provided AES key is wrong");
if (parent == null)
key.setCreationTimeSeconds(getCreationTimeSeconds());
return key;
}
@ -427,7 +432,19 @@ public class DeterministicKey extends ECKey {
@Override
public String toString() {
return String.format("pub:%s chaincode:%s path:%s", HEX.encode(getPubKey()),
HEX.encode(getChainCode()), getPathAsString());
return String.format("pub:%s chaincode:%s path:%s time:%d", HEX.encode(getPubKey()),
HEX.encode(getChainCode()), getPathAsString(), getCreationTimeSeconds());
}
/**
* The creation time of a deterministic key is equal to that of its parent, unless this key is the root of a tree
* in which case the time is stored alongside the key as per normal, see {@link com.google.bitcoin.core.ECKey#getCreationTimeSeconds()}.
*/
@Override
public long getCreationTimeSeconds() {
if (parent != null)
return parent.getCreationTimeSeconds();
else
return super.getCreationTimeSeconds();
}
}

View file

@ -17,6 +17,7 @@
package com.google.bitcoin.crypto;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Utils;
import com.google.common.collect.ImmutableList;
import org.spongycastle.crypto.macs.HMac;
import org.spongycastle.math.ec.ECPoint;
@ -60,6 +61,8 @@ public final class HDKeyDerivation {
DeterministicKey masterPrivKey = createMasterPrivKeyFromBytes(il, ir);
Arrays.fill(il, (byte)0);
Arrays.fill(ir, (byte)0);
// Child deterministic keys will chain up to their parents to find the keys.
masterPrivKey.setCreationTimeSeconds(Utils.currentTimeSeconds());
return masterPrivKey;
}

View file

@ -102,6 +102,12 @@ public class MockTransactionBroadcaster implements TransactionBroadcaster {
return waitForTxFuture().tx;
}
public Transaction waitForTransactionAndSucceed() {
TxFuturePair pair = waitForTxFuture();
pair.succeed();
return pair.tx;
}
public TxFuturePair waitForTxFuture() {
try {
return broadcasts.take();

View file

@ -23,6 +23,7 @@ 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.common.collect.Lists;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos;
import org.spongycastle.crypto.params.KeyParameter;
@ -567,4 +568,21 @@ public class BasicKeyChain implements EncryptableKeyChain {
lock.unlock();
}
}
/** Returns a list of all ECKeys created after the given UNIX time. */
public List<ECKey> findKeysBefore(long timeSecs) {
lock.lock();
try {
List<ECKey> results = Lists.newLinkedList();
for (ECKey key : hashToKeys.values()) {
final long keyTime = key.getCreationTimeSeconds();
if (keyTime < timeSecs) {
results.add(key);
}
}
return results;
} finally {
lock.unlock();
}
}
}

View file

@ -124,7 +124,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
* object.
*/
public DeterministicKeyChain(SecureRandom random) {
this(getRandomSeed(random), Utils.currentTimeMillis() / 1000);
this(getRandomSeed(random), Utils.currentTimeSeconds());
}
private static byte[] getRandomSeed(SecureRandom random) {
@ -209,6 +209,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
basicKeyChain = new BasicKeyChain(crypter);
if (!seed.isEncrypted()) {
rootKey = HDKeyDerivation.createMasterPrivateKey(checkNotNull(seed.getSecretBytes()));
rootKey.setCreationTimeSeconds(seed.getCreationTimeSeconds());
initializeHierarchyUnencrypted(rootKey);
} else {
// We can't initialize ourselves with just an encrypted seed, so we expected deserialization code to do the
@ -527,10 +528,14 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
detKey.setIssuedSubkeys(issuedInternalKeys);
detKey.setLookaheadSize(lookaheadSize);
}
// flag the very first key of following keychain
// Flag the very first key of following keychain.
if (entries.isEmpty() && isFollowing()) {
detKey.setIsFollowing(true);
}
if (key.getParent() != null) {
// HD keys inherit the timestamp of their parent if they have one, so no need to serialize it.
proto.clearCreationTimestamp();
}
entries.add(proto.build());
}
return entries;
@ -638,6 +643,8 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
detkey = new DeterministicKey(immutablePath, chainCode, pubkey, null, parent);
}
}
if (key.hasCreationTimestamp())
detkey.setCreationTimeSeconds(key.getCreationTimestamp() / 1000);
if (log.isDebugEnabled())
log.debug("Deserializing: DETERMINISTIC_KEY: {}", detkey);
if (!isWatchingAccountKey) {

View file

@ -0,0 +1,52 @@
/**
* Copyright 2014 the bitcoinj authors.
*
* 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 java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
/**
* A filtering coin selector delegates to another coin selector, but won't select outputs spent by the given transactions.
*/
public class FilteringCoinSelector implements CoinSelector {
protected CoinSelector delegate;
protected HashSet<TransactionOutPoint> spent = new HashSet<TransactionOutPoint>();
public FilteringCoinSelector(CoinSelector delegate) {
this.delegate = delegate;
}
public void excludeOutputsSpentBy(Transaction tx) {
for (TransactionInput input : tx.getInputs()) {
spent.add(input.getOutpoint());
}
}
@Override
public CoinSelection select(Coin target, LinkedList<TransactionOutput> candidates) {
Iterator<TransactionOutput> iter = candidates.iterator();
while (iter.hasNext()) {
TransactionOutput output = iter.next();
if (spent.contains(output.getOutPointFor())) iter.remove();
}
return delegate.select(target, candidates);
}
}

View file

@ -184,9 +184,11 @@ public class KeyChainGroup {
}
}
private void createAndActivateNewHDChain() {
/** Adds a new HD chain to the chains list, and make it the default chain (from which keys are issued). */
public void createAndActivateNewHDChain() {
// We can't do auto upgrade here because we don't know the rotation time, if any.
final DeterministicKeyChain chain = new DeterministicKeyChain(new SecureRandom());
log.info("Creating and activating a new HD chain: {}", chain);
for (ListenerRegistration<KeyChainEventListener> registration : basic.getListeners())
chain.addEventListener(registration.listener, registration.executor);
if (lookaheadSize >= 0)
@ -796,19 +798,21 @@ public class KeyChainGroup {
for (ECKey key : basic.getKeys())
formatKeyWithAddress(includePrivateKeys, key, builder);
}
List<String> chainStrs = Lists.newLinkedList();
for (DeterministicKeyChain chain : chains) {
final StringBuilder builder2 = new StringBuilder();
DeterministicSeed seed = chain.getSeed();
if (seed != null) {
if (seed.isEncrypted()) {
builder.append(String.format("Seed is encrypted%n"));
builder2.append(String.format("Seed is encrypted%n"));
} else if (includePrivateKeys) {
final List<String> words = seed.toMnemonicCode();
builder.append(
builder2.append(
String.format("Seed as words: %s%nSeed as hex: %s%n", Joiner.on(' ').join(words),
seed.toHexString())
);
}
builder.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(), new Date(seed.getCreationTimeSeconds() * 1000)));
builder2.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(), new Date(seed.getCreationTimeSeconds() * 1000)));
}
final DeterministicKey watchingKey = chain.getWatchingKey();
// Don't show if it's been imported from a watching wallet already, because it'd result in a weird/
@ -816,21 +820,25 @@ public class KeyChainGroup {
// due to the parent fingerprint being missing/not stored. In future we could store the parent fingerprint
// optionally as well to fix this, but it seems unimportant for now.
if (watchingKey.getParent() != null) {
builder.append(String.format("Key to watch: %s%n%n", watchingKey.serializePubB58()));
builder2.append(String.format("Key to watch: %s%n", watchingKey.serializePubB58()));
}
if (isMarried(chain)) {
Collection<DeterministicKeyChain> followingChains = followingKeychains.get(chain.getWatchingKey());
for (DeterministicKeyChain followingChain : followingChains) {
builder.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58()));
builder2.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58()));
}
builder.append("\n");
builder2.append(String.format("%n"));
for (Script script : marriedKeysScripts.values())
formatScript(ScriptBuilder.createP2SHOutputScript(script), builder);
formatScript(ScriptBuilder.createP2SHOutputScript(script), builder2);
} else {
for (ECKey key : chain.getKeys())
formatKeyWithAddress(includePrivateKeys, key, builder);
formatKeyWithAddress(includePrivateKeys, key, builder2);
}
for (ECKey key : chain.getKeys())
formatKeyWithAddress(includePrivateKeys, key, builder2);
chainStrs.add(builder2.toString());
}
builder.append(Joiner.on(String.format("%n")).join(chainStrs));
return builder.toString();
}
@ -852,4 +860,9 @@ public class KeyChainGroup {
builder.append(includePrivateKeys ? key.toStringWithPrivate() : key.toString());
builder.append("\n");
}
/** Returns a copy of the current list of chains. */
public List<DeterministicKeyChain> getDeterministicKeyChains() {
return new ArrayList<DeterministicKeyChain>(chains);
}
}

View file

@ -32,12 +32,10 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -2228,47 +2226,61 @@ public class WalletTest extends TestWithWallet {
assertEquals(outputValue, request.tx.getOutput(0).getValue());
}
@Ignore("Key rotation temporarily disabled during HD wallet migration")
@Test
public void keyRotation() throws Exception {
public void keyRotationRandom() throws Exception {
Utils.setMockClock();
// Start with an empty wallet (no HD chain).
wallet = new Wallet(params);
// Watch out for wallet-initiated broadcasts.
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
wallet.setKeyRotationEnabled(true);
// Send three cents to two different keys, then add a key and mark the initial keys as compromised.
ECKey key1 = wallet.freshReceiveKey();
ECKey key2 = wallet.freshReceiveKey();
// Send three cents to two different random keys, then add a key and mark the initial keys as compromised.
ECKey key1 = new ECKey();
key1.setCreationTimeSeconds(Utils.currentTimeSeconds() - (86400 * 2));
ECKey key2 = new ECKey();
key2.setCreationTimeSeconds(Utils.currentTimeSeconds() - 86400);
wallet.importKey(key1);
wallet.importKey(key2);
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);
Utils.rollMockClock(86400);
Date compromiseTime = Utils.now();
assertEquals(0, broadcaster.size());
assertFalse(wallet.isKeyRotating(key1));
// Rotate the wallet.
ECKey key3 = wallet.freshReceiveKey();
// We see a broadcast triggered by setting the rotation time.
// We got compromised! We have an old style random-only wallet. So let's upgrade to HD: for that we need a fresh
// 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);
ECKey key3 = new ECKey();
wallet.importKey(key3);
wallet.setKeyRotationTime(compromiseTime);
assertTrue(wallet.isKeyRotating(key1));
Transaction tx = broadcaster.waitForTransaction();
wallet.maybeDoMaintenance(null, true);
Transaction tx = broadcaster.waitForTransactionAndSucceed();
final Coin THREE_CENTS = CENT.add(CENT).add(CENT);
assertEquals(THREE_CENTS, tx.getValueSentFromMe(wallet));
assertEquals(THREE_CENTS.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), tx.getValueSentToMe(wallet));
// TX is a raw pay to pubkey.
assertArrayEquals(key3.getPubKey(), tx.getOutput(0).getScriptPubKey().getPubKey());
// TX sends to one of our addresses (for now we ignore married wallets).
final Address toAddress = tx.getOutput(0).getScriptPubKey().getToAddress(params);
final ECKey rotatingToKey = wallet.findKeyFromPubHash(toAddress.getHash160());
assertNotNull(rotatingToKey);
assertFalse(wallet.isKeyRotating(rotatingToKey));
assertEquals(3, tx.getInputs().size());
// It confirms.
sendMoneyToWallet(tx, AbstractBlockChain.NewBlockType.BEST_CHAIN);
// Now receive some more money to key3 (secure) via a new block and check that nothing happens.
sendMoneyToWallet(wallet, CENT, key3.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
// 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);
assertTrue(wallet.maybeDoMaintenance(null, true).get().isEmpty());
assertEquals(0, broadcaster.size());
// Receive money via a new block on key1 and ensure it's immediately moved.
// 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);
tx = broadcaster.waitForTransaction();
assertArrayEquals(key3.getPubKey(), tx.getOutput(0).getScriptPubKey().getPubKey());
wallet.maybeDoMaintenance(null, true);
tx = broadcaster.waitForTransactionAndSucceed();
assertNotNull(wallet.findKeyFromPubHash(tx.getOutput(0).getScriptPubKey().getPubKeyHash()));
log.info("Unexpected thing: {}", tx);
assertEquals(1, tx.getInputs().size());
assertEquals(1, tx.getOutputs().size());
assertEquals(CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), tx.getOutput(0).getValue());
@ -2282,6 +2294,7 @@ public class WalletTest extends TestWithWallet {
wallet = new WalletProtobufSerializer().readWallet(params, null, protos);
tx = wallet.getTransaction(tx.getHash());
checkNotNull(tx);
assertEquals(Transaction.Purpose.KEY_ROTATION, tx.getPurpose());
// Have to divide here to avoid mismatch due to second-level precision in serialisation.
assertEquals(compromiseTime.getTime() / 1000, wallet.getKeyRotationTime().getTime() / 1000);
@ -2293,6 +2306,27 @@ public class WalletTest extends TestWithWallet {
assertArrayEquals(address.getHash160(), tx.getOutput(0).getScriptPubKey().getPubKeyHash());
}
@Test
public void keyRotationHD() throws Exception {
// Test that if we rotate an HD chain, a new one is created and all arrivals on the old keys are moved.
Utils.setMockClock();
ECKey key1 = wallet.freshReceiveKey();
ECKey key2 = wallet.freshReceiveKey();
sendMoneyToWallet(wallet, CENT, key1.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
sendMoneyToWallet(wallet, CENT, key2.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
DeterministicKey watchKey1 = wallet.getWatchingKey();
// A day later, we get compromised.
Utils.rollMockClock(86400);
wallet.setKeyRotationTime(Utils.currentTimeSeconds());
wallet.setKeyRotationEnabled(true);
List<Transaction> txns = wallet.maybeDoMaintenance(null, false).get();
assertEquals(1, txns.size());
DeterministicKey watchKey2 = wallet.getWatchingKey();
assertNotEquals(watchKey1, watchKey2);
}
//@Test //- this test is slow, disable for now.
public void fragmentedReKeying() throws Exception {
// Send lots of small coins and check the fee is correct.
@ -2311,8 +2345,9 @@ public class WalletTest extends TestWithWallet {
Utils.rollMockClock(86400);
wallet.freshReceiveKey();
wallet.setKeyRotationTime(compromise);
wallet.maybeDoMaintenance(null, true);
Transaction tx = broadcaster.waitForTransaction();
Transaction tx = broadcaster.waitForTransactionAndSucceed();
final Coin valueSentToMe = tx.getValueSentToMe(wallet);
Coin fee = tx.getValueSentFromMe(wallet).subtract(valueSentToMe);
assertEquals(Coin.valueOf(900000), fee);

View file

@ -168,6 +168,11 @@ public class ChildKeyDerivationTest {
public void serializeToTextAndBytes() {
DeterministicKey key1 = HDKeyDerivation.createMasterPrivateKey("satoshi lives!".getBytes());
DeterministicKey key2 = HDKeyDerivation.deriveChildKey(key1, ChildNumber.ZERO_HARDENED);
// Creation time can't survive the xpub serialization format unfortunately.
key1.setCreationTimeSeconds(0);
key2.setCreationTimeSeconds(0);
{
final String pub58 = key1.serializePubB58();
final String priv58 = key1.serializePrivB58();

View file

@ -255,17 +255,21 @@ public class BasicKeyChainTest {
}
@Test
public void oldestKeyAfter() throws Exception {
public void keysBeforeAndAfter() throws Exception {
Utils.setMockClock();
long now = Utils.currentTimeSeconds();
final ECKey key1 = new ECKey();
Utils.rollMockClock(86400);
final ECKey key2 = new ECKey();
final ArrayList<ECKey> keys = Lists.newArrayList(key1, key2);
final List<ECKey> keys = Lists.newArrayList(key1, key2);
assertEquals(2, chain.importKeys(keys));
assertNull(chain.findOldestKeyAfter(now + 86400 * 2));
assertEquals(key1, chain.findOldestKeyAfter(now - 1));
assertEquals(key2, chain.findOldestKeyAfter(now + 86400 - 1));
assertEquals(2, chain.findKeysBefore(now + 86400 * 2).size());
assertEquals(1, chain.findKeysBefore(now + 1).size());
assertEquals(0, chain.findKeysBefore(now - 1).size());
}
}

View file

@ -229,6 +229,7 @@ public class DeterministicKeyChainTest {
final String pub58 = watchingKey.serializePubB58();
assertEquals("xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi", pub58);
watchingKey = DeterministicKey.deserializeB58(null, pub58);
watchingKey.setCreationTimeSeconds(100000);
chain = DeterministicKeyChain.watch(watchingKey);
assertEquals(DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS, chain.getEarliestKeyCreationTime());
chain.setLookaheadSize(10);

View file

@ -5,7 +5,7 @@ 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
creation_timestamp: 1389353062000
deterministic_key {
chain_code: ",\263\335\024\031\221c;~\325\326\272\367*\r+\032H\270\026\234\226\357\222i_VxP\200x\252"
}
@ -13,7 +13,6 @@ deterministic_key {
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
@ -22,7 +21,6 @@ deterministic_key {
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
@ -34,7 +32,6 @@ deterministic_key {
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
@ -45,7 +42,6 @@ deterministic_key {
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
@ -55,7 +51,6 @@ deterministic_key {
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
@ -65,7 +60,6 @@ deterministic_key {
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
@ -75,7 +69,6 @@ deterministic_key {
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
@ -85,7 +78,6 @@ deterministic_key {
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
@ -95,7 +87,6 @@ deterministic_key {
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
@ -105,7 +96,6 @@ deterministic_key {
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
@ -115,7 +105,6 @@ deterministic_key {
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
@ -125,7 +114,6 @@ deterministic_key {
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
@ -135,7 +123,6 @@ deterministic_key {
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
@ -145,7 +132,6 @@ deterministic_key {
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
@ -155,7 +141,6 @@ deterministic_key {
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
@ -165,7 +150,6 @@ deterministic_key {
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
@ -175,7 +159,6 @@ deterministic_key {
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
@ -185,7 +168,6 @@ deterministic_key {
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
@ -195,7 +177,6 @@ deterministic_key {
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
@ -205,7 +186,6 @@ deterministic_key {
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
@ -215,7 +195,6 @@ deterministic_key {
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
@ -225,7 +204,6 @@ deterministic_key {
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
@ -235,7 +213,6 @@ deterministic_key {
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
@ -245,7 +222,6 @@ deterministic_key {
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
@ -255,7 +231,6 @@ deterministic_key {
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
@ -265,7 +240,6 @@ deterministic_key {
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

View file

@ -1,6 +1,6 @@
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
creation_timestamp: 100000000
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
@ -8,7 +8,6 @@ deterministic_key {
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
@ -19,7 +18,6 @@ deterministic_key {
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
@ -30,7 +28,6 @@ deterministic_key {
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
@ -40,7 +37,6 @@ deterministic_key {
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
@ -50,7 +46,6 @@ deterministic_key {
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
@ -60,7 +55,6 @@ deterministic_key {
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
@ -70,7 +64,6 @@ deterministic_key {
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
@ -80,7 +73,6 @@ deterministic_key {
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
@ -90,7 +82,6 @@ deterministic_key {
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
@ -100,7 +91,6 @@ deterministic_key {
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
@ -110,7 +100,6 @@ deterministic_key {
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
@ -120,7 +109,6 @@ deterministic_key {
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
@ -130,7 +118,6 @@ deterministic_key {
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
@ -140,7 +127,6 @@ deterministic_key {
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
@ -150,7 +136,6 @@ deterministic_key {
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
@ -160,7 +145,6 @@ deterministic_key {
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
@ -170,7 +154,6 @@ deterministic_key {
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
@ -180,7 +163,6 @@ deterministic_key {
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
@ -190,7 +172,6 @@ deterministic_key {
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
@ -200,7 +181,6 @@ deterministic_key {
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
@ -210,7 +190,6 @@ deterministic_key {
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
@ -220,7 +199,6 @@ deterministic_key {
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
@ -230,7 +208,6 @@ deterministic_key {
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
@ -240,7 +217,6 @@ deterministic_key {
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
@ -250,7 +226,6 @@ deterministic_key {
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

View file

@ -40,6 +40,7 @@ import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Resources;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.subgraph.orchid.TorClient;
import joptsimple.OptionParser;
@ -163,7 +164,8 @@ public class WalletTool {
SEND,
ENCRYPT,
DECRYPT,
MARRY
MARRY,
ROTATE,
}
public enum WaitForEnum {
@ -356,6 +358,7 @@ public class WalletTool {
case ENCRYPT: encrypt(); break;
case DECRYPT: decrypt(); break;
case MARRY: marry(); break;
case ROTATE: rotate(); break;
}
if (!wallet.isConsistent()) {
@ -397,6 +400,27 @@ public class WalletTool {
wallet.addFollowingAccountKeys(keys.build());
}
private static void rotate() throws BlockStoreException {
setup();
peers.startAsync();
peers.awaitRunning();
// Set a key rotation time and possibly broadcast the resulting maintenance transactions.
long rotationTimeSecs = Utils.currentTimeSeconds();
if (options.has(dateFlag)) {
rotationTimeSecs = options.valueOf(dateFlag).getTime() / 1000;
}
log.info("Setting wallet key rotation time to {}", rotationTimeSecs);
wallet.setKeyRotationEnabled(true);
wallet.setKeyRotationTime(rotationTimeSecs);
KeyParameter aesKey = null;
if (wallet.isEncrypted()) {
aesKey = passwordToKey(true);
if (aesKey == null)
return;
}
Futures.getUnchecked(wallet.maybeDoMaintenance(aesKey, true));
}
private static void encrypt() {
if (password == null) {
System.err.println("You must provide a --password");

View file

@ -47,6 +47,10 @@ Usage: wallet-tool --flags action-name
--no-pki disables pki verification for payment requests.
encrypt Requires --password and uses it to encrypt the wallet in place.
decrypt Requires --password and uses it to decrypt the wallet in place.
rotate Takes --date and sets that as the key rotation time. Any coins controlled by keys or HD chains
created before this date will be re-spent to a key (from an HD tree) that was created after it.
If --date is missing, the current time is assumed. If the time covers all keys, a new HD tree
will be created from a new random seed.
>>> GENERAL OPTIONS
--debuglog Enables logging from the core library.