def request_transaction(self, node, port, tx_hash): url = self.TRANSACTIONS_URL.format(node, port, tx_hash) try: response = requests.get(url) if response.status_code == 200: tx_dict = response.json() transaction = Transaction( tx_dict['source'], tx_dict['destination'], tx_dict['amount'], tx_dict['fee'], tx_dict['prev_hash'], tx_dict['tx_type'], tx_dict['timestamp'], tx_dict['tx_hash'], tx_dict['asset'], tx_dict['data'], tx_dict['signature'] ) if transaction.tx_hash != tx_dict['tx_hash']: logger.warn("Invalid transaction hash: {} should be {}. Transaction ignored." .format(tx_dict['tx_hash'], transaction.tx_hash)) return None return transaction except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return None
def __synchronize(self, node): # synchronize with sender repeat_sync = True while repeat_sync is True: current_height = self.blockchain.get_height() peer_height = self.api_client.request_height(node) if peer_height is not None and peer_height > current_height: # 100 blocks of overlap should be sufficient to find a common block lest we are on a forked branch start_height = current_height - 100 if current_height > 100 else 1 if current_height < peer_height - 500: # we are way behind. end_height = start_height + 500 else: end_height = peer_height repeat_sync = False peer_blocks_inv = self.api_client.audit( node, start_height, end_height) last_common_block = self.__find_last_common_block( peer_blocks_inv) if last_common_block is None: logger.warn( "Completely out of sync with peer at {}".format(node)) break block_header, branch, height = last_common_block # construct list of missing block hashes to request from the peer hashes_to_query = peer_blocks_inv[peer_blocks_inv. index(block_header.hash) + 1:] for block_hash in hashes_to_query: block_header = self.api_client.request_block_header( node, self.FULL_NODE_PORT, block_hash=block_hash) self.__process_block_header(block_header, node) else: repeat_sync = False
def validate_block(self, block, merkle_root): if block.block_header.merkle_root != merkle_root: logger.warn("invalid merkle root") return False if not self.check_block_reward(block): logger.warn("Invalid block reward") return False return True
def request_blocks_inv(self, node, port, start_height, stop_height): # Used when a synchronization between peers is needed url = self.BLOCKS_INV_URL.format(node, port, start_height, stop_height) try: response = requests.get(url) if response.status_code == 200: block_dict = response.json() return block_dict['block_hashes'] except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return None
def audit(self, node, start_height, end_height): # Audit node's blocks_inv and sync if necessary url = self.BLOCKS_INV_URL.format(node, self.FULL_NODE_PORT, start_height, end_height) try: response = requests.get(url) if response.status_code == 200: tx_dict = response.json() return tx_dict.get('blocks_inv') except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return None
def request_transactions_inv(self, node, port, block_hash): # Request a list of transaction hashes that belong to a block hash. Used when recreating a block from a # block header url = self.TRANSACTIONS_INV_URL.format(node, port, block_hash) try: response = requests.get(url) if response.status_code == 200: tx_dict = response.json() return tx_dict['tx_hashes'] except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return None
def push_synchronize(self, node, blocks_inv, current_height, host): # Push local blocks_inv to remote node to initiate a sync data = { "host": host, "type": MessageType.SYNCHRONIZE.value, "data": {"height": current_height, "blocks_inv": blocks_inv} } logger.debug("sending sync request to peer at: {}".format(node)) url = self.INBOX_URL.format(node, self.FULL_NODE_PORT) try: response = requests.post(url, json=data) except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return
def broadcast_block_inv(self, block_hashes, host): # Used for (re)broadcasting a new block that was received and added data = { "host": host, "type": MessageType.BLOCK_INV.value, "data": block_hashes } logger.debug("broadcasting block inv: {}".format(data)) for node in self.peers.get_all_peers(): url = self.INBOX_URL.format(node, self.FULL_NODE_PORT) try: response = requests.post(url, json=data) except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return
def broadcast_unconfirmed_transaction_inv(self, tx_hashes, host): # Used for (re)broadcasting a new transaction that was received and added data = { "host": host, "type": MessageType.UNCONFIRMED_TRANSACTION_INV.value, "data": tx_hashes } logger.debug("broadcasting transaction inv: {}".format(data)) for node in self.peers.get_all_peers(): url = self.INBOX_URL.format(node, self.FULL_NODE_PORT) try: response = requests.post(url, json=data) except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return
def broadcast_block_header(self, block_header, host): # Used only when broadcasting a block header that originated (mined) locally data = { "host": host, "type": MessageType.BLOCK_HEADER.value, "data": block_header.to_json() } logger.debug("broadcasting block header: {}".format(data)) for node in self.peers.get_all_peers(): url = self.INBOX_URL.format(node, self.FULL_NODE_PORT) try: response = requests.post(url, json=data) except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return
def request_block_header(self, node, port, block_hash=None, height=None): if block_hash is not None: url = self.BLOCKS_URL.format(node, port, "hash", block_hash) elif height is not None: url = self.BLOCKS_URL.format(node, port, "height", height) else: url = self.BLOCKS_URL.format(node, port, "height", "latest") try: response = requests.get(url) if response.status_code == 200: block_dict = response.json() block_header = BlockHeader(block_dict['previous_hash'], block_dict['merkle_root'], block_dict['timestamp'], block_dict['nonce'], block_dict['version']) return block_header except requests.exceptions.RequestException as re: logger.warn("Request Exception with host: {}".format(node)) self.peers.record_downtime(node) return None
def validate_transaction(self, transaction): """ Validate a single transaction. Check for double-spend, invalid signature, and insufficient funds :param transaction: :return: boolean :rtype: boolean """ if self.blockchain.find_duplicate_transactions(transaction.tx_hash): logger.warn( 'Transaction not valid. Double-spend prevented: {}'.format( transaction.tx_hash)) return False if not transaction.verify(): logger.warn( 'Transaction not valid. Invalid transaction signature: {}'. format(transaction.tx_hash)) return False balance = self.blockchain.get_balance(transaction.source) if transaction.amount + transaction.fee > balance: logger.warn( 'Transaction not valid. Insufficient funds: {}'.format( transaction.tx_hash)) return False return True
def validate_block_transactions_inv(self, transactions_inv): """ Checks a list of transaction hashes, checks for double-spends and/or entries in the mempool Returns a list of unknown transaction hashes :param transactions_inv: :return: block_transactions, missing_transactions_inv :rtype: tuple(list, list) """ missing_transactions_inv = [] block_transactions = [] for tx_hash in transactions_inv: if self.blockchain.find_duplicate_transactions(tx_hash): logger.warn( 'Transaction not valid. Double-spend prevented: {}'. format(tx_hash)) return False transaction = self.mempool.get_unconfirmed_transaction(tx_hash) if transaction is None: missing_transactions_inv.append(tx_hash) else: block_transactions.append(transaction) return block_transactions, missing_transactions_inv
def validate_block_header(self, block_header, transactions_inv): if self.blockchain.get_block_header_by_hash(block_header.hash): logger.warn('Block Header already exists') return False if block_header.version != config['network']['version']: logger.warn('Incompatible version') return False if block_header.merkle_root != self.calculate_merkle_root( transactions_inv): logger.warn('Invalid merkle root') return False previous_block = self.blockchain.get_block_header_by_hash( block_header.previous_hash) if previous_block is None: return None previous_block_header, previous_block_branch, previous_block_height = previous_block if self.blockchain.calculate_hash_difficulty( previous_block_height + 1) > block_header.hash_difficulty: logger.warn('Invalid hash difficulty') return False return previous_block_height + 1
def check_block_reward(self, block): reward_amount = self.blockchain.get_reward(block.height) for transaction in block.transactions[1:]: if TransactionType( transaction.tx_type) == TransactionType.COINBASE: logger.warn("Block not valid. Multiple coinbases detected") return False reward_amount += transaction.fee # first transaction is coinbase reward_transaction = block.transactions[0] if TransactionType( reward_transaction.tx_type) != TransactionType.COINBASE: logger.warn("Block not valid. Missing coinbase") return False if reward_transaction.amount != reward_amount: logger.warn("Invalid block reward {} should be {}".format( reward_transaction.amount, reward_amount)) return False if reward_transaction.source != "0": logger.warn("Invalid Coinbase source") return False return True
def check_peers_full(self, host, known_peers): if self.peers.get_peers_count() < self.MIN_PEERS: host_data = {"host": host, "network": config['network']} for peer in known_peers: if self.peers.get_peers_count() >= self.MAX_PEERS: break if peer == host: continue status_url = self.STATUS_URL.format(peer, self.FULL_NODE_PORT) connect_url = self.CONNECT_URL.format(peer, self.FULL_NODE_PORT) try: response = requests.get(status_url) if response.status_code != 200: # Downtime or error if self.peers.get_peer(peer): self.peers.record_downtime(peer) logger.warn("Downtime recorded for node %s", peer) continue if response.json( ) != config['network']: # Incompatible network if self.peers.get_peer(peer): self.peers.remove_peer(peer) logger.warn("Incompatible network with node %s", peer) continue if self.peers.get_peer(peer) is None: response = requests.post(connect_url, json=host_data) if response.status_code == 202 and response.json().get( "success") is True: self.peers.add_peer(peer) except requests.exceptions.RequestException as re: logger.warn( "Request exception while attempting to reach %s", peer) if self.peers.get_peer(peer): self.peers.record_downtime(peer) return
def client(): helptext = ''' Available commands: =================== balance <public key (optional)> send <destination> <amount> <fee> publickey privatekey history <public key (optional)> quit or exit ''' peers = Peers() api_client = ApiClient(peers) encrypted = config['user']['encrypted_private_key'] if encrypted is None: print( "\n\nNo private key provided. A new wallet will be generated for you...\n\n" ) wallet = Client(peers, api_client) else: passphrase = getpass("Enter passphrase: ") encrypted = codecs.decode(encrypted, 'hex') nonce = encrypted[0:16] tag = encrypted[16:32] ciphertext = encrypted[32:] hashedpass = hashlib.sha256(passphrase.encode('utf-8')).digest() cipher = AES.new(hashedpass, AES.MODE_EAX, nonce) try: private_key = cipher.decrypt_and_verify(ciphertext, tag) wallet = Client(peers, api_client, private_key) except ValueError as ve: logger.warn('Invalid passphrase') print("\n\nInvalid passphrase\n\n") sys.exit(1) while True: cmd = input("{} ({}) wallet > ".format( config['network']['name'], config['network']['ticker_symbol'])) cmd_split = cmd.split() try: if cmd_split[0] == "balance": if len(cmd_split) == 2: print(wallet.get_balance(cmd_split[1])) else: print(wallet.get_balance()) elif cmd_split[0] == "send": if len(cmd_split) == 4: print( wallet.create_transaction(cmd_split[1], float(cmd_split[2]), float(cmd_split[3]))) else: print("\nRequires destination, amount, fee\n") elif cmd_split[0] == "publickey": print(wallet.get_public_key()) elif cmd_split[0] == "privatekey": print(wallet.get_private_key()) elif cmd_split[0] == "history": if len(cmd_split) == 2: print(wallet.get_transaction_history(cmd_split[1])) else: print(wallet.get_transaction_history()) elif cmd_split[0] in ("quit", "exit"): sys.exit(0) else: # help print(helptext) except IndexError: pass
def worker(self): while True: msg = Queue.dequeue() sender = msg.get('host', '') msg_type = MessageType(msg.get('type')) data = msg.get('data') if msg_type == MessageType.BLOCK_HEADER: block_header = BlockHeader.from_dict(json.loads(data)) if sender == self.HOST: self.api_client.broadcast_block_inv([block_header.hash], self.HOST) else: self.__process_block_header(block_header, sender) continue elif msg_type == MessageType.UNCONFIRMED_TRANSACTION: unconfirmed_transaction = Transaction.from_dict(data) if sender == self.HOST: # transaction already validated before being enqueued valid = True else: valid = self.validator.validate_transaction( unconfirmed_transaction) if valid: self.api_client.broadcast_unconfirmed_transaction_inv( [unconfirmed_transaction.tx_hash], self.HOST) continue elif msg_type == MessageType.BLOCK_INV: missing_block_headers = [] for block_hash in data: # aggregate unknown block header hashes block_header = self.blockchain.get_block_header_by_hash( block_hash) if block_header is None: missing_block_headers.append(block_hash) for block_hash in missing_block_headers: # We don't have these blocks in our database. Fetch them from the sender block_header = self.api_client.request_block_header( sender, self.FULL_NODE_PORT, block_hash=block_hash) self.__process_block_header(block_header, sender) continue elif msg_type == MessageType.UNCONFIRMED_TRANSACTION_INV: missing_transactions = [] new_unconfirmed_transactions = [] for tx_hash in data: # skip known unconfirmed transactions transaction = self.blockchain.get_transaction_by_hash( tx_hash) if transaction: continue unconfirmed_transaction = self.mempool.get_unconfirmed_transaction( tx_hash) if unconfirmed_transaction: continue missing_transactions.append(tx_hash) for tx_hash in missing_transactions: # retrieve unknown unconfirmed transactions transaction = self.api_client.request_transaction( sender, self.FULL_NODE_PORT, tx_hash) valid = self.validator.validate_transaction(transaction) if valid: # validate and store retrieved unconfirmed transactions self.mempool.push_unconfirmed_transaction(transaction) new_unconfirmed_transactions.append(tx_hash) if len(new_unconfirmed_transactions): # broadcast new unconfirmed transactions self.api_client.broadcast_unconfirmed_transaction_inv( new_unconfirmed_transactions) continue else: logger.warn("Encountered unknown message type %s from %s", msg_type, sender) pass