Exemplo n.º 1
0
 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])
Exemplo n.º 2
0
 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
Exemplo n.º 3
0
 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
Exemplo n.º 4
0
    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()
Exemplo n.º 5
0
    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
Exemplo n.º 6
0
    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
Exemplo n.º 7
0
    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())}"
        )
Exemplo n.º 8
0
    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