Ejemplo n.º 1
0
 def __init__(
     self,
     name: str,
     env: simpy.Environment,
     bus: EventBus,
     diff_1_target: int,
     protocol_type: DownstreamConnectionProcessor,
     device_information: dict,
     simulate_luck=True,
     *args,
     **kwargs
 ):
     self.name = name
     self.env = env
     self.bus = bus
     self.diff_1_target = diff_1_target
     self.protocol_type = protocol_type
     self.device_information = device_information
     self.connection_processor = None
     self.work_meter = HashrateMeter(env)
     self.mine_proc = None
     self.job_uid = None
     self.share_diff = None
     self.recv_loop_process = None
     self.is_mining = True
     self.simulate_luck = simulate_luck
Ejemplo n.º 2
0
    def __init__(
        self,
        name: str,
        env: simpy.Environment,
        bus: EventBus,
        protocol_type: UpstreamConnectionProcessor,
        default_target: coins.Target,
        extranonce2_size: int = 8,
        avg_pool_block_time: float = 60,
        enable_vardiff: bool = False,
        desired_submits_per_sec: float = 0.3,
        simulate_luck: bool = True,
    ):
        """

        :type protocol_type:
        """
        self.name = name
        self.env = env
        self.bus = bus
        self.default_target = default_target
        self.extranonce2_size = extranonce2_size
        self.avg_pool_block_time = avg_pool_block_time

        # Prepare initial prevhash for the very first
        self.__generate_new_prev_hash()
        # Per connection message processors
        self.connection_processors = dict()
        self.connection_processor_clz = protocol_type

        self.pow_update_process = env.process(self.__pow_update())

        self.meter_accepted = HashrateMeter(self.env)
        self.meter_rejected_stale = HashrateMeter(self.env)
        self.meter_process = env.process(self.__pool_speed_meter())
        self.enable_vardiff = enable_vardiff
        self.desired_submits_per_sec = desired_submits_per_sec
        self.simulate_luck = simulate_luck

        self.extra_meters = []

        self.accepted_submits = 0
        self.stale_submits = 0
        self.rejected_submits = 0

        self.accepted_shares = 0
        self.stale_shares = 0
Ejemplo n.º 3
0
    def __init__(
        self,
        name: str,
        env: simpy.Environment,
        bus: EventBus,
        translation_type: UpstreamConnectionProcessor,
        upstream_connection_factory: ConnectionFactory,
        upstream_node: AcceptingConnection,
        default_target: coins.Target,
        extranonce2_size: int = 8,
    ):
        """

        :param translation_type: object for handling incoming downstream
        connections (requires an UpstreamConnectionProcessor as we are handling
        incoming connections)
        """
        self.name = name
        self.env = env
        self.bus = bus
        self.default_target = default_target
        self.extranonce2_size = extranonce2_size

        # Per connection message processors
        self.connection_processors = dict()
        self.connection_processor_clz = translation_type

        self.upstream_node = upstream_node
        self.upstream_connection_factory = upstream_connection_factory

        self.meter_accepted = HashrateMeter(self.env)
        self.meter_rejected_stale = HashrateMeter(self.env)
        self.meter_process = env.process(self.__pool_speed_meter())

        self.accepted_submits = 0
        self.stale_submits = 0
        self.rejected_submits = 0

        self.accepted_shares = 0
        self.stale_shares = 0
Ejemplo n.º 4
0
class Miner(object):
    def __init__(
        self,
        name: str,
        env: simpy.Environment,
        bus: EventBus,
        diff_1_target: int,
        protocol_type: DownstreamConnectionProcessor,
        device_information: dict,
        simulate_luck=True,
        *args,
        **kwargs
    ):
        self.name = name
        self.env = env
        self.bus = bus
        self.diff_1_target = diff_1_target
        self.protocol_type = protocol_type
        self.device_information = device_information
        self.connection_processor = None
        self.work_meter = HashrateMeter(env)
        self.mine_proc = None
        self.job_uid = None
        self.share_diff = None
        self.recv_loop_process = None
        self.is_mining = True
        self.simulate_luck = simulate_luck

    def get_actual_speed(self):
        return self.device_information.get('speed_ghps') if self.is_mining else 0

    def mine(self, job: MiningJob):
        share_diff = job.diff_target.to_difficulty()
        avg_time = share_diff * 4.294967296 / self.device_information.get('speed_ghps')

        # Report the current hashrate at the beginning when of mining
        self.__emit_hashrate_msg_on_bus(job, avg_time)

        while True:
            try:
                yield self.env.timeout(
                    np.random.exponential(avg_time) if self.simulate_luck else avg_time
                )
            except simpy.Interrupt:
                self.__emit_aux_msg_on_bus('Mining aborted (external signal)')
                break

            # To simulate miner failures we can disable mining
            if self.is_mining:
                self.work_meter.measure(share_diff)
                self.__emit_hashrate_msg_on_bus(job, avg_time)
                self.__emit_aux_msg_on_bus('solution found for job {}'.format(job.uid))

                self.connection_processor.submit_mining_solution(job)

    def connect_to_pool(self, connection: Connection, target):
        assert self.connection_processor is None, 'BUG: miner is already connected'
        connection.connect_to(target)

        self.connection_processor = self.protocol_type(self, connection)
        self.__emit_aux_msg_on_bus('Connecting to pool {}'.format(target.name))

    def disconnect(self):
        self.__emit_aux_msg_on_bus('Disconnecting from pool')
        if self.mine_proc:
            self.mine_proc.interrupt()
        # Mining is shutdown, terminate any protocol message processing
        self.connection_processor.terminate()
        self.connection_processor.disconnect()
        self.connection_processor = None

    def new_mining_session(self, diff_target: coins.Target):
        """Creates a new mining session"""
        session = MiningSession(
            name=self.name,
            env=self.env,
            bus=self.bus,
            # TODO remove once the backlinks are not needed
            owner=None,
            diff_target=diff_target,
            enable_vardiff=False,
        )
        self.__emit_aux_msg_on_bus('NEW MINING SESSION ()'.format(session))
        return session

    def mine_on_new_job(self, job: MiningJob, flush_any_pending_work=True):
        """Start working on a new job

         TODO implement more advanced flush policy handling (e.g. wait for the current
          job to finish if flush_flush_any_pending_work is not required)
        """
        # Interrupt the mining process for now
        if self.mine_proc is not None:
            self.mine_proc.interrupt()
        # Restart the process with a new job
        self.mine_proc = self.env.process(self.mine(job))

    def set_is_mining(self, is_mining):
        self.is_mining = is_mining

    def __emit_aux_msg_on_bus(self, msg: str):
        self.bus.emit(
            self.name,
            self.env.now,
            self.connection_processor.connection.uid
            if self.connection_processor
            else None,
            msg,
        )

    def __emit_hashrate_msg_on_bus(self, job: MiningJob, avg_share_time):
        """Reports hashrate statistics on the message bus

        :param job: current job that is being mined
        :return:
        """
        self.__emit_aux_msg_on_bus(
            'mining with diff {} | speed {} Gh/s | avg share time {} | job uid {}'.format(
                job.diff_target.to_difficulty(),
                self.work_meter.get_speed(),
                avg_share_time,
                job.uid,
            )
        )
Ejemplo n.º 5
0
class Proxy(AcceptingConnection):
    """Represents a generic proxy for translating of forwarding a protocol.

    The pool keeps statistics about:

    - accepted submits and shares: submit count and difficulty sum (shares) for valid
    solutions
    - stale submits and shares: submit count and difficulty sum (shares) for solutions
    that have been sent after new block is found
    - rejected submits: submit count of invalid submit attempts that don't refer any
    particular job
    """

    meter_period = 60

    def __init__(
        self,
        name: str,
        env: simpy.Environment,
        bus: EventBus,
        translation_type: UpstreamConnectionProcessor,
        upstream_connection_factory: ConnectionFactory,
        upstream_node: AcceptingConnection,
        default_target: coins.Target,
        extranonce2_size: int = 8,
    ):
        """

        :param translation_type: object for handling incoming downstream
        connections (requires an UpstreamConnectionProcessor as we are handling
        incoming connections)
        """
        self.name = name
        self.env = env
        self.bus = bus
        self.default_target = default_target
        self.extranonce2_size = extranonce2_size

        # Per connection message processors
        self.connection_processors = dict()
        self.connection_processor_clz = translation_type

        self.upstream_node = upstream_node
        self.upstream_connection_factory = upstream_connection_factory

        self.meter_accepted = HashrateMeter(self.env)
        self.meter_rejected_stale = HashrateMeter(self.env)
        self.meter_process = env.process(self.__pool_speed_meter())

        self.accepted_submits = 0
        self.stale_submits = 0
        self.rejected_submits = 0

        self.accepted_shares = 0
        self.stale_shares = 0

    def reset_stats(self):
        self.accepted_submits = 0
        self.stale_submits = 0
        self.rejected_submits = 0
        self.accepted_shares = 0
        self.stale_shares = 0

    def connect_in(self, connection: Connection):
        if connection.port != 'stratum':
            raise ValueError('{} port is not supported'.format(
                connection.port))
        # Build message processor for the new connection
        self.connection_processors[
            connection.uid] = self.connection_processor_clz(self, connection)

    def disconnect(self, connection: Connection):
        if connection.uid not in self.connection_processors:
            return
        self.connection_processors[connection.uid].terminate()
        del self.connection_processors[connection.uid]

    def new_mining_session(self, owner, on_vardiff_change, clz=MiningSession):
        """Creates a new mining session"""
        session = clz(
            name=self.name,
            env=self.env,
            bus=self.bus,
            owner=owner,
            diff_target=self.default_target,
            enable_vardiff=self.enable_vardiff,
            vardiff_time_window=self.meter_accepted.window_size,
            vardiff_desired_submits_per_sec=self.desired_submits_per_sec,
            on_vardiff_change=on_vardiff_change,
        )
        self.__emit_aux_msg_on_bus('NEW MINING SESSION ()'.format(session))

        return session

    def account_accepted_shares(self, diff_target: coins.Target):
        self.accepted_submits += 1
        self.accepted_shares += diff_target.to_difficulty()
        self.meter_accepted.measure(diff_target.to_difficulty())

    def account_stale_shares(self, diff_target: coins.Target):
        self.stale_submits += 1
        self.stale_shares += diff_target.to_difficulty()
        self.meter_rejected_stale.measure(diff_target.to_difficulty())

    def account_rejected_submits(self):
        self.rejected_submits += 1

    def process_submit(self, submit_job_uid, session: MiningSession, on_accept,
                       on_reject):
        if session.job_registry.contains(submit_job_uid):
            diff_target = session.job_registry.get_job_diff_target(
                submit_job_uid)
            # Global accounting
            self.account_accepted_shares(diff_target)
            # Per session accounting
            session.account_diff_shares(diff_target.to_difficulty())
            on_accept(diff_target)
        elif session.job_registry.contains_invalid(submit_job_uid):
            diff_target = session.job_registry.get_invalid_job_diff_target(
                submit_job_uid)
            self.account_stale_shares(diff_target)
            on_reject(diff_target)
        else:
            self.account_rejected_submits()
            on_reject(None)

    def __pool_speed_meter(self):
        while True:
            yield self.env.timeout(self.meter_period)
            speed = self.meter_accepted.get_speed()
            submit_speed = self.meter_accepted.get_submit_per_secs()
            if speed is None or submit_speed is None:
                self.__emit_aux_msg_on_bus('SPEED: N/A Gh/s, N/A submits/s')
            else:
                self.__emit_aux_msg_on_bus(
                    'SPEED: {0:.2f} Gh/s, {1:.4f} submits/s'.format(
                        speed, submit_speed))

    def __emit_aux_msg_on_bus(self, msg):
        self.bus.emit(self.name, self.env.now, None, msg)
Ejemplo n.º 6
0
class Pool(AcceptingConnection):
    """Represents a generic mining pool.

    It handles connections and delegates work to actual protocol specific object

    The pool keeps statistics about:

    - accepted submits and shares: submit count and difficulty sum (shares) for valid
    solutions
    - stale submits and shares: submit count and difficulty sum (shares) for solutions
    that have been sent after new block is found
    - rejected submits: submit count of invalid submit attempts that don't refer any
    particular job
    """

    meter_period = 60

    def __init__(
        self,
        name: str,
        env: simpy.Environment,
        bus: EventBus,
        protocol_type: UpstreamConnectionProcessor,
        default_target: coins.Target,
        extranonce2_size: int = 8,
        avg_pool_block_time: float = 60,
        enable_vardiff: bool = False,
        desired_submits_per_sec: float = 0.3,
        simulate_luck: bool = True,
    ):
        """

        :type protocol_type:
        """
        self.name = name
        self.env = env
        self.bus = bus
        self.default_target = default_target
        self.extranonce2_size = extranonce2_size
        self.avg_pool_block_time = avg_pool_block_time

        # Prepare initial prevhash for the very first
        self.__generate_new_prev_hash()
        # Per connection message processors
        self.connection_processors = dict()
        self.connection_processor_clz = protocol_type

        self.pow_update_process = env.process(self.__pow_update())

        self.meter_accepted = HashrateMeter(self.env)
        self.meter_rejected_stale = HashrateMeter(self.env)
        self.meter_process = env.process(self.__pool_speed_meter())
        self.enable_vardiff = enable_vardiff
        self.desired_submits_per_sec = desired_submits_per_sec
        self.simulate_luck = simulate_luck

        self.extra_meters = []

        self.accepted_submits = 0
        self.stale_submits = 0
        self.rejected_submits = 0

        self.accepted_shares = 0
        self.stale_shares = 0

    def reset_stats(self):
        self.accepted_submits = 0
        self.stale_submits = 0
        self.rejected_submits = 0
        self.accepted_shares = 0
        self.stale_shares = 0

    def connect_in(self, connection: Connection):
        if connection.port != 'stratum':
            raise ValueError('{} port is not supported'.format(
                connection.port))
        # Build message processor for the new connection
        self.connection_processors[
            connection.uid] = self.connection_processor_clz(self, connection)

    def disconnect(self, connection: Connection):
        if connection.uid not in self.connection_processors:
            return
        self.connection_processors[connection.uid].terminate()
        del self.connection_processors[connection.uid]

    def new_mining_session(self, owner, on_vardiff_change, clz=MiningSession):
        """Creates a new mining session"""
        session = clz(
            name=self.name,
            env=self.env,
            bus=self.bus,
            owner=owner,
            diff_target=self.default_target,
            enable_vardiff=self.enable_vardiff,
            vardiff_time_window=self.meter_accepted.window_size,
            vardiff_desired_submits_per_sec=self.desired_submits_per_sec,
            on_vardiff_change=on_vardiff_change,
        )
        self.__emit_aux_msg_on_bus('NEW MINING SESSION ()'.format(session))

        return session

    def add_extra_meter(self, meter: HashrateMeter):
        self.extra_meters.append(meter)

    def account_accepted_shares(self, diff_target: coins.Target):
        self.accepted_submits += 1
        self.accepted_shares += diff_target.to_difficulty()
        self.meter_accepted.measure(diff_target.to_difficulty())

    def account_stale_shares(self, diff_target: coins.Target):
        self.stale_submits += 1
        self.stale_shares += diff_target.to_difficulty()
        self.meter_rejected_stale.measure(diff_target.to_difficulty())

    def account_rejected_submits(self):
        self.rejected_submits += 1

    def process_submit(self, submit_job_uid, session: MiningSession, on_accept,
                       on_reject):
        if session.job_registry.contains(submit_job_uid):
            diff_target = session.job_registry.get_job_diff_target(
                submit_job_uid)
            # Global accounting
            self.account_accepted_shares(diff_target)
            # Per session accounting
            session.account_diff_shares(diff_target.to_difficulty())
            on_accept(diff_target)
        elif session.job_registry.contains_invalid(submit_job_uid):
            diff_target = session.job_registry.get_invalid_job_diff_target(
                submit_job_uid)
            self.account_stale_shares(diff_target)
            on_reject(diff_target)
        else:
            self.account_rejected_submits()
            on_reject(None)

    def __pow_update(self):
        """This process simulates finding new blocks based on pool's hashrate"""
        while True:
            # simulate pool block time using exponential distribution
            yield self.env.timeout(
                np.random.exponential(self.avg_pool_block_time) if self.
                simulate_luck else self.avg_pool_block_time)
            # Simulate the new block hash by calculating sha256 of current time
            self.__generate_new_prev_hash()

            self.__emit_aux_msg_on_bus('NEW_BLOCK: {}'.format(
                self.prev_hash.hex()))

            for connection_processor in self.connection_processors.values():
                connection_processor.on_new_block()

    def __generate_new_prev_hash(self):
        """Generates a new prevhash based on current time.
        """
        # TODO: this is not very precise as to events that would trigger this method in
        #  the same second would yield the same prev hash value,  we should consider
        #  specifying prev hash as a simple sequence number
        self.prev_hash = hashlib.sha256(bytes(int(self.env.now))).digest()

    def __pool_speed_meter(self):
        while True:
            yield self.env.timeout(self.meter_period)
            speed = self.meter_accepted.get_speed()
            submit_speed = self.meter_accepted.get_submit_per_secs()
            if speed is None or submit_speed is None:
                self.__emit_aux_msg_on_bus('SPEED: N/A Gh/s, N/A submits/s')
            else:
                self.__emit_aux_msg_on_bus(
                    'SPEED: {0:.2f} Gh/s, {1:.4f} submits/s'.format(
                        speed, submit_speed))

    def __emit_aux_msg_on_bus(self, msg):
        self.bus.emit(self.name, self.env.now, None, msg)
Ejemplo n.º 7
0
 def run(self):
     """Explicit activation starts any simulation processes associated with the session"""
     self.meter = HashrateMeter(self.env)
     if self.enable_vardiff:
         self.vardiff_process = self.env.process(self.__vardiff_loop())
Ejemplo n.º 8
0
class MiningSession:
    """Represents a mining session that can adjust its difficulty target"""

    min_factor = 0.25
    max_factor = 4

    def __init__(
        self,
        name: str,
        env: simpy.Environment,
        bus: EventBus,
        owner,
        diff_target: coins.Target,
        enable_vardiff,
        vardiff_time_window=None,
        vardiff_desired_submits_per_sec=None,
        on_vardiff_change=None,
    ):
        """
        """
        self.name = name
        self.env = env
        self.bus = bus
        self.owner = owner
        self.curr_diff_target = diff_target
        self.enable_vardiff = enable_vardiff
        self.meter = None
        self.vardiff_process = None
        self.vardiff_time_window_size = vardiff_time_window
        self.vardiff_desired_submits_per_sec = vardiff_desired_submits_per_sec
        self.on_vardiff_change = on_vardiff_change

        self.job_registry = MiningJobRegistry()

    @property
    def curr_target(self):
        """Derives target from current difficulty on the session"""
        return self.curr_diff_target

    def set_target(self, target):
        self.curr_diff_target = target

    def new_mining_job(self, job_uid=None):
        """Generates a new job using current session's target"""
        return self.job_registry.new_mining_job(self.curr_target, job_uid)

    def run(self):
        """Explicit activation starts any simulation processes associated with the session"""
        self.meter = HashrateMeter(self.env)
        if self.enable_vardiff:
            self.vardiff_process = self.env.process(self.__vardiff_loop())

    def account_diff_shares(self, diff: int):
        assert (
            self.meter
            is not None), 'BUG: session not running yet, cannot account shares'
        self.meter.measure(diff)

    def terminate(self):
        """Complete shutdown of the session"""
        self.meter.terminate()
        if self.enable_vardiff:
            self.vardiff_process.interrupt()

    def __vardiff_loop(self):
        while True:
            try:
                submits_per_sec = self.meter.get_submit_per_secs()
                if submits_per_sec is None:
                    # no accepted shares, we will halve the diff
                    factor = 0.5
                else:
                    factor = submits_per_sec / self.vardiff_desired_submits_per_sec
                if factor < self.min_factor:
                    factor = self.min_factor
                elif factor > self.max_factor:
                    factor = self.max_factor
                self.curr_diff_target.div_by_factor(factor)
                self.__emit_aux_msg_on_bus('DIFF_UPDATE(target={})'.format(
                    self.curr_diff_target)),
                self.on_vardiff_change(self)
                yield self.env.timeout(self.vardiff_time_window_size)
            except simpy.Interrupt:
                break

    def __emit_aux_msg_on_bus(self, msg):
        self.bus.emit(self.name, self.env.now, self.owner, msg)