def get_previous_consensus_block_from_remote(self, block, peer): # TODO: async conversion retry = 0 while True: try: url = 'http://' + peer.to_string( ) + '/get-block?hash=' + block.prev_hash if self.debug: print('getting block', url) res = requests.get(url, timeout=1, headers={'Connection': 'close'}) except: if retry == 1: raise BadPeerException() else: retry += 1 continue try: if self.debug: print('response code: ', res.status_code) new_block = Block.from_dict( json.loads(res.content.decode('utf-8'))) if int(new_block.version) == CHAIN.get_version_for_height( new_block.index): return new_block else: return None except: return None
def __init__(self, blocks=None, partial=False): self.config = get_config() self.mongo = self.config.mongo self.blocks = [] last_index = None for block in blocks: if not isinstance(block, Block): block = Block.from_dict(block) if last_index and (block.index - last_index) != 1: raise Exception( 'Either incomplete blockchain or unordered. block {} vs last {}' .format(block.index, last_index)) # In that case: (most often, dup buried block), check if block n+1 exists then remove the wrong block(s) # see when inserting/replacing block how the dup insert occurs. self.blocks.append(block) last_index = block.index self.partial = partial if not self.blocks: return # allow nothing if self.blocks and self.blocks[0].index != 0 and not self.partial: raise Exception( 'Blocks do not start with zero index. Either incomplete blockchain or unordered.' )
def verify_existing_blockchain(self, reset=False): self.app_log.info('verifying existing blockchain') result = self.existing_blockchain.verify(self.output) if result['verified']: print('Block height: %s | time: %s' % (self.latest_block.index, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) return True else: self.app_log.debug(result) if 'last_good_block' in result: self.mongo.db.blocks.remove( {"index": { "$gt": result['last_good_block'].index }}, multi=True) else: self.mongo.db.blocks.remove({"index": {"$gt": 0}}, multi=True) self.app_log.debug("{} {}".format(result['message'], '...truncating')) self.config.BU.latest_block = None latest_block = self.config.BU.get_latest_block() if latest_block: self.latest_block = Block.from_dict(latest_block) else: if not self.prevent_genesis: self.insert_genesis() self.existing_blockchain = Blockchain(self.config.BU.get_blocks())
async def do_payout(self): # first check which blocks we won. # then determine if we have already paid out # they must be 6 blocks deep latest_block = Block.from_dict(await self.config.BU.get_latest_block_async()) won_blocks = self.config.mongo.async_db.blocks.find({ 'transactions.outputs.to': self.config.address, 'index': 37467 }).sort([('index', 1)]) async for won_block in won_blocks: won_block = Block.from_dict(won_block) if self.config.debug: self.app_log.debug(won_block.index) if (won_block.index + 6) <= latest_block.index: await self.do_payout_for_block(won_block)
def rank_consensus_blocks(self): # rank is based on target, total chain difficulty, and chain validity records = self.get_consensus_blocks_by_index(self.latest_block.index + 1) lowest = self.lowest ranks = [] for record in records: peer = Peer.from_string(record['peer']) block = Block.from_dict(record['block']) target = int(record['block']['hash'], 16) if target < lowest: ranks.append({'target': target, 'block': block, 'peer': peer}) return sorted(ranks, key=lambda x: x['target'])
async def get_next_consensus_block_from_local(self, block): #table cleanup new_block = await self.mongo.async_db.consensus.find_one({ 'block.prevHash': block.hash, 'block.index': (block.index + 1), 'block.version': CHAIN.get_version_for_height((block.index + 1)) }) if new_block: new_block = Block.from_dict(new_block['block']) if int(new_block.version) == CHAIN.get_version_for_height( new_block.index): return new_block else: return None return None
def __init__(self, debug=False, peers=None, prevent_genesis=False): self.app_log = logging.getLogger("tornado.application") self.debug = debug self.config = get_config() self.mongo = self.config.mongo self.prevent_genesis = prevent_genesis if peers: self.peers = peers else: self.peers = Peers() latest_block = self.config.BU.get_latest_block() if latest_block: self.latest_block = Block.from_dict(latest_block) else: if not self.prevent_genesis: self.insert_genesis() self.existing_blockchain = Blockchain(self.config.BU.get_blocks())
async def get_previous_consensus_block_from_local(self, block, peer): #table cleanup new_block = await self.mongo.async_db.consensus.find_one({ 'block.hash': block.prev_hash, 'block.index': (block.index - 1), 'block.version': CHAIN.get_version_for_height((block.index - 1)), 'ignore': { '$ne': True } }) if new_block: new_block = Block.from_dict(new_block['block']) if int(new_block.version) == CHAIN.get_version_for_height( new_block.index): return new_block else: return None return None
async def on_latest_block(self, data): from yadacoin.block import Block # Circular reference. Not good! - Do we need the object here? # processing in this object rather than ClientChatNamespace so consensus data is available from peers self.latest_peer_block = Block.from_dict(data) if not self.peers.syncing: self.app_log.debug("Trying to sync on latest block from {}".format( self.peer.to_string())) my_index = self.config.BU.get_latest_block()['index'] if data['index'] == my_index + 1: self.app_log.debug( "Next index, trying to merge from {}".format( self.peer.to_string())) if await self.consensus.process_next_block(data, self.peer): pass # if ok, block was inserted and event triggered by import block # await self.peers.on_block_insert(data) elif data['index'] > my_index + 1: self.app_log.debug( "Missing blocks between {} and {} , asking more to {}". format(my_index, data['index'], self.peer.to_string())) data = { "start_index": my_index + 1, "end_index": my_index + 1 + CHAIN.MAX_BLOCKS_PER_MESSAGE } await self.client.emit('get_blocks', data=data, namespace="/chat") elif data['index'] == my_index: self.app_log.debug("Same index, ignoring {} from {}".format( data['index'], self.peer.to_string())) else: # We have better self.app_log.debug( "We have higher index, sending {} to ws {}".format( data['index'], self.peer.to_string())) block = self.config.BU.get_latest_block() block['time_utc'] = ts_to_utc(block['time']) await self.client.emit('latest_block', data=block, namespace="/chat")
async def process_next_block(self, block_data: dict, peer, trigger_event=True) -> bool: """This is the common entry point for all new possible blocks to enter consensus and chain""" block_object = Block.from_dict(block_data) if block_object.in_the_future(): # Most important self.app_log.warning( 'Block in the future for height %s from peer: %s' % (block_object.index, peer.to_string())) return False # TODO: there is no check before inserting into consensus (previous hash, nor diff, nor valid tx) await self.insert_consensus_block(block_object, peer) self.app_log.debug("Consensus ok {}".format(block_object.index)) res = await self.import_block( { 'peer': peer.to_string(), 'block': block_data }, trigger_event=trigger_event) self.app_log.debug("Import_block {} {}".format(block_object.index, res)) return res
async def retrace(self, block, peer): """We got a non compatible block. Retrace other chains to find a common ancestor and evaluate chains.""" # TODO: more async conversion TBD here. Low priority since not called often atm. # TODO: cleanup print and logging # TODO: limit possible retrace blocks vs max(known chains) - store in chain config try: self.app_log.info("Retracing...") blocks = [block] while 1: if self.debug: self.app_log.info("{} : {}".format(block.hash, block.index)) # get the previous block from either the consensus collection in mongo # or attempt to get the block from the remote peer previous_consensus_block = await self.get_previous_consensus_block_from_local( block, peer) if previous_consensus_block: block = previous_consensus_block blocks.append(block) else: if peer.is_me: self.mongo.db.consensus.update( { 'peer': peer.to_string(), 'index': { '$gte': block.index } }, {'$set': { 'ignore': True }}, multi=True) return try: previous_consensus_block = self.get_previous_consensus_block_from_remote( block, peer) except BadPeerException as e: self.mongo.db.consensus.update( { 'peer': peer.to_string(), 'index': { '$gte': block.index } }, {'$set': { 'ignore': True }}, multi=True) except: pass if previous_consensus_block and previous_consensus_block.index + 1 == block.index: block = previous_consensus_block blocks.append(block) try: await self.insert_consensus_block(block, peer) except Exception as e: if self.debug: self.app_log.warning( "Exception retrace insert_consensus_block: {}" .format(e) ) # we should do something here to keep it from looping on this failed block else: # identify missing and prune # if the pruned chain is still longer, we'll take it if previous_consensus_block: block = previous_consensus_block blocks = [block] else: return if self.debug: self.app_log.info('attempting sync at {}, len {}'.format( block.prev_hash, len(self.existing_blockchain.blocks))) # if they do have it, query our consensus collection for prevHash of that block, repeat 1 and 2 until index 1 if self.existing_blockchain.blocks[block.index - 1].hash == block.prev_hash: prev_blocks_check = self.existing_blockchain.blocks[ block.index - 1] if self.debug: self.app_log.debug("Previous block {}: {}".format( prev_blocks_check.hash, prev_blocks_check.index)) blocks = sorted(blocks, key=lambda x: x.index) block_for_next = blocks[-1] while 1: next_block = await self.get_next_consensus_block_from_local( block_for_next) if next_block: blocks.append(next_block) block_for_next = next_block else: break # self.peers.init(self.config.network) self.app_log.debug( 'requesting {} ...'.format(block_for_next.index + 1)) for apeer in self.peers.peers: # TODO: there was a "while 1:" there, that got the retrace stuck with only 1 peer and no escape route. # recheck the logic. try: # if self.debug: # self.app_log.debug('requesting {} from {}'.format(block_for_next.index + 1, apeer.to_string())) result = requests.get( 'http://{peer}/get-blocks?start_index={start_index}&end_index={end_index}' .format(peer=apeer.to_string(), start_index=block_for_next.index + 1, end_index=block_for_next.index + 100), timeout=1) remote_blocks = [ Block.from_dict(x) for x in json.loads(result.content) ] break_out = False for remote_block in remote_blocks: if remote_block.prev_hash == block_for_next.hash: blocks.append(remote_block) block_for_next = remote_block else: break_out = True break if break_out: break except Exception as e: if self.debug: print(e) break # if we have it in our blockchain, then we've hit the fork point # now we have to loop through the current block array and build a blockchain # then we compare the block height and difficulty of the two chains # replace our current chain if necessary by removing them from the database # then looping though our new chain, inserting the new blocks def subchain_gen(existing_blocks, addon_blocks, gen_block): for x in existing_blocks[:addon_blocks[0].index]: if x.index < gen_block.index: yield x for x in addon_blocks: yield x # If the block height is equal, we throw out the inbound chain, it muse be greater # If the block height is lower, we throw it out # if the block height is heigher, we compare the difficulty of the entire chain existing_difficulty = self.get_difficulty( self.existing_blockchain.blocks) inbound_difficulty = self.get_difficulty( subchain_gen(self.existing_blockchain.blocks, blocks, block)) if (blocks[-1].index >= self.existing_blockchain.blocks[-1].index and inbound_difficulty >= existing_difficulty): for block in blocks: try: if block.index == 0: continue await self.integrate_block_with_existing_chain( block) if self.debug: self.app_log.debug('inserted {}'.format( block.index)) except ForkException as e: back_one_block = block while 1: back_one_block = self.mongo.db.consensus.find_one( { 'block.hash': back_one_block.prev_hash }) if back_one_block: back_one_block = Block.from_dict( back_one_block['block']) try: result = self.integrate_block_with_existing_chain( back_one_block) if result: await self.integrate_block_with_existing_chain( block) break except ForkException as e: pass else: return except AboveTargetException as e: return except IndexError as e: return self.app_log.info( "Retrace result: replaced chain with incoming") return else: if not peer.is_me: if self.debug: self.app_log.info( "Incoming chain lost {} {} {} {}".format( inbound_difficulty, existing_difficulty, blocks[-1].index, self.existing_blockchain.blocks[-1]. index)) for block in blocks: self.mongo.db.consensus.update( {'block.hash': block.hash}, {'$set': { 'ignore': True }}, multi=True) return # lets go down the hash path to see where prevHash is in our blockchain, hopefully before the genesis block # we need some way of making sure we have all previous blocks until we hit a block with prevHash in our main blockchain #there is no else, we just loop again # if we get to index 1 and prev hash doesn't match the genesis, throw out the chain and black list the peer # if we get a fork point, prevHash is found in our consensus or genesis, then we compare the current # blockchain against the proposed chain. # TODO: Here, compare vs current known consensus height, and limit to consensus height - CHAIN.MAX_RETRACE_DEPTH if block.index == 0: self.app_log.info("Retrace result: zero index reached") return self.app_log.info("Retrace result: doesn't follow any known chain" ) # throwing out the block for now return except Exception as e: exc_type, exc_obj, exc_tb = exc_info() fname = path.split(exc_tb.tb_frame.f_code.co_filename)[1] self.app_log.warning("{} {} {}".format(exc_type, fname, exc_tb.tb_lineno)) raise
async def integrate_block_with_existing_chain(self, block: Block, extra_blocks=None): """Even in case of retrace, this is the only place where we insert a new block into the block collection and update BU""" try: # TODO: reorg the checks, to have the faster ones first. # Like, here we begin with checking every tx one by one, when <e did not even check index and provided hash matched previous one. try: block.verify() except Exception as e: print("Integrate block error 1", e) return False for transaction in block.transactions: try: if extra_blocks: transaction.extra_blocks = extra_blocks transaction.verify() except InvalidTransactionException as e: print(e) return False except InvalidTransactionSignatureException as e: print(e) return False except MissingInputTransactionException as e: print(e) return False except NotEnoughMoneyException as e: print(e) return False except Exception as e: print(e) return False if block.index == 0: return True height = block.index last_block = self.existing_blockchain.blocks[block.index - 1] if last_block.index != (block.index - 1) or last_block.hash != block.prev_hash: print("Integrate block error 2") raise ForkException() if not last_block: print("Integrate block error 3") raise ForkException() target = BlockFactory.get_target(height, last_block, block, self.existing_blockchain) delta_t = int(time()) - int(last_block.time) special_target = CHAIN.special_target(block.index, block.target, delta_t, get_config().network) target_block_time = CHAIN.target_block_time(self.config.network) if block.index >= 35200 and delta_t < 600 and block.special_min: raise Exception('Special min block too soon') # TODO: use a CHAIN constant for pow blocks limits if ((int(block.hash, 16) < target) or (block.special_min and int(block.hash, 16) < special_target) or (block.special_min and block.index < 35200) or (block.index >= 35200 and block.index < 38600 and block.special_min and (int(block.time) - int(last_block.time)) > target_block_time) ): if last_block.index == ( block.index - 1) and last_block.hash == block.prev_hash: # self.mongo.db.blocks.update({'index': block.index}, block.to_dict(), upsert=True) # self.mongo.db.blocks.remove({'index': {"$gt": block.index}}, multi=True) # todo: is this useful? can we have more blocks above? No because if we had, we would have raised just above await self.mongo.async_db.block.delete_many( {'index': { "$gte": block.index }}) await self.mongo.async_db.blocks.replace_one( {'index': block.index}, block.to_dict(), upsert=True) # TODO: why do we need to keep that one in memory? try: self.existing_blockchain.blocks[block.index] = block del self.existing_blockchain.blocks[block.index + 1:] except: self.existing_blockchain.blocks.append(block) if self.debug: self.app_log.info( "New block inserted for height: {}".format( block.index)) await self.config.on_new_block( block) # This will propagate to BU return True else: print("Integrate block error 4") raise ForkException() else: print("Integrate block error 5") raise AboveTargetException() return False # unreachable code except Exception as e: exc_type, exc_obj, exc_tb = exc_info() fname = path.split(exc_tb.tb_frame.f_code.co_filename)[1] self.app_log.warning( "integrate_block_with_existing_chain {} {} {}".format( exc_type, fname, exc_tb.tb_lineno)) raise
async def import_block(self, block_data: dict, trigger_event=True) -> bool: """Block_data contains peer and block keys. Tries to import that block, retrace if necessary sends True if that block was inserted, False if it fails or if a retrace was needed. This is the central entry point for inserting a block, that will modify the local chain and trigger the event, unless we asked not to, because we're in a batch insert context""" try: block = Block.from_dict(block_data['block']) peer = Peer.from_string(block_data['peer']) if 'extra_blocks' in block_data: extra_blocks = None # extra_blocks = [Block.from_dict( x) for x in block_data['extra_blocks']] # Not used later on, just ram and resources usage else: extra_blocks = None self.app_log.debug("Latest block was {} {} {} {}".format( self.latest_block.hash, block.prev_hash, self.latest_block.index, (block.index - 1))) if int(block.index) > CHAIN.CHECK_TIME_FROM and int( block.time) < int(self.latest_block.time): self.app_log.warning( "New block {} can't be at a sooner time than previous one. Rejecting" .format(block.index)) await self.mongo.async_db.consensus.update_one( { 'peer': peer.to_string(), 'index': block.index, 'id': block.signature }, {'$set': { 'ignore': True }}) return False if int(block.index) > CHAIN.CHECK_TIME_FROM and ( int(block.time) < (int(self.latest_block.time) + 600)) and block.special_min: self.app_log.warning( "New special min block {} too soon. Rejecting".format( block.index)) await self.mongo.async_db.consensus.update_one( { 'peer': peer.to_string(), 'index': block.index, 'id': block.signature }, {'$set': { 'ignore': True }}) return False try: result = await self.integrate_block_with_existing_chain( block, extra_blocks) if result is False: # TODO: factorize await self.mongo.async_db.consensus.update_one( { 'peer': peer.to_string(), 'index': block.index, 'id': block.signature }, {'$set': { 'ignore': True }}) elif trigger_event: await self.trigger_update_event(block_data['block']) return result except DuplicateKeyError as e: await self.mongo.async_db.consensus.update_one( { 'peer': peer.to_string(), 'index': block.index, 'id': block.signature }, {'$set': { 'ignore': True }}) except AboveTargetException as e: await self.mongo.async_db.consensus.update_one( { 'peer': peer.to_string(), 'index': block.index, 'id': block.signature }, {'$set': { 'ignore': True }}) except ForkException as e: await self.retrace(block, peer) if trigger_event: await self.trigger_update_event() return False except IndexError as e: await self.retrace(block, peer) if trigger_event: await self.trigger_update_event() return False except Exception as e: print("348", e) exc_type, exc_obj, exc_tb = exc_info() fname = path.split(exc_tb.tb_frame.f_code.co_filename)[1] self.app_log.warning("{} {} {}".format(exc_type, fname, exc_tb.tb_lineno)) await self.mongo.async_db.consensus.update_one( { 'peer': peer.to_string(), 'index': block.index, 'id': block.signature }, {'$set': { 'ignore': True }}) except Exception as e: exc_type, exc_obj, exc_tb = exc_info() fname = path.split(exc_tb.tb_frame.f_code.co_filename)[1] self.app_log.warning("{} {} {}".format(exc_type, fname, exc_tb.tb_lineno)) if trigger_event: await self.trigger_update_event() return False if trigger_event: await self.trigger_update_event() return True
async def search_network_for_new(self): # Peers.init( self.config.network) if self.config.network == 'regnet': return if self.peers.syncing: self.app_log.debug( "Already syncing, ignoring search_network_for_new") if len(self.config.force_polling): # This is a temp hack until everyone updated polling_peers = [ "{}:{}".format(peer['host'], peer['port']) for peer in self.config.force_polling ] else: if len(self.peers.peers) < 2: await self.peers.refresh() await async_sleep(20) if len(self.peers.peers) < 1: self.app_log.info("No peer to connect to yet") await async_sleep(10) return polling_peers = [peer.to_string() for peer in self.peers.peers] # TODO: use an aio lock self.app_log.debug('requesting {} ...'.format(self.latest_block.index + 1)) http_client = AsyncHTTPClient() # for peer in self.peers.peers: for peer_string in polling_peers: self.peers.syncing = True try: self.app_log.debug('requesting {} from {}'.format( self.latest_block.index + 1, peer_string)) peer = Peer.from_string(peer_string) try: url = 'http://{peer}/get-blocks?start_index={start_index}&end_index={end_index}'\ .format(peer=peer_string, start_index=int(self.latest_block.index) +1, end_index=int(self.latest_block.index) + 100) request = HTTPRequest(url, connect_timeout=3, request_timeout=5) response = await http_client.fetch(request) if response.code != 200: continue # result = requests.get(url, timeout=2) except HTTPError as e: self.app_log.warning( 'Error requesting from {} ...'.format(peer_string)) # add to failed peers await self.peers.increment_failed(peer) continue except ConnectTimeoutError as e: self.app_log.warning( 'Timeout requesting from {} ...'.format(peer_string)) # add to failed peers await self.peers.increment_failed(peer) continue except Exception as e: self.app_log.error( 'error {} requesting from {} ...'.format( e, peer_string)) await self.peers.increment_failed(peer) continue try: blocks = json.loads(response.body.decode('utf-8')) # blocks = json.loads(result.content) except ValueError: continue inserted = False for block in blocks: # print("looking for ", self.existing_blockchain.blocks[-1].index + 1) block = Block.from_dict(block) if block.index == ( self.existing_blockchain.blocks[-1].index + 1): await self.insert_consensus_block(block, peer) # print("consensus ok", block.index) res = await self.import_block( { 'peer': peer_string, 'block': block.to_dict(), 'extra_blocks': blocks }, trigger_event=False) # print("import ", block.index, res) if res: self.latest_block = block inserted = True else: # 2 cases: bad block, or retrace. if self.existing_blockchain.blocks[ -1].index == self.latest_block.index: # bad block, nothing moved, early exit self.app_log.debug('Bad block {}'.format( block.index)) else: # retraced, sync self.latest_block = Block.from_dict( await self.config.BU.get_latest_block_async()) self.app_log.debug('retraced up to {}'.format( self.latest_block.index)) inserted = True # in both case, no need to process further blocks break else: break #print("pass", block.index) if inserted: await self.trigger_update_event() # await self.peers.on_block_insert(self.latest_block.to_dict()) except Exception as e: if self.debug: self.app_log.warning(e) finally: self.peers.syncing = False
def get_latest_consensus_block(self): latests = self.get_latest_consensus_blocks() for latest in latests: if int(latest['block']['version']) == CHAIN.get_version_for_height( latest['block']['index']): return Block.from_dict(latest['block'])
async def sync_bottom_up(self): try: #bottom up syncing last_latest = self.latest_block self.latest_block = Block.from_dict( await self.config.BU.get_latest_block_async()) if self.latest_block.index > last_latest.index: self.app_log.info( 'Block height: %s | time: %s' % (self.latest_block.index, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) self.remove_pending_transactions_now_in_chain() self.remove_fastgraph_transactions_now_in_chain() latest_consensus = await self.mongo.async_db.consensus.find_one({ 'index': self.latest_block.index + 1, 'block.version': CHAIN.get_version_for_height(self.latest_block.index + 1), 'ignore': { '$ne': True } }) if latest_consensus: latest_consensus = Block.from_dict(latest_consensus['block']) if self.debug: self.app_log.info("Latest consensus_block {}".format( latest_consensus.index)) records = await self.mongo.async_db.consensus.find({ 'index': self.latest_block.index + 1, 'block.version': CHAIN.get_version_for_height(self.latest_block.index + 1), 'ignore': { '$ne': True } }).to_list(length=100) for record in sorted( records, key=lambda x: int(x['block']['target'], 16)): await self.import_block(record) last_latest = self.latest_block self.latest_block = Block.from_dict( await self.config.BU.get_latest_block_async()) if self.latest_block.index > last_latest.index: self.app_log.info('Block height: %s | time: %s' % ( self.latest_block.index, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) latest_consensus_now = await self.mongo.async_db.consensus.find_one( { 'index': self.latest_block.index + 1, 'block.version': CHAIN.get_version_for_height(self.latest_block.index + 1), 'ignore': { '$ne': True } }) if latest_consensus_now and latest_consensus.index == latest_consensus_now[ 'index']: await self.search_network_for_new() return True else: await self.search_network_for_new() return True except Exception as e: exc_type, exc_obj, exc_tb = exc_info() fname = path.split(exc_tb.tb_frame.f_code.co_filename)[1] self.app_log.warning("{} {} {}".format(exc_type, fname, exc_tb.tb_lineno)) raise
def get_block_objs(self): from yadacoin.block import Block # from yadacoin.transaction import Transaction, Input, Crypt blocks = self.get_blocks() block_objs = [Block.from_dict(block) for block in blocks] return block_objs