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

KeyChainGroup, Wallet: Implement the upgrade path Basic --> P2PKH --> P2WPKH.

This commit is contained in:
Andreas Schildbach 2019-02-12 18:52:27 +01:00
parent 3c73f5e8a1
commit 05efa7e69e
6 changed files with 373 additions and 107 deletions

View File

@ -50,7 +50,10 @@ import static com.google.common.base.Preconditions.*;
* <p>There can be active keychains for each output script type. However this class almost entirely only works on
* the default active keychain (see {@link #getActiveKeyChain()}). The other active keychains
* (see {@link #getActiveKeyChain(ScriptType, long)}) are meant as fallback for if a sender doesn't understand a
* certain new script type (e.g. P2WPKH which comes with the new Bech32 address format).</p>
* certain new script type (e.g. P2WPKH which comes with the new Bech32 address format). Active keychains
* share the same seed, so that upgrading the wallet
* (see {@link #upgradeToDeterministic(ScriptType, KeyChainGroupStructure, long, KeyParameter)}) to understand
* a new script type doesn't require a fresh backup.</p>
*
* <p>If a key rotation time is set, it may be necessary to add a new DeterministicKeyChain with a fresh seed
* and also preserve the old one, so funds can be swept from the rotating keys. In this case, there may be
@ -424,6 +427,15 @@ public class KeyChainGroup implements KeyBag {
return chains.get(chains.size() - 1);
}
/**
* Merge all active chains from the given keychain group into this keychain group.
*/
public final void mergeActiveKeyChains(KeyChainGroup from, long keyRotationTimeSecs) {
checkArgument(isEncrypted() == from.isEncrypted(), "encrypted and non-encrypted keychains cannot be mixed");
for (DeterministicKeyChain chain : from.getActiveKeyChains(keyRotationTimeSecs))
addAndActivateHDChain(chain);
}
/**
* Gets the current lookahead size being used for ALL deterministic key chains. See
* {@link DeterministicKeyChain#setLookaheadSize(int)}
@ -835,9 +847,18 @@ public class KeyChainGroup implements KeyBag {
}
/**
* If the key chain contains only random keys and no deterministic key chains, this method will create a chain
* based on the oldest non-rotating private key (i.e. the seed is derived from the old wallet).
* <p>This method will upgrade the wallet along the following path: {@code Basic --> P2PKH --> P2WPKH}</p>
* <p>It won't skip any steps in that upgrade path because the user might be restoring from a backup and
* still expects money on the P2PKH chain.</p>
* <p>It will extract and reuse the seed from the current wallet, so that a fresh backup isn't required
* after upgrading. If coming from a basic chain containing only random keys this means it will pick the
* oldest non-rotating private key as a seed.</p>
* <p>Note that for upgrading an encrypted wallet, the decryption key is needed. In future, we could skip
* that requirement for a {@code P2PKH --> P2WPKH} upgrade and just clone the encryped seed, but currently
* the key is needed even for that.</p>
*
* @param preferredScriptType desired script type for the active keychain
* @param structure keychain group structure to derive an account path from
* @param keyRotationTimeSecs If non-zero, UNIX time for which keys created before this are assumed to be
* compromised or weak, those keys will not be used for deterministic upgrade.
* @param aesKey If non-null, the encryption key the keychain is encrypted under. If the keychain is encrypted
@ -849,66 +870,95 @@ public class KeyChainGroup implements KeyBag {
* @throws java.lang.IllegalArgumentException if the rotation time specified excludes all keys.
* @throws DeterministicUpgradeRequiresPassword if the key chain group is encrypted
* and you should provide the users encryption key.
* @return the DeterministicKeyChain that was created by the upgrade.
*/
public DeterministicKeyChain upgradeToDeterministic(long keyRotationTimeSecs, @Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword, AllRandomKeysRotating {
public void upgradeToDeterministic(Script.ScriptType preferredScriptType, KeyChainGroupStructure structure,
long keyRotationTimeSecs, @Nullable KeyParameter aesKey)
throws DeterministicUpgradeRequiresPassword, AllRandomKeysRotating {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
checkState(basic.numKeys() > 0);
checkNotNull(structure);
checkArgument(keyRotationTimeSecs >= 0);
// Subtract one because the key rotation time might have been set to the creation time of the first known good
// 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 (!isDeterministicUpgradeRequired(preferredScriptType, keyRotationTimeSecs))
return; // Nothing to do.
if (keyToUse.isEncrypted()) {
if (aesKey == null) {
// We can't auto upgrade because we don't know the users password at this point. We throw an
// exception so the calling code knows to abort the load and ask the user for their password, they can
// then try loading the wallet again passing in the AES key.
//
// There are a few different approaches we could have used here, but they all suck. The most obvious
// is to try and be as lazy as possible, running in the old random-wallet mode until the user enters
// their password for some other reason and doing the upgrade then. But this could result in strange
// and unexpected UI flows for the user, as well as complicating the job of wallet developers who then
// have to support both "old" and "new" UI modes simultaneously, switching them on the fly. Given that
// this is a one-off transition, it seems more reasonable to just ask the user for their password
// on startup, and then the wallet app can have all the widgets for accessing seed words etc active
// all the time.
throw new DeterministicUpgradeRequiresPassword();
// Basic --> P2PKH upgrade
if (basic.numKeys() > 0 && getActiveKeyChain(Script.ScriptType.P2PKH, keyRotationTimeSecs) == null) {
// Subtract one because the key rotation time might have been set to the creation time of the first known good
// key, in which case, that's the one we want to find.
ECKey keyToUse = basic.findOldestKeyAfter(keyRotationTimeSecs - 1);
if (keyToUse == null)
throw new AllRandomKeysRotating();
boolean keyWasEncrypted = keyToUse.isEncrypted();
if (keyWasEncrypted) {
if (aesKey == null) {
// We can't auto upgrade because we don't know the users password at this point. We throw an
// exception so the calling code knows to abort the load and ask the user for their password, they can
// then try loading the wallet again passing in the AES key.
//
// There are a few different approaches we could have used here, but they all suck. The most obvious
// is to try and be as lazy as possible, running in the old random-wallet mode until the user enters
// their password for some other reason and doing the upgrade then. But this could result in strange
// and unexpected UI flows for the user, as well as complicating the job of wallet developers who then
// have to support both "old" and "new" UI modes simultaneously, switching them on the fly. Given that
// this is a one-off transition, it seems more reasonable to just ask the user for their password
// on startup, and then the wallet app can have all the widgets for accessing seed words etc active
// all the time.
throw new DeterministicUpgradeRequiresPassword();
}
keyToUse = keyToUse.decrypt(aesKey);
} else if (aesKey != null) {
throw new IllegalStateException("AES Key was provided but wallet is not encrypted.");
}
keyToUse = keyToUse.decrypt(aesKey);
} else if (aesKey != null) {
throw new IllegalStateException("AES Key was provided but wallet is not encrypted.");
log.info(
"Upgrading from basic keychain to P2PKH deterministic keychain. Using oldest non-rotating private key (address: {})",
LegacyAddress.fromKey(params, keyToUse));
byte[] entropy = checkNotNull(keyToUse.getSecretBytes());
// Private keys should be at least 128 bits long.
checkState(entropy.length >= DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);
// We reduce the entropy here to 128 bits because people like to write their seeds down on paper, and 128
// bits should be sufficient forever unless the laws of the universe change or ECC is broken; in either case
// we all have bigger problems.
entropy = Arrays.copyOfRange(entropy, 0, DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8); // final argument is exclusive range.
checkState(entropy.length == DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);
DeterministicKeyChain chain = DeterministicKeyChain.builder()
.entropy(entropy, keyToUse.getCreationTimeSeconds())
.outputScriptType(Script.ScriptType.P2PKH)
.accountPath(structure.accountPathFor(Script.ScriptType.P2PKH)).build();
if (keyWasEncrypted)
chain = chain.toEncrypted(checkNotNull(keyCrypter), aesKey);
addAndActivateHDChain(chain);
}
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.");
// P2PKH --> P2WPKH upgrade
if (preferredScriptType == Script.ScriptType.P2WPKH
&& getActiveKeyChain(Script.ScriptType.P2WPKH, keyRotationTimeSecs) == null) {
DeterministicSeed seed = getActiveKeyChain(Script.ScriptType.P2PKH, keyRotationTimeSecs).getSeed();
boolean seedWasEncrypted = seed.isEncrypted();
if (seedWasEncrypted) {
if (aesKey == null)
throw new DeterministicUpgradeRequiresPassword();
seed = seed.decrypt(keyCrypter, "", aesKey);
}
log.info("Upgrading from P2PKH to P2WPKH deterministic keychain. Using seed: {}", seed);
DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed)
.outputScriptType(Script.ScriptType.P2WPKH)
.accountPath(structure.accountPathFor(Script.ScriptType.P2WPKH)).build();
if (seedWasEncrypted)
chain = chain.toEncrypted(checkNotNull(keyCrypter), aesKey);
addAndActivateHDChain(chain);
}
log.info("Instantiating new HD chain using oldest non-rotating private key (address: {})", LegacyAddress.fromKey(params, keyToUse));
byte[] entropy = checkNotNull(keyToUse.getSecretBytes());
// Private keys should be at least 128 bits long.
checkState(entropy.length >= DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);
// We reduce the entropy here to 128 bits because people like to write their seeds down on paper, and 128
// bits should be sufficient forever unless the laws of the universe change or ECC is broken; in either case
// we all have bigger problems.
entropy = Arrays.copyOfRange(entropy, 0, DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8); // final argument is exclusive range.
checkState(entropy.length == DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);
String passphrase = ""; // FIXME allow non-empty passphrase
DeterministicKeyChain chain = DeterministicKeyChain.builder()
.entropy(entropy, keyToUse.getCreationTimeSeconds()).passphrase(passphrase).build();
if (aesKey != null) {
chain = chain.toEncrypted(checkNotNull(basic.getKeyCrypter()), aesKey);
}
chains.add(chain);
return chain;
}
/** Returns true if the group contains random keys but no HD chains. */
public boolean isDeterministicUpgradeRequired() {
return basic.numKeys() > 0 && chains != null && chains.isEmpty();
/**
* Returns true if a call to {@link #upgradeToDeterministic(ScriptType, KeyChainGroupStructure, long, KeyParameter)} is required
* in order to have an active deterministic keychain of the desired script type.
*/
public boolean isDeterministicUpgradeRequired(Script.ScriptType preferredScriptType, long keyRotationTimeSecs) {
if (!isSupportsDeterministicChains())
return false;
if (getActiveKeyChain(preferredScriptType, keyRotationTimeSecs) == null)
return true;
return false;
}
private static EnumMap<KeyChain.KeyPurpose, DeterministicKey> createCurrentKeysMap(List<DeterministicKeyChain> chains) {

View File

@ -741,10 +741,24 @@ public class Wallet extends BaseTaggableObject
* to do this will result in an exception being thrown. For non-encrypted wallets, the upgrade will be done for
* 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(Script.ScriptType outputScriptType, @Nullable KeyParameter aesKey)
throws DeterministicUpgradeRequiresPassword {
upgradeToDeterministic(outputScriptType, KeyChainGroupStructure.DEFAULT, aesKey);
}
/**
* Upgrades the wallet to be deterministic (BIP32). You should call this, possibly providing the users encryption
* key, after loading a wallet produced by previous versions of bitcoinj. If the wallet is encrypted the key
* <b>must</b> be provided, due to the way the seed is derived deterministically from private key bytes: failing
* to do this will result in an exception being thrown. For non-encrypted wallets, the upgrade will be done for
* you automatically the first time a new key is requested (this happens when spending due to the change address).
*/
public void upgradeToDeterministic(Script.ScriptType outputScriptType, KeyChainGroupStructure structure,
@Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword {
keyChainGroupLock.lock();
try {
keyChainGroup.upgradeToDeterministic(vKeyRotationTimestamp, aesKey);
long keyRotationTimeSecs = vKeyRotationTimestamp;
keyChainGroup.upgradeToDeterministic(outputScriptType, structure, keyRotationTimeSecs, aesKey);
} finally {
keyChainGroupLock.unlock();
}
@ -752,13 +766,14 @@ public class Wallet extends BaseTaggableObject
/**
* Returns true if the wallet contains random keys and no HD chains, in which case you should call
* {@link #upgradeToDeterministic(KeyParameter)} before attempting to do anything
* {@link #upgradeToDeterministic(ScriptType, KeyParameter)} before attempting to do anything
* that would require a new address or key.
*/
public boolean isDeterministicUpgradeRequired() {
public boolean isDeterministicUpgradeRequired(Script.ScriptType outputScriptType) {
keyChainGroupLock.lock();
try {
return keyChainGroup.isDeterministicUpgradeRequired();
long keyRotationTimeSecs = vKeyRotationTimestamp;
return keyChainGroup.isDeterministicUpgradeRequired(outputScriptType, keyRotationTimeSecs);
} finally {
keyChainGroupLock.unlock();
}
@ -5218,18 +5233,39 @@ public class Wallet extends BaseTaggableObject
* 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 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.
* @throws org.bitcoinj.wallet.DeterministicUpgradeRequiresPassword if key rotation requires the users password.
*/
public ListenableFuture<List<Transaction>> doMaintenance(@Nullable KeyParameter aesKey, boolean signAndSend) throws DeterministicUpgradeRequiresPassword {
public ListenableFuture<List<Transaction>> doMaintenance(@Nullable KeyParameter aesKey, boolean signAndSend)
throws DeterministicUpgradeRequiresPassword {
return doMaintenance(KeyChainGroupStructure.DEFAULT, aesKey, signAndSend);
}
/**
* A wallet app should call this from time to time 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 andSend set to false and look at the returned list of
* 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 structure to derive the account path from if a new seed needs to be created
* @param aesKey the users password, if any.
* @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.
* @throws org.bitcoinj.wallet.DeterministicUpgradeRequiresPassword if key rotation requires the users password.
*/
public ListenableFuture<List<Transaction>> doMaintenance(KeyChainGroupStructure structure,
@Nullable KeyParameter aesKey, boolean signAndSend) throws DeterministicUpgradeRequiresPassword {
List<Transaction> txns;
lock.lock();
keyChainGroupLock.lock();
try {
txns = maybeRotateKeys(aesKey, signAndSend);
txns = maybeRotateKeys(structure, aesKey, signAndSend);
if (!signAndSend)
return Futures.immediateFuture(txns);
} finally {
@ -5263,7 +5299,8 @@ public class Wallet extends BaseTaggableObject
// Checks to see if any coins are controlled by rotating keys and if so, spends them.
@GuardedBy("keyChainGroupLock")
private List<Transaction> maybeRotateKeys(@Nullable KeyParameter aesKey, boolean sign) throws DeterministicUpgradeRequiresPassword {
private List<Transaction> maybeRotateKeys(KeyChainGroupStructure structure, @Nullable KeyParameter aesKey,
boolean sign) throws DeterministicUpgradeRequiresPassword {
checkState(lock.isHeldByCurrentThread());
checkState(keyChainGroupLock.isHeldByCurrentThread());
List<Transaction> results = Lists.newLinkedList();
@ -5273,27 +5310,50 @@ public class Wallet extends BaseTaggableObject
// We might have to create a new HD hierarchy if the previous ones are now rotating.
boolean allChainsRotating = true;
for (DeterministicKeyChain chain : keyChainGroup.getDeterministicKeyChains()) {
if (chain.getEarliestKeyCreationTime() >= keyRotationTimestamp) {
allChainsRotating = false;
break;
Script.ScriptType preferredScriptType = Script.ScriptType.P2PKH;
if (keyChainGroup.isSupportsDeterministicChains()) {
for (DeterministicKeyChain chain : keyChainGroup.getDeterministicKeyChains()) {
if (chain.getEarliestKeyCreationTime() >= keyRotationTimestamp) {
allChainsRotating = false;
preferredScriptType = chain.getOutputScriptType();
}
}
}
if (allChainsRotating) {
try {
if (keyChainGroup.getImportedKeys().isEmpty()) {
log.info("All HD chains are currently rotating and we have no random keys, creating fresh HD chain ...");
DeterministicKeyChain chain = DeterministicKeyChain.builder().random(new SecureRandom()).build();
keyChainGroup.addAndActivateHDChain(chain);
KeyChainGroup newChains = KeyChainGroup.builder(params, structure).fromRandom(preferredScriptType)
.build();
if (keyChainGroup.isEncrypted()) {
if (aesKey == null)
throw new DeterministicUpgradeRequiresPassword();
KeyCrypter keyCrypter = keyChainGroup.getKeyCrypter();
keyChainGroup.decrypt(aesKey);
keyChainGroup.mergeActiveKeyChains(newChains, keyRotationTimestamp);
keyChainGroup.encrypt(keyCrypter, aesKey);
} else {
keyChainGroup.mergeActiveKeyChains(newChains, keyRotationTimestamp);
}
} else {
log.info("All HD chains are currently rotating, attempting to create a new one from the next oldest non-rotating key material ...");
keyChainGroup.upgradeToDeterministic(keyRotationTimestamp, aesKey);
keyChainGroup.upgradeToDeterministic(preferredScriptType, structure, 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.");
DeterministicKeyChain chain = DeterministicKeyChain.builder().random(new SecureRandom()).build();
keyChainGroup.addAndActivateHDChain(chain);
KeyChainGroup newChains = KeyChainGroup.builder(params, structure).fromRandom(preferredScriptType)
.build();
if (keyChainGroup.isEncrypted()) {
if (aesKey == null)
throw new DeterministicUpgradeRequiresPassword();
KeyCrypter keyCrypter = keyChainGroup.getKeyCrypter();
keyChainGroup.decrypt(aesKey);
keyChainGroup.mergeActiveKeyChains(newChains, keyRotationTimestamp);
keyChainGroup.encrypt(keyCrypter, aesKey);
} else {
keyChainGroup.mergeActiveKeyChains(newChains, keyRotationTimestamp);
}
}
saveNow();
}

View File

@ -523,7 +523,8 @@ public class KeyChainGroupTest {
// Check that if we try to use HD features in a KCG that only has random keys, we get an exception.
group = KeyChainGroup.builder(MAINNET).build();
group.importKeys(new ECKey(), new ECKey());
assertTrue(group.isDeterministicUpgradeRequired());
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH, 0));
group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); // throws
}
@ -538,14 +539,19 @@ public class KeyChainGroupTest {
group.importKeys(key2, key1);
List<Protos.Key> protobufs = group.serializeToProtobuf();
group.upgradeToDeterministic(0, null);
assertFalse(group.isDeterministicUpgradeRequired());
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, null);
assertFalse(group.isEncrypted());
assertFalse(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH, 0));
DeterministicKey dkey1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicSeed seed1 = group.getActiveKeyChain().getSeed();
assertNotNull(seed1);
group = KeyChainGroup.fromProtobufUnencrypted(MAINNET, protobufs);
group.upgradeToDeterministic(0, null); // Should give same result as last time.
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, null); // Should give same result as last time.
assertFalse(group.isEncrypted());
assertFalse(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH, 0));
DeterministicKey dkey2 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicSeed seed2 = group.getActiveKeyChain().getSeed();
assertEquals(seed1, seed2);
@ -566,7 +572,7 @@ public class KeyChainGroupTest {
Utils.rollMockClock(86400);
ECKey key3 = new ECKey();
group.importKeys(key2, key1, key3);
group.upgradeToDeterministic(now + 10, null);
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, now + 10, null);
DeterministicSeed seed = group.getActiveKeyChain().getSeed();
assertNotNull(seed);
// Check we used the right key: oldest non rotating.
@ -581,17 +587,19 @@ public class KeyChainGroupTest {
group.importKeys(key);
final KeyCrypterScrypt crypter = new KeyCrypterScrypt();
final KeyParameter aesKey = crypter.deriveKey("abc");
assertTrue(group.isDeterministicUpgradeRequired());
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
group.encrypt(crypter, aesKey);
assertTrue(group.isDeterministicUpgradeRequired());
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
try {
group.upgradeToDeterministic(0, null);
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, null);
fail();
} catch (DeterministicUpgradeRequiresPassword e) {
// Expected.
}
group.upgradeToDeterministic(0, aesKey);
assertFalse(group.isDeterministicUpgradeRequired());
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, aesKey);
assertTrue(group.isEncrypted());
assertFalse(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH, 0));
final DeterministicSeed deterministicSeed = group.getActiveKeyChain().getSeed();
assertNotNull(deterministicSeed);
assertTrue(deterministicSeed.isEncrypted());

View File

@ -3051,7 +3051,7 @@ public class WalletTest extends TestWithWallet {
Utils.rollMockClock(86400);
wallet = new Wallet(UNITTEST, kcg); // This avoids the automatic HD initialisation
assertTrue(kcg.getDeterministicKeyChains().isEmpty());
wallet.upgradeToDeterministic(null);
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
DeterministicKey badWatchingKey = wallet.getWatchingKey();
assertEquals(badKey.getCreationTimeSeconds(), badWatchingKey.getCreationTimeSeconds());
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, LegacyAddress.fromKey(UNITTEST, badWatchingKey));
@ -3292,45 +3292,166 @@ public class WalletTest extends TestWithWallet {
}
@Test
public void upgradeToDeterministic_unencrypted() throws Exception {
// This isn't very deep because most of it is tested in KeyChainGroupTest and Wallet just forwards most logic
// there.
// Create an old-style random wallet.
wallet = Wallet.fromKeys(UNITTEST, Arrays.asList(new ECKey(), new ECKey()));
assertTrue(wallet.isDeterministicUpgradeRequired());
public void upgradeToDeterministic_basic_to_P2PKH_unencrypted() throws Exception {
wallet = new Wallet(UNITTEST, KeyChainGroup.builder(UNITTEST).build());
wallet.importKeys(Arrays.asList(new ECKey(), new ECKey()));
assertFalse(wallet.isEncrypted());
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
try {
wallet.freshReceiveKey();
fail();
} catch (DeterministicUpgradeRequiredException e) {
// Expected.
}
wallet.upgradeToDeterministic(null);
assertFalse(wallet.isDeterministicUpgradeRequired());
// Use an HD feature.
wallet.freshReceiveKey(); // works.
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2PKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2PKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_encrypted() throws Exception {
// Create an old-style random wallet.
wallet = Wallet.fromKeys(UNITTEST, Arrays.asList(new ECKey(), new ECKey()));
assertTrue(wallet.isDeterministicUpgradeRequired());
KeyCrypter crypter = new KeyCrypterScrypt();
KeyParameter aesKey = crypter.deriveKey("abc");
wallet.encrypt(crypter, aesKey);
public void upgradeToDeterministic_basic_to_P2PKH_encrypted() throws Exception {
wallet = new Wallet(UNITTEST, KeyChainGroup.builder(UNITTEST).build());
wallet.importKeys(Arrays.asList(new ECKey(), new ECKey()));
assertFalse(wallet.isEncrypted());
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
KeyParameter aesKey = new KeyCrypterScrypt().deriveKey("abc");
wallet.encrypt(new KeyCrypterScrypt(), aesKey);
assertTrue(wallet.isEncrypted());
try {
wallet.freshReceiveKey();
fail();
} catch (DeterministicUpgradeRequiredException e) {
// Expected.
}
try {
wallet.upgradeToDeterministic(null);
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
fail();
} catch (DeterministicUpgradeRequiresPassword e) {
// Expected.
}
wallet.upgradeToDeterministic(aesKey);
assertFalse(wallet.isDeterministicUpgradeRequired());
// Use an HD feature.
wallet.freshReceiveKey(); // works.
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, aesKey);
assertTrue(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2PKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2PKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_basic_to_P2WPKH_unencrypted() throws Exception {
wallet = new Wallet(UNITTEST, KeyChainGroup.builder(UNITTEST).build());
wallet.importKeys(Arrays.asList(new ECKey(), new ECKey()));
assertFalse(wallet.isEncrypted());
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
try {
wallet.freshReceiveKey();
fail();
} catch (DeterministicUpgradeRequiredException e) {
// Expected.
}
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, null);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2WPKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2WPKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_basic_to_P2WPKH_encrypted() throws Exception {
wallet = new Wallet(UNITTEST, KeyChainGroup.builder(UNITTEST).build());
wallet.importKeys(Arrays.asList(new ECKey(), new ECKey()));
assertFalse(wallet.isEncrypted());
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
KeyParameter aesKey = new KeyCrypterScrypt().deriveKey("abc");
wallet.encrypt(new KeyCrypterScrypt(), aesKey);
assertTrue(wallet.isEncrypted());
try {
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, null);
fail();
} catch (DeterministicUpgradeRequiresPassword e) {
// Expected.
}
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, aesKey);
assertTrue(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2WPKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2WPKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_P2PKH_to_P2WPKH_unencrypted() throws Exception {
wallet = Wallet.createDeterministic(UNITTEST, Script.ScriptType.P2PKH);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2PKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2PKH, wallet.freshReceiveAddress().getOutputScriptType());
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, null);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2WPKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2WPKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_P2PKH_to_P2WPKH_encrypted() throws Exception {
wallet = Wallet.createDeterministic(UNITTEST, Script.ScriptType.P2PKH);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
KeyParameter aesKey = new KeyCrypterScrypt().deriveKey("abc");
wallet.encrypt(new KeyCrypterScrypt(), aesKey);
assertTrue(wallet.isEncrypted());
assertEquals(Script.ScriptType.P2PKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2PKH, wallet.freshReceiveAddress().getOutputScriptType());
try {
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, null);
fail();
} catch (DeterministicUpgradeRequiresPassword e) {
// Expected.
}
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, aesKey);
assertTrue(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2WPKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2WPKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_noDowngrade_unencrypted() throws Exception {
wallet = Wallet.createDeterministic(UNITTEST, Script.ScriptType.P2WPKH);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2WPKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2WPKH, wallet.freshReceiveAddress().getOutputScriptType());
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2WPKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2WPKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test(expected = IllegalStateException.class)
@ -3425,7 +3546,7 @@ public class WalletTest extends TestWithWallet {
Wallet wallet = Wallet.fromKeys(UNITTEST, Arrays.asList(key));
assertEquals(1, wallet.getImportedKeys().size());
assertEquals(key, wallet.getImportedKeys().get(0));
wallet.upgradeToDeterministic(null);
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
String seed = wallet.getKeyChainSeed().toHexString();
assertEquals("5ca8cd6c01aa004d3c5396c628b78a4a89462f412f460a845b594ac42eceaa264b0e14dcd4fe73d4ed08ce06f4c28facfa85042d26d784ab2798a870bb7af556", seed);
}

View File

@ -211,6 +211,7 @@ public class WalletTool {
ENCRYPT,
DECRYPT,
MARRY,
UPGRADE,
ROTATE,
SET_CREATION_TIME,
}
@ -235,7 +236,8 @@ public class WalletTool {
OptionSpec<String> walletFileName = parser.accepts("wallet").withRequiredArg().defaultsTo("wallet");
seedFlag = parser.accepts("seed").withRequiredArg();
watchFlag = parser.accepts("watchkey").withRequiredArg();
outputScriptTypeFlag = parser.accepts("output-script-type").withRequiredArg().ofType(Script.ScriptType.class);
outputScriptTypeFlag = parser.accepts("output-script-type").withRequiredArg().ofType(Script.ScriptType.class)
.defaultsTo(Script.ScriptType.P2PKH);
OptionSpec<NetworkEnum> netFlag = parser.accepts("net").withRequiredArg().ofType(NetworkEnum.class).defaultsTo(NetworkEnum.MAIN);
dateFlag = parser.accepts("date").withRequiredArg().ofType(Date.class)
.withValuesConvertedBy(DateConverter.datePattern("yyyy/MM/dd"));
@ -480,6 +482,7 @@ public class WalletTool {
case ENCRYPT: encrypt(); break;
case DECRYPT: decrypt(); break;
case MARRY: marry(); break;
case UPGRADE: upgrade(); break;
case ROTATE: rotate(); break;
case SET_CREATION_TIME: setCreationTime(); break;
}
@ -555,6 +558,27 @@ public class WalletTool {
wallet.addAndActivateHDChain(chain);
}
private static void upgrade() {
DeterministicKeyChain activeKeyChain = wallet.getActiveKeyChain();
ScriptType currentOutputScriptType = activeKeyChain != null ? activeKeyChain.getOutputScriptType() : null;
ScriptType outputScriptType = options.valueOf(outputScriptTypeFlag);
if (!wallet.isDeterministicUpgradeRequired(outputScriptType)) {
System.err
.println("No upgrade from " + (currentOutputScriptType != null ? currentOutputScriptType : "basic")
+ " to " + outputScriptType);
return;
}
KeyParameter aesKey = null;
if (wallet.isEncrypted()) {
aesKey = passwordToKey(true);
if (aesKey == null)
return;
}
wallet.upgradeToDeterministic(outputScriptType, aesKey);
System.out.println("Upgraded from " + (currentOutputScriptType != null ? currentOutputScriptType : "basic")
+ " to " + outputScriptType);
}
private static void rotate() throws BlockStoreException {
setup();
peerGroup.start();

View File

@ -13,7 +13,7 @@ Usage: wallet-tool --flags action-name
If --seed is present, it should specify either a mnemonic code or hex/base58 raw seed bytes.
If --watchkey is present, it creates a watching wallet using the specified base58 xpub.
If --seed or --watchkey is combined with either --date or --unixtime, use that as a birthdate for.
If --output-script-type, use that for deriving addresses.
If --output-script-type is present, use that for deriving addresses.
the wallet. See the set-creation-time action for the meaning of these flags.
marry Makes the wallet married with other parties, requiring multisig to spend funds.
External public keys for other signing parties must be specified with --xpubkeys (comma separated).
@ -57,6 +57,8 @@ 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.
upgrade Upgrade basic or deterministic wallets to deterministic wallets of the given script type.
If --output-script-type is present, use that as the upgrade target.
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
@ -100,6 +102,7 @@ Usage: wallet-tool --flags action-name
--chain=<file> Specifies the name of the file that stores the block chain.
--force Overrides any safety checks on the requested action.
--date Provide a date in form YYYY/MM/DD to any action that requires one.
--output-script-type Provide an output script type to any action that requires one. May be P2PKH or P2WPKH.
--peers=1.2.3.4 Comma separated IP addresses/domain names for connections instead of peer discovery.
--offline If specified when sending, don't try and connect, just write the tx to the wallet.
--condition=... Allows you to specify a numeric condition for other commands. The format is