All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bitcoinj.wallet.WalletProtobufSerializer Maven / Gradle / Ivy

There is a newer version: 0.17-beta1
Show newest version
/*
 * Copyright 2012 Google Inc.
 * Copyright 2014 Andreas Schildbach
 *
 * 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 org.bitcoinj.wallet;

import com.google.protobuf.ByteString;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.WireFormat;
import org.bitcoinj.base.Coin;
import org.bitcoinj.core.LockTime;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.PeerAddress;
import org.bitcoinj.base.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.core.TransactionWitness;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.params.BitcoinNetworkParams;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptException;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.base.utils.Fiat;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * Serialize and de-serialize a wallet to a byte stream containing a
 * protocol buffer. Protocol buffers are
 * a data interchange format developed by Google with an efficient binary representation, a type safe specification
 * language and compilers that generate code to work with those data structures for many languages. Protocol buffers
 * can have their format evolved over time: conceptually they represent data using (tag, length, value) tuples. The
 * format is defined by the {@code wallet.proto} file in the bitcoinj source distribution.
 * 

* The most common operations are writeWallet and readWallet, which do * the obvious operations on Output/InputStreams. You can use a {@link ByteArrayInputStream} and equivalent * {@link ByteArrayOutputStream} if you'd like byte arrays instead. The protocol buffer can also be manipulated * in its object form if you'd like to modify the flattened data structure before serialization to binary. *

* You can extend the wallet format with additional fields specific to your application if you want, but make sure * to either put the extra data in the provided extension areas, or select tag numbers that are unlikely to be used * by anyone else. * * @author Miron Cuperman * @author Andreas Schildbach */ public class WalletProtobufSerializer { private static final Logger log = LoggerFactory.getLogger(WalletProtobufSerializer.class); /** Current version used for serializing wallets. A version higher than this is considered from the future. */ public static final int CURRENT_WALLET_VERSION = Protos.Wallet.getDefaultInstance().getVersion(); // 512 MB private static final int WALLET_SIZE_LIMIT = 512 * 1024 * 1024; // Used for de-serialization protected Map txMap; private boolean requireMandatoryExtensions = true; private boolean requireAllExtensionsKnown = false; private int walletWriteBufferSize = CodedOutputStream.DEFAULT_BUFFER_SIZE; @FunctionalInterface public interface WalletFactory { Wallet create(NetworkParameters params, KeyChainGroup keyChainGroup); WalletFactory DEFAULT = Wallet::new; } private final WalletFactory factory; private KeyChainFactory keyChainFactory; public WalletProtobufSerializer() { this(WalletFactory.DEFAULT); } public WalletProtobufSerializer(WalletFactory factory) { txMap = new HashMap<>(); this.factory = factory; this.keyChainFactory = new DefaultKeyChainFactory(); } public void setKeyChainFactory(KeyChainFactory keyChainFactory) { this.keyChainFactory = keyChainFactory; } /** * If this property is set to false, then unknown mandatory extensions will be ignored instead of causing load * errors. You should only use this if you know exactly what you are doing, as the extension data will NOT be * round-tripped, possibly resulting in a corrupted wallet if you save it back out again. */ public void setRequireMandatoryExtensions(boolean value) { requireMandatoryExtensions = value; } /** * If this property is set to true, the wallet will fail to load if any found extensions are unknown.. */ public void setRequireAllExtensionsKnown(boolean value) { requireAllExtensionsKnown = value; } /** * Change buffer size for writing wallet to output stream. Default is {@link com.google.protobuf.CodedOutputStream#DEFAULT_BUFFER_SIZE} * @param walletWriteBufferSize - buffer size in bytes */ public void setWalletWriteBufferSize(int walletWriteBufferSize) { this.walletWriteBufferSize = walletWriteBufferSize; } /** * Formats the given wallet (transactions and keys) to the given output stream in protocol buffer format.

* * Equivalent to {@code walletToProto(wallet).writeTo(output);} */ public void writeWallet(Wallet wallet, OutputStream output) throws IOException { Protos.Wallet walletProto = walletToProto(wallet); final CodedOutputStream codedOutput = CodedOutputStream.newInstance(output, this.walletWriteBufferSize); walletProto.writeTo(codedOutput); codedOutput.flush(); } /** * Returns the given wallet formatted as text. The text format is that used by protocol buffers and * it is designed more for debugging than storage. It is not well specified and wallets are largely binary data * structures anyway, consisting as they do of keys (large random numbers) and {@link Transaction}s which also * mostly contain keys and hashes. */ public String walletToText(Wallet wallet) { Protos.Wallet walletProto = walletToProto(wallet); return walletProto.toString(); } /** * Converts the given wallet to the object representation of the protocol buffers. This can be modified, or * additional data fields set, before serialization takes place. */ public Protos.Wallet walletToProto(Wallet wallet) { Protos.Wallet.Builder walletBuilder = Protos.Wallet.newBuilder(); walletBuilder.setNetworkIdentifier(wallet.getNetworkParameters().getId()); if (wallet.getDescription() != null) { walletBuilder.setDescription(wallet.getDescription()); } for (WalletTransaction wtx : wallet.getWalletTransactions()) { Protos.Transaction txProto = makeTxProto(wtx); walletBuilder.addTransaction(txProto); } walletBuilder.addAllKey(wallet.serializeKeyChainGroupToProtobufInternal()); for (Script script : wallet.getWatchedScripts()) { Protos.Script protoScript = Protos.Script.newBuilder() .setProgram(ByteString.copyFrom(script.getProgram())) .setCreationTimestamp(script.creationTime().orElse(Instant.EPOCH).toEpochMilli()) .build(); walletBuilder.addWatchedScript(protoScript); } // Populate the lastSeenBlockHash field. Sha256Hash lastSeenBlockHash = wallet.getLastBlockSeenHash(); if (lastSeenBlockHash != null) { walletBuilder.setLastSeenBlockHash(hashToByteString(lastSeenBlockHash)); walletBuilder.setLastSeenBlockHeight(wallet.getLastBlockSeenHeight()); } wallet.lastBlockSeenTime().ifPresent( time -> walletBuilder.setLastSeenBlockTimeSecs(time.getEpochSecond())); // Populate the scrypt parameters. KeyCrypter keyCrypter = wallet.getKeyCrypter(); if (keyCrypter == null) { // The wallet is unencrypted. walletBuilder.setEncryptionType(EncryptionType.UNENCRYPTED); } else { // The wallet is encrypted. walletBuilder.setEncryptionType(keyCrypter.getUnderstoodEncryptionType()); if (keyCrypter instanceof KeyCrypterScrypt) { KeyCrypterScrypt keyCrypterScrypt = (KeyCrypterScrypt) keyCrypter; walletBuilder.setEncryptionParameters(keyCrypterScrypt.getScryptParameters()); } else { // Some other form of encryption has been specified that we do not know how to persist. throw new RuntimeException("The wallet has encryption of type '" + keyCrypter.getUnderstoodEncryptionType() + "' but this WalletProtobufSerializer does not know how to persist this."); } } Optional keyRotationTime = wallet.keyRotationTime(); if (keyRotationTime.isPresent()) { long timeSecs = keyRotationTime.get().getEpochSecond(); walletBuilder.setKeyRotationTime(timeSecs); } populateExtensions(wallet, walletBuilder); for (Map.Entry entry : wallet.getTags().entrySet()) { Protos.Tag.Builder tag = Protos.Tag.newBuilder().setTag(entry.getKey()).setData(entry.getValue()); walletBuilder.addTags(tag); } // Populate the wallet version. walletBuilder.setVersion(wallet.getVersion()); return walletBuilder.build(); } private static void populateExtensions(Wallet wallet, Protos.Wallet.Builder walletBuilder) { for (WalletExtension extension : wallet.getExtensions().values()) { Protos.Extension.Builder proto = Protos.Extension.newBuilder(); proto.setId(extension.getWalletExtensionID()); proto.setMandatory(extension.isWalletExtensionMandatory()); proto.setData(ByteString.copyFrom(extension.serializeWalletExtension())); walletBuilder.addExtension(proto); } } 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.getTxId())) .setVersion((int) tx.getVersion()); tx.updateTime().ifPresent( time -> txBuilder.setUpdatedAt(time.toEpochMilli())); LockTime locktime = tx.lockTime(); if (locktime.isSet()) { txBuilder.setLockTime((int) locktime.rawValue()); } // 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); if (input.hasWitness()) { TransactionWitness witness = input.getWitness(); Protos.ScriptWitness.Builder witnessBuilder = Protos.ScriptWitness.newBuilder(); int pushCount = witness.getPushCount(); for (int i = 0; i < pushCount; i++) witnessBuilder.addData(ByteString.copyFrom(witness.getPush(i))); inputBuilder.setWitness(witnessBuilder); } 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().getTxId(); outputBuilder.setSpentByTransactionHash(hashToByteString(spendingHash)) .setSpentByTransactionIndex(spentBy.getIndex()); } txBuilder.addTransactionOutput(outputBuilder); } // Handle which blocks tx was seen in. final Map appearsInHashes = tx.getAppearsInHashes(); if (appearsInHashes != null) { for (Map.Entry 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); } Protos.Transaction.Purpose purpose; switch (tx.getPurpose()) { case UNKNOWN: purpose = Protos.Transaction.Purpose.UNKNOWN; break; case USER_PAYMENT: purpose = Protos.Transaction.Purpose.USER_PAYMENT; break; case KEY_ROTATION: purpose = Protos.Transaction.Purpose.KEY_ROTATION; break; case ASSURANCE_CONTRACT_CLAIM: purpose = Protos.Transaction.Purpose.ASSURANCE_CONTRACT_CLAIM; break; case ASSURANCE_CONTRACT_PLEDGE: purpose = Protos.Transaction.Purpose.ASSURANCE_CONTRACT_PLEDGE; break; case ASSURANCE_CONTRACT_STUB: purpose = Protos.Transaction.Purpose.ASSURANCE_CONTRACT_STUB; break; case RAISE_FEE: purpose = Protos.Transaction.Purpose.RAISE_FEE; break; default: throw new RuntimeException("New tx purpose serialization not implemented."); } txBuilder.setPurpose(purpose); ExchangeRate exchangeRate = tx.getExchangeRate(); if (exchangeRate != null) { Protos.ExchangeRate.Builder exchangeRateBuilder = Protos.ExchangeRate.newBuilder() .setCoinValue(exchangeRate.coin.value).setFiatValue(exchangeRate.fiat.value) .setFiatCurrencyCode(exchangeRate.fiat.currencyCode); txBuilder.setExchangeRate(exchangeRateBuilder); } if (tx.getMemo() != null) txBuilder.setMemo(tx.getMemo()); 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.forNumber(confidence.getConfidenceType().getValue())); if (confidence.getConfidenceType() == ConfidenceType.BUILDING) { confidenceBuilder.setAppearedAtHeight(confidence.getAppearedAtChainHeight()); confidenceBuilder.setDepth(confidence.getDepthInBlocks()); } if (confidence.getConfidenceType() == 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().getTxId(); 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 (PeerAddress address : confidence.getBroadcastBy()) { Protos.PeerAddress proto = Protos.PeerAddress.newBuilder() .setIpAddress(ByteString.copyFrom(address.getAddr().getAddress())) .setPort(address.getPort()) .setServices(address.getServices().longValue()) .build(); confidenceBuilder.addBroadcastBy(proto); } confidence.lastBroadcastTime().ifPresent( lastBroadcastTime -> confidenceBuilder.setLastBroadcastedAt(lastBroadcastTime.toEpochMilli()) ); txBuilder.setConfidence(confidenceBuilder); } public static ByteString hashToByteString(Sha256Hash hash) { return ByteString.copyFrom(hash.getBytes()); } public static Sha256Hash byteStringToHash(ByteString bs) { return Sha256Hash.wrap(bs.toByteArray()); } /** *

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.

* *

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, @Nullable WalletExtension... walletExtensions) throws UnreadableWalletException { return readWallet(input, false, walletExtensions); } /** *

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. Do not simply call this method again on the same * Wallet object with {@code forceReset} set {@code true}. It won't work.

* *

If {@code forceReset} is {@code true}, then no transactions are loaded from the wallet, and it is configured * to replay transactions from the blockchain (as if the wallet had been loaded and {@link Wallet#reset()} * had been called immediately thereafter). * *

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, boolean forceReset, @Nullable WalletExtension[] extensions) throws UnreadableWalletException { try { Protos.Wallet walletProto = parseToProto(input); final String paramsID = walletProto.getNetworkIdentifier(); NetworkParameters params = BitcoinNetworkParams.fromID(paramsID); if (params == null) throw new UnreadableWalletException("Unknown network parameters ID " + paramsID); return readWallet(params, extensions, walletProto, forceReset); } catch (IOException | IllegalArgumentException | IllegalStateException e) { throw new UnreadableWalletException("Could not parse input stream to protobuf", e); } } /** *

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.

* *

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(NetworkParameters params, @Nullable WalletExtension[] extensions, Protos.Wallet walletProto) throws UnreadableWalletException { return readWallet(params, extensions, walletProto, false); } /** *

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. Do not simply call this method again on the same * Wallet object with {@code forceReset} set {@code true}. It won't work.

* *

If {@code forceReset} is {@code true}, then no transactions are loaded from the wallet, and it is configured * to replay transactions from the blockchain (as if the wallet had been loaded and {@link Wallet#reset()} * had been called immediately thereafter). * *

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(NetworkParameters params, @Nullable WalletExtension[] extensions, Protos.Wallet walletProto, boolean forceReset) throws UnreadableWalletException { if (walletProto.getVersion() > CURRENT_WALLET_VERSION) throw new UnreadableWalletException.FutureVersion(); if (!walletProto.getNetworkIdentifier().equals(params.getId())) throw new UnreadableWalletException.WrongNetwork(); // Read the scrypt parameters that specify how encryption and decryption is performed. KeyChainGroup keyChainGroup; if (walletProto.hasEncryptionParameters()) { Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters(); final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(encryptionParameters); keyChainGroup = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), keyCrypter, keyChainFactory); } else { keyChainGroup = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList(), keyChainFactory); } Wallet wallet = factory.create(params, keyChainGroup); List