Example #1
0
    def __init__(
        self,
        consensus_type: ConsensusType,
        create_block_async_func: Callable[[], Awaitable[Optional[Block]]],
        add_block_async_func: Callable[[Block], Awaitable[None]],
        get_mining_param_func: Callable[[], Dict[str, Any]],
        remote: bool = False,
    ):
        """Mining will happen on a subprocess managed by this class

        create_block_async_func: takes no argument, returns a block (either RootBlock or MinorBlock)
        add_block_async_func: takes a block, add it to chain
        get_mining_param_func: takes no argument, returns the mining-specific params
        """
        self.consensus_type = consensus_type

        self.create_block_async_func = create_block_async_func
        self.add_block_async_func = add_block_async_func
        self.get_mining_param_func = get_mining_param_func
        self.enabled = False
        self.process = None

        self.input_q = AioQueue()  # [(MiningWork, param dict)]
        self.output_q = AioQueue()  # [MiningResult]

        # header hash -> work
        self.work_map = {}  # type: Dict[bytes, Block]

        if not remote and consensus_type != ConsensusType.POW_SIMULATE:
            Logger.warning("Mining locally, could be slow and error-prone")
        # remote miner specific attributes
        self.remote = remote
        self.current_work = None  # type: Optional[Block]
Example #2
0
    def __init__(
        self,
        consensus_type: ConsensusType,
        create_block_async_func: Callable[[], Awaitable[Union[MinorBlock,
                                                              RootBlock]]],
        add_block_async_func: Callable[[Union[MinorBlock, RootBlock]],
                                       Awaitable[None]],
        get_mining_param_func: Callable[[], Dict[str, Any]],
        # TODO: clean this up if confirmed not used
        env,
    ):
        """Mining will happen on a subprocess managed by this class

        create_block_async_func: takes no argument, returns a block (either RootBlock or MinorBlock)
        add_block_async_func: takes a block, add it to chain
        get_mining_param_func: takes no argument, returns the mining-specific params
        """
        if consensus_type == ConsensusType.POW_SIMULATE:
            self.mine_func = Miner.simulate_mine
        elif consensus_type == ConsensusType.POW_ETHASH:
            self.mine_func = Miner.mine_ethash
        elif consensus_type == ConsensusType.POW_SHA3SHA3:
            self.mine_func = Miner.mine_sha3sha3
        else:
            raise ValueError("Consensus? (╯°□°)╯︵ ┻━┻")

        self.create_block_async_func = create_block_async_func
        self.add_block_async_func = add_block_async_func
        self.get_mining_param_func = get_mining_param_func
        self.enabled = False
        self.process = None
        self.env = env

        self.input_q = AioQueue()  # [(block, target_time)]
        self.output_q = AioQueue()  # [block]
Example #3
0
class WebsocketQueue:
    queue: AioQueue

    def __init__(self):
        self.queue = AioQueue()

    async def send(self, message):
        self.queue.put(message)
Example #4
0
    def __init__(
        self,
        consensus_type: ConsensusType,
        create_block_async_func: Callable[..., Awaitable[Optional[Block]]],
        add_block_async_func: Callable[[Block], Awaitable[None]],
        get_mining_param_func: Callable[[], Dict[str, Any]],
        get_header_tip_func: Callable[[], Header],
        remote: bool = False,
        root_signer_private_key: Optional[KeyAPI.PrivateKey] = None,
    ):
        """Mining will happen on a subprocess managed by this class

        create_block_async_func: takes no argument, returns a block (either RootBlock or MinorBlock)
        add_block_async_func: takes a block, add it to chain
        get_mining_param_func: takes no argument, returns the mining-specific params
        """
        self.consensus_type = consensus_type

        self.create_block_async_func = create_block_async_func
        self.add_block_async_func = add_block_async_func
        self.get_mining_param_func = get_mining_param_func
        self.get_header_tip_func = get_header_tip_func
        self.enabled = False
        self.process = None

        self.input_q = AioQueue()  # [(MiningWork, param dict)]
        self.output_q = AioQueue()  # [MiningResult]

        # header hash -> block under work
        # max size (tx max 258 bytes, gas limit 12m) ~= ((12m / 21000) * 258) * 128 = 18mb
        self.work_map = LRUCache(maxsize=128)

        if not remote and consensus_type != ConsensusType.POW_SIMULATE:
            Logger.warning("Mining locally, could be slow and error-prone")
        # remote miner specific attributes
        self.remote = remote
        # coinbase address -> header hash
        # key can be None, meaning default coinbase address from local config
        self.current_works = LRUCache(128)
        self.root_signer_private_key = root_signer_private_key
Example #5
0
    def start(self):
        self._tasks = AioJoinableQueue(maxsize=1)
        self._results = AioQueue()
        self._exit = AioEvent()

        self._converter = ConverterWorker(self._tasks, self._results,
                                          self._exit)
        self._converter.start()

        camera = self._camera.property("mediaObject")

        self._probe = QVideoProbe(self)
        self._probe.setSource(camera)
        self._probe.videoFrameProbed.connect(self.handleFrame)
Example #6
0
    def __init__(
        self,
        consensus_type: ConsensusType,
        create_block_async_func: Callable[[], Awaitable[Union[MinorBlock,
                                                              RootBlock]]],
        add_block_async_func: Callable[[Union[MinorBlock, RootBlock]],
                                       Awaitable[None]],
        get_target_block_time_func: Callable[[], float],
        # TODO: clean this up if confirmed not used
        env,
    ):
        """Mining will happen on a subprocess managed by this class

        create_block_async_func: takes no argument, returns a block (either RootBlock or MinorBlock)
        add_block_async_func: takes a block, add it to chain
        get_target_block_time_func: takes no argument, returns the target block time in second
        """
        # TODO: add other mining functions
        check(consensus_type == ConsensusType.POW_SIMULATE)
        if consensus_type == ConsensusType.POW_SIMULATE:
            self.mine_func = Miner.simulate_mine
        elif consensus_type == ConsensusType.POW_ETHASH:
            self.mine_func = Miner.mine_ethash
        elif consensus_type == ConsensusType.POW_SHA3SHA3:
            self.mine_func = Miner.mine_sha3sha3
        else:
            raise ValueError("Consensus? ( う-´)づ︻╦̵̵̿╤──   \(˚☐˚”)/")

        self.create_block_async_func = create_block_async_func
        self.add_block_async_func = add_block_async_func
        self.get_target_block_time_func = get_target_block_time_func
        self.enabled = False
        self.process = None
        self.env = env

        self.input_q = AioQueue()  # [(block, target_time)]
        self.output_q = AioQueue()  # [block]
Example #7
0
    def __init__(self, config_fn, assert_fn):
        super().__init__()
        self.log = get_logger("TestableProcess")

        self.config_fn = config_fn  # Function to configure object with mocks
        self.assert_fn = assert_fn  # Function to run assertions on inserted mock properties

        self.cmd_q = AioQueue()  # Used to pass commands into blocking object
        self.sig_q = Queue()  # Used to block on .start() and wait for child proc

        self.test_proc = LProcess(target=self._start_test, args=(self.sig_q, self.cmd_q))
        self.log.debug("Starting test")
        self.test_proc.start()

        self.log.debug("waiting for child ready sig")
        rdy = self.sig_q.get()
        self.log.debug("got rdy sig: {}".format(rdy))
    def start(self):
        if self._running:
            return

        if self._camera is None:
            print("error!! no camera!!")
            return

        print("FACERECOGNIZER start")

        self._tasks = AioJoinableQueue(maxsize=1)
        self._results = AioQueue()
        self._exit = AioEvent()
        self._recognizer = FaceRecognizerWorker(self._tasks, self._results, self._exit)
        self._recognizer.start()

        self._probe.videoFrameProbed.connect(self.handleFrame)

        self._running = True
Example #9
0
class Miner:
    def __init__(
        self,
        consensus_type: ConsensusType,
        create_block_async_func: Callable[[], Awaitable[Optional[Block]]],
        add_block_async_func: Callable[[Block], Awaitable[None]],
        get_mining_param_func: Callable[[], Dict[str, Any]],
        remote: bool = False,
    ):
        """Mining will happen on a subprocess managed by this class

        create_block_async_func: takes no argument, returns a block (either RootBlock or MinorBlock)
        add_block_async_func: takes a block, add it to chain
        get_mining_param_func: takes no argument, returns the mining-specific params
        """
        self.consensus_type = consensus_type

        self.create_block_async_func = create_block_async_func
        self.add_block_async_func = add_block_async_func
        self.get_mining_param_func = get_mining_param_func
        self.enabled = False
        self.process = None

        self.input_q = AioQueue()  # [(MiningWork, param dict)]
        self.output_q = AioQueue()  # [MiningResult]

        # header hash -> work
        self.work_map = {}  # type: Dict[bytes, Block]

        if not remote and consensus_type != ConsensusType.POW_SIMULATE:
            Logger.warning("Mining locally, could be slow and error-prone")
        # remote miner specific attributes
        self.remote = remote
        self.current_work = None  # type: Optional[Block]

    def start(self):
        self.enabled = True
        self._mine_new_block_async()

    def is_enabled(self):
        return self.enabled

    def disable(self):
        """Stop the mining process if there is one"""
        if self.enabled and self.process:
            # end the mining process
            self.input_q.put((None, {}))
        self.enabled = False

    def _mine_new_block_async(self):
        async def handle_mined_block():
            while True:
                res = await self.output_q.coro_get()  # type: MiningResult
                if not res:
                    return  # empty result means ending
                # start mining before processing and propagating mined block
                self._mine_new_block_async()
                block = self.work_map[res.header_hash]
                block.header.nonce = res.nonce
                block.header.mixhash = res.mixhash
                del self.work_map[res.header_hash]
                self._track(block)
                try:
                    # FIXME: Root block should include latest minor block headers while it's being mined
                    # This is a hack to get the latest minor block included since testnet does not check difficulty
                    if self.consensus_type == ConsensusType.POW_SIMULATE:
                        block = await self.create_block_async_func()
                        block.header.nonce = random.randint(0, 2**32 - 1)
                        self._track(block)
                        self._log_status(block)
                    await self.add_block_async_func(block)
                except Exception as ex:
                    Logger.error(ex)

        async def mine_new_block():
            """Get a new block and start mining.
            If a mining process has already been started, update the process to mine the new block.
            """
            block = await self.create_block_async_func()
            if not block:
                self.input_q.put((None, {}))
                return
            mining_params = self.get_mining_param_func()
            # handle mining simulation's timing
            if "target_block_time" in mining_params:
                target_block_time = mining_params["target_block_time"]
                mining_params["target_time"] = (
                    block.header.create_time +
                    self._get_block_time(block, target_block_time))
            work = MiningWork(
                block.header.get_hash_for_mining(),
                block.header.height,
                block.header.difficulty,
            )
            self.work_map[work.hash] = block
            if self.process:
                self.input_q.put((work, mining_params))
                return

            self.process = AioProcess(
                target=self._mine_loop,
                args=(
                    self.consensus_type,
                    work,
                    mining_params,
                    self.input_q,
                    self.output_q,
                ),
            )
            self.process.start()
            await handle_mined_block()

        # no-op if enabled or mining remotely
        if not self.enabled or self.remote:
            return None
        return asyncio.ensure_future(mine_new_block())

    async def get_work(self, now=None) -> MiningWork:
        if not self.remote:
            raise ValueError("Should only be used for remote miner")

        if now is None:  # clock open for mock
            now = time.time()
        # 5 sec interval magic number
        if not self.current_work or now - self.current_work.header.create_time > 5:
            block = await self.create_block_async_func()
            if not block:
                raise RuntimeError("Failed to create block")
            self.current_work = block

        header = self.current_work.header
        header_hash = header.get_hash_for_mining()
        # store in memory for future retrieval during work submission
        self.work_map[header_hash] = self.current_work

        # clean up worker map
        # TODO: for now, same param as go-ethereum
        self.work_map = {
            h: b
            for h, b in self.work_map.items()
            if now - b.header.create_time < 7 * 12
        }

        return MiningWork(header_hash, header.height, header.difficulty)

    async def submit_work(self, header_hash: bytes, nonce: int,
                          mixhash: bytes) -> bool:
        if not self.remote:
            raise ValueError("Should only be used for remote miner")

        if header_hash not in self.work_map:
            return False
        block = self.work_map[header_hash]
        header = copy.copy(block.header)
        header.nonce, header.mixhash = nonce, mixhash
        try:
            validate_seal(header, self.consensus_type)
        except ValueError:
            return False

        block.header = header  # actual update
        try:
            await self.add_block_async_func(block)
            del self.work_map[header_hash]
            self.current_work = None
            return True
        except Exception as ex:
            Logger.error(ex)
            return False

    @staticmethod
    def _mine_loop(
        consensus_type: ConsensusType,
        work: MiningWork,
        mining_params: Dict,
        input_q: MultiProcessingQueue,
        output_q: MultiProcessingQueue,
    ):
        consensus_to_mining_algo = {
            ConsensusType.POW_SIMULATE: Simulate,
            ConsensusType.POW_ETHASH: Ethash,
            ConsensusType.POW_SHA3SHA3: DoubleSHA256,
        }
        mining_algo_gen = consensus_to_mining_algo[consensus_type]
        # TODO: maybe add rounds to config json
        rounds = mining_params.get("rounds", 100)
        progress = {}
        # outer loop for mining forever
        while True:
            # empty work means termination
            if not work:
                output_q.put(None)
                return

            mining_algo = mining_algo_gen(work, **mining_params)
            # progress tracking if mining param contains shard info
            if "shard" in mining_params:
                shard = mining_params["shard"]
                # skip blocks with lower height
                if shard in progress and progress[shard] >= work.height:
                    continue
            # inner loop for iterating nonce
            start_nonce = 0
            while True:
                res = mining_algo.mine(start_nonce + 1,
                                       start_nonce + 1 + rounds)
                if res:
                    output_q.put(res)
                    if "shard" in mining_params:
                        progress[mining_params["shard"]] = work.height
                    work, mining_params = input_q.get(block=True)
                    break  # break inner loop to refresh mining params
                # no result for mining, check if new work arrives
                # if yes, discard current work and restart
                try:
                    work, mining_params = input_q.get_nowait()
                    break  # break inner loop to refresh mining params
                except Exception:  # queue empty
                    pass
                # update param and keep mining
                start_nonce += rounds

    @staticmethod
    def _track(block: Block):
        """Post-process block to track block propagation latency"""
        tracking_data = json.loads(block.tracking_data.decode("utf-8"))
        tracking_data["mined"] = time_ms()
        block.tracking_data = json.dumps(tracking_data).encode("utf-8")

    @staticmethod
    def _log_status(block: Block):
        is_root = isinstance(block, RootBlock)
        shard = "R" if is_root else block.header.branch.get_shard_id()
        count = len(block.minor_block_header_list) if is_root else len(
            block.tx_list)
        elapsed = time.time() - block.header.create_time
        Logger.info_every_sec(
            "[{}] {} [{}] ({:.2f}) {}".format(
                shard,
                block.header.height,
                count,
                elapsed,
                block.header.get_hash().hex(),
            ),
            60,
        )

    @staticmethod
    def _get_block_time(block: Block, target_block_time) -> float:
        if isinstance(block, MinorBlock):
            # Adjust the target block time to compensate computation time
            gas_used_ratio = block.meta.evm_gas_used / block.header.evm_gas_limit
            target_block_time = target_block_time * (1 - gas_used_ratio * 0.4)
            Logger.debug("[{}] target block time {:.2f}".format(
                block.header.branch.get_shard_id(), target_block_time))
        return numpy.random.exponential(target_block_time)
Example #10
0
class Miner:
    def __init__(
        self,
        consensus_type: ConsensusType,
        create_block_async_func: Callable[..., Awaitable[Optional[Block]]],
        add_block_async_func: Callable[[Block], Awaitable[None]],
        get_mining_param_func: Callable[[], Dict[str, Any]],
        get_header_tip_func: Callable[[], Header],
        remote: bool = False,
        root_signer_private_key: Optional[KeyAPI.PrivateKey] = None,
    ):
        """Mining will happen on a subprocess managed by this class

        create_block_async_func: takes no argument, returns a block (either RootBlock or MinorBlock)
        add_block_async_func: takes a block, add it to chain
        get_mining_param_func: takes no argument, returns the mining-specific params
        """
        self.consensus_type = consensus_type

        self.create_block_async_func = create_block_async_func
        self.add_block_async_func = add_block_async_func
        self.get_mining_param_func = get_mining_param_func
        self.get_header_tip_func = get_header_tip_func
        self.enabled = False
        self.process = None

        self.input_q = AioQueue()  # [(MiningWork, param dict)]
        self.output_q = AioQueue()  # [MiningResult]

        # header hash -> block under work
        # max size (tx max 258 bytes, gas limit 12m) ~= ((12m / 21000) * 258) * 128 = 18mb
        self.work_map = LRUCache(maxsize=128)

        if not remote and consensus_type != ConsensusType.POW_SIMULATE:
            Logger.warning("Mining locally, could be slow and error-prone")
        # remote miner specific attributes
        self.remote = remote
        # coinbase address -> header hash
        # key can be None, meaning default coinbase address from local config
        self.current_works = LRUCache(128)
        self.root_signer_private_key = root_signer_private_key

    def start(self):
        self.enabled = True
        self._mine_new_block_async()

    def is_enabled(self):
        return self.enabled

    def disable(self):
        """Stop the mining process if there is one"""
        if self.enabled and self.process:
            # end the mining process
            self.input_q.put((None, {}))
        self.enabled = False

    def _mine_new_block_async(self):
        async def handle_mined_block():
            while True:
                res = await self.output_q.coro_get()  # type: MiningResult
                if not res:
                    return  # empty result means ending
                # start mining before processing and propagating mined block
                self._mine_new_block_async()
                block = self.work_map[res.header_hash]
                block.header.nonce = res.nonce
                block.header.mixhash = res.mixhash
                del self.work_map[res.header_hash]
                self._track(block)
                try:
                    # FIXME: Root block should include latest minor block headers while it's being mined
                    # This is a hack to get the latest minor block included since testnet does not check difficulty
                    if self.consensus_type == ConsensusType.POW_SIMULATE:
                        block = await self.create_block_async_func(
                            Address.create_empty_account())
                        block.header.nonce = random.randint(0, 2**32 - 1)
                        self._track(block)
                        self._log_status(block)
                    await self.add_block_async_func(block)
                except Exception:
                    Logger.error_exception()

        async def mine_new_block():
            """Get a new block and start mining.
            If a mining process has already been started, update the process to mine the new block.
            """
            block = await self.create_block_async_func(
                Address.create_empty_account())
            if not block:
                self.input_q.put((None, {}))
                return
            mining_params = self.get_mining_param_func()
            mining_params["consensus_type"] = self.consensus_type
            # handle mining simulation's timing
            if "target_block_time" in mining_params:
                target_block_time = mining_params["target_block_time"]
                mining_params["target_time"] = (
                    block.header.create_time +
                    self._get_block_time(block, target_block_time))
            work = MiningWork(
                block.header.get_hash_for_mining(),
                block.header.height,
                block.header.difficulty,
            )
            self.work_map[work.hash] = block
            if self.process:
                self.input_q.put((work, mining_params))
                return

            self.process = AioProcess(
                target=self.mine_loop,
                args=(work, mining_params, self.input_q, self.output_q),
            )
            self.process.start()
            await handle_mined_block()

        # no-op if enabled or mining remotely
        if not self.enabled or self.remote:
            return None
        return asyncio.ensure_future(mine_new_block())

    async def get_work(self,
                       coinbase_addr: Address,
                       now=None) -> (MiningWork, Block):
        if not self.remote:
            raise ValueError("Should only be used for remote miner")

        if now is None:  # clock open for mock
            now = time.time()

        block = None
        header_hash = self.current_works.get(coinbase_addr)
        if header_hash:
            block = self.work_map.get(header_hash)
        tip_hash = self.get_header_tip_func().get_hash()
        if (not block  # no work cache
                or block.header.hash_prev_block != tip_hash  # cache outdated
                or now - block.header.create_time > 10  # stale
            ):
            block = await self.create_block_async_func(coinbase_addr,
                                                       retry=False)
            if not block:
                raise RuntimeError("Failed to create block")
            header_hash = block.header.get_hash_for_mining()
            self.current_works[coinbase_addr] = header_hash
            self.work_map[header_hash] = block

        header = block.header
        return (
            MiningWork(header_hash, header.height, header.difficulty),
            copy.deepcopy(block),
        )

    async def submit_work(
        self,
        header_hash: bytes,
        nonce: int,
        mixhash: bytes,
        signature: Optional[bytes] = None,
    ) -> bool:
        if not self.remote:
            raise ValueError("Should only be used for remote miner")

        if header_hash not in self.work_map:
            return False
        # this copy is necessary since there might be multiple submissions concurrently
        block = copy.deepcopy(self.work_map[header_hash])
        header = block.header

        # reject if tip updated
        tip_hash = self.get_header_tip_func().get_hash()
        if header.hash_prev_block != tip_hash:
            del self.work_map[header_hash]
            return False

        header.nonce, header.mixhash = nonce, mixhash
        # sign using the root_signer_private_key
        if self.root_signer_private_key and isinstance(block, RootBlock):
            header.sign_with_private_key(self.root_signer_private_key)

        # remote sign as a guardian
        if isinstance(block, RootBlock) and signature is not None:
            header.signature = signature

        try:
            await self.add_block_async_func(block)
            # a previous submission of the same work could have removed the key
            if header_hash in self.work_map:
                del self.work_map[header_hash]
            return True
        except Exception:
            Logger.error_exception()
            return False

    @staticmethod
    def mine_loop(
        work: Optional[MiningWork],
        mining_params: Dict,
        input_q: Queue,
        output_q: Queue,
        debug=False,
    ):
        consensus_to_mining_algo = {
            ConsensusType.POW_SIMULATE: Simulate,
            ConsensusType.POW_ETHASH: Ethash,
            ConsensusType.POW_QKCHASH: Qkchash,
            ConsensusType.POW_DOUBLESHA256: DoubleSHA256,
        }
        progress = {}

        def debug_log(msg: str, prob: float):
            if not debug:
                return
            random.random() < prob and print(msg)

        try:
            # outer loop for mining forever
            while True:
                # empty work means termination
                if not work:
                    output_q.put(None)
                    return

                debug_log("outer mining loop", 0.1)
                consensus_type = mining_params["consensus_type"]
                mining_algo_gen = consensus_to_mining_algo[consensus_type]
                mining_algo = mining_algo_gen(work, **mining_params)
                # progress tracking if mining param contains shard info
                if "full_shard_id" in mining_params:
                    full_shard_id = mining_params["full_shard_id"]
                    # skip blocks with height lower or equal
                    if (full_shard_id in progress
                            and progress[full_shard_id] >= work.height):
                        # get newer work and restart mining
                        debug_log("stale work, try to get new one", 1.0)
                        work, mining_params = input_q.get(block=True)
                        continue

                rounds = mining_params.get("rounds", 100)
                start_nonce = random.randint(0, MAX_NONCE)
                # inner loop for iterating nonce
                while True:
                    if start_nonce > MAX_NONCE:
                        start_nonce = 0
                    end_nonce = min(start_nonce + rounds, MAX_NONCE + 1)
                    res = mining_algo.mine(start_nonce,
                                           end_nonce)  # [start, end)
                    debug_log("one round of mining", 0.01)
                    if res:
                        debug_log("mining success", 1.0)
                        output_q.put(res)
                        if "full_shard_id" in mining_params:
                            progress[
                                mining_params["full_shard_id"]] = work.height
                        work, mining_params = input_q.get(block=True)
                        break  # break inner loop to refresh mining params
                    # no result for mining, check if new work arrives
                    # if yes, discard current work and restart
                    try:
                        work, mining_params = input_q.get_nowait()
                        break  # break inner loop to refresh mining params
                    except QueueEmpty:
                        debug_log("empty queue", 0.1)
                        pass
                    # update param and keep mining
                    start_nonce += rounds
        except:
            from sys import exc_info

            exc_type, exc_obj, exc_trace = exc_info()
            print("exc_type", exc_type)
            print("exc_obj", exc_obj)
            print("exc_trace", exc_trace)

    @staticmethod
    def _track(block: Block):
        """Post-process block to track block propagation latency"""
        tracking_data = json.loads(block.tracking_data.decode("utf-8"))
        tracking_data["mined"] = time_ms()
        block.tracking_data = json.dumps(tracking_data).encode("utf-8")

    @staticmethod
    def _log_status(block: Block):
        is_root = isinstance(block, RootBlock)
        full_shard_id = "R" if is_root else block.header.branch.get_full_shard_id(
        )
        count = len(block.minor_block_header_list) if is_root else len(
            block.tx_list)
        elapsed = time.time() - block.header.create_time
        Logger.info_every_sec(
            "[{}] {} [{}] ({:.2f}) {}".format(
                full_shard_id,
                block.header.height,
                count,
                elapsed,
                block.header.get_hash().hex(),
            ),
            60,
        )

    @staticmethod
    def _get_block_time(block: Block, target_block_time) -> float:
        if isinstance(block, MinorBlock):
            # Adjust the target block time to compensate computation time
            gas_used_ratio = block.meta.evm_gas_used / block.header.evm_gas_limit
            target_block_time = target_block_time * (1 - gas_used_ratio * 0.4)
            Logger.debug("[{}] target block time {:.2f}".format(
                block.header.branch.get_full_shard_id(), target_block_time))
        return numpy.random.exponential(target_block_time)
Example #11
0
class Miner:
    def __init__(
        self,
        consensus_type: ConsensusType,
        create_block_async_func: Callable[[], Awaitable[Block]],
        add_block_async_func: Callable[[Block], Awaitable[None]],
        get_mining_param_func: Callable[[], Dict[str, Any]],
        remote: bool = False,
    ):
        """Mining will happen on a subprocess managed by this class

        create_block_async_func: takes no argument, returns a block (either RootBlock or MinorBlock)
        add_block_async_func: takes a block, add it to chain
        get_mining_param_func: takes no argument, returns the mining-specific params
        """
        self.consensus_type = consensus_type

        self.create_block_async_func = create_block_async_func
        self.add_block_async_func = add_block_async_func
        self.get_mining_param_func = get_mining_param_func
        self.enabled = False
        self.process = None

        self.input_q = AioQueue()  # [(block, param dict)]
        self.output_q = AioQueue()  # [block]

        if not remote and consensus_type != ConsensusType.POW_SIMULATE:
            Logger.warning("Mining locally, could be slow and error-prone")
        # remote miner specific attributes
        self.remote = remote
        self.current_work = None  # type: Optional[Block]
        # header hash -> work, updated only by remote miner get & put
        self.work_map = {}  # type: Dict[bytes, Block]

    def start(self):
        self.enabled = True
        self._mine_new_block_async()

    def is_enabled(self):
        return self.enabled

    def disable(self):
        """Stop the mining process if there is one"""
        if self.enabled and self.process:
            # end the mining process
            self.input_q.put((None, {}))
        self.enabled = False

    def _mine_new_block_async(self):
        async def handle_mined_block(instance: Miner):
            while True:
                block = await instance.output_q.coro_get()
                if not block:
                    return
                # start mining before processing and propagating mined block
                instance._mine_new_block_async()
                try:
                    # Root block should include latest minor block headers while it's being mined
                    # This is a hack to get the latest minor block included since testnet does not check difficulty
                    if instance.consensus_type == ConsensusType.POW_SIMULATE:
                        block = await self.create_block_async_func()
                        Simulate(block, target_block_time=0).post_process_mined_block(
                            block
                        )
                    await instance.add_block_async_func(block)
                except Exception as ex:
                    Logger.error(ex)

        async def mine_new_block(instance: Miner):
            """Get a new block and start mining.
            If a mining process has already been started, update the process to mine the new block.
            """
            block = await instance.create_block_async_func()
            mining_params = instance.get_mining_param_func()
            if instance.process:
                instance.input_q.put((block, mining_params))
                return

            instance.process = AioProcess(
                target=instance._mine_loop,
                args=(
                    instance.consensus_type,
                    block,
                    instance.input_q,
                    instance.output_q,
                    mining_params,
                ),
            )
            instance.process.start()
            await handle_mined_block(instance)

        # no-op if enabled or mining remotely
        if not self.enabled or self.remote:
            return None
        return asyncio.ensure_future(mine_new_block(self))

    async def get_work(self, now=None) -> MiningWork:
        if not self.remote:
            raise ValueError("Should only be used for remote miner")

        if now is None:  # clock open for mock
            now = time.time()
        # 5 sec interval magic number
        if not self.current_work or now - self.current_work.header.create_time > 5:
            block = await self.create_block_async_func()
            self.current_work = block

        header = self.current_work.header
        header_hash = header.get_hash_for_mining()
        # store in memory for future retrieval during work submission
        self.work_map[header_hash] = self.current_work

        # clean up worker map
        # TODO: for now, same param as go-ethereum
        self.work_map = {
            h: b
            for h, b in self.work_map.items()
            if now - b.header.create_time < 7 * 12
        }

        return MiningWork(header_hash, header.height, header.difficulty)

    async def submit_work(self, header_hash: bytes, nonce: int, mixhash: bytes) -> bool:
        if not self.remote:
            raise ValueError("Should only be used for remote miner")

        if header_hash not in self.work_map:
            return False
        block = self.work_map[header_hash]
        header = copy.copy(block.header)
        header.nonce, header.mixhash = nonce, mixhash
        try:
            validate_seal(header, self.consensus_type)
        except ValueError:
            return False

        block.header = header  # actual update
        try:
            await self.add_block_async_func(block)
            del self.work_map[header_hash]
            self.current_work = None
            return True
        except Exception as ex:
            Logger.error(ex)
            return False

    @staticmethod
    def _mine_loop(
        consensus_type: ConsensusType,
        block: Block,
        input_q: MultiProcessingQueue,
        output_q: MultiProcessingQueue,
        mining_params: Dict,
    ):
        consensus_to_mining_algo = {
            ConsensusType.POW_SIMULATE: Simulate,
            ConsensusType.POW_ETHASH: Ethash,
            ConsensusType.POW_SHA3SHA3: DoubleSHA256,
        }
        mining_algo_gen = consensus_to_mining_algo[consensus_type]
        # TODO: maybe add rounds to config json
        rounds = mining_params.get("rounds", 100)
        # outer loop for mining forever
        while True:
            # `None` block means termination
            if not block:
                output_q.put(None)
                return

            start_nonce = 0
            mining_algo = mining_algo_gen(block, **mining_params)
            # inner loop for iterating nonce
            while True:
                found = mining_algo.mine(start_nonce + 1, start_nonce + 1 + rounds)
                if found:
                    mining_algo.post_process_mined_block(block)
                    output_q.put(block)
                    block, _ = input_q.get(block=True)  # blocking
                    break  # break inner loop to refresh mining params
                # check if new block arrives. if yes, discard current progress and restart
                try:
                    block, _ = input_q.get_nowait()
                    break  # break inner loop to refresh mining params
                except Exception:  # queue empty
                    pass
                # update param and keep mining
                start_nonce += rounds
Example #12
0
import motor
from aioprocessing import AioQueue

from backend import db_validation
from backend.docker_env import Docker
from backend.engine.workers import workers
from backend.ng import NG
from backend.server import Webserver
import asyncio
import argparse

if __name__ == "__main__":
    process_queue = AioQueue()

    parser = argparse.ArgumentParser()
    parser.add_argument("-d",
                        "--dev",
                        help="Set the application in developer mode.",
                        action="store_true",
                        default=False)
    args = parser.parse_args()

    debug = False
    """Start angular dev mode."""
    angular = NG()
    if args.dev:
        debug = True
        angular.daemon = True
        angular.start()
    """Start docker services."""
    x = Docker()
Example #13
0
 def __init__(self, configs):
     super().__init__()
     self.configs = configs
     self.input_q = AioQueue()
     self.output_q = AioQueue()
     self.process = None
Example #14
0
class ExternalMiner(threading.Thread):
    """One external miner could handles multiple shards."""

    def __init__(self, configs):
        super().__init__()
        self.configs = configs
        self.input_q = AioQueue()
        self.output_q = AioQueue()
        self.process = None

    def run(self):
        work_map = {}  # type: Dict[bytes, Tuple[MiningWork, int]]

        # start the thread to get work
        def get_work():
            # hash -> shard
            nonlocal work_map, self
            # shard -> work
            existing_work = {}  # type: Dict[int, MiningWork]
            while True:
                for config in self.configs:
                    shard_id = config["shard_id"]
                    try:
                        work = get_work_rpc(shard_id)
                    except Exception:
                        # ignore network errors and try next one
                        logger.error("Failed to get work")
                        continue
                    # skip duplicate work
                    if (
                        shard_id in existing_work
                        and existing_work[shard_id].hash == work.hash
                    ):
                        continue
                    mining_params = {
                        "consensus_type": config["consensus_type"],
                        "shard": shard_id,
                    }
                    if self.process:
                        self.input_q.put((work, mining_params))
                        logger.info(
                            "Pushed work to %s height %d"
                            % (repr_shard(shard_id), work.height)
                        )
                    else:
                        # start the process to mine
                        self.process = AioProcess(
                            target=Miner.mine_loop,
                            args=(work, mining_params, self.input_q, self.output_q),
                        )
                        self.process.start()
                        logger.info(
                            "Started mining process for %s" % repr_shard(shard_id)
                        )
                    # bookkeeping
                    existing_work[shard_id] = work
                    work_map[work.hash] = (work, shard_id)
                # random sleep 1~2 secs
                time.sleep(random.uniform(1.0, 2.0))

        get_work_thread = threading.Thread(target=get_work)
        get_work_thread.start()

        # the current thread handles the work submission
        while True:
            res = self.output_q.get(block=True)  # type: MiningResult
            work, shard_id = work_map[res.header_hash]
            while True:
                try:
                    success = submit_work_rpc(shard_id, res)
                    break
                except Exception:
                    logger.error("Failed to submit work, backing off...")
                    time.sleep(0.5)

            logger.info(
                "Mining result submission result: %s for %s height %d"
                % (
                    "success" if success else "failure",
                    repr_shard(shard_id),
                    work.height,
                )
            )
            del work_map[res.header_hash]  # clear bookkeeping
Example #15
0
 def __init__(self):
     self.queue = AioQueue()