/** * Copyright 2011 Google Inc. * * 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.google.digitalcoin.core; import com.google.digitalcoin.crypto.KeyCrypterScrypt; import org.digitalcoinj.wallet.Protos.Wallet.EncryptionType; import org.spongycastle.crypto.params.KeyParameter; import com.google.digitalcoin.core.TransactionConfidence.ConfidenceType; import com.google.digitalcoin.core.WalletTransaction.Pool; import com.google.digitalcoin.crypto.KeyCrypter; import com.google.digitalcoin.crypto.KeyCrypterException; import com.google.digitalcoin.store.WalletProtobufSerializer; import com.google.digitalcoin.utils.Locks; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.math.BigInteger; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import static com.google.digitalcoin.core.Utils.digitalcoinValueToFriendlyString; import static com.google.common.base.Preconditions.*; // To do list: // // - Make the keychain member protected and switch it to be a hashmap of some kind so key lookup ops are faster. // - Refactor how keys are managed to better handle things like deterministic wallets in future. // - Decompose the class where possible: break logic out into classes that can be customized/replaced by the user. // - [Auto]saving to a backing store // - Key management // - just generally make Wallet smaller and easier to work with // - Make clearing of transactions able to only rewind the wallet a certain distance instead of all blocks. /** * <p>A Wallet stores keys and a record of transactions that send and receive value from those keys. Using these, * it is able to create new transactions that spend the recorded transactions, and this is the fundamental operation * of the Digitalcoin protocol.</p> * * <p>To learn more about this class, read <b><a href="http://code.google.com/p/digitalcoinj/wiki/WorkingWithTheWallet"> * working with the wallet.</a></b></p> * * <p>To fill up a Wallet with transactions, you need to use it in combination with a {@link BlockChain} and various * other objects, see the <a href="http://code.google.com/p/digitalcoinj/wiki/GettingStarted">Getting started</a> tutorial * on the website to learn more about how to set everything up.</p> * * <p>Wallets can be serialized using either Java serialization - this is not compatible across versions of digitalcoinj, * or protocol buffer serialization. You need to save the wallet whenever it changes, there is an auto-save feature * that simplifies this for you although you're still responsible for manually triggering a save when your app is about * to quit because the auto-save feature waits a moment before actually committing to disk to avoid IO thrashing when * the wallet is changing very fast (eg due to a block chain sync). See * {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.digitalcoin.core.Wallet.AutosaveEventListener)} * for more information about this.</p> */ public class Wallet implements Serializable, BlockChainListener { private static final Logger log = LoggerFactory.getLogger(Wallet.class); private static final long serialVersionUID = 2L; protected final ReentrantLock lock = Locks.lock("wallet"); // Algorithm for movement of transactions between pools. Outbound tx = us spending coins. Inbound tx = us // receiving coins. If a tx is both inbound and outbound (spend with change) it is considered outbound for the // purposes of the explanation below. // // 1. Outbound tx is created by us: ->pending // 2. Outbound tx that was broadcast is accepted into the main chain: // <-pending and // If there is a change output ->unspent // If there is no change output ->spent // 3. Outbound tx that was broadcast is accepted into a side chain: // ->inactive (remains in pending). // 4. Inbound tx is accepted into the best chain: // ->unspent/spent // check if any pending transactions spend these outputs, if so, potentially <-unspent ->spent // 5. Inbound tx is accepted into a side chain: // ->inactive // Whilst it's also 'pending' in some sense, in that miners will probably try and incorporate it into the // best chain, we don't mark it as such here. It'll eventually show up after a re-org. // 6. Outbound tx that is pending shares inputs with a tx that appears in the main chain: // <-pending ->dead // // Re-orgs: // 1. Tx is present in old chain and not present in new chain // <-unspent/spent ->pending // These newly inactive transactions will (if they are relevant to us) eventually come back via receive() // as miners resurrect them and re-include into the new best chain. // 2. Tx is not present in old chain and is present in new chain // <-inactive and ->unspent/spent // 3. Tx is present in new chain and shares inputs with a pending transaction, including those that were resurrected // due to point (1) // <-pending ->dead // // Balance: // Take all the candidates for spending from unspent and pending. Select the ones that are actually available // according to our spend policy. Sum them up. /** * Map of txhash->Transactions that have not made it into the best chain yet. They are eligible to move there but * are waiting for a miner to create a block on the best chain including them. These transactions inputs count as * spent for the purposes of calculating our balance but their outputs are not available for spending yet. This * means after a spend, our balance can actually go down temporarily before going up again! We should fix this to * allow spending of pending transactions. * * Pending transactions get announced to peers when they first connect. This means that if we're currently offline, * we can still create spends and upload them to the network later. */ final Map<Sha256Hash, Transaction> pending; /** * Map of txhash->Transactions where the Transaction has unspent outputs. These are transactions we can use * to pay other people and so count towards our balance. Transactions only appear in this map if they are part * of the best chain. Transactions we have broacast that are not confirmed yet appear in pending even though they * may have unspent "change" outputs.<p> * <p/> * Note: for now we will not allow spends of transactions that did not make it into the block chain. The code * that handles this in Digitalcoin C++ is complicated. Satoshis code will not allow you to spend unconfirmed coins, * however, it does seem to support dependency resolution entirely within the context of the memory pool so * theoretically you could spend zero-conf coins and all of them would be included together. To simplify we'll * make people wait but it would be a good improvement to resolve this in future. */ final Map<Sha256Hash, Transaction> unspent; /** * Map of txhash->Transactions where the Transactions outputs are all fully spent. They are kept separately so * the time to create a spend does not grow infinitely as wallets become more used. Some of these transactions * may not have appeared in a block yet if they were created by us to spend coins and that spend is still being * worked on by miners.<p> * <p/> * Transactions only appear in this map if they are part of the best chain. */ final Map<Sha256Hash, Transaction> spent; /** * An inactive transaction is one that is seen only in a block that is not a part of the best chain. We keep it * around in case a re-org promotes a different chain to be the best. In this case some (not necessarily all) * inactive transactions will be moved out to unspent and spent, and some might be moved in.<p> * <p/> * Note that in the case where a transaction appears in both the best chain and a side chain as well, it is not * placed in this map. It's an error for a transaction to be in both the inactive pool and unspent/spent. */ final Map<Sha256Hash, Transaction> inactive; /** * A dead transaction is one that's been overridden by a double spend. Such a transaction is pending except it * will never confirm and so should be presented to the user in some unique way - flashing red for example. This * should nearly never happen in normal usage. Dead transactions can be "resurrected" by re-orgs just like any * other. Dead transactions are not in the pending pool. */ final Map<Sha256Hash, Transaction> dead; /** * A list of public/private EC keys owned by this user. Access it using addKey[s], hasKey[s] and findPubKeyFromHash. */ public ArrayList<ECKey> keychain; private final NetworkParameters params; private Sha256Hash lastBlockSeenHash; private int lastBlockSeenHeight = -1; private transient CopyOnWriteArrayList<WalletEventListener> eventListeners; // Auto-save code. This all should be generalized in future to not be file specific so you can easily store the // wallet into a database using the same mechanism. However we need to inform stores of each specific change with // some objects representing those changes, which is more complex. To avoid poor performance in 0.6 on phones that // have a lot of transactions in their wallet, we use the simpler approach. It's needed because the wallet stores // the number of confirmations and accumulated work done for each transaction, so each block changes each tx. private transient File autosaveToFile; private transient boolean dirty; // Is a write of the wallet necessary? private transient AutosaveEventListener autosaveEventListener; private transient long autosaveDelayMs; // A listener that relays confidence changes from the transaction confidence object to the wallet event listener, // as a convenience to API users so they don't have to register on every transaction themselves. private transient TransactionConfidence.Listener txConfidenceListener; // If a TX hash appears in this set then notifyNewBestBlock will ignore it, as its confidence was already set up // in receive() via Transaction.setBlockAppearance(). As the BlockChain always calls notifyNewBestBlock even if // it sent transactions to the wallet, without this we'd double count. private transient HashSet<Sha256Hash> ignoreNextNewBlock; // Whether or not to ignore nLockTime > 0 transactions that are received to the mempool. private boolean acceptTimeLockedTransactions; /** Represents the results of a {@link CoinSelector#select(java.math.BigInteger, java.util.LinkedList)} operation */ public static class CoinSelection { public BigInteger valueGathered; public List<TransactionOutput> gathered; public CoinSelection(BigInteger valueGathered, List<TransactionOutput> gathered) { this.valueGathered = valueGathered; this.gathered = gathered; } } /** * A CoinSelector is responsible for picking some outputs to spend, from the list of all spendable outputs. It * allows you to customize the policies for creation of transactions to suit your needs. The select operation * may return a {@link CoinSelection} that has a valueGathered lower than the requested target, if there's not * enough money in the wallet. */ public interface CoinSelector { public CoinSelection select(BigInteger target, LinkedList<TransactionOutput> candidates); } public static class DefaultCoinSelector implements CoinSelector { public CoinSelection select(BigInteger biTarget, LinkedList<TransactionOutput> candidates) { long target = biTarget.longValue(); long total = 0; LinkedList<TransactionOutput> selected = Lists.newLinkedList(); // Sort the inputs by age so we use oldest first. // TODO: Consider changing the wallets internal format to track just outputs and keep them ordered. ArrayList<TransactionOutput> sortedOutputs = new ArrayList<TransactionOutput>(candidates); Collections.sort(sortedOutputs, new Comparator<TransactionOutput>() { public int compare(TransactionOutput a, TransactionOutput b) { int depth1 = 0; int depth2 = 0; TransactionConfidence conf1 = a.parentTransaction.getConfidence(); TransactionConfidence conf2 = b.parentTransaction.getConfidence(); if (conf1.getConfidenceType() == ConfidenceType.BUILDING) depth1 = conf1.getDepthInBlocks(); if (conf2.getConfidenceType() == ConfidenceType.BUILDING) depth2 = conf2.getDepthInBlocks(); if (depth1 < depth2) return 1; else if (depth1 > depth2) return -1; // Their depths are equal (possibly pending) so sort by hash to ensure a total ordering. BigInteger aHash = a.parentTransaction.getHash().toBigInteger(); BigInteger bHash = b.parentTransaction.getHash().toBigInteger(); return aHash.compareTo(bHash); } }); // Now iterate over the sorted outputs until we have got as close to the target as possible or a little // bit over (excessive value will be change). for (TransactionOutput output : sortedOutputs) { if (total >= target) break; // Only pick chain-included transactions, or transactions that are ours and pending. if (!shouldSelect(output.parentTransaction)) continue; selected.add(output); total += output.getValue().longValue(); } // Total may be lower than target here, if the given candidates were insufficient to create to requested // transaction. return new CoinSelection(BigInteger.valueOf(total), selected); } /** Sub-classes can override this to just customize whether transactions are usable, but keep age sorting. */ protected boolean shouldSelect(Transaction tx) { return isSelectable(tx); } public static boolean isSelectable(Transaction tx) { // Only pick chain-included transactions, or transactions that are ours and pending. TransactionConfidence confidence = tx.getConfidence(); ConfidenceType type = confidence.getConfidenceType(); boolean pending = type.equals(ConfidenceType.NOT_SEEN_IN_CHAIN) || type.equals(ConfidenceType.NOT_IN_BEST_CHAIN); boolean confirmed = type.equals(ConfidenceType.BUILDING); if (!confirmed) { // If the transaction is still pending ... if (!pending) return false; // And it was created by us ... if (!confidence.getSource().equals(TransactionConfidence.Source.SELF)) return false; // And it's been seen by the network and propagated ... if (confidence.numBroadcastPeers() <= 1) return false; // Then it's OK to select. } return true; } } private transient CoinSelector coinSelector = new DefaultCoinSelector(); // The keyCrypter for the wallet. This specifies the algorithm used for encrypting and decrypting the private keys. private KeyCrypter keyCrypter; // The wallet version. This is an int that can be used to track breaking changes in the wallet format. // You can also use it to detect wallets that come from the future (ie they contain features you // do not know how to deal with). private int version; // User-provided description that may help people keep track of what a wallet is for. String description; /** * Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead, * see loadFromFile. */ public Wallet(NetworkParameters params) { this(params, null); } /** * Create a wallet with a keyCrypter to use in encrypting and decrypting keys. */ public Wallet(NetworkParameters params, KeyCrypter keyCrypter) { this.keyCrypter = keyCrypter; this.params = checkNotNull(params); keychain = new ArrayList<ECKey>(); unspent = new HashMap<Sha256Hash, Transaction>(); spent = new HashMap<Sha256Hash, Transaction>(); inactive = new HashMap<Sha256Hash, Transaction>(); pending = new HashMap<Sha256Hash, Transaction>(); dead = new HashMap<Sha256Hash, Transaction>(); eventListeners = new CopyOnWriteArrayList<WalletEventListener>(); createTransientState(); } private void createTransientState() { ignoreNextNewBlock = new HashSet<Sha256Hash>(); txConfidenceListener = new TransactionConfidence.Listener() { @Override public void onConfidenceChanged(Transaction tx) { lock.lock(); // The invokers unlock us immediately so if an exception is thrown, the lock will be already open. invokeOnTransactionConfidenceChanged(tx); // Many onWalletChanged events will not occur because they are suppressed, eg, because: // - we are inside a re-org // - we are in the middle of processing a block // - the confidence is changing because a new best block was accepted // It will run in cases like: // - the tx is pending and another peer announced it // - the tx is pending and was killed by a detected double spend that was not in a block // The latter case cannot happen today because we won't hear about it, but in future this may // become more common if conflict notices are implemented. invokeOnWalletChanged(); lock.unlock(); } }; acceptTimeLockedTransactions = false; } public NetworkParameters getNetworkParameters() { return params; } /** * Returns a snapshot of the keychain. This view is not live. */ public Iterable<ECKey> getKeys() { lock.lock(); try { return new ArrayList<ECKey>(keychain); } finally { lock.unlock(); } } /** * Returns the number of keys in the keychain. */ public int getKeychainSize() { lock.lock(); try { return keychain.size(); } finally { lock.unlock(); } } private void saveToFile(File temp, File destFile) throws IOException { FileOutputStream stream = null; try { stream = new FileOutputStream(temp); saveToFileStream(stream); // Attempt to force the bits to hit the disk. In reality the OS or hard disk itself may still decide // to not write through to physical media for at least a few seconds, but this is the best we can do. stream.flush(); stream.getFD().sync(); stream.close(); stream = null; if (Utils.isWindows()) { // Work around an issue on Windows whereby you can't rename over existing files. File canonical = destFile.getCanonicalFile(); canonical.delete(); if (temp.renameTo(canonical)) return; // else fall through. throw new IOException("Failed to rename " + temp + " to " + canonical); } else if (!temp.renameTo(destFile)) { throw new IOException("Failed to rename " + temp + " to " + destFile); } lock.lock(); try { if (destFile.equals(autosaveToFile)) { dirty = false; } } finally { lock.unlock(); } } finally { if (stream != null) { stream.close(); } } } /** * Uses protobuf serialization to save the wallet to the given file. To learn more about this file format, see * {@link WalletProtobufSerializer}. Writes out first to a temporary file in the same directory and then renames * once written. */ public void saveToFile(File f) throws IOException { File directory = f.getAbsoluteFile().getParentFile(); File temp = File.createTempFile("wallet", null, directory); saveToFile(temp, f); } /** * <p>Whether or not the wallet will ignore transactions that have a lockTime parameter > 0. By default, all such * transactions are ignored, because they are useful only in special protocols and such a transaction may not * confirm as fast as an app typically expects. By setting this property to true, you are acknowledging that * you understand what time-locked transactions are, and that your code is capable of handling them without risk. * For instance you are not providing anything valuable in return for an unconfirmed transaction that has a lock * time far in the future (which opens you up to Finney attacks).</p> * * <p>Note that this property is not serialized. So you have to set it to true each time you load or create a * wallet.</p> */ public void setAcceptTimeLockedTransactions(boolean acceptTimeLockedTransactions) { lock.lock(); try { this.acceptTimeLockedTransactions = acceptTimeLockedTransactions; } finally { lock.unlock(); } } /** * See {@link Wallet#setAcceptTimeLockedTransactions(boolean)} for an explanation of this property. */ public boolean doesAcceptTimeLockedTransactions() { lock.lock(); try { return acceptTimeLockedTransactions; } finally { lock.unlock(); } } // Auto-saving can be done on a background thread if the user wishes it, this is to avoid stalling threads calling // into the wallet on serialization/disk access all the time which is important in GUI apps where you don't want // the main thread to ever wait on disk (otherwise you lose a lot of responsiveness). The primary case where it // can be a problem is during block chain syncup - the wallet has to be saved after every block to record where // it got up to and for updating the transaction confidence data, which can slow down block chain download a lot. // So this thread not only puts the work of saving onto a background thread but also coalesces requests together. private static class AutosaveThread extends Thread { private static DelayQueue<AutosaveThread.WalletSaveRequest> walletRefs = new DelayQueue<WalletSaveRequest>(); private static AutosaveThread globalThread; private AutosaveThread() { // Allow the JVM to shut down without waiting for this thread. Note this means users could lose auto-saves // if they don't explicitly save the wallet before terminating! setDaemon(true); setName("Wallet auto save thread"); setPriority(Thread.MIN_PRIORITY); // Avoid competing with the UI. } /** Returns the global instance that services all wallets. It never shuts down. */ public static void maybeStart() { if (walletRefs.size() == 0) return; synchronized (AutosaveThread.class) { if (globalThread == null) { globalThread = new AutosaveThread(); globalThread.start(); } } } /** Called by a wallet when it's become dirty (changed). Will start the background thread if needed. */ public static void registerForSave(Wallet wallet, long delayMsec) { walletRefs.add(new WalletSaveRequest(wallet, delayMsec)); maybeStart(); } public void run() { log.info("Auto-save thread starting up"); while (true) { try { WalletSaveRequest req = walletRefs.poll(5, TimeUnit.SECONDS); if (req == null) { if (walletRefs.size() == 0) { // No work to do for the given delay period, so let's shut down and free up memory. // We'll get started up again if a wallet changes once more. break; } else { // There's work but nothing to do just yet. Go back to sleep and try again. continue; } } req.wallet.lock.lock(); try { if (req.wallet.dirty) { if (req.wallet.autoSave()) { // Something went wrong, abort! break; } } } finally { req.wallet.lock.unlock(); } } catch (InterruptedException e) { log.error("Auto-save thread interrupted during wait", e); break; } } log.info("Auto-save thread shutting down"); synchronized (AutosaveThread.class) { Preconditions.checkState(globalThread == this); // There should only be one global thread. globalThread = null; } // There's a possible shutdown race where work is added after we decided to shutdown but before // we cleared globalThread. maybeStart(); } private static class WalletSaveRequest implements Delayed { public final Wallet wallet; public final long startTimeMs, requestedDelayMs; public WalletSaveRequest(Wallet wallet, long requestedDelayMs) { this.startTimeMs = System.currentTimeMillis(); this.requestedDelayMs = requestedDelayMs; this.wallet = wallet; } public long getDelay(TimeUnit timeUnit) { long delayRemainingMs = requestedDelayMs - (System.currentTimeMillis() - startTimeMs); return timeUnit.convert(delayRemainingMs, TimeUnit.MILLISECONDS); } public int compareTo(Delayed delayed) { if (delayed == this) return 0; long delta = getDelay(TimeUnit.MILLISECONDS) - delayed.getDelay(TimeUnit.MILLISECONDS); return (delta > 0 ? 1 : (delta < 0 ? -1 : 0)); } @Override public boolean equals(Object obj) { if (!(obj instanceof WalletSaveRequest)) return false; WalletSaveRequest w = (WalletSaveRequest) obj; return w.startTimeMs == startTimeMs && w.requestedDelayMs == requestedDelayMs && w.wallet == wallet; } @Override public int hashCode() { return Objects.hashCode(wallet, startTimeMs, requestedDelayMs); } } } /** Returns true if the auto-save thread should abort */ private boolean autoSave() { lock.lock(); final Sha256Hash lastBlockSeenHash = this.lastBlockSeenHash; final AutosaveEventListener autosaveEventListener = this.autosaveEventListener; final File autosaveToFile = this.autosaveToFile; lock.unlock(); try { log.info("Auto-saving wallet, last seen block is {}", lastBlockSeenHash); File directory = autosaveToFile.getAbsoluteFile().getParentFile(); File temp = File.createTempFile("wallet", null, directory); if (autosaveEventListener != null) autosaveEventListener.onBeforeAutoSave(temp); // This will clear the dirty flag. saveToFile(temp, autosaveToFile); if (autosaveEventListener != null) autosaveEventListener.onAfterAutoSave(autosaveToFile); } catch (Exception e) { if (autosaveEventListener != null && autosaveEventListener.caughtException(e)) return true; else throw new RuntimeException(e); } return false; } /** * Implementors can handle exceptions thrown during wallet auto-save, and to do pre/post treatment of the wallet. */ public interface AutosaveEventListener { /** * Called on the auto-save thread if an exception is caught whilst saving the wallet. * @return if true, terminates the auto-save thread. Otherwise sleeps and then tries again. */ public boolean caughtException(Throwable t); /** * Called on the auto-save thread when a new temporary file is created but before the wallet data is saved * to it. If you want to do something here like adjust permissions, go ahead and do so. The wallet is locked * whilst this method is run. */ public void onBeforeAutoSave(File tempFile); /** * Called on the auto-save thread after the newly created temporary file has been filled with data and renamed. * The wallet is locked whilst this method is run. */ public void onAfterAutoSave(File newlySavedFile); } /** * <p>Sets up the wallet to auto-save itself to the given file, using temp files with atomic renames to ensure * consistency. After connecting to a file, you no longer need to save the wallet manually, it will do it * whenever necessary. Protocol buffer serialization will be used.</p> * * <p>If delayTime is set, a background thread will be created and the wallet will only be saved to * disk every so many time units. If no changes have occurred for the given time period, nothing will be written. * In this way disk IO can be rate limited. It's a good idea to set this as otherwise the wallet can change very * frequently, eg if there are a lot of transactions in it or during block sync, and there will be a lot of redundant * writes. Note that when a new key is added, that always results in an immediate save regardless of * delayTime. <b>You should still save the wallet manually when your program is about to shut down as the JVM * will not wait for the background thread.</b></p> * * <p>An event listener can be provided. If a delay >0 was specified, it will be called on a background thread * with the wallet locked when an auto-save occurs. If delay is zero or you do something that always triggers * an immediate save, like adding a key, the event listener will be invoked on the calling threads.</p> * * @param f The destination file to save to. * @param delayTime How many time units to wait until saving the wallet on a background thread. * @param timeUnit the unit of measurement for delayTime. * @param eventListener callback to be informed when the auto-save thread does things, or null */ public void autosaveToFile(File f, long delayTime, TimeUnit timeUnit, AutosaveEventListener eventListener) { lock.lock(); try { Preconditions.checkArgument(delayTime >= 0); autosaveToFile = Preconditions.checkNotNull(f); if (delayTime > 0) { autosaveEventListener = eventListener; autosaveDelayMs = TimeUnit.MILLISECONDS.convert(delayTime, timeUnit); } } finally { lock.unlock(); } } private void queueAutoSave() { lock.lock(); try { if (this.autosaveToFile == null) return; if (autosaveDelayMs == 0) { // No delay time was specified, so save now. try { saveToFile(autosaveToFile); } catch (IOException e) { throw new RuntimeException(e); } } else { // If we need to, tell the auto save thread to wake us up. This will start the background thread if one // doesn't already exist. It will wake up once the delay expires and call autoSave(). // The background thread is shared between all wallets. if (!dirty) { dirty = true; AutosaveThread.registerForSave(this, autosaveDelayMs); } } } finally { lock.unlock(); } } /** * Uses protobuf serialization to save the wallet to the given file stream. To learn more about this file format, see * {@link WalletProtobufSerializer}. */ public void saveToFileStream(OutputStream f) throws IOException { lock.lock(); try { new WalletProtobufSerializer().writeWallet(this, f); } finally { lock.unlock(); } } /** Returns the parameters this wallet was created with. */ public NetworkParameters getParams() { return params; } /** * Returns a wallet deserialized from the given file. */ public static Wallet loadFromFile(File f) throws IOException { FileInputStream stream = new FileInputStream(f); try { return loadFromFileStream(stream); } finally { stream.close(); } } public boolean isConsistent() { lock.lock(); try { boolean success = true; // Pending and inactive can overlap, so merge them before counting HashSet<Transaction> pendingInactive = new HashSet<Transaction>(); pendingInactive.addAll(pending.values()); pendingInactive.addAll(inactive.values()); Set<Transaction> transactions = getTransactions(true, true); Set<Sha256Hash> hashes = new HashSet<Sha256Hash>(); for (Transaction tx : transactions) { hashes.add(tx.getHash()); } int size1 = transactions.size(); if (size1 != hashes.size()) { log.error("Two transactions with same hash"); success = false; } int size2 = unspent.size() + spent.size() + pendingInactive.size() + dead.size(); if (size1 != size2) { log.error("Inconsistent wallet sizes: {} {}", size1, size2); success = false; } for (Transaction tx : unspent.values()) { if (!tx.isConsistent(this, false)) { success = false; log.error("Inconsistent unspent tx {}", tx.getHashAsString()); } } for (Transaction tx : spent.values()) { if (!tx.isConsistent(this, true)) { success = false; log.error("Inconsistent spent tx {}", tx.getHashAsString()); } } if (!success) log.error(toString()); return success; } finally { lock.unlock(); } } /** * 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); } if (!wallet.isConsistent()) { log.error("Loaded an inconsistent wallet"); } return wallet; } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); createTransientState(); } /** * Called by the {@link BlockChain} when we receive a new filtered block that contains a transactions previously * received by a call to @{link receivePending}.<p> * * This is necessary for the internal book-keeping Wallet does. When a transaction is received that sends us * coins it is added to a pool so we can use it later to create spends. When a transaction is received that * consumes outputs they are marked as spent so they won't be used in future.<p> * * A transaction that spends our own coins can be received either because a spend we created was accepted by the * network and thus made it into a block, or because our keys are being shared between multiple instances and * some other node spent the coins instead. We still have to know about that to avoid accidentally trying to * double spend.<p> * * A transaction may be received multiple times if is included into blocks in parallel chains. The blockType * parameter describes whether the containing block is on the main/best chain or whether it's on a presently * inactive side chain. We must still record these transactions and the blocks they appear in because a future * block might change which chain is best causing a reorganize. A re-org can totally change our balance! */ public void notifyTransactionIsInBlock(Sha256Hash txHash, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException { lock.lock(); try { Transaction tx = pending.get(txHash); if (tx == null) return; receive(tx, block, blockType, false); } finally { lock.unlock(); } } /** The results of examining the dependency graph of a pending transaction for protocol abuse. */ protected static class AnalysisResult { // Which tx, if any, had a non-zero lock time. Transaction timeLocked; // In future, depth, fees, if any are non-standard, anything else that's interesting ... } /** * <p>Called when we have found a transaction (via network broadcast or otherwise) that is relevant to this wallet * and want to record it. Note that we <b>cannot verify these transactions at all</b>, they may spend fictional * coins or be otherwise invalid. They are useful to inform the user about coins they can expect to receive soon, * and if you trust the sender of the transaction you can choose to assume they are in fact valid and will not * be double spent as an optimization.</p> * * <p>Before this method is called, {@link Wallet#isPendingTransactionRelevant(Transaction)} should have been * called to decide whether the wallet cares about the transaction - if it does, then this method expects the * transaction and any dependencies it has which are still in the memory pool.</p> */ public void receivePending(Transaction tx, List<Transaction> dependencies) throws VerificationException { // Can run in a peer thread. This method will only be called if a prior call to isPendingTransactionRelevant // returned true, so we already know by this point that it sends coins to or from our wallet, or is a double // spend against one of our other pending transactions. // // Do a brief risk analysis of the transaction and its dependencies to check for any possible attacks. lock.lock(); try { // Repeat the check of relevancy here, even though the caller may have already done so - this is to avoid // race conditions where receivePending may be being called in parallel. if (!isPendingTransactionRelevant(tx)) return; AnalysisResult analysis = analyzeTransactionAndDependencies(tx, dependencies); if (analysis.timeLocked != null && !doesAcceptTimeLockedTransactions()) { log.warn("Transaction {}, dependency of {} has a time lock value of {}", new Object[]{ analysis.timeLocked.getHashAsString(), tx.getHashAsString(), analysis.timeLocked.getLockTime()}); return; } BigInteger valueSentToMe = tx.getValueSentToMe(this); BigInteger valueSentFromMe = tx.getValueSentFromMe(this); if (log.isInfoEnabled()) { log.info(String.format("Received a pending transaction %s that spends %s BTC from our own wallet," + " and sends us %s BTC", tx.getHashAsString(), Utils.digitalcoinValueToFriendlyString(valueSentFromMe), Utils.digitalcoinValueToFriendlyString(valueSentToMe))); } if (tx.getConfidence().getSource().equals(TransactionConfidence.Source.UNKNOWN)) { log.warn("Wallet received transaction with an unknown source. Consider tagging tx!"); } // Mark the tx as having been seen but is not yet in the chain. This will normally have been done already by // the Peer before we got to this point, but in some cases (unit tests, other sources of transactions) it may // have been missed out. ConfidenceType currentConfidence = tx.getConfidence().getConfidenceType(); if (currentConfidence == ConfidenceType.UNKNOWN) { tx.getConfidence().setConfidenceType(ConfidenceType.NOT_SEEN_IN_CHAIN); // Manually invoke the wallet tx confidence listener here as we didn't yet commit therefore the // txConfidenceListener wasn't added. invokeOnTransactionConfidenceChanged(tx); } // If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This // ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the // timestamp on the transaction and registers/runs event listeners. // // Note that after we return from this function, the wallet may have been modified. commitTx(tx); } finally { lock.unlock(); } } private static AnalysisResult analyzeTransactionAndDependencies(Transaction tx, List<Transaction> dependencies) { AnalysisResult result = new AnalysisResult(); if (tx.getLockTime() > 0) result.timeLocked = tx; if (dependencies != null) { for (Transaction dep : dependencies) { if (dep.getLockTime() > 0) { result.timeLocked = dep; } } } return result; } /** * This method is used by a {@link Peer} to find out if a transaction that has been announced is interesting, * that is, whether we should bother downloading its dependencies and exploring the transaction to decide how * risky it is. If this method returns true then {@link Wallet#receivePending(Transaction, java.util.List)} * will soon be called with the transactions dependencies as well. */ boolean isPendingTransactionRelevant(Transaction tx) throws ScriptException { lock.lock(); try { // Ignore it if we already know about this transaction. Receiving a pending transaction never moves it // between pools. EnumSet<Pool> containingPools = getContainingPools(tx); if (!containingPools.equals(EnumSet.noneOf(Pool.class))) { log.debug("Received tx we already saw in a block or created ourselves: " + tx.getHashAsString()); return false; } // We only care about transactions that: // - Send us coins // - Spend our coins if (!isTransactionRelevant(tx)) { log.debug("Received tx that isn't relevant to this wallet, discarding."); return false; } if (tx.getLockTime() > 0 && !acceptTimeLockedTransactions) { log.warn("Received transaction {} with a lock time of {}, but not configured to accept these, discarding", tx.getHashAsString(), tx.getLockTime()); return false; } return true; } finally { lock.unlock(); } } /** * <p>Returns true if the given transaction sends coins to any of our keys, or has inputs spending any of our outputs, * and if includeDoubleSpending is true, also returns true if tx has inputs that are spending outputs which are * not ours but which are spent by pending transactions.</p> * * <p>Note that if the tx has inputs containing one of our keys, but the connected transaction is not in the wallet, * it will not be considered relevant.</p> */ public boolean isTransactionRelevant(Transaction tx) throws ScriptException { lock.lock(); try { return tx.getValueSentFromMe(this).compareTo(BigInteger.ZERO) > 0 || tx.getValueSentToMe(this).compareTo(BigInteger.ZERO) > 0 || checkForDoubleSpendAgainstPending(tx, false); } finally { lock.unlock(); } } /** * Checks if "tx" is spending any inputs of pending transactions. Not a general check, but it can work even if * the double spent inputs are not ours. Returns the pending tx that was double spent or null if none found. */ private boolean checkForDoubleSpendAgainstPending(Transaction tx, boolean takeAction) { checkState(lock.isLocked()); // Compile a set of outpoints that are spent by tx. HashSet<TransactionOutPoint> outpoints = new HashSet<TransactionOutPoint>(); for (TransactionInput input : tx.getInputs()) { outpoints.add(input.getOutpoint()); } // Now for each pending transaction, see if it shares any outpoints with this tx. for (Transaction p : pending.values()) { for (TransactionInput input : p.getInputs()) { // This relies on the fact that TransactionOutPoint equality is defined at the protocol not object // level - outpoints from two different inputs that point to the same output compare the same. TransactionOutPoint outpoint = input.getOutpoint(); if (outpoints.contains(outpoint)) { // It does, it's a double spend against the pending pool, which makes it relevant. if (takeAction) { // Look for the actual input object in tx that is double spending. TransactionInput overridingInput = null; for (TransactionInput txInput : tx.getInputs()) { if (txInput.getOutpoint().equals(outpoint)) overridingInput = txInput; } killTx(tx, checkNotNull(overridingInput), p); } return true; } } } return false; } /** * Called by the {@link BlockChain} when we receive a new block that sends coins to one of our addresses or * spends coins from one of our addresses (note that a single transaction can do both).<p> * * This is necessary for the internal book-keeping Wallet does. When a transaction is received that sends us * coins it is added to a pool so we can use it later to create spends. When a transaction is received that * consumes outputs they are marked as spent so they won't be used in future.<p> * * A transaction that spends our own coins can be received either because a spend we created was accepted by the * network and thus made it into a block, or because our keys are being shared between multiple instances and * some other node spent the coins instead. We still have to know about that to avoid accidentally trying to * double spend.<p> * * A transaction may be received multiple times if is included into blocks in parallel chains. The blockType * parameter describes whether the containing block is on the main/best chain or whether it's on a presently * inactive side chain. We must still record these transactions and the blocks they appear in because a future * block might change which chain is best causing a reorganize. A re-org can totally change our balance! */ public void receiveFromBlock(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException { lock.lock(); try { receive(tx, block, blockType, false); } finally { lock.unlock(); } } private void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType, boolean reorg) throws VerificationException { // Runs in a peer thread. checkState(lock.isLocked()); BigInteger prevBalance = getBalance(); Sha256Hash txHash = tx.getHash(); boolean bestChain = blockType == BlockChain.NewBlockType.BEST_CHAIN; boolean sideChain = blockType == BlockChain.NewBlockType.SIDE_CHAIN; BigInteger valueSentFromMe = tx.getValueSentFromMe(this); BigInteger valueSentToMe = tx.getValueSentToMe(this); BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe); if (!reorg) { log.info("Received tx {} for {} BTC: {}", new Object[]{sideChain ? "on a side chain" : "", digitalcoinValueToFriendlyString(valueDifference), tx.getHashAsString()}); } onWalletChangedSuppressions++; // If this transaction is already in the wallet we may need to move it into a different pool. At the very // least we need to ensure we're manipulating the canonical object rather than a duplicate. Transaction wtx; if ((wtx = pending.remove(txHash)) != null) { // Make sure "tx" is always the canonical object we want to manipulate, send to event handlers, etc. tx = wtx; log.info(" <-pending"); // A transaction we created appeared in a block. Probably this is a spend we broadcast that has been // accepted by the network. if (bestChain) { // Was confirmed. if (tx.isEveryOwnedOutputSpent(this)) { // There were no change transactions so this tx is fully spent log.info(" ->spent"); addWalletTransaction(Pool.SPENT, tx); } else { // There was change back to us, or this tx was purely a spend back to ourselves (perhaps for // anonymization purposes). log.info(" ->unspent"); addWalletTransaction(Pool.UNSPENT, tx); } } else if (sideChain) { // The transaction was accepted on an inactive side chain, but not yet by the best chain. log.info(" ->inactive"); // It's OK for this to already be in the inactive pool because there can be multiple independent side // chains in which it appears: // // b1 --> b2 // \-> b3 // \-> b4 (at this point it's already present in 'inactive' boolean alreadyPresent = inactive.put(tx.getHash(), tx) != null; if (alreadyPresent) log.info("Saw a transaction be incorporated into multiple independent side chains"); // Put it back into the pending pool, because 'pending' means 'waiting to be included in best chain'. pending.put(tx.getHash(), tx); } } else { // This TX wasn't in the memory pool. It could be sending us coins and also spending our own coins if keys // are being shared between different wallets. if (sideChain) { if (unspent.containsKey(tx.getHash()) || spent.containsKey(tx.getHash())) { // This side chain block contains transactions that already appeared in the best chain. It's normal, // we don't need to consider this transaction inactive, we can just ignore it. } else { log.info(" ->inactive"); addWalletTransaction(Pool.INACTIVE, tx); } } else if (bestChain) { // Saw a non-pending transaction appear on the best chain, ie, we are replaying the chain or a spend // that we never saw broadcast (and did not originate) got included. // // TODO: This can trigger tx confidence listeners to be run in the case of double spends. We may need to // delay the execution of the listeners until the bottom to avoid the wallet mutating during updates. processTxFromBestChain(tx); } } log.info("Balance is now: " + digitalcoinValueToFriendlyString(getBalance())); // WARNING: The code beyond this point can trigger event listeners on transaction confidence objects, which are // in turn allowed to re-enter the Wallet. This means we cannot assume anything about the state of the wallet // from now on. The balance just received may already be spent. if (block != null) { // Mark the tx as appearing in this block so we can find it later after a re-org. This also tells the tx // confidence object about the block and sets its work done/depth appropriately. tx.setBlockAppearance(block, bestChain); if (bestChain) { // Don't notify this tx of work done in notifyNewBestBlock which will be called immediately after // this method has been called by BlockChain for all relevant transactions. Otherwise we'd double // count. ignoreNextNewBlock.add(txHash); } } // Inform anyone interested that we have received or sent coins but only if: // - This is not due to a re-org. // - The coins appeared on the best chain. // - We did in fact receive some new money. // - We have not already informed the user about the coins when we received the tx broadcast, or for our // own spends. If users want to know when a broadcast tx becomes confirmed, they need to use tx confidence // listeners. // // TODO: Decide whether to run the event listeners, if a tx confidence listener already modified the wallet. boolean wasPending = wtx != null; if (!reorg && bestChain && !wasPending) { BigInteger newBalance = getBalance(); int diff = valueDifference.compareTo(BigInteger.ZERO); // We pick one callback based on the value difference, though a tx can of course both send and receive // coins from the wallet. if (diff > 0) { invokeOnCoinsReceived(tx, prevBalance, newBalance); } else if (diff < 0) { invokeOnCoinsSent(tx, prevBalance, newBalance); } else { // We have a transaction that didn't change our balance. Probably we sent coins between our own keys. invokeOnWalletChanged(); } } // Wallet change notification will be sent shortly after the block is finished processing, in notifyNewBestBlock onWalletChangedSuppressions--; checkState(isConsistent()); queueAutoSave(); } /** * <p>Called by the {@link BlockChain} when a new block on the best chain is seen, AFTER relevant wallet * transactions are extracted and sent to us UNLESS the new block caused a re-org, in which case this will * not be called (the {@link Wallet#reorganize(StoredBlock, java.util.List, java.util.List)} method will * call this one in that case).</p> * <p/> * <p>Used to update confidence data in each transaction and last seen block hash. Triggers auto saving. * Invokes the onWalletChanged event listener if there were any affected transactions.</p> */ public void notifyNewBestBlock(StoredBlock block) throws VerificationException { // Check to see if this block has been seen before. Sha256Hash newBlockHash = block.getHeader().getHash(); if (newBlockHash.equals(getLastBlockSeenHash())) return; lock.lock(); try { // Store the new block hash. setLastBlockSeenHash(newBlockHash); setLastBlockSeenHeight(block.getHeight()); // TODO: Clarify the code below. // Notify all the BUILDING transactions of the new block. // This is so that they can update their work done and depth. onWalletChangedSuppressions++; Set<Transaction> transactions = getTransactions(true, false); for (Transaction tx : transactions) { if (ignoreNextNewBlock.contains(tx.getHash())) { // tx was already processed in receive() due to it appearing in this block, so we don't want to // notify the tx confidence of work done twice, it'd result in miscounting. ignoreNextNewBlock.remove(tx.getHash()); } else { tx.getConfidence().notifyWorkDone(block.getHeader()); } } queueAutoSave(); onWalletChangedSuppressions--; invokeOnWalletChanged(); } finally { lock.unlock(); } } /** * Handle when a transaction becomes newly active on the best chain, either due to receiving a new block or a * re-org making inactive transactions active. */ private void processTxFromBestChain(Transaction tx) throws VerificationException { checkState(lock.isLocked()); // This TX may spend our existing outputs even though it was not pending. This can happen in unit // tests, if keys are moved between wallets, if we're catching up to the chain given only a set of keys, // or if a dead coinbase transaction has moved back onto the main chain. boolean isDeadCoinbase = tx.isCoinBase() && dead.containsKey(tx.getHash()); if (isDeadCoinbase) { // There is a dead coinbase tx being received on the best chain. A coinbase tx is made dead when it moves // to a side chain but it can be switched back on a reorg and 'resurrected' back to spent or unspent. // So take it out of the dead pool. log.info(" coinbase tx {} <-dead: confidence {}", tx.getHashAsString(), tx.getConfidence().getConfidenceType().name()); dead.remove(tx.getHash()); } if (inactive.containsKey(tx.getHash())) { // This transaction was seen first on a side chain, but now it's also been seen in the best chain. // So we don't need to track it as inactive anymore. log.info(" new tx {} <-inactive", tx.getHashAsString()); inactive.remove(tx.getHash()); } // Update tx and other unspent/pending transactions by connecting inputs/outputs. updateForSpends(tx, true); // Now make sure it ends up in the right pool. Also, handle the case where this TX is double-spending // against our pending transactions. Note that a tx may double spend our pending transactions and also send // us money/spend our money. boolean hasOutputsToMe = tx.getValueSentToMe(this, true).compareTo(BigInteger.ZERO) > 0; if (hasOutputsToMe) { // Needs to go into either unspent or spent (if the outputs were already spent by a pending tx). if (tx.isEveryOwnedOutputSpent(this)) { log.info(" new tx {} ->spent (by pending)", tx.getHashAsString()); addWalletTransaction(Pool.SPENT, tx); } else { log.info(" new tx {} ->unspent", tx.getHashAsString()); addWalletTransaction(Pool.UNSPENT, tx); } } else if (tx.getValueSentFromMe(this).compareTo(BigInteger.ZERO) > 0) { // Didn't send us any money, but did spend some. Keep it around for record keeping purposes. log.info(" new tx {} ->spent", tx.getHashAsString()); addWalletTransaction(Pool.SPENT, tx); } checkForDoubleSpendAgainstPending(tx, true); } /** * <p>Updates the wallet by checking if this TX spends any of our outputs, and marking them as spent if so. If * fromChain is true, also checks to see if any pending transaction spends outputs of this transaction and marks * the spent flags appropriately.</p> * * <p>It can be called in two contexts. One is when we receive a transaction on the best chain but it wasn't pending, * this most commonly happens when we have a set of keys but the wallet transactions were wiped and we are catching * up with the block chain. It can also happen if a block includes a transaction we never saw at broadcast time. * If this tx double spends, it takes precedence over our pending transactions and the pending tx goes dead.</p> * * <p>The other context it can be called is from {@link Wallet#receivePending(Transaction, java.util.List)}, * ie we saw a tx be broadcast or one was submitted directly that spends our own coins. If this tx double spends * it does NOT take precedence because the winner will be resolved by the miners - we assume that our version will * win, if we are wrong then when a block appears the tx will go dead.</p> * * @param tx The transaction which is being updated. * @param fromChain If true, the tx appeared on the current best chain, if false it was pending. */ private void updateForSpends(Transaction tx, boolean fromChain) throws VerificationException { checkState(lock.isLocked()); for (TransactionInput input : tx.getInputs()) { TransactionInput.ConnectionResult result = input.connect(unspent, TransactionInput.ConnectMode.ABORT_ON_CONFLICT); if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { // Not found in the unspent map. Try again with the spent map. result = input.connect(spent, TransactionInput.ConnectMode.ABORT_ON_CONFLICT); if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { // Not found in the unspent and spent maps. Try again with the pending map. result = input.connect(pending, TransactionInput.ConnectMode.ABORT_ON_CONFLICT); if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { // Doesn't spend any of our outputs or is coinbase. continue; } } } if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) { if (fromChain) { // This will be handled later by processTxFromBestChain. } else { // We saw two pending transactions that double spend each other. We don't know which will win. // Either that, or we somehow allowed ourselves to create double spends ourselves! // TODO: Find some way to communicate to the user that both transactions in jeopardy. log.warn("Saw double spend from another pending transaction, ignoring tx {}", tx.getHashAsString()); log.warn(" offending input is input {}", tx.getInputs().indexOf(input)); } } else if (result == TransactionInput.ConnectionResult.SUCCESS) { // Otherwise we saw a transaction spend our coins, but we didn't try and spend them ourselves yet. // The outputs are already marked as spent by the connect call above, so check if there are any more for // us to use. Move if not. Transaction connected = checkNotNull(input.getOutpoint().fromTx); maybeMovePool(connected, "prevtx"); } } // Now check each output and see if there is a pending transaction which spends it. This shouldn't normally // ever occur because we expect transactions to arrive in temporal order, but this assumption can be violated // when we receive a pending transaction from the mempool that is relevant to us, which spends coins that we // didn't see arrive on the best chain yet. For instance, because of a chain replay or because of our keys were // used by another wallet somewhere else. if (fromChain) { for (Transaction pendingTx : pending.values()) { for (TransactionInput input : pendingTx.getInputs()) { TransactionInput.ConnectionResult result = input.connect(tx, TransactionInput.ConnectMode.ABORT_ON_CONFLICT); // This TX is supposed to have just appeared on the best chain, so its outputs should not be marked // as spent yet. If they are, it means something is happening out of order. checkState(result != TransactionInput.ConnectionResult.ALREADY_SPENT); if (result == TransactionInput.ConnectionResult.SUCCESS) { log.info("Connected pending tx input {}:{}", pendingTx.getHashAsString(), pendingTx.getInputs().indexOf(input)); } } // If the transactions outputs are now all spent, it will be moved into the spent pool by the // processTxFromBestChain method. } } } // Updates the wallet when a double spend occurs. private void killTx(Transaction overridingTx, TransactionInput overridingInput, Transaction killedTx) { TransactionOutPoint overriddenOutPoint = overridingInput.getOutpoint(); // It is expected that we may not have the overridden/double-spent tx in our wallet ... in the (common?!) case // where somebody is stealing money from us, the overriden tx belongs to someone else. log.warn("Saw double spend of {} from chain override pending tx {}", overriddenOutPoint, killedTx.getHashAsString()); log.warn(" <-pending ->dead killed by {}", overridingTx.getHashAsString()); pending.remove(killedTx.getHash()); addWalletTransaction(Pool.DEAD, killedTx); log.info("Disconnecting inputs of the newly dead tx"); for (TransactionInput deadInput : killedTx.getInputs()) { Transaction connected = deadInput.getOutpoint().fromTx; if (connected == null) continue; deadInput.disconnect(); maybeMovePool(connected, "kill"); } // Try and connect the overriding input to something in our wallet. It's expected that this will mostly fail // because when somebody else is double-spending away a payment they made to us, we won't have the overridden // tx as it's not ours to begin with. It'll only be found if we're double spending our own payments. log.info("Trying to connect overriding tx back"); TransactionInput.ConnectionResult result = overridingInput.connect(unspent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT); if (result == TransactionInput.ConnectionResult.SUCCESS) { maybeMovePool(overridingInput.getOutpoint().fromTx, "kill"); } else { result = overridingInput.connect(spent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT); if (result == TransactionInput.ConnectionResult.SUCCESS) { maybeMovePool(overridingInput.getOutpoint().fromTx, "kill"); } } log.info("Informing tx listeners of double spend event"); killedTx.getConfidence().setOverridingTransaction(overridingTx); // RE-ENTRY POINT // TODO: Recursively kill other transactions that were double spent. } /** * If the transactions outputs are all marked as spent, and it's in the unspent map, move it. * If the owned transactions outputs are not all marked as spent, and it's in the spent map, move it. */ private void maybeMovePool(Transaction tx, String context) { checkState(lock.isLocked()); if (tx.isEveryOwnedOutputSpent(this)) { // There's nothing left I can spend in this transaction. if (unspent.remove(tx.getHash()) != null) { if (log.isInfoEnabled()) { log.info(" {} {} <-unspent ->spent", tx.getHashAsString(), context); } spent.put(tx.getHash(), tx); } } else { if (spent.remove(tx.getHash()) != null) { if (log.isInfoEnabled()) { log.info(" {} {} <-spent ->unspent", tx.getHashAsString(), context); } unspent.put(tx.getHash(), tx); } } } /** * Adds an event listener object. Methods on this object are called when something interesting happens, * like receiving money. */ public void addEventListener(WalletEventListener listener) { eventListeners.add(listener); } /** * Removes the given event listener object. Returns true if the listener was removed, * false if that listener was never added. */ public boolean removeEventListener(WalletEventListener listener) { return eventListeners.remove(listener); } /** * <p>Updates the wallet with the given transaction: puts it into the pending pool, sets the spent flags and runs * the onCoinsSent/onCoinsReceived event listener. Used in two situations:</p> * * <ol> * <li>When we have just successfully transmitted the tx we created to the network.</li> * <li>When we receive a pending transaction that didn't appear in the chain yet, and we did not create it.</li> * </ol> * * <p>Triggers an auto save.</p> */ public void commitTx(Transaction tx) throws VerificationException { lock.lock(); try { checkArgument(!pending.containsKey(tx.getHash()), "commitTx called on the same transaction twice"); log.info("commitTx of {}", tx.getHashAsString()); BigInteger balance = getBalance(); tx.setUpdateTime(Utils.now()); // Mark the outputs we're spending as spent so we won't try and use them in future creations. This will also // move any transactions that are now fully spent to the spent map so we can skip them when creating future // spends. updateForSpends(tx, false); // Add to the pending pool. It'll be moved out once we receive this transaction on the best chain. // This also registers txConfidenceListener so wallet listeners get informed. log.info("->pending: {}", tx.getHashAsString()); addWalletTransaction(Pool.PENDING, tx); // Event listeners may re-enter so we cannot make assumptions about wallet state after this loop completes. try { BigInteger valueSentFromMe = tx.getValueSentFromMe(this); BigInteger valueSentToMe = tx.getValueSentToMe(this); BigInteger newBalance = balance.add(valueSentToMe).subtract(valueSentFromMe); if (valueSentToMe.compareTo(BigInteger.ZERO) > 0) invokeOnCoinsReceived(tx, balance, newBalance); if (valueSentFromMe.compareTo(BigInteger.ZERO) > 0) invokeOnCoinsSent(tx, balance, newBalance); invokeOnWalletChanged(); } catch (ScriptException e) { // Cannot happen as we just created this transaction ourselves. throw new RuntimeException(e); } checkState(isConsistent()); queueAutoSave(); } finally { lock.unlock(); } } /** * Returns a set of all transactions in the wallet. * @param includeDead If true, transactions that were overridden by a double spend are included. * @param includeInactive If true, transactions that are on side chains (are unspendable) are included. */ public Set<Transaction> getTransactions(boolean includeDead, boolean includeInactive) { lock.lock(); try { Set<Transaction> all = new HashSet<Transaction>(); all.addAll(unspent.values()); all.addAll(spent.values()); all.addAll(pending.values()); if (includeDead) all.addAll(dead.values()); if (includeInactive) all.addAll(inactive.values()); return all; } finally { lock.unlock(); } } /** * Returns a set of all WalletTransactions in the wallet. */ public Iterable<WalletTransaction> getWalletTransactions() { lock.lock(); try { HashSet<Transaction> pendingInactive = new HashSet<Transaction>(); pendingInactive.addAll(pending.values()); pendingInactive.retainAll(inactive.values()); HashSet<Transaction> onlyPending = new HashSet<Transaction>(); HashSet<Transaction> onlyInactive = new HashSet<Transaction>(); onlyPending.addAll(pending.values()); onlyPending.removeAll(pendingInactive); onlyInactive.addAll(inactive.values()); onlyInactive.removeAll(pendingInactive); Set<WalletTransaction> all = new HashSet<WalletTransaction>(); addWalletTransactionsToSet(all, Pool.UNSPENT, unspent.values()); addWalletTransactionsToSet(all, Pool.SPENT, spent.values()); addWalletTransactionsToSet(all, Pool.DEAD, dead.values()); addWalletTransactionsToSet(all, Pool.PENDING, onlyPending); addWalletTransactionsToSet(all, Pool.INACTIVE, onlyInactive); addWalletTransactionsToSet(all, Pool.PENDING_INACTIVE, pendingInactive); return all; } finally { lock.unlock(); } } private static void addWalletTransactionsToSet(Set<WalletTransaction> txs, Pool poolType, Collection<Transaction> pool) { for (Transaction tx : pool) { txs.add(new WalletTransaction(poolType, tx)); } } /** * Adds a transaction that has been associated with a particular wallet pool. This is intended for usage by * deserialization code, such as the {@link WalletProtobufSerializer} class. It isn't normally useful for * applications. It does not trigger auto saving. */ public void addWalletTransaction(WalletTransaction wtx) { lock.lock(); try { addWalletTransaction(wtx.getPool(), wtx.getTransaction()); } finally { lock.unlock(); } } /** * Adds the given transaction to the given pools and registers a confidence change listener on it. */ private void addWalletTransaction(Pool pool, Transaction tx) { checkState(lock.isLocked()); switch (pool) { case UNSPENT: Preconditions.checkState(unspent.put(tx.getHash(), tx) == null); break; case SPENT: Preconditions.checkState(spent.put(tx.getHash(), tx) == null); break; case PENDING: Preconditions.checkState(pending.put(tx.getHash(), tx) == null); break; case DEAD: Preconditions.checkState(dead.put(tx.getHash(), tx) == null); break; case INACTIVE: Preconditions.checkState(inactive.put(tx.getHash(), tx) == null); break; case PENDING_INACTIVE: Preconditions.checkState(pending.put(tx.getHash(), tx) == null); Preconditions.checkState(inactive.put(tx.getHash(), tx) == null); break; default: throw new RuntimeException("Unknown wallet transaction type " + pool); } // This is safe even if the listener has been added before, as TransactionConfidence ignores duplicate // registration requests. That makes the code in the wallet simpler. tx.getConfidence().addEventListener(txConfidenceListener); } /** * Returns all non-dead, active transactions ordered by recency. */ public List<Transaction> getTransactionsByTime() { return getRecentTransactions(0, false); } /** * Returns an list of N transactions, ordered by increasing age. Transactions on side chains are not included. * Dead transactions (overridden by double spends) are optionally included. <p> * <p/> * Note: the current implementation is O(num transactions in wallet). Regardless of how many transactions are * requested, the cost is always the same. In future, requesting smaller numbers of transactions may be faster * depending on how the wallet is implemented (eg if backed by a database). */ public List<Transaction> getRecentTransactions(int numTransactions, boolean includeDead) { lock.lock(); try { checkArgument(numTransactions >= 0); // Firstly, put all transactions into an array. int size = getPoolSize(Pool.UNSPENT) + getPoolSize(Pool.SPENT) + getPoolSize(Pool.PENDING); if (numTransactions > size || numTransactions == 0) { numTransactions = size; } ArrayList<Transaction> all = new ArrayList<Transaction>(getTransactions(includeDead, false)); // Order by date. Collections.sort(all, Collections.reverseOrder(new Comparator<Transaction>() { public int compare(Transaction t1, Transaction t2) { return t1.getUpdateTime().compareTo(t2.getUpdateTime()); } })); if (numTransactions == all.size()) { return all; } else { all.subList(numTransactions, all.size()).clear(); return all; } } finally { lock.unlock(); } } /** * Returns a transaction object given its hash, if it exists in this wallet, or null otherwise. */ public Transaction getTransaction(Sha256Hash hash) { lock.lock(); try { Transaction tx; if ((tx = pending.get(hash)) != null) return tx; else if ((tx = unspent.get(hash)) != null) return tx; else if ((tx = spent.get(hash)) != null) return tx; else if ((tx = inactive.get(hash)) != null) return tx; else if ((tx = dead.get(hash)) != null) return tx; return null; } finally { lock.unlock(); } } /** * Deletes transactions which appeared above the given block height from the wallet, but does not touch the keys. * This is useful if you have some keys and wish to replay the block chain into the wallet in order to pick them up. * Triggers auto saving. */ public void clearTransactions(int fromHeight) { lock.lock(); try { if (fromHeight == 0) { unspent.clear(); spent.clear(); pending.clear(); inactive.clear(); dead.clear(); queueAutoSave(); } else { throw new UnsupportedOperationException(); } } finally { lock.unlock(); } } EnumSet<Pool> getContainingPools(Transaction tx) { lock.lock(); try { EnumSet<Pool> result = EnumSet.noneOf(Pool.class); Sha256Hash txHash = tx.getHash(); if (unspent.containsKey(txHash)) { result.add(Pool.UNSPENT); } if (spent.containsKey(txHash)) { result.add(Pool.SPENT); } if (pending.containsKey(txHash)) { result.add(Pool.PENDING); } if (inactive.containsKey(txHash)) { result.add(Pool.INACTIVE); } if (dead.containsKey(txHash)) { result.add(Pool.DEAD); } return result; } finally { lock.unlock(); } } int getPoolSize(WalletTransaction.Pool pool) { lock.lock(); try { switch (pool) { case UNSPENT: return unspent.size(); case SPENT: return spent.size(); case PENDING: return pending.size(); case INACTIVE: return inactive.size(); case DEAD: return dead.size(); case ALL: return unspent.size() + spent.size() + pending.size() + inactive.size() + dead.size(); } throw new RuntimeException("Unreachable"); } finally { lock.unlock(); } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // SEND APIS // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** A SendResult is returned to you as part of sending coins to a recipient. */ public static class SendResult { /** The Digitalcoin transaction message that moves the money. */ public Transaction tx; /** A future that will complete once the tx message has been successfully broadcast to the network. */ public ListenableFuture<Transaction> broadcastComplete; } /** * A SendRequest gives the wallet information about precisely how to send money to a recipient or set of recipients. * Static methods are provided to help you create SendRequests and there are a few helper methods on the wallet that * just simplify the most common use cases. You may wish to customize a SendRequest if you want to attach a fee or * modify the change address. */ public static class SendRequest { /** * A transaction, probably incomplete, that describes the outline of what you want to do. This typically will * mean it has some outputs to the intended destinations, but no inputs or change address (and therefore no * fees) - the wallet will calculate all that for you and update tx later. */ public Transaction tx; /** * "Change" means the difference between the value gathered by a transactions inputs (the size of which you * don't really control as it depends on who sent you money), and the value being sent somewhere else. The * change address should be selected from this wallet, normally. <b>If null this will be chosen for you.</b> */ public Address changeAddress = null; /** * A transaction can have a fee attached, which is defined as the difference between the input values * and output values. Any value taken in that is not provided to an output can be claimed by a miner. This * is how mining is incentivized in later years of the Digitalcoin system when inflation drops. It also provides * a way for people to prioritize their transactions over others and is used as a way to make denial of service * attacks expensive. Some transactions require a fee due to their structure - currently digitalcoinj does not * correctly calculate this! As of late 2012 most transactions require no fee. */ public BigInteger fee = BigInteger.ZERO; /** * The AES key to use to decrypt the private keys before signing. * If null then no decryption will be performed and if decryption is required an exception will be thrown. * You can get this from a password by doing wallet.getKeyCrypter().derivePassword(password). */ public KeyParameter aesKey = null; // Tracks if this has been passed to wallet.completeTx already: just a safety check. private boolean completed; private SendRequest() {} public static SendRequest to(Address destination, BigInteger value) { SendRequest req = new Wallet.SendRequest(); req.tx = new Transaction(destination.getParameters()); req.tx.addOutput(value, destination); return req; } public static SendRequest to(NetworkParameters params, ECKey destination, BigInteger value) { SendRequest req = new SendRequest(); req.tx = new Transaction(params); req.tx.addOutput(value, destination); return req; } /** Simply wraps a pre-built incomplete transaction provided by you. */ public static SendRequest forTx(Transaction tx) { SendRequest req = new SendRequest(); req.tx = tx; return req; } } /** * <p>Statelessly creates a transaction that sends the given value to address. The change is sent to * {@link Wallet#getChangeAddress()}, so you must have added at least one key.</p> * * <p>If you just want to send money quickly, you probably want * {@link Wallet#sendCoins(PeerGroup, Address, java.math.BigInteger)} instead. That will create the sending * transaction, commit to the wallet and broadcast it to the network all in one go. This method is lower level * and lets you see the proposed transaction before anything is done with it.</p> * * <p>This is a helper method that is equivalent to using {@link Wallet.SendRequest#to(Address, java.math.BigInteger)} * followed by {@link Wallet#completeTx(com.google.digitalcoin.core.Wallet.SendRequest)} and returning the requests * transaction object. If you want more control over the process, just do those two steps yourself.</p> * * <p>IMPORTANT: This method does NOT update the wallet. If you call createSend again you may get two transactions * that spend the same coins. You have to call {@link Wallet#commitTx(Transaction)} on the created transaction to * prevent this, but that should only occur once the transaction has been accepted by the network. This implies * you cannot have more than one outstanding sending tx at once.</p> * * @param address The Digitalcoin address to send the money to. * @param nanocoins How much currency to send, in nanocoins. * @return either the created Transaction or null if there are insufficient coins. * coins as spent until commitTx is called on the result. */ public Transaction createSend(Address address, BigInteger nanocoins) { SendRequest req = SendRequest.to(address, nanocoins); if (completeTx(req)) { return req.tx; } else { return null; // No money. } } /** * Sends coins to the given address but does not broadcast the resulting pending transaction. It is still stored * in the wallet, so when the wallet is added to a {@link PeerGroup} or {@link Peer} the transaction will be * announced to the network. The given {@link SendRequest} is completed first using * {@link Wallet#completeTx(com.google.digitalcoin.core.Wallet.SendRequest)} to make it valid. * * @return the Transaction that was created, or null if there are insufficient coins in the wallet. */ public Transaction sendCoinsOffline(SendRequest request) { lock.lock(); try { if (!completeTx(request)) return null; // Not enough money! :-( commitTx(request.tx); return request.tx; } catch (VerificationException e) { throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves. } finally { lock.unlock(); } } /** * <p>Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to * {@link Wallet#getChangeAddress()}. No fee is attached <b>even if one would be required</b>.</p> * * <p>The returned object provides both the transaction, and a future that can be used to learn when the broadcast * is complete. Complete means, if the PeerGroup is limited to only one connection, when it was written out to * the socket. Otherwise when the transaction is written out and we heard it back from a different peer.</p> * * <p>Note that the sending transaction is committed to the wallet immediately, not when the transaction is * successfully broadcast. This means that even if the network hasn't heard about your transaction you won't be * able to spend those same coins again.</p> * * @param peerGroup a PeerGroup to use for broadcast or null. * @param to Which address to send coins to. * @param value How much value to send. You can use Utils.toNanoCoins() to calculate this. * @return An object containing the transaction that was created, and a future for the broadcast of it. */ public SendResult sendCoins(PeerGroup peerGroup, Address to, BigInteger value) { SendRequest request = SendRequest.to(to, value); return sendCoins(peerGroup, request); } /** * <p>Sends coins according to the given request, via the given {@link PeerGroup}.</p> * * <p>The returned object provides both the transaction, and a future that can be used to learn when the broadcast * is complete. Complete means, if the PeerGroup is limited to only one connection, when it was written out to * the socket. Otherwise when the transaction is written out and we heard it back from a different peer.</p> * * <p>Note that the sending transaction is committed to the wallet immediately, not when the transaction is * successfully broadcast. This means that even if the network hasn't heard about your transaction you won't be * able to spend those same coins again.</p> * * @param peerGroup a PeerGroup to use for broadcast or null. * @param request the SendRequest that describes what to do, get one using static methods on SendRequest itself. * @return An object containing the transaction that was created, and a future for the broadcast of it. */ public SendResult sendCoins(PeerGroup peerGroup, SendRequest request) { // Does not need to be synchronized as sendCoinsOffline is and the rest is all thread-local. // Commit the TX to the wallet immediately so the spent coins won't be reused. // TODO: We should probably allow the request to specify tx commit only after the network has accepted it. Transaction tx = sendCoinsOffline(request); if (tx == null) return null; // Not enough money. SendResult result = new SendResult(); result.tx = tx; // The tx has been committed to the pending pool by this point (via sendCoinsOffline -> commitTx), so it has // a txConfidenceListener registered. Once the tx is broadcast the peers will update the memory pool with the // count of seen peers, the memory pool will update the transaction confidence object, that will invoke the // txConfidenceListener which will in turn invoke the wallets event listener onTransactionConfidenceChanged // method. result.broadcastComplete = peerGroup.broadcastTransaction(tx); return result; } /** * Sends coins to the given address, via the given {@link Peer}. Change is returned to {@link Wallet#getChangeAddress()}. * If an exception is thrown by {@link Peer#sendMessage(Message)} the transaction is still committed, so the * pending transaction must be broadcast <b>by you</b> at some other time. * * @return The {@link Transaction} that was created or null if there was insufficient balance to send the coins. * @throws IOException if there was a problem broadcasting the transaction */ public Transaction sendCoins(Peer peer, SendRequest request) throws IOException { Transaction tx = sendCoinsOffline(request); if (tx == null) return null; // Not enough money. peer.sendMessage(tx); return tx; } /** * Given a spend request containing an incomplete transaction, makes it valid by adding inputs and outputs according * to the instructions in the request. The transaction in the request is modified by this method. * * @param req a SendRequest that contains the incomplete transaction and details for how to make it valid. * @throws IllegalArgumentException if you try and complete the same SendRequest twice. * @return False if we cannot afford this send, true otherwise. */ public boolean completeTx(SendRequest req) { lock.lock(); try { Preconditions.checkArgument(!req.completed, "Given SendRequest has already been completed."); // Calculate the amount of value we need to import. BigInteger value = BigInteger.ZERO; for (TransactionOutput output : req.tx.getOutputs()) { value = value.add(output.getValue()); } value = value.add(req.fee); log.info("Completing send tx with {} outputs totalling {}", req.tx.getOutputs().size(), digitalcoinValueToFriendlyString(value)); // Calculate a list of ALL potential candidates for spending and then ask a coin selector to provide us // with the actual outputs that'll be used to gather the required amount of value. In this way, users // can customize coin selection policies. // // Note that this code is poorly optimized: the spend candidates only alter when transactions in the wallet // change - it could be pre-calculated and held in RAM, and this is probably an optimization worth doing. // Note that output.isMine(this) needs to test the keychain which is currently an array, so it's // O(candidate outputs ^ keychain.size())! There's lots of low hanging fruit here. LinkedList<TransactionOutput> candidates = calculateSpendCandidates(true); // Of the coins we could spend, pick some that we actually will spend. CoinSelection selection = coinSelector.select(value, candidates); // Can we afford this? if (selection.valueGathered.compareTo(value) < 0) { log.warn("Insufficient value in wallet for send, missing " + digitalcoinValueToFriendlyString(value.subtract(selection.valueGathered))); // TODO: Should throw an exception here. return false; } checkState(selection.gathered.size() > 0); req.tx.getConfidence().setConfidenceType(ConfidenceType.NOT_SEEN_IN_CHAIN); BigInteger change = selection.valueGathered.subtract(value); if (change.compareTo(BigInteger.ZERO) > 0) { // The value of the inputs is greater than what we want to send. Just like in real life then, // we need to take back some coins ... this is called "change". Add another output that sends the change // back to us. The address comes either from the request or getChangeAddress() as a default. Address changeAddress = req.changeAddress != null ? req.changeAddress : getChangeAddress(); log.info(" with {} coins change", digitalcoinValueToFriendlyString(change)); req.tx.addOutput(new TransactionOutput(params, req.tx, change, changeAddress)); } for (TransactionOutput output : selection.gathered) { req.tx.addInput(output); } // Now sign the inputs, thus proving that we are entitled to redeem the connected outputs. try { req.tx.signInputs(Transaction.SigHash.ALL, this, req.aesKey); } catch (ScriptException e) { // If this happens it means an output script in a wallet tx could not be understood. That should never // happen, if it does it means the wallet has got into an inconsistent state. throw new RuntimeException(e); } // Check size. int size = req.tx.digitalcoinSerialize().length; if (size > Transaction.MAX_STANDARD_TX_SIZE) { // TODO: Throw an exception here. log.error("Transaction could not be created without exceeding max size: {} vs {}", size, Transaction.MAX_STANDARD_TX_SIZE); return false; } // Label the transaction as being self created. We can use this later to spend its change output even before // the transaction is confirmed. req.tx.getConfidence().setSource(TransactionConfidence.Source.SELF); req.completed = true; log.info(" completed {} with {} inputs", req.tx.getHashAsString(), req.tx.getInputs().size()); return true; } finally { lock.unlock(); } } private LinkedList<TransactionOutput> calculateSpendCandidates(boolean excludeImmatureCoinbases) { checkState(lock.isLocked()); LinkedList<TransactionOutput> candidates = Lists.newLinkedList(); for (Transaction tx : Iterables.concat(unspent.values(), pending.values())) { // Do not try and spend coinbases that were mined too recently, the protocol forbids it. if (excludeImmatureCoinbases && !tx.isMature()) continue; for (TransactionOutput output : tx.getOutputs()) { if (!output.isAvailableForSpending()) continue; if (!output.isMine(this)) continue; candidates.add(output); } } return candidates; } Address getChangeAddress() { lock.lock(); try { // For now let's just pick the first key in our keychain. In future we might want to do something else to // give the user better privacy here, eg in incognito mode. checkState(keychain.size() > 0, "Can't send value without an address to use for receiving change"); ECKey first = keychain.get(0); return first.toAddress(params); } finally { lock.unlock(); } } /** * Adds the given ECKey to the wallet. There is currently no way to delete keys (that would result in coin loss). * If {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.digitalcoin.core.Wallet.AutosaveEventListener)} * has been called, triggers an auto save bypassing the normal coalescing delay and event handlers. * If the key already exists in the wallet, does nothing and returns false. */ public boolean addKey(final ECKey key) { return addKeys(Lists.newArrayList(key)) == 1; } /** * Adds the given keys to the wallet. There is currently no way to delete keys (that would result in coin loss). * If {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.digitalcoin.core.Wallet.AutosaveEventListener)} * has been called, triggers an auto save bypassing the normal coalescing delay and event handlers. * Returns the number of keys added, after duplicates are ignored. The onKeyAdded event will be called for each key * in the list that was not already present. */ public int addKeys(final List<ECKey> keys) { int added = 0; lock.lock(); try { // TODO: Consider making keys a sorted list or hashset so membership testing is faster. for (final ECKey key : keys) { if (keychain.contains(key)) continue; // If the key has a keyCrypter that does not match the Wallet's then a KeyCrypterException is thrown. // This is done because only one keyCrypter is persisted per Wallet and hence all the keys must be homogenous. if (keyCrypter != null && keyCrypter.getUnderstoodEncryptionType() != EncryptionType.UNENCRYPTED) { if (key.isEncrypted() && !keyCrypter.equals(key.getKeyCrypter())) { throw new KeyCrypterException("Cannot add key " + key.toString() + " because the keyCrypter does not match the wallets. Keys must be homogenous."); } } keychain.add(key); added++; } if (autosaveToFile != null) { autoSave(); } } finally { lock.unlock(); } for (ECKey key : keys) { // TODO: Change this interface to be batch-oriented. for (WalletEventListener listener : eventListeners) { listener.onKeyAdded(key); } } return added; } /** * Locates a keypair from the keychain given the hash of the public key. This is needed when finding out which * key we need to use to redeem a transaction output. * * @return ECKey object or null if no such key was found. */ public ECKey findKeyFromPubHash(byte[] pubkeyHash) { lock.lock(); try { for (ECKey key : keychain) { if (Arrays.equals(key.getPubKeyHash(), pubkeyHash)) return key; } return null; } finally { lock.unlock(); } } /** Returns true if the given key is in the wallet, false otherwise. Currently an O(N) operation. */ public boolean hasKey(ECKey key) { lock.lock(); try { return keychain.contains(key); } finally { lock.unlock(); } } /** * Returns true if this wallet contains a public key which hashes to the given hash. */ public boolean isPubKeyHashMine(byte[] pubkeyHash) { return findKeyFromPubHash(pubkeyHash) != null; } /** * Locates a keypair from the keychain given the raw public key bytes. * @return ECKey or null if no such key was found. */ public ECKey findKeyFromPubKey(byte[] pubkey) { lock.lock(); try { for (ECKey key : keychain) { if (Arrays.equals(key.getPubKey(), pubkey)) return key; } return null; } finally { lock.unlock(); } } /** * Returns true if this wallet contains a keypair with the given public key. */ public boolean isPubKeyMine(byte[] pubkey) { return findKeyFromPubKey(pubkey) != null; } /** * <p>It's possible to calculate a wallets balance from multiple points of view. This enum selects which * getBalance() should use.</p> * * <p>Consider a real-world example: you buy a snack costing $5 but you only have a $10 bill. At the start you have * $10 viewed from every possible angle. After you order the snack you hand over your $10 bill. From the * perspective of your wallet you have zero dollars (AVAILABLE). But you know in a few seconds the shopkeeper * will give you back $5 change so most people in practice would say they have $5 (ESTIMATED).</p> */ public enum BalanceType { /** * Balance calculated assuming all pending transactions are in fact included into the best chain by miners. * This includes the value of immature coinbase transactions. */ ESTIMATED, /** * Balance that can be safely used to create new spends. This is whatever the default coin selector would * make available, which by default means transaction outputs with at least 1 confirmation and pending * transactions created by our own wallet which have been propagated across the network. */ AVAILABLE } /** * Returns the AVAILABLE balance of this wallet. See {@link BalanceType#AVAILABLE} for details on what this * means. */ public BigInteger getBalance() { return getBalance(BalanceType.AVAILABLE); } /** * Returns the balance of this wallet as calculated by the provided balanceType. */ public BigInteger getBalance(BalanceType balanceType) { lock.lock(); try { if (balanceType == BalanceType.AVAILABLE) { return getBalance(coinSelector); } else if (balanceType == BalanceType.ESTIMATED) { LinkedList<TransactionOutput> all = calculateSpendCandidates(false); BigInteger value = BigInteger.ZERO; for (TransactionOutput out : all) value = value.add(out.getValue()); return value; } else { throw new AssertionError("Unknown balance type"); // Unreachable. } } finally { lock.unlock(); } } /** * Returns the balance that would be considered spendable by the given coin selector. Just asks it to select * as many coins as possible and returns the total. */ public BigInteger getBalance(CoinSelector selector) { lock.lock(); try { checkNotNull(selector); LinkedList<TransactionOutput> candidates = calculateSpendCandidates(true); CoinSelection selection = selector.select(NetworkParameters.MAX_MONEY, candidates); return selection.valueGathered; } finally { lock.unlock(); } } @Override public String toString() { return toString(false, null); } /** * Formats the wallet as a human readable piece of text. Intended for debugging, the format is not meant to be * stable or human readable. * @param includePrivateKeys Whether raw private key data should be included. * @param chain If set, will be used to estimate lock times for block timelocked transactions. * @return */ public String toString(boolean includePrivateKeys, AbstractBlockChain chain) { lock.lock(); try { StringBuilder builder = new StringBuilder(); builder.append(String.format("Wallet containing %s BTC in:%n", digitalcoinValueToFriendlyString(getBalance()))); builder.append(String.format(" %d unspent transactions%n", unspent.size())); builder.append(String.format(" %d spent transactions%n", spent.size())); builder.append(String.format(" %d pending transactions%n", pending.size())); builder.append(String.format(" %d inactive transactions%n", inactive.size())); builder.append(String.format(" %d dead transactions%n", dead.size())); builder.append(String.format("Last seen best block: (%d) %s%n", getLastBlockSeenHeight(), getLastBlockSeenHash())); if (this.keyCrypter != null) { builder.append(String.format("Encryption: %s%n", keyCrypter.toString())); } // Do the keys. builder.append("\nKeys:\n"); for (ECKey key : keychain) { builder.append(" addr:"); builder.append(key.toAddress(params)); builder.append(" "); builder.append(includePrivateKeys ? key.toStringWithPrivate() : key.toString()); builder.append("\n"); } // Print the transactions themselves if (unspent.size() > 0) { builder.append("\nUNSPENT:\n"); toStringHelper(builder, unspent, chain); } if (spent.size() > 0) { builder.append("\nSPENT:\n"); toStringHelper(builder, spent, chain); } if (pending.size() > 0) { builder.append("\nPENDING:\n"); toStringHelper(builder, pending, chain); } if (inactive.size() > 0) { builder.append("\nINACTIVE:\n"); toStringHelper(builder, inactive, chain); } if (dead.size() > 0) { builder.append("\nDEAD:\n"); toStringHelper(builder, dead, chain); } return builder.toString(); } finally { lock.unlock(); } } private void toStringHelper(StringBuilder builder, Map<Sha256Hash, Transaction> transactionMap, AbstractBlockChain chain) { checkState(lock.isLocked()); for (Transaction tx : transactionMap.values()) { try { builder.append("Sends "); builder.append(Utils.digitalcoinValueToFriendlyString(tx.getValueSentFromMe(this))); builder.append(" and receives "); builder.append(Utils.digitalcoinValueToFriendlyString(tx.getValueSentToMe(this))); builder.append(", total value "); builder.append(Utils.digitalcoinValueToFriendlyString(tx.getValue(this))); builder.append(".\n"); } catch (ScriptException e) { // Ignore and don't print this line. } builder.append(tx.toString(chain)); } } /** * <p>Don't call this directly. It's not intended for API users.</p> * * <p>Called by the {@link BlockChain} when the best chain (representing total work done) has changed. In this case, * we need to go through our transactions and find out if any have become invalid. It's possible for our balance * to go down in this case: money we thought we had can suddenly vanish if the rest of the network agrees it * should be so.</p> * * <p>The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last.</p> */ public void reorganize(StoredBlock splitPoint, List<StoredBlock> oldBlocks, List<StoredBlock> newBlocks) throws VerificationException { lock.lock(); try { // This runs on any peer thread with the block chain synchronized. // // The reorganize functionality of the wallet is tested in ChainSplitTests. // // For each transaction we track which blocks they appeared in. Once a re-org takes place we have to find all // transactions in the old branch, all transactions in the new branch and find the difference of those sets. // // receive() has been called on the block that is triggering the re-org before this is called. List<Sha256Hash> oldBlockHashes = new ArrayList<Sha256Hash>(oldBlocks.size()); List<Sha256Hash> newBlockHashes = new ArrayList<Sha256Hash>(newBlocks.size()); log.info("Old part of chain (top to bottom):"); for (StoredBlock b : oldBlocks) { log.info(" {}", b.getHeader().getHashAsString()); oldBlockHashes.add(b.getHeader().getHash()); } log.info("New part of chain (top to bottom):"); for (StoredBlock b : newBlocks) { log.info(" {}", b.getHeader().getHashAsString()); newBlockHashes.add(b.getHeader().getHash()); } // Transactions that appear in the old chain segment. Map<Sha256Hash, Transaction> oldChainTransactions = new HashMap<Sha256Hash, Transaction>(); // Transactions that appear in the old chain segment and NOT the new chain segment. Map<Sha256Hash, Transaction> onlyOldChainTransactions = new HashMap<Sha256Hash, Transaction>(); // Transactions that appear in the new chain segment. Map<Sha256Hash, Transaction> newChainTransactions = new HashMap<Sha256Hash, Transaction>(); // Transactions that don't appear in either the new or the old section, ie, the shared trunk. Map<Sha256Hash, Transaction> commonChainTransactions = new HashMap<Sha256Hash, Transaction>(); Map<Sha256Hash, Transaction> all = new HashMap<Sha256Hash, Transaction>(); all.putAll(unspent); all.putAll(spent); all.putAll(inactive); // Dead coinbase transactions are potentially resurrected so added to the list of tx to process. for (Transaction tx : dead.values()) { if (tx.isCoinBase()) { all.put(tx.getHash(), tx); } } for (Transaction tx : all.values()) { Collection<Sha256Hash> appearsIn = tx.getAppearsInHashes(); checkNotNull(appearsIn); // If the set of blocks this transaction appears in is disjoint with one of the chain segments it means // the transaction was never incorporated by a miner into that side of the chain. boolean inOldSection = !Collections.disjoint(appearsIn, oldBlockHashes); boolean inNewSection = !Collections.disjoint(appearsIn, newBlockHashes); boolean inCommonSection = !inNewSection && !inOldSection; if (inCommonSection) { boolean alreadyPresent = commonChainTransactions.put(tx.getHash(), tx) != null; checkState(!alreadyPresent, "Transaction appears twice in common chain segment"); } else { if (inOldSection) { boolean alreadyPresent = oldChainTransactions.put(tx.getHash(), tx) != null; checkState(!alreadyPresent, "Transaction appears twice in old chain segment"); if (!inNewSection) { alreadyPresent = onlyOldChainTransactions.put(tx.getHash(), tx) != null; checkState(!alreadyPresent, "Transaction appears twice in only-old map"); } } if (inNewSection) { boolean alreadyPresent = newChainTransactions.put(tx.getHash(), tx) != null; checkState(!alreadyPresent, "Transaction appears twice in new chain segment"); } } } // If there is no difference it means we have nothing we need to do and the user does not care. boolean affectedUs = !oldChainTransactions.equals(newChainTransactions); log.info(affectedUs ? "Re-org affected our transactions" : "Re-org had no effect on our transactions"); if (!affectedUs) return; // Avoid spuriously informing the user of wallet changes whilst we're re-organizing. This also prevents the // user from modifying wallet contents (eg, trying to spend) whilst we're in the middle of the process. onWalletChangedSuppressions++; // For simplicity we will reprocess every transaction to ensure it's in the right bucket and has the right // connections. Attempting to update each one with minimal work is possible but complex and was leading to // edge cases that were hard to fix. As re-orgs are rare the amount of work this implies should be manageable // unless the user has an enormous wallet. As an optimization fully spent transactions buried deeper than // 1000 blocks could be put into yet another bucket which we never touch and assume re-orgs cannot affect. for (Transaction tx : onlyOldChainTransactions.values()) log.info(" Only Old: {}", tx.getHashAsString()); for (Transaction tx : oldChainTransactions.values()) log.info(" Old: {}", tx.getHashAsString()); for (Transaction tx : newChainTransactions.values()) log.info(" New: {}", tx.getHashAsString()); // Break all the existing connections. for (Transaction tx : all.values()) tx.disconnectInputs(); for (Transaction tx : pending.values()) tx.disconnectInputs(); // Reconnect the transactions in the common part of the chain. for (Transaction tx : commonChainTransactions.values()) { TransactionInput badInput = tx.connectForReorganize(all); checkState(badInput == null, "Failed to connect %s, %s", tx.getHashAsString(), badInput == null ? "" : badInput.toString()); } // Recalculate the unspent/spent buckets for the transactions the re-org did not affect. log.info("Moving transactions"); unspent.clear(); spent.clear(); inactive.clear(); for (Transaction tx : commonChainTransactions.values()) { int unspentOutputs = 0; for (TransactionOutput output : tx.getOutputs()) { if (output.isAvailableForSpending() && output.isMine(this)) unspentOutputs++; } if (unspentOutputs > 0) { log.info(" TX {} ->unspent", tx.getHashAsString()); unspent.put(tx.getHash(), tx); } else { log.info(" TX {} ->spent", tx.getHashAsString()); spent.put(tx.getHash(), tx); } } // Inform all transactions that exist only in the old chain that they have moved, so they can update confidence // and timestamps. Transactions will be told they're on the new best chain when the blocks are replayed. for (Transaction tx : onlyOldChainTransactions.values()) { tx.notifyNotOnBestChain(); // Kill any coinbase transactions that are only in the old chain. // These transactions are no longer valid. if (tx.isCoinBase()) { // Move the transaction to the dead pool. if (unspent.containsKey(tx.getHash())) { log.info(" coinbase tx {} unspent->dead", tx.getHashAsString()); unspent.remove(tx.getHash()); } else if (spent.containsKey(tx.getHash())) { log.info(" coinbase tx {} spent->dead", tx.getHashAsString()); // TODO Remove any dependent child transactions of the just removed coinbase transaction. spent.remove(tx.getHash()); } dead.put(tx.getHash(), tx); // Set transaction confidence to dead and notify listeners. tx.getConfidence().setConfidenceType(ConfidenceType.DEAD); } } // Now replay the act of receiving the blocks that were previously in a side chain. This will: // - Move any transactions that were pending and are now accepted into the right bucket. // - Connect the newly active transactions. Collections.reverse(newBlocks); // Need bottom-to-top but we get top-to-bottom. // The old blocks have contributed to the depth and work done for all the transactions in the // wallet that are in blocks up to and including the chain split block. // The total depth and work done is calculated here and then subtracted from the appropriate transactions. int depthToSubtract = oldBlocks.size(); BigInteger workDoneToSubtract = BigInteger.ZERO; for (StoredBlock b : oldBlocks) { workDoneToSubtract = workDoneToSubtract.add(b.getHeader().getWork()); } log.info("DepthToSubtract = " + depthToSubtract + ", workDoneToSubtract = " + workDoneToSubtract); // Remove depthToSubtract and workDoneToSubtract from all transactions in the wallet except for pending and inactive // (i.e. the transactions in the two chains of blocks we are reorganising). subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, spent.values()); subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, unspent.values()); subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, dead.values()); // The effective last seen block is now the split point so set the lastSeenBlockHash. setLastBlockSeenHash(splitPoint.getHeader().getHash()); for (StoredBlock b : newBlocks) { log.info("Replaying block {}", b.getHeader().getHashAsString()); // Replay means: find the transactions that should be in that block, send them to the wallet, inform of // new best block, repeat. Set<Transaction> txns = new HashSet<Transaction>(); Sha256Hash blockHash = b.getHeader().getHash(); for (Transaction tx : newChainTransactions.values()) { if (tx.getAppearsInHashes().contains(blockHash)) { txns.add(tx); log.info(" containing tx {}", tx.getHashAsString()); } } if (!txns.isEmpty()) { // Add the transactions to the new blocks. for (Transaction t : txns) { try { receive(t, b, BlockChain.NewBlockType.BEST_CHAIN, true); } catch (ScriptException e) { throw new RuntimeException(e); // Cannot happen as these blocks were already verified. } } } notifyNewBestBlock(b); } // Find the transactions that didn't make it into the new chain yet. For each input, try to connect it to the // transactions that are in {spent,unspent,pending}. Check the status of each input. For inactive // transactions that only send us money, we put them into the inactive pool where they sit around waiting for // another re-org or re-inclusion into the main chain. For inactive transactions where we spent money we must // put them back into the pending pool if we can reconnect them, so we don't create a double spend whilst the // network heals itself. Map<Sha256Hash, Transaction> pool = new HashMap<Sha256Hash, Transaction>(); pool.putAll(unspent); pool.putAll(spent); pool.putAll(pending); Map<Sha256Hash, Transaction> toReprocess = new HashMap<Sha256Hash, Transaction>(); toReprocess.putAll(onlyOldChainTransactions); toReprocess.putAll(pending); log.info("Reprocessing transactions not in new best chain:"); // Note, we must reprocess dead transactions first. The reason is that if there is a double spend across // chains from our own coins we get a complicated situation: // // 1) We switch to a new chain (B) that contains a double spend overriding a pending transaction. The // pending transaction goes dead. // 2) We switch BACK to the first chain (A). The dead transaction must go pending again. // 3) We resurrect the transactions that were in chain (B) and assume the miners will start work on putting them // in to the chain, but it's not possible because it's a double spend. So now that transaction must become // dead instead of pending. // // This only occurs when we are double spending our own coins. for (Transaction tx : dead.values()) { reprocessUnincludedTxAfterReorg(pool, tx); } for (Transaction tx : toReprocess.values()) { reprocessUnincludedTxAfterReorg(pool, tx); } log.info("post-reorg balance is {}", Utils.digitalcoinValueToFriendlyString(getBalance())); // Inform event listeners that a re-org took place. They should save the wallet at this point. invokeOnReorganize(); onWalletChangedSuppressions--; invokeOnWalletChanged(); checkState(isConsistent()); } finally { lock.unlock(); } } /** * Subtract the supplied depth and work done from the given transactions. */ private static void subtractDepthAndWorkDone(int depthToSubtract, BigInteger workDoneToSubtract, Collection<Transaction> transactions) { for (Transaction tx : transactions) { if (tx.getConfidence().getConfidenceType() == ConfidenceType.BUILDING) { tx.getConfidence().setDepthInBlocks(tx.getConfidence().getDepthInBlocks() - depthToSubtract); tx.getConfidence().setWorkDone(tx.getConfidence().getWorkDone().subtract(workDoneToSubtract)); } } } private void reprocessUnincludedTxAfterReorg(Map<Sha256Hash, Transaction> pool, Transaction tx) { checkState(lock.isLocked()); log.info("TX {}", tx.getHashAsString() + ", confidence = " + tx.getConfidence().getConfidenceType().name()); boolean isDeadCoinbase = tx.isCoinBase() && ConfidenceType.DEAD == tx.getConfidence().getConfidenceType(); // Dead coinbase transactions on a side chain stay dead. if (isDeadCoinbase) { return; } int numInputs = tx.getInputs().size(); int noSuchTx = 0; int success = 0; boolean isDead = false; // The transactions that we connected inputs to, so we can go back later and move them into the right // bucket if all their outputs got spent. Set<Transaction> connectedTransactions = new HashSet<Transaction>(); for (TransactionInput input : tx.getInputs()) { TransactionInput.ConnectionResult result = input.connect(pool, TransactionInput.ConnectMode.ABORT_ON_CONFLICT); if (result == TransactionInput.ConnectionResult.SUCCESS) { success++; TransactionOutput connectedOutput = checkNotNull(input.getConnectedOutput(pool)); connectedTransactions.add(checkNotNull(connectedOutput.parentTransaction)); } else if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { noSuchTx++; } else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) { isDead = true; // This transaction was replaced by a double spend on the new chain. Did you just reverse // your own transaction? I hope not!! log.info(" ->dead, will not confirm now unless there's another re-org", tx.getHashAsString()); TransactionOutput doubleSpent = input.getConnectedOutput(pool); Transaction replacement = doubleSpent.getSpentBy().getParentTransaction(); dead.put(tx.getHash(), tx); pending.remove(tx.getHash()); // This updates the tx confidence type automatically. tx.getConfidence().setOverridingTransaction(replacement); break; } } if (isDead) return; // If all inputs do not appear in this wallet move to inactive. if (noSuchTx == numInputs) { log.info(" ->inactive", tx.getHashAsString() + ", confidence = " + tx.getConfidence().getConfidenceType().name()); inactive.put(tx.getHash(), tx); dead.remove(tx.getHash()); } else if (success == numInputs - noSuchTx) { // All inputs are either valid for spending or don't come from us. Miners are trying to reinclude it. log.info(" ->pending", tx.getHashAsString() + ", confidence = " + tx.getConfidence().getConfidenceType().name()); pending.put(tx.getHash(), tx); dead.remove(tx.getHash()); } // The act of re-connecting this un-included transaction may have caused other transactions to become fully // spent so move them into the right bucket here to keep performance good. for (Transaction maybeSpent : connectedTransactions) { maybeMovePool(maybeSpent, "reorg"); } } /** * Returns an immutable view of the transactions currently waiting for network confirmations. */ public Collection<Transaction> getPendingTransactions() { lock.lock(); try { return Collections.unmodifiableCollection(pending.values()); } finally { lock.unlock(); } } /** * Returns the earliest creation time of the keys in this wallet, in seconds since the epoch, ie the min of * {@link com.google.digitalcoin.core.ECKey#getCreationTimeSeconds()}. This can return zero if at least one key does * not have that data (was created before key timestamping was implemented). <p> * * This method is most often used in conjunction with {@link PeerGroup#setFastCatchupTimeSecs(long)} in order to * optimize chain download for new users of wallet apps. Backwards compatibility notice: if you get zero from this * method, you can instead use the time of the first release of your software, as it's guaranteed no users will * have wallets pre-dating this time. <p> * * If there are no keys in the wallet, the current time is returned. */ public long getEarliestKeyCreationTime() { lock.lock(); try { if (keychain.size() == 0) { return Utils.now().getTime() / 1000; } long earliestTime = Long.MAX_VALUE; for (ECKey key : keychain) { earliestTime = Math.min(key.getCreationTimeSeconds(), earliestTime); } return earliestTime; } finally { lock.unlock(); } } /** Returns the hash of the last seen best-chain block. */ public Sha256Hash getLastBlockSeenHash() { lock.lock(); try { return lastBlockSeenHash; } finally { lock.unlock(); } } public void setLastBlockSeenHash(Sha256Hash lastBlockSeenHash) { lock.lock(); try { this.lastBlockSeenHash = lastBlockSeenHash; } finally { lock.unlock(); } } public void setLastBlockSeenHeight(int lastBlockSeenHeight) { lock.lock(); try { this.lastBlockSeenHeight = lastBlockSeenHeight; } finally { lock.unlock(); } } /** Returns the height of the last seen best-chain block. Can be -1 if a wallet is old and doesn't have that data. */ public int getLastBlockSeenHeight() { lock.lock(); try { return lastBlockSeenHeight; } finally { lock.unlock(); } } /** * Convenience wrapper around {@link Wallet#encrypt(com.google.digitalcoin.crypto.KeyCrypter, * org.spongycastle.crypto.params.KeyParameter)} which uses the default Scrypt key derivation algorithm and * parameters, derives a key from the given password and returns the created key. */ public KeyParameter encrypt(CharSequence password) { checkNotNull(password); checkArgument(password.length() > 0); KeyCrypter scrypt = new KeyCrypterScrypt(); KeyParameter derivedKey = scrypt.deriveKey(password); encrypt(scrypt, derivedKey); return derivedKey; } /** * Encrypt the wallet using the KeyCrypter and the AES key. A good default KeyCrypter to use is * {@link com.google.digitalcoin.crypto.KeyCrypterScrypt}. * * @param keyCrypter The KeyCrypter that specifies how to encrypt/ decrypt a key * @param aesKey AES key to use (normally created using KeyCrypter#deriveKey and cached as it is time consuming to create from a password) * @throws KeyCrypterException Thrown if the wallet encryption fails. If so, the wallet state is unchanged. */ public void encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) { lock.lock(); try { checkNotNull(keyCrypter); checkState(getEncryptionType() == EncryptionType.UNENCRYPTED, "Wallet is already encrypted"); // Create a new arraylist that will contain the encrypted keys ArrayList<ECKey> encryptedKeyChain = new ArrayList<ECKey>(); for (ECKey key : keychain) { if (key.isEncrypted()) { // Key is already encrypted - add as is. encryptedKeyChain.add(key); } else { // Encrypt the key. ECKey encryptedKey = key.encrypt(keyCrypter, aesKey); // Check that the encrypted key can be successfully decrypted. // This is done as it is a critical failure if the private key cannot be decrypted successfully // (all digitalcoin controlled by that private key is lost forever). // For a correctly constructed keyCrypter the encryption should always be reversible so it is just being as cautious as possible. if (!ECKey.encryptionIsReversible(key, encryptedKey, keyCrypter, aesKey)) { // Abort encryption throw new KeyCrypterException("The key " + key.toString() + " cannot be successfully decrypted after encryption so aborting wallet encryption."); } encryptedKeyChain.add(encryptedKey); } } // Now ready to use the encrypted keychain so go through the old keychain clearing all the unencrypted private keys. // (This is to avoid the possibility of key recovery from memory). for (ECKey key : keychain) { if (!key.isEncrypted()) { key.clearPrivateKey(); } } // Replace the old keychain with the encrypted one. keychain = encryptedKeyChain; // The wallet is now encrypted. this.keyCrypter = keyCrypter; if (autosaveToFile != null) { autoSave(); } } finally { lock.unlock(); } } /** * Decrypt the wallet with the wallets keyCrypter and AES key. * * @param aesKey AES key to use (normally created using KeyCrypter#deriveKey and cached as it is time consuming to create from a password) * @throws KeyCrypterException Thrown if the wallet decryption fails. If so, the wallet state is unchanged. */ public void decrypt(KeyParameter aesKey) { lock.lock(); try { // Check the wallet is already encrypted - you cannot decrypt an unencrypted wallet. checkState(getEncryptionType() != EncryptionType.UNENCRYPTED, "Wallet is already decrypted"); // Check that the wallet keyCrypter is non-null. // This is set either at construction (if an encrypted wallet is created) or by wallet encryption. checkNotNull(keyCrypter); // Create a new arraylist that will contain the decrypted keys ArrayList<ECKey> decryptedKeyChain = new ArrayList<ECKey>(); for (ECKey key : keychain) { // Decrypt the key. if (!key.isEncrypted()) { // Not encrypted - add to chain as is. decryptedKeyChain.add(key); } else { ECKey decryptedECKey = key.decrypt(keyCrypter, aesKey); decryptedKeyChain.add(decryptedECKey); } } // Replace the old keychain with the unencrypted one. keychain = decryptedKeyChain; // The wallet is now unencrypted. keyCrypter = null; if (autosaveToFile != null) { autoSave(); } } finally { lock.unlock(); } } /** * Create a new, random encrypted ECKey and add it to the wallet. * * @param keyCrypter The keyCrypter to use in encrypting the new key * @param aesKey The AES key to use to encrypt the new key * @return ECKey the new, encrypted ECKey */ public ECKey addNewEncryptedKey(KeyCrypter keyCrypter, KeyParameter aesKey) { ECKey newKey = (new ECKey()).encrypt(checkNotNull(keyCrypter), checkNotNull(aesKey)); addKey(newKey); return newKey; } /** * <p>Convenience wrapper around {@link Wallet#addNewEncryptedKey(com.google.digitalcoin.crypto.KeyCrypter, * org.spongycastle.crypto.params.KeyParameter)} which just derives the key afresh and uses the pre-set * keycrypter. The wallet must have been encrypted using one of the encrypt methods previously.</p> * * <p>Note that key derivation is deliberately very slow! So if you plan to add multiple keys, it can be * faster to use the other method instead and re-use the {@link KeyParameter} object instead.</p> */ public ECKey addNewEncryptedKey(CharSequence password) { lock.lock(); try { checkNotNull(keyCrypter, "Wallet is not encrypted, you must call encrypt() first."); return addNewEncryptedKey(keyCrypter, keyCrypter.deriveKey(password)); } finally { lock.unlock(); } } /** * Check whether the password can decrypt the first key in the wallet. * This can be used to check the validity of an entered password. * * @return boolean true if password supplied can decrypt the first private key in the wallet, false otherwise. */ public boolean checkPassword(CharSequence password) { lock.lock(); try { if (keyCrypter == null) { // The password cannot decrypt anything as the keyCrypter is null. return false; } return checkAESKey(keyCrypter.deriveKey(checkNotNull(password))); } finally { lock.unlock(); } } /** * Check whether the AES key can decrypt the first encrypted key in the wallet. * * @returns boolean true if AES key supplied can decrypt the first encrypted private key in the wallet, false otherwise. */ public boolean checkAESKey(KeyParameter aesKey) { lock.lock(); try { // If no keys then cannot decrypt. if (!getKeys().iterator().hasNext()) return false; // Find the first encrypted key in the wallet. ECKey firstEncryptedECKey = null; Iterator<ECKey> iterator = getKeys().iterator(); while (iterator.hasNext() && firstEncryptedECKey == null) { ECKey loopECKey = iterator.next(); if (loopECKey.isEncrypted()) { firstEncryptedECKey = loopECKey; } } // There are no encrypted keys in the wallet. if (firstEncryptedECKey == null) return false; String originalAddress = firstEncryptedECKey.toAddress(getNetworkParameters()).toString(); if (firstEncryptedECKey.isEncrypted() && firstEncryptedECKey.getEncryptedPrivateKey() != null) { try { ECKey rebornKey = firstEncryptedECKey.decrypt(keyCrypter, aesKey); // Check that the decrypted private key's address is correct ie it decrypted accurately. String rebornAddress = rebornKey.toAddress(getNetworkParameters()).toString(); return originalAddress.equals(rebornAddress); } catch (KeyCrypterException ede) { // The AES key supplied is incorrect. return false; } } return false; } finally { lock.unlock(); } } /** * Get the wallet's KeyCrypter. * (Used in encrypting/ decrypting an ECKey). */ public KeyCrypter getKeyCrypter() { lock.lock(); try { return keyCrypter; } finally { lock.unlock(); } } /** * Get the type of encryption used for this wallet. * * (This is a convenience method - the encryption type is actually stored in the keyCrypter). */ public EncryptionType getEncryptionType() { lock.lock(); try { if (keyCrypter == null) { // Unencrypted wallet. return EncryptionType.UNENCRYPTED; } else { return keyCrypter.getUnderstoodEncryptionType(); } } finally { lock.unlock(); } } /** Returns true if the wallet is encrypted using any scheme, false if not. */ public boolean isEncrypted() { return getEncryptionType() != EncryptionType.UNENCRYPTED; } /** * Get the version of the Wallet. * This is an int you can use to indicate which versions of wallets your code understands, * and which come from the future (and hence cannot be safely loaded). */ public int getVersion() { return version; } /** * Set the version number of the wallet. See {@link Wallet#getVersion()}. */ public void setVersion(int version) { this.version = version; } /** * Set the description of the wallet. * This is a Unicode encoding string typically entered by the user as descriptive text for the wallet. */ public void setDescription(String description) { this.description = description; } /** * Get the description of the wallet. See {@link Wallet#setDescription(String))} * @return */ public String getDescription() { return description; } /** * Gets the number of elements that will be added to a bloom filter returned by getBloomFilter */ public int getBloomFilterElementCount() { int size = getKeychainSize() * 2; for (Transaction tx : getTransactions(false, true)) { for (TransactionOutput out : tx.getOutputs()) { try { if (out.isMine(this) && out.getScriptPubKey().isSentToRawPubKey()) size++; } catch (ScriptException e) { throw new RuntimeException(e); // If it is ours, we parsed the script corectly, so this shouldn't happen } } } return size; } /** * Gets a bloom filter that contains all of the public keys from this wallet, and which will provide the given * false-positive rate. See the docs for {@link BloomFilter} for a brief explanation of anonymity when using filters. */ public BloomFilter getBloomFilter(double falsePositiveRate) { return getBloomFilter(getBloomFilterElementCount(), falsePositiveRate, (long)(Math.random()*Long.MAX_VALUE)); } /** * Gets a bloom filter that contains all of the public keys from this wallet, * and which will provide the given false-positive rate if it has size elements. * Keep in mind that you will get 2 elements in the bloom filter for each key in the wallet. * * This is used to generate a BloomFilter which can be #{link BloomFilter.merge}d with another. * It could also be used if you have a specific target for the filter's size. * * See the docs for {@link BloomFilter#BloomFilter(int, double)} for a brief explanation of anonymity when using bloom filters. */ public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak) { BloomFilter filter = new BloomFilter(size, falsePositiveRate, nTweak); lock.lock(); try { for (ECKey key : keychain) { filter.insert(key.getPubKey()); filter.insert(key.getPubKeyHash()); } } finally { lock.unlock(); } for (Transaction tx : getTransactions(false, true)) { for (int i = 0; i < tx.getOutputs().size(); i++) { TransactionOutput out = tx.getOutputs().get(i); try { if (out.isMine(this) && out.getScriptPubKey().isSentToRawPubKey()) { TransactionOutPoint outPoint = new TransactionOutPoint(params, i, tx); filter.insert(outPoint.digitalcoinSerialize()); } } catch (ScriptException e) { throw new RuntimeException(e); // If it is ours, we parsed the script corectly, so this shouldn't happen } } } return filter; } /** Returns the {@link CoinSelector} object which controls which outputs can be spent by this wallet. */ public CoinSelector getCoinSelector() { lock.lock(); try { return coinSelector; } finally { lock.unlock(); } } /** * A coin selector is responsible for choosing which outputs to spend when creating transactions. The default * selector implements a policy of spending transactions that appeared in the best chain and pending transactions * that were created by this wallet, but not others. */ public void setCoinSelector(CoinSelector coinSelector) { lock.lock(); try { this.coinSelector = coinSelector; } finally { lock.unlock(); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Boilerplate for running event listeners - unlocks the wallet, runs, re-locks. private void invokeOnTransactionConfidenceChanged(Transaction tx) { checkState(lock.isLocked()); lock.unlock(); try { for (WalletEventListener listener : eventListeners) { listener.onTransactionConfidenceChanged(this, tx); } } finally { lock.lock(); } } private int onWalletChangedSuppressions = 0; private void invokeOnWalletChanged() { // Don't invoke the callback in some circumstances, eg, whilst we are re-organizing or fiddling with // transactions due to a new block arriving. It will be called later instead. checkState(lock.isLocked()); Preconditions.checkState(onWalletChangedSuppressions >= 0); if (onWalletChangedSuppressions > 0) return; lock.unlock(); try { for (WalletEventListener listener : eventListeners) { listener.onWalletChanged(this); } } finally { lock.lock(); } } private void invokeOnCoinsReceived(Transaction tx, BigInteger balance, BigInteger newBalance) { checkState(lock.isLocked()); lock.unlock(); try { for (WalletEventListener listener : eventListeners) { listener.onCoinsReceived(Wallet.this, tx, balance, newBalance); } } finally { lock.lock(); } } private void invokeOnCoinsSent(Transaction tx, BigInteger prevBalance, BigInteger newBalance) { checkState(lock.isLocked()); lock.unlock(); try { for (WalletEventListener listener : eventListeners) { listener.onCoinsSent(Wallet.this, tx, prevBalance, newBalance); } } finally { lock.lock(); } } private void invokeOnReorganize() { checkState(lock.isLocked()); lock.unlock(); try { for (WalletEventListener listener : eventListeners) { listener.onReorganize(Wallet.this); } } finally { lock.lock(); } } }