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
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
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]
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)
def validateAccount(account): try: validate = validate_account_id(account) if validate == account: return True except: return False
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
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
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))
def set_account_id(self, account_id): self._account_id = normalize_account_id( validate_account_id(account_id))
def set_representative(self, representative): if representative: self._representative = normalize_account_id( validate_account_id(representative)) else: self._representative = None
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
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}
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")