Skip to content

Commit

Permalink
HD Wallets: support watching wallets in Wallet and wallet-tool.
Browse files Browse the repository at this point in the history
Also, respect includePrivateKeys flag for the seed in wallet.toString again.
  • Loading branch information
mikehearn committed May 29, 2014
1 parent c7f7fd2 commit 51b71a4
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 45 deletions.
29 changes: 24 additions & 5 deletions core/src/main/java/com/google/bitcoin/core/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package com.google.bitcoin.core;

import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
Expand Down Expand Up @@ -212,6 +213,10 @@ public static Wallet fromSeed(NetworkParameters params, DeterministicSeed 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.
/** For internal use only. */
public Wallet(NetworkParameters params, KeyChainGroup keyChainGroup) {
Expand Down Expand Up @@ -274,7 +279,7 @@ public NetworkParameters getNetworkParameters() {
* 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).
*/
public ECKey currentKey(KeyChain.KeyPurpose purpose) {
public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) {
lock.lock();
try {
return keychain.currentKey(purpose);
Expand All @@ -287,7 +292,7 @@ public ECKey currentKey(KeyChain.KeyPurpose purpose) {
* 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.
*/
public ECKey currentReceiveKey() {
public DeterministicKey currentReceiveKey() {
return currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
}

Expand All @@ -299,10 +304,10 @@ public ECKey currentReceiveKey() {
* 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.
*/
public ECKey freshKey(KeyChain.KeyPurpose purpose) {
public DeterministicKey freshKey(KeyChain.KeyPurpose purpose) {
lock.lock();
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
// and that's not quite so important, so we could coalesce for more performance.
saveNow();
Expand All @@ -316,7 +321,7 @@ public ECKey freshKey(KeyChain.KeyPurpose purpose) {
* 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.
*/
public ECKey freshReceiveKey() {
public DeterministicKey freshReceiveKey() {
return freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
}

Expand Down Expand Up @@ -456,6 +461,20 @@ public int getKeychainLookaheadSize() {
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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,10 @@ public ECDSASignature sign(Sha256Hash input, @Nullable KeyParameter aesKey) thro
} else {
// If it's not encrypted, derive the private via the parents.
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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
* {@link com.google.bitcoin.crypto.DeterministicKey#serializePubB58()}. The resulting "xpub..." string encodes
* 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)}
* (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
* {@link com.google.bitcoin.crypto.DeterministicKey} by adding support for serialization to and from protobufs,
Expand Down Expand Up @@ -135,19 +136,18 @@ public DeterministicKeyChain(DeterministicSeed seed) {
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
* 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.
*/
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) {
return new DeterministicKeyChain(accountKey);
}
Expand Down Expand Up @@ -445,7 +445,7 @@ public static List<DeterministicKeyChain> fromProtobuf(List<Protos.Key> keys, @N
if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH))
throw new UnreadableWalletException("Expecting account key but found key with path: " +
HDUtils.formatPath(accountKey.getPath()));
chain = DeterministicKeyChain.watch(accountKey);
chain = new DeterministicKeyChain(accountKey);
isWatchingAccountKey = true;
} else {
chain = new DeterministicKeyChain(seed, crypter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public DeterministicSeed(EncryptedData encryptedSeed, long creationTimeSeconds)
/**
* Constructs a seed from a BIP 39 mnemonic code. See {@link com.google.bitcoin.crypto.MnemonicCode} for more
* 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.
* @throws MnemonicException if there is a problem decoding the words.
*/
Expand Down
49 changes: 29 additions & 20 deletions core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ public KeyChainGroup(DeterministicSeed seed) {
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.
private KeyChainGroup(@Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains, @Nullable KeyCrypter crypter) {
this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
Expand All @@ -89,7 +97,7 @@ private void createAndActivateNewHDChain() {
* 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).
*/
public ECKey currentKey(KeyChain.KeyPurpose purpose) {
public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) {
final DeterministicKey current = currentKeys.get(purpose);
return current != null ? current : freshKey(purpose);
}
Expand All @@ -102,7 +110,7 @@ public ECKey currentKey(KeyChain.KeyPurpose purpose) {
* 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.
*/
public ECKey freshKey(KeyChain.KeyPurpose purpose) {
public DeterministicKey freshKey(KeyChain.KeyPurpose purpose) {
DeterministicKeyChain chain = getActiveKeyChain();
DeterministicKey key = chain.getKey(purpose); // Always returns the next key along the key chain.
currentKeys.put(purpose, key);
Expand Down Expand Up @@ -372,26 +380,27 @@ public String toString(@Nullable NetworkParameters params, boolean includePrivat
for (ECKey key : basic.getKeys())
formatKeyWithAddress(params, includePrivateKeys, key, builder);
}
final String newline = String.format("%n");
for (DeterministicKeyChain chain : chains) {
DeterministicSeed seed = chain.getSeed();
if (seed != null && !seed.isEncrypted()) {
final List<String> words = seed.toMnemonicCode();
builder.append("Seed as words: ");
builder.append(Joiner.on(' ').join(words));
builder.append(newline);
builder.append("Seed as hex: ");
builder.append(seed.toHexString());
builder.append(newline);
builder.append("Seed birthday: ");
builder.append(seed.getCreationTimeSeconds());
builder.append(" [" + new Date(seed.getCreationTimeSeconds() * 1000) + "]");
builder.append(newline);
builder.append(newline);
} else {
builder.append("Seed is encrypted");
builder.append(newline);
builder.append(newline);
if (seed != null) {
if (seed.isEncrypted()) {
builder.append(String.format("Seed is encrypted%n"));
} else if (includePrivateKeys) {
final List<String> words = seed.toMnemonicCode();
builder.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)));
}
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/
// unintuitive result where the watching key in a watching wallet is not the one it was created with
// 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()));
}
for (ECKey key : chain.getKeys())
formatKeyWithAddress(params, includePrivateKeys, key, builder);
Expand Down
22 changes: 18 additions & 4 deletions core/src/test/java/com/google/bitcoin/core/WalletTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@

import com.google.bitcoin.core.Transaction.SigHash;
import com.google.bitcoin.core.Wallet.SendRequest;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.crypto.TransactionSignature;
import com.google.bitcoin.crypto.*;
import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.testing.FakeTxBuilder;
import com.google.bitcoin.testing.MockTransactionBroadcaster;
Expand Down Expand Up @@ -1098,6 +1095,23 @@ public void pubkeyOnlyScripts() throws Exception {
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
public void watchingScripts() throws Exception {
// Verify that pending transactions to watched addresses are relevant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public void watchingChain() throws UnreadableWalletException {
final String pub58 = watchingKey.serializePubB58();
assertEquals("xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi", pub58);
watchingKey = DeterministicKey.deserializeB58(null, pub58);
chain = DeterministicKeyChain.watch(watchingKey);
chain = new DeterministicKeyChain(watchingKey);
chain.setLookaheadSize(10);

assertEquals(key1.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint());
Expand All @@ -241,7 +241,8 @@ public void watchingChain() throws UnreadableWalletException {
// Can't sign with a key from a watching chain.
key.sign(Sha256Hash.ZERO_HASH);
fail();
} catch (IllegalStateException e) {
} catch (ECKey.MissingPrivateKeyException e) {
// Ignored.
}
// Test we can serialize and deserialize a watching chain OK.
List<Protos.Key> serialization = chain.serializeToProtobuf();
Expand All @@ -254,7 +255,7 @@ public void watchingChain() throws UnreadableWalletException {
@Test(expected = IllegalStateException.class)
public void watchingCannotEncrypt() throws Exception {
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");
}

Expand Down
7 changes: 6 additions & 1 deletion tools/src/main/java/com/google/bitcoin/tools/WalletTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package com.google.bitcoin.tools;

import com.google.bitcoin.core.*;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.MnemonicCode;
import com.google.bitcoin.crypto.MnemonicException;
Expand Down Expand Up @@ -74,7 +75,7 @@ public class WalletTool {
private static OptionSet options;
private static OptionSpec<Date> dateFlag;
private static OptionSpec<Integer> unixtimeFlag;
private static OptionSpec<String> seedFlag;
private static OptionSpec<String> seedFlag, watchFlag;

private static NetworkParameters params;
private static File walletFile;
Expand Down Expand Up @@ -183,6 +184,7 @@ public static void main(String[] args) throws Exception {
parser.accepts("debuglog");
OptionSpec<String> walletFileName = parser.accepts("wallet").withRequiredArg().defaultsTo("wallet");
seedFlag = parser.accepts("seed").withRequiredArg();
watchFlag = parser.accepts("watchkey").withRequiredArg();
OptionSpec<NetworkEnum> netFlag = parser.accepts("net").withOptionalArg().ofType(NetworkEnum.class).defaultsTo(NetworkEnum.PROD);
dateFlag = parser.accepts("date").withRequiredArg().ofType(Date.class)
.withValuesConvertedBy(DateConverter.datePattern("yyyy/MM/dd"));
Expand Down Expand Up @@ -824,6 +826,9 @@ private static void createWallet(OptionSet options, NetworkParameters params, Fi
seed = new DeterministicSeed(bits, creationTimeSecs);
}
wallet = Wallet.fromSeed(params, seed);
} else if (options.has(watchFlag)) {
DeterministicKey watchKey = DeterministicKey.deserializeB58(null, options.valueOf(watchFlag));
wallet = Wallet.fromWatchingKey(params, watchKey);
} else {
wallet = new Wallet(params);
}
Expand Down

0 comments on commit 51b71a4

Please sign in to comment.