diff --git a/core/src/main/java/org/bitcoinj/core/ECKey.java b/core/src/main/java/org/bitcoinj/core/ECKey.java index 200cace6..d3ffef93 100644 --- a/core/src/main/java/org/bitcoinj/core/ECKey.java +++ b/core/src/main/java/org/bitcoinj/core/ECKey.java @@ -401,6 +401,11 @@ public class ECKey implements EncryptableItem, Serializable { return priv != null; } + /** Returns true if this key is watch only, meaning it has a public key but no private key. */ + public boolean isWatching() { + return isPubKeyOnly() && !isEncrypted(); + } + /** * Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by the Bitcoin reference * implementation in its wallet storage format. diff --git a/core/src/main/java/org/bitcoinj/core/Wallet.java b/core/src/main/java/org/bitcoinj/core/Wallet.java index 0280df71..70ebc3f1 100644 --- a/core/src/main/java/org/bitcoinj/core/Wallet.java +++ b/core/src/main/java/org/bitcoinj/core/Wallet.java @@ -750,6 +750,23 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha } } + /** + * Returns whether this wallet consists entirely of watching keys (unencrypted keys with no private part). Mixed + * wallets are forbidden. + * + * @throws IllegalStateException + * if there are no keys, or if there is a mix between watching and non-watching keys. + */ + public boolean isWatching() { + keychainLock.lock(); + try { + maybeUpgradeToHD(); + return keychain.isWatching(); + } finally { + keychainLock.unlock(); + } + } + /** * Return true if we are watching this address. */ diff --git a/core/src/main/java/org/bitcoinj/wallet/BasicKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/BasicKeyChain.java index 75a2813b..2e44a462 100644 --- a/core/src/main/java/org/bitcoinj/wallet/BasicKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/BasicKeyChain.java @@ -47,6 +47,7 @@ public class BasicKeyChain implements EncryptableKeyChain { private final LinkedHashMap hashToKeys; private final LinkedHashMap pubkeyToKeys; @Nullable private final KeyCrypter keyCrypter; + private boolean isWatching; private final CopyOnWriteArrayList> listeners; @@ -167,6 +168,14 @@ public class BasicKeyChain implements EncryptableKeyChain { } private void importKeyLocked(ECKey key) { + if (hashToKeys.isEmpty()) { + isWatching = key.isWatching(); + } else { + if (key.isWatching() && !isWatching) + throw new IllegalArgumentException("Key is watching but chain is not"); + if (!key.isWatching() && isWatching) + throw new IllegalArgumentException("Key is not watching but chain is"); + } ECKey previousKey = pubkeyToKeys.put(ByteString.copyFrom(key.getPubKey()), key); hashToKeys.put(ByteString.copyFrom(key.getPubKeyHash()), key); checkState(previousKey == null); @@ -221,6 +230,21 @@ public class BasicKeyChain implements EncryptableKeyChain { return pubkeyToKeys.size(); } + /** + * Returns whether this chain consists entirely of watching keys (unencrypted keys with no private part). Mixed + * chains are forbidden. Null means the chain is empty. + */ + public Boolean isWatching() { + lock.lock(); + try { + if (hashToKeys.isEmpty()) + return null; + return isWatching; + } finally { + lock.unlock(); + } + } + /** * Removes the given key from the keychain. Be very careful with this - losing a private key destroys the * money associated with it. diff --git a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java index df3ae665..6677c7ef 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java @@ -628,6 +628,12 @@ public class DeterministicKeyChain implements EncryptableKeyChain { return getKeyByPath(ACCOUNT_ZERO_PATH); } + /** Returns true if this chain is watch only, meaning it has public keys but no private key. */ + public boolean isWatching() { + DeterministicKey key = getKeyByPath(ACCOUNT_ZERO_PATH); + return key.isWatching(); + } + @Override public int numKeys() { // We need to return here the total number of keys including the lookahead zone, not the number of keys we diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java index 80118538..a4e13d13 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java @@ -551,6 +551,30 @@ public class KeyChainGroup implements KeyBag { return keyCrypter != null; } + /** + * Returns whether this chain has only watching keys (unencrypted keys with no private part). Mixed chains are + * forbidden. + * + * @throws IllegalStateException + * if there are no keys, or if there is a mix between watching and non-watching keys. + */ + public boolean isWatching() { + Boolean basicChainIsWatching = basic.isWatching(); + Boolean deterministicChainIsWatching = !chains.isEmpty() ? getActiveKeyChain().isWatching() : null; + if (basicChainIsWatching == null && deterministicChainIsWatching == null) + throw new IllegalStateException("No keys"); + if (basicChainIsWatching == null && deterministicChainIsWatching != null) + return deterministicChainIsWatching; + if (basicChainIsWatching != null && deterministicChainIsWatching == null) + return basicChainIsWatching; + if (basicChainIsWatching == deterministicChainIsWatching) + return deterministicChainIsWatching; + if (basicChainIsWatching && !deterministicChainIsWatching) + throw new IllegalStateException("Basic chain is watching, deterministic chain is not"); + else + throw new IllegalStateException("Basic chain is not watching, deterministic chain is"); + } + /** Returns the key crypter or null if the group is not encrypted. */ @Nullable public KeyCrypter getKeyCrypter() { return keyCrypter; } diff --git a/core/src/test/java/org/bitcoinj/core/BloomFilterTest.java b/core/src/test/java/org/bitcoinj/core/BloomFilterTest.java index 790e5176..2a9e922f 100644 --- a/core/src/test/java/org/bitcoinj/core/BloomFilterTest.java +++ b/core/src/test/java/org/bitcoinj/core/BloomFilterTest.java @@ -77,7 +77,7 @@ public class BloomFilterTest { KeyChainGroup group = new KeyChainGroup(params); // Add a random key which happens to have been used in a recent generation - group.importKeys(privKey.getKey(), ECKey.fromPublicOnly(HEX.decode("03cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99"))); + group.importKeys(ECKey.fromPublicOnly(privKey.getKey().getPubKeyPoint()), ECKey.fromPublicOnly(HEX.decode("03cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99"))); Wallet wallet = new Wallet(context, group); wallet.commitTx(new Transaction(params, HEX.decode("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d038754030114062f503253482fffffffff01c05e559500000000232103cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99ac00000000"))); diff --git a/core/src/test/java/org/bitcoinj/core/WalletTest.java b/core/src/test/java/org/bitcoinj/core/WalletTest.java index afbb899e..fa81c4c5 100644 --- a/core/src/test/java/org/bitcoinj/core/WalletTest.java +++ b/core/src/test/java/org/bitcoinj/core/WalletTest.java @@ -1152,6 +1152,15 @@ public class WalletTest extends TestWithWallet { log.info(t2.toString(chain)); } + @Test + public void isWatching() { + assertFalse(wallet.isWatching()); + Wallet watchingWallet = Wallet.fromWatchingKey(params, wallet.getWatchingKey().dropPrivateBytes().dropParent()); + assertTrue(watchingWallet.isWatching()); + wallet.encrypt(PASSWORD1); + assertFalse(wallet.isWatching()); + } + @Test(expected = ECKey.MissingPrivateKeyException.class) public void watchingWallet() throws Exception { DeterministicKey watchKey = wallet.getWatchingKey(); diff --git a/core/src/test/java/org/bitcoinj/wallet/BasicKeyChainTest.java b/core/src/test/java/org/bitcoinj/wallet/BasicKeyChainTest.java index 13d03d85..f71f3a23 100644 --- a/core/src/test/java/org/bitcoinj/wallet/BasicKeyChainTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/BasicKeyChainTest.java @@ -148,6 +148,7 @@ public class BasicKeyChainTest { ECKey key = chain.findKeyFromPubKey(key1.getPubKey()); assertTrue(key.isEncrypted()); assertTrue(key.isPubKeyOnly()); + assertFalse(key.isWatching()); assertNull(key.getSecretBytes()); try { @@ -165,6 +166,7 @@ public class BasicKeyChainTest { key = chain.findKeyFromPubKey(key1.getPubKey()); assertFalse(key.isEncrypted()); assertFalse(key.isPubKeyOnly()); + assertFalse(key.isWatching()); key.getPrivKeyBytes(); } diff --git a/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java b/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java index eaf84a48..f5c958a2 100644 --- a/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java @@ -27,6 +27,7 @@ import org.junit.Test; import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.util.Arrays; +import java.math.BigInteger; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -582,4 +583,44 @@ public class KeyChainGroupTest { Address addr3 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertNotEquals(addr2, addr3); } + + @Test + public void isNotWatching() { + group = new KeyChainGroup(params); + final ECKey key = ECKey.fromPrivate(BigInteger.TEN); + group.importKeys(key); + assertFalse(group.isWatching()); + } + + @Test + public void isWatching() { + group = new KeyChainGroup( + params, + DeterministicKey + .deserializeB58( + "xpub69bjfJ91ikC5ghsqsVDHNq2dRGaV2HHVx7Y9LXi27LN9BWWAXPTQr4u8U3wAtap8bLdHdkqPpAcZmhMS5SnrMQC4ccaoBccFhh315P4UYzo", + params)); + final ECKey watchingKey = ECKey.fromPublicOnly(new ECKey().getPubKeyPoint()); + group.importKeys(watchingKey); + assertTrue(group.isWatching()); + } + + @Test(expected = IllegalStateException.class) + public void isWatchingNoKeys() { + group = new KeyChainGroup(params); + group.isWatching(); + } + + @Test(expected = IllegalStateException.class) + public void isWatchingMixedKeys() { + group = new KeyChainGroup( + params, + DeterministicKey + .deserializeB58( + "xpub69bjfJ91ikC5ghsqsVDHNq2dRGaV2HHVx7Y9LXi27LN9BWWAXPTQr4u8U3wAtap8bLdHdkqPpAcZmhMS5SnrMQC4ccaoBccFhh315P4UYzo", + params)); + final ECKey key = ECKey.fromPrivate(BigInteger.TEN); + group.importKeys(key); + group.isWatching(); + } }