class TestMiner(TestCase): def setUp(self): self.time = 1526830525 self.m_mining_qaddress = alice.qaddress self.m_mining_address = parse_qaddress(self.m_mining_qaddress) self.chain_manager = Mock(spec=ChainManager) self.parent_block = Block() self.parent_difficulty = StringToUInt256('0') # tuple (0,0,0,0,0...) length 32 self.m_pre_block_logic = Mock(spec=POW.pre_block_logic, name='hello') m_add_unprocessed_txn_fn = create_autospec(P2PFactory.add_unprocessed_txn) mining_thread_count = 1 self.miner = Miner(self.m_pre_block_logic, self.m_mining_address, self.chain_manager, mining_thread_count, m_add_unprocessed_txn_fn) self.txpool = Mock(spec=TransactionPool) self.txpool.transactions = [] def test_prepare_next_unmined_block_template_works(self, m_getTime, m_logger): """ All the setup stuff you need before you actually mine a block goes here. It's broken out into this function, because if you have a mining pool, this function prepares the getblocktemplate for the pool. """ m_getTime.return_value = self.time self.chain_manager.get_measurement.return_value = 60 self.txpool.transactions = [] self.assertIsNone(self.miner._current_difficulty) self.assertIsNone(self.miner._current_target) self.assertIsNone(self.miner._measurement) self.miner.prepare_next_unmined_block_template(self.m_mining_address, self.txpool, self.parent_block, self.parent_difficulty) self.assertEqual(self.miner._current_difficulty, StringToUInt256('2')) self.assertEqual(self.miner._current_target, StringToUInt256( '115792089237316195423570985008687907853269984665640564039457584007913129639807')) self.assertEqual(self.miner._measurement, 60) # because we set it earlier in this test def test_prepare_next_unmined_block_template_exception(self, m_getTime, m_logger): """ If this function should throw an exception, nothing should happen except a call to the logger. """ m_getTime.return_value = self.time self.chain_manager.get_measurement.side_effect = ValueError self.txpool.transactions = [] self.assertIsNone(self.miner._current_difficulty) self.assertIsNone(self.miner._current_target) self.assertIsNone(self.miner._measurement) self.miner.prepare_next_unmined_block_template(self.m_mining_address, self.txpool, self.parent_block, self.parent_difficulty) self.assertIsNone(self.miner._current_difficulty) self.assertIsNone(self.miner._current_target) self.assertIsNone(self.miner._measurement) m_logger.warning.assert_called_once() m_logger.exception.assert_called_once() def test_start_mining_works(self, m_getTime, m_logger): m_getTime.return_value = self.time # Do prepare_next_unmined_block_template()'s job self.miner._mining_block = Block() # From sample run of test_prepare_next_unmined_block_template_works() self.miner._measurement = 60 self.miner._current_difficulty = StringToUInt256('0') self.miner._current_target = \ StringToUInt256('115792089237316195423570985008687907853269984665640564039457584007913129639807') # start() is from Qryptominer, let's not actually mine in a test with patch('qrl.core.Miner.Miner.start', spec=True) as m_start: self.miner.start_mining(self.parent_block, self.parent_difficulty) m_start.assert_called_once() def test_get_block_to_mine_no_existing_block_being_mined_upon(self, m_getTime, m_logger): """ This function takes a Qaddress, and returns a blob for the miner/mining pool to work on. It makes sure we have a block we're trying to mine and that the coinbase points to the Qaddress. In this test we check that: 1. If we don't have a block we're trying to mine on, it generates one on the fly. """ m_getTime.return_value = 1526830525 self.miner._current_difficulty = StringToUInt256('1') blob, difficulty = self.miner.get_block_to_mine(self.m_mining_qaddress.encode(), self.txpool, self.parent_block, self.parent_difficulty) self.assertEqual(difficulty, 1) # because self.miner._current_difficulty was set above self.assertEqual(blob, '0014db80611fbf16e342a2afb8b77b1f513f9db21de3ff905c0c27ea0078c489248f37f9e2a22400000000000000000000000000000000004bfaabbf147f985be702a373183be1be77100b24') # noqa def test_get_block_to_mine_not_mining_upon_last_block(self, m_getTime, m_logger): """ In this test we check that: 2. If we aren't mining upon the last block, it regenerates the blocktemplate. """ m_getTime.return_value = 1526830525 self.miner._current_difficulty = StringToUInt256('1') m_mining_block = Mock(autospec=Block) m_mining_block.mining_blob = b'big_bad_blob' m_mining_block.prev_headerhash = b'nothing should be equal to this' self.miner._mining_block = m_mining_block blob, difficulty = self.miner.get_block_to_mine(self.m_mining_qaddress.encode(), self.txpool, self.parent_block, self.parent_difficulty) self.assertEqual(difficulty, 1) # because self.miner._current_difficulty was set above self.assertEqual(blob, '0014db80611fbf16e342a2afb8b77b1f513f9db21de3ff905c0c27ea0078c489248f37f9e2a22400000000000000000000000000000000004bfaabbf147f985be702a373183be1be77100b24') # noqa def test_get_block_to_mine_perfect_block_no_changes(self, m_getTime, m_logger): """ In this test, we check that the function makes no changes to the block if the coinbase addr is our addr, and we are on the latest block. """ m_coinbase = Mock(autospec=CoinBase, name='I am a Coinbase') m_coinbase.coinbase.addr_to = self.m_mining_address m_parent_block = Mock(autospec=Block, name='mock parent_block') m_parent_block.transactions = [m_coinbase] m_mining_block = Mock(autospec=Block, name='mock _mining_block') m_mining_block.transactions = [m_coinbase] m_mining_block.mining_blob = b'this is the blob you should iterate the nonce upon' self.miner._mining_block = m_mining_block self.miner._current_difficulty = StringToUInt256('1') m_parent_block.headerhash = b'block_headerhash' m_mining_block.prev_headerhash = b'block_headerhash' blob, difficulty = self.miner.get_block_to_mine(self.m_mining_qaddress.encode(), self.txpool, m_parent_block, self.parent_difficulty) self.assertEqual(blob, '746869732069732074686520626c6f6220796f752073686f756c64206974657261746520746865206e6f6e63652075706f6e') self.assertEqual(difficulty, 1) def test_get_block_to_mine_we_have_a_block_in_mind(self, m_getTime, m_logger): """ In this test, we check that 1. The function checks that the blocktemplate's coinbase points to the given Qaddress 2. The function checks that we are mining on the tip :param m_logger: :return: """ m_coinbase = Mock(autospec=CoinBase, name='I am a Coinbase') m_coinbase.coinbase.addr_to = self.m_mining_address m_parent_block = Mock(autospec=Block, name='mock parent_block') m_parent_block.transactions = [m_coinbase] m_mining_block = Mock(autospec=Block, name='mock _mining_block') m_mining_block.transactions = [m_coinbase] m_mining_block.mining_blob = b'this is the blob you should iterate the nonce upon' self.miner._mining_block = m_mining_block self.miner._current_difficulty = StringToUInt256('1') # If the coinbase doesn't point to us, make it point to us. foreign_qaddress = bob.qaddress m_parent_block.headerhash = b'block_headerhash' m_mining_block.prev_headerhash = b'block_headerhash' blob, difficulty = self.miner.get_block_to_mine(foreign_qaddress.encode(), self.txpool, m_parent_block, self.parent_difficulty) # actually, the blob's value will not change because mining_block.update_mining_address() is a mock. # it will have the same value as in test_get_block_to_mine_perfect_block_no_changes() # it's enough to see that it actually runs m_mining_block.update_mining_address.assert_called_once() self.assertIsNotNone(blob) self.assertEqual(difficulty, 1) def test_get_block_to_mine_chokes_on_invalid_mining_address(self, m_getTime, m_logger): invalid_address = self.m_mining_qaddress + 'aaaa' m_parent_block = Mock(autospec=Block, name='mock parent_block') with self.assertRaises(ValueError): self.miner.get_block_to_mine(invalid_address.encode(), self.txpool, m_parent_block, self.parent_difficulty) def test_submit_mined_block(self, m_getTime, m_logger): """ This runs when a miner submits a blob with a valid nonce. It returns True only if BlockHeader says the nonce position is okay, and the PoWValidator says the nonce is valid. :param m_getTime: :param m_logger: :return: """ m_mining_block = Mock(autospec=Block, name='mock _mining_block') m_mining_block.verify_blob.return_value = False self.miner._mining_block = m_mining_block blob = 'this is a blob12345that was the nonce'.encode() result = self.miner.submit_mined_block(blob) self.assertFalse(result) m_mining_block.verify_blob.return_value = True self.chain_manager.validate_mining_nonce = MagicMock(return_value=False) result = self.miner.submit_mined_block(blob) self.assertFalse(result) m_mining_block.verify_blob.return_value = True self.chain_manager.validate_mining_nonce = MagicMock(return_value=True) self.m_pre_block_logic.return_value = True result = self.miner.submit_mined_block(blob) self.assertTrue(result)
class POW(ConsensusMechanism): def __init__(self, chain_manager: ChainManager, p2p_factory, sync_state: SyncState, time_provider, mining_address: bytes, mining_thread_count): super().__init__(chain_manager) self.sync_state = sync_state self.time_provider = time_provider self.miner_toggler = False self.mining_address = mining_address self.p2p_factory = p2p_factory # FIXME: Decouple from p2pFactory. Comms vs node logic self.p2p_factory.pow = self # FIXME: Temporary hack to keep things working while refactoring self.miner = Miner(self.pre_block_logic, self.mining_address, self.chain_manager.state, mining_thread_count, self.p2p_factory.add_unprocessed_txn) self._miner_lock = threading.Lock() ######## self.last_pow_cycle = 0 self.last_bk_time = 0 self.last_pb_time = 0 self.epoch_diff = None ################################################## ################################################## ################################################## ################################################## def start(self): self.restart_monitor_bk(80) reactor.callLater(20, self.initialize_pow) def _handler_state_unsynced(self): self.miner.cancel() self.last_bk_time = time.time() self.restart_unsynced_logic() def _handler_state_syncing(self): self.last_pb_time = time.time() def _handler_state_synced(self): self.last_pow_cycle = time.time() last_block = self.chain_manager.last_block self.mine_next(last_block) def _handler_state_forked(self): pass def update_node_state(self, new_sync_state: ESyncState): self.sync_state.state = new_sync_state logger.info('Status changed to %s', self.sync_state.state) _mapping = { ESyncState.unsynced: self._handler_state_unsynced, ESyncState.syncing: self._handler_state_syncing, ESyncState.synced: self._handler_state_synced, ESyncState.forked: self._handler_state_forked, } _mapping[self.sync_state.state]() def stop_monitor_bk(self): try: reactor.monitor_bk.cancel() except Exception: # No need to log this exception pass def restart_monitor_bk(self, delay: int): self.stop_monitor_bk() reactor.monitor_bk = reactor.callLater(delay, self.monitor_bk) def monitor_bk(self): # FIXME: Too many magic numbers / timing constants # FIXME: This is obsolete time_diff1 = time.time() - self.last_pow_cycle if 90 < time_diff1: if self.sync_state.state == ESyncState.unsynced: if time.time() - self.last_bk_time > 120: self.last_pow_cycle = time.time() logger.info(' POW cycle activated by monitor_bk() ') self.update_node_state(ESyncState.synced) reactor.monitor_bk = reactor.callLater(60, self.monitor_bk) return time_diff2 = time.time() - self.last_pb_time if self.sync_state.state == ESyncState.syncing and time_diff2 > 60: self.update_node_state(ESyncState.unsynced) self.epoch_diff = -1 reactor.monitor_bk = reactor.callLater(60, self.monitor_bk) def initialize_pow(self): reactor.callLater(0, self.update_node_state, ESyncState.synced) reactor.callLater(60, self.monitor_miner) ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## def restart_unsynced_logic(self, delay=0): logger.info('Restarting unsynced logic in %s seconds', delay) try: reactor.unsynced_logic.cancel() except Exception: # No need to log this exception pass reactor.unsynced_logic = reactor.callLater(delay, self.unsynced_logic) def unsynced_logic(self): if self.sync_state.state != ESyncState.synced: self.p2p_factory.broadcast_get_synced_state() reactor.request_peer_blockheight = reactor.callLater( 0, self.p2p_factory.request_peer_blockheight) reactor.unsynced_logic = reactor.callLater(20, self.start_download) def start_download(self): # FIXME: Why PoW is downloading blocks? # add peers and their identity to requested list # FMBH if self.sync_state.state == ESyncState.synced: return logger.info('Checking Download..') if self.p2p_factory.connections == 0: logger.warning('No connected peers. Moving to synced state') self.update_node_state(ESyncState.synced) return self.update_node_state(ESyncState.syncing) logger.info('Initializing download from %s', self.chain_manager.height + 1) self.p2p_factory.randomize_block_fetch() ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## def monitor_miner(self): reactor.callLater(60, self.monitor_miner) if not config.user.mining_enabled: return if not self.miner.isRunning() or self.miner_toggler: logger.debug('Mine next called by monitor_miner') self.miner_toggler = False self.mine_next(self.chain_manager.last_block) elif self.miner.solutionAvailable(): self.miner_toggler = True else: self.miner_toggler = False def pre_block_logic(self, block: Block): logger.debug('Checking miner lock') with self._miner_lock: logger.debug('Inside add_block') result = self.chain_manager.add_block(block) logger.debug('trigger_miner %s', self.chain_manager.trigger_miner) if self.chain_manager.trigger_miner: self.mine_next(self.chain_manager.last_block) if not result: logger.debug('Block Rejected %s %s', block.block_number, bin2hstr(block.headerhash)) return reactor.callLater(0, self.broadcast_block, block) def broadcast_block(self, block): if self.sync_state.state == ESyncState.synced: self.p2p_factory.broadcast_block(block) def isSynced(self, block_timestamp) -> bool: if block_timestamp + config.dev.minimum_minting_delay > ntp.getTime(): self.update_node_state(ESyncState.synced) return True return False def mine_next(self, parent_block): if config.user.mining_enabled: parent_metadata = self.chain_manager.state.get_block_metadata( parent_block.headerhash) self.miner.prepare_next_unmined_block_template( mining_address=self.mining_address, tx_pool=self.chain_manager.tx_pool, parent_block=parent_block, parent_difficulty=parent_metadata.block_difficulty) logger.info('Mining Block #%s', parent_block.block_number + 1) self.miner.start_mining(parent_block, parent_metadata.block_difficulty)
class POW(ConsensusMechanism): def __init__(self, chain_manager: ChainManager, p2p_factory, sync_state: SyncState, time_provider, slaves: list): super().__init__(chain_manager) self.sync_state = sync_state self.time_provider = time_provider self.miner_toggler = False self.slaves = slaves self.p2p_factory = p2p_factory # FIXME: Decouple from p2pFactory. Comms vs node logic self.p2p_factory.pow = self # FIXME: Temporary hack to keep things working while refactoring self.miner = Miner(self.pre_block_logic, self.slaves, self.chain_manager.state, self.p2p_factory.add_unprocessed_txn) self._miner_lock = threading.Lock() ######## self.last_pow_cycle = 0 self.last_bk_time = 0 self.last_pb_time = 0 self.epoch_diff = None ################################################## ################################################## ################################################## ################################################## def start(self): self.restart_monitor_bk(80) reactor.callLater(20, self.initialize_pow) def _handler_state_unsynced(self): self.miner.cancel() self.last_bk_time = time.time() self.restart_unsynced_logic() def _handler_state_syncing(self): self.miner.cancel() self.last_pb_time = time.time() def _handler_state_synced(self): self.last_pow_cycle = time.time() last_block = self.chain_manager.last_block self.mine_next(last_block) def _handler_state_forked(self): pass def update_node_state(self, new_sync_state: ESyncState): self.sync_state.state = new_sync_state logger.info('Status changed to %s', self.sync_state.state) _mapping = { ESyncState.unsynced: self._handler_state_unsynced, ESyncState.syncing: self._handler_state_syncing, ESyncState.synced: self._handler_state_synced, ESyncState.forked: self._handler_state_forked, } _mapping[self.sync_state.state]() def stop_monitor_bk(self): try: reactor.monitor_bk.cancel() except Exception: # No need to log this exception pass def restart_monitor_bk(self, delay: int): self.stop_monitor_bk() reactor.monitor_bk = reactor.callLater(delay, self.monitor_bk) def monitor_bk(self): # FIXME: Too many magic numbers / timing constants # FIXME: This is obsolete time_diff1 = time.time() - self.last_pow_cycle if 90 < time_diff1: if self.sync_state.state == ESyncState.unsynced: if time.time() - self.last_bk_time > 120: self.last_pow_cycle = time.time() logger.info(' POW cycle activated by monitor_bk() ') self.update_node_state(ESyncState.synced) reactor.monitor_bk = reactor.callLater(60, self.monitor_bk) return time_diff2 = time.time() - self.last_pb_time if self.sync_state.state == ESyncState.syncing and time_diff2 > 60: self.update_node_state(ESyncState.unsynced) self.epoch_diff = -1 reactor.monitor_bk = reactor.callLater(60, self.monitor_bk) def initialize_pow(self): reactor.callLater(30, self.update_node_state, ESyncState.synced) reactor.callLater(60, self.monitor_miner) ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## def restart_unsynced_logic(self, delay=0): logger.info('Restarting unsynced logic in %s seconds', delay) try: reactor.unsynced_logic.cancel() except Exception: # No need to log this exception pass reactor.unsynced_logic = reactor.callLater(delay, self.unsynced_logic) def unsynced_logic(self): if self.sync_state.state != ESyncState.synced: self.p2p_factory.broadcast_get_synced_state() reactor.request_peer_blockheight = reactor.callLater(0, self.p2p_factory.request_peer_blockheight) reactor.unsynced_logic = reactor.callLater(20, self.start_download) def start_download(self): # FIXME: Why PoW is downloading blocks? # add peers and their identity to requested list # FMBH if self.sync_state.state == ESyncState.synced: return logger.info('Checking Download..') if self.p2p_factory.connections == 0: logger.warning('No connected peers. Moving to synced state') self.update_node_state(ESyncState.synced) return self.update_node_state(ESyncState.syncing) logger.info('Initializing download from %s', self.chain_manager.height + 1) self.p2p_factory.randomize_block_fetch() ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## ############################################## def monitor_miner(self): reactor.callLater(60, self.monitor_miner) if self.p2p_factory.is_syncing(): return if not self.miner.isRunning() or self.miner_toggler: logger.debug('Mine next called by monitor_miner') self.miner_toggler = False self.mine_next(self.chain_manager.last_block) elif self.miner.solutionAvailable(): self.miner_toggler = True else: self.miner_toggler = False def pre_block_logic(self, block: Block): logger.debug('Checking miner lock') with self._miner_lock: logger.debug('Inside add_block') result = self.chain_manager.add_block(block) logger.debug('trigger_miner %s', self.chain_manager.trigger_miner) logger.debug('is_syncing %s', self.p2p_factory.is_syncing()) if not self.p2p_factory.is_syncing(): if self.chain_manager.trigger_miner or not self.miner.isRunning(): self.mine_next(self.chain_manager.last_block) if not result: logger.debug('Block Rejected %s %s', block.block_number, bin2hstr(block.headerhash)) return reactor.callLater(0, self.broadcast_block, block) def broadcast_block(self, block): if self.sync_state.state == ESyncState.synced: self.p2p_factory.broadcast_block(block) def isSynced(self, block_timestamp) -> bool: if block_timestamp + config.dev.minimum_minting_delay > ntp.getTime(): self.update_node_state(ESyncState.synced) return True return False def mine_next(self, parent_block): if config.user.mining_enabled: parent_metadata = self.chain_manager.state.get_block_metadata(parent_block.headerhash) logger.info('Mining Block #%s', parent_block.block_number + 1) self.miner.start_mining(self.chain_manager.tx_pool, parent_block, parent_metadata.block_difficulty)