electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit d9cf4c2b63ed8c6c07b26a31d322cd5e69e55ad1
parent fca20e33f5a172c86b1d09dcac009d7aa0f27fbe
Author: ThomasV <thomasv@electrum.org>
Date:   Tue,  6 Feb 2018 06:16:19 +0100

Merge pull request #3810 from SomberNight/conflicting_txns

conflicting transactions
Diffstat:
Mgui/qt/transaction_dialog.py | 7+++++--
Mlib/commands.py | 3++-
Mlib/transaction.py | 8++++++++
Mlib/wallet.py | 127++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
4 files changed, 115 insertions(+), 30 deletions(-)

diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py @@ -179,10 +179,13 @@ class TxDialog(QDialog, MessageBoxMixin): self.main_window.sign_tx(self.tx, sign_done) def save(self): - self.wallet.add_transaction(self.tx.txid(), self.tx) + if not self.wallet.add_transaction(self.tx.txid(), self.tx): + self.show_error(_("Transaction could not be saved. It conflicts with current history.")) + return self.wallet.save_transactions(write=True) - self.main_window.history_list.update() + # need to update at least: history_list, utxo_list, address_list + self.main_window.need_update.set() self.save_button.setDisabled(True) self.show_message(_("Transaction saved successfully")) diff --git a/lib/commands.py b/lib/commands.py @@ -627,7 +627,8 @@ class Commands: def addtransaction(self, tx): """ Add a transaction to the wallet history """ tx = Transaction(tx) - self.wallet.add_transaction(tx.txid(), tx) + if not self.wallet.add_transaction(tx.txid(), tx): + return False self.wallet.save_transactions() return tx.txid() diff --git a/lib/transaction.py b/lib/transaction.py @@ -798,6 +798,14 @@ class Transaction: return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4) @classmethod + def get_outpoint_from_txin(cls, txin): + if txin['type'] == 'coinbase': + return None + prevout_hash = txin['prevout_hash'] + prevout_n = txin['prevout_n'] + return prevout_hash + ':%d' % prevout_n + + @classmethod def serialize_input(self, txin, script): # Prev hash and index s = self.serialize_outpoint(txin) diff --git a/lib/wallet.py b/lib/wallet.py @@ -188,7 +188,7 @@ class Abstract_Wallet(PrintError): self.load_keystore() self.load_addresses() self.load_transactions() - self.build_reverse_history() + self.build_spent_outpoints() # load requests self.receive_requests = self.storage.get('payment_requests', {}) @@ -204,8 +204,10 @@ class Abstract_Wallet(PrintError): # interface.is_up_to_date() returns true when all requests have been answered and processed # wallet.up_to_date is true when the wallet is synchronized (stronger requirement) self.up_to_date = False + + # locks: if you need to take multiple ones, acquire them in the order they are defined here! self.lock = threading.Lock() - self.transaction_lock = threading.Lock() + self.transaction_lock = threading.RLock() self.check_history() @@ -238,7 +240,8 @@ class Abstract_Wallet(PrintError): for tx_hash, raw in tx_list.items(): tx = Transaction(raw) self.transactions[tx_hash] = tx - if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None and (tx_hash not in self.pruned_txo.values()): + if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None \ + and (tx_hash not in self.pruned_txo.values()): self.print_error("removing unreferenced tx", tx_hash) self.transactions.pop(tx_hash) @@ -258,24 +261,25 @@ class Abstract_Wallet(PrintError): self.storage.write() def clear_history(self): - with self.transaction_lock: - self.txi = {} - self.txo = {} - self.tx_fees = {} - self.pruned_txo = {} - self.save_transactions() with self.lock: - self.history = {} - self.tx_addr_hist = {} + with self.transaction_lock: + self.txi = {} + self.txo = {} + self.tx_fees = {} + self.pruned_txo = {} + self.spent_outpoints = {} + self.history = {} + self.save_transactions() @profiler - def build_reverse_history(self): - self.tx_addr_hist = {} - for addr, hist in self.history.items(): - for tx_hash, h in hist: - s = self.tx_addr_hist.get(tx_hash, set()) - s.add(addr) - self.tx_addr_hist[tx_hash] = s + def build_spent_outpoints(self): + self.spent_outpoints = {} + for txid, tx in self.transactions.items(): + for txi in tx.inputs(): + ser = Transaction.get_outpoint_from_txin(txi) + if ser is None: + continue + self.spent_outpoints[ser] = txid @profiler def check_history(self): @@ -415,7 +419,7 @@ class Abstract_Wallet(PrintError): return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) def get_tx_height(self, tx_hash): - """ return the height and timestamp of a transaction. """ + """ Given a transaction, returns (height, conf, timestamp) """ with self.lock: if tx_hash in self.verified_tx: height, timestamp, pos = self.verified_tx[tx_hash] @@ -682,10 +686,69 @@ class Abstract_Wallet(PrintError): self.print_error("found pay-to-pubkey address:", addr) return addr + def get_conflicting_transactions(self, tx): + """Returns a set of transaction hashes from the wallet history that are + directly conflicting with tx, i.e. they have common outpoints being + spent with tx. If the tx is already in wallet history, that will not be + reported as a conflict. + """ + conflicting_txns = set() + with self.transaction_lock: + for txi in tx.inputs(): + ser = Transaction.get_outpoint_from_txin(txi) + if ser is None: + continue + spending_tx_hash = self.spent_outpoints.get(ser, None) + if spending_tx_hash is None: + continue + # this outpoint (ser) has already been spent, by spending_tx + if spending_tx_hash not in self.transactions: + # can't find this txn: delete and ignore it + self.spent_outpoints.pop(ser) + continue + conflicting_txns |= {spending_tx_hash} + txid = tx.txid() + if txid in conflicting_txns: + # this tx is already in history, so it conflicts with itself + if len(conflicting_txns) > 1: + raise Exception('Found conflicting transactions already in wallet history.') + conflicting_txns -= {txid} + return conflicting_txns + def add_transaction(self, tx_hash, tx): is_coinbase = tx.inputs()[0]['type'] == 'coinbase' related = False with self.transaction_lock: + # Find all conflicting transactions. + # In case of a conflict, + # 1. confirmed > mempool > local + # 2. this new txn has priority over existing ones + # When this method exits, there must NOT be any conflict, so + # either keep this txn and remove all conflicting (along with dependencies) + # or drop this txn + conflicting_txns = self.get_conflicting_transactions(tx) + if conflicting_txns: + tx_height = self.get_tx_height(tx_hash)[0] + existing_mempool_txn = any( + self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) + for tx_hash2 in conflicting_txns) + existing_confirmed_txn = any( + self.get_tx_height(tx_hash2)[0] > 0 + for tx_hash2 in conflicting_txns) + if existing_confirmed_txn and tx_height <= 0: + # this is a non-confirmed tx that conflicts with confirmed txns; drop. + return False + if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL: + # this is a local tx that conflicts with non-local txns; drop. + return False + # keep this txn and remove all conflicting + to_remove = set() + to_remove |= conflicting_txns + for conflicting_tx_hash in conflicting_txns: + to_remove |= self.get_depending_transactions(conflicting_tx_hash) + for tx_hash2 in to_remove: + self.remove_transaction(tx_hash2) + # add inputs self.txi[tx_hash] = d = {} for txi in tx.inputs(): @@ -694,6 +757,7 @@ class Abstract_Wallet(PrintError): prevout_hash = txi['prevout_hash'] prevout_n = txi['prevout_n'] ser = prevout_hash + ':%d'%prevout_n + self.spent_outpoints[ser] = tx_hash if addr == "(pubkey)": addr = self.find_pay_to_pubkey_address(prevout_hash, prevout_n) # find value from prev output @@ -739,14 +803,27 @@ class Abstract_Wallet(PrintError): # save self.transactions[tx_hash] = tx + return True def remove_transaction(self, tx_hash): + def undo_spend(outpoint_to_txid_map): + if tx: + # if we have the tx, this should often be faster + for txi in tx.inputs(): + ser = Transaction.get_outpoint_from_txin(txi) + outpoint_to_txid_map.pop(ser, None) + else: + for ser, hh in list(outpoint_to_txid_map.items()): + if hh == tx_hash: + outpoint_to_txid_map.pop(ser) + with self.transaction_lock: self.print_error("removing tx from history", tx_hash) #tx = self.transactions.pop(tx_hash) - for ser, hh in list(self.pruned_txo.items()): - if hh == tx_hash: - self.pruned_txo.pop(ser) + tx = self.transactions.get(tx_hash, None) + undo_spend(self.pruned_txo) + undo_spend(self.spent_outpoints) + # add tx to pruned_txo, and undo the txi addition for next_tx, dd in self.txi.items(): for addr, l in list(dd.items()): @@ -768,8 +845,8 @@ class Abstract_Wallet(PrintError): self.print_error("tx was not in history", tx_hash) def receive_tx_callback(self, tx_hash, tx, tx_height): - self.add_transaction(tx_hash, tx) self.add_unverified_tx(tx_hash, tx_height) + self.add_transaction(tx_hash, tx) def receive_history_callback(self, addr, hist, tx_fees): with self.lock: @@ -785,10 +862,6 @@ class Abstract_Wallet(PrintError): for tx_hash, tx_height in hist: # add it in case it was previously unconfirmed self.add_unverified_tx(tx_hash, tx_height) - # add reference in tx_addr_hist - s = self.tx_addr_hist.get(tx_hash, set()) - s.add(addr) - self.tx_addr_hist[tx_hash] = s # if addr is new, we have to recompute txi and txo tx = self.transactions.get(tx_hash) if tx is not None and self.txi.get(tx_hash, {}).get(addr) is None and self.txo.get(tx_hash, {}).get(addr) is None: