Esempio n. 1
0
    def add_to_address_book(self, account_id, name):
        """
        Add an account ID to the address book

        :param str account_id: Account ID
        :param str name: Name for the account ID in the address book

        :raises nanolib.InvalidAccount: If the account ID is invalid
        """
        validate_account_id(account_id)

        self.address_book[account_id] = name
Esempio n. 2
0
    def send(self, account_id, amount, callbacks=None):
        """
        Create a send block and add it to the blockchain

        :param str account_id: Destination account ID
        :param amount: Amount to send
        :type amount: int or str
        :param callbacks: Optional set of callbacks
        :type callbacks: siliqua.util.Callbacks

        :returns: Created block
        :rtype: siliqua.wallet.accounts.Block
        """
        if not self.private_key:
            raise ValueError("Private key required to send")

        # TODO: Decimals are also accepted implicitly here.
        if isinstance(amount, float):
            raise TypeError("Floating numbers are not allowed")

        if isinstance(amount, str) and not amount.isdigit():
            raise ValueError("Strings can only contain an integer")

        validate_account_id(account_id)

        amount = int(amount)

        if amount < 1:
            raise ValueError("Value must be at least 1 raw")

        if self.balance < amount:
            raise InsufficientBalance(
                "Insufficient balance to perform transaction")

        head_block = self.blocks[-1]

        raw_block = RawBlock(block_type="state",
                             account=self.account_id,
                             previous=head_block.block_hash,
                             representative=self.representative_to_add,
                             balance=self.balance - amount,
                             link_as_account=account_id)
        block = Block(block=raw_block,
                      confirmed=False,
                      timestamp=get_current_timestamp())

        self.attach_precomputed_work(block)
        self.add_block(block, callbacks=callbacks)

        return block
Esempio n. 3
0
    def remove_from_address_book(self, account_id):
        """
        Remove an account ID from the address book

        :param str account_id: Account ID

        :raises nanolib.InvalidAccount: If the account ID is invalid
        :raises KeyError: If the account ID is not in the address book
        """
        validate_account_id(account_id)

        if account_id not in self.address_book:
            raise KeyError("Account ID not in the address book")

        del self.address_book[account_id]
Esempio n. 4
0
    def add_account_from_account_id(self, account_id):
        """
        Add watching-only account with an account ID.

        :param str account_id: Account ID

        :raises AccountAlreadyExists: Account already exists

        :returns: Created account
        :rtype: siliqua.wallet.accounts.Acconut
        """
        validate_account_id(account_id)

        account = Account(account_id=account_id, source=AccountSource.WATCHING)

        return self.add_account(account)
Esempio n. 5
0
def validateAccount(account):
    try:
        validate = validate_account_id(account)
        if validate == account:
            return True
    except:
        return False
Esempio n. 6
0
    def change_representative(self, representative, callbacks=None):
        """
        Change the account representative. A new block is also appended
        to the blockchain unless the account is empty.

        :param str representative: Representative account ID
        :param callbacks: Optional set of callbacks to run
        :type callbacks: siliqua.util.Callbacks

        :return: Block instance if block was created, otherwise True
        :rtype: siliqua.wallet.accounts.Block or None
        """
        if account_ids_equal(self.representative_to_add, representative):
            raise ValueError(
                "The representative is already assigned for this account")

        validate_account_id(representative)

        if self.blocks:
            # If we have opened this account already,
            # create a change block and add the representative
            # If not, store the representative
            # and use it later when this account is opened
            representative_to_add = (representative if representative else
                                     EMPTY_REPRESENTATIVE)
            head_block = self.blocks[-1]

            raw_block = RawBlock(block_type="state",
                                 account=self.account_id,
                                 previous=head_block.block_hash,
                                 representative=representative_to_add,
                                 balance=head_block.balance,
                                 link=None)
            block = Block(block=raw_block,
                          confirmed=False,
                          timestamp=get_current_timestamp())

            self.attach_precomputed_work(block)
            self.add_block(block, callbacks=None)

            self.representative = representative
            return block
        else:
            self.representative = representative
            return True
Esempio n. 7
0
    async def service_handler(self, data):
        if not {'hash', 'user', 'api_key'} <= data.keys():
            raise InvalidRequest(
                "Incorrect submission. Required information: user, api_key, hash"
            )

        service, api_key = data['user'], data.pop('api_key')
        api_key = hash_key(api_key)

        # Verify API Key
        db_key = await self.database.hash_get(f"service:{service}", "api_key")
        if db_key is None:
            logger.info(
                f"Received request with non existing service {service}")
            raise InvalidRequest("Invalid credentials")
        elif not api_key == db_key:
            logger.info(
                f"Received request with non existing api key {api_key} for service {service}"
            )
            raise InvalidRequest("Invalid credentials")

        async with self.service_throttlers[service]:
            block_hash = data['hash']
            account = data.get('account', None)
            difficulty = data.get('difficulty', None)
            multiplier = data.get('multiplier', None)
            if multiplier:
                try:
                    multiplier = float(multiplier)
                except:
                    raise InvalidRequest(f"Multiplier must be a float")
                difficulty = nanolib.work.derive_work_difficulty(
                    multiplier, base_difficulty=self.base_difficulty)

            try:
                block_hash = nanolib.validate_block_hash(block_hash)
                if account:
                    account = account.replace("xrb_", "nano_")
                    nanolib.validate_account_id(account)
                if difficulty:
                    nanolib.validate_difficulty(difficulty)
            except nanolib.InvalidBlockHash:
                raise InvalidRequest("Invalid hash")
            except nanolib.InvalidAccount:
                raise InvalidRequest("Invalid account")
            except nanolib.InvalidDifficulty:
                raise InvalidRequest("Difficulty too low")
            except ValueError:
                raise InvalidRequest("Invalid difficulty")

            if difficulty:
                multiplier = nanolib.work.derive_work_multiplier(
                    difficulty, base_difficulty=self.base_difficulty)
                if multiplier > config.max_multiplier:
                    raise InvalidRequest(
                        f"Difficulty too high. Maximum: {nanolib.work.derive_work_difficulty(config.max_multiplier, base_difficulty=self.base_difficulty)} ( {config.max_multiplier} multiplier )"
                    )
                elif multiplier < 1.0:
                    raise InvalidRequest(f"Difficulty too low. Minimum: 1.0")

            # Check if hash in redis db, if so return work
            work = await self.database.get(f"block:{block_hash}")

            if work is None:
                # Set incomplete work
                await self.database.insert_expire(f"block:{block_hash}",
                                                  DpowServer.WORK_PENDING,
                                                  config.block_expiry)

            work_type = "ondemand"
            if work and work != DpowServer.WORK_PENDING:
                work_type = "precache"
                if difficulty:
                    precached_difficulty = nanolib.work.get_work_value(
                        block_hash, work, as_hex=True)
                    precached_multiplier = nanolib.work.derive_work_multiplier(
                        precached_difficulty,
                        base_difficulty=self.base_difficulty)
                    if precached_multiplier < DpowServer.FORCE_ONDEMAND_THRESHOLD * multiplier:
                        # Force ondemand since the precache difficulty is not close enough to requested difficulty
                        work_type = "ondemand"
                        await self.database.insert_expire(
                            f"block:{block_hash}", DpowServer.WORK_PENDING,
                            config.block_expiry)
                        logger.info(
                            f"Forcing ondemand: precached {precached_multiplier} vs requested {multiplier}"
                        )
                    else:
                        difficulty = precached_difficulty

            if work_type == "ondemand":
                # Set work type
                await self.database.insert_expire(f"work-type:{block_hash}",
                                                  work_type,
                                                  config.block_expiry)

                if block_hash not in self.work_futures:
                    # If account is not provided, service runs a risk of the next work not being precached for
                    # There is still the possibility we recognize the need to precache based on the previous block
                    if account:
                        # Update account frontier
                        asyncio.ensure_future(
                            self.database.insert_expire(
                                f"account:{account}", block_hash,
                                config.account_expiry))

                    # Set difficulty in DB if provided
                    if difficulty:
                        await self.database.insert_expire(
                            f"block-difficulty:{block_hash}", difficulty,
                            DpowServer.DIFFICULTY_EXPIRY)

                    # Base difficulty if not provided
                    difficulty = difficulty or self.base_difficulty

                    # Create a Future to be set with work when complete
                    self.work_futures[block_hash] = loop.create_future()

                    # Ask for work on demand
                    await self.mqtt.send(f"work/ondemand",
                                         f"{block_hash},{difficulty}",
                                         qos=QOS_0)

                timeout = data.get('timeout', 5)
                try:
                    timeout = int(timeout)
                    if timeout < 1 or timeout > 30:
                        raise
                except:
                    raise InvalidRequest(
                        "Timeout must be an integer between 1 and 30")

                try:
                    work = await asyncio.wait_for(
                        self.work_futures[block_hash], timeout=timeout)
                except asyncio.CancelledError:
                    logger.debug(f"Future was cancelled for {block_hash}")
                    work = await self.database.get(f"block:{block_hash}")
                    if not work:
                        logger.error(
                            "Future was cancelled and work result not set in database"
                        )
                        raise RetryRequest()
                except asyncio.TimeoutError:
                    logger.warn(
                        f"Timeout of {timeout} reached for {block_hash}")
                    raise RequestTimeout()
                finally:
                    try:
                        future = self.work_futures.pop(block_hash)
                        future.cancel()
                    except Exception:
                        pass
                # logger.info(f"Work received: {work}")
            else:
                # logger.info(f"Work in cache: {work}")
                pass

            # Increase the work type counter for this service
            asyncio.ensure_future(
                self.database.hash_increment(f"service:{service}", work_type))

            # Final work validation
            try:
                nanolib.validate_work(block_hash,
                                      work,
                                      difficulty=difficulty
                                      or self.base_difficulty)
            except nanolib.InvalidWork:
                db_difficulty = await self.database.get(
                    f"block-difficulty:{block_hash}")
                logger.critical(
                    f"Work could not be validated! Request difficulty {difficulty or self.base_difficulty} result difficulty {nanolib.work.get_work_value(block_hash, work, as_hex=True)} , hash {block_hash} work {work} type {work_type} DB difficulty {db_difficulty}"
                )

            response = {'work': work, 'hash': block_hash}
            logger.info(
                f"Request handled for {service} -> {work_type} : {data} : {work}"
            )

        return response
Esempio n. 8
0
    async def client_handler(self, topic, content):
        try:
            # Content is expected as CSV block,work,client
            block_hash, work, client = content.split(',')
            # logger.info(f"Message {block_hash} {work} {client}")
        except Exception:
            # logger.warn(f"Could not parse message: {e}")
            return

        # Check if work is needed
        # - Block is removed from DB once account frontier that contained it is updated
        # - Block corresponding value is WORK_PENDING if work is pending
        available = await self.database.get(f"block:{block_hash}")
        if not available or available != DpowServer.WORK_PENDING:
            return

        work_type = await self.database.get(f"work-type:{block_hash}")
        if not work_type:
            work_type = "precache"  # expired ?

        difficulty = await self.database.get(f"block-difficulty:{block_hash}")

        try:
            nanolib.validate_work(block_hash,
                                  work,
                                  difficulty=difficulty
                                  or self.base_difficulty)
        except nanolib.InvalidWork:
            # logger.debug(f"Client {client} provided invalid work {work} for {block_hash}")
            return
        except:
            return

        # Used as a lock - if value already existed, then some other client finished before
        if not await self.database.insert_if_noexist_expire(
                f"block-lock:{block_hash}", '1', 5):
            return

        # Set work result in DB
        await self.database.insert_expire(f"block:{block_hash}", work,
                                          config.block_expiry)

        # Set Future result if in memory
        try:
            resulting_work = self.work_futures[block_hash]
            if not resulting_work.done():
                resulting_work.set_result(work)
        except KeyError:
            pass
        except Exception as e:
            logger.error(f"Unknown error when setting work future: {e}")

        # As we've got work now send cancel command to clients and do a stats update
        await self.mqtt.send(f"cancel/{work_type}", block_hash, qos=QOS_0)

        try:
            nanolib.validate_account_id(client)
        except nanolib.InvalidAccount:
            await self.mqtt.send(
                f"client/{client}",
                ujson.dumps({
                    "error":
                    f"Work accepted but account {client} is invalid"
                }))
            return

        # Account information and DB update
        await asyncio.gather(self.client_update(client, work_type, block_hash),
                             self.database.increment(f"stats:{work_type}"),
                             self.database.set_add(f"clients", client))
Esempio n. 9
0
 def set_account_id(self, account_id):
     self._account_id = normalize_account_id(
         validate_account_id(account_id))
Esempio n. 10
0
 def set_representative(self, representative):
     if representative:
         self._representative = normalize_account_id(
             validate_account_id(representative))
     else:
         self._representative = None
Esempio n. 11
0
    async def service_handler(self, data):
        error = None
        timeout = False
        response = {}
        try:
            if {'hash', 'user', 'api_key'} <= data.keys():
                service, api_key = data['user'], data['api_key']
                api_key = hash_key(api_key)

                #Verify API Key
                db_key = await self.database.hash_get(f"service:{service}",
                                                      "api_key")
                if db_key is None:
                    logger.info(
                        f"Received request with non existing service {service}"
                    )
                    error = "Invalid credentials"
                elif not api_key == db_key:
                    logger.info(
                        f"Received request with non existing api key {api_key} for service {service}"
                    )
                    error = "Invalid credentials"

                if not error:
                    block_hash = data['hash']
                    account = data.get('account', None)
                    difficulty = data.get('difficulty', None)

                    try:
                        block_hash = nanolib.validate_block_hash(block_hash)
                        if account:
                            account = account.replace("xrb_", "nano_")
                            nanolib.validate_account_id(account)
                        if difficulty:
                            nanolib.validate_difficulty(difficulty)
                    except nanolib.InvalidBlockHash:
                        error = "Invalid hash"
                    except nanolib.InvalidAccount:
                        error = "Invalid account"
                    except ValueError:
                        error = "Invalid difficulty"
                    except nanolib.InvalidDifficulty:
                        error = "Difficulty too low"

                    if not error and difficulty:
                        difficulty_multiplier = nanolib.work.derive_work_multiplier(
                            difficulty)
                        if difficulty_multiplier > DpowServer.MAX_DIFFICULTY_MULTIPLIER:
                            error = f"Difficulty too high. Maximum: {nanolib.work.derive_work_difficulty(DpowServer.MAX_DIFFICULTY_MULTIPLIER)} ( {DpowServer.MAX_DIFFICULTY_MULTIPLIER} multiplier )"

                if not error:
                    #Check if hash in redis db, if so return work
                    work = await self.database.get(f"block:{block_hash}")

                    if work is None:
                        # Set incomplete work
                        await self.database.insert_expire(
                            f"block:{block_hash}", DpowServer.WORK_PENDING,
                            DpowServer.BLOCK_EXPIRY)

                    work_type = "ondemand"
                    if work and work != DpowServer.WORK_PENDING:
                        work_type = "precache"
                        if difficulty:
                            precached_multiplier = nanolib.work.derive_work_multiplier(
                                hex(
                                    nanolib.work.get_work_value(
                                        block_hash, work))[2:])
                            if precached_multiplier < DpowServer.FORCE_ONDEMAND_THRESHOLD * difficulty_multiplier:
                                # Force ondemand since the precache difficulty is not close enough to requested difficulty
                                work_type = "ondemand"
                                await self.database.insert(
                                    f"block:{block_hash}",
                                    DpowServer.WORK_PENDING)
                                logger.warn(
                                    f"Forcing ondemand: precached {precached_multiplier} vs requested {difficulty_multiplier}"
                                )

                    if work_type == "ondemand":
                        # If account is not provided, service runs a risk of the next work not being precached for
                        # There is still the possibility we recognize the need to precache based on the previous block
                        if account:
                            # Update account frontier
                            asyncio.ensure_future(
                                self.database.insert_expire(
                                    f"account:{account}", block_hash,
                                    DpowServer.ACCOUNT_EXPIRY))

                        # Create a Future to be set with work when complete
                        self.work_futures[block_hash] = loop.create_future()

                        # Set difficulty in DB if provided
                        if difficulty:
                            await self.database.insert_expire(
                                f"block-difficulty:{block_hash}", difficulty,
                                DpowServer.DIFFICULTY_EXPIRY)

                        # Base difficulty if not provided
                        difficulty = difficulty or nanolib.work.WORK_DIFFICULTY

                        # Ask for work on demand
                        await self.mqtt.send(f"work/ondemand",
                                             f"{block_hash},{difficulty}",
                                             qos=QOS_0)

                        # Wait on the work for some time
                        timeout = max(int(data.get('timeout', 5)), 1)
                        try:
                            work = await asyncio.wait_for(
                                self.work_futures[block_hash], timeout=timeout)
                        except asyncio.TimeoutError:
                            logger.warn(
                                f"Timeout of {timeout} reached for {block_hash}"
                            )
                            error = "Timeout reached without work"
                            timeout = True
                        finally:
                            try:
                                future = self.work_futures.pop(block_hash)
                                future.cancel()
                            except:
                                pass
                        # logger.info(f"Work received: {work}")
                    else:
                        # logger.info(f"Work in cache: {work}")
                        pass

                    # Increase the work type counter for this service
                    asyncio.ensure_future(
                        self.database.hash_increment(f"service:{service}",
                                                     work_type))
            else:
                error = "Incorrect submission. Required information: user, api_key, hash"
        except Exception as e:
            logger.critical(f"Unknown exception: {e}")
            if not error:
                error = f"Unknown error, please report the following timestamp to the maintainers: {datetime.datetime.now()}"

        if 'id' in data:
            response['id'] = data['id']
        if error:
            response['error'] = error
            if timeout:
                response['timeout'] = timeout
        else:
            response['work'] = work

        return response
Esempio n. 12
0
def importConfig():
    #get worker api config
    try:
        with open('config/worker_config.json') as worker_config:
            data = json.load(worker_config)
        worker = {
            "account":
            data['reward_account'].replace("xrb_", "nano_"),
            "private_key":
            data['private_key'].upper(),
            "public_key":
            nanolib.get_account_key_pair(data['private_key']).public.upper(),
            "representative":
            data['representative'].replace("xrb_", "nano_"),
            "default_fee":
            data['fee'],
            "node":
            to_url(data['node']),
            "worker_node":
            to_url(data['worker']),
            "max_multiplier":
            data['max_multiplier'],
            "use_dynamic_pow":
            data['use_dynamic_pow'],
            "use_dynamic_fee":
            data['use_dynamic_fee'],
            "dynamic_pow_interval":
            data['dynamic_pow_interval'],
            "show_network_difficulty":
            data['show_network_difficulty'],
            "service_listen":
            data['listen'],
            "service_port":
            data['port'],
        }
        worker["default_fee"] = to_raws(str(
            worker["default_fee"]))  #convert mNano fee to raws
    except Exception as err:
        raise Exception("worker_config.json error: " + str(err))

    #Get worker registration config
    try:
        with open('config/register_config.json') as register_config:
            data_register = json.load(register_config)
        register_config = {
            "account":
            data_register['registration_account'].replace("xrb_", "nano_"),
            "register_code":
            int(data_register['register_code']),
            "get_ip": {
                "ipv4": to_url(data_register['get_ip']['ipv4']),
                "ipv6": to_url(data_register['get_ip']['ipv6'])
            },
            "default_ip_version":
            ip_version(data_register['default_ip_version'])
        }
    except Exception as err:
        raise Exception("worker_config.json error: " + str(err))

    #Check config file
    try:
        nanolib.validate_account_id(worker["account"])
        nanolib.validate_private_key(worker["private_key"])
        nanolib.validate_account_id(worker["representative"])
    except Exception as e:
        raise Exception(
            "Invalid config in worker_config.json found! Details: " + str(e))

    #Check config file
    try:
        nanolib.validate_account_id(register_config["account"])
    except Exception as e:
        raise Exception(
            "Invalid config in register_config.json found! Details: " + str(e))

    #Check if key pair is valid
    if worker["account"] != nanolib.get_account_id(
            private_key=worker["private_key"], prefix="nano_"):
        raise Exception("Invalid key pair")

    return {"worker_config": worker, "register_config": register_config}
Esempio n. 13
0
from flask import Flask, jsonify, request
from functions import block_create, balance, frontier, broadcast, get_difficulty, solve_work, get_my_ip, pending_filter, receive, check_history, register, encode_ip, worker, register_config
from nanolib import Block, validate_account_id, validate_private_key, get_account_id
import requests
import json
import waitress

app = Flask(__name__)

#Check config
try:
    validate_account_id(worker["account"])
    validate_private_key(worker["private_key"])
    validate_account_id(worker["representative"])
except Exception as e:
    print("Invalid worker_config.json settings found! Details: ")
    print(e)
    quit()

#Check if key pair is valid
if worker["account"] != get_account_id(private_key=worker["private_key"],
                                       prefix="nano_"):
    print("Invalid key pair")
    quit()
print("Configurations okay")

#Check if Node is online
try:
    r = requests.post(worker["node"], json={"action": "version"})
except:
    print("Node " + worker["node"] + " Offline! Exiting")