mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-02-23 06:35:17 +01:00
KeyChainGroup: remove upgrade path from basic to deterministic
This commit is contained in:
parent
20aee84773
commit
4e8a19997d
3 changed files with 5 additions and 250 deletions
|
@ -218,7 +218,7 @@ public class KeyChainGroup implements KeyBag {
|
|||
|
||||
private BasicKeyChain basic;
|
||||
private final NetworkParameters params;
|
||||
// Keychains for deterministically derived keys. If this is null, no chains should be created automatically.
|
||||
// Keychains for deterministically derived keys.
|
||||
protected final @Nullable LinkedList<DeterministicKeyChain> chains;
|
||||
// currentKeys is used for normal, non-multisig/married wallets. currentAddresses is used when we're handing out
|
||||
// P2SH addresses. They're mutually exclusive.
|
||||
|
@ -282,7 +282,7 @@ public class KeyChainGroup implements KeyBag {
|
|||
}
|
||||
}
|
||||
|
||||
/** Returns true if it contains any deterministic keychain or one could be created. */
|
||||
/** Returns true if it contains any deterministic keychain. */
|
||||
public boolean isSupportsDeterministicChains() {
|
||||
return chains != null;
|
||||
}
|
||||
|
@ -949,62 +949,13 @@ public class KeyChainGroup implements KeyBag {
|
|||
*/
|
||||
public void upgradeToDeterministic(Script.ScriptType preferredScriptType, KeyChainGroupStructure structure,
|
||||
long keyRotationTimeSecs, @Nullable KeyParameter aesKey)
|
||||
throws DeterministicUpgradeRequiresPassword, AllRandomKeysRotating {
|
||||
throws DeterministicUpgradeRequiresPassword {
|
||||
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
|
||||
checkNotNull(structure);
|
||||
checkArgument(keyRotationTimeSecs >= 0);
|
||||
if (!isDeterministicUpgradeRequired(preferredScriptType, keyRotationTimeSecs))
|
||||
return; // Nothing to do.
|
||||
|
||||
// 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.");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// P2PKH --> P2WPKH upgrade
|
||||
if (preferredScriptType == Script.ScriptType.P2WPKH
|
||||
&& getActiveKeyChain(Script.ScriptType.P2WPKH, keyRotationTimeSecs) == null) {
|
||||
|
|
|
@ -542,13 +542,7 @@ public class KeyChainGroupTest {
|
|||
|
||||
@Test
|
||||
public void deterministicUpgradeUnencrypted() throws Exception {
|
||||
// Check that a group that contains only random keys has its HD chain created using the private key bytes of
|
||||
// the oldest random key, so upgrading the same wallet twice gives the same outcome.
|
||||
group = KeyChainGroup.builder(MAINNET).lookaheadSize(LOOKAHEAD_SIZE).build();
|
||||
ECKey key1 = new ECKey();
|
||||
Utils.rollMockClock(86400);
|
||||
ECKey key2 = new ECKey();
|
||||
group.importKeys(key2, key1);
|
||||
group = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2PKH).lookaheadSize(LOOKAHEAD_SIZE).build();
|
||||
|
||||
List<Protos.Key> protobufs = group.serializeToProtobuf();
|
||||
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, null);
|
||||
|
@ -568,45 +562,12 @@ public class KeyChainGroupTest {
|
|||
DeterministicSeed seed2 = group.getActiveKeyChain().getSeed();
|
||||
assertEquals(seed1, seed2);
|
||||
assertEquals(dkey1, dkey2);
|
||||
|
||||
// Check we used the right (oldest) key despite backwards import order.
|
||||
byte[] truncatedBytes = Arrays.copyOfRange(key1.getSecretBytes(), 0, 16);
|
||||
assertArrayEquals(seed1.getEntropyBytes(), truncatedBytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deterministicUpgradeRotating() throws Exception {
|
||||
group = KeyChainGroup.builder(MAINNET).lookaheadSize(LOOKAHEAD_SIZE).build();
|
||||
long now = Utils.currentTimeSeconds();
|
||||
ECKey key1 = new ECKey();
|
||||
Utils.rollMockClock(86400);
|
||||
ECKey key2 = new ECKey();
|
||||
Utils.rollMockClock(86400);
|
||||
ECKey key3 = new ECKey();
|
||||
group.importKeys(key2, key1, key3);
|
||||
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.
|
||||
byte[] truncatedBytes = Arrays.copyOfRange(key2.getSecretBytes(), 0, 16);
|
||||
assertArrayEquals(seed.getEntropyBytes(), truncatedBytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deterministicUpgradeEncrypted() throws Exception {
|
||||
group = KeyChainGroup.builder(MAINNET).build();
|
||||
final ECKey key = new ECKey();
|
||||
group.importKeys(key);
|
||||
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
|
||||
group = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2PKH).build();
|
||||
group.encrypt(KEY_CRYPTER, AES_KEY);
|
||||
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
|
||||
try {
|
||||
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, null);
|
||||
fail();
|
||||
} catch (DeterministicUpgradeRequiresPassword e) {
|
||||
// Expected.
|
||||
}
|
||||
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, AES_KEY);
|
||||
assertTrue(group.isEncrypted());
|
||||
assertFalse(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
|
||||
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH, 0));
|
||||
|
@ -614,9 +575,6 @@ public class KeyChainGroupTest {
|
|||
assertNotNull(deterministicSeed);
|
||||
assertTrue(deterministicSeed.isEncrypted());
|
||||
byte[] entropy = checkNotNull(group.getActiveKeyChain().toDecrypted(AES_KEY).getSeed()).getEntropyBytes();
|
||||
// Check we used the right key: oldest non rotating.
|
||||
byte[] truncatedBytes = Arrays.copyOfRange(key.getSecretBytes(), 0, 16);
|
||||
assertArrayEquals(entropy, truncatedBytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -2982,55 +2982,6 @@ public class WalletTest extends TestWithWallet {
|
|||
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.
|
||||
KeyChainGroup kcg = KeyChainGroup.builder(UNITTEST).build();
|
||||
kcg.importKeys(badKey, goodKey);
|
||||
Utils.rollMockClock(86400);
|
||||
wallet = new Wallet(UNITTEST, kcg); // This avoids the automatic HD initialisation
|
||||
assertTrue(kcg.getDeterministicKeyChains().isEmpty());
|
||||
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));
|
||||
|
||||
// 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).getScriptPubKey().getToAddress(UNITTEST);
|
||||
ECKey usedKey = wallet.findKeyFromPubKeyHash(output.getHash(), output.getOutputScriptType());
|
||||
assertEquals(goodKey.getCreationTimeSeconds(), usedKey.getCreationTimeSeconds());
|
||||
assertEquals(goodKey.getCreationTimeSeconds(), wallet.freshReceiveKey().getCreationTimeSeconds());
|
||||
assertEquals("mrM3TpCnav5YQuVA1xLercCGJH4DXujMtv", LegacyAddress.fromKey(UNITTEST, usedKey).toString());
|
||||
DeterministicKeyChain c = kcg.getDeterministicKeyChains().get(1);
|
||||
assertEquals(c.getEarliestKeyCreationTime(), goodKey.getCreationTimeSeconds());
|
||||
assertEquals(2, kcg.getDeterministicKeyChains().size());
|
||||
|
||||
// Commit the maint txns.
|
||||
wallet.commitTx(txns.get(0));
|
||||
|
||||
// Check next maintenance does nothing.
|
||||
assertTrue(wallet.doMaintenance(null, false).get().isEmpty());
|
||||
assertEquals(c, kcg.getDeterministicKeyChains().get(1));
|
||||
assertEquals(2, kcg.getDeterministicKeyChains().size());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void importOfHDKeyForbidden() throws Exception {
|
||||
wallet.importKey(wallet.freshReceiveKey());
|
||||
|
@ -3234,108 +3185,6 @@ public class WalletTest extends TestWithWallet {
|
|||
assertEquals(1, keys.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
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(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_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(SCRYPT_ITERATIONS).deriveKey("abc");
|
||||
wallet.encrypt(new KeyCrypterScrypt(), aesKey);
|
||||
assertTrue(wallet.isEncrypted());
|
||||
try {
|
||||
wallet.freshReceiveKey();
|
||||
fail();
|
||||
} catch (DeterministicUpgradeRequiredException e) {
|
||||
// Expected.
|
||||
}
|
||||
try {
|
||||
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
|
||||
fail();
|
||||
} catch (DeterministicUpgradeRequiresPassword e) {
|
||||
// Expected.
|
||||
}
|
||||
|
||||
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(SCRYPT_ITERATIONS).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);
|
||||
|
@ -3489,9 +3338,6 @@ 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(Script.ScriptType.P2PKH, null);
|
||||
String seed = wallet.getKeyChainSeed().toHexString();
|
||||
assertEquals("5ca8cd6c01aa004d3c5396c628b78a4a89462f412f460a845b594ac42eceaa264b0e14dcd4fe73d4ed08ce06f4c28facfa85042d26d784ab2798a870bb7af556", seed);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Add table
Reference in a new issue