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:
parent
c7f7fd29e0
commit
51b71a4363
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user