electrum

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

commit bca6ad52410ca5ee8c5f4af3e589c0bcb24d8c5f
parent 9a7112009057bf40c40ed7644ec5a6e3619b6a8a
Author: SomberNight <somber.night@protonmail.com>
Date:   Tue, 26 Mar 2019 21:01:43 +0100

verifier: fix logic bug. after reorg, some verifs were not undone

after a reorg, in a many fork/orphan chains scenario,
we would sometimes not undo SPV for enough blocks

functions in blockchain.py somewhat based on kyuupichan/bitcoinX@5126bd15ef0c9ba36e17a455513452ebed7b2328

Diffstat:
Melectrum/address_synchronizer.py | 4++--
Melectrum/blockchain.py | 21+++++++++++++++++++++
Melectrum/tests/test_blockchain.py | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/verifier.py | 15+++++++--------
4 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py @@ -525,14 +525,14 @@ class AddressSynchronizer(PrintError): with self.lock: return dict(self.unverified_tx) # copy - def undo_verifications(self, blockchain, height): + def undo_verifications(self, blockchain, above_height): '''Used by the verifier when a reorg has happened''' txs = set() with self.lock: for tx_hash in self.db.list_verified_tx(): info = self.db.get_verified_tx(tx_hash) tx_height = info.height - if tx_height >= height: + if tx_height > above_height: header = blockchain.read_header(tx_height) if not header or hash_header(header) != info.header_hash: self.db.remove_verified_tx(tx_hash) diff --git a/electrum/blockchain.py b/electrum/blockchain.py @@ -201,6 +201,27 @@ class Blockchain(util.PrintError): with blockchains_lock: return list(filter(lambda y: y.parent==self, blockchains.values())) + def get_parent_heights(self) -> Mapping['Blockchain', int]: + """Returns map: (parent chain -> height of last common block)""" + with blockchains_lock: + result = {self: self.height()} + chain = self + while True: + parent = chain.parent + if parent is None: break + result[parent] = chain.forkpoint - 1 + chain = parent + return result + + def get_height_of_last_common_block_with_chain(self, other_chain: 'Blockchain') -> int: + last_common_block_height = 0 + our_parents = self.get_parent_heights() + their_parents = other_chain.get_parent_heights() + for chain in our_parents: + if chain in their_parents: + h = min(our_parents[chain], their_parents[chain]) + last_common_block_height = max(last_common_block_height, h) + return last_common_block_height @with_lock def get_branch_size(self) -> int: diff --git a/electrum/tests/test_blockchain.py b/electrum/tests/test_blockchain.py @@ -70,6 +70,62 @@ class TestBlockchain(SequentialTestCase): self.assertTrue(chain.can_connect(header)) chain.save_header(header) + def test_get_height_of_last_common_block_with_chain(self): + blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain( + config=self.config, forkpoint=0, parent=None, + forkpoint_hash=constants.net.GENESIS, prev_hash=None) + open(chain_u.path(), 'w+').close() + self._append_header(chain_u, self.HEADERS['A']) + self._append_header(chain_u, self.HEADERS['B']) + self._append_header(chain_u, self.HEADERS['C']) + self._append_header(chain_u, self.HEADERS['D']) + self._append_header(chain_u, self.HEADERS['E']) + self._append_header(chain_u, self.HEADERS['F']) + self._append_header(chain_u, self.HEADERS['O']) + self._append_header(chain_u, self.HEADERS['P']) + self._append_header(chain_u, self.HEADERS['Q']) + + chain_l = chain_u.fork(self.HEADERS['G']) + self._append_header(chain_l, self.HEADERS['H']) + self._append_header(chain_l, self.HEADERS['I']) + self._append_header(chain_l, self.HEADERS['J']) + self._append_header(chain_l, self.HEADERS['K']) + self._append_header(chain_l, self.HEADERS['L']) + + self.assertEqual({chain_u: 8, chain_l: 5}, chain_u.get_parent_heights()) + self.assertEqual({chain_l: 11}, chain_l.get_parent_heights()) + + chain_z = chain_l.fork(self.HEADERS['M']) + self._append_header(chain_z, self.HEADERS['N']) + self._append_header(chain_z, self.HEADERS['X']) + self._append_header(chain_z, self.HEADERS['Y']) + self._append_header(chain_z, self.HEADERS['Z']) + + self.assertEqual({chain_u: 8, chain_z: 5}, chain_u.get_parent_heights()) + self.assertEqual({chain_l: 11, chain_z: 8}, chain_l.get_parent_heights()) + self.assertEqual({chain_z: 13}, chain_z.get_parent_heights()) + self.assertEqual(5, chain_u.get_height_of_last_common_block_with_chain(chain_l)) + self.assertEqual(5, chain_l.get_height_of_last_common_block_with_chain(chain_u)) + self.assertEqual(5, chain_u.get_height_of_last_common_block_with_chain(chain_z)) + self.assertEqual(5, chain_z.get_height_of_last_common_block_with_chain(chain_u)) + self.assertEqual(8, chain_l.get_height_of_last_common_block_with_chain(chain_z)) + self.assertEqual(8, chain_z.get_height_of_last_common_block_with_chain(chain_l)) + + self._append_header(chain_u, self.HEADERS['R']) + self._append_header(chain_u, self.HEADERS['S']) + self._append_header(chain_u, self.HEADERS['T']) + self._append_header(chain_u, self.HEADERS['U']) + + self.assertEqual({chain_u: 12, chain_z: 5}, chain_u.get_parent_heights()) + self.assertEqual({chain_l: 11, chain_z: 8}, chain_l.get_parent_heights()) + self.assertEqual({chain_z: 13}, chain_z.get_parent_heights()) + self.assertEqual(5, chain_u.get_height_of_last_common_block_with_chain(chain_l)) + self.assertEqual(5, chain_l.get_height_of_last_common_block_with_chain(chain_u)) + self.assertEqual(5, chain_u.get_height_of_last_common_block_with_chain(chain_z)) + self.assertEqual(5, chain_z.get_height_of_last_common_block_with_chain(chain_u)) + self.assertEqual(8, chain_l.get_height_of_last_common_block_with_chain(chain_z)) + self.assertEqual(8, chain_z.get_height_of_last_common_block_with_chain(chain_l)) + def test_parents_after_forking(self): blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain( config=self.config, forkpoint=0, parent=None, diff --git a/electrum/verifier.py b/electrum/verifier.py @@ -168,18 +168,17 @@ class SPV(NetworkJobOnDefaultServer): raise InnerNodeOfSpvProofIsValidTx() async def _maybe_undo_verifications(self): - def undo_verifications(): - height = self.blockchain.get_max_forkpoint() - self.print_error("undoing verifications back to height {}".format(height)) - tx_hashes = self.wallet.undo_verifications(self.blockchain, height) + old_chain = self.blockchain + cur_chain = self.network.blockchain() + if cur_chain != old_chain: + self.blockchain = cur_chain + above_height = cur_chain.get_height_of_last_common_block_with_chain(old_chain) + self.print_error(f"undoing verifications above height {above_height}") + tx_hashes = self.wallet.undo_verifications(self.blockchain, above_height) for tx_hash in tx_hashes: self.print_error("redoing", tx_hash) self.remove_spv_proof_for_tx(tx_hash) - if self.network.blockchain() != self.blockchain: - self.blockchain = self.network.blockchain() - undo_verifications() - def remove_spv_proof_for_tx(self, tx_hash): self.merkle_roots.pop(tx_hash, None) try: