<reponame>lukasdornak/coinminer
import hashlib, json, random, os, re, requests, time
import multiprocessing as mp

from operator import itemgetter
from datetime import datetime

import settings


class StrategyManager:
    """ Class for managing Strategies """
    def __init__(self, stats_length=None):
        if stats_length is not None:
            self.stats_length = stats_length

    SUCCESS = 's'
    TIMED_OUT = 't'
    UNSUCCESS = 'u'

    CODE_CHOICES = [SUCCESS, TIMED_OUT, UNSUCCESS]

    strategies = []

    stats_length = settings.STATS_LENGTH

    _current_strategy = None

    def register_strategy(self, func, stats=''):
        """ Register strategy of selecting transactions.

        func is strategy function described in strategies.py
        stats is optional string of statistic codes to be set.
        """
        self.strategies.append({
            'func': func,
            'stats': stats,
            'rate': 1,
            'times': [],
            'avg_time': 0
        })

    def register_strategies(self, func_list, stats_dict=None):
        """ Register multiple strategies at once. """
        if stats_dict is None:
            stats_dict = {}
        for func in func_list:
            self.register_strategy(func, stats_dict.get(func.__name__, ''))

    def set_stats(self, code, time=None):
        """ Add new statistic record for currently used strategy

        code must be one of these:
        StrategyManager.SUCCESS : when new block has been successfully added to block chain
        StrategyManager.TIMED_ONE : when mining workers has been stopped due to transaction expiration.
        StrategyManager.UNSUCCESS : when mining workers has been stopped due to change of block chain state
                                    or when server refused block for any reason
        time should be passed only if block has been mined out and sent regardless server's answer.

        TO DO: Amount of mined out coins should be also recorded
        """
        cs = self._current_strategy
        if cs is not None and code in self.CODE_CHOICES:
            cs['stats'] += code                         # add new stat record
            if len(cs['stats']) >= self.stats_length:   # if len of stats exceeds stats_length
                cs['stats'] = cs['stats'][1:]           # drop out oldest stat from stats
            # calculate strategy rating
            cs['rate'] = cs['stats'].count(self.SUCCESS) / len(cs['stats'])

            if time is not None:
                cs['times'].append(time)                    # add new time record
                if len(cs['times']) >= self.stats_length:   # if len of times exceeds stats_length
                    cs['times'] = cs['times'][1:]           # drop out oldest time from times
                # calculate average time
                cs['avg_time'] = sum(cs['times']) / len(cs['times'])

    def select_strategy(self):
        """ Select strategy to use in next mining loop.

        iterate through strategies sorted by rating.
        Choose one with probability equal to it's rating.
        If no strategy chosen, take random one.

        TO DO:
        Strategy is selected based on success rating only,
        but it should also take into account average
        amount of coins per block and also average time
        spent by mining a block.
        """
        for strategy in sorted(self.strategies, key=itemgetter('rate'), reverse=True):
            if random.random() <= strategy['rate']:
                return strategy
        return random.choice(self.strategies)

    def use_strategy(self, transaction_list):
        """ Apply strategy to given transaction_list and return it's result. """
        self._current_strategy = self.select_strategy()
        stats = self._current_strategy.copy()
        stats.pop('func')
        return self._current_strategy['func'](transaction_list, stats)

    def print_stats(self, difficulty):
        """ Print statistics in nice table. """
        mined = get_actuals().get(settings.MINER)
        stats = [['strategy name', 'success', 'timed out', 'unsuccess', 'rate [%]', 'avg time [s]']]
        stats += [[strategy['func'].__name__,
                   strategy['stats'].count(self.SUCCESS),
                   strategy['stats'].count(self.TIMED_OUT),
                   strategy['stats'].count(self.UNSUCCESS),
                   round(strategy['rate'] * 100, 1),
                   round(strategy['avg_time'],1)] for strategy in self.strategies]
        os.system('clear')
        print(f"Miner name: { settings.MINER }\n"
              f"Mined out coins: { mined }\n"
              f"Difficulty: {difficulty}\n"
              f"Curent strategy: { self._current_strategy['func'].__name__ }")
        for row in stats:
            print("{: <20} {: >14} {: >14} {: >14} {: >14} {: >14}".format(*row))


def get_state():
    url = settings.HOST + '/api/coingame/state'
    resp = requests.get(url=url)
    return json.loads(resp.text)


def get_actuals():
    url = settings.HOST + '/api/coingame/actuals'
    resp = requests.get(url=url)
    return json.loads(resp.text)


def parse_transaction(transaction):
    """ Convert transaction string to dictionary. """
    transaction = dict(map(lambda t: t.split(': '), re.split('\n(?! )', transaction)[:-1]))
    transaction['Data'] = transaction['Data'].replace('\n', '\n    ')
    transaction['Id'] = int(transaction['Id'])
    transaction['Fee'] = float(transaction['Fee'])
    transaction['ValidTo'] = datetime.strptime(transaction['ValidTo'], '%Y-%m-%d %H:%M:%S.%f')
    return transaction


def get_transactions():
    url = settings.HOST + '/api/coingame/txpool'
    resp = requests.get(url)
    transaction_list = resp.text.split('--- !Transaction\n')[1:]
    return list(map(parse_transaction, transaction_list))


def get_valid_to(transaction_list):
    """ Get deadline for selected transactions. """
    return sorted(transaction_list, key=itemgetter('ValidTo'))[:1][0]['ValidTo']


def create_block_template(state, transaction_list):
    """ Create block template with unique timestamp. """
    block_template = f"""--- !Hash
Digest: '{ state.get('Digest') }'
--- !Block
Timestamp: { datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ') }
Difficulty: { state.get('Difficulty') }
Nonce: <nonce>
Miner: { settings.MINER }
Transactions:
  - !Transaction
    Fee: { state.get('Fee') }
"""
    for transaction in transaction_list:
        block_template += f"""  - !Transaction
    Id: { transaction['Id'] }
    Fee: { transaction['Fee'] }
    Data: { transaction['Data'] }
"""
    return block_template


ZEROS = {
    '1': 3,
    '2': 2,
    '3': 2,
    '4': 1,
    '5': 1,
    '6': 1,
    '7': 1,
    '8': 0,
    '9': 0,
    'a': 0,
    'b': 0,
    'c': 0,
    'd': 0,
    'e': 0,
    'f': 0,
}


def calculate_difficulty(block):
    blockbytes = block.encode('utf-8')
    hashobj = hashlib.sha384()
    hashobj.update(blockbytes)
    hashobj.update(blockbytes)

    difficulty = 0
    for h in hashobj.hexdigest():
        if h != '0':
            return difficulty + ZEROS[h]
        difficulty += 4
    return difficulty


def search_for_block(block_template, difficulty):
    nonce = 1
    while True:
        block = block_template.replace('<nonce>', str(nonce), 1)
        if calculate_difficulty(block) >= difficulty:
            return block
        nonce += 1


def worker_loop(block_template, difficulty, blocks):
    block = search_for_block(block_template, difficulty)
    blocks.append(block)


def push_block(block):
    url = settings.HOST + '/api/coingame'
    resp = requests.put(url=url, data=block)
    if json.loads(resp.text).get('result') == 'OK':
        return StrategyManager.SUCCESS
    return StrategyManager.UNSUCCESS


def main_loop(strategy_manager):
    state = get_state()
    # get transactions and apply strategic select
    transaction_list = strategy_manager.use_strategy(get_transactions())
    valid_to = get_valid_to(transaction_list)

    with mp.Manager() as manager:
        blocks = manager.list()  # Space for handing in blocks
        workers = []
        for i in range(settings.WORKERS):
            # create workers with unique timestamp in template
            block_template = create_block_template(state, transaction_list)
            workers.append(mp.Process(target=worker_loop, args=[block_template, state.get('Difficulty'), blocks]))
        start_time = loop_time = time.time()
        for w in workers:
            w.start()
        while not len(blocks):              # while no block received from any worker yet
            if time.time() - loop_time >= 1:    # every second
                if state != get_state():        # check if state has not changed
                    for w in workers:
                        w.terminate()
                    strategy_manager.set_stats(code=strategy_manager.UNSUCCESS)
                    strategy_manager.print_stats(state.get('Difficulty'))
                    return 0
                loop_time = time.time()
            if datetime.utcnow() >= valid_to:   # check if selected transactions are still valid
                for w in workers:
                    w.terminate()
                strategy_manager.set_stats(code=strategy_manager.TIMED_OUT)
                strategy_manager.print_stats(state.get('Difficulty'))
                return 0
        for w in workers:                   # once new block appears
            w.terminate()
        code = push_block(blocks[0])
        strategy_manager.set_stats(code=code, time=time.time()-start_time)
        strategy_manager.print_stats(state.get('Difficulty'))
        return 0

import hashlib, json, random, os, re, requests, time
import multiprocessing as mp

from operator import itemgetter
from datetime import datetime

import settings


class StrategyManager:
    """ Class for managing Strategies """
    def __init__(self, stats_length=None):
        if stats_length is not None:
            self.stats_length = stats_length

    SUCCESS = 's'
    TIMED_OUT = 't'
    UNSUCCESS = 'u'

    CODE_CHOICES = [SUCCESS, TIMED_OUT, UNSUCCESS]

    strategies = []

    stats_length = settings.STATS_LENGTH

    _current_strategy = None

    def register_strategy(self, func, stats=''):
        """ Register strategy of selecting transactions.

        func is strategy function described in strategies.py
        stats is optional string of statistic codes to be set.
        """
        self.strategies.append({
            'func': func,
            'stats': stats,
            'rate': 1,
            'times': [],
            'avg_time': 0
        })

    def register_strategies(self, func_list, stats_dict=None):
        """ Register multiple strategies at once. """
        if stats_dict is None:
            stats_dict = {}
        for func in func_list:
            self.register_strategy(func, stats_dict.get(func.__name__, ''))

    def set_stats(self, code, time=None):
        """ Add new statistic record for currently used strategy

        code must be one of these:
        StrategyManager.SUCCESS : when new block has been successfully added to block chain
        StrategyManager.TIMED_ONE : when mining workers has been stopped due to transaction expiration.
        StrategyManager.UNSUCCESS : when mining workers has been stopped due to change of block chain state
                                    or when server refused block for any reason
        time should be passed only if block has been mined out and sent regardless server's answer.

        TO DO: Amount of mined out coins should be also recorded
        """
        cs = self._current_strategy
        if cs is not None and code in self.CODE_CHOICES:
            cs['stats'] += code                         # add new stat record
            if len(cs['stats']) >= self.stats_length:   # if len of stats exceeds stats_length
                cs['stats'] = cs['stats'][1:]           # drop out oldest stat from stats
            # calculate strategy rating
            cs['rate'] = cs['stats'].count(self.SUCCESS) / len(cs['stats'])

            if time is not None:
                cs['times'].append(time)                    # add new time record
                if len(cs['times']) >= self.stats_length:   # if len of times exceeds stats_length
                    cs['times'] = cs['times'][1:]           # drop out oldest time from times
                # calculate average time
                cs['avg_time'] = sum(cs['times']) / len(cs['times'])

    def select_strategy(self):
        """ Select strategy to use in next mining loop.

        iterate through strategies sorted by rating.
        Choose one with probability equal to it's rating.
        If no strategy chosen, take random one.

        TO DO:
        Strategy is selected based on success rating only,
        but it should also take into account average
        amount of coins per block and also average time
        spent by mining a block.
        """
        for strategy in sorted(self.strategies, key=itemgetter('rate'), reverse=True):
            if random.random() <= strategy['rate']:
                return strategy
        return random.choice(self.strategies)

    def use_strategy(self, transaction_list):
        """ Apply strategy to given transaction_list and return it's result. """
        self._current_strategy = self.select_strategy()
        stats = self._current_strategy.copy()
        stats.pop('func')
        return self._current_strategy['func'](transaction_list, stats)

    def print_stats(self, difficulty):
        """ Print statistics in nice table. """
        mined = get_actuals().get(settings.MINER)
        stats = [['strategy name', 'success', 'timed out', 'unsuccess', 'rate [%]', 'avg time [s]']]
        stats += [[strategy['func'].__name__,
                   strategy['stats'].count(self.SUCCESS),
                   strategy['stats'].count(self.TIMED_OUT),
                   strategy['stats'].count(self.UNSUCCESS),
                   round(strategy['rate'] * 100, 1),
                   round(strategy['avg_time'],1)] for strategy in self.strategies]
        os.system('clear')
        print(f"Miner name: { settings.MINER }\n"
              f"Mined out coins: { mined }\n"
              f"Difficulty: {difficulty}\n"
              f"Curent strategy: { self._current_strategy['func'].__name__ }")
        for row in stats:
            print("{: <20} {: >14} {: >14} {: >14} {: >14} {: >14}".format(*row))


def get_state():
    url = settings.HOST + '/api/coingame/state'
    resp = requests.get(url=url)
    return json.loads(resp.text)


def get_actuals():
    url = settings.HOST + '/api/coingame/actuals'
    resp = requests.get(url=url)
    return json.loads(resp.text)


def parse_transaction(transaction):
    """ Convert transaction string to dictionary. """
    transaction = dict(map(lambda t: t.split(': '), re.split('\n(?! )', transaction)[:-1]))
    transaction['Data'] = transaction['Data'].replace('\n', '\n    ')
    transaction['Id'] = int(transaction['Id'])
    transaction['Fee'] = float(transaction['Fee'])
    transaction['ValidTo'] = datetime.strptime(transaction['ValidTo'], '%Y-%m-%d %H:%M:%S.%f')
    return transaction


def get_transactions():
    url = settings.HOST + '/api/coingame/txpool'
    resp = requests.get(url)
    transaction_list = resp.text.split('--- !Transaction\n')[1:]
    return list(map(parse_transaction, transaction_list))


def get_valid_to(transaction_list):
    """ Get deadline for selected transactions. """
    return sorted(transaction_list, key=itemgetter('ValidTo'))[:1][0]['ValidTo']


def create_block_template(state, transaction_list):
    """ Create block template with unique timestamp. """
    block_template = f"""--- !Hash
Digest: '{ state.get('Digest') }'
--- !Block
Timestamp: { datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ') }
Difficulty: { state.get('Difficulty') }
Nonce: <nonce>
Miner: { settings.MINER }
Transactions:
  - !Transaction
    Fee: { state.get('Fee') }
"""
    for transaction in transaction_list:
        block_template += f"""  - !Transaction
    Id: { transaction['Id'] }
    Fee: { transaction['Fee'] }
    Data: { transaction['Data'] }
"""
    return block_template


ZEROS = {
    '1': 3,
    '2': 2,
    '3': 2,
    '4': 1,
    '5': 1,
    '6': 1,
    '7': 1,
    '8': 0,
    '9': 0,
    'a': 0,
    'b': 0,
    'c': 0,
    'd': 0,
    'e': 0,
    'f': 0,
}


def calculate_difficulty(block):
    blockbytes = block.encode('utf-8')
    hashobj = hashlib.sha384()
    hashobj.update(blockbytes)
    hashobj.update(blockbytes)

    difficulty = 0
    for h in hashobj.hexdigest():
        if h != '0':
            return difficulty + ZEROS[h]
        difficulty += 4
    return difficulty


def search_for_block(block_template, difficulty):
    nonce = 1
    while True:
        block = block_template.replace('<nonce>', str(nonce), 1)
        if calculate_difficulty(block) >= difficulty:
            return block
        nonce += 1


def worker_loop(block_template, difficulty, blocks):
    block = search_for_block(block_template, difficulty)
    blocks.append(block)


def push_block(block):
    url = settings.HOST + '/api/coingame'
    resp = requests.put(url=url, data=block)
    if json.loads(resp.text).get('result') == 'OK':
        return StrategyManager.SUCCESS
    return StrategyManager.UNSUCCESS


def main_loop(strategy_manager):
    state = get_state()
    # get transactions and apply strategic select
    transaction_list = strategy_manager.use_strategy(get_transactions())
    valid_to = get_valid_to(transaction_list)

    with mp.Manager() as manager:
        blocks = manager.list()  # Space for handing in blocks
        workers = []
        for i in range(settings.WORKERS):
            # create workers with unique timestamp in template
            block_template = create_block_template(state, transaction_list)
            workers.append(mp.Process(target=worker_loop, args=[block_template, state.get('Difficulty'), blocks]))
        start_time = loop_time = time.time()
        for w in workers:
            w.start()
        while not len(blocks):              # while no block received from any worker yet
            if time.time() - loop_time >= 1:    # every second
                if state != get_state():        # check if state has not changed
                    for w in workers:
                        w.terminate()
                    strategy_manager.set_stats(code=strategy_manager.UNSUCCESS)
                    strategy_manager.print_stats(state.get('Difficulty'))
                    return 0
                loop_time = time.time()
            if datetime.utcnow() >= valid_to:   # check if selected transactions are still valid
                for w in workers:
                    w.terminate()
                strategy_manager.set_stats(code=strategy_manager.TIMED_OUT)
                strategy_manager.print_stats(state.get('Difficulty'))
                return 0
        for w in workers:                   # once new block appears
            w.terminate()
        code = push_block(blocks[0])
        strategy_manager.set_stats(code=code, time=time.time()-start_time)
        strategy_manager.print_stats(state.get('Difficulty'))
        return 0
Class for managing Strategies Register strategy of selecting transactions.          func is strategy function described in strategies.py         stats is optional string of statistic codes to be set. Register multiple strategies at once. Add new statistic record for currently used strategy          code must be one of these:         StrategyManager.SUCCESS : when new block has been successfully added to block chain         StrategyManager.TIMED_ONE : when mining workers has been stopped due to transaction expiration.         StrategyManager.UNSUCCESS : when mining workers has been stopped due to change of block chain state                                     or when server refused block for any reason         time should be passed only if block has been mined out and sent regardless server's answer.          TO DO: Amount of mined out coins should be also recorded # add new stat record # if len of stats exceeds stats_length # drop out oldest stat from stats # calculate strategy rating # add new time record # if len of times exceeds stats_length # drop out oldest time from times # calculate average time Select strategy to use in next mining loop.          iterate through strategies sorted by rating.         Choose one with probability equal to it's rating.         If no strategy chosen, take random one.          TO DO:         Strategy is selected based on success rating only,         but it should also take into account average         amount of coins per block and also average time         spent by mining a block. Apply strategy to given transaction_list and return it's result. Print statistics in nice table. Convert transaction string to dictionary. Get deadline for selected transactions. Create block template with unique timestamp. --- !Hash Digest: '{ state.get('Digest') }' --- !Block Timestamp: { datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ') } Difficulty: { state.get('Difficulty') } Nonce: <nonce> Miner: { settings.MINER } Transactions:   - !Transaction     Fee: { state.get('Fee') } - !Transaction     Id: { transaction['Id'] }     Fee: { transaction['Fee'] }     Data: { transaction['Data'] } # get transactions and apply strategic select # Space for handing in blocks # create workers with unique timestamp in template # while no block received from any worker yet # every second # check if state has not changed # check if selected transactions are still valid # once new block appears