def handle_get_data_message_received( self, header: MessageHeader, get_data_message: GetDataMessage) -> None: if get_data_message.data_type != DATA_BLOCK: raise NotImplementedError( "We can only deal w/ DATA_BLOCK GetDataMessage objects for now" ) coinstate = self.local_peer.chain_manager.coinstate if get_data_message.hash not in coinstate.block_by_hash: # we simply silently ignore GetDataMessage for hashes we don't have... future work: inc banscore, or ... self.local_peer.logger.debug( "%15s ConnectedRemotePeer.handle_data_message_received for unknown hash %s" % (self.host, human(get_data_message.hash))) return data_message = DataMessage( DATA_BLOCK, coinstate.block_by_hash[get_data_message.hash]) self.local_peer.logger.debug( "%15s ConnectedRemotePeer.handle_data_message_received for hash %s h. %s" % (self.host, human(get_data_message.hash), coinstate.block_by_hash[get_data_message.hash].height)) self.send_message(data_message, prev_header=header)
def handle_inventory_message_received(self, header: MessageHeader, message: InventoryMessage) -> None: self.local_peer.logger.info( "%15s ConnectedRemotePeer.handle_inventory_message_received(%d)" % (self.host, len(message.items))) if len(message.items) > 0: self.local_peer.logger.info( "%15s %s .. %s" % (self.host, human( message.items[0].hash), human(message.items[-1].hash))) if len(message.items) > GET_BLOCKS_INVENTORY_SIZE: raise Exception("Inventory msg too big") if message.items == []: self.local_peer.logger.info( "%15s ConnectedRemotePeer.last_empty_inventory_response_at set" % self.host) self.last_empty_inventory_response_at = int( time()) # TODO time() as a pass-along? self.waiting_for_inventory = False return self.inventory_messages.append(InventoryMessageState(header, message)) self.check_inventory_messages() # speed optimization: go ahead and ask for more inventory now, there is no reason to wait get_blocks_message = GetBlocksMessage([message.items[-1].hash]) self.send_message(get_blocks_message, prev_header=header)
def add_transaction_to_pool(self, transaction: Transaction) -> bool: with self.lock: self.local_peer.logger.info( "%15s ChainManager.add_transaction_to_pool(%s)" % ("", human(transaction.hash()))) try: validate_non_coinbase_transaction_by_itself(transaction) assert self.coinstate.current_chain_hash validate_non_coinbase_transaction_in_coinstate( transaction, self.coinstate.current_chain_hash, self.coinstate) # Horribly inefficiently implemented (AKA 'room for improvement) validate_no_duplicate_output_references_in_transactions( self.transaction_pool + [transaction]) # we don't do validate_no_duplicate_transactions here (assuming it's just been done before # add_transaction_to_pool). except ValidateTransactionError as e: # TODO: dirty hack at this particular point... to allow for e.g. out-of-order transactions to not take # down the whole peer, but this should more specifically match for a short list of OK problems. self.local_peer.logger.warning("%15s INVALID transaction %s" % ("", str(e))) self.local_peer.disk_interface.save_transaction_for_debugging( transaction) return False # not successful self.transaction_pool.append(transaction) return True # successfully added
def show_chain_stats(self) -> None: coinstate = self.chain_manager.coinstate def get_block_timespan_factor(n: int) -> float: # Current block duration over past n block as a factor of DESIRED_BLOCK_TIMESPAN, e.g. 0.5 for twice desired # speed diff = coinstate.head( ).timestamp - coinstate.at_head.block_by_height[ coinstate.head().height - n].timestamp return diff / (DESIRED_BLOCK_TIMESPAN * n) # type: ignore def get_network_hash_rate(n: int) -> float: total_over_blocks = sum( calc_work(coinstate.at_head.block_by_height[ coinstate.head().height - i].target) for i in range(n)) diff = coinstate.head( ).timestamp - coinstate.at_head.block_by_height[ coinstate.head().height - n].timestamp return total_over_blocks / diff # type: ignore print("WASTELAND STATS") print("Current target: ", human(coinstate.head().target)) print("Current work: ", calc_work(coinstate.head().target)) print("Timespan factor:", get_block_timespan_factor(100)) print("Hash rate: ", get_network_hash_rate(100))
def handle_get_blocks_message_received(self, header: MessageHeader, message: GetBlocksMessage) -> None: self.local_peer.logger.info("%15s ConnectedRemotePeer.handle_get_blocks_message_received()" % self.host) coinstate = self.local_peer.chain_manager.coinstate self.local_peer.logger.debug("%15s ... at coinstate %s" % (self.host, coinstate)) for potential_start_hash in message.potential_start_hashes: self.local_peer.logger.debug("%15s ... psh %s" % (self.host, human(potential_start_hash))) if potential_start_hash in coinstate.block_by_hash: start_height = coinstate.block_by_hash[potential_start_hash].height + 1 # + 1: sent hash is last known if start_height not in coinstate.by_height_at_head(): # we have no new info self.local_peer.logger.debug("%15s ... no new info" % self.host) self.send_message(InventoryMessage([]), prev_header=header) return if coinstate.by_height_at_head()[start_height].previous_block_hash == potential_start_hash: # this final if checks that this particular potential_start_hash is on our active chain break else: # no break start_height = 1 # genesis is last known max_height = coinstate.head().height + 1 # + 1: range is exclusive, but we need to send this last block also items = [ InventoryItem(DATA_BLOCK, coinstate.by_height_at_head()[height].hash()) for height in range(start_height, min(start_height + GET_BLOCKS_INVENTORY_SIZE, max_height)) ] self.local_peer.logger.info("%15s ... returning from start_height=%d, %d items" % (self.host, start_height, len(items))) self.send_message(InventoryMessage(items), prev_header=header)
def handle_block_received(self, header: MessageHeader, message: DataMessage) -> None: block: Block = message.data # type: ignore coinstate_prior = self.local_peer.chain_manager.coinstate block_hash = block.hash() self.remove_from_inventory(block_hash) if block_hash not in coinstate_prior.block_by_hash: if block.header.summary.previous_block_hash not in coinstate_prior.block_by_hash: # This is not common, so it doesn't need special handling. self.local_peer.logger.info( "%15s at height=%d, block received out of order for height=%d: %s" % (self.host, coinstate_prior.head().height, block.height, human(block_hash))) return validate_block_by_itself(block, int(time())) coinstate_changed = coinstate_prior.add_block_no_validation(block) if header.in_response_to == 0 or block.height % IBD_VALIDATION_SKIP == 0: # Validation is very slow, and we don't have to validate every block in a blockchain, so # during IBD, we only validate every Nth block where N := IBD_VALIDATION_SKIP. # Because the BLOCKS are part of a CHAIN of hashes, every valid block[n] guarantees a valid # block[n-1]. Just to keep things clean, we avoid writing unvalidated blocks to disk until # their next "validated descendent" is encountered (this is unnecessary, but neat). # During normal operation (non-IBD) we just validate every block because we're not in a hurry. try: validate_block_in_coinstate(block, coinstate_prior) # very slow except Exception: self.local_peer.logger.info( "%15s INVALID block: %s" % (self.host, traceback.format_exc())) if self.local_peer.chain_manager.last_known_valid_coinstate: self.local_peer.chain_manager.set_coinstate( self.local_peer.chain_manager. last_known_valid_coinstate) return self.local_peer.chain_manager.set_coinstate(coinstate_changed, validated=True) self.local_peer.disk_interface.write_chain_cache_to_disk( coinstate_changed) else: self.local_peer.chain_manager.set_coinstate(coinstate_changed, validated=False) if block == coinstate_changed.head( ) and header.in_response_to == 0: # "header.in_response_to == 0" is being used as a bit of a proxy for "not in IBD" here, but it would be # better to check for that state more explicitly. We don't want to broadcast blocks while in IBD, # because in that state the fact that some block is our new head doesn't mean at all that we're talking # about the real chain's new head, and only the latter is relevant to the rest of the world. self.local_peer.network_manager.broadcast_block(block)
def main() -> None: parser = DefaultArgumentParser() parser.add_argument("annotation", help="Some text to help you remember a meaning for this receive address.") args = parser.parse_args() configure_logging_from_args(args) wallet = open_or_init_wallet() public_key = wallet.get_annotated_public_key(args.annotation) save_wallet(wallet) print("SKE" + human(public_key) + "PTI")
def read_chain_from_disk() -> CoinState: if os.path.isfile('chain.cache'): print("Reading cached chain") with open('chain.cache', 'rb') as file: coinstate = CoinState.load(lambda: pickle.load(file)) else: coinstate = CoinState.zero() rewrite = False if os.path.isdir('chain'): # the code below is no longer needed by normal users, but some old testcases still rely on it: for filename in sorted(os.listdir('chain')): (height_str, hash_str) = filename.split("-") (height, hash) = (int(height_str), computer(hash_str)) if hash not in coinstate.block_by_hash: if height % 1000 == 0: print(filename) if os.path.getsize(f"chain/{filename}") == 0: print("Stopping at empty block file: %s" % filename) break with open(Path("chain") / filename, 'rb') as f: try: block = Block.stream_deserialize(f) except Exception as e: raise Exception("Corrupted block on disk: %s" % filename) from e try: coinstate = coinstate.add_block_no_validation(block) except Exception: print("Failed to add block at height=%d, previous_hash=%s" % (block.height, human(block.header.summary.previous_block_hash))) break rewrite = True if rewrite: DiskInterface().write_chain_cache_to_disk(coinstate) return coinstate
def broadcast_block(self, block: Block) -> None: self.local_peer.logger.info("%15s ChainManager.broadcast_block(%s)" % ("", human(block.hash()))) self.broadcast_message(DataMessage(DATA_BLOCK, block))
def build_explorer(coinstate): assert os.getenv("EXPLORER_DIR") explorer_dir = Path(os.getenv("EXPLORER_DIR")) public_key_balances_2 = immutables.Map() for height in range(coinstate.head().height + 1): print("Block", height) block = coinstate.at_head.block_by_height[height] potential_message = block.transactions[0].inputs[0].signature.signature if all([(32 <= b < 127) or (b == 10) for b in potential_message]): msg = "```\n" + str(potential_message, encoding="ascii") + "\n```" else: msg = "" unspent_transaction_outs = get_unspent_transaction_outs_before_block(coinstate, block) public_key_balances_2 = build_pkb2_block(coinstate, block, public_key_balances_2) with open(explorer_dir / (human(block.hash()) + '.md'), 'w') as block_f: block_f.write(f"""## Block {human(block.hash())} Attribute | Value --- | --- Height | {block.height} Hash | {human(block.hash())} Timestamp | {datetime.fromtimestamp(block.timestamp, tz=timezone.utc).isoformat()} Target | {human(block.target)} Merke root | {human(block.merkle_root_hash)} Nonce | {block.nonce} {msg} ### Transactions Hash | Amount --- | --- """) for transaction in block.transactions: h = human(transaction.hash()) v = show_coin(sum(o.value for o in transaction.outputs)) block_f.write(f"""[{h}]({h}.md) | {v} \n""") with open(explorer_dir / (human(transaction.hash()) + ".md"), 'w') as transaction_f: transaction_f.write(f"""## Transaction {human(transaction.hash())} In block [{human(block.hash())}]({human(block.hash())}.md) ### Inputs Transaction | Output Index | Value | Address --- | --- | --- | --- """) for input in transaction.inputs: output_reference = input.output_reference if output_reference.hash != 32 * b'\x00': output = unspent_transaction_outs[output_reference] h = human(output_reference.hash) v = show_coin(output.value) a = "SKE" + human(output.public_key.public_key) + "PTI" transaction_f.write(f"""[{h}]({h}.md) | {output_reference.index} | """ f"""{v} | [{a}]({a}.md)\n""") else: h = human(output_reference.hash) v = "" a = "Thin Air" transaction_f.write(f"""{h} | {output_reference.index} | """ f"""{v} | {a}\n""") transaction_f.write("""### Outputs Value | Address --- | --- """) for output in transaction.outputs: v = show_coin(output.value) a = "SKE" + human(output.public_key.public_key) + "PTI" transaction_f.write(f"""{v} | [{a}]({a}.md)\n""") for pk, pkb2 in public_key_balances_2.items(): v = show_coin(pkb2.value) address = "SKE" + human(pk.public_key) + "PTI" with open(explorer_dir / (address + ".md"), 'w') as address_f: address_f.write(f"""## {address} Current balance: {v} (as of block {coinstate.head().height}) ## Received in Transaction | Output Index --- | --- """) for output_reference in pkb2.all_output_references: h = human(output_reference.hash) address_f.write(f"""[{h}]({h}.md) | {output_reference.index}\n""") if len(pkb2.spent_in_transactions) > 0: address_f.write(""" ## Spent in Transaction | ... --- | --- """) for transaction in pkb2.spent_in_transactions: address_f.write(f"""{human(transaction.hash())} | ...\n""") else: address_f.write(""" ## Spent in -- not spent -- """)
def save_transaction_for_debugging(self, transaction: Transaction) -> None: with open("/tmp/%s.transaction" % human(transaction.hash()), 'wb') as f: f.write(transaction.serialize())
def read_chain_from_disk() -> CoinState: if os.path.isfile('chain.cache'): print("Reading cached chain") with open('chain.cache', 'rb') as file: coinstate = CoinState.load(lambda: pickle.load(file)) else: try: print("Pre-download blockchain from trusted source to 'chain.zip'") with urllib.request.urlopen(TRUSTED_BLOCKCHAIN_ZIP) as resp: with open('chain.zip', 'wb') as outfile: outfile.write(resp.read()) print("Reading initial chain from zipfile") coinstate = CoinState.zero() with zipfile.ZipFile('chain.zip') as zip: for entry in zip.infolist(): if not entry.is_dir(): filename = entry.filename.split('/')[1] height = int(filename.split("-")[0]) if height % 1000 == 0: print(filename) data = zip.read(entry) block = Block.stream_deserialize(BytesIO(data)) coinstate = coinstate.add_block_no_validation(block) except Exception: print( "Error reading zip file. We'll start with an empty blockchain instead." + traceback.format_exc()) coinstate = CoinState.zero() rewrite = False if os.path.isdir('chain'): # the code below is no longer needed by normal users, but some old testcases still rely on it: for filename in sorted(os.listdir('chain')): (height_str, hash_str) = filename.split("-") (height, hash) = (int(height_str), computer(hash_str)) if hash not in coinstate.block_by_hash: if height % 1000 == 0: print(filename) if os.path.getsize(f"chain/{filename}") == 0: print("Stopping at empty block file: %s" % filename) break with open(Path("chain") / filename, 'rb') as f: try: block = Block.stream_deserialize(f) except Exception as e: raise Exception("Corrupted block on disk: %s" % filename) from e try: coinstate = coinstate.add_block_no_validation(block) except Exception: print( "Failed to add block at height=%d, previous_hash=%s" % (block.height, human(block.header.summary.previous_block_hash))) break rewrite = True if rewrite: DiskInterface().write_chain_cache_to_disk(coinstate) return coinstate