From 99f32b16affab716b67fe338b831c124adc7064a Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 11 Jul 2013 16:03:33 +0200 Subject: [PATCH] Introduce UnreadableWalletException and make WalletProtobufSerializer throw it in all cases where there's a problem reading the wallet. Resolves issue 415. Resolves issue 416. --- .../java/com/google/bitcoin/core/Wallet.java | 41 +++------ .../store/UnreadableWalletException.java | 15 ++++ .../store/WalletProtobufSerializer.java | 87 +++++++++++-------- .../store/WalletProtobufSerializerTest.java | 4 +- .../google/bitcoin/examples/PingService.java | 4 +- .../bitcoin/examples/toywallet/ToyWallet.java | 4 +- 6 files changed, 84 insertions(+), 71 deletions(-) create mode 100644 core/src/main/java/com/google/bitcoin/store/UnreadableWalletException.java diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index a70264148..72e9493c7 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -21,6 +21,7 @@ import com.google.bitcoin.core.WalletTransaction.Pool; import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.crypto.KeyCrypterScrypt; +import com.google.bitcoin.store.UnreadableWalletException; import com.google.bitcoin.store.WalletProtobufSerializer; import com.google.bitcoin.utils.ListenerRegistration; import com.google.bitcoin.utils.Threading; @@ -511,12 +512,17 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi /** * Returns a wallet deserialized from the given file. */ - public static Wallet loadFromFile(File f) throws IOException { - FileInputStream stream = new FileInputStream(f); + public static Wallet loadFromFile(File f) throws UnreadableWalletException { try { - return loadFromFileStream(stream); - } finally { - stream.close(); + FileInputStream stream = null; + try { + stream = new FileInputStream(f); + return loadFromFileStream(stream); + } finally { + if (stream != null) stream.close(); + } + } catch (IOException e) { + throw new UnreadableWalletException("Could not open file", e); } } @@ -568,29 +574,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi /** * Returns a wallet deserialized from the given input stream. */ - public static Wallet loadFromFileStream(InputStream stream) throws IOException { - // Determine what kind of wallet stream this is: Java Serialization or protobuf format. - stream = new BufferedInputStream(stream); - stream.mark(100); - boolean serialization = stream.read() == 0xac && stream.read() == 0xed; - stream.reset(); - - Wallet wallet; - - if (serialization) { - ObjectInputStream ois = null; - try { - ois = new ObjectInputStream(stream); - wallet = (Wallet) ois.readObject(); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } finally { - if (ois != null) ois.close(); - } - } else { - wallet = new WalletProtobufSerializer().readWallet(stream); - } - + public static Wallet loadFromFileStream(InputStream stream) throws UnreadableWalletException { + Wallet wallet = new WalletProtobufSerializer().readWallet(stream); if (!wallet.isConsistent()) { log.error("Loaded an inconsistent wallet"); } diff --git a/core/src/main/java/com/google/bitcoin/store/UnreadableWalletException.java b/core/src/main/java/com/google/bitcoin/store/UnreadableWalletException.java new file mode 100644 index 000000000..43cba410a --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/store/UnreadableWalletException.java @@ -0,0 +1,15 @@ +package com.google.bitcoin.store; + +/** + * Thrown by the {@link WalletProtobufSerializer} when the serialized protocol buffer is either corrupted, + * internally inconsistent or appears to be from the future. + */ +public class UnreadableWalletException extends Exception { + public UnreadableWalletException(String s) { + super(s); + } + + public UnreadableWalletException(String s, Throwable t) { + super(s, t); + } +} diff --git a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java index f62cf2117..e9df066c1 100644 --- a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java +++ b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java @@ -16,6 +16,18 @@ package com.google.bitcoin.store; +import com.google.bitcoin.core.*; +import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; +import com.google.bitcoin.crypto.EncryptedPrivateKey; +import com.google.bitcoin.crypto.KeyCrypter; +import com.google.bitcoin.crypto.KeyCrypterScrypt; +import com.google.protobuf.ByteString; +import com.google.protobuf.TextFormat; +import org.bitcoinj.wallet.Protos; +import org.bitcoinj.wallet.Protos.Wallet.EncryptionType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -27,19 +39,6 @@ import java.util.HashMap; import java.util.ListIterator; import java.util.Map; -import com.google.bitcoin.core.*; -import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; -import com.google.bitcoin.crypto.EncryptedPrivateKey; -import com.google.bitcoin.crypto.KeyCrypter; -import com.google.bitcoin.crypto.KeyCrypterScrypt; -import com.google.common.base.Preconditions; -import com.google.protobuf.ByteString; -import com.google.protobuf.TextFormat; -import org.bitcoinj.wallet.Protos; -import org.bitcoinj.wallet.Protos.Wallet.EncryptionType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import static com.google.common.base.Preconditions.checkNotNull; /** @@ -297,12 +296,23 @@ public class WalletProtobufSerializer { } /** - * Parses a wallet from the given stream, using the provided Wallet instance to load data into. This is primarily + *

Parses a wallet from the given stream, using the provided Wallet instance to load data into. This is primarily * used when you want to register extensions. Data in the proto will be added into the wallet where applicable and - * overwrite where not. + * overwrite where not.

+ * + *

A wallet can be unreadable for various reasons, such as inability to open the file, corrupt data, internally + * inconsistent data, a wallet extension marked as mandatory that cannot be handled and so on. You should always + * handle {@link UnreadableWalletException} and communicate failure to the user in an appropriate manner.

+ * + * @throws UnreadableWalletException thrown in various error conditions (see description). */ - public Wallet readWallet(InputStream input) throws IOException { - Protos.Wallet walletProto = parseToProto(input); + public Wallet readWallet(InputStream input) throws UnreadableWalletException { + Protos.Wallet walletProto = null; + try { + walletProto = parseToProto(input); + } catch (IOException e) { + throw new UnreadableWalletException("Could not load wallet file", e); + } // System.out.println(TextFormat.printToString(walletProto)); @@ -313,15 +323,17 @@ public class WalletProtobufSerializer { } /** - * Loads wallet data from the given protocol buffer and inserts it into the given Wallet object. This is primarily + *

Loads wallet data from the given protocol buffer and inserts it into the given Wallet object. This is primarily * useful when you wish to pre-register extension objects. Note that if loading fails the provided Wallet object - * may be in an indeterminate state and should be thrown away. + * may be in an indeterminate state and should be thrown away.

* - * @throws IOException if there is a problem reading the stream. - * @throws IllegalArgumentException if the wallet is corrupt. + *

A wallet can be unreadable for various reasons, such as inability to open the file, corrupt data, internally + * inconsistent data, a wallet extension marked as mandatory that cannot be handled and so on. You should always + * handle {@link UnreadableWalletException} and communicate failure to the user in an appropriate manner.

+ * + * @throws UnreadableWalletException thrown in various error conditions (see description). */ - public void readWallet(Protos.Wallet walletProto, Wallet wallet) throws IOException { - // TODO: This method should throw more specific exception types than IllegalArgumentException. + public void readWallet(Protos.Wallet walletProto, Wallet wallet) throws UnreadableWalletException { // Read the scrypt parameters that specify how encryption and decryption is performed. if (walletProto.hasEncryptionParameters()) { Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters(); @@ -335,7 +347,7 @@ public class WalletProtobufSerializer { // Read all keys for (Protos.Key keyProto : walletProto.getKeyList()) { if (!(keyProto.getType() == Protos.Key.Type.ORIGINAL || keyProto.getType() == Protos.Key.Type.ENCRYPTED_SCRYPT_AES)) { - throw new IllegalArgumentException("Unknown key type in wallet, type = " + keyProto.getType()); + throw new UnreadableWalletException("Unknown key type in wallet, type = " + keyProto.getType()); } byte[] privKey = keyProto.hasPrivateKey() ? keyProto.getPrivateKey().toByteArray() : null; @@ -394,14 +406,14 @@ public class WalletProtobufSerializer { txMap.clear(); } - private static void loadExtensions(Wallet wallet, Protos.Wallet walletProto) { + private static void loadExtensions(Wallet wallet, Protos.Wallet walletProto) throws UnreadableWalletException { final Map extensions = wallet.getExtensions(); for (Protos.Extension extProto : walletProto.getExtensionList()) { String id = extProto.getId(); WalletExtension extension = extensions.get(id); if (extension == null) { if (extProto.getMandatory()) { - throw new IllegalArgumentException("Unknown mandatory extension in wallet: " + id); + throw new UnreadableWalletException("Unknown mandatory extension in wallet: " + id); } } else { log.info("Loading wallet extension {}", id); @@ -409,7 +421,7 @@ public class WalletProtobufSerializer { extension.deserializeWalletExtension(wallet, extProto.getData().toByteArray()); } catch (Exception e) { if (extProto.getMandatory()) - throw new IllegalArgumentException("Unknown mandatory extension in wallet: " + id); + throw new UnreadableWalletException("Could not parse mandatory extension in wallet: " + id); } } } @@ -424,7 +436,7 @@ public class WalletProtobufSerializer { return Protos.Wallet.parseFrom(input); } - private void readTransaction(Protos.Transaction txProto, NetworkParameters params) { + private void readTransaction(Protos.Transaction txProto, NetworkParameters params) throws UnreadableWalletException { Transaction tx = new Transaction(params); if (txProto.hasUpdatedAt()) { tx.setUpdateTime(new Date(txProto.getUpdatedAt())); @@ -460,14 +472,14 @@ public class WalletProtobufSerializer { // Transaction should now be complete. Sha256Hash protoHash = byteStringToHash(txProto.getHash()); - Preconditions.checkState(tx.getHash().equals(protoHash), - "Transaction did not deserialize completely: %s vs %s", tx.getHash(), protoHash); - Preconditions.checkState(!txMap.containsKey(txProto.getHash()), - "Wallet contained duplicate transaction %s", byteStringToHash(txProto.getHash())); + if (!tx.getHash().equals(protoHash)) + throw new UnreadableWalletException(String.format("Transaction did not deserialize completely: %s vs %s", tx.getHash(), protoHash)); + if (txMap.containsKey(txProto.getHash())) + throw new UnreadableWalletException("Wallet contained duplicate transaction " + byteStringToHash(txProto.getHash())); txMap.put(txProto.getHash(), tx); } - private WalletTransaction connectTransactionOutputs(org.bitcoinj.wallet.Protos.Transaction txProto) { + private WalletTransaction connectTransactionOutputs(org.bitcoinj.wallet.Protos.Transaction txProto) throws UnreadableWalletException { Transaction tx = txMap.get(txProto.getHash()); WalletTransaction.Pool pool = WalletTransaction.Pool.valueOf(txProto.getPool().getNumber()); if (pool == WalletTransaction.Pool.INACTIVE || pool == WalletTransaction.Pool.PENDING_INACTIVE) { @@ -483,9 +495,10 @@ public class WalletProtobufSerializer { if (transactionOutput.hasSpentByTransactionHash()) { final ByteString spentByTransactionHash = transactionOutput.getSpentByTransactionHash(); Transaction spendingTx = txMap.get(spentByTransactionHash); - if (spendingTx == null) - throw new IllegalArgumentException(String.format("Could not connect %s to %s", + if (spendingTx == null) { + throw new UnreadableWalletException(String.format("Could not connect %s to %s", tx.getHashAsString(), byteStringToHash(spentByTransactionHash))); + } final int spendingIndex = transactionOutput.getSpentByTransactionIndex(); TransactionInput input = checkNotNull(spendingTx.getInput(spendingIndex)); input.connect(output); @@ -502,7 +515,7 @@ public class WalletProtobufSerializer { } private void readConfidence(Transaction tx, Protos.TransactionConfidence confidenceProto, - TransactionConfidence confidence) { + TransactionConfidence confidence) throws UnreadableWalletException { // We are lenient here because tx confidence is not an essential part of the wallet. // If the tx has an unknown type of confidence, ignore. if (!confidenceProto.hasType()) { @@ -561,7 +574,7 @@ public class WalletProtobufSerializer { try { ip = InetAddress.getByAddress(proto.getIpAddress().toByteArray()); } catch (UnknownHostException e) { - throw new RuntimeException(e); // IP address is of invalid length. + throw new UnreadableWalletException("Peer IP address does not have the right length", e); } int port = proto.getPort(); PeerAddress address = new PeerAddress(ip, port); diff --git a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java index dcb1a2150..1a20afd8c 100644 --- a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java +++ b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java @@ -275,8 +275,8 @@ public class WalletProtobufSerializerTest { try { new WalletProtobufSerializer().readWallet(proto, wallet2); fail(); - } catch (IllegalArgumentException e) { - // Expected. + } catch (UnreadableWalletException e) { + assertTrue(e.getMessage().contains("mandatory")); } Wallet wallet3 = new Wallet(params); // This time it works. diff --git a/examples/src/main/java/com/google/bitcoin/examples/PingService.java b/examples/src/main/java/com/google/bitcoin/examples/PingService.java index b3edf358a..83f1ac628 100644 --- a/examples/src/main/java/com/google/bitcoin/examples/PingService.java +++ b/examples/src/main/java/com/google/bitcoin/examples/PingService.java @@ -24,13 +24,13 @@ import com.google.bitcoin.params.RegTestParams; import com.google.bitcoin.params.TestNet3Params; import com.google.bitcoin.store.BlockStore; import com.google.bitcoin.store.SPVBlockStore; +import com.google.bitcoin.store.UnreadableWalletException; import com.google.bitcoin.utils.BriefLogFormatter; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import java.io.File; import java.io.FileInputStream; -import java.io.IOException; import java.math.BigInteger; import java.net.InetAddress; @@ -97,7 +97,7 @@ public class PingService { // Wipe the wallet if the chain file was deleted. if (walletFile.exists() && chainFile.exists()) w = Wallet.loadFromFile(walletFile); - } catch (IOException e) { + } catch (UnreadableWalletException e) { System.err.println("Couldn't load wallet: " + e); // Fall through. } diff --git a/examples/src/main/java/com/google/bitcoin/examples/toywallet/ToyWallet.java b/examples/src/main/java/com/google/bitcoin/examples/toywallet/ToyWallet.java index ef39b76e3..3b91cdfd1 100644 --- a/examples/src/main/java/com/google/bitcoin/examples/toywallet/ToyWallet.java +++ b/examples/src/main/java/com/google/bitcoin/examples/toywallet/ToyWallet.java @@ -22,6 +22,7 @@ import com.google.bitcoin.params.MainNetParams; import com.google.bitcoin.params.TestNet3Params; import com.google.bitcoin.store.H2FullPrunedBlockStore; import com.google.bitcoin.store.SPVBlockStore; +import com.google.bitcoin.store.UnreadableWalletException; import com.google.bitcoin.utils.BriefLogFormatter; import com.google.common.collect.Lists; import org.spongycastle.util.encoders.Hex; @@ -34,7 +35,6 @@ import java.awt.datatransfer.StringSelection; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; -import java.io.IOException; import java.math.BigInteger; import java.util.Date; import java.util.LinkedList; @@ -125,7 +125,7 @@ public class ToyWallet { walletFile = new File("toy.wallet"); try { wallet = Wallet.loadFromFile(walletFile); - } catch (IOException e) { + } catch (UnreadableWalletException e) { wallet = new Wallet(params); // Allow user to specify the first key on the command line as: