Introduce UnreadableWalletException and make WalletProtobufSerializer throw it in all cases where there's a problem reading the wallet. Resolves issue 415. Resolves issue 416.

This commit is contained in:
Mike Hearn 2013-07-11 16:03:33 +02:00
parent be89be6f43
commit 99f32b16af
6 changed files with 84 additions and 71 deletions

View file

@ -21,6 +21,7 @@ import com.google.bitcoin.core.WalletTransaction.Pool;
import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt; import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.store.WalletProtobufSerializer; import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.utils.ListenerRegistration; import com.google.bitcoin.utils.ListenerRegistration;
import com.google.bitcoin.utils.Threading; 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. * Returns a wallet deserialized from the given file.
*/ */
public static Wallet loadFromFile(File f) throws IOException { public static Wallet loadFromFile(File f) throws UnreadableWalletException {
FileInputStream stream = new FileInputStream(f);
try { try {
return loadFromFileStream(stream); FileInputStream stream = null;
} finally { try {
stream.close(); 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. * Returns a wallet deserialized from the given input stream.
*/ */
public static Wallet loadFromFileStream(InputStream stream) throws IOException { public static Wallet loadFromFileStream(InputStream stream) throws UnreadableWalletException {
// Determine what kind of wallet stream this is: Java Serialization or protobuf format. Wallet wallet = new WalletProtobufSerializer().readWallet(stream);
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);
}
if (!wallet.isConsistent()) { if (!wallet.isConsistent()) {
log.error("Loaded an inconsistent wallet"); log.error("Loaded an inconsistent wallet");
} }

View file

@ -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);
}
}

View file

@ -16,6 +16,18 @@
package com.google.bitcoin.store; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -27,19 +39,6 @@ import java.util.HashMap;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map; 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; 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 * <p>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 * 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.</p>
*
* <p>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.</p>
*
* @throws UnreadableWalletException thrown in various error conditions (see description).
*/ */
public Wallet readWallet(InputStream input) throws IOException { public Wallet readWallet(InputStream input) throws UnreadableWalletException {
Protos.Wallet walletProto = parseToProto(input); 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)); // 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 * <p>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 * 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.</p>
* *
* @throws IOException if there is a problem reading the stream. * <p>A wallet can be unreadable for various reasons, such as inability to open the file, corrupt data, internally
* @throws IllegalArgumentException if the wallet is corrupt. * 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.</p>
*
* @throws UnreadableWalletException thrown in various error conditions (see description).
*/ */
public void readWallet(Protos.Wallet walletProto, Wallet wallet) throws IOException { public void readWallet(Protos.Wallet walletProto, Wallet wallet) throws UnreadableWalletException {
// TODO: This method should throw more specific exception types than IllegalArgumentException.
// Read the scrypt parameters that specify how encryption and decryption is performed. // Read the scrypt parameters that specify how encryption and decryption is performed.
if (walletProto.hasEncryptionParameters()) { if (walletProto.hasEncryptionParameters()) {
Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters(); Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
@ -335,7 +347,7 @@ public class WalletProtobufSerializer {
// Read all keys // Read all keys
for (Protos.Key keyProto : walletProto.getKeyList()) { for (Protos.Key keyProto : walletProto.getKeyList()) {
if (!(keyProto.getType() == Protos.Key.Type.ORIGINAL || keyProto.getType() == Protos.Key.Type.ENCRYPTED_SCRYPT_AES)) { 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; byte[] privKey = keyProto.hasPrivateKey() ? keyProto.getPrivateKey().toByteArray() : null;
@ -394,14 +406,14 @@ public class WalletProtobufSerializer {
txMap.clear(); txMap.clear();
} }
private static void loadExtensions(Wallet wallet, Protos.Wallet walletProto) { private static void loadExtensions(Wallet wallet, Protos.Wallet walletProto) throws UnreadableWalletException {
final Map<String, WalletExtension> extensions = wallet.getExtensions(); final Map<String, WalletExtension> extensions = wallet.getExtensions();
for (Protos.Extension extProto : walletProto.getExtensionList()) { for (Protos.Extension extProto : walletProto.getExtensionList()) {
String id = extProto.getId(); String id = extProto.getId();
WalletExtension extension = extensions.get(id); WalletExtension extension = extensions.get(id);
if (extension == null) { if (extension == null) {
if (extProto.getMandatory()) { if (extProto.getMandatory()) {
throw new IllegalArgumentException("Unknown mandatory extension in wallet: " + id); throw new UnreadableWalletException("Unknown mandatory extension in wallet: " + id);
} }
} else { } else {
log.info("Loading wallet extension {}", id); log.info("Loading wallet extension {}", id);
@ -409,7 +421,7 @@ public class WalletProtobufSerializer {
extension.deserializeWalletExtension(wallet, extProto.getData().toByteArray()); extension.deserializeWalletExtension(wallet, extProto.getData().toByteArray());
} catch (Exception e) { } catch (Exception e) {
if (extProto.getMandatory()) 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); 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); Transaction tx = new Transaction(params);
if (txProto.hasUpdatedAt()) { if (txProto.hasUpdatedAt()) {
tx.setUpdateTime(new Date(txProto.getUpdatedAt())); tx.setUpdateTime(new Date(txProto.getUpdatedAt()));
@ -460,14 +472,14 @@ public class WalletProtobufSerializer {
// Transaction should now be complete. // Transaction should now be complete.
Sha256Hash protoHash = byteStringToHash(txProto.getHash()); Sha256Hash protoHash = byteStringToHash(txProto.getHash());
Preconditions.checkState(tx.getHash().equals(protoHash), if (!tx.getHash().equals(protoHash))
"Transaction did not deserialize completely: %s vs %s", tx.getHash(), protoHash); throw new UnreadableWalletException(String.format("Transaction did not deserialize completely: %s vs %s", tx.getHash(), protoHash));
Preconditions.checkState(!txMap.containsKey(txProto.getHash()), if (txMap.containsKey(txProto.getHash()))
"Wallet contained duplicate transaction %s", byteStringToHash(txProto.getHash())); throw new UnreadableWalletException("Wallet contained duplicate transaction " + byteStringToHash(txProto.getHash()));
txMap.put(txProto.getHash(), tx); 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()); Transaction tx = txMap.get(txProto.getHash());
WalletTransaction.Pool pool = WalletTransaction.Pool.valueOf(txProto.getPool().getNumber()); WalletTransaction.Pool pool = WalletTransaction.Pool.valueOf(txProto.getPool().getNumber());
if (pool == WalletTransaction.Pool.INACTIVE || pool == WalletTransaction.Pool.PENDING_INACTIVE) { if (pool == WalletTransaction.Pool.INACTIVE || pool == WalletTransaction.Pool.PENDING_INACTIVE) {
@ -483,9 +495,10 @@ public class WalletProtobufSerializer {
if (transactionOutput.hasSpentByTransactionHash()) { if (transactionOutput.hasSpentByTransactionHash()) {
final ByteString spentByTransactionHash = transactionOutput.getSpentByTransactionHash(); final ByteString spentByTransactionHash = transactionOutput.getSpentByTransactionHash();
Transaction spendingTx = txMap.get(spentByTransactionHash); Transaction spendingTx = txMap.get(spentByTransactionHash);
if (spendingTx == null) if (spendingTx == null) {
throw new IllegalArgumentException(String.format("Could not connect %s to %s", throw new UnreadableWalletException(String.format("Could not connect %s to %s",
tx.getHashAsString(), byteStringToHash(spentByTransactionHash))); tx.getHashAsString(), byteStringToHash(spentByTransactionHash)));
}
final int spendingIndex = transactionOutput.getSpentByTransactionIndex(); final int spendingIndex = transactionOutput.getSpentByTransactionIndex();
TransactionInput input = checkNotNull(spendingTx.getInput(spendingIndex)); TransactionInput input = checkNotNull(spendingTx.getInput(spendingIndex));
input.connect(output); input.connect(output);
@ -502,7 +515,7 @@ public class WalletProtobufSerializer {
} }
private void readConfidence(Transaction tx, Protos.TransactionConfidence confidenceProto, 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. // 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 the tx has an unknown type of confidence, ignore.
if (!confidenceProto.hasType()) { if (!confidenceProto.hasType()) {
@ -561,7 +574,7 @@ public class WalletProtobufSerializer {
try { try {
ip = InetAddress.getByAddress(proto.getIpAddress().toByteArray()); ip = InetAddress.getByAddress(proto.getIpAddress().toByteArray());
} catch (UnknownHostException e) { } 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(); int port = proto.getPort();
PeerAddress address = new PeerAddress(ip, port); PeerAddress address = new PeerAddress(ip, port);

View file

@ -275,8 +275,8 @@ public class WalletProtobufSerializerTest {
try { try {
new WalletProtobufSerializer().readWallet(proto, wallet2); new WalletProtobufSerializer().readWallet(proto, wallet2);
fail(); fail();
} catch (IllegalArgumentException e) { } catch (UnreadableWalletException e) {
// Expected. assertTrue(e.getMessage().contains("mandatory"));
} }
Wallet wallet3 = new Wallet(params); Wallet wallet3 = new Wallet(params);
// This time it works. // This time it works.

View file

@ -24,13 +24,13 @@ import com.google.bitcoin.params.RegTestParams;
import com.google.bitcoin.params.TestNet3Params; import com.google.bitcoin.params.TestNet3Params;
import com.google.bitcoin.store.BlockStore; import com.google.bitcoin.store.BlockStore;
import com.google.bitcoin.store.SPVBlockStore; import com.google.bitcoin.store.SPVBlockStore;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.utils.BriefLogFormatter; import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.InetAddress; import java.net.InetAddress;
@ -97,7 +97,7 @@ public class PingService {
// Wipe the wallet if the chain file was deleted. // Wipe the wallet if the chain file was deleted.
if (walletFile.exists() && chainFile.exists()) if (walletFile.exists() && chainFile.exists())
w = Wallet.loadFromFile(walletFile); w = Wallet.loadFromFile(walletFile);
} catch (IOException e) { } catch (UnreadableWalletException e) {
System.err.println("Couldn't load wallet: " + e); System.err.println("Couldn't load wallet: " + e);
// Fall through. // Fall through.
} }

View file

@ -22,6 +22,7 @@ import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.TestNet3Params; import com.google.bitcoin.params.TestNet3Params;
import com.google.bitcoin.store.H2FullPrunedBlockStore; import com.google.bitcoin.store.H2FullPrunedBlockStore;
import com.google.bitcoin.store.SPVBlockStore; import com.google.bitcoin.store.SPVBlockStore;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.utils.BriefLogFormatter; import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.spongycastle.util.encoders.Hex; import org.spongycastle.util.encoders.Hex;
@ -34,7 +35,6 @@ import java.awt.datatransfer.StringSelection;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
@ -125,7 +125,7 @@ public class ToyWallet {
walletFile = new File("toy.wallet"); walletFile = new File("toy.wallet");
try { try {
wallet = Wallet.loadFromFile(walletFile); wallet = Wallet.loadFromFile(walletFile);
} catch (IOException e) { } catch (UnreadableWalletException e) {
wallet = new Wallet(params); wallet = new Wallet(params);
// Allow user to specify the first key on the command line as: // Allow user to specify the first key on the command line as: