async def get_block_record(self, header_hash: bytes32) -> BlockRecord: cursor = await self.db_connection.execute( "SELECT * from block_records WHERE header_hash=?", (header_hash.hex(),) ) row = await cursor.fetchone() await cursor.close() return BlockRecord.from_bytes(row[3])
async def get_block_record(self, header_hash: bytes32) -> Optional[BlockRecord]: """Gets a block record from the database, if present""" cursor = await self.db_connection.execute( "SELECT * from block_records WHERE header_hash=?", (header_hash.hex(),) ) row = await cursor.fetchone() await cursor.close() if row is not None: return BlockRecord.from_bytes(row[3]) else: return None
async def get_lca_path(self) -> Dict[bytes32, BlockRecord]: """ Returns block records representing the blockchain from the genesis block up to the LCA (least common ancestor). Note that the DB also contains many blocks not on this path, due to reorgs. """ cursor = await self.db_connection.execute( "SELECT * from block_records WHERE in_lca_path=1") rows = await cursor.fetchall() await cursor.close() hash_to_br: Dict = {} max_height = -1 for row in rows: br = BlockRecord.from_bytes(row[4]) hash_to_br[bytes.fromhex(row[0])] = br assert row[0] == br.header_hash.hex() assert row[1] == br.height if br.height > max_height: max_height = br.height # Makes sure there's exactly one block per height assert max_height == len(list(rows)) - 1 return hash_to_br
async def test_store(self): db_filename = Path("blockchain_wallet_store_test.db") if db_filename.exists(): db_filename.unlink() db_connection = await aiosqlite.connect(db_filename) store = await WalletStore.create(db_connection) try: coin_1 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_2 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_3 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_4 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) record_replaced = WalletCoinRecord(coin_1, uint32(8), uint32(0), False, True, WalletType.STANDARD_WALLET, 0) record_1 = WalletCoinRecord(coin_1, uint32(4), uint32(0), False, True, WalletType.STANDARD_WALLET, 0) record_2 = WalletCoinRecord(coin_2, uint32(5), uint32(0), False, True, WalletType.STANDARD_WALLET, 0) record_3 = WalletCoinRecord( coin_3, uint32(5), uint32(10), True, False, WalletType.STANDARD_WALLET, 0, ) record_4 = WalletCoinRecord( coin_4, uint32(5), uint32(15), True, False, WalletType.STANDARD_WALLET, 0, ) # Test add (replace) and get assert await store.get_coin_record(coin_1.name()) is None await store.add_coin_record(record_replaced) await store.add_coin_record(record_1) await store.add_coin_record(record_2) await store.add_coin_record(record_3) await store.add_coin_record(record_4) assert await store.get_coin_record(coin_1.name()) == record_1 # Test persistance await db_connection.close() db_connection = await aiosqlite.connect(db_filename) store = await WalletStore.create(db_connection) assert await store.get_coin_record(coin_1.name()) == record_1 # Test set spent await store.set_spent(coin_1.name(), uint32(12)) assert (await store.get_coin_record(coin_1.name())).spent assert (await store.get_coin_record(coin_1.name() )).spent_block_index == 12 # No coins at height 3 assert len(await store.get_unspent_coins_at_height(3)) == 0 assert len(await store.get_unspent_coins_at_height(4)) == 1 assert len(await store.get_unspent_coins_at_height(5)) == 4 assert len(await store.get_unspent_coins_at_height(11)) == 3 assert len(await store.get_unspent_coins_at_height(12)) == 2 assert len(await store.get_unspent_coins_at_height(15)) == 1 assert len(await store.get_unspent_coins_at_height(16)) == 1 assert len(await store.get_unspent_coins_at_height()) == 1 assert len(await store.get_unspent_coins_for_wallet(0)) == 1 assert len(await store.get_unspent_coins_for_wallet(1)) == 0 coin_5 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) record_5 = WalletCoinRecord( coin_5, uint32(5), uint32(15), False, False, WalletType.STANDARD_WALLET, 1, ) await store.add_coin_record(record_5) assert len(await store.get_unspent_coins_for_wallet(1)) == 1 assert len(await store.get_spendable_for_index(100, 1)) == 1 assert len(await store.get_spendable_for_index(100, 0)) == 1 assert len(await store.get_spendable_for_index(0, 0)) == 0 coin_6 = Coin(token_bytes(32), coin_4.puzzle_hash, uint64(12312)) await store.add_coin_record(record_5) record_6 = WalletCoinRecord( coin_6, uint32(5), uint32(15), True, False, WalletType.STANDARD_WALLET, 2, ) await store.add_coin_record(record_6) assert (len(await store.get_coin_records_by_puzzle_hash( record_6.coin.puzzle_hash)) == 2) # 4 and 6 assert (len(await store.get_coin_records_by_puzzle_hash(token_bytes(32) )) == 0) assert await store.get_coin_record_by_coin_id(coin_6.name() ) == record_6 assert await store.get_coin_record_by_coin_id(token_bytes(32) ) is None # BLOCKS assert len(await store.get_lca_path()) == 0 # NOT lca block br_1 = BlockRecord( token_bytes(32), token_bytes(32), uint32(0), uint128(100), None, None, None, None, uint64(0), ) assert await store.get_block_record(br_1.header_hash) is None await store.add_block_record(br_1, False) assert len(await store.get_lca_path()) == 0 assert await store.get_block_record(br_1.header_hash) == br_1 # LCA genesis await store.add_block_record(br_1, True) assert await store.get_block_record(br_1.header_hash) == br_1 assert len(await store.get_lca_path()) == 1 assert (await store.get_lca_path())[br_1.header_hash] == br_1 br_2 = BlockRecord( token_bytes(32), token_bytes(32), uint32(1), uint128(100), None, None, None, None, uint64(0), ) await store.add_block_record(br_2, False) assert len(await store.get_lca_path()) == 1 await store.add_block_to_path(br_2.header_hash) assert len(await store.get_lca_path()) == 2 assert (await store.get_lca_path())[br_2.header_hash] == br_2 br_3 = BlockRecord( token_bytes(32), token_bytes(32), uint32(2), uint128(100), None, None, None, None, uint64(0), ) await store.add_block_record(br_3, True) assert len(await store.get_lca_path()) == 3 await store.remove_blocks_from_path(1) assert len(await store.get_lca_path()) == 2 await store.rollback_lca_to_block(0) assert len(await store.get_unspent_coins_at_height()) == 0 coin_7 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_8 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_9 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_10 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) record_7 = WalletCoinRecord(coin_7, uint32(0), uint32(1), True, False, WalletType.STANDARD_WALLET, 1) record_8 = WalletCoinRecord(coin_8, uint32(1), uint32(2), True, False, WalletType.STANDARD_WALLET, 1) record_9 = WalletCoinRecord(coin_9, uint32(2), uint32(3), True, False, WalletType.STANDARD_WALLET, 1) record_10 = WalletCoinRecord( coin_10, uint32(3), uint32(4), True, False, WalletType.STANDARD_WALLET, 1, ) await store.add_coin_record(record_7) await store.add_coin_record(record_8) await store.add_coin_record(record_9) await store.add_coin_record(record_10) assert len(await store.get_unspent_coins_at_height(0)) == 1 assert len(await store.get_unspent_coins_at_height(1)) == 1 assert len(await store.get_unspent_coins_at_height(2)) == 1 assert len(await store.get_unspent_coins_at_height(3)) == 1 assert len(await store.get_unspent_coins_at_height(4)) == 0 await store.add_block_record(br_2, True) await store.add_block_record(br_3, True) await store.rollback_lca_to_block(1) assert len(await store.get_unspent_coins_at_height(0)) == 1 assert len(await store.get_unspent_coins_at_height(1)) == 1 assert len(await store.get_unspent_coins_at_height(2)) == 1 assert len(await store.get_unspent_coins_at_height(3)) == 1 assert len(await store.get_unspent_coins_at_height(4)) == 1 except AssertionError: await db_connection.close() raise await db_connection.close()
async def respond_additions(self, response: wallet_protocol.RespondAdditions): """ The full node has responded with the additions for a block. We will use this to try to finish the block, and add it to the state. """ if self._shut_down: return if response.header_hash not in self.cached_blocks: self.log.warning("Do not have header for additions") return block_record, header_block, transaction_filter = self.cached_blocks[ response.header_hash] assert response.height == block_record.height additions: List[Coin] if response.proofs is None: # If there are no proofs, it means all additions were returned in the response. # we must find the ones relevant to our wallets. all_coins: List[Coin] = [] for puzzle_hash, coin_list_0 in response.coins: all_coins += coin_list_0 additions = await self.wallet_state_manager.get_relevant_additions( all_coins) # Verify root additions_merkle_set = MerkleSet() # Addition Merkle set contains puzzlehash and hash of all coins with that puzzlehash for puzzle_hash, coins in response.coins: additions_merkle_set.add_already_hashed(puzzle_hash) additions_merkle_set.add_already_hashed(hash_coin_list(coins)) additions_root = additions_merkle_set.get_root() if header_block.header.data.additions_root != additions_root: return else: # This means the full node has responded only with the relevant additions # for our wallet. Each merkle proof must be verified. additions = [] assert len(response.coins) == len(response.proofs) for i in range(len(response.coins)): assert response.coins[i][0] == response.proofs[i][0] coin_list_1: List[Coin] = response.coins[i][1] puzzle_hash_proof: bytes32 = response.proofs[i][1] coin_list_proof: Optional[bytes32] = response.proofs[i][2] if len(coin_list_1) == 0: # Verify exclusion proof for puzzle hash assert confirm_not_included_already_hashed( header_block.header.data.additions_root, response.coins[i][0], puzzle_hash_proof, ) else: # Verify inclusion proof for puzzle hash assert confirm_included_already_hashed( header_block.header.data.additions_root, response.coins[i][0], puzzle_hash_proof, ) # Verify inclusion proof for coin list assert confirm_included_already_hashed( header_block.header.data.additions_root, hash_coin_list(coin_list_1), coin_list_proof, ) for coin in coin_list_1: assert coin.puzzle_hash == response.coins[i][0] additions += coin_list_1 new_br = BlockRecord( block_record.header_hash, block_record.prev_header_hash, block_record.height, block_record.weight, additions, None, block_record.total_iters, header_block.challenge.get_hash(), ) self.cached_blocks[response.header_hash] = ( new_br, header_block, transaction_filter, ) if transaction_filter is None: raise RuntimeError("Got additions for block with no transactions.") ( _, removals, ) = await self.wallet_state_manager.get_filter_additions_removals( new_br, transaction_filter) request_all_removals = False for coin in additions: puzzle_store = self.wallet_state_manager.puzzle_store record_info: Optional[ DerivationRecord] = await puzzle_store.get_derivation_record_for_puzzle_hash( coin.puzzle_hash.hex()) if (record_info is not None and record_info.wallet_type == WalletType.COLOURED_COIN): request_all_removals = True break if len(removals) > 0 or request_all_removals: if request_all_removals: request_r = wallet_protocol.RequestRemovals( header_block.height, header_block.header_hash, None) else: request_r = wallet_protocol.RequestRemovals( header_block.height, header_block.header_hash, removals) yield OutboundMessage( NodeType.FULL_NODE, Message("request_removals", request_r), Delivery.RESPOND, ) else: # We have collected all three things: header, additions, and removals (since there are no # relevant removals for us). Can proceed. Otherwise, we wait for the removals to arrive. new_br = BlockRecord( new_br.header_hash, new_br.prev_header_hash, new_br.height, new_br.weight, new_br.additions, [], new_br.total_iters, new_br.new_challenge_hash, ) respond_header_msg: Optional[ wallet_protocol.RespondHeader] = await self._block_finished( new_br, header_block, transaction_filter) if respond_header_msg is not None: async for msg in self.respond_header(respond_header_msg): yield msg
async def respond_header(self, response: wallet_protocol.RespondHeader): """ The full node responds to our RequestHeader call. We cannot finish this block until we have the required additions / removals for our wallets. """ while True: if self._shut_down: return # We loop, to avoid infinite recursion. At the end of each iteration, we might want to # process the next block, if it exists. block = response.header_block # If we already have, return if block.header_hash in self.wallet_state_manager.block_records: return if block.height < 1: return block_record = BlockRecord( block.header_hash, block.prev_header_hash, block.height, block.weight, None, None, response.header_block.header.data.total_iters, response.header_block.challenge.get_hash(), ) if self.wallet_state_manager.sync_mode: self.potential_blocks_received[uint32(block.height)].set() self.potential_header_hashes[block.height] = block.header_hash # Caches the block so we can finalize it when additions and removals arrive self.cached_blocks[block_record.header_hash] = ( block_record, block, response.transactions_filter, ) if block.prev_header_hash not in self.wallet_state_manager.block_records: # We do not have the previous block record, so wait for that. When the previous gets added to chain, # this method will get called again and we can continue. During sync, the previous blocks are already # requested. During normal operation, this might not be the case. self.future_block_hashes[ block.prev_header_hash] = block.header_hash lca = self.wallet_state_manager.block_records[ self.wallet_state_manager.lca] if (block_record.height - lca.height < self.short_sync_threshold and not self.wallet_state_manager.sync_mode): # Only requests the previous block if we are not in sync mode, close to the new block, # and don't have prev header_request = wallet_protocol.RequestHeader( uint32(block_record.height - 1), block_record.prev_header_hash, ) yield OutboundMessage( NodeType.FULL_NODE, Message("request_header", header_request), Delivery.RESPOND, ) return # If the block has transactions that we are interested in, fetch adds/deletes if response.transactions_filter is not None: ( additions, removals, ) = await self.wallet_state_manager.get_filter_additions_removals( block_record, response.transactions_filter) if len(additions) > 0 or len(removals) > 0: request_a = wallet_protocol.RequestAdditions( block.height, block.header_hash, additions) yield OutboundMessage( NodeType.FULL_NODE, Message("request_additions", request_a), Delivery.RESPOND, ) return # If we don't have any transactions in filter, don't fetch, and finish the block block_record = BlockRecord( block_record.header_hash, block_record.prev_header_hash, block_record.height, block_record.weight, [], [], block_record.total_iters, block_record.new_challenge_hash, ) respond_header_msg: Optional[ wallet_protocol.RespondHeader] = await self._block_finished( block_record, block, response.transactions_filter) if respond_header_msg is None: return else: response = respond_header_msg
async def _sync(self): """ Wallet has fallen far behind (or is starting up for the first time), and must be synced up to the LCA of the blockchain. """ # 1. Get all header hashes self.header_hashes = [] self.header_hashes_error = False self.proof_hashes = [] self.potential_header_hashes = {} genesis = FullBlock.from_bytes(self.constants["GENESIS_BLOCK"]) genesis_challenge = genesis.proof_of_space.challenge_hash request_header_hashes = wallet_protocol.RequestAllHeaderHashesAfter( uint32(0), genesis_challenge) yield OutboundMessage( NodeType.FULL_NODE, Message("request_all_header_hashes_after", request_header_hashes), Delivery.RESPOND, ) timeout = 100 sleep_interval = 10 sleep_interval_short = 1 start_wait = time.time() while time.time() - start_wait < timeout: if self._shut_down: return if self.header_hashes_error: raise ValueError( f"Received error from full node while fetching hashes from {request_header_hashes}." ) if len(self.header_hashes) > 0: break await asyncio.sleep(0.5) if len(self.header_hashes) == 0: raise TimeoutError("Took too long to fetch header hashes.") # 2. Find fork point fork_point_height: uint32 = self.wallet_state_manager.find_fork_point_alternate_chain( self.header_hashes) fork_point_hash: bytes32 = self.header_hashes[fork_point_height] # Sync a little behind, in case there is a short reorg tip_height = (len(self.header_hashes) - 5 if len(self.header_hashes) > 5 else len(self.header_hashes)) self.log.info( f"Fork point: {fork_point_hash} at height {fork_point_height}. Will sync up to {tip_height}" ) for height in range(0, tip_height + 1): self.potential_blocks_received[uint32(height)] = asyncio.Event() header_validate_start_height: uint32 if self.config["starting_height"] == 0: header_validate_start_height = fork_point_height else: # Request all proof hashes request_proof_hashes = wallet_protocol.RequestAllProofHashes() yield OutboundMessage( NodeType.FULL_NODE, Message("request_all_proof_hashes", request_proof_hashes), Delivery.RESPOND, ) start_wait = time.time() while time.time() - start_wait < timeout: if self._shut_down: return if len(self.proof_hashes) > 0: break await asyncio.sleep(0.5) if len(self.proof_hashes) == 0: raise TimeoutError("Took too long to fetch proof hashes.") if len(self.proof_hashes) < tip_height: raise ValueError("Not enough proof hashes fetched.") # Creates map from height to difficulty heights: List[uint32] = [] difficulty_weights: List[uint64] = [] difficulty: uint64 for i in range(tip_height): if self.proof_hashes[i][1] is not None: difficulty = self.proof_hashes[i][1] if i > (fork_point_height + 1) and i % 2 == 1: # Only add odd heights heights.append(uint32(i)) difficulty_weights.append(difficulty) # Randomly sample based on difficulty query_heights_odd = sorted( list( set( random.choices(heights, difficulty_weights, k=min(100, len(heights)))))) query_heights: List[uint32] = [] for odd_height in query_heights_odd: query_heights += [uint32(odd_height - 1), odd_height] # Send requests for these heights # Verify these proofs last_request_time = float(0) highest_height_requested = uint32(0) request_made = False for height_index in range(len(query_heights)): total_time_slept = 0 while True: if self._shut_down: return if total_time_slept > timeout: raise TimeoutError("Took too long to fetch blocks") # Request batches that we don't have yet for batch_start_index in range( height_index, min( height_index + self.config["num_sync_batches"], len(query_heights), ), ): blocks_missing = not self.potential_blocks_received[ uint32(query_heights[batch_start_index])].is_set() if ((time.time() - last_request_time > sleep_interval and blocks_missing) or (query_heights[batch_start_index]) > highest_height_requested): self.log.info( f"Requesting sync header {query_heights[batch_start_index]}" ) if (query_heights[batch_start_index] > highest_height_requested): highest_height_requested = uint32( query_heights[batch_start_index]) request_made = True request_header = wallet_protocol.RequestHeader( uint32(query_heights[batch_start_index]), self.header_hashes[ query_heights[batch_start_index]], ) yield OutboundMessage( NodeType.FULL_NODE, Message("request_header", request_header), Delivery.RANDOM, ) if request_made: last_request_time = time.time() request_made = False try: aw = self.potential_blocks_received[uint32( query_heights[height_index])].wait() await asyncio.wait_for(aw, timeout=sleep_interval) break except concurrent.futures.TimeoutError: total_time_slept += sleep_interval self.log.info("Did not receive desired headers") self.log.info( f"Finished downloading sample of headers at heights: {query_heights}, validating." ) # Validates the downloaded proofs assert self.wallet_state_manager.validate_select_proofs( self.proof_hashes, query_heights_odd, self.cached_blocks, self.potential_header_hashes, ) self.log.info("All proofs validated successfuly.") # Add blockrecords one at a time, to catch up to starting height weight = self.wallet_state_manager.block_records[ fork_point_hash].weight header_validate_start_height = min( max(fork_point_height, self.config["starting_height"] - 1), tip_height + 1, ) if fork_point_height == 0: difficulty = self.constants["DIFFICULTY_STARTING"] else: fork_point_parent_hash = self.wallet_state_manager.block_records[ fork_point_hash].prev_header_hash fork_point_parent_weight = self.wallet_state_manager.block_records[ fork_point_parent_hash] difficulty = uint64(weight - fork_point_parent_weight) for height in range(fork_point_height + 1, header_validate_start_height): _, difficulty_change, total_iters = self.proof_hashes[height] weight += difficulty block_record = BlockRecord( self.header_hashes[height], self.header_hashes[height - 1], uint32(height), weight, [], [], total_iters, None, ) res = await self.wallet_state_manager.receive_block( block_record, None) assert (res == ReceiveBlockResult.ADDED_TO_HEAD or res == ReceiveBlockResult.ADDED_AS_ORPHAN) self.log.info( f"Fast sync successful up to height {header_validate_start_height - 1}" ) # Download headers in batches, and verify them as they come in. We download a few batches ahead, # in case there are delays. TODO(mariano): optimize sync by pipelining last_request_time = float(0) highest_height_requested = uint32(0) request_made = False for height_checkpoint in range(header_validate_start_height + 1, tip_height + 1): total_time_slept = 0 while True: if self._shut_down: return if total_time_slept > timeout: raise TimeoutError("Took too long to fetch blocks") # Request batches that we don't have yet for batch_start in range( height_checkpoint, min( height_checkpoint + self.config["num_sync_batches"], tip_height + 1, ), ): batch_end = min(batch_start + 1, tip_height + 1) blocks_missing = any([ not (self.potential_blocks_received[uint32(h)] ).is_set() for h in range(batch_start, batch_end) ]) if (time.time() - last_request_time > sleep_interval and blocks_missing ) or (batch_end - 1) > highest_height_requested: self.log.info(f"Requesting sync header {batch_start}") if batch_end - 1 > highest_height_requested: highest_height_requested = uint32(batch_end - 1) request_made = True request_header = wallet_protocol.RequestHeader( uint32(batch_start), self.header_hashes[batch_start], ) yield OutboundMessage( NodeType.FULL_NODE, Message("request_header", request_header), Delivery.RANDOM, ) if request_made: last_request_time = time.time() request_made = False awaitables = [ self.potential_blocks_received[uint32( height_checkpoint)].wait() ] future = asyncio.gather(*awaitables, return_exceptions=True) try: await asyncio.wait_for(future, timeout=sleep_interval) except concurrent.futures.TimeoutError: try: await future except asyncio.CancelledError: pass total_time_slept += sleep_interval self.log.info("Did not receive desired headers") continue # Succesfully downloaded header. Now confirm it's added to chain. hh = self.potential_header_hashes[height_checkpoint] if hh in self.wallet_state_manager.block_records: # Successfully added the block to chain break else: # Not added to chain yet. Try again soon. await asyncio.sleep(sleep_interval_short) total_time_slept += sleep_interval_short if hh in self.wallet_state_manager.block_records: break else: self.log.warning( "Received header, but it has not been added to chain. Retrying." ) _, hb, tfilter = self.cached_blocks[hh] respond_header_msg = wallet_protocol.RespondHeader( hb, tfilter) async for msg in self.respond_header( respond_header_msg): yield msg self.log.info( f"Finished sync process up to height {max(self.wallet_state_manager.height_to_hash.keys())}" )
async def respond_removals(self, response: wallet_protocol.RespondRemovals): """ The full node has responded with the removals for a block. We will use this to try to finish the block, and add it to the state. """ if self._shut_down: return if (response.header_hash not in self.cached_blocks or self.cached_blocks[response.header_hash][0].additions is None): self.log.warning( "Do not have header for removals, or do not have additions") return block_record, header_block, transaction_filter = self.cached_blocks[ response.header_hash] assert response.height == block_record.height all_coins: List[Coin] = [] for coin_name, coin in response.coins: if coin is not None: all_coins.append(coin) if response.proofs is None: # If there are no proofs, it means all removals were returned in the response. # we must find the ones relevant to our wallets. # Verify removals root removals_merkle_set = MerkleSet() for coin in all_coins: if coin is not None: removals_merkle_set.add_already_hashed(coin.name()) removals_root = removals_merkle_set.get_root() assert header_block.header.data.removals_root == removals_root else: # This means the full node has responded only with the relevant removals # for our wallet. Each merkle proof must be verified. assert len(response.coins) == len(response.proofs) for i in range(len(response.coins)): # Coins are in the same order as proofs assert response.coins[i][0] == response.proofs[i][0] coin = response.coins[i][1] if coin is None: # Verifies merkle proof of exclusion assert confirm_not_included_already_hashed( header_block.header.data.removals_root, response.coins[i][0], response.proofs[i][1], ) else: # Verifies merkle proof of inclusion of coin name assert response.coins[i][0] == coin.name() assert confirm_included_already_hashed( header_block.header.data.removals_root, coin.name(), response.proofs[i][1], ) new_br = BlockRecord( block_record.header_hash, block_record.prev_header_hash, block_record.height, block_record.weight, block_record.additions, all_coins, block_record.total_iters, header_block.challenge.get_hash(), ) self.cached_blocks[response.header_hash] = ( new_br, header_block, transaction_filter, ) # We have collected all three things: header, additions, and removals. Can proceed. respond_header_msg: Optional[ wallet_protocol.RespondHeader] = await self._block_finished( new_br, header_block, transaction_filter) if respond_header_msg is not None: async for msg in self.respond_header(respond_header_msg): yield msg
async def create( key_config: Dict, config: Dict, db_path: Path, constants: Dict, name: str = None, ): self = WalletStateManager() self.config = config self.constants = constants if name: self.log = logging.getLogger(name) else: self.log = logging.getLogger(__name__) self.lock = asyncio.Lock() self.db_connection = await aiosqlite.connect(db_path) self.wallet_store = await WalletStore.create(self.db_connection) self.tx_store = await WalletTransactionStore.create(self.db_connection) self.puzzle_store = await WalletPuzzleStore.create(self.db_connection) self.user_store = await WalletUserStore.create(self.db_connection) self.lca = None self.sync_mode = False self.height_to_hash = {} self.block_records = await self.wallet_store.get_lca_path() genesis = FullBlock.from_bytes(self.constants["GENESIS_BLOCK"]) self.genesis = genesis self.state_changed_callback = None self.pending_tx_callback = None self.difficulty_resets_prev = {} self.db_path = db_path main_wallet_info = await self.user_store.get_wallet_by_id(1) assert main_wallet_info is not None self.main_wallet = await Wallet.create(config, key_config, self, main_wallet_info) self.wallets = {} main_wallet = await Wallet.create(config, key_config, self, main_wallet_info) self.wallets[main_wallet_info.id] = main_wallet for wallet_info in await self.get_all_wallets(): self.log.info(f"wallet_info {wallet_info}") if wallet_info.type == WalletType.STANDARD_WALLET: if wallet_info.id == 1: continue wallet = await Wallet.create(config, key_config, self, main_wallet_info) self.wallets[wallet_info.id] = wallet elif wallet_info.type == WalletType.RATE_LIMITED: wallet = await RLWallet.create( config, key_config, self, wallet_info, self.main_wallet, ) self.wallets[wallet_info.id] = wallet async with self.puzzle_store.lock: await self.create_more_puzzle_hashes(from_zero=True) if len(self.block_records) > 0: # Initializes the state based on the DB block records # Header hash with the highest weight self.lca = max((item[1].weight, item[0]) for item in self.block_records.items())[1] for key, value in self.block_records.items(): self.height_to_hash[value.height] = value.header_hash # Checks genesis block is the same in config, as in DB assert self.block_records[genesis.header_hash].height == 0 assert self.block_records[ genesis.header_hash].weight == genesis.weight else: # Loads the genesis block if there are no blocks genesis_challenge = Challenge( genesis.proof_of_space.challenge_hash, std_hash(genesis.proof_of_space.get_hash() + genesis.proof_of_time.output.get_hash()), None, ) genesis_hb = HeaderBlock( genesis.proof_of_space, genesis.proof_of_time, genesis_challenge, genesis.header, ) await self.receive_block( BlockRecord( genesis.header_hash, genesis.prev_header_hash, uint32(0), genesis.weight, [], [], genesis_hb.header.data.total_iters, genesis_challenge.get_hash(), ), genesis_hb, ) return self
async def receive_block( self, block: BlockRecord, header_block: Optional[HeaderBlock] = None, ) -> ReceiveBlockResult: """ Adds a new block to the blockchain. It doesn't have to be a new tip, can also be an orphan, but it must be connected to the blockchain. If a header block is specified, the full header and proofs will be validated. Otherwise, the block is added without validation (for use in fast sync). If validation succeeds, block is adedd to DB. If it's a new TIP, transactions are reorged accordingly. """ cb_and_fees_additions = [] if header_block is not None: coinbase = header_block.header.data.coinbase fees_coin = header_block.header.data.fees_coin if await self.is_addition_relevant(coinbase): cb_and_fees_additions.append(coinbase) if await self.is_addition_relevant(fees_coin): cb_and_fees_additions.append(fees_coin) assert block.additions is not None if len(cb_and_fees_additions) > 0: block = BlockRecord( block.header_hash, block.prev_header_hash, block.height, block.weight, block.additions + cb_and_fees_additions, block.removals, block.total_iters, block.new_challenge_hash, ) assert block.additions is not None assert block.removals is not None async with self.lock: if block.header_hash in self.block_records: return ReceiveBlockResult.ALREADY_HAVE_BLOCK if block.prev_header_hash not in self.block_records and block.height != 0: return ReceiveBlockResult.DISCONNECTED_BLOCK if header_block is not None: if not await self.validate_header_block(block, header_block): return ReceiveBlockResult.INVALID_BLOCK if (block.height + 1 ) % self.constants["DIFFICULTY_EPOCH"] == self.constants[ "DIFFICULTY_DELAY"]: assert header_block.challenge.new_work_difficulty is not None self.difficulty_resets_prev[ block. header_hash] = header_block.challenge.new_work_difficulty if (block.height + 1) % self.constants["DIFFICULTY_EPOCH"] == 0: assert block.total_iters is not None # Block is valid, so add it to the blockchain self.block_records[block.header_hash] = block await self.wallet_store.add_block_record(block, False) max_puzzle_index = uint32(0) async with self.puzzle_store.lock: for addition in block.additions: index = await self.puzzle_store.index_for_puzzle_hash( addition.puzzle_hash) assert index is not None if index > max_puzzle_index: max_puzzle_index = index await self.puzzle_store.set_used_up_to(max_puzzle_index) await self.create_more_puzzle_hashes() # Genesis case if self.lca is None: assert block.height == 0 await self.wallet_store.add_block_to_path(block.header_hash) self.lca = block.header_hash for coin in block.additions: await self.coin_added(coin, block.height, False) for coin_name in block.removals: await self.coin_removed(coin_name, block.height) self.state_changed("coin_added") self.state_changed("coin_removed") self.height_to_hash[uint32(0)] = block.header_hash return ReceiveBlockResult.ADDED_TO_HEAD # Not genesis, updated LCA if block.weight > self.block_records[self.lca].weight: fork_h = self._find_fork_point_in_chain( self.block_records[self.lca], block) await self.reorg_rollback(fork_h) # Add blocks between fork point and new lca fork_hash = self.height_to_hash[fork_h] blocks_to_add: List[BlockRecord] = [] tip_hash: bytes32 = block.header_hash while True: if tip_hash == fork_hash or tip_hash == self.genesis.header_hash: break record = self.block_records[tip_hash] blocks_to_add.append(record) tip_hash = record.prev_header_hash blocks_to_add.reverse() for path_block in blocks_to_add: self.height_to_hash[ path_block.height] = path_block.header_hash await self.wallet_store.add_block_to_path( path_block.header_hash) assert (path_block.additions is not None and path_block.removals is not None) for coin in path_block.additions: is_coinbase = False if (bytes32((path_block.height).to_bytes( 32, "big")) == coin.parent_coin_info or std_hash(std_hash(path_block.height)) == coin.parent_coin_info): is_coinbase = True await self.coin_added(coin, path_block.height, is_coinbase) for coin_name in path_block.removals: await self.coin_removed(coin_name, path_block.height) self.lca = block.header_hash self.state_changed("coin_added") self.state_changed("coin_removed") self.state_changed("new_block") return ReceiveBlockResult.ADDED_TO_HEAD return ReceiveBlockResult.ADDED_AS_ORPHAN