3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-14 11:15: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;
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;
@ -212,6 +213,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
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) {
@ -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
* 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);
@ -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
* {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} as the parameter.
*/
public ECKey currentReceiveKey() {
public DeterministicKey currentReceiveKey() {
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
* 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();
@ -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
* {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} as the parameter.
*/
public ECKey freshReceiveKey() {
public DeterministicKey freshReceiveKey() {
return freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
}
@ -456,6 +461,20 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
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.
*/

View File

@ -219,7 +219,10 @@ public class DeterministicKey extends ECKey {
} 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);
}
}

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
* 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,
@ -135,19 +136,18 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
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);
}
@ -445,7 +445,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
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);

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
* 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.
*/

View File

@ -67,6 +67,14 @@ public class KeyChainGroup {
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;
@ -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
* 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);
}
@ -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
* 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);
@ -372,26 +380,27 @@ public class KeyChainGroup {
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);

View File

@ -19,10 +19,7 @@ package com.google.bitcoin.core;
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;
@ -1098,6 +1095,23 @@ public class WalletTest extends TestWithWallet {
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

View File

@ -230,7 +230,7 @@ public class DeterministicKeyChainTest {
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());
@ -241,7 +241,8 @@ public class DeterministicKeyChainTest {
// 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();
@ -254,7 +255,7 @@ public class DeterministicKeyChainTest {
@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");
}

View File

@ -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;
@ -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;
@ -183,6 +184,7 @@ public class WalletTool {
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"));
@ -824,6 +826,9 @@ public class WalletTool {
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);
}