Exemple #1
0
    def test_accumulated_weight_indirect_block(self):
        """ All new blocks belong to case (i).
        """
        self.assertEqual(len(self.genesis_blocks), 1)
        manager = self.create_peer('testnet', tx_storage=self.tx_storage)

        # Mine 3 blocks in a row with no transaction but the genesis
        blocks = add_new_blocks(manager, 3, advance_clock=15)
        add_blocks_unlock_reward(manager)

        # Add some transactions between blocks
        tx_list = add_new_transactions(manager, 20, advance_clock=15)

        # Mine more 2 blocks in a row with no transactions between them
        blocks = add_new_blocks(manager, 2, weight=8)

        tx0 = tx_list[0]
        for block in blocks:
            self.assertNotIn(tx0.hash, block.parents)

        # All transactions and blocks should be verifying tx_list[0] directly or
        # indirectly.
        expected = 0
        for tx in tx_list:
            expected = sum_weights(expected, tx.weight)
        for block in blocks:
            expected = sum_weights(expected, block.weight)

        meta = tx0.update_accumulated_weight()
        self.assertAlmostEqual(meta.accumulated_weight, expected)
Exemple #2
0
    def calculate_share_weight(self) -> float:
        """
        Calculate the target share weight for the current miner.
        Uses last jobs statistics to aim for an share time equals
        `StratumProtocol.AVERAGE_JOB_TIME`

        :return: job weight for miner to take AVERAGE_JOB_TIME to solve it.
        :rtype: float
        """
        if len(self.job_ids) <= 1:
            return settings.MIN_BLOCK_WEIGHT

        mn = self.jobs[self.job_ids[0]].tx.timestamp
        mx = self.jobs[self.job_ids[-1]].tx.timestamp
        dt = max(mx - mn, 1)

        acc_weight = 0.0
        for job in self.jobs.values():
            if job.submitted is not None:
                acc_weight = sum_weights(acc_weight, job.weight)

        hash_rate = acc_weight - log(dt, 2)
        self.estimated_hash_rate = hash_rate
        share_weight = hash_rate + log(self.AVERAGE_JOB_TIME, 2)
        share_weight = max(share_weight, settings.MIN_SHARE_WEIGHT)
        return share_weight
    def test_regular_block_template(self):
        manager = self.create_peer('testnet', tx_storage=self.tx_storage)

        # add 100 blocks
        blocks = add_new_blocks(manager, 100, advance_clock=15)

        block_templates = manager.get_block_templates()
        self.assertEqual(len(block_templates), 1)
        self.assertEqual(
            block_templates[0],
            BlockTemplate(
                versions={0, 3},
                reward=settings.INITIAL_TOKEN_UNITS_PER_BLOCK * 100,
                weight=1.0,
                timestamp_now=int(manager.reactor.seconds()),
                timestamp_min=blocks[-1].timestamp + 1,
                timestamp_max=blocks[-1].timestamp +
                settings.MAX_DISTANCE_BETWEEN_BLOCKS - 1,
                # parents=[blocks[-1].hash, self.genesis_txs[-1].hash, self.genesis_txs[-2].hash],
                parents=block_templates[0].parents,
                parents_any=[],
                height=101,  # genesis is 0
                score=sum_weights(blocks[-1].get_metadata().score, 1.0),
            ))

        self.assertConsensusValid(manager)
Exemple #4
0
    def logsum(self, other: 'Weight') -> 'Weight':
        """ Make a "logarithmic sum" on base 2.

        That is `x.logsum(y)` is equivalent to `log2(2**x + 2**y)`, although there are some precision differences.

        Currently is just a proxy to `hathor.transaction.sum_weights`.
        """
        from hathor.transaction import sum_weights
        return Weight(sum_weights(self, other))
Exemple #5
0
    def update_voided_info(self, tx: Transaction) -> None:
        """ This method should be called only once when the transactions is added to the DAG.
        """
        assert tx.hash is not None
        assert tx.storage is not None

        voided_by: Set[bytes] = set()

        # Union of voided_by of parents
        for parent in tx.get_parents():
            parent_meta = parent.get_metadata()
            if parent_meta.voided_by:
                voided_by.update(parent_meta.voided_by)

        # Union of voided_by of inputs
        for txin in tx.inputs:
            spent_tx = tx.storage.get_transaction(txin.tx_id)
            spent_meta = spent_tx.get_metadata()
            if spent_meta.voided_by:
                voided_by.update(spent_meta.voided_by)

        # Update accumulated weight of the transactions voiding us.
        assert tx.hash not in voided_by
        for h in voided_by:
            tx2 = tx.storage.get_transaction(h)
            tx2_meta = tx2.get_metadata()
            tx2_meta.accumulated_weight = sum_weights(
                tx2_meta.accumulated_weight, tx.weight)
            assert tx2.storage is not None
            tx2.storage.save_transaction(tx2, only_metadata=True)

        # Then, we add ourselves.
        meta = tx.get_metadata()
        assert not meta.voided_by or meta.voided_by == {tx.hash}
        assert meta.accumulated_weight == tx.weight
        if meta.conflict_with:
            voided_by.add(tx.hash)

        if voided_by:
            meta.voided_by = voided_by.copy()
            tx.storage.save_transaction(tx, only_metadata=True)
            tx.storage._del_from_cache(tx)  # XXX: accessing private method

        # Check conflicts of the transactions voiding us.
        for h in voided_by:
            if h == tx.hash:
                continue
            conflict_tx = tx.storage.get_transaction(h)
            if not conflict_tx.is_block:
                assert isinstance(conflict_tx, Transaction)
                self.check_conflicts(conflict_tx)

        # Finally, check our conflicts.
        meta = tx.get_metadata()
        if meta.voided_by == {tx.hash}:
            self.check_conflicts(tx)
Exemple #6
0
    def should_mine_block(self) -> bool:
        """
        Calculates whether the next mining job should be an block or not,
        based on the recent history of mined jobs.

        :return: whether the next mining job should be an block or not.
        :rtype: bool
        """
        if len(self.factory.tx_queue) == 0:
            self.log.debug('empty queue')
            return True

        if not self.mine_txs:
            return True

        # Asure miners won't spend more time on tx jobs than on block jobs
        # Prevents against DoS from tx with huge weight
        tx_acc_weight = 0.0
        block_acc_weight = 0.0

        # Asure miners won't mine more tx jobs than block jobs
        # Prevents against DoS from lots of tx with small weight
        tx_count = 0
        block_count = 0

        for job in self.jobs.values():
            if job.submitted is None:
                continue

            if isinstance(job.tx, Block):
                tx_count += 1
                block_acc_weight = sum_weights(block_acc_weight, job.weight)
            else:
                block_count += 1
                tx_acc_weight = sum_weights(tx_acc_weight, job.weight)

        return block_acc_weight <= tx_acc_weight and tx_count <= block_count
Exemple #7
0
    def test_block_template_after_genesis(self):
        manager = self.create_peer('testnet', tx_storage=self.tx_storage)

        block_templates = manager.get_block_templates()
        self.assertEqual(len(block_templates), 1)
        self.assertEqual(block_templates[0], BlockTemplate(
            versions={0, 3},
            reward=settings.INITIAL_TOKEN_UNITS_PER_BLOCK * 100,
            weight=1.0,
            timestamp_now=int(manager.reactor.seconds()),
            timestamp_min=settings.GENESIS_TIMESTAMP + 3,
            timestamp_max=0xffffffff,  # no limit for next block after genesis
            # parents=[tx.hash for tx in self.genesis_blocks + self.genesis_txs],
            parents=block_templates[0].parents,
            parents_any=[],
            height=1,  # genesis is 0
            score=sum_weights(self.genesis_blocks[0].weight, 1.0),
        ))
    def test_single_chain(self):
        """ All new blocks belong to case (i).
        """
        self.assertEqual(len(self.genesis_blocks), 1)
        manager = self.create_peer('testnet', tx_storage=self.tx_storage)

        # The initial score is the sum of the genesis
        score = self.genesis_blocks[0].weight
        for tx in self.genesis_txs:
            score = sum_weights(score, tx.weight)

        # Mine 100 blocks in a row with no transaction but the genesis
        blocks = add_new_blocks(manager, 100, advance_clock=15)
        for i, block in enumerate(blocks):
            meta = block.get_metadata(force_reload=True)
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)

        # Add some transactions between blocks
        txs = add_new_transactions(manager, 30, advance_clock=15)
        for tx in txs:
            score = sum_weights(score, tx.weight)

        # Mine 50 more blocks in a row with no transactions between them
        blocks = add_new_blocks(manager, 50)
        for i, block in enumerate(blocks):
            meta = block.get_metadata()
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)
            self.assertAlmostEqual(
                manager.consensus_algorithm.block_algorithm.calculate_score(
                    block), meta.score)

        # Mine 15 more blocks with 10 transactions between each block
        for _ in range(15):
            txs = add_new_transactions(manager, 10, advance_clock=15)
            for tx in txs:
                score = sum_weights(score, tx.weight)

            blocks = add_new_blocks(manager, 1)
            for i, block in enumerate(blocks):
                meta = block.get_metadata()
                score = sum_weights(score, block.weight)
                self.assertAlmostEqual(score, meta.score)
                self.assertAlmostEqual(
                    manager.consensus_algorithm.block_algorithm.
                    calculate_score(block), meta.score)

        self.assertConsensusValid(manager)
Exemple #9
0
    def calculate_block_difficulty(self, block: Block) -> float:
        """ Calculate block difficulty according to the ascendents of `block`, aka DAA/difficulty adjustment algorithm

        The algorithm used is described in [RFC 22](https://gitlab.com/HathorNetwork/rfcs/merge_requests/22).

        The new difficulty must not be less than `self.min_block_weight`.
        """
        # In test mode we don't validate the block difficulty
        if self.test_mode & TestMode.TEST_BLOCK_WEIGHT:
            return 1.0

        if block.is_genesis:
            return self.min_block_weight

        root = block
        parent = root.get_block_parent()
        N = min(2 * settings.BLOCK_DIFFICULTY_N_BLOCKS,
                parent.get_metadata().height - 1)
        K = N // 2
        T = self.avg_time_between_blocks
        S = 5
        if N < 10:
            return self.min_block_weight

        blocks: List[Block] = []
        while len(blocks) < N + 1:
            root = root.get_block_parent()
            assert isinstance(root, Block)
            assert root is not None
            blocks.append(root)

        # TODO: revise if this assertion can be safely removed
        assert blocks == sorted(blocks, key=lambda tx: -tx.timestamp)
        blocks = list(reversed(blocks))

        assert len(blocks) == N + 1
        solvetimes, weights = zip(
            *((block.timestamp - prev_block.timestamp, block.weight)
              for prev_block, block in hathor.util.iwindows(blocks, 2)))
        assert len(solvetimes) == len(
            weights
        ) == N, f'got {len(solvetimes)}, {len(weights)} expected {N}'

        sum_solvetimes = 0.0
        logsum_weights = 0.0

        prefix_sum_solvetimes = [0]
        for x in solvetimes:
            prefix_sum_solvetimes.append(prefix_sum_solvetimes[-1] + x)

        # Loop through N most recent blocks. N is most recently solved block.
        for i in range(K, N):
            solvetime = solvetimes[i]
            weight = weights[i]
            x = (prefix_sum_solvetimes[i + 1] -
                 prefix_sum_solvetimes[i - K]) / K
            ki = K * (x - T)**2 / (2 * T * T)
            ki = max(1, ki / S)
            sum_solvetimes += ki * solvetime
            logsum_weights = sum_weights(logsum_weights, log(ki, 2) + weight)

        weight = logsum_weights - log(sum_solvetimes, 2) + log(T, 2)

        # Apply weight decay
        weight -= self.get_weight_decay_amount(block.timestamp -
                                               parent.timestamp)

        # Apply minimum weight
        if weight < self.min_block_weight:
            weight = self.min_block_weight

        return weight
Exemple #10
0
 def _make_block_template(self,
                          parent_block: Block,
                          parent_txs: 'ParentTxs',
                          current_timestamp: int,
                          with_weight_decay: bool = False) -> BlockTemplate:
     """ Further implementation of making block template, used by make_block_template and make_custom_block_template
     """
     assert parent_block.hash is not None
     # the absolute minimum would be the previous timestamp + 1
     timestamp_abs_min = parent_block.timestamp + 1
     # and absolute maximum limited by max time between blocks
     if not parent_block.is_genesis:
         timestamp_abs_max = parent_block.timestamp + settings.MAX_DISTANCE_BETWEEN_BLOCKS - 1
     else:
         timestamp_abs_max = 0xffffffff
     assert timestamp_abs_max > timestamp_abs_min
     # actual minimum depends on the timestamps of the parent txs
     # it has to be at least the max timestamp of parents + 1
     timestamp_min = max(timestamp_abs_min, parent_txs.max_timestamp + 1)
     assert timestamp_min <= timestamp_abs_max
     # when we have weight decay, the max timestamp will be when the next decay happens
     if with_weight_decay and settings.WEIGHT_DECAY_ENABLED:
         # we either have passed the first decay or not, the range will vary depending on that
         if timestamp_min > timestamp_abs_min + settings.WEIGHT_DECAY_ACTIVATE_DISTANCE:
             timestamp_max_decay = timestamp_min + settings.WEIGHT_DECAY_WINDOW_SIZE
         else:
             timestamp_max_decay = timestamp_abs_min + settings.WEIGHT_DECAY_ACTIVATE_DISTANCE
         timestamp_max = min(timestamp_abs_max, timestamp_max_decay)
     else:
         timestamp_max = timestamp_abs_max
     timestamp = min(max(current_timestamp, timestamp_min), timestamp_max)
     weight = daa.calculate_next_weight(parent_block, timestamp)
     parent_block_metadata = parent_block.get_metadata()
     height = parent_block_metadata.height + 1
     parents = [parent_block.hash] + parent_txs.must_include
     parents_any = parent_txs.can_include
     # simplify representation when you only have one to choose from
     if len(parents) + len(parents_any) == 3:
         parents.extend(sorted(parents_any))
         parents_any = []
     assert len(parents) + len(
         parents_any) >= 3, 'There should be enough parents to choose from'
     assert 1 <= len(parents) <= 3, 'Impossible number of parents'
     if __debug__ and len(parents) == 3:
         assert len(
             parents_any
         ) == 0, 'Extra parents to choose from that cannot be chosen'
     return BlockTemplate(
         versions={
             TxVersion.REGULAR_BLOCK.value,
             TxVersion.MERGE_MINED_BLOCK.value
         },
         reward=daa.get_tokens_issued_per_block(height),
         weight=weight,
         timestamp_now=current_timestamp,
         timestamp_min=timestamp_min,
         timestamp_max=timestamp_max,
         parents=parents,
         parents_any=parents_any,
         height=height,
         score=sum_weights(parent_block_metadata.score, weight),
     )
Exemple #11
0
def calculate_next_weight(parent_block: 'Block', timestamp: int) -> float:
    """ Calculate the next block weight, aka DAA/difficulty adjustment algorithm.

    The algorithm used is described in [RFC 22](https://gitlab.com/HathorNetwork/rfcs/merge_requests/22).

    The weight must not be less than `MIN_BLOCK_WEIGHT`.
    """
    if TEST_MODE & TestMode.TEST_BLOCK_WEIGHT:
        return 1.0

    from hathor.transaction import sum_weights

    root = parent_block
    N = min(2 * settings.BLOCK_DIFFICULTY_N_BLOCKS,
            parent_block.get_metadata().height - 1)
    K = N // 2
    T = AVG_TIME_BETWEEN_BLOCKS
    S = 5
    if N < 10:
        return MIN_BLOCK_WEIGHT

    blocks: List['Block'] = []
    while len(blocks) < N + 1:
        blocks.append(root)
        root = root.get_block_parent()
        assert root is not None

    # TODO: revise if this assertion can be safely removed
    assert blocks == sorted(blocks, key=lambda tx: -tx.timestamp)
    blocks = list(reversed(blocks))

    assert len(blocks) == N + 1
    solvetimes, weights = zip(*((block.timestamp - prev_block.timestamp,
                                 block.weight)
                                for prev_block, block in iwindows(blocks, 2)))
    assert len(solvetimes) == len(
        weights) == N, f'got {len(solvetimes)}, {len(weights)} expected {N}'

    sum_solvetimes = 0.0
    logsum_weights = 0.0

    prefix_sum_solvetimes = [0]
    for st in solvetimes:
        prefix_sum_solvetimes.append(prefix_sum_solvetimes[-1] + st)

    # Loop through N most recent blocks. N is most recently solved block.
    for i in range(K, N):
        solvetime = solvetimes[i]
        weight = weights[i]
        x = (prefix_sum_solvetimes[i + 1] - prefix_sum_solvetimes[i - K]) / K
        ki = K * (x - T)**2 / (2 * T * T)
        ki = max(1, ki / S)
        sum_solvetimes += ki * solvetime
        logsum_weights = sum_weights(logsum_weights, log(ki, 2) + weight)

    weight = logsum_weights - log(sum_solvetimes, 2) + log(T, 2)

    # Apply weight decay
    weight -= get_weight_decay_amount(timestamp - parent_block.timestamp)

    # Apply minimum weight
    if weight < MIN_BLOCK_WEIGHT:
        weight = MIN_BLOCK_WEIGHT

    return weight
Exemple #12
0
    def update_voided_info(self, tx: Transaction) -> None:
        """ This method should be called only once when the transactions is added to the DAG.
        """
        assert tx.hash is not None
        assert tx.storage is not None

        voided_by: Set[bytes] = set()

        # Union of voided_by of parents
        for parent in tx.get_parents():
            parent_meta = parent.get_metadata()
            if parent_meta.voided_by:
                voided_by.update(
                    self.consensus.filter_out_soft_voided_entries(
                        parent, parent_meta.voided_by))
        assert settings.SOFT_VOIDED_ID not in voided_by
        assert not (self.soft_voided_tx_ids & voided_by)

        # Union of voided_by of inputs
        for txin in tx.inputs:
            spent_tx = tx.storage.get_transaction(txin.tx_id)
            spent_meta = spent_tx.get_metadata()
            if spent_meta.voided_by:
                voided_by.update(spent_meta.voided_by)
                voided_by.discard(settings.SOFT_VOIDED_ID)
        assert settings.SOFT_VOIDED_ID not in voided_by

        # Update accumulated weight of the transactions voiding us.
        assert tx.hash not in voided_by
        for h in voided_by:
            if h == settings.SOFT_VOIDED_ID:
                continue
            tx2 = tx.storage.get_transaction(h)
            tx2_meta = tx2.get_metadata()
            tx2_meta.accumulated_weight = sum_weights(
                tx2_meta.accumulated_weight, tx.weight)
            assert tx2.storage is not None
            tx2.storage.save_transaction(tx2, only_metadata=True)

        # Then, we add ourselves.
        meta = tx.get_metadata()
        assert not meta.voided_by or meta.voided_by == {tx.hash}
        assert meta.accumulated_weight == tx.weight
        if tx.hash in self.soft_voided_tx_ids:
            voided_by.add(settings.SOFT_VOIDED_ID)
            voided_by.add(tx.hash)
        if meta.conflict_with:
            voided_by.add(tx.hash)

        # We must save before marking conflicts as voided because
        # the conflicting tx might affect this tx's voided_by metadata.
        if voided_by:
            meta.voided_by = voided_by.copy()
            tx.storage.save_transaction(tx, only_metadata=True)
            tx.storage.del_from_indexes(tx)

        # Check conflicts of the transactions voiding us.
        for h in voided_by:
            if h == settings.SOFT_VOIDED_ID:
                continue
            if h == tx.hash:
                continue
            tx2 = tx.storage.get_transaction(h)
            if not tx2.is_block:
                assert isinstance(tx2, Transaction)
                self.check_conflicts(tx2)

        # Mark voided conflicts as voided.
        for h in meta.conflict_with or []:
            conflict_tx = cast(Transaction, tx.storage.get_transaction(h))
            conflict_tx_meta = conflict_tx.get_metadata()
            if conflict_tx_meta.voided_by:
                self.mark_as_voided(conflict_tx)

        # Finally, check our conflicts.
        meta = tx.get_metadata()
        if meta.voided_by == {tx.hash}:
            self.check_conflicts(tx)

        # Assert the final state is valid.
        self.assert_valid_consensus(tx)
Exemple #13
0
    def _score_block_dfs(self, block: BaseTransaction, used: Set[bytes],
                         mark_as_best_chain: bool,
                         newest_timestamp: int) -> float:
        """ Internal method to run a DFS. It is used by `calculate_score()`.
        """
        assert block.storage is not None
        assert block.hash is not None
        assert block.is_block

        storage = block.storage

        from hathor.transaction import Block
        score = block.weight
        for parent in block.get_parents():
            if parent.is_block:
                assert isinstance(parent, Block)
                if parent.timestamp <= newest_timestamp:
                    meta = parent.get_metadata()
                    x = meta.score
                else:
                    x = self._score_block_dfs(parent, used, mark_as_best_chain,
                                              newest_timestamp)
                score = sum_weights(score, x)

            else:
                from hathor.transaction.storage.traversal import BFSWalk
                bfs = BFSWalk(storage,
                              is_dag_verifications=True,
                              is_left_to_right=False)
                for tx in bfs.run(parent, skip_root=False):
                    assert tx.hash is not None
                    assert not tx.is_block

                    if tx.hash in used:
                        bfs.skip_neighbors(tx)
                        continue
                    used.add(tx.hash)

                    meta = tx.get_metadata()
                    if meta.first_block:
                        first_block = storage.get_transaction(meta.first_block)
                        if first_block.timestamp <= newest_timestamp:
                            bfs.skip_neighbors(tx)
                            continue

                    if mark_as_best_chain:
                        assert meta.first_block is None
                        meta.first_block = block.hash
                        storage.save_transaction(tx, only_metadata=True)

                    score = sum_weights(score, tx.weight)

        # Always save the score when it is calculated.
        meta = block.get_metadata()
        if not meta.score:
            meta.score = score
            storage.save_transaction(block, only_metadata=True)
        else:
            # The score of a block is immutable since the sub-DAG behind it is immutable as well.
            # Thus, if we have already calculated it, we just check the consistency of the calculation.
            # Unfortunately we may have to calculate it more than once when a new block arrives in a side
            # side because the `first_block` points only to the best chain.
            assert abs(meta.score - score) < 1e-10, \
                   'hash={} meta.score={} score={}'.format(block.hash.hex(), meta.score, score)

        return score
Exemple #14
0
    def update_voided_info(self, block: Block) -> None:
        """ This method is called only once when a new block arrives.

        The blockchain part of the DAG is a tree with the genesis block as the root.
        I'll say the a block A is connected to a block B when A verifies B, i.e., B is a parent of A.

        A chain is a sequence of connected blocks starting in a leaf and ending in the root, i.e., any path from a leaf
        to the root is a chain. Given a chain, its head is a leaf in the tree, and its tail is the sub-chain without
        the head.

        The best chain is a chain that has the highest score of all chains.

        The score of a block is calculated as the sum of the weights of all transactions and blocks both direcly and
        indirectly verified by the block. The score of a chain is defined as the score of its head.

        The side chains are the chains whose scores are smaller than the best chain's.
        The head of the side chains are always voided blocks.

        There are two possible states for the block chain:
        (i)  It has a single best chain, i.e., one chain has the highest score
        (ii) It has multiple best chains, i.e., two or more chains have the same score (and this score is the highest
             among the chains)

        When there are multiple best chains, I'll call them best chain candidates.

        The arrived block can be connected in four possible ways:
        (i)   To the head of a best chain
        (ii)  To the tail of the best chain
        (iii) To the head of a side chain
        (iv)  To the tail of a side chain

        Thus, there are eight cases to be handled when a new block arrives, which are:
        (i)    Single best chain, connected to the head of the best chain
        (ii)   Single best chain, connected to the tail of the best chain
        (iii)  Single best chain, connected to the head of a side chain
        (iv)   Single best chain, connected to the tail of a side chain
        (v)    Multiple best chains, connected to the head of a best chain
        (vi)   Multiple best chains, connected to the tail of a best chain
        (vii)  Multiple best chains, connected to the head of a side chain
        (viii) Multiple best chains, connected to the tail of a side chain

        Case (i) is trivial because the single best chain will remain as the best chain. So, just calculate the new
        score and that's it.

        Case (v) is also trivial. As there are multiple best chains and the new block is connected to the head of one
        of them, this will be the new winner. So, the blockchain state will change to a single best chain again.

        In the other cases, we must calculate the score and compare with the best score.

        When there are multiple best chains, all their heads will be voided.
        """
        assert block.weight > 0, 'This algorithm assumes that block\'s weight is always greater than zero'
        if not block.parents:
            assert block.is_genesis is True
            self.update_score_and_mark_as_the_best_chain(block)
            return

        assert block.storage is not None
        assert block.hash is not None

        storage = block.storage
        assert storage.indexes is not None

        # Union of voided_by of parents
        voided_by: Set[bytes] = self.union_voided_by_from_parents(block)

        # Update accumulated weight of the transactions voiding us.
        assert block.hash not in voided_by
        for h in voided_by:
            tx = storage.get_transaction(h)
            tx_meta = tx.get_metadata()
            tx_meta.accumulated_weight = sum_weights(
                tx_meta.accumulated_weight, block.weight)
            storage.save_transaction(tx, only_metadata=True)

        # Check conflicts of the transactions voiding us.
        for h in voided_by:
            tx = storage.get_transaction(h)
            if not tx.is_block:
                assert isinstance(tx, Transaction)
                self.consensus.transaction_algorithm.check_conflicts(tx)

        parent = block.get_block_parent()
        parent_meta = parent.get_metadata()
        assert block.hash in parent_meta.children

        # This method is called after the metadata of the parent is updated.
        # So, if the parent has only one child, it must be the current block.
        is_connected_to_the_head = bool(len(parent_meta.children) == 1)
        is_connected_to_the_best_chain = bool(not parent_meta.voided_by)

        if is_connected_to_the_head and is_connected_to_the_best_chain:
            # Case (i): Single best chain, connected to the head of the best chain
            self.update_score_and_mark_as_the_best_chain_if_possible(block)
            # As `update_score_and_mark_as_the_best_chain_if_possible` may affect `voided_by`,
            # we need to check that block is not voided.
            meta = block.get_metadata()
            if not meta.voided_by:
                storage.indexes.height.add_new(meta.height, block.hash,
                                               block.timestamp)
                storage.update_best_block_tips_cache([block.hash])
            # The following assert must be true, but it is commented out for performance reasons.
            if settings.SLOW_ASSERTS:
                assert len(storage.get_best_block_tips(skip_cache=True)) == 1
        else:
            # Resolve all other cases, but (i).
            log = self.log.new(block=block.hash_hex)
            log.debug(
                'this block is not the head of the bestchain',
                is_connected_to_the_head=is_connected_to_the_head,
                is_connected_to_the_best_chain=is_connected_to_the_best_chain)

            # First, void this block.
            self.mark_as_voided(block, skip_remove_first_block_markers=True)

            # Get the score of the best chains.
            # We need to void this block first, because otherwise it would always be one of the heads.
            heads = [
                cast(Block, storage.get_transaction(h))
                for h in storage.get_best_block_tips()
            ]
            best_score = None
            for head in heads:
                head_meta = head.get_metadata(force_reload=True)
                if best_score is None:
                    best_score = head_meta.score
                else:
                    # All heads must have the same score.
                    assert abs(best_score - head_meta.score) < 1e-10
            assert isinstance(best_score, (int, float))

            # Calculate the score.
            # We cannot calculate score before getting the heads.
            score = self.calculate_score(block)

            # Finally, check who the winner is.
            if score <= best_score - settings.WEIGHT_TOL:
                # Just update voided_by from parents.
                self.update_voided_by_from_parents(block)

            else:
                # Either eveyone has the same score or there is a winner.

                valid_heads = []
                for head in heads:
                    meta = head.get_metadata()
                    if not meta.voided_by:
                        valid_heads.append(head)

                # We must have at most one valid head.
                # Either we have a single best chain or all chains have already been voided.
                assert len(
                    valid_heads
                ) <= 1, 'We must never have more than one valid head'

                # Add voided_by to all heads.
                self.add_voided_by_to_multiple_chains(block, heads)

                if score >= best_score + settings.WEIGHT_TOL:
                    # We have a new winner candidate.
                    self.update_score_and_mark_as_the_best_chain_if_possible(
                        block)
                    # As `update_score_and_mark_as_the_best_chain_if_possible` may affect `voided_by`,
                    # we need to check that block is not voided.
                    meta = block.get_metadata()
                    if not meta.voided_by:
                        self.log.debug('index new winner block',
                                       height=meta.height,
                                       block=block.hash_hex)
                        # We update the height cache index with the new winner chain
                        storage.indexes.height.update_new_chain(
                            meta.height, block)
                        storage.update_best_block_tips_cache([block.hash])
                else:
                    storage.update_best_block_tips_cache(
                        [not_none(blk.hash) for blk in heads])
    def test_single_fork_not_best(self):
        """ New blocks belong to cases (i), (ii), (iii), and (iv).
        The best chain never changes. All other chains are side chains.
        """
        self.assertEqual(len(self.genesis_blocks), 1)
        manager = self.create_peer('testnet', tx_storage=self.tx_storage)

        # The initial score is the sum of the genesis
        score = self.genesis_blocks[0].weight
        for tx in self.genesis_txs:
            score = sum_weights(score, tx.weight)

        # Mine 30 blocks in a row with no transactions
        blocks = add_new_blocks(manager, 30, advance_clock=15)
        for i, block in enumerate(blocks):
            meta = block.get_metadata()
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)

        # Add some transactions between blocks
        txs = add_new_transactions(manager, 5, advance_clock=15)
        for tx in txs:
            score = sum_weights(score, tx.weight)

        # Mine 1 blocks
        blocks = add_new_blocks(manager, 1, advance_clock=15)
        for i, block in enumerate(blocks):
            meta = block.get_metadata()
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)

        # Generate a block which will be a fork in the middle of the chain
        # Change the order of the transactions to change the hash
        fork_block1 = manager.generate_mining_block()
        fork_block1.parents = [fork_block1.parents[0]
                               ] + fork_block1.parents[:0:-1]
        fork_block1.resolve()
        fork_block1.verify()

        # Mine 8 blocks in a row
        blocks = add_new_blocks(manager, 8, advance_clock=15)
        for i, block in enumerate(blocks):
            meta = block.get_metadata()
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)

        # Fork block must have the same parents as blocks[0] as well as the same score
        self.assertEqual(set(blocks[0].parents), set(fork_block1.parents))

        # Propagate fork block.
        # This block belongs to case (ii).
        self.assertTrue(manager.propagate_tx(fork_block1))
        fork_meta1 = fork_block1.get_metadata()
        self.assertEqual(fork_meta1.voided_by, {fork_block1.hash})

        # Add some transactions between blocks
        txs = add_new_transactions(manager, 5, advance_clock=15)
        for tx in txs:
            score = sum_weights(score, tx.weight)

        # Mine 5 blocks in a row
        # These blocks belong to case (i).
        blocks = add_new_blocks(manager, 5, advance_clock=15)
        for i, block in enumerate(blocks):
            meta = block.get_metadata()
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)

        # Add some transactions between blocks
        txs = add_new_transactions(manager, 2, advance_clock=15)
        for tx in txs:
            score = sum_weights(score, tx.weight)

        # Propagate a block connected to the voided chain
        # These blocks belongs to case (iii).
        sidechain1 = add_new_blocks(manager,
                                    3,
                                    parent_block_hash=fork_block1.hash)
        for block in sidechain1:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        # Add some transactions between blocks
        txs = add_new_transactions(manager, 2, advance_clock=15)
        for tx in txs:
            score = sum_weights(score, tx.weight)

        # Propagate a block connected to the voided chain
        # This block belongs to case (iv).
        fork_block3 = manager.generate_mining_block(
            parent_block_hash=fork_block1.hash)
        fork_block3.resolve()
        fork_block3.verify()
        self.assertTrue(manager.propagate_tx(fork_block3))
        fork_meta3 = fork_block3.get_metadata()
        self.assertEqual(fork_meta3.voided_by, {fork_block3.hash})

        self.assertConsensusValid(manager)
    def test_multiple_forks(self):
        self.assertEqual(len(self.genesis_blocks), 1)
        manager = self.create_peer('testnet', tx_storage=self.tx_storage)

        # The initial score is the sum of the genesis
        score = self.genesis_blocks[0].weight
        for tx in self.genesis_txs:
            score = sum_weights(score, tx.weight)

        # Mine 30 blocks in a row with no transactions, case (i).
        blocks = add_new_blocks(manager, 30, advance_clock=15)
        for i, block in enumerate(blocks):
            meta = block.get_metadata()
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)

        # Add some transactions between blocks
        txs1 = add_new_transactions(manager, 5, advance_clock=15)
        for tx in txs1:
            score = sum_weights(score, tx.weight)

        # Mine 1 blocks, case (i).
        blocks = add_new_blocks(manager, 1, advance_clock=15)
        block_before_fork = blocks[0]
        for i, block in enumerate(blocks):
            meta = block.get_metadata()
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)

        for tx in txs1:
            meta = tx.get_metadata(force_reload=True)
            self.assertEqual(meta.first_block, blocks[0].hash)

        # Add some transactions between blocks
        txs2 = add_new_transactions(manager, 3, advance_clock=15)
        for tx in txs2:
            score = sum_weights(score, tx.weight)

        # Mine 5 blocks in a row, case (i).
        blocks = add_new_blocks(manager, 5, advance_clock=15)
        for i, block in enumerate(blocks):
            meta = block.get_metadata()
            score = sum_weights(score, block.weight)
            self.assertAlmostEqual(score, meta.score)

        # Mine 4 blocks, starting a fork.
        # All these blocks belong to case (ii).
        sidechain = add_new_blocks(manager,
                                   4,
                                   advance_clock=15,
                                   parent_block_hash=blocks[0].parents[0])

        # Fork block must have the same parents as blocks[0] as well as the same score
        self.assertEqual(set(blocks[0].parents), set(sidechain[0].parents))

        for block in blocks:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, None)

        for block in sidechain:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        # Propagate a block connected to the voided chain, case (iii).
        fork_block2 = manager.generate_mining_block(
            parent_block_hash=sidechain[-1].hash)
        fork_block2.resolve()
        fork_block2.verify()
        self.assertTrue(manager.propagate_tx(fork_block2))
        sidechain.append(fork_block2)

        # Now, both chains have the same score.
        for block in blocks:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        for block in sidechain:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        for tx in txs1:
            meta = tx.get_metadata(force_reload=True)
            self.assertEqual(meta.first_block, block_before_fork.hash)

        for tx in txs2:
            meta = tx.get_metadata(force_reload=True)
            self.assertIsNone(meta.first_block)

        # Mine 1 block, starting another fork.
        # This block belongs to case (vi).
        sidechain2 = add_new_blocks(manager,
                                    1,
                                    advance_clock=15,
                                    parent_block_hash=sidechain[0].hash)

        for block in sidechain2:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        # Mine 2 more blocks in the new fork.
        # These blocks belong to case (vii).
        sidechain2 += add_new_blocks(manager,
                                     2,
                                     advance_clock=15,
                                     parent_block_hash=sidechain2[-1].hash)

        for block in sidechain2:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        # Mine 1 block, starting another fork from sidechain2.
        # This block belongs to case (viii).
        sidechain3 = add_new_blocks(manager,
                                    1,
                                    advance_clock=15,
                                    parent_block_hash=sidechain2[-2].hash)

        for block in sidechain3:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        # Propagate a block connected to the side chain, case (v).
        fork_block3 = manager.generate_mining_block(
            parent_block_hash=fork_block2.hash)
        fork_block3.resolve()
        fork_block3.verify()
        self.assertTrue(manager.propagate_tx(fork_block3))
        sidechain.append(fork_block3)

        # The side chains have exceeded the score (after it has the same score)
        for block in blocks:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        for block in sidechain:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, None)

        # from hathor.graphviz import GraphvizVisualizer
        # dot = GraphvizVisualizer(manager.tx_storage, include_verifications=True, include_funds=True).dot()
        # dot.render('dot0')

        for tx in txs2:
            meta = tx.get_metadata(force_reload=True)
            self.assertEqual(meta.first_block, sidechain[0].hash)

        # Propagate a block connected to the side chain, case (v).
        # Another side chain has direcly exceeded the best score.
        fork_block4 = manager.generate_mining_block(
            parent_block_hash=sidechain3[-1].hash)
        fork_block4.weight = 10
        fork_block4.resolve()
        fork_block4.verify()
        self.assertTrue(manager.propagate_tx(fork_block4))
        sidechain3.append(fork_block4)

        for block in blocks:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        for block in sidechain[1:]:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        for block in sidechain2[-1:]:
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, {block.hash})

        for block in chain(sidechain[:1], sidechain2[:-1], sidechain3):
            meta = block.get_metadata(force_reload=True)
            self.assertEqual(meta.voided_by, None)

        for tx in txs2:
            meta = tx.get_metadata(force_reload=True)
            self.assertEqual(meta.first_block, sidechain[0].hash)

        # dot = manager.tx_storage.graphviz(format='pdf')
        # dot.render('test_fork')

        self.assertConsensusValid(manager)