3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-15 11:45:51 +00:00

HD Wallets: support watching wallets in Wallet and wallet-tool.

Also, respect includePrivateKeys flag for the seed in wallet.toString again.
This commit is contained in:
Mike Hearn 2014-03-28 20:22:31 +01:00
parent c7f7fd29e0
commit 51b71a4363
8 changed files with 96 additions and 45 deletions

View File

@ -18,6 +18,7 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt; import com.google.bitcoin.crypto.KeyCrypterScrypt;
@ -212,6 +213,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
return new Wallet(params, new KeyChainGroup(seed)); return new Wallet(params, new KeyChainGroup(seed));
} }
public static Wallet fromWatchingKey(NetworkParameters params, DeterministicKey watchKey) {
return new Wallet(params, new KeyChainGroup(watchKey));
}
// TODO: When this class moves to the Wallet package, along with the protobuf serializer, then hide this. // TODO: When this class moves to the Wallet package, along with the protobuf serializer, then hide this.
/** For internal use only. */ /** For internal use only. */
public Wallet(NetworkParameters params, KeyChainGroup keyChainGroup) { public Wallet(NetworkParameters params, KeyChainGroup keyChainGroup) {
@ -274,7 +279,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* it's actually seen in a pending or confirmed transaction, at which point this method will start returning * it's actually seen in a pending or confirmed transaction, at which point this method will start returning
* a different key (for each purpose independently). * a different key (for each purpose independently).
*/ */
public ECKey currentKey(KeyChain.KeyPurpose purpose) { public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) {
lock.lock(); lock.lock();
try { try {
return keychain.currentKey(purpose); return keychain.currentKey(purpose);
@ -287,7 +292,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* An alias for calling {@link #currentKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} with * An alias for calling {@link #currentKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} with
* {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} as the parameter. * {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} as the parameter.
*/ */
public ECKey currentReceiveKey() { public DeterministicKey currentReceiveKey() {
return currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); return currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
} }
@ -299,10 +304,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* into a receive coins wizard type UI. You should use this when the user is definitely going to hand this key out * into a receive coins wizard type UI. You should use this when the user is definitely going to hand this key out
* to someone who wishes to send money. * to someone who wishes to send money.
*/ */
public ECKey freshKey(KeyChain.KeyPurpose purpose) { public DeterministicKey freshKey(KeyChain.KeyPurpose purpose) {
lock.lock(); lock.lock();
try { try {
ECKey key = keychain.freshKey(purpose); DeterministicKey key = keychain.freshKey(purpose);
// Do we really need an immediate hard save? Arguably all this is doing is saving the 'current' key // Do we really need an immediate hard save? Arguably all this is doing is saving the 'current' key
// and that's not quite so important, so we could coalesce for more performance. // and that's not quite so important, so we could coalesce for more performance.
saveNow(); saveNow();
@ -316,7 +321,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
* An alias for calling {@link #freshKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} with * An alias for calling {@link #freshKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} with
* {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} as the parameter. * {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} as the parameter.
*/ */
public ECKey freshReceiveKey() { public DeterministicKey freshReceiveKey() {
return freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); return freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
} }
@ -456,6 +461,20 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
return keychain.getLookaheadSize(); return keychain.getLookaheadSize();
} }
/**
* Returns a public-only DeterministicKey that can be used to set up a watching wallet: that is, a wallet that
* can import transactions from the block chain just as the normal wallet can, but which cannot spend. Watching
* wallets are very useful for things like web servers that accept payments.
*/
public DeterministicKey getWatchingKey() {
lock.lock();
try {
return keychain.getActiveKeyChain().getWatchingKey();
} finally {
lock.unlock();
}
}
/** /**
* Return true if we are watching this address. * Return true if we are watching this address.
*/ */

View File

@ -219,7 +219,10 @@ public class DeterministicKey extends ECKey {
} else { } else {
// If it's not encrypted, derive the private via the parents. // If it's not encrypted, derive the private via the parents.
final BigInteger privateKey = findOrDerivePrivateKey(); final BigInteger privateKey = findOrDerivePrivateKey();
checkState(privateKey != null, "This key is a part of a public-key only heirarchy and cannot be used for signing"); if (privateKey == null) {
// This key is a part of a public-key only heirarchy and cannot be used for signing
throw new MissingPrivateKeyException();
}
return super.doSign(input, privateKey); return super.doSign(input, privateKey);
} }
} }

View File

@ -62,7 +62,8 @@ import static com.google.common.collect.Lists.newLinkedList;
* {@link com.google.bitcoin.crypto.DeterministicKey#serializePubB58()}. The resulting "xpub..." string encodes * {@link com.google.bitcoin.crypto.DeterministicKey#serializePubB58()}. The resulting "xpub..." string encodes
* sufficient information about the account key to create a watching chain via * sufficient information about the account key to create a watching chain via
* {@link com.google.bitcoin.crypto.DeterministicKey#deserializeB58(com.google.bitcoin.crypto.DeterministicKey, String)} * {@link com.google.bitcoin.crypto.DeterministicKey#deserializeB58(com.google.bitcoin.crypto.DeterministicKey, String)}
* (with null as the first parameter) and then {@link #watch(com.google.bitcoin.crypto.DeterministicKey)}.</p> * (with null as the first parameter) and then
* {@link DeterministicKeyChain#DeterministicKeyChain(com.google.bitcoin.crypto.DeterministicKey)}.</p>
* *
* <p>This class builds on {@link com.google.bitcoin.crypto.DeterministicHierarchy} and * <p>This class builds on {@link com.google.bitcoin.crypto.DeterministicHierarchy} and
* {@link com.google.bitcoin.crypto.DeterministicKey} by adding support for serialization to and from protobufs, * {@link com.google.bitcoin.crypto.DeterministicKey} by adding support for serialization to and from protobufs,
@ -135,19 +136,18 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
this(seed, null); this(seed, null);
} }
// c'tor for building watching chains, we keep it private and give it a static name to make the purpose clear.
private DeterministicKeyChain(DeterministicKey accountKey) {
checkArgument(accountKey.isPubKeyOnly(), "Private subtrees not currently supported");
checkArgument(accountKey.getPath().size() == 1, "You can only watch an account key currently");
basicKeyChain = new BasicKeyChain();
initializeHierarchyUnencrypted(accountKey);
}
/** /**
* Creates a deterministic key chain that watches the given (public only) root key. You can use this to calculate * Creates a deterministic key chain that watches the given (public only) root key. You can use this to calculate
* balances and generally follow along, but spending is not possible with such a chain. Currently you can't use * balances and generally follow along, but spending is not possible with such a chain. Currently you can't use
* this method to watch an arbitrary fragment of some other tree, this limitation may be removed in future. * this method to watch an arbitrary fragment of some other tree, this limitation may be removed in future.
*/ */
public DeterministicKeyChain(DeterministicKey watchingKey) {
checkArgument(watchingKey.isPubKeyOnly(), "Private subtrees not currently supported");
checkArgument(watchingKey.getPath().size() == 1, "You can only watch an account key currently");
basicKeyChain = new BasicKeyChain();
initializeHierarchyUnencrypted(watchingKey);
}
public static DeterministicKeyChain watch(DeterministicKey accountKey) { public static DeterministicKeyChain watch(DeterministicKey accountKey) {
return new DeterministicKeyChain(accountKey); return new DeterministicKeyChain(accountKey);
} }
@ -445,7 +445,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH)) if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH))
throw new UnreadableWalletException("Expecting account key but found key with path: " + throw new UnreadableWalletException("Expecting account key but found key with path: " +
HDUtils.formatPath(accountKey.getPath())); HDUtils.formatPath(accountKey.getPath()));
chain = DeterministicKeyChain.watch(accountKey); chain = new DeterministicKeyChain(accountKey);
isWatchingAccountKey = true; isWatchingAccountKey = true;
} else { } else {
chain = new DeterministicKeyChain(seed, crypter); chain = new DeterministicKeyChain(seed, crypter);

View File

@ -66,7 +66,7 @@ public class DeterministicSeed implements EncryptableItem {
/** /**
* Constructs a seed from a BIP 39 mnemonic code. See {@link com.google.bitcoin.crypto.MnemonicCode} for more * Constructs a seed from a BIP 39 mnemonic code. See {@link com.google.bitcoin.crypto.MnemonicCode} for more
* details on this scheme. * details on this scheme.
* @param words A list of 12 words. * @param words A list of words.
* @param creationTimeSeconds When the seed was originally created, UNIX time. * @param creationTimeSeconds When the seed was originally created, UNIX time.
* @throws MnemonicException if there is a problem decoding the words. * @throws MnemonicException if there is a problem decoding the words.
*/ */

View File

@ -67,6 +67,14 @@ public class KeyChainGroup {
this(null, ImmutableList.of(new DeterministicKeyChain(seed)), null); this(null, ImmutableList.of(new DeterministicKeyChain(seed)), null);
} }
/**
* Creates a keychain group with no basic chain, and an HD chain that is watching the given watching key.
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
*/
public KeyChainGroup(DeterministicKey watchKey) {
this(null, ImmutableList.of(new DeterministicKeyChain(watchKey)), null);
}
// Used for deserialization. // Used for deserialization.
private KeyChainGroup(@Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains, @Nullable KeyCrypter crypter) { private KeyChainGroup(@Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains, @Nullable KeyCrypter crypter) {
this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain; this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
@ -89,7 +97,7 @@ public class KeyChainGroup {
* it's actually seen in a pending or confirmed transaction, at which point this method will start returning * it's actually seen in a pending or confirmed transaction, at which point this method will start returning
* a different key (for each purpose independently). * a different key (for each purpose independently).
*/ */
public ECKey currentKey(KeyChain.KeyPurpose purpose) { public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) {
final DeterministicKey current = currentKeys.get(purpose); final DeterministicKey current = currentKeys.get(purpose);
return current != null ? current : freshKey(purpose); return current != null ? current : freshKey(purpose);
} }
@ -102,7 +110,7 @@ public class KeyChainGroup {
* into a receive coins wizard type UI. You should use this when the user is definitely going to hand this key out * into a receive coins wizard type UI. You should use this when the user is definitely going to hand this key out
* to someone who wishes to send money. * to someone who wishes to send money.
*/ */
public ECKey freshKey(KeyChain.KeyPurpose purpose) { public DeterministicKey freshKey(KeyChain.KeyPurpose purpose) {
DeterministicKeyChain chain = getActiveKeyChain(); DeterministicKeyChain chain = getActiveKeyChain();
DeterministicKey key = chain.getKey(purpose); // Always returns the next key along the key chain. DeterministicKey key = chain.getKey(purpose); // Always returns the next key along the key chain.
currentKeys.put(purpose, key); currentKeys.put(purpose, key);
@ -372,26 +380,27 @@ public class KeyChainGroup {
for (ECKey key : basic.getKeys()) for (ECKey key : basic.getKeys())
formatKeyWithAddress(params, includePrivateKeys, key, builder); formatKeyWithAddress(params, includePrivateKeys, key, builder);
} }
final String newline = String.format("%n");
for (DeterministicKeyChain chain : chains) { for (DeterministicKeyChain chain : chains) {
DeterministicSeed seed = chain.getSeed(); DeterministicSeed seed = chain.getSeed();
if (seed != null && !seed.isEncrypted()) { if (seed != null) {
final List<String> words = seed.toMnemonicCode(); if (seed.isEncrypted()) {
builder.append("Seed as words: "); builder.append(String.format("Seed is encrypted%n"));
builder.append(Joiner.on(' ').join(words)); } else if (includePrivateKeys) {
builder.append(newline); final List<String> words = seed.toMnemonicCode();
builder.append("Seed as hex: "); builder.append(
builder.append(seed.toHexString()); String.format("Seed as words: %s%nSeed as hex: %s%n", Joiner.on(' ').join(words),
builder.append(newline); seed.toHexString())
builder.append("Seed birthday: "); );
builder.append(seed.getCreationTimeSeconds()); }
builder.append(" [" + new Date(seed.getCreationTimeSeconds() * 1000) + "]"); builder.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(), new Date(seed.getCreationTimeSeconds() * 1000)));
builder.append(newline); }
builder.append(newline); final DeterministicKey watchingKey = chain.getWatchingKey();
} else { // Don't show if it's been imported from a watching wallet already, because it'd result in a weird/
builder.append("Seed is encrypted"); // unintuitive result where the watching key in a watching wallet is not the one it was created with
builder.append(newline); // due to the parent fingerprint being missing/not stored. In future we could store the parent fingerprint
builder.append(newline); // 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()));
} }
for (ECKey key : chain.getKeys()) for (ECKey key : chain.getKeys())
formatKeyWithAddress(params, includePrivateKeys, key, builder); formatKeyWithAddress(params, includePrivateKeys, key, builder);

View File

@ -19,10 +19,7 @@ package com.google.bitcoin.core;
import com.google.bitcoin.core.Transaction.SigHash; import com.google.bitcoin.core.Transaction.SigHash;
import com.google.bitcoin.core.Wallet.SendRequest; import com.google.bitcoin.core.Wallet.SendRequest;
import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.*;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.crypto.TransactionSignature;
import com.google.bitcoin.store.WalletProtobufSerializer; import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.testing.FakeTxBuilder; import com.google.bitcoin.testing.FakeTxBuilder;
import com.google.bitcoin.testing.MockTransactionBroadcaster; import com.google.bitcoin.testing.MockTransactionBroadcaster;
@ -1098,6 +1095,23 @@ public class WalletTest extends TestWithWallet {
log.info(t2.toString(chain)); log.info(t2.toString(chain));
} }
@Test(expected = ECKey.MissingPrivateKeyException.class)
public void watchingWallet() throws Exception {
DeterministicKey key = wallet.freshReceiveKey();
DeterministicKey watchKey = wallet.getWatchingKey();
String serialized = watchKey.serializePubB58();
watchKey = DeterministicKey.deserializeB58(null, serialized);
Wallet watchingWallet = Wallet.fromWatchingKey(params, watchKey);
DeterministicKey key2 = watchingWallet.freshReceiveKey();
assertEquals(key, key2);
key = wallet.freshKey(KeyChain.KeyPurpose.CHANGE);
key2 = watchingWallet.freshKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(key, key2);
key.sign(Sha256Hash.ZERO_HASH);
key2.sign(Sha256Hash.ZERO_HASH);
}
@Test @Test
public void watchingScripts() throws Exception { public void watchingScripts() throws Exception {
// Verify that pending transactions to watched addresses are relevant // Verify that pending transactions to watched addresses are relevant

View File

@ -230,7 +230,7 @@ public class DeterministicKeyChainTest {
final String pub58 = watchingKey.serializePubB58(); final String pub58 = watchingKey.serializePubB58();
assertEquals("xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi", pub58); assertEquals("xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi", pub58);
watchingKey = DeterministicKey.deserializeB58(null, pub58); watchingKey = DeterministicKey.deserializeB58(null, pub58);
chain = DeterministicKeyChain.watch(watchingKey); chain = new DeterministicKeyChain(watchingKey);
chain.setLookaheadSize(10); chain.setLookaheadSize(10);
assertEquals(key1.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint()); assertEquals(key1.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint());
@ -241,7 +241,8 @@ public class DeterministicKeyChainTest {
// Can't sign with a key from a watching chain. // Can't sign with a key from a watching chain.
key.sign(Sha256Hash.ZERO_HASH); key.sign(Sha256Hash.ZERO_HASH);
fail(); fail();
} catch (IllegalStateException e) { } catch (ECKey.MissingPrivateKeyException e) {
// Ignored.
} }
// Test we can serialize and deserialize a watching chain OK. // Test we can serialize and deserialize a watching chain OK.
List<Protos.Key> serialization = chain.serializeToProtobuf(); List<Protos.Key> serialization = chain.serializeToProtobuf();
@ -254,7 +255,7 @@ public class DeterministicKeyChainTest {
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void watchingCannotEncrypt() throws Exception { public void watchingCannotEncrypt() throws Exception {
final DeterministicKey accountKey = chain.getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH); final DeterministicKey accountKey = chain.getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH);
chain = DeterministicKeyChain.watch(accountKey.getPubOnly()); chain = new DeterministicKeyChain(accountKey.getPubOnly());
chain = chain.toEncrypted("this doesn't make any sense"); chain = chain.toEncrypted("this doesn't make any sense");
} }

View File

@ -18,6 +18,7 @@
package com.google.bitcoin.tools; package com.google.bitcoin.tools;
import com.google.bitcoin.core.*; import com.google.bitcoin.core.*;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.MnemonicCode; import com.google.bitcoin.crypto.MnemonicCode;
import com.google.bitcoin.crypto.MnemonicException; import com.google.bitcoin.crypto.MnemonicException;
@ -74,7 +75,7 @@ public class WalletTool {
private static OptionSet options; private static OptionSet options;
private static OptionSpec<Date> dateFlag; private static OptionSpec<Date> dateFlag;
private static OptionSpec<Integer> unixtimeFlag; private static OptionSpec<Integer> unixtimeFlag;
private static OptionSpec<String> seedFlag; private static OptionSpec<String> seedFlag, watchFlag;
private static NetworkParameters params; private static NetworkParameters params;
private static File walletFile; private static File walletFile;
@ -183,6 +184,7 @@ public class WalletTool {
parser.accepts("debuglog"); parser.accepts("debuglog");
OptionSpec<String> walletFileName = parser.accepts("wallet").withRequiredArg().defaultsTo("wallet"); OptionSpec<String> walletFileName = parser.accepts("wallet").withRequiredArg().defaultsTo("wallet");
seedFlag = parser.accepts("seed").withRequiredArg(); seedFlag = parser.accepts("seed").withRequiredArg();
watchFlag = parser.accepts("watchkey").withRequiredArg();
OptionSpec<NetworkEnum> netFlag = parser.accepts("net").withOptionalArg().ofType(NetworkEnum.class).defaultsTo(NetworkEnum.PROD); OptionSpec<NetworkEnum> netFlag = parser.accepts("net").withOptionalArg().ofType(NetworkEnum.class).defaultsTo(NetworkEnum.PROD);
dateFlag = parser.accepts("date").withRequiredArg().ofType(Date.class) dateFlag = parser.accepts("date").withRequiredArg().ofType(Date.class)
.withValuesConvertedBy(DateConverter.datePattern("yyyy/MM/dd")); .withValuesConvertedBy(DateConverter.datePattern("yyyy/MM/dd"));
@ -824,6 +826,9 @@ public class WalletTool {
seed = new DeterministicSeed(bits, creationTimeSecs); seed = new DeterministicSeed(bits, creationTimeSecs);
} }
wallet = Wallet.fromSeed(params, seed); wallet = Wallet.fromSeed(params, seed);
} else if (options.has(watchFlag)) {
DeterministicKey watchKey = DeterministicKey.deserializeB58(null, options.valueOf(watchFlag));
wallet = Wallet.fromWatchingKey(params, watchKey);
} else { } else {
wallet = new Wallet(params); wallet = new Wallet(params);
} }