verifier: better handle reorgs (and storage upgrade)
This commit is contained in:
@@ -31,6 +31,7 @@ from .util import PrintError, profiler, bfh
|
|||||||
from .transaction import Transaction
|
from .transaction import Transaction
|
||||||
from .synchronizer import Synchronizer
|
from .synchronizer import Synchronizer
|
||||||
from .verifier import SPV
|
from .verifier import SPV
|
||||||
|
from .blockchain import hash_header
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
|
||||||
TX_HEIGHT_LOCAL = -2
|
TX_HEIGHT_LOCAL = -2
|
||||||
@@ -45,6 +46,7 @@ class UnrelatedTransactionException(AddTransactionException):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("Transaction is unrelated to this wallet.")
|
return _("Transaction is unrelated to this wallet.")
|
||||||
|
|
||||||
|
|
||||||
class AddressSynchronizer(PrintError):
|
class AddressSynchronizer(PrintError):
|
||||||
"""
|
"""
|
||||||
inherited by wallet
|
inherited by wallet
|
||||||
@@ -61,7 +63,7 @@ class AddressSynchronizer(PrintError):
|
|||||||
self.transaction_lock = threading.RLock()
|
self.transaction_lock = threading.RLock()
|
||||||
# address -> list(txid, height)
|
# address -> list(txid, height)
|
||||||
self.history = storage.get('addr_history',{})
|
self.history = storage.get('addr_history',{})
|
||||||
# Verified transactions. txid -> (height, timestamp, block_pos). Access with self.lock.
|
# Verified transactions. txid -> (height, timestamp, block_pos, block_hash). Access with self.lock.
|
||||||
self.verified_tx = storage.get('verified_tx3', {})
|
self.verified_tx = storage.get('verified_tx3', {})
|
||||||
# Transactions pending verification. txid -> tx_height. Access with self.lock.
|
# Transactions pending verification. txid -> tx_height. Access with self.lock.
|
||||||
self.unverified_tx = defaultdict(int)
|
self.unverified_tx = defaultdict(int)
|
||||||
@@ -434,7 +436,7 @@ class AddressSynchronizer(PrintError):
|
|||||||
"return position, even if the tx is unverified"
|
"return position, even if the tx is unverified"
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if tx_hash in self.verified_tx:
|
if tx_hash in self.verified_tx:
|
||||||
height, timestamp, pos = self.verified_tx[tx_hash]
|
height, timestamp, pos, header_hash = self.verified_tx[tx_hash]
|
||||||
return height, pos
|
return height, pos
|
||||||
elif tx_hash in self.unverified_tx:
|
elif tx_hash in self.unverified_tx:
|
||||||
height = self.unverified_tx[tx_hash]
|
height = self.unverified_tx[tx_hash]
|
||||||
@@ -462,7 +464,7 @@ class AddressSynchronizer(PrintError):
|
|||||||
history = []
|
history = []
|
||||||
for tx_hash in tx_deltas:
|
for tx_hash in tx_deltas:
|
||||||
delta = tx_deltas[tx_hash]
|
delta = tx_deltas[tx_hash]
|
||||||
height, conf, timestamp = self.get_tx_height(tx_hash)
|
height, conf, timestamp, header_hash = self.get_tx_height(tx_hash)
|
||||||
history.append((tx_hash, height, conf, timestamp, delta))
|
history.append((tx_hash, height, conf, timestamp, delta))
|
||||||
history.sort(key = lambda x: self.get_txpos(x[0]))
|
history.sort(key = lambda x: self.get_txpos(x[0]))
|
||||||
history.reverse()
|
history.reverse()
|
||||||
@@ -503,24 +505,26 @@ class AddressSynchronizer(PrintError):
|
|||||||
self._history_local[addr] = cur_hist
|
self._history_local[addr] = cur_hist
|
||||||
|
|
||||||
def add_unverified_tx(self, tx_hash, tx_height):
|
def add_unverified_tx(self, tx_hash, tx_height):
|
||||||
if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \
|
if tx_hash in self.verified_tx:
|
||||||
and tx_hash in self.verified_tx:
|
if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
|
||||||
|
with self.lock:
|
||||||
|
self.verified_tx.pop(tx_hash)
|
||||||
|
if self.verifier:
|
||||||
|
self.verifier.remove_spv_proof_for_tx(tx_hash)
|
||||||
|
else:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.verified_tx.pop(tx_hash)
|
# tx will be verified only if height > 0
|
||||||
|
self.unverified_tx[tx_hash] = tx_height
|
||||||
|
# to remove pending proof requests:
|
||||||
if self.verifier:
|
if self.verifier:
|
||||||
self.verifier.remove_spv_proof_for_tx(tx_hash)
|
self.verifier.remove_spv_proof_for_tx(tx_hash)
|
||||||
|
|
||||||
# tx will be verified only if height > 0
|
|
||||||
if tx_hash not in self.verified_tx:
|
|
||||||
with self.lock:
|
|
||||||
self.unverified_tx[tx_hash] = tx_height
|
|
||||||
|
|
||||||
def add_verified_tx(self, tx_hash, info):
|
def add_verified_tx(self, tx_hash, info):
|
||||||
# Remove from the unverified map and add to the verified map
|
# Remove from the unverified map and add to the verified map
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.unverified_tx.pop(tx_hash, None)
|
self.unverified_tx.pop(tx_hash, None)
|
||||||
self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos)
|
self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos, header_hash)
|
||||||
height, conf, timestamp = self.get_tx_height(tx_hash)
|
height, conf, timestamp, header_hash = self.get_tx_height(tx_hash)
|
||||||
self.network.trigger_callback('verified', tx_hash, height, conf, timestamp)
|
self.network.trigger_callback('verified', tx_hash, height, conf, timestamp)
|
||||||
|
|
||||||
def get_unverified_txs(self):
|
def get_unverified_txs(self):
|
||||||
@@ -533,12 +537,21 @@ class AddressSynchronizer(PrintError):
|
|||||||
txs = set()
|
txs = set()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
for tx_hash, item in list(self.verified_tx.items()):
|
for tx_hash, item in list(self.verified_tx.items()):
|
||||||
tx_height, timestamp, pos = item
|
tx_height, timestamp, pos, header_hash = item
|
||||||
if tx_height >= height:
|
if tx_height >= height:
|
||||||
header = blockchain.read_header(tx_height)
|
header = blockchain.read_header(tx_height)
|
||||||
# fixme: use block hash, not timestamp
|
if not header or hash_header(header) != header_hash:
|
||||||
if not header or header.get('timestamp') != timestamp:
|
|
||||||
self.verified_tx.pop(tx_hash, None)
|
self.verified_tx.pop(tx_hash, None)
|
||||||
|
# NOTE: we should add these txns to self.unverified_tx,
|
||||||
|
# but with what height?
|
||||||
|
# If on the new fork after the reorg, the txn is at the
|
||||||
|
# same height, we will not get a status update for the
|
||||||
|
# address. If the txn is not mined or at a diff height,
|
||||||
|
# we should get a status update. Unless we put tx into
|
||||||
|
# unverified_tx, it will turn into local. So we put it
|
||||||
|
# into unverified_tx with the old height, and if we get
|
||||||
|
# a status update, that will overwrite it.
|
||||||
|
self.unverified_tx[tx_hash] = tx_height
|
||||||
txs.add(tx_hash)
|
txs.add(tx_hash)
|
||||||
return txs
|
return txs
|
||||||
|
|
||||||
@@ -547,18 +560,18 @@ class AddressSynchronizer(PrintError):
|
|||||||
return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
|
return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
|
||||||
|
|
||||||
def get_tx_height(self, tx_hash):
|
def get_tx_height(self, tx_hash):
|
||||||
""" Given a transaction, returns (height, conf, timestamp) """
|
""" Given a transaction, returns (height, conf, timestamp, header_hash) """
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if tx_hash in self.verified_tx:
|
if tx_hash in self.verified_tx:
|
||||||
height, timestamp, pos = self.verified_tx[tx_hash]
|
height, timestamp, pos, header_hash = self.verified_tx[tx_hash]
|
||||||
conf = max(self.get_local_height() - height + 1, 0)
|
conf = max(self.get_local_height() - height + 1, 0)
|
||||||
return height, conf, timestamp
|
return height, conf, timestamp, header_hash
|
||||||
elif tx_hash in self.unverified_tx:
|
elif tx_hash in self.unverified_tx:
|
||||||
height = self.unverified_tx[tx_hash]
|
height = self.unverified_tx[tx_hash]
|
||||||
return height, 0, None
|
return height, 0, None, None
|
||||||
else:
|
else:
|
||||||
# local transaction
|
# local transaction
|
||||||
return TX_HEIGHT_LOCAL, 0, None
|
return TX_HEIGHT_LOCAL, 0, None, None
|
||||||
|
|
||||||
def set_up_to_date(self, up_to_date):
|
def set_up_to_date(self, up_to_date):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
|
|||||||
column_title = self.headerItem().text(column)
|
column_title = self.headerItem().text(column)
|
||||||
column_data = item.text(column)
|
column_data = item.text(column)
|
||||||
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
|
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
|
||||||
height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
|
height, conf, timestamp, header_hash = self.wallet.get_tx_height(tx_hash)
|
||||||
tx = self.wallet.transactions.get(tx_hash)
|
tx = self.wallet.transactions.get(tx_hash)
|
||||||
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
||||||
is_unconfirmed = height <= 0
|
is_unconfirmed = height <= 0
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from .keystore import bip44_derivation
|
|||||||
|
|
||||||
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
||||||
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
||||||
FINAL_SEED_VERSION = 17 # electrum >= 2.7 will set this to prevent
|
FINAL_SEED_VERSION = 18 # electrum >= 2.7 will set this to prevent
|
||||||
# old versions from overwriting new format
|
# old versions from overwriting new format
|
||||||
|
|
||||||
|
|
||||||
@@ -356,6 +356,7 @@ class WalletStorage(JsonDB):
|
|||||||
self.convert_version_15()
|
self.convert_version_15()
|
||||||
self.convert_version_16()
|
self.convert_version_16()
|
||||||
self.convert_version_17()
|
self.convert_version_17()
|
||||||
|
self.convert_version_18()
|
||||||
|
|
||||||
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
||||||
self.write()
|
self.write()
|
||||||
@@ -570,6 +571,15 @@ class WalletStorage(JsonDB):
|
|||||||
|
|
||||||
self.put('seed_version', 17)
|
self.put('seed_version', 17)
|
||||||
|
|
||||||
|
def convert_version_18(self):
|
||||||
|
# delete verified_tx3 as its structure changed
|
||||||
|
if not self._is_upgrade_method_needed(17, 17):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.put('verified_tx3', None)
|
||||||
|
|
||||||
|
self.put('seed_version', 18)
|
||||||
|
|
||||||
def convert_imported(self):
|
def convert_imported(self):
|
||||||
if not self._is_upgrade_method_needed(0, 13):
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from typing import Sequence, Optional
|
|||||||
from .util import ThreadJob, bh2u
|
from .util import ThreadJob, bh2u
|
||||||
from .bitcoin import Hash, hash_decode, hash_encode
|
from .bitcoin import Hash, hash_decode, hash_encode
|
||||||
from .transaction import Transaction
|
from .transaction import Transaction
|
||||||
|
from .blockchain import hash_header
|
||||||
|
|
||||||
|
|
||||||
class MerkleVerificationFailure(Exception): pass
|
class MerkleVerificationFailure(Exception): pass
|
||||||
@@ -108,7 +109,8 @@ class SPV(ThreadJob):
|
|||||||
self.requested_merkle.remove(tx_hash)
|
self.requested_merkle.remove(tx_hash)
|
||||||
except KeyError: pass
|
except KeyError: pass
|
||||||
self.print_error("verified %s" % tx_hash)
|
self.print_error("verified %s" % tx_hash)
|
||||||
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
|
header_hash = hash_header(header)
|
||||||
|
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos, header_hash))
|
||||||
if self.is_up_to_date() and self.wallet.is_up_to_date():
|
if self.is_up_to_date() and self.wallet.is_up_to_date():
|
||||||
self.wallet.save_verified_tx(write=True)
|
self.wallet.save_verified_tx(write=True)
|
||||||
|
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||||||
if tx.is_complete():
|
if tx.is_complete():
|
||||||
if tx_hash in self.transactions.keys():
|
if tx_hash in self.transactions.keys():
|
||||||
label = self.get_label(tx_hash)
|
label = self.get_label(tx_hash)
|
||||||
height, conf, timestamp = self.get_tx_height(tx_hash)
|
height, conf, timestamp, header_hash = self.get_tx_height(tx_hash)
|
||||||
if height > 0:
|
if height > 0:
|
||||||
if conf:
|
if conf:
|
||||||
status = _("{} confirmations").format(conf)
|
status = _("{} confirmations").format(conf)
|
||||||
@@ -839,7 +839,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||||||
txid, n = txo.split(':')
|
txid, n = txo.split(':')
|
||||||
info = self.verified_tx.get(txid)
|
info = self.verified_tx.get(txid)
|
||||||
if info:
|
if info:
|
||||||
tx_height, timestamp, pos = info
|
tx_height, timestamp, pos, header_hash = info
|
||||||
conf = local_height - tx_height
|
conf = local_height - tx_height
|
||||||
else:
|
else:
|
||||||
conf = 0
|
conf = 0
|
||||||
@@ -1091,7 +1091,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||||||
|
|
||||||
def price_at_timestamp(self, txid, price_func):
|
def price_at_timestamp(self, txid, price_func):
|
||||||
"""Returns fiat price of bitcoin at the time tx got confirmed."""
|
"""Returns fiat price of bitcoin at the time tx got confirmed."""
|
||||||
height, conf, timestamp = self.get_tx_height(txid)
|
height, conf, timestamp, header_hash = self.get_tx_height(txid)
|
||||||
return price_func(timestamp if timestamp else time.time())
|
return price_func(timestamp if timestamp else time.time())
|
||||||
|
|
||||||
def unrealized_gains(self, domain, price_func, ccy):
|
def unrealized_gains(self, domain, price_func, ccy):
|
||||||
|
|||||||
Reference in New Issue
Block a user