Exemple #1
0
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)
Exemple #2
0
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)
Exemple #3
0
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)