import logging
import time
import asyncio
import typing
import signal
from bisect import bisect_right
from struct import pack
from concurrent.futures.thread import ThreadPoolExecutor
from typing import Optional, List, Tuple, Set, DefaultDict, Dict
from prometheus_client import Gauge, Histogram
from collections import defaultdict

from scribe import __version__, PROMETHEUS_NAMESPACE
from scribe.db.db import HubDB
from scribe.db.prefixes import ACTIVATED_SUPPORT_TXO_TYPE, ACTIVATED_CLAIM_TXO_TYPE
from scribe.db.prefixes import PendingActivationKey, PendingActivationValue, ClaimToTXOValue
from scribe.common import hash_to_hex_str, hash160, RPCError, HISTOGRAM_BUCKETS
from scribe.blockchain.daemon import LBCDaemon
from scribe.blockchain.transaction import Tx, TxOutput, TxInput
from scribe.blockchain.prefetcher import Prefetcher
from scribe.schema.url import normalize_name
if typing.TYPE_CHECKING:
    from scribe.env import Env
    from scribe.db.revertable import RevertableOpStack


class ChainError(Exception):
    """Raised on error processing blocks."""


class StagedClaimtrieItem(typing.NamedTuple):
    name: str
    normalized_name: str
    claim_hash: bytes
    amount: int
    expiration_height: int
    tx_num: int
    position: int
    root_tx_num: int
    root_position: int
    channel_signature_is_valid: bool
    signing_hash: Optional[bytes]
    reposted_claim_hash: Optional[bytes]

    @property
    def is_update(self) -> bool:
        return (self.tx_num, self.position) != (self.root_tx_num, self.root_position)

    def invalidate_signature(self) -> 'StagedClaimtrieItem':
        return StagedClaimtrieItem(
            self.name, self.normalized_name, self.claim_hash, self.amount, self.expiration_height, self.tx_num,
            self.position, self.root_tx_num, self.root_position, False, None, self.reposted_claim_hash
        )


NAMESPACE = f"{PROMETHEUS_NAMESPACE}_writer"


class BlockProcessor:
    """Process blocks and update the DB state to match.

    Employ a prefetcher to prefetch blocks in batches for processing.
    Coordinate backing up in case of chain reorganisations.
    """

    block_count_metric = Gauge(
        "block_count", "Number of processed blocks", namespace=NAMESPACE
    )
    block_update_time_metric = Histogram(
        "block_time", "Block update times", namespace=NAMESPACE, buckets=HISTOGRAM_BUCKETS
    )
    reorg_count_metric = Gauge(
        "reorg_count", "Number of reorgs", namespace=NAMESPACE
    )

    def __init__(self, env: 'Env'):
        self.cancellable_tasks = []

        self.env = env
        self.state_lock = asyncio.Lock()
        self.daemon = LBCDaemon(env.coin, env.daemon_url)
        self._chain_executor = ThreadPoolExecutor(1, thread_name_prefix='block-processor')
        self.db = HubDB(
            env.coin, env.db_dir, env.cache_MB, env.reorg_limit, env.cache_all_claim_txos, env.cache_all_tx_hashes,
            max_open_files=env.db_max_open_files, blocking_channel_ids=env.blocking_channel_ids,
            filtering_channel_ids=env.filtering_channel_ids, executor=self._chain_executor
        )
        self.shutdown_event = asyncio.Event()
        self.coin = env.coin
        self.wait_for_blocks_duration = 0.1

        self._caught_up_event: Optional[asyncio.Event] = None
        self.height = 0
        self.tip = bytes.fromhex(self.coin.GENESIS_HASH)[::-1]
        self.tx_count = 0

        self.blocks_event = asyncio.Event()
        self.prefetcher = Prefetcher(self.daemon, env.coin, self.blocks_event)
        self.logger = logging.getLogger(__name__)

        # Meta
        self.touched_hashXs: Set[bytes] = set()

        # UTXO cache
        self.utxo_cache: Dict[Tuple[bytes, int], Tuple[bytes, int]] = {}

        # Claimtrie cache
        self.db_op_stack: Optional['RevertableOpStack'] = None

        #################################
        # attributes used for calculating stake activations and takeovers per block
        #################################

        self.taken_over_names: Set[str] = set()
        # txo to pending claim
        self.txo_to_claim: Dict[Tuple[int, int], StagedClaimtrieItem] = {}
        # claim hash to pending claim txo
        self.claim_hash_to_txo: Dict[bytes, Tuple[int, int]] = {}
        # claim hash to lists of pending support txos
        self.support_txos_by_claim: DefaultDict[bytes, List[Tuple[int, int]]] = defaultdict(list)
        # support txo: (supported claim hash, support amount)
        self.support_txo_to_claim: Dict[Tuple[int, int], Tuple[bytes, int]] = {}
        # removed supports {name: {claim_hash: [(tx_num, nout), ...]}}
        self.removed_support_txos_by_name_by_claim: DefaultDict[str, DefaultDict[bytes, List[Tuple[int, int]]]] = \
            defaultdict(lambda: defaultdict(list))
        self.abandoned_claims: Dict[bytes, StagedClaimtrieItem] = {}
        self.updated_claims: Set[bytes] = set()
        # removed activated support amounts by claim hash
        self.removed_active_support_amount_by_claim: DefaultDict[bytes, List[int]] = defaultdict(list)
        # pending activated support amounts by claim hash
        self.activated_support_amount_by_claim: DefaultDict[bytes, List[int]] = defaultdict(list)
        # pending activated name and claim hash to claim/update txo amount
        self.activated_claim_amount_by_name_and_hash: Dict[Tuple[str, bytes], int] = {}
        # pending claim and support activations per claim hash per name,
        # used to process takeovers due to added activations
        activation_by_claim_by_name_type = DefaultDict[str, DefaultDict[bytes, List[Tuple[PendingActivationKey, int]]]]
        self.activation_by_claim_by_name: activation_by_claim_by_name_type = defaultdict(lambda: defaultdict(list))
        # these are used for detecting early takeovers by not yet activated claims/supports
        self.possible_future_support_amounts_by_claim_hash: DefaultDict[bytes, List[int]] = defaultdict(list)
        self.possible_future_claim_amount_by_name_and_hash: Dict[Tuple[str, bytes], int] = {}
        self.possible_future_support_txos_by_claim_hash: DefaultDict[bytes, List[Tuple[int, int]]] = defaultdict(list)

        self.removed_claims_to_send_es = set()  # cumulative changes across blocks to send ES
        self.touched_claims_to_send_es = set()

        self.removed_claim_hashes: Set[bytes] = set()  # per block changes
        self.touched_claim_hashes: Set[bytes] = set()

        self.signatures_changed = set()

        self.pending_reposted = set()
        self.pending_channel_counts = defaultdict(lambda: 0)
        self.pending_support_amount_change = defaultdict(lambda: 0)

        self.pending_channels = {}
        self.amount_cache = {}
        self.expired_claim_hashes: Set[bytes] = set()

        self.doesnt_have_valid_signature: Set[bytes] = set()
        self.claim_channels: Dict[bytes, bytes] = {}
        self.hashXs_by_tx: DefaultDict[bytes, List[int]] = defaultdict(list)

        self.pending_transaction_num_mapping: Dict[bytes, int] = {}
        self.pending_transactions: Dict[int, bytes] = {}

        self._stopping = False
        self._ready_to_stop = asyncio.Event()

    async def run_in_thread_with_lock(self, func, *args):
        # Run in a thread to prevent blocking.  Shielded so that
        # cancellations from shutdown don't lose work - when the task
        # completes the data will be flushed and then we shut down.
        # Take the state lock to be certain in-memory state is
        # consistent and not being updated elsewhere.
        async def run_in_thread_locked():
            async with self.state_lock:
                return await asyncio.get_event_loop().run_in_executor(self._chain_executor, func, *args)
        return await asyncio.shield(run_in_thread_locked())

    async def run_in_thread(self, func, *args):
        async def run_in_thread():
            return await asyncio.get_event_loop().run_in_executor(self._chain_executor, func, *args)
        return await asyncio.shield(run_in_thread())

    async def refresh_mempool(self):
        def fetch_mempool(mempool_prefix):
            return {
                k.tx_hash: v.raw_tx for (k, v) in mempool_prefix.iterate()
            }

        def update_mempool(unsafe_commit, mempool_prefix, to_put, to_delete):
            for tx_hash, raw_tx in to_put:
                mempool_prefix.stage_put((tx_hash,), (raw_tx,))
            for tx_hash, raw_tx in to_delete.items():
                mempool_prefix.stage_delete((tx_hash,), (raw_tx,))
            unsafe_commit()

        async with self.state_lock:
            current_mempool = await self.run_in_thread(fetch_mempool, self.db.prefix_db.mempool_tx)
            _to_put = []
            try:
                mempool_hashes = await self.daemon.mempool_hashes()
            except (TypeError, RPCError):
                self.logger.warning("failed to get mempool tx hashes, reorg underway?")
                return
            for hh in mempool_hashes:
                tx_hash = bytes.fromhex(hh)[::-1]
                if tx_hash in current_mempool:
                    current_mempool.pop(tx_hash)
                else:
                    try:
                        _to_put.append((tx_hash, bytes.fromhex(await self.daemon.getrawtransaction(hh))))
                    except (TypeError, RPCError):
                        self.logger.warning("failed to get a mempool tx, reorg underway?")
                        return
            if current_mempool:
                if bytes.fromhex(await self.daemon.getbestblockhash())[::-1] != self.coin.header_hash(self.db.headers[-1]):
                    return
            await self.run_in_thread(
                update_mempool, self.db.prefix_db.unsafe_commit, self.db.prefix_db.mempool_tx, _to_put, current_mempool
            )

    async def check_and_advance_blocks(self, raw_blocks):
        """Process the list of raw blocks passed.  Detects and handles
        reorgs.
        """

        if not raw_blocks:
            return
        first = self.height + 1
        blocks = [self.coin.block(raw_block, first + n)
                  for n, raw_block in enumerate(raw_blocks)]
        headers = [block.header for block in blocks]
        hprevs = [self.coin.header_prevhash(h) for h in headers]
        chain = [self.tip] + [self.coin.header_hash(h) for h in headers[:-1]]

        if hprevs == chain:
            total_start = time.perf_counter()
            try:
                for block in blocks:
                    if self._stopping:
                        return
                    start = time.perf_counter()
                    start_count = self.tx_count
                    txo_count = await self.run_in_thread_with_lock(self.advance_block, block)
                    self.logger.info(
                        "writer advanced to %i (%i txs, %i txos) in %0.3fs", self.height, self.tx_count - start_count,
                        txo_count, time.perf_counter() - start
                    )
                    if self.height == self.coin.nExtendedClaimExpirationForkHeight:
                        self.logger.warning(
                            "applying extended claim expiration fork on claims accepted by, %i", self.height
                        )
                        await self.run_in_thread_with_lock(self.db.apply_expiration_extension_fork)
            except:
                self.logger.exception("advance blocks failed")
                raise
            processed_time = time.perf_counter() - total_start
            self.block_count_metric.set(self.height)
            self.block_update_time_metric.observe(processed_time)
            self.touched_hashXs.clear()
        elif hprevs[0] != chain[0]:
            min_start_height = max(self.height - self.coin.REORG_LIMIT, 0)
            count = 1
            block_hashes_from_lbrycrd = await self.daemon.block_hex_hashes(
                min_start_height, self.coin.REORG_LIMIT
            )
            for height, block_hash in zip(
                    reversed(range(min_start_height, min_start_height + self.coin.REORG_LIMIT)),
                    reversed(block_hashes_from_lbrycrd)):
                if self.db.get_block_hash(height)[::-1].hex() == block_hash:
                    break
                count += 1
            self.logger.warning(f"blockchain reorg detected at {self.height}, unwinding last {count} blocks")
            try:
                assert count > 0, count
                for _ in range(count):
                    await self.run_in_thread_with_lock(self.backup_block)
                    self.logger.info(f'backed up to height {self.height:,d}')

                    if self.env.cache_all_claim_txos:
                        await self.db._read_claim_txos()  # TODO: don't do this
                await self.prefetcher.reset_height(self.height)
                self.reorg_count_metric.inc()
            except:
                self.logger.exception("reorg blocks failed")
                raise
            finally:
                self.logger.info("backed up to block %i", self.height)
        else:
            # It is probably possible but extremely rare that what
            # bitcoind returns doesn't form a chain because it
            # reorg-ed the chain as it was processing the batched
            # block hash requests.  Should this happen it's simplest
            # just to reset the prefetcher and try again.
            self.logger.warning('daemon blocks do not form a chain; '
                                'resetting the prefetcher')
            await self.prefetcher.reset_height(self.height)

    def _add_claim_or_update(self, height: int, txo: 'TxOutput', tx_hash: bytes, tx_num: int, nout: int,
                             spent_claims: typing.Dict[bytes, typing.Tuple[int, int, str]], first_input: 'TxInput'):
        try:
            claim_name = txo.claim.name.decode()
        except UnicodeDecodeError:
            claim_name = ''.join(chr(c) for c in txo.claim.name)
        try:
            normalized_name = normalize_name(claim_name)
        except UnicodeDecodeError:
            normalized_name = claim_name
        if txo.is_claim:
            claim_hash = hash160(tx_hash + pack('>I', nout))[::-1]
            # print(f"\tnew {claim_hash.hex()} ({tx_num} {txo.value})")
        else:
            claim_hash = txo.claim.claim_hash[::-1]
            # print(f"\tupdate {claim_hash.hex()} ({tx_num} {txo.value})")

        signing_channel_hash = None
        channel_signature_is_valid = False
        reposted_claim_hash = None

        try:
            signable = txo.metadata
            is_repost = signable.is_repost
            if is_repost:
                reposted_claim_hash = signable.repost.reference.claim_hash[::-1]
                self.pending_reposted.add(reposted_claim_hash)
            is_channel = signable.is_channel
            if is_channel:
                self.pending_channels[claim_hash] = signable.channel.public_key_bytes
            if signable.is_signed:
                signing_channel_hash = signable.signing_channel_hash[::-1]
        except:  # google.protobuf.message.DecodeError: Could not parse JSON.
            signable = None
            # is_repost = False
            # is_channel = False
            reposted_claim_hash = None

        self.doesnt_have_valid_signature.add(claim_hash)
        raw_channel_tx = None
        if signable and signable.signing_channel_hash:
            signing_channel = self.db.get_claim_txo(signing_channel_hash)

            if signing_channel:
                raw_channel_tx = self.db.prefix_db.tx.get(
                    self.db.get_tx_hash(signing_channel.tx_num), deserialize_value=False
                )
            channel_pub_key_bytes = None
            try:
                if not signing_channel:
                    if txo.metadata.signing_channel_hash[::-1] in self.pending_channels:
                        channel_pub_key_bytes = self.pending_channels[signing_channel_hash]
                elif raw_channel_tx:
                    chan_output = self.coin.transaction(raw_channel_tx).outputs[signing_channel.position]
                    channel_meta = chan_output.metadata  # TODO: catch decode/type errors explicitly
                    channel_pub_key_bytes = channel_meta.channel.public_key_bytes
                if channel_pub_key_bytes:
                    channel_signature_is_valid = self.coin.verify_signed_metadata(
                        channel_pub_key_bytes, txo, first_input
                    )
                    if channel_signature_is_valid:
                        # print("\tvalidated signed claim")
                        self.pending_channel_counts[signing_channel_hash] += 1
                        self.doesnt_have_valid_signature.remove(claim_hash)
                        self.claim_channels[claim_hash] = signing_channel_hash
                    # else:
                    #     print("\tfailed to validate signed claim")
            except:
                self.logger.exception(f"error validating channel signature for %s:%i", tx_hash[::-1].hex(), nout)

        if txo.is_claim:  # it's a root claim
            root_tx_num, root_idx = tx_num, nout
            previous_amount = 0
        else:  # it's a claim update
            if claim_hash not in spent_claims:
                # print(f"\tthis is a wonky tx, contains unlinked claim update {claim_hash.hex()}")
                return
            if normalized_name != spent_claims[claim_hash][2]:
                self.logger.warning(
                    f"{tx_hash[::-1].hex()} contains mismatched name for claim update {claim_hash.hex()}"
                )
                return
            (prev_tx_num, prev_idx, _) = spent_claims.pop(claim_hash)
            # print(f"\tupdate {claim_hash.hex()} {tx_hash[::-1].hex()} {txo.value}")
            if (prev_tx_num, prev_idx) in self.txo_to_claim:
                previous_claim = self.txo_to_claim.pop((prev_tx_num, prev_idx))
                self.claim_hash_to_txo.pop(claim_hash)
                root_tx_num, root_idx = previous_claim.root_tx_num, previous_claim.root_position
            else:
                previous_claim = self._make_pending_claim_txo(claim_hash)
                root_tx_num, root_idx = previous_claim.root_tx_num, previous_claim.root_position
                activation = self.db.get_activation(prev_tx_num, prev_idx)
                claim_name = previous_claim.name
                self.get_remove_activate_ops(
                    ACTIVATED_CLAIM_TXO_TYPE, claim_hash, prev_tx_num, prev_idx, activation, normalized_name,
                    previous_claim.amount
                )
            previous_amount = previous_claim.amount
            self.updated_claims.add(claim_hash)

        if self.env.cache_all_claim_txos:
            self.db.claim_to_txo[claim_hash] = ClaimToTXOValue(
                tx_num, nout, root_tx_num, root_idx, txo.value, channel_signature_is_valid, claim_name
            )
            self.db.txo_to_claim[tx_num][nout] = claim_hash

        pending = StagedClaimtrieItem(
            claim_name, normalized_name, claim_hash, txo.value, self.coin.get_expiration_height(height), tx_num, nout,
            root_tx_num, root_idx, channel_signature_is_valid, signing_channel_hash, reposted_claim_hash
        )
        self.txo_to_claim[(tx_num, nout)] = pending
        self.claim_hash_to_txo[claim_hash] = (tx_num, nout)
        self.get_add_claim_utxo_ops(pending)

    def get_add_claim_utxo_ops(self, pending: StagedClaimtrieItem):
        # claim tip by claim hash
        self.db.prefix_db.claim_to_txo.stage_put(
            (pending.claim_hash,), (pending.tx_num, pending.position, pending.root_tx_num, pending.root_position,
                                    pending.amount, pending.channel_signature_is_valid, pending.name)
        )
        # claim hash by txo
        self.db.prefix_db.txo_to_claim.stage_put(
            (pending.tx_num, pending.position), (pending.claim_hash, pending.normalized_name)
        )

        # claim expiration
        self.db.prefix_db.claim_expiration.stage_put(
            (pending.expiration_height, pending.tx_num, pending.position),
            (pending.claim_hash, pending.normalized_name)
        )

        # short url resolution
        for prefix_len in range(10):
            self.db.prefix_db.claim_short_id.stage_put(
                (pending.normalized_name, pending.claim_hash.hex()[:prefix_len + 1],
                 pending.root_tx_num, pending.root_position),
                (pending.tx_num, pending.position)
            )

        if pending.signing_hash and pending.channel_signature_is_valid:
            # channel by stream
            self.db.prefix_db.claim_to_channel.stage_put(
                (pending.claim_hash, pending.tx_num, pending.position), (pending.signing_hash,)
            )
            # stream by channel
            self.db.prefix_db.channel_to_claim.stage_put(
                (pending.signing_hash, pending.normalized_name, pending.tx_num, pending.position),
                (pending.claim_hash,)
            )

        if pending.reposted_claim_hash:
            self.db.prefix_db.repost.stage_put((pending.claim_hash,), (pending.reposted_claim_hash,))
            self.db.prefix_db.reposted_claim.stage_put(
                (pending.reposted_claim_hash, pending.tx_num, pending.position), (pending.claim_hash,)
            )

    def get_remove_claim_utxo_ops(self, pending: StagedClaimtrieItem):
        # claim tip by claim hash
        self.db.prefix_db.claim_to_txo.stage_delete(
            (pending.claim_hash,), (pending.tx_num, pending.position, pending.root_tx_num, pending.root_position,
                                    pending.amount, pending.channel_signature_is_valid, pending.name)
        )
        # claim hash by txo
        self.db.prefix_db.txo_to_claim.stage_delete(
            (pending.tx_num, pending.position), (pending.claim_hash, pending.normalized_name)
        )

        # claim expiration
        self.db.prefix_db.claim_expiration.stage_delete(
            (pending.expiration_height, pending.tx_num, pending.position),
            (pending.claim_hash, pending.normalized_name)
        )

        # short url resolution
        for prefix_len in range(10):
            self.db.prefix_db.claim_short_id.stage_delete(
                (pending.normalized_name, pending.claim_hash.hex()[:prefix_len + 1],
                 pending.root_tx_num, pending.root_position),
                (pending.tx_num, pending.position)
            )

        if pending.signing_hash and pending.channel_signature_is_valid:
            # channel by stream
            self.db.prefix_db.claim_to_channel.stage_delete(
                (pending.claim_hash, pending.tx_num, pending.position), (pending.signing_hash,)
            )
            # stream by channel
            self.db.prefix_db.channel_to_claim.stage_delete(
                (pending.signing_hash, pending.normalized_name, pending.tx_num, pending.position),
                (pending.claim_hash,)
            )

        if pending.reposted_claim_hash:
            self.db.prefix_db.repost.stage_delete((pending.claim_hash,), (pending.reposted_claim_hash,))
            self.db.prefix_db.reposted_claim.stage_delete(
                (pending.reposted_claim_hash, pending.tx_num, pending.position), (pending.claim_hash,)
            )

    def _add_support(self, height: int, txo: 'TxOutput', tx_num: int, nout: int):
        supported_claim_hash = txo.support.claim_hash[::-1]
        self.support_txos_by_claim[supported_claim_hash].append((tx_num, nout))
        self.support_txo_to_claim[(tx_num, nout)] = supported_claim_hash, txo.value
        # print(f"\tsupport claim {supported_claim_hash.hex()} +{txo.value}")

        self.db.prefix_db.claim_to_support.stage_put((supported_claim_hash, tx_num, nout), (txo.value,))
        self.db.prefix_db.support_to_claim.stage_put((tx_num, nout), (supported_claim_hash,))
        self.pending_support_amount_change[supported_claim_hash] += txo.value

    def _add_claim_or_support(self, height: int, tx_hash: bytes, tx_num: int, nout: int, txo: 'TxOutput',
                              spent_claims: typing.Dict[bytes, Tuple[int, int, str]], first_input: 'TxInput'):
        if txo.is_claim or txo.is_update:
            self._add_claim_or_update(height, txo, tx_hash, tx_num, nout, spent_claims, first_input)
        elif txo.is_support:
            self._add_support(height, txo, tx_num, nout)

    def _spend_support_txo(self, height: int, txin: TxInput):
        txin_num = self.get_pending_tx_num(txin.prev_hash)
        activation = 0
        if (txin_num, txin.prev_idx) in self.support_txo_to_claim:
            spent_support, support_amount = self.support_txo_to_claim.pop((txin_num, txin.prev_idx))
            self.support_txos_by_claim[spent_support].remove((txin_num, txin.prev_idx))
            supported_name = self._get_pending_claim_name(spent_support)
            self.removed_support_txos_by_name_by_claim[supported_name][spent_support].append((txin_num, txin.prev_idx))
        else:
            spent_support, support_amount = self.db.get_supported_claim_from_txo(txin_num, txin.prev_idx)
            if not spent_support:  # it is not a support
                return
            supported_name = self._get_pending_claim_name(spent_support)
            if supported_name is not None:
                self.removed_support_txos_by_name_by_claim[supported_name][spent_support].append(
                    (txin_num, txin.prev_idx))
            activation = self.db.get_activation(txin_num, txin.prev_idx, is_support=True)
            if 0 < activation < self.height + 1:
                self.removed_active_support_amount_by_claim[spent_support].append(support_amount)
            if supported_name is not None and activation > 0:
                self.get_remove_activate_ops(
                    ACTIVATED_SUPPORT_TXO_TYPE, spent_support, txin_num, txin.prev_idx, activation, supported_name,
                    support_amount
                )
        # print(f"\tspent support for {spent_support.hex()} activation:{activation} {support_amount}")
        self.db.prefix_db.claim_to_support.stage_delete((spent_support, txin_num, txin.prev_idx), (support_amount,))
        self.db.prefix_db.support_to_claim.stage_delete((txin_num, txin.prev_idx), (spent_support,))
        self.pending_support_amount_change[spent_support] -= support_amount

    def _spend_claim_txo(self, txin: TxInput, spent_claims: Dict[bytes, Tuple[int, int, str]]) -> bool:
        txin_num = self.get_pending_tx_num(txin.prev_hash)
        if (txin_num, txin.prev_idx) in self.txo_to_claim:
            spent = self.txo_to_claim[(txin_num, txin.prev_idx)]
        else:
            if not self.db.get_cached_claim_exists(txin_num, txin.prev_idx):
                # txo is not a claim
                return False
            spent_claim_hash_and_name = self.db.get_claim_from_txo(
                txin_num, txin.prev_idx
            )
            assert spent_claim_hash_and_name is not None
            spent = self._make_pending_claim_txo(spent_claim_hash_and_name.claim_hash)

        if self.env.cache_all_claim_txos:
            claim_hash = self.db.txo_to_claim[txin_num].pop(txin.prev_idx)
            if not self.db.txo_to_claim[txin_num]:
                self.db.txo_to_claim.pop(txin_num)
            self.db.claim_to_txo.pop(claim_hash)
        if spent.reposted_claim_hash:
            self.pending_reposted.add(spent.reposted_claim_hash)
        if spent.signing_hash and spent.channel_signature_is_valid and spent.signing_hash not in self.abandoned_claims:
            self.pending_channel_counts[spent.signing_hash] -= 1
        spent_claims[spent.claim_hash] = (spent.tx_num, spent.position, spent.normalized_name)
        # print(f"\tspend lbry://{spent.name}#{spent.claim_hash.hex()}")
        self.get_remove_claim_utxo_ops(spent)
        return True

    def _spend_claim_or_support_txo(self, height: int, txin: TxInput, spent_claims):
        if not self._spend_claim_txo(txin, spent_claims):
            self._spend_support_txo(height, txin)

    def _abandon_claim(self, claim_hash: bytes, tx_num: int, nout: int, normalized_name: str):
        if (tx_num, nout) in self.txo_to_claim:
            pending = self.txo_to_claim.pop((tx_num, nout))
            self.claim_hash_to_txo.pop(claim_hash)
            self.abandoned_claims[pending.claim_hash] = pending
            claim_root_tx_num, claim_root_idx = pending.root_tx_num, pending.root_position
            prev_amount, prev_signing_hash = pending.amount, pending.signing_hash
            reposted_claim_hash, name = pending.reposted_claim_hash, pending.name
            expiration = self.coin.get_expiration_height(self.height)
            signature_is_valid = pending.channel_signature_is_valid
        else:
            v = self.db.get_claim_txo(
                claim_hash
            )
            claim_root_tx_num, claim_root_idx, prev_amount = v.root_tx_num,  v.root_position, v.amount
            signature_is_valid, name = v.channel_signature_is_valid, v.name
            prev_signing_hash = self.db.get_channel_for_claim(claim_hash, tx_num, nout)
            reposted_claim_hash = self.db.get_repost(claim_hash)
            expiration = self.coin.get_expiration_height(bisect_right(self.db.tx_counts, tx_num))
        self.abandoned_claims[claim_hash] = staged = StagedClaimtrieItem(
            name, normalized_name, claim_hash, prev_amount, expiration, tx_num, nout, claim_root_tx_num,
            claim_root_idx, signature_is_valid, prev_signing_hash, reposted_claim_hash
        )
        for support_txo_to_clear in self.support_txos_by_claim[claim_hash]:
            self.support_txo_to_claim.pop(support_txo_to_clear)
        self.support_txos_by_claim[claim_hash].clear()
        self.support_txos_by_claim.pop(claim_hash)
        if normalized_name.startswith('@'):  # abandon a channel, invalidate signatures
            self._invalidate_channel_signatures(claim_hash)

    def _get_invalidate_signature_ops(self, pending: StagedClaimtrieItem):
        if not pending.signing_hash:
            return
        self.db.prefix_db.claim_to_channel.stage_delete(
            (pending.claim_hash, pending.tx_num, pending.position), (pending.signing_hash,)
        )
        if pending.channel_signature_is_valid:
            self.db.prefix_db.channel_to_claim.stage_delete(
                (pending.signing_hash, pending.normalized_name, pending.tx_num, pending.position),
                (pending.claim_hash,)
            )
            self.db.prefix_db.claim_to_txo.stage_delete(
                (pending.claim_hash,),
                (pending.tx_num, pending.position, pending.root_tx_num, pending.root_position, pending.amount,
                 pending.channel_signature_is_valid, pending.name)
            )
            self.db.prefix_db.claim_to_txo.stage_put(
                (pending.claim_hash,),
                (pending.tx_num, pending.position, pending.root_tx_num, pending.root_position, pending.amount,
                 False, pending.name)
            )

    def _invalidate_channel_signatures(self, claim_hash: bytes):
        for (signed_claim_hash, ) in self.db.prefix_db.channel_to_claim.iterate(
                prefix=(claim_hash, ), include_key=False):
            if signed_claim_hash in self.abandoned_claims or signed_claim_hash in self.expired_claim_hashes:
                continue
            # there is no longer a signing channel for this claim as of this block
            if signed_claim_hash in self.doesnt_have_valid_signature:
                continue
            # the signing channel changed in this block
            if signed_claim_hash in self.claim_channels and signed_claim_hash != self.claim_channels[signed_claim_hash]:
                continue

            # if the claim with an invalidated signature is in this block, update the StagedClaimtrieItem
            # so that if we later try to spend it in this block we won't try to delete the channel info twice
            if signed_claim_hash in self.claim_hash_to_txo:
                signed_claim_txo = self.claim_hash_to_txo[signed_claim_hash]
                claim = self.txo_to_claim[signed_claim_txo]
                if claim.signing_hash != claim_hash:  # claim was already invalidated this block
                    continue
                self.txo_to_claim[signed_claim_txo] = claim.invalidate_signature()
            else:
                claim = self._make_pending_claim_txo(signed_claim_hash)
            self.signatures_changed.add(signed_claim_hash)
            self.pending_channel_counts[claim_hash] -= 1
            self._get_invalidate_signature_ops(claim)

        for staged in list(self.txo_to_claim.values()):
            needs_invalidate = staged.claim_hash not in self.doesnt_have_valid_signature
            if staged.signing_hash == claim_hash and needs_invalidate:
                self._get_invalidate_signature_ops(staged)
                self.txo_to_claim[self.claim_hash_to_txo[staged.claim_hash]] = staged.invalidate_signature()
                self.signatures_changed.add(staged.claim_hash)
                self.pending_channel_counts[claim_hash] -= 1

    def _make_pending_claim_txo(self, claim_hash: bytes):
        claim = self.db.get_claim_txo(claim_hash)
        if claim_hash in self.doesnt_have_valid_signature:
            signing_hash = None
        else:
            signing_hash = self.db.get_channel_for_claim(claim_hash, claim.tx_num, claim.position)
        reposted_claim_hash = self.db.get_repost(claim_hash)
        return StagedClaimtrieItem(
            claim.name, claim.normalized_name, claim_hash, claim.amount,
            self.coin.get_expiration_height(
                bisect_right(self.db.tx_counts, claim.tx_num),
                extended=self.height >= self.coin.nExtendedClaimExpirationForkHeight
            ),
            claim.tx_num, claim.position, claim.root_tx_num, claim.root_position,
            claim.channel_signature_is_valid, signing_hash, reposted_claim_hash
        )

    def _expire_claims(self, height: int):
        expired = self.db.get_expired_by_height(height)
        self.expired_claim_hashes.update(set(expired.keys()))
        spent_claims = {}
        for expired_claim_hash, (tx_num, position, name, txi) in expired.items():
            if (tx_num, position) not in self.txo_to_claim:
                self._spend_claim_txo(txi, spent_claims)
        if expired:
            # abandon the channels last to handle abandoned signed claims in the same tx,
            # see test_abandon_channel_and_claims_in_same_tx
            expired_channels = {}
            for abandoned_claim_hash, (tx_num, nout, normalized_name) in spent_claims.items():
                self._abandon_claim(abandoned_claim_hash, tx_num, nout, normalized_name)

                if normalized_name.startswith('@'):
                    expired_channels[abandoned_claim_hash] = (tx_num, nout, normalized_name)
                else:
                    # print(f"\texpire {abandoned_claim_hash.hex()} {tx_num} {nout}")
                    self._abandon_claim(abandoned_claim_hash, tx_num, nout, normalized_name)

            # do this to follow the same content claim removing pathway as if a claim (possible channel) was abandoned
            for abandoned_claim_hash, (tx_num, nout, normalized_name) in expired_channels.items():
                # print(f"\texpire {abandoned_claim_hash.hex()} {tx_num} {nout}")
                self._abandon_claim(abandoned_claim_hash, tx_num, nout, normalized_name)

    def _cached_get_active_amount(self, claim_hash: bytes, txo_type: int, height: int) -> int:
        if (claim_hash, txo_type, height) in self.amount_cache:
            return self.amount_cache[(claim_hash, txo_type, height)]
        if txo_type == ACTIVATED_CLAIM_TXO_TYPE:
            if claim_hash in self.claim_hash_to_txo:
                amount = self.txo_to_claim[self.claim_hash_to_txo[claim_hash]].amount
            else:
                amount = self.db.get_active_amount_as_of_height(
                    claim_hash, height
                )
            self.amount_cache[(claim_hash, txo_type, height)] = amount
        else:
            self.amount_cache[(claim_hash, txo_type, height)] = amount = self.db._get_active_amount(
                claim_hash, txo_type, height
            )
        return amount

    def _get_pending_claim_amount(self, name: str, claim_hash: bytes, height=None) -> int:
        if (name, claim_hash) in self.activated_claim_amount_by_name_and_hash:
            if claim_hash in self.claim_hash_to_txo:
                return self.txo_to_claim[self.claim_hash_to_txo[claim_hash]].amount
            return self.activated_claim_amount_by_name_and_hash[(name, claim_hash)]
        if (name, claim_hash) in self.possible_future_claim_amount_by_name_and_hash:
            return self.possible_future_claim_amount_by_name_and_hash[(name, claim_hash)]
        return self._cached_get_active_amount(claim_hash, ACTIVATED_CLAIM_TXO_TYPE, height or (self.height + 1))

    def _get_pending_claim_name(self, claim_hash: bytes) -> Optional[str]:
        assert claim_hash is not None
        if claim_hash in self.claim_hash_to_txo:
            return self.txo_to_claim[self.claim_hash_to_txo[claim_hash]].normalized_name
        claim_info = self.db.get_claim_txo(claim_hash)
        if claim_info:
            return claim_info.normalized_name

    def _get_pending_supported_amount(self, claim_hash: bytes, height: Optional[int] = None) -> int:
        amount = self._cached_get_active_amount(claim_hash, ACTIVATED_SUPPORT_TXO_TYPE, height or (self.height + 1))
        if claim_hash in self.activated_support_amount_by_claim:
            amount += sum(self.activated_support_amount_by_claim[claim_hash])
        if claim_hash in self.possible_future_support_amounts_by_claim_hash:
            amount += sum(self.possible_future_support_amounts_by_claim_hash[claim_hash])
        if claim_hash in self.removed_active_support_amount_by_claim:
            return amount - sum(self.removed_active_support_amount_by_claim[claim_hash])
        return amount

    def _get_pending_effective_amount(self, name: str, claim_hash: bytes, height: Optional[int] = None) -> int:
        claim_amount = self._get_pending_claim_amount(name, claim_hash, height=height)
        support_amount = self._get_pending_supported_amount(claim_hash, height=height)
        return claim_amount + support_amount

    def get_activate_ops(self, txo_type: int, claim_hash: bytes, tx_num: int, position: int,
                          activation_height: int, name: str, amount: int):
        self.db.prefix_db.activated.stage_put(
            (txo_type, tx_num, position), (activation_height, claim_hash, name)
        )
        self.db.prefix_db.pending_activation.stage_put(
            (activation_height, txo_type, tx_num, position), (claim_hash, name)
        )
        self.db.prefix_db.active_amount.stage_put(
            (claim_hash, txo_type, activation_height, tx_num, position), (amount,)
        )

    def get_remove_activate_ops(self, txo_type: int, claim_hash: bytes, tx_num: int, position: int,
                                activation_height: int, name: str, amount: int):
        self.db.prefix_db.activated.stage_delete(
            (txo_type, tx_num, position), (activation_height, claim_hash, name)
        )
        self.db.prefix_db.pending_activation.stage_delete(
            (activation_height, txo_type, tx_num, position), (claim_hash, name)
        )
        self.db.prefix_db.active_amount.stage_delete(
            (claim_hash, txo_type, activation_height, tx_num, position), (amount,)
        )

    def _get_takeover_ops(self, height: int):

        # cache for controlling claims as of the previous block
        controlling_claims = {}

        def get_controlling(_name):
            if _name not in controlling_claims:
                _controlling = self.db.get_controlling_claim(_name)
                controlling_claims[_name] = _controlling
            else:
                _controlling = controlling_claims[_name]
            return _controlling

        names_with_abandoned_or_updated_controlling_claims: List[str] = []

        # get the claims and supports previously scheduled to be activated at this block
        activated_at_height = self.db.get_activated_at_height(height)
        activate_in_future = defaultdict(lambda: defaultdict(list))
        future_activations = defaultdict(dict)

        def get_delayed_activate_ops(name: str, claim_hash: bytes, is_new_claim: bool, tx_num: int, nout: int,
                                     amount: int, is_support: bool):
            controlling = get_controlling(name)
            nothing_is_controlling = not controlling
            staged_is_controlling = False if not controlling else claim_hash == controlling.claim_hash
            controlling_is_abandoned = False if not controlling else \
                name in names_with_abandoned_or_updated_controlling_claims

            if nothing_is_controlling or staged_is_controlling or controlling_is_abandoned:
                delay = 0
            elif is_new_claim:
                delay = self.coin.get_delay_for_name(height - controlling.height)
            else:
                controlling_effective_amount = self._get_pending_effective_amount(name, controlling.claim_hash)
                staged_effective_amount = self._get_pending_effective_amount(name, claim_hash)
                staged_update_could_cause_takeover = staged_effective_amount > controlling_effective_amount
                delay = 0 if not staged_update_could_cause_takeover else self.coin.get_delay_for_name(
                    height - controlling.height
                )
            if delay == 0:  # if delay was 0 it needs to be considered for takeovers
                activated_at_height[PendingActivationValue(claim_hash, name)].append(
                    PendingActivationKey(
                        height, ACTIVATED_SUPPORT_TXO_TYPE if is_support else ACTIVATED_CLAIM_TXO_TYPE, tx_num, nout
                    )
                )
            else:  # if the delay was higher if still needs to be considered if something else triggers a takeover
                activate_in_future[name][claim_hash].append((
                    PendingActivationKey(
                        height + delay, ACTIVATED_SUPPORT_TXO_TYPE if is_support else ACTIVATED_CLAIM_TXO_TYPE,
                        tx_num, nout
                    ), amount
                ))
                if is_support:
                    self.possible_future_support_txos_by_claim_hash[claim_hash].append((tx_num, nout))
            self.get_activate_ops(
                ACTIVATED_SUPPORT_TXO_TYPE if is_support else ACTIVATED_CLAIM_TXO_TYPE, claim_hash, tx_num, nout,
                height + delay, name, amount
            )

        # determine names needing takeover/deletion due to controlling claims being abandoned
        # and add ops to deactivate abandoned claims
        for claim_hash, staged in self.abandoned_claims.items():
            controlling = get_controlling(staged.normalized_name)
            if controlling and controlling.claim_hash == claim_hash:
                names_with_abandoned_or_updated_controlling_claims.append(staged.normalized_name)
                # print(f"\t{staged.name} needs takeover")
            activation = self.db.get_activation(staged.tx_num, staged.position)
            if activation > 0:  #  db returns -1 for non-existent txos
                # removed queued future activation from the db
                self.get_remove_activate_ops(
                    ACTIVATED_CLAIM_TXO_TYPE, staged.claim_hash, staged.tx_num, staged.position,
                    activation, staged.normalized_name, staged.amount
                )
            else:
                # it hadn't yet been activated
                pass

        # get the removed activated supports for controlling claims to determine if takeovers are possible
        abandoned_support_check_need_takeover = defaultdict(list)
        for claim_hash, amounts in self.removed_active_support_amount_by_claim.items():
            name = self._get_pending_claim_name(claim_hash)
            if name is None:
                continue
            controlling = get_controlling(name)
            if controlling and controlling.claim_hash == claim_hash and \
                    name not in names_with_abandoned_or_updated_controlling_claims:
                abandoned_support_check_need_takeover[(name, claim_hash)].extend(amounts)

        # get the controlling claims with updates to the claim to check if takeover is needed
        for claim_hash in self.updated_claims:
            if claim_hash in self.abandoned_claims:
                continue
            name = self._get_pending_claim_name(claim_hash)
            if name is None:
                continue
            controlling = get_controlling(name)
            if controlling and controlling.claim_hash == claim_hash and \
                    name not in names_with_abandoned_or_updated_controlling_claims:
                names_with_abandoned_or_updated_controlling_claims.append(name)

        # prepare to activate or delay activation of the pending claims being added this block
        for (tx_num, nout), staged in self.txo_to_claim.items():
            is_delayed = not staged.is_update
            prev_txo = self.db.get_cached_claim_txo(staged.claim_hash)
            if prev_txo:
                prev_activation = self.db.get_activation(prev_txo.tx_num, prev_txo.position)
                if height < prev_activation or prev_activation < 0:
                    is_delayed = True
            get_delayed_activate_ops(
                staged.normalized_name, staged.claim_hash, is_delayed, tx_num, nout, staged.amount,
                is_support=False
            )

        # and the supports
        for (tx_num, nout), (claim_hash, amount) in self.support_txo_to_claim.items():
            if claim_hash in self.abandoned_claims:
                continue
            elif claim_hash in self.claim_hash_to_txo:
                name = self.txo_to_claim[self.claim_hash_to_txo[claim_hash]].normalized_name
                staged_is_new_claim = not self.txo_to_claim[self.claim_hash_to_txo[claim_hash]].is_update
            else:
                supported_claim_info = self.db.get_claim_txo(claim_hash)
                if not supported_claim_info:
                    # the supported claim doesn't exist
                    continue
                else:
                    v = supported_claim_info
                name = v.normalized_name
                staged_is_new_claim = (v.root_tx_num, v.root_position) == (v.tx_num, v.position)
            get_delayed_activate_ops(
                name, claim_hash, staged_is_new_claim, tx_num, nout, amount, is_support=True
            )

        # add the activation/delayed-activation ops
        for activated, activated_txos in activated_at_height.items():
            controlling = get_controlling(activated.normalized_name)
            if activated.claim_hash in self.abandoned_claims:
                continue
            reactivate = False
            if not controlling or controlling.claim_hash == activated.claim_hash:
                # there is no delay for claims to a name without a controlling value or to the controlling value
                reactivate = True
            for activated_txo in activated_txos:
                if activated_txo.is_support and (activated_txo.tx_num, activated_txo.position) in \
                        self.removed_support_txos_by_name_by_claim[activated.normalized_name][activated.claim_hash]:
                    # print("\tskip activate support for pending abandoned claim")
                    continue
                if activated_txo.is_claim:
                    txo_type = ACTIVATED_CLAIM_TXO_TYPE
                    txo_tup = (activated_txo.tx_num, activated_txo.position)
                    if txo_tup in self.txo_to_claim:
                        amount = self.txo_to_claim[txo_tup].amount
                    else:
                        amount = self.db.get_claim_txo_amount(
                            activated.claim_hash
                        )
                    if amount is None:
                        # print("\tskip activate for non existent claim")
                        continue
                    self.activated_claim_amount_by_name_and_hash[(activated.normalized_name, activated.claim_hash)] = amount
                else:
                    txo_type = ACTIVATED_SUPPORT_TXO_TYPE
                    txo_tup = (activated_txo.tx_num, activated_txo.position)
                    if txo_tup in self.support_txo_to_claim:
                        amount = self.support_txo_to_claim[txo_tup][1]
                    else:
                        amount = self.db.get_support_txo_amount(
                            activated.claim_hash, activated_txo.tx_num, activated_txo.position
                        )
                    if amount is None:
                        # print("\tskip activate support for non existent claim")
                        continue
                    self.activated_support_amount_by_claim[activated.claim_hash].append(amount)
                self.activation_by_claim_by_name[activated.normalized_name][activated.claim_hash].append((activated_txo, amount))
                # print(f"\tactivate {'support' if txo_type == ACTIVATED_SUPPORT_TXO_TYPE else 'claim'} "
                #       f"{activated.claim_hash.hex()} @ {activated_txo.height}")

        # go through claims where the controlling claim or supports to the controlling claim have been abandoned
        # check if takeovers are needed or if the name node is now empty
        need_reactivate_if_takes_over = {}
        for need_takeover in names_with_abandoned_or_updated_controlling_claims:
            existing = self.db.get_claim_txos_for_name(need_takeover)
            has_candidate = False
            # add existing claims to the queue for the takeover
            # track that we need to reactivate these if one of them becomes controlling
            for candidate_claim_hash, (tx_num, nout) in existing.items():
                if candidate_claim_hash in self.abandoned_claims:
                    continue
                has_candidate = True
                existing_activation = self.db.get_activation(tx_num, nout)
                activate_key = PendingActivationKey(
                    existing_activation, ACTIVATED_CLAIM_TXO_TYPE, tx_num, nout
                )
                self.activation_by_claim_by_name[need_takeover][candidate_claim_hash].append((
                    activate_key, self.db.get_claim_txo_amount(candidate_claim_hash)
                ))
                need_reactivate_if_takes_over[(need_takeover, candidate_claim_hash)] = activate_key
                # print(f"\tcandidate to takeover abandoned controlling claim for "
                #       f"{activate_key.tx_num}:{activate_key.position} {activate_key.is_claim}")
            if not has_candidate:
                # remove name takeover entry, the name is now unclaimed
                controlling = get_controlling(need_takeover)
                self.db.prefix_db.claim_takeover.stage_delete(
                    (need_takeover,), (controlling.claim_hash, controlling.height)
                )

        # scan for possible takeovers out of the accumulated activations, of these make sure there
        # aren't any future activations for the taken over names with yet higher amounts, if there are
        # these need to get activated now and take over instead. for example:
        # claim A is winning for 0.1 for long enough for a > 1 takeover delay
        # claim B is made for 0.2
        # a block later, claim C is made for 0.3, it will schedule to activate 1 (or rarely 2) block(s) after B
        # upon the delayed activation of B, we need to detect to activate C and make it take over early instead

        claim_exists = {}
        for activated, activated_claim_txo in self.db.get_future_activated(height).items():
            # uses the pending effective amount for the future activation height, not the current height
            future_amount = self._get_pending_claim_amount(
                activated.normalized_name, activated.claim_hash, activated_claim_txo.height + 1
            )
            if activated.claim_hash not in claim_exists:
                claim_exists[activated.claim_hash] = activated.claim_hash in self.claim_hash_to_txo or (
                        self.db.get_claim_txo(activated.claim_hash) is not None)
            if claim_exists[activated.claim_hash] and activated.claim_hash not in self.abandoned_claims:
                v = future_amount, activated, activated_claim_txo
                future_activations[activated.normalized_name][activated.claim_hash] = v

        for name, future_activated in activate_in_future.items():
            for claim_hash, activated in future_activated.items():
                if claim_hash not in claim_exists:
                    claim_exists[claim_hash] = claim_hash in self.claim_hash_to_txo or (
                            self.db.get_claim_txo(claim_hash) is not None)
                if not claim_exists[claim_hash]:
                    continue
                if claim_hash in self.abandoned_claims:
                    continue
                for txo in activated:
                    v = txo[1], PendingActivationValue(claim_hash, name), txo[0]
                    future_activations[name][claim_hash] = v
                    if txo[0].is_claim:
                        self.possible_future_claim_amount_by_name_and_hash[(name, claim_hash)] = txo[1]
                    else:
                        self.possible_future_support_amounts_by_claim_hash[claim_hash].append(txo[1])

        # process takeovers
        checked_names = set()
        for name, activated in self.activation_by_claim_by_name.items():
            checked_names.add(name)
            controlling = controlling_claims[name]
            amounts = {
                claim_hash: self._get_pending_effective_amount(name, claim_hash)
                for claim_hash in activated.keys() if claim_hash not in self.abandoned_claims
            }
            # if there is a controlling claim include it in the amounts to ensure it remains the max
            if controlling and controlling.claim_hash not in self.abandoned_claims:
                amounts[controlling.claim_hash] = self._get_pending_effective_amount(name, controlling.claim_hash)
            winning_claim_hash = max(amounts, key=lambda x: amounts[x])
            if not controlling or (winning_claim_hash != controlling.claim_hash and
                                   name in names_with_abandoned_or_updated_controlling_claims) or \
                    ((winning_claim_hash != controlling.claim_hash) and (amounts[winning_claim_hash] > amounts[controlling.claim_hash])):
                amounts_with_future_activations = {claim_hash: amount for claim_hash, amount in amounts.items()}
                amounts_with_future_activations.update(
                    {
                        claim_hash: self._get_pending_effective_amount(
                            name, claim_hash, self.height + 1 + self.coin.maxTakeoverDelay
                        ) for claim_hash in future_activations[name]
                    }
                )
                winning_including_future_activations = max(
                    amounts_with_future_activations, key=lambda x: amounts_with_future_activations[x]
                )
                future_winning_amount = amounts_with_future_activations[winning_including_future_activations]

                if winning_claim_hash != winning_including_future_activations and \
                        future_winning_amount > amounts[winning_claim_hash]:
                    # print(f"\ttakeover by {winning_claim_hash.hex()} triggered early activation and "
                    #       f"takeover by {winning_including_future_activations.hex()} at {height}")
                    # handle a pending activated claim jumping the takeover delay when another name takes over
                    if winning_including_future_activations not in self.claim_hash_to_txo:
                        claim = self.db.get_claim_txo(winning_including_future_activations)
                        tx_num = claim.tx_num
                        position = claim.position
                        amount = claim.amount
                        activation = self.db.get_activation(tx_num, position)
                    else:
                        tx_num, position = self.claim_hash_to_txo[winning_including_future_activations]
                        amount = self.txo_to_claim[(tx_num, position)].amount
                        activation = None
                        for (k, tx_amount) in activate_in_future[name][winning_including_future_activations]:
                            if (k.tx_num, k.position) == (tx_num, position):
                                activation = k.height
                                break
                        if activation is None:
                            # TODO: reproduce this in an integration test (block 604718)
                            _k = PendingActivationValue(winning_including_future_activations, name)
                            if _k in activated_at_height:
                                for pending_activation in activated_at_height[_k]:
                                    if (pending_activation.tx_num, pending_activation.position) == (tx_num, position):
                                        activation = pending_activation.height
                                        break
                        assert None not in (amount, activation)
                    # update the claim that's activating early
                    self.get_remove_activate_ops(
                        ACTIVATED_CLAIM_TXO_TYPE, winning_including_future_activations, tx_num,
                        position, activation, name, amount
                    )
                    self.get_activate_ops(
                        ACTIVATED_CLAIM_TXO_TYPE, winning_including_future_activations, tx_num,
                        position, height, name, amount
                    )

                    for (k, amount) in activate_in_future[name][winning_including_future_activations]:
                        txo = (k.tx_num, k.position)
                        if txo in self.possible_future_support_txos_by_claim_hash[winning_including_future_activations]:
                            self.get_remove_activate_ops(
                                ACTIVATED_SUPPORT_TXO_TYPE, winning_including_future_activations, k.tx_num,
                                k.position, k.height, name, amount
                            )
                            self.get_activate_ops(
                                ACTIVATED_SUPPORT_TXO_TYPE, winning_including_future_activations, k.tx_num,
                                k.position, height, name, amount
                            )
                    self.taken_over_names.add(name)
                    if controlling:
                        self.db.prefix_db.claim_takeover.stage_delete(
                            (name,), (controlling.claim_hash, controlling.height)
                        )
                    self.db.prefix_db.claim_takeover.stage_put((name,), (winning_including_future_activations, height))
                    self.touched_claim_hashes.add(winning_including_future_activations)
                    if controlling and controlling.claim_hash not in self.abandoned_claims:
                        self.touched_claim_hashes.add(controlling.claim_hash)
                elif not controlling or (winning_claim_hash != controlling.claim_hash and
                                       name in names_with_abandoned_or_updated_controlling_claims) or \
                        ((winning_claim_hash != controlling.claim_hash) and (amounts[winning_claim_hash] > amounts[controlling.claim_hash])):
                    # print(f"\ttakeover by {winning_claim_hash.hex()} at {height}")
                    if (name, winning_claim_hash) in need_reactivate_if_takes_over:
                        previous_pending_activate = need_reactivate_if_takes_over[(name, winning_claim_hash)]
                        amount = self.db.get_claim_txo_amount(
                            winning_claim_hash
                        )
                        if winning_claim_hash in self.claim_hash_to_txo:
                            tx_num, position = self.claim_hash_to_txo[winning_claim_hash]
                            amount = self.txo_to_claim[(tx_num, position)].amount
                        else:
                            tx_num, position = previous_pending_activate.tx_num, previous_pending_activate.position
                        if previous_pending_activate.height > height:
                            # the claim had a pending activation in the future, move it to now
                            if tx_num < self.tx_count:
                                self.get_remove_activate_ops(
                                    ACTIVATED_CLAIM_TXO_TYPE, winning_claim_hash, tx_num,
                                    position, previous_pending_activate.height, name, amount
                                )
                            self.get_activate_ops(
                                ACTIVATED_CLAIM_TXO_TYPE, winning_claim_hash, tx_num,
                                position, height, name, amount
                            )
                    self.taken_over_names.add(name)
                    if controlling:
                        self.db.prefix_db.claim_takeover.stage_delete(
                            (name,), (controlling.claim_hash, controlling.height)
                        )
                    self.db.prefix_db.claim_takeover.stage_put((name,), (winning_claim_hash, height))
                    if controlling and controlling.claim_hash not in self.abandoned_claims:
                        self.touched_claim_hashes.add(controlling.claim_hash)
                    self.touched_claim_hashes.add(winning_claim_hash)
                elif winning_claim_hash == controlling.claim_hash:
                    # print("\tstill winning")
                    pass
                else:
                    # print("\tno takeover")
                    pass

        # handle remaining takeovers from abandoned supports
        for (name, claim_hash), amounts in abandoned_support_check_need_takeover.items():
            if name in checked_names:
                continue
            checked_names.add(name)
            controlling = get_controlling(name)
            amounts = {
                claim_hash: self._get_pending_effective_amount(name, claim_hash)
                for claim_hash in self.db.get_claims_for_name(name) if claim_hash not in self.abandoned_claims
            }
            if controlling and controlling.claim_hash not in self.abandoned_claims:
                amounts[controlling.claim_hash] = self._get_pending_effective_amount(name, controlling.claim_hash)
            winning = max(amounts, key=lambda x: amounts[x])

            if (controlling and winning != controlling.claim_hash) or (not controlling and winning):
                self.taken_over_names.add(name)
                # print(f"\ttakeover from abandoned support {controlling.claim_hash.hex()} -> {winning.hex()}")
                if controlling:
                    self.db.prefix_db.claim_takeover.stage_delete(
                        (name,), (controlling.claim_hash, controlling.height)
                    )
                self.db.prefix_db.claim_takeover.stage_put((name,), (winning, height))
                if controlling:
                    self.touched_claim_hashes.add(controlling.claim_hash)
                self.touched_claim_hashes.add(winning)

    def _get_cumulative_update_ops(self, height: int):
        # update the last takeover height for names with takeovers
        for name in self.taken_over_names:
            self.touched_claim_hashes.update(
                {claim_hash for claim_hash in self.db.get_claims_for_name(name)
                 if claim_hash not in self.abandoned_claims}
            )

        # gather cumulative removed/touched sets to update the search index
        self.removed_claim_hashes.update(set(self.abandoned_claims.keys()))
        self.touched_claim_hashes.difference_update(self.removed_claim_hashes)
        self.touched_claim_hashes.update(
            set(
                map(lambda item: item[1], self.activated_claim_amount_by_name_and_hash.keys())
            ).union(
                set(self.claim_hash_to_txo.keys())
            ).union(
                self.removed_active_support_amount_by_claim.keys()
            ).union(
                self.signatures_changed
            ).union(
                set(self.removed_active_support_amount_by_claim.keys())
            ).union(
                set(self.activated_support_amount_by_claim.keys())
            ).union(
                set(self.pending_support_amount_change.keys())
            ).difference(
                self.removed_claim_hashes
            )
        )

        # update support amount totals
        for supported_claim, amount in self.pending_support_amount_change.items():
            existing = self.db.prefix_db.support_amount.get(supported_claim)
            total = amount
            if existing is not None:
                total += existing.amount
                self.db.prefix_db.support_amount.stage_delete((supported_claim,), existing)
            self.db.prefix_db.support_amount.stage_put((supported_claim,), (total,))

        # use the cumulative changes to update bid ordered resolve
        for removed in self.removed_claim_hashes:
            removed_claim = self.db.get_claim_txo(removed)
            if removed_claim:
                amt = self.db.get_url_effective_amount(
                    removed_claim.normalized_name, removed
                )
                if amt:
                    self.db.prefix_db.effective_amount.stage_delete(
                        (removed_claim.normalized_name, amt.effective_amount, amt.tx_num, amt.position), (removed,)
                    )
        for touched in self.touched_claim_hashes:
            prev_effective_amount = 0

            if touched in self.claim_hash_to_txo:
                pending = self.txo_to_claim[self.claim_hash_to_txo[touched]]
                name, tx_num, position = pending.normalized_name, pending.tx_num, pending.position
                claim_from_db = self.db.get_claim_txo(touched)
                if claim_from_db:
                    claim_amount_info = self.db.get_url_effective_amount(name, touched)
                    if claim_amount_info:
                        prev_effective_amount = claim_amount_info.effective_amount
                        self.db.prefix_db.effective_amount.stage_delete(
                            (name, claim_amount_info.effective_amount, claim_amount_info.tx_num,
                             claim_amount_info.position), (touched,)
                        )
            else:
                v = self.db.get_claim_txo(touched)
                if not v:
                    continue
                name, tx_num, position = v.normalized_name, v.tx_num, v.position
                amt = self.db.get_url_effective_amount(name, touched)
                if amt:
                    prev_effective_amount = amt.effective_amount
                    self.db.prefix_db.effective_amount.stage_delete(
                        (name, prev_effective_amount, amt.tx_num, amt.position), (touched,)
                    )

            new_effective_amount = self._get_pending_effective_amount(name, touched)
            self.db.prefix_db.effective_amount.stage_put(
                (name, new_effective_amount, tx_num, position), (touched,)
            )
            if touched in self.claim_hash_to_txo or touched in self.removed_claim_hashes \
                    or touched in self.pending_support_amount_change:
                # exclude sending notifications for claims/supports that activated but
                # weren't added/spent in this block
                self.db.prefix_db.trending_notification.stage_put(
                    (height, touched), (prev_effective_amount, new_effective_amount)
                )

        for channel_hash, count in self.pending_channel_counts.items():
            if count != 0:
                channel_count_val = self.db.prefix_db.channel_count.get(channel_hash)
                channel_count = 0 if not channel_count_val else channel_count_val.count
                if channel_count_val is not None:
                    self.db.prefix_db.channel_count.stage_delete((channel_hash,), (channel_count,))
                self.db.prefix_db.channel_count.stage_put((channel_hash,), (channel_count + count,))

        self.touched_claim_hashes.update(
            {k for k in self.pending_reposted if k not in self.removed_claim_hashes}
        )
        self.touched_claim_hashes.update(
            {k for k, v in self.pending_channel_counts.items() if v != 0 and k not in self.removed_claim_hashes}
        )
        self.touched_claims_to_send_es.update(self.touched_claim_hashes)
        self.touched_claims_to_send_es.difference_update(self.removed_claim_hashes)
        self.removed_claims_to_send_es.update(self.removed_claim_hashes)

    def advance_block(self, block):
        height = self.height + 1
        # print("advance ", height)
        # Use local vars for speed in the loops
        tx_count = self.tx_count
        spend_utxo = self.spend_utxo
        add_utxo = self.add_utxo
        spend_claim_or_support_txo = self._spend_claim_or_support_txo
        add_claim_or_support = self._add_claim_or_support
        txs: List[Tuple[Tx, bytes]] = block.transactions
        txo_count = 0

        self.db.prefix_db.block_hash.stage_put(key_args=(height,), value_args=(self.coin.header_hash(block.header),))
        self.db.prefix_db.header.stage_put(key_args=(height,), value_args=(block.header,))
        self.db.prefix_db.block_txs.stage_put(key_args=(height,), value_args=([tx_hash for tx, tx_hash in txs],))

        for tx, tx_hash in txs:
            spent_claims = {}
            # clean up mempool, delete txs that were already in mempool/staged to be added
            # leave txs in mempool that werent in the block
            mempool_tx = self.db.prefix_db.mempool_tx.get_pending(tx_hash)
            if mempool_tx:
                self.db.prefix_db.mempool_tx.stage_delete((tx_hash,), mempool_tx)

            self.db.prefix_db.tx.stage_put(key_args=(tx_hash,), value_args=(tx.raw,))
            self.db.prefix_db.tx_num.stage_put(key_args=(tx_hash,), value_args=(tx_count,))
            self.db.prefix_db.tx_hash.stage_put(key_args=(tx_count,), value_args=(tx_hash,))

            # Spend the inputs
            for txin in tx.inputs:
                if txin.is_generation():
                    continue
                # spend utxo for address histories
                hashX = spend_utxo(txin.prev_hash, txin.prev_idx)
                if hashX:
                    if tx_count not in self.hashXs_by_tx[hashX]:
                        self.hashXs_by_tx[hashX].append(tx_count)
                # spend claim/support txo
                spend_claim_or_support_txo(height, txin, spent_claims)

            # Add the new UTXOs
            for nout, txout in enumerate(tx.outputs):
                txo_count += 1
                # Get the hashX.  Ignore unspendable outputs
                hashX = add_utxo(tx_hash, tx_count, nout, txout)
                if hashX:
                    # self._set_hashX_cache(hashX)
                    if tx_count not in self.hashXs_by_tx[hashX]:
                        self.hashXs_by_tx[hashX].append(tx_count)
                # add claim/support txo
                add_claim_or_support(
                    height, tx_hash, tx_count, nout, txout, spent_claims, tx.inputs[0]
                )

            # Handle abandoned claims
            abandoned_channels = {}
            # abandon the channels last to handle abandoned signed claims in the same tx,
            # see test_abandon_channel_and_claims_in_same_tx
            for abandoned_claim_hash, (tx_num, nout, normalized_name) in spent_claims.items():
                if normalized_name.startswith('@'):
                    abandoned_channels[abandoned_claim_hash] = (tx_num, nout, normalized_name)
                else:
                    # print(f"\tabandon {normalized_name} {abandoned_claim_hash.hex()} {tx_num} {nout}")
                    self._abandon_claim(abandoned_claim_hash, tx_num, nout, normalized_name)

            for abandoned_claim_hash, (tx_num, nout, normalized_name) in abandoned_channels.items():
                # print(f"\tabandon {normalized_name} {abandoned_claim_hash.hex()} {tx_num} {nout}")
                self._abandon_claim(abandoned_claim_hash, tx_num, nout, normalized_name)
            self.pending_transactions[tx_count] = tx_hash
            self.pending_transaction_num_mapping[tx_hash] = tx_count
            if self.env.cache_all_tx_hashes:
                self.db.total_transactions.append(tx_hash)
                self.db.tx_num_mapping[tx_hash] = tx_count
            tx_count += 1

        # handle expired claims
        self._expire_claims(height)

        # activate claims and process takeovers
        self._get_takeover_ops(height)

        # update effective amount and update sets of touched and deleted claims
        self._get_cumulative_update_ops(height)

        self.db.prefix_db.touched_hashX.stage_put((height,), (list(sorted(self.touched_hashXs)),))

        self.db.prefix_db.tx_count.stage_put(key_args=(height,), value_args=(tx_count,))

        for hashX, new_history in self.hashXs_by_tx.items():
            if not new_history:
                continue
            self.db.prefix_db.hashX_history.stage_put(key_args=(hashX, height), value_args=(new_history,))

        self.tx_count = tx_count
        self.db.tx_counts.append(self.tx_count)

        cached_max_reorg_depth = self.daemon.cached_height() - self.env.reorg_limit

        # if height >= cached_max_reorg_depth:
        self.db.prefix_db.touched_or_deleted.stage_put(
            key_args=(height,), value_args=(self.touched_claim_hashes, self.removed_claim_hashes)
        )

        self.height = height
        self.db.headers.append(block.header)
        self.tip = self.coin.header_hash(block.header)

        self.db.fs_height = self.height
        self.db.fs_tx_count = self.tx_count
        self.db.hist_flush_count += 1
        self.db.hist_unflushed_count = 0
        self.db.utxo_flush_count = self.db.hist_flush_count
        self.db.db_height = self.height
        self.db.db_tx_count = self.tx_count
        self.db.db_tip = self.tip
        self.db.last_flush_tx_count = self.db.fs_tx_count
        now = time.time()
        self.db.wall_time += now - self.db.last_flush
        self.db.last_flush = now
        self.db.write_db_state()

        # flush the changes
        save_undo = (self.daemon.cached_height() - self.height) <= self.env.reorg_limit

        if save_undo:
            self.db.prefix_db.commit(self.height, self.tip)
        else:
            self.db.prefix_db.unsafe_commit()
        self.clear_after_advance_or_reorg()
        self.db.assert_db_state()
        # print("*************\n")
        return txo_count

    def clear_after_advance_or_reorg(self):
        self.txo_to_claim.clear()
        self.claim_hash_to_txo.clear()
        self.support_txos_by_claim.clear()
        self.support_txo_to_claim.clear()
        self.removed_support_txos_by_name_by_claim.clear()
        self.abandoned_claims.clear()
        self.removed_active_support_amount_by_claim.clear()
        self.activated_support_amount_by_claim.clear()
        self.activated_claim_amount_by_name_and_hash.clear()
        self.activation_by_claim_by_name.clear()
        self.possible_future_claim_amount_by_name_and_hash.clear()
        self.possible_future_support_amounts_by_claim_hash.clear()
        self.possible_future_support_txos_by_claim_hash.clear()
        self.pending_channels.clear()
        self.amount_cache.clear()
        self.signatures_changed.clear()
        self.expired_claim_hashes.clear()
        self.doesnt_have_valid_signature.clear()
        self.claim_channels.clear()
        self.utxo_cache.clear()
        self.hashXs_by_tx.clear()
        self.removed_claim_hashes.clear()
        self.touched_claim_hashes.clear()
        self.pending_reposted.clear()
        self.pending_channel_counts.clear()
        self.updated_claims.clear()
        self.taken_over_names.clear()
        self.pending_transaction_num_mapping.clear()
        self.pending_transactions.clear()
        self.pending_support_amount_change.clear()
        self.touched_hashXs.clear()

    def backup_block(self):
        assert len(self.db.prefix_db._op_stack) == 0
        touched_and_deleted = self.db.prefix_db.touched_or_deleted.get(self.height)
        self.touched_claims_to_send_es.update(touched_and_deleted.touched_claims)
        self.removed_claims_to_send_es.difference_update(touched_and_deleted.touched_claims)
        self.removed_claims_to_send_es.update(touched_and_deleted.deleted_claims)

        # self.db.assert_flushed(self.flush_data())
        self.logger.info("backup block %i", self.height)
        # Check and update self.tip

        self.db.tx_counts.pop()
        reverted_block_hash = self.coin.header_hash(self.db.headers.pop())
        self.tip = self.coin.header_hash(self.db.headers[-1])
        if self.env.cache_all_tx_hashes:
            while len(self.db.total_transactions) > self.db.tx_counts[-1]:
                self.db.tx_num_mapping.pop(self.db.total_transactions.pop())
                self.tx_count -= 1
        else:
            self.tx_count = self.db.tx_counts[-1]
        self.height -= 1

        # self.touched can include other addresses which is
        # harmless, but remove None.
        self.touched_hashXs.discard(None)

        assert self.height < self.db.db_height
        assert not self.db.hist_unflushed

        start_time = time.time()
        tx_delta = self.tx_count - self.db.last_flush_tx_count
        ###
        self.db.fs_tx_count = self.tx_count
        # Truncate header_mc: header count is 1 more than the height.
        self.db.header_mc.truncate(self.height + 1)
        ###
        # Not certain this is needed, but it doesn't hurt
        self.db.hist_flush_count += 1

        while self.db.fs_height > self.height:
            self.db.fs_height -= 1
        self.db.utxo_flush_count = self.db.hist_flush_count
        self.db.db_height = self.height
        self.db.db_tx_count = self.tx_count
        self.db.db_tip = self.tip
        # Flush state last as it reads the wall time.
        now = time.time()
        self.db.wall_time += now - self.db.last_flush
        self.db.last_flush = now
        self.db.last_flush_tx_count = self.db.fs_tx_count

        # rollback
        self.db.prefix_db.rollback(self.height + 1, reverted_block_hash)
        self.db.es_sync_height = self.height
        self.db.write_db_state()
        self.db.prefix_db.unsafe_commit()

        self.clear_after_advance_or_reorg()
        self.db.assert_db_state()

        elapsed = self.db.last_flush - start_time
        self.logger.warning(f'backup flush #{self.db.hist_flush_count:,d} took {elapsed:.1f}s. '
                            f'Height {self.height:,d} txs: {self.tx_count:,d} ({tx_delta:+,d})')

    def add_utxo(self, tx_hash: bytes, tx_num: int, nout: int, txout: 'TxOutput') -> Optional[bytes]:
        hashX = self.coin.hashX_from_txo(txout)
        if hashX:
            self.touched_hashXs.add(hashX)
            self.utxo_cache[(tx_hash, nout)] = (hashX, txout.value)
            self.db.prefix_db.utxo.stage_put((hashX, tx_num, nout), (txout.value,))
            self.db.prefix_db.hashX_utxo.stage_put((tx_hash[:4], tx_num, nout), (hashX,))
            return hashX

    def get_pending_tx_num(self, tx_hash: bytes) -> int:
        if tx_hash in self.pending_transaction_num_mapping:
            return self.pending_transaction_num_mapping[tx_hash]
        else:
            return self.db.get_tx_num(tx_hash)

    def spend_utxo(self, tx_hash: bytes, nout: int):
        hashX, amount = self.utxo_cache.pop((tx_hash, nout), (None, None))
        txin_num = self.get_pending_tx_num(tx_hash)
        if not hashX:
            hashX_value = self.db.prefix_db.hashX_utxo.get(tx_hash[:4], txin_num, nout)
            if not hashX_value:
                return
            hashX = hashX_value.hashX
            utxo_value = self.db.prefix_db.utxo.get(hashX, txin_num, nout)
            if not utxo_value:
                self.logger.warning(
                    "%s:%s is not found in UTXO db for %s", hash_to_hex_str(tx_hash), nout, hash_to_hex_str(hashX)
                )
                raise ChainError(
                    f"{hash_to_hex_str(tx_hash)}:{nout} is not found in UTXO db for {hash_to_hex_str(hashX)}"
                )
            self.touched_hashXs.add(hashX)
            self.db.prefix_db.hashX_utxo.stage_delete((tx_hash[:4], txin_num, nout), hashX_value)
            self.db.prefix_db.utxo.stage_delete((hashX, txin_num, nout), utxo_value)
            return hashX
        elif amount is not None:
            self.db.prefix_db.hashX_utxo.stage_delete((tx_hash[:4], txin_num, nout), (hashX,))
            self.db.prefix_db.utxo.stage_delete((hashX, txin_num, nout), (amount,))
            self.touched_hashXs.add(hashX)
            return hashX

    async def process_blocks_and_mempool_forever(self):
        """Loop forever processing blocks as they arrive."""
        try:
            while not self._stopping:
                if self.height == self.daemon.cached_height():
                    if not self._caught_up_event.is_set():
                        await self._first_caught_up()
                        self._caught_up_event.set()
                try:
                    await asyncio.wait_for(self.blocks_event.wait(), self.wait_for_blocks_duration)
                except asyncio.TimeoutError:
                    pass
                self.blocks_event.clear()
                blocks = self.prefetcher.get_prefetched_blocks()
                if self._stopping:
                    break
                if not blocks:
                    try:
                        await self.refresh_mempool()
                    except asyncio.CancelledError:
                        raise
                    except Exception:
                        self.logger.exception("error while updating mempool txs")
                        raise
                else:
                    try:
                        await self.check_and_advance_blocks(blocks)
                    except asyncio.CancelledError:
                        raise
                    except Exception:
                        self.logger.exception("error while processing txs")
                        raise
        finally:
            self._ready_to_stop.set()

    async def _first_caught_up(self):
        self.logger.info(f'caught up to height {self.height}')
        # Flush everything but with first_sync->False state.
        first_sync = self.db.first_sync
        self.db.first_sync = False

        def flush():
            assert len(self.db.prefix_db._op_stack) == 0
            self.db.write_db_state()
            self.db.prefix_db.unsafe_commit()
            self.db.assert_db_state()

        await self.run_in_thread_with_lock(flush)

        if first_sync:
            self.logger.info(f'{__version__} synced to '
                             f'height {self.height:,d}, halting here.')
            self.shutdown_event.set()

    async def open(self):
        self.db.open_db()
        self.height = self.db.db_height
        self.tip = self.db.db_tip
        self.tx_count = self.db.db_tx_count
        await self.db.initialize_caches()

    async def fetch_and_process_blocks(self, caught_up_event):
        """Fetch, process and index blocks from the daemon.

        Sets caught_up_event when first caught up.  Flushes to disk
        and shuts down cleanly if cancelled.

        This is mainly because if, during initial sync ElectrumX is
        asked to shut down when a large number of blocks have been
        processed but not written to disk, it should write those to
        disk before exiting, as otherwise a significant amount of work
        could be lost.
        """

        await self.open()

        self._caught_up_event = caught_up_event
        try:
            await asyncio.wait([
                self.prefetcher.main_loop(self.height),
                self.process_blocks_and_mempool_forever()
            ])
        except asyncio.CancelledError:
            raise
        except:
            self.logger.exception("Block processing failed!")
            raise
        finally:
            # Shut down block processing
            self.logger.info('closing the DB for a clean shutdown...')
            self._chain_executor.shutdown(wait=True)
            self.db.close()

    async def start(self):
        self._stopping = False
        env = self.env
        self.logger.info(f'software version: {__version__}')
        self.logger.info(f'event loop policy: {env.loop_policy}')
        self.logger.info(f'reorg limit is {env.reorg_limit:,d} blocks')

        await self.daemon.height()

        def _start_cancellable(run, *args):
            _flag = asyncio.Event()
            self.cancellable_tasks.append(asyncio.ensure_future(run(*args, _flag)))
            return _flag.wait()

        await _start_cancellable(self.fetch_and_process_blocks)

    async def stop(self):
        self._stopping = True
        await self._ready_to_stop.wait()
        for task in reversed(self.cancellable_tasks):
            task.cancel()
        await asyncio.wait(self.cancellable_tasks)
        self.shutdown_event.set()
        await self.daemon.close()

    def run(self):
        loop = asyncio.get_event_loop()
        loop.set_default_executor(self._chain_executor)

        def __exit():
            raise SystemExit()
        try:
            loop.add_signal_handler(signal.SIGINT, __exit)
            loop.add_signal_handler(signal.SIGTERM, __exit)
            loop.run_until_complete(self.start())
            loop.run_until_complete(self.shutdown_event.wait())
        except (SystemExit, KeyboardInterrupt):
            pass
        finally:
            loop.run_until_complete(self.stop())