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)
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)
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))
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)
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
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)
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
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), )
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
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)
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
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)