/** * Copyright 2012 Google Inc. * Copyright 2014 Andreas Schildbach * Copyright 2014 John L. Jegutanis * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.mygeopay.core.wallet; import com.mygeopay.core.coins.CoinID; import com.mygeopay.core.coins.CoinType; import com.mygeopay.core.network.AddressStatus; import com.mygeopay.core.protos.Protos; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionConfidence.ConfidenceType; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.crypto.KeyCrypter; import org.bitcoinj.params.Networks; import org.bitcoinj.store.UnreadableWalletException; import org.bitcoinj.wallet.WalletTransaction; import com.google.protobuf.ByteString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.ListIterator; import java.util.Map; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; import static org.bitcoinj.params.Networks.Family.PEERCOIN; import static org.bitcoinj.params.Networks.Family.NUBITS; import static org.bitcoinj.params.Networks.Family.REDDCOIN; /** * @author John L. Jegutanis */ public class WalletPocketProtobufSerializer { private static final Logger log = LoggerFactory.getLogger(WalletPocketProtobufSerializer.class); // Used for de-serialization protected Map<ByteString, Transaction> txMap = new HashMap<ByteString, Transaction>(); public static Protos.WalletPocket toProtobuf(WalletPocketHD pocket) { Protos.WalletPocket.Builder walletBuilder = Protos.WalletPocket.newBuilder(); walletBuilder.setNetworkIdentifier(pocket.getCoinType().getId()); if (pocket.getDescription() != null) { walletBuilder.setDescription(pocket.getDescription()); } if (pocket.getId() != null) { walletBuilder.setId(pocket.getId()); } for (AddressStatus status : pocket.getAllAddressStatus()) { Protos.AddressStatus.Builder addressStatus = Protos.AddressStatus.newBuilder(); if (status.getStatus() == null) { continue; // Don't serialize null statuses } addressStatus.setAddress(status.getAddress().toString()); addressStatus.setStatus(status.getStatus()); // TODO check null values walletBuilder.addAddressStatus(addressStatus.build()); } for (WalletTransaction wtx : pocket.getWalletTransactions()) { Protos.Transaction txProto = makeTxProto(wtx); walletBuilder.addTransaction(txProto); } walletBuilder.addAllKey(pocket.serializeKeychainToProtobuf()); // Populate the lastSeenBlockHash field. if (pocket.getLastBlockSeenHash() != null) { walletBuilder.setLastSeenBlockHash(hashToByteString(pocket.getLastBlockSeenHash())); walletBuilder.setLastSeenBlockHeight(pocket.getLastBlockSeenHeight()); } if (pocket.getLastBlockSeenTimeSecs() > 0) { walletBuilder.setLastSeenBlockTimeSecs(pocket.getLastBlockSeenTimeSecs()); } return walletBuilder.build(); } private static Protos.Transaction makeTxProto(WalletTransaction wtx) { Transaction tx = wtx.getTransaction(); Protos.Transaction.Builder txBuilder = Protos.Transaction.newBuilder(); txBuilder.setPool(getProtoPool(wtx)) .setHash(hashToByteString(tx.getHash())) .setVersion((int) tx.getVersion()); if (Networks.isFamily(tx.getParams(), PEERCOIN, NUBITS, REDDCOIN)) { txBuilder.setTime((int) tx.getTime()); } if (Networks.isFamily(tx.getParams(), NUBITS)) { txBuilder.setTokenId(tx.getTokenId()); } if (tx.getUpdateTime() != null) { txBuilder.setUpdatedAt(tx.getUpdateTime().getTime()); } if (tx.getLockTime() > 0) { txBuilder.setLockTime((int)tx.getLockTime()); } // Handle inputs. for (TransactionInput input : tx.getInputs()) { Protos.TransactionInput.Builder inputBuilder = Protos.TransactionInput.newBuilder() .setScriptBytes(ByteString.copyFrom(input.getScriptBytes())) .setTransactionOutPointHash(hashToByteString(input.getOutpoint().getHash())) .setTransactionOutPointIndex((int) input.getOutpoint().getIndex()); if (input.hasSequence()) inputBuilder.setSequence((int) input.getSequenceNumber()); if (input.getValue() != null) inputBuilder.setValue(input.getValue().value); txBuilder.addTransactionInput(inputBuilder); } // Handle outputs. for (TransactionOutput output : tx.getOutputs()) { Protos.TransactionOutput.Builder outputBuilder = Protos.TransactionOutput.newBuilder() .setScriptBytes(ByteString.copyFrom(output.getScriptBytes())) .setValue(output.getValue().value); final TransactionInput spentBy = output.getSpentBy(); if (spentBy != null) { Sha256Hash spendingHash = spentBy.getParentTransaction().getHash(); int spentByTransactionIndex = spentBy.getParentTransaction().getInputs().indexOf(spentBy); outputBuilder.setSpentByTransactionHash(hashToByteString(spendingHash)) .setSpentByTransactionIndex(spentByTransactionIndex); } txBuilder.addTransactionOutput(outputBuilder); } // Handle which blocks tx was seen in. final Map<Sha256Hash, Integer> appearsInHashes = tx.getAppearsInHashes(); if (appearsInHashes != null) { for (Map.Entry<Sha256Hash, Integer> entry : appearsInHashes.entrySet()) { txBuilder.addBlockHash(hashToByteString(entry.getKey())); txBuilder.addBlockRelativityOffsets(entry.getValue()); } } if (tx.hasConfidence()) { TransactionConfidence confidence = tx.getConfidence(); Protos.TransactionConfidence.Builder confidenceBuilder = Protos.TransactionConfidence.newBuilder(); writeConfidence(txBuilder, confidence, confidenceBuilder); } return txBuilder.build(); } private static Protos.Transaction.Pool getProtoPool(WalletTransaction wtx) { switch (wtx.getPool()) { case UNSPENT: return Protos.Transaction.Pool.UNSPENT; case SPENT: return Protos.Transaction.Pool.SPENT; case DEAD: return Protos.Transaction.Pool.DEAD; case PENDING: return Protos.Transaction.Pool.PENDING; default: throw new RuntimeException("Unreachable"); } } private static void writeConfidence(Protos.Transaction.Builder txBuilder, TransactionConfidence confidence, Protos.TransactionConfidence.Builder confidenceBuilder) { synchronized (confidence) { confidenceBuilder.setType(Protos.TransactionConfidence.Type.valueOf(confidence.getConfidenceType().getValue())); if (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { confidenceBuilder.setAppearedAtHeight(confidence.getAppearedAtChainHeight()); confidenceBuilder.setDepth(confidence.getDepthInBlocks()); } if (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.DEAD) { // Copy in the overriding transaction, if available. // (A dead coinbase transaction has no overriding transaction). if (confidence.getOverridingTransaction() != null) { Sha256Hash overridingHash = confidence.getOverridingTransaction().getHash(); confidenceBuilder.setOverridingTransaction(hashToByteString(overridingHash)); } } TransactionConfidence.Source source = confidence.getSource(); switch (source) { case SELF: confidenceBuilder.setSource(Protos.TransactionConfidence.Source.SOURCE_SELF); break; case NETWORK: confidenceBuilder.setSource(Protos.TransactionConfidence.Source.SOURCE_NETWORK); break; case UNKNOWN: // Fall through. default: confidenceBuilder.setSource(Protos.TransactionConfidence.Source.SOURCE_UNKNOWN); break; } } for (ListIterator<PeerAddress> it = confidence.getBroadcastBy(); it.hasNext();) { PeerAddress address = it.next(); Protos.PeerAddress proto = Protos.PeerAddress.newBuilder() .setIpAddress(ByteString.copyFrom(address.getAddr().getAddress())) .setPort(address.getPort()) .setServices(address.getServices().longValue()) .build(); confidenceBuilder.addBroadcastBy(proto); } txBuilder.setConfidence(confidenceBuilder); } public static ByteString hashToByteString(Sha256Hash hash) { return ByteString.copyFrom(hash.getBytes()); } public static Sha256Hash byteStringToHash(ByteString bs) { return new Sha256Hash(bs.toByteArray()); } /** * <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 * may be in an indeterminate state and should be thrown away.</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 WalletPocketHD readWallet(Protos.WalletPocket walletProto, @Nullable KeyCrypter keyCrypter) throws UnreadableWalletException { CoinType coinType; try { coinType = CoinID.typeFromId(walletProto.getNetworkIdentifier()); } catch (IllegalArgumentException e) { throw new UnreadableWalletException("Unknown network parameters ID " + walletProto.getNetworkIdentifier()); } // Read the scrypt parameters that specify how encryption and decryption is performed. SimpleHDKeyChain chain; if (keyCrypter != null) { chain = SimpleHDKeyChain.fromProtobuf(walletProto.getKeyList(), keyCrypter); } else { chain = SimpleHDKeyChain.fromProtobuf(walletProto.getKeyList()); } WalletPocketHD pocket; if (walletProto.hasId()) { pocket = new WalletPocketHD(walletProto.getId(), chain, coinType); } else { pocket = new WalletPocketHD(chain, coinType); } if (walletProto.hasDescription()) { pocket.setDescription(walletProto.getDescription()); } try { // Read all transactions and insert into the txMap. for (Protos.Transaction txProto : walletProto.getTransactionList()) { readTransaction(txProto, coinType); } // Update transaction outputs to point to inputs that spend them ArrayList<WalletTransaction> wtxs = new ArrayList<WalletTransaction>(walletProto.getTransactionList().size()); for (Protos.Transaction txProto : walletProto.getTransactionList()) { wtxs.add(connectTransactionOutputs(txProto)); } pocket.restoreWalletTransactions(wtxs); // Read all the address statuses try { for (Protos.AddressStatus sp : walletProto.getAddressStatusList()) { Address addr = new Address(coinType, sp.getAddress()); AddressStatus status = new AddressStatus(addr, sp.getStatus()); pocket.commitAddressStatus(status); } } catch (AddressFormatException e) { throw new UnreadableWalletException(e.getMessage(), e); } // Update the lastBlockSeenHash. if (!walletProto.hasLastSeenBlockHash()) { pocket.setLastBlockSeenHash(null); } else { pocket.setLastBlockSeenHash(byteStringToHash(walletProto.getLastSeenBlockHash())); } if (!walletProto.hasLastSeenBlockHeight()) { pocket.setLastBlockSeenHeight(-1); } else { pocket.setLastBlockSeenHeight(walletProto.getLastSeenBlockHeight()); } // Will default to zero if not present. pocket.setLastBlockSeenTimeSecs(walletProto.getLastSeenBlockTimeSecs()); } catch (UnreadableWalletException e) { log.warn("Could not restore transactions. Trying refreshing {} pocket.", coinType.getName()); pocket.refresh(); } finally { // Make sure the object can be re-used to read another wallet without corruption. txMap.clear(); } return pocket; } private void readTransaction(Protos.Transaction txProto, CoinType params) throws UnreadableWalletException { Transaction tx = new Transaction(params); tx.setVersion(txProto.getVersion()); if (Networks.isFamily(tx.getParams(), PEERCOIN, NUBITS, REDDCOIN)) { tx.setTime(txProto.getTime()); } if (Networks.isFamily(tx.getParams(), NUBITS)) { tx.setTokenId((byte) (0xFF & txProto.getTokenId())); } if (txProto.hasUpdatedAt()) { tx.setUpdateTime(new Date(txProto.getUpdatedAt())); } for (Protos.TransactionOutput outputProto : txProto.getTransactionOutputList()) { Coin value = Coin.valueOf(outputProto.getValue()); byte[] scriptBytes = outputProto.getScriptBytes().toByteArray(); TransactionOutput output = new TransactionOutput(params, tx, value, scriptBytes); tx.addOutput(output); } for (Protos.TransactionInput inputProto : txProto.getTransactionInputList()) { byte[] scriptBytes = inputProto.getScriptBytes().toByteArray(); TransactionOutPoint outpoint = new TransactionOutPoint(params, inputProto.getTransactionOutPointIndex() & 0xFFFFFFFFL, byteStringToHash(inputProto.getTransactionOutPointHash()) ); Coin value = inputProto.hasValue() ? Coin.valueOf(inputProto.getValue()) : null; TransactionInput input = new TransactionInput(params, tx, scriptBytes, outpoint, value); if (inputProto.hasSequence()) { input.setSequenceNumber(inputProto.getSequence()); } tx.addInput(input); } for (int i = 0; i < txProto.getBlockHashCount(); i++) { ByteString blockHash = txProto.getBlockHash(i); int relativityOffset = 0; if (txProto.getBlockRelativityOffsetsCount() > 0) relativityOffset = txProto.getBlockRelativityOffsets(i); tx.addBlockAppearance(byteStringToHash(blockHash), relativityOffset); } if (txProto.hasLockTime()) { tx.setLockTime(0xffffffffL & txProto.getLockTime()); } // tx.setPurpose(Transaction.Purpose.USER_PAYMENT); // Transaction should now be complete. Sha256Hash protoHash = 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(Protos.Transaction txProto) throws UnreadableWalletException { Transaction tx = txMap.get(txProto.getHash()); final WalletTransaction.Pool pool; switch (txProto.getPool()) { case DEAD: pool = WalletTransaction.Pool.DEAD; break; case PENDING: pool = WalletTransaction.Pool.PENDING; break; case SPENT: pool = WalletTransaction.Pool.SPENT; break; case UNSPENT: pool = WalletTransaction.Pool.UNSPENT; break; default: throw new UnreadableWalletException("Unknown transaction pool: " + txProto.getPool()); } for (int i = 0 ; i < tx.getOutputs().size() ; i++) { TransactionOutput output = tx.getOutputs().get(i); final Protos.TransactionOutput transactionOutput = txProto.getTransactionOutput(i); if (transactionOutput.hasSpentByTransactionHash()) { final ByteString spentByTransactionHash = transactionOutput.getSpentByTransactionHash(); Transaction spendingTx = txMap.get(spentByTransactionHash); 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), "A spending index does not exist"); input.connect(output); } } if (txProto.hasConfidence()) { Protos.TransactionConfidence confidenceProto = txProto.getConfidence(); TransactionConfidence confidence = tx.getConfidence(); readConfidence(tx, confidenceProto, confidence); } return new WalletTransaction(pool, tx); } private void readConfidence(Transaction tx, Protos.TransactionConfidence confidenceProto, 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()) { log.warn("Unknown confidence type for tx {}", tx.getHashAsString()); return; } ConfidenceType confidenceType; switch (confidenceProto.getType()) { case BUILDING: confidenceType = ConfidenceType.BUILDING; break; case DEAD: confidenceType = ConfidenceType.DEAD; break; case PENDING: confidenceType = ConfidenceType.PENDING; break; case UNKNOWN: // Fall through. default: confidenceType = ConfidenceType.UNKNOWN; break; } confidence.setConfidenceType(confidenceType); if (confidenceProto.hasAppearedAtHeight()) { if (confidence.getConfidenceType() != ConfidenceType.BUILDING) { log.warn("Have appearedAtHeight but not BUILDING for tx {}", tx.getHashAsString()); return; } confidence.setAppearedAtChainHeight(confidenceProto.getAppearedAtHeight()); } if (confidenceProto.hasDepth()) { if (confidence.getConfidenceType() != ConfidenceType.BUILDING) { log.warn("Have depth but not BUILDING for tx {}", tx.getHashAsString()); return; } confidence.setDepthInBlocks(confidenceProto.getDepth()); } if (confidenceProto.hasOverridingTransaction()) { if (confidence.getConfidenceType() != ConfidenceType.DEAD) { log.warn("Have overridingTransaction but not OVERRIDDEN for tx {}", tx.getHashAsString()); return; } Transaction overridingTransaction = txMap.get(confidenceProto.getOverridingTransaction()); if (overridingTransaction == null) { log.warn("Have overridingTransaction that is not in wallet for tx {}", tx.getHashAsString()); return; } confidence.setOverridingTransaction(overridingTransaction); } for (Protos.PeerAddress proto : confidenceProto.getBroadcastByList()) { InetAddress ip; try { ip = InetAddress.getByAddress(proto.getIpAddress().toByteArray()); } catch (UnknownHostException e) { throw new UnreadableWalletException("Peer IP address does not have the right length", e); } int port = proto.getPort(); PeerAddress address = new PeerAddress(ip, port); address.setServices(BigInteger.valueOf(proto.getServices())); confidence.markBroadcastBy(address); } switch (confidenceProto.getSource()) { case SOURCE_SELF: confidence.setSource(TransactionConfidence.Source.SELF); break; case SOURCE_NETWORK: confidence.setSource(TransactionConfidence.Source.NETWORK); break; case SOURCE_UNKNOWN: // Fall through. default: confidence.setSource(TransactionConfidence.Source.UNKNOWN); break; } } }