def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)

        address = 'HC7w4j7mPet49BBN5a2An3XUiPvK6C1TL7'

        self.client = HathorClientTest(server_url='')
        self.loop.run_until_complete(self.client.start())
        self.manager = TxMiningManager(backend=self.client, address=address)
        self.loop.run_until_complete(self.manager.start())
        self.loop.run_until_complete(self.manager.wait_for_block_template())
        self.assertTrue(len(self.manager.block_template) > 0)
    def setUp(self):
        address = 'HC7w4j7mPet49BBN5a2An3XUiPvK6C1TL7'

        from tests.utils import Clock
        self.clock = Clock(self.loop)
        self.clock.enable()

        self.client = HathorClientTest(server_url='')
        self.loop.run_until_complete(self.client.start())
        self.manager = TxMiningManager(backend=self.client, address=address)
        self.loop.run_until_complete(self.manager.start())
        self.loop.run_until_complete(self.manager.wait_for_block_template())
        self.assertTrue(len(self.manager.block_template) > 0)
    def test_invalid_mining_address(self):
        from hathorlib.exceptions import InvalidAddress
        address = 'HC7w4j7mPet49BBN5a2An3XUiPvK6C1TL7'

        invalid_addresses = [
            ('Invalid base58', address[:-1] + 'I'),  # No 'I' in base58 symbols.
            ('Invalid checksum', address[:-1] + 'A'),
            ('Invalid size (smaller)', address[:-1]),
            ('Invalid size (bigger)', address + '7'),
        ]
        for idx, (cause, invalid_address) in enumerate(invalid_addresses):
            with self.assertRaises(InvalidAddress):
                print('Address #{}: {} ({})'.format(idx, cause, invalid_address))
                TxMiningManager(backend=self.client, address=invalid_address)
 def setUp(self):
     self.manager = TxMiningManager(backend=None, address=None)
     self.tmpdir = tempfile.mkdtemp()
Пример #5
0
 async def get_application(self):
     self.manager = TxMiningManager(backend=None, address=None)
     self.myapp = App(self.manager)
     return self.myapp.app
Пример #6
0
def execute(args: Namespace) -> None:
    """Run the service according to the args."""
    from hathorlib.client import HathorClient

    from txstratum.api import App
    from txstratum.manager import TxMiningManager
    from txstratum.utils import start_logging

    # Configure log.
    start_logging()
    if os.path.exists(args.log_config):
        logging.config.fileConfig(args.log_config)
        from structlog.stdlib import LoggerFactory
        structlog.configure(logger_factory=LoggerFactory())
        logger.info('tx-mining-service', backend=args.backend)
        logger.info('Configuring log...', log_config=args.log_config)
    else:
        logger.info('tx-mining-service', backend=args.backend)
        logger.info('Log config file not found; using default configuration.',
                    log_config=args.log_config)

    # Set up all parts.
    loop = asyncio.get_event_loop()

    backend = HathorClient(args.backend)
    manager = TxMiningManager(
        backend=backend,
        address=args.address,
    )
    loop.run_until_complete(backend.start())
    loop.run_until_complete(manager.start())
    server = loop.run_until_complete(
        loop.create_server(manager, '0.0.0.0', args.stratum_port))

    if args.prometheus:
        from txstratum.prometheus import PrometheusExporter
        metrics = PrometheusExporter(manager, args.prometheus)
        metrics.start()

    api_app = App(manager,
                  max_tx_weight=args.max_tx_weight,
                  max_timestamp_delta=args.max_timestamp_delta,
                  tx_timeout=args.tx_timeout,
                  fix_invalid_timestamp=args.fix_invalid_timestamp)
    logger.info('API Configuration',
                max_tx_weight=api_app.max_tx_weight,
                tx_timeout=api_app.tx_timeout,
                max_timestamp_delta=api_app.max_timestamp_delta,
                fix_invalid_timestamp=api_app.fix_invalid_timestamp)
    web_runner = web.AppRunner(api_app.app)
    loop.run_until_complete(web_runner.setup())
    site = web.TCPSite(web_runner, '0.0.0.0', args.api_port)
    loop.run_until_complete(site.start())

    try:
        logger.info('Stratum Server running at 0.0.0.0:{}...'.format(
            args.stratum_port))
        logger.info('TxMining API running at 0.0.0.0:{}...'.format(
            args.api_port))
        if args.testnet:
            logger.info('Running with testnet config file')
        loop.run_forever()
    except KeyboardInterrupt:
        logger.info('Stopping...')

    server.close()
    loop.run_until_complete(server.wait_closed())
    loop.run_until_complete(backend.stop())
    loop.close()
class ManagerClockedTestCase(asynctest.ClockedTestCase):  # type: ignore
    def setUp(self):
        address = 'HC7w4j7mPet49BBN5a2An3XUiPvK6C1TL7'

        from tests.utils import Clock
        self.clock = Clock(self.loop)
        self.clock.enable()

        self.client = HathorClientTest(server_url='')
        self.loop.run_until_complete(self.client.start())
        self.manager = TxMiningManager(backend=self.client, address=address)
        self.loop.run_until_complete(self.manager.start())
        self.loop.run_until_complete(self.manager.wait_for_block_template())
        self.assertTrue(len(self.manager.block_template) > 0)

    def tearDown(self):
        self.clock.disable()

    async def test_block_timestamp_update(self):
        job = self.manager.get_best_job(None)
        self.assertTrue(True, job.is_block)

        job.update_timestamp(force=True)
        self.assertEqual(int(txstratum.time.time()), job._block.timestamp)

        # Update timestamp.
        await self.advance(10)
        job.update_timestamp()
        self.assertEqual(int(txstratum.time.time()), job._block.timestamp)

        # Do not update timestamp.
        old_ts = txstratum.time.time()
        await self.advance(40)
        job.update_timestamp()
        self.assertEqual(int(old_ts), job._block.timestamp)

    async def test_tx_resubmit(self):
        job1 = TxJob(TX1_DATA, timeout=10)
        ret1 = self.manager.add_job(job1)
        self.assertTrue(ret1)

        # When a similar job is submitted, manager declines it.
        job2 = TxJob(TX1_DATA)
        ret2 = self.manager.add_job(job2)
        self.assertFalse(ret2)

        # Wait until job1 is marked as timeout.
        await self.advance(15)
        self.assertEqual(job1.status, JobStatus.TIMEOUT)

        # Try to resubmit a similar job.
        job3 = TxJob(TX1_DATA)
        ret3 = self.manager.add_job(job3)
        self.assertTrue(ret3)

    async def test_tx_timeout_and_cleanup(self):
        job1 = TxJob(TX1_DATA, timeout=10)
        ret1 = self.manager.add_job(job1)
        self.assertTrue(ret1)
        self.assertIn(job1, self.manager.tx_queue)
        self.assertIn(job1.uuid, self.manager.tx_jobs)

        # Wait until job1 is marked as timeout.
        await self.advance(15)
        self.assertEqual(job1.status, JobStatus.TIMEOUT)
        self.assertNotIn(job1, self.manager.tx_queue)
        self.assertIn(job1.uuid, self.manager.tx_jobs)

        # Wait until job1 is cleared.
        await self.advance(self.manager.TX_CLEAN_UP_INTERVAL)
        self.assertNotIn(job1, self.manager.tx_queue)
        self.assertNotIn(job1.uuid, self.manager.tx_jobs)

    async def test_tx_race_condition(self):
        """Test race condition caused when job2 replaces job1 and job1's clean up is close to be executed.
        In this case job1's clean up was cleaning job2 instead.
        """
        job1 = TxJob(TX1_DATA, timeout=10)
        ret1 = self.manager.add_job(job1)
        self.assertTrue(ret1)

        # Wait until job1 is marked as timeout.
        await self.advance(10)
        self.assertEqual(job1.status, JobStatus.TIMEOUT)
        self.assertNotIn(job1, self.manager.tx_queue)
        self.assertIn(job1.uuid, self.manager.tx_jobs)

        # We are 1 second away to cleanup the tx.
        await self.advance(self.manager.TX_CLEAN_UP_INTERVAL - 1)

        # Resubmit a similar job.
        job2 = TxJob(TX1_DATA, timeout=10)
        ret2 = self.manager.add_job(job2)
        self.assertTrue(ret2)
        self.assertIn(job2, self.manager.tx_queue)
        self.assertIn(job2.uuid, self.manager.tx_jobs)

        # Reach the cleanup time of job1.
        await self.advance(2)
        self.assertEqual(job2.status, JobStatus.ENQUEUED)
        self.assertIn(job2, self.manager.tx_queue)
        self.assertIn(job2.uuid, self.manager.tx_jobs)

        # Job2 timeouts.
        await self.advance(15)
        self.assertEqual(job2.status, JobStatus.TIMEOUT)
class ManagerTestCase(unittest.TestCase):
    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)

        address = 'HC7w4j7mPet49BBN5a2An3XUiPvK6C1TL7'

        self.client = HathorClientTest(server_url='')
        self.loop.run_until_complete(self.client.start())
        self.manager = TxMiningManager(backend=self.client, address=address)
        self.loop.run_until_complete(self.manager.start())
        self.loop.run_until_complete(self.manager.wait_for_block_template())
        self.assertTrue(len(self.manager.block_template) > 0)

    def _run_all_pending_events(self):
        """Run all pending events."""
        # pending = asyncio.all_tasks(self.loop)
        # self.loop.run_until_complete(asyncio.gather(*pending))
        async def _fn():
            pass
        future = asyncio.ensure_future(_fn())
        self.loop.run_until_complete(future)

    def test_invalid_mining_address(self):
        from hathorlib.exceptions import InvalidAddress
        address = 'HC7w4j7mPet49BBN5a2An3XUiPvK6C1TL7'

        invalid_addresses = [
            ('Invalid base58', address[:-1] + 'I'),  # No 'I' in base58 symbols.
            ('Invalid checksum', address[:-1] + 'A'),
            ('Invalid size (smaller)', address[:-1]),
            ('Invalid size (bigger)', address + '7'),
        ]
        for idx, (cause, invalid_address) in enumerate(invalid_addresses):
            with self.assertRaises(InvalidAddress):
                print('Address #{}: {} ({})'.format(idx, cause, invalid_address))
                TxMiningManager(backend=self.client, address=invalid_address)

    def test_miner_connect_disconnect(self):
        conn = StratumProtocol(self.manager)
        conn.connection_made(transport=None)
        self.assertEqual(1, len(self.manager.connections))
        self.assertEqual(0, len(self.manager.miners))
        conn.connection_lost(exc=None)
        self.assertEqual(0, len(self.manager.connections))
        self.assertEqual(0, len(self.manager.miners))

    def test_miner_connect_ready_disconnect(self):
        conn = StratumProtocol(self.manager)
        transport = Mock()
        conn.connection_made(transport=transport)
        self.assertEqual(1, len(self.manager.connections))
        self.assertEqual(0, len(self.manager.miners))

        conn.method_subscribe(params=None, msgid=None)
        conn.method_authorize(params=None, msgid=None)
        self.assertEqual(1, len(self.manager.miners))

        conn.connection_lost(exc=None)
        self.assertEqual(0, len(self.manager.connections))
        self.assertEqual(0, len(self.manager.miners))

    def test_many_miners_connect_ready_disconnect(self, qty=5):
        transport = Mock()
        connections = []
        for idx in range(qty):
            conn = StratumProtocol(self.manager)
            conn.connection_made(transport=transport)
            self.assertEqual(idx + 1, len(self.manager.connections))
            self.assertEqual(0, len(self.manager.miners))
            connections.append(conn)

        self.assertEqual(qty, len(self.manager.connections))
        self.assertEqual(0, len(self.manager.miners))

        for idx, conn in enumerate(connections):
            conn.method_subscribe(params=None, msgid=None)
            conn.method_authorize(params=None, msgid=None)
            self.assertEqual(idx + 1, len(self.manager.miners))

        self.assertEqual(qty, len(self.manager.connections))
        self.assertEqual(qty, len(self.manager.miners))
        self.manager.status()

        for idx, conn in enumerate(connections):
            conn.connection_lost(exc=None)
            self.assertEqual(qty - idx - 1, len(self.manager.connections))
            self.assertEqual(qty - idx - 1, len(self.manager.miners))

        self.assertEqual(0, len(self.manager.connections))
        self.assertEqual(0, len(self.manager.miners))

    def test_miner_some_jsonrpc_methods(self):
        conn = StratumProtocol(self.manager)
        conn.connection_made(transport=None)

        conn.send_result = MagicMock(return_value=None)
        conn.method_extranonce_subscribe(params=None, msgid=None)
        conn.send_result.assert_called_with(None, True)

        conn.send_result = MagicMock(return_value=None)
        conn.method_multi_version(params=None, msgid=None)
        conn.send_result.assert_called_with(None, True)

    def test_miner_method_subscribe_invalid_address1(self):
        conn = StratumProtocol(self.manager)
        transport = Mock()
        conn.connection_made(transport=transport)
        conn.send_error = MagicMock(return_value=None)

        params = {
            'address': 'abc!'
        }
        conn.method_subscribe(params=params, msgid=None)
        conn.send_error.assert_called_once()
        transport.close.assert_called_once()

    def test_miner_method_subscribe_invalid_address2(self):
        conn = StratumProtocol(self.manager)
        transport = Mock()
        conn.connection_made(transport=transport)
        conn.send_error = MagicMock(return_value=None)

        params = {
            'address': 'ZiCa'
        }
        conn.method_subscribe(params=params, msgid=None)
        conn.send_error.assert_called_once()
        transport.close.assert_called_once()

    def test_miner_method_subscribe_invalid_address3(self):
        conn = StratumProtocol(self.manager)
        transport = Mock()
        conn.connection_made(transport=transport)
        conn.send_error = MagicMock(return_value=None)

        params = {
            'address': 'HVZjvL1FJ23kH3buGNuttVRsRKq66WHXXX'
        }
        conn.method_subscribe(params=params, msgid=None)
        conn.send_error.assert_called_once()
        transport.close.assert_called_once()

    def test_miner_method_subscribe_valid_address(self):
        conn = StratumProtocol(self.manager)
        transport = Mock()
        conn.connection_made(transport=transport)
        conn.send_error = MagicMock(return_value=None)

        params = {
            'address': 'HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ'
        }
        conn.method_subscribe(params=params, msgid=None)
        conn.send_error.assert_not_called()
        transport.close.assert_not_called()

    def _get_ready_miner(self, address: Optional[str] = None) -> StratumProtocol:
        conn = StratumProtocol(self.manager)
        conn._update_job_timestamp = False

        transport = Mock()
        conn.connection_made(transport=transport)

        if address:
            params = {'address': address}
        else:
            params = {}
        conn.method_subscribe(params=params, msgid=None)
        conn.method_authorize(params=None, msgid=None)
        return conn

    def test_miner_invalid_address(self):
        conn = StratumProtocol(self.manager)
        conn.send_error = MagicMock(return_value=None)

        transport = Mock()
        conn.connection_made(transport=transport)

        params = {'address': 'X'}
        conn.method_subscribe(params=params, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.INVALID_ADDRESS)

    def test_miner_only_blocks_submit_failed_1(self):
        conn = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)
        conn.send_error = MagicMock(return_value=None)
        conn.method_submit(params={}, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.INVALID_PARAMS, ANY)

    def test_miner_only_blocks_submit_failed_2(self):
        conn = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)

        params = {
            'job_id': 'abc!',
            'nonce': '123',
        }
        conn.send_error = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.INVALID_PARAMS, ANY)

    def test_miner_only_blocks_submit_failed_3(self):
        conn = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)

        params = {
            'job_id': 'ffff',
            'nonce': '123',
        }
        conn.send_error = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.JOB_NOT_FOUND)

    def test_miner_only_blocks_submit_failed_4(self):
        conn = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)

        params = {
            'job_id': conn.current_job.uuid.hex(),
            'nonce': 'FFZZ',
        }
        conn.send_error = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.INVALID_PARAMS, ANY)

    def test_miner_only_blocks_submit_failed_5(self):
        conn = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)

        params = {
            'job_id': conn.current_job.uuid.hex(),
            'nonce': '123',
        }
        conn.send_error = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.INVALID_SOLUTION)

    def test_miner_only_blocks_submit(self):
        conn = self._get_ready_miner()
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)
        self.assertEqual(0, conn.current_job.height)

        # First submission: success
        params = {
            'job_id': conn.current_job.uuid.hex(),
            'nonce': '00000000000000000000000000278a7e',
        }
        conn.send_error = MagicMock(return_value=None)
        conn.send_result = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_not_called()
        conn.send_result.assert_called_once_with(None, 'ok')

        # Second submission: stale job
        conn.send_error = MagicMock(return_value=None)
        conn.send_result = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.STALE_JOB, ANY)
        conn.send_result.assert_not_called()

        self._run_all_pending_events()
        self.loop.run_until_complete(self.manager.update_block_template())
        self.assertEqual(1, conn.current_job.height)

        # conn.connection_lost(exc=None)
        # self.loop.run_until_complete(self.manager.stop())

    def test_miner_only_blocks_update_block(self):
        conn = self._get_ready_miner()
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)
        self.assertEqual(0, conn.current_job.height)

        # Hathor full node returned a new block template.
        self.client.next_block_template()
        self.loop.run_until_complete(self.manager.update_block_template())
        self._run_all_pending_events()

        self.assertEqual(1, conn.current_job.height)

    def test_two_miners_same_submission_1(self):
        conn1 = self._get_ready_miner()
        conn2 = self._get_ready_miner()
        self.assertEqual(0, conn1.current_job.height)
        self.assertEqual(0, conn2.current_job.height)

        # First submission: success
        params = {
            'job_id': conn1.current_job.uuid.hex(),
            'nonce': '00000000000000000000000000278a7e',
        }
        conn1.send_error = MagicMock(return_value=None)
        conn1.send_result = MagicMock(return_value=None)
        self.manager.backend.push_tx_or_block = MagicMock(return_value=asyncio.Future())
        conn1.method_submit(params=params, msgid=None)
        conn1.send_error.assert_not_called()
        conn1.send_result.assert_called_once_with(None, 'ok')
        self.manager.backend.push_tx_or_block.assert_called_once()

        # As the main loop is not running, the jobs have not been updated yet.
        # Second submission: success, but it won't be propagated.
        conn2.send_error = MagicMock(return_value=None)
        conn2.send_result = MagicMock(return_value=None)
        self.manager.backend.push_tx_or_block = MagicMock(return_value=asyncio.Future())
        conn2.method_submit(params=params, msgid=None)
        conn1.send_error.assert_not_called()
        conn1.send_result.assert_called_once_with(None, 'ok')
        self.manager.backend.push_tx_or_block.assert_not_called()

    def test_two_miners_same_submission_2(self):
        conn1 = self._get_ready_miner()
        conn2 = self._get_ready_miner()
        self.assertEqual(0, conn1.current_job.height)
        self.assertEqual(0, conn2.current_job.height)
        params1 = {
            'job_id': conn1.current_job.uuid.hex(),
            'nonce': '00000000000000000000000000278a7e',
        }
        params2 = {
            'job_id': conn2.current_job.uuid.hex(),
            'nonce': '00000000000000000000000000278a7e',
        }

        # First submission: success
        conn1.send_error = MagicMock(return_value=None)
        conn1.send_result = MagicMock(return_value=None)
        conn1.method_submit(params=params1, msgid=None)
        conn1.send_error.assert_not_called()
        conn1.send_result.assert_called_once_with(None, 'ok')

        # Run the main loop to update the jobs.
        self._run_all_pending_events()
        self.loop.run_until_complete(self.manager.update_block_template())
        self.assertEqual(1, conn1.current_job.height)
        self.assertEqual(1, conn2.current_job.height)

        # As jobs have been updated, the submission from the second miner will be accepted but not propagated.
        # Second submission: success and not propagated.
        conn2.send_error = MagicMock(return_value=None)
        conn2.send_result = MagicMock(return_value=None)
        self.manager.backend.push_tx_or_block = MagicMock(return_value=asyncio.Future())
        conn2.method_submit(params=params2, msgid=None)
        conn1.send_error.assert_not_called()
        conn1.send_result.assert_called_once_with(None, 'ok')
        self.manager.backend.push_tx_or_block.assert_not_called()

    def _run_basic_tx_tests(self, conn, tx_data, tx_nonce):
        job = TxJob(tx_data)
        ret = self.manager.add_job(job)
        self.assertFalse(conn.current_job.is_block)
        self.assertEqual(conn.current_job.tx_job, job)
        self.assertTrue(ret)

        # First submission: wrong nonce
        params = {
            'job_id': conn.current_job.uuid.hex(),
            'nonce': '84e20000',
        }
        conn.send_error = MagicMock(return_value=None)
        conn.send_result = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.INVALID_SOLUTION)
        conn.send_result.assert_not_called()
        self.assertFalse(conn.current_job.is_block)

        # Second submission: success
        params = {
            'job_id': conn.current_job.uuid.hex(),
            'nonce': tx_nonce,
        }
        conn.send_error = MagicMock(return_value=None)
        conn.send_result = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_not_called()
        conn.send_result.assert_called_once_with(None, 'ok')

        # Third submission: stale
        conn.send_error = MagicMock(return_value=None)
        conn.send_result = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_called_once_with(None, conn.STALE_JOB, ANY)
        conn.send_result.assert_not_called()

    def test_one_miner_one_tx(self):
        conn = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)
        self.assertEqual(0, conn.current_job.height)

        self._run_basic_tx_tests(conn, TX1_DATA, TX1_NONCE)

        # Run loop and check that the miner gets a block
        self._run_all_pending_events()
        self.assertTrue(conn.current_job.is_block)
        self.assertEqual(0, conn.current_job.height)

    def test_one_miner_two_txs(self):
        conn = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)
        self.assertEqual(0, conn.current_job.height)

        job1 = TxJob(TX1_DATA)
        job2 = TxJob(TX2_DATA)
        ret1 = self.manager.add_job(job1)
        ret2 = self.manager.add_job(job2)
        self.assertFalse(conn.current_job.is_block)
        self.assertEqual(conn.current_job.tx_job, job1)
        self.assertTrue(ret1)
        self.assertTrue(ret2)

        # First submission: success
        params = {
            'job_id': conn.current_job.uuid.hex(),
            'nonce': TX1_NONCE,
        }
        conn.send_error = MagicMock(return_value=None)
        conn.send_result = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_not_called()
        conn.send_result.assert_called_once_with(None, 'ok')

        # Run loop and check that the miner gets the next tx
        self._run_all_pending_events()
        self.assertFalse(conn.current_job.is_block)
        self.assertEqual(conn.current_job.tx_job, job2)

        # First submission: success
        params = {
            'job_id': conn.current_job.uuid.hex(),
            'nonce': TX2_NONCE,
        }
        conn.send_error = MagicMock(return_value=None)
        conn.send_result = MagicMock(return_value=None)
        conn.method_submit(params=params, msgid=None)
        conn.send_error.assert_not_called()
        conn.send_result.assert_called_once_with(None, 'ok')

        # Run loop and check that the miner gets a block
        self._run_all_pending_events()
        self.assertTrue(conn.current_job.is_block)
        self.assertEqual(0, conn.current_job.height)

    def test_mining_tx_connection_lost(self):
        conn1 = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn1.current_job)
        self.assertTrue(conn1.current_job.is_block)
        self.assertEqual(0, conn1.current_job.height)

        conn2 = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn2.current_job)
        self.assertTrue(conn2.current_job.is_block)
        self.assertEqual(0, conn2.current_job.height)

        job = TxJob(TX1_DATA)
        ret = self.manager.add_job(job)
        self.assertTrue(ret)
        self.assertFalse(conn1.current_job.is_block)
        self.assertEqual(conn1.current_job.tx_job, job)
        self.assertEqual(conn2.current_job.tx_job, job)

        # Miner 1 disconnects.
        conn1.connection_lost(exc=None)
        self.assertFalse(conn2.current_job.is_block)
        self.assertEqual(conn2.current_job.tx_job, job)

        # Miner 2 disconnects. Tx stays on the queue.
        conn2.connection_lost(exc=None)
        self.assertEqual(deque([job]), self.manager.tx_queue)

        # Miner 3 connects. Tx is sent to the new miner.
        conn3 = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertFalse(conn3.current_job.is_block)
        self.assertEqual(conn3.current_job.tx_job, job)

    def test_token_creation_tx(self):
        conn = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn.current_job)
        self.assertTrue(conn.current_job.is_block)
        self.assertEqual(0, conn.current_job.height)

        self._run_basic_tx_tests(conn, TOKEN_CREATION_TX_DATA, TOKEN_CREATION_TX_NONCE)

        # Run loop and check that the miner gets a block
        self._run_all_pending_events()
        self.assertTrue(conn.current_job.is_block)
        self.assertEqual(0, conn.current_job.height)

    def test_no_miners_at_start(self):
        from txstratum.constants import DEFAULT_EXPECTED_MINING_TIME

        expected_queue_time = 0

        job1 = TxJob(TX1_DATA)
        self.assertTrue(self.manager.add_job(job1))
        self.assertEqual(DEFAULT_EXPECTED_MINING_TIME, job1.expected_mining_time)
        self.assertEqual(0, job1.expected_queue_time)
        self.assertEqual(1, len(self.manager.tx_queue))

        if DEFAULT_EXPECTED_MINING_TIME > 0:
            expected_queue_time += DEFAULT_EXPECTED_MINING_TIME

        job2 = TxJob(TX2_DATA)
        self.assertTrue(self.manager.add_job(job2))
        self.assertEqual(DEFAULT_EXPECTED_MINING_TIME, job2.expected_mining_time)
        self.assertEqual(expected_queue_time, job2.expected_queue_time)
        self.assertEqual(2, len(self.manager.tx_queue))

        if DEFAULT_EXPECTED_MINING_TIME > 0:
            expected_queue_time += DEFAULT_EXPECTED_MINING_TIME

        job3 = TxJob(TOKEN_CREATION_TX_DATA)
        self.assertTrue(self.manager.add_job(job3))
        self.assertEqual(DEFAULT_EXPECTED_MINING_TIME, job3.expected_mining_time)
        self.assertEqual(expected_queue_time, job3.expected_queue_time)
        self.assertEqual(3, len(self.manager.tx_queue))

        self.assertEqual([job1, job2, job3], list(self.manager.tx_queue))

        # First miner connects and receives job1.
        conn1 = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn1.current_job)
        self.assertEqual(job1, conn1.current_job.tx_job)

        # Second miner connects and receives job1.
        conn2 = self._get_ready_miner('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ')
        self.assertIsNotNone(conn2.current_job)
        self.assertEqual(job1, conn2.current_job.tx_job)