Ejemplo n.º 1
0
    async def test_find_fork_point(self):
        blocks = bt.get_consecutive_blocks(test_constants, 10, [], 9, b"7")
        blocks_2 = bt.get_consecutive_blocks(test_constants, 6, blocks[:5], 9, b"8")
        blocks_3 = bt.get_consecutive_blocks(test_constants, 8, blocks[:3], 9, b"9")

        blocks_reorg = bt.get_consecutive_blocks(test_constants, 3, blocks[:9], 9, b"9")

        db_path = Path("blockchain_test.db")
        if db_path.exists():
            db_path.unlink()
        connection = await aiosqlite.connect(db_path)
        coin_store = await CoinStore.create(connection)
        store = await BlockStore.create(connection)
        b: Blockchain = await Blockchain.create(coin_store, store, test_constants)
        for i in range(1, len(blocks)):
            await b.receive_block(blocks[i])

        for i in range(1, len(blocks_2)):
            await b.receive_block(blocks_2[i])

        assert (
            find_fork_point_in_chain(b.headers, blocks[10].header, blocks_2[10].header)
            == 4
        )

        for i in range(1, len(blocks_3)):
            await b.receive_block(blocks_3[i])

        assert (
            find_fork_point_in_chain(b.headers, blocks[10].header, blocks_3[10].header)
            == 2
        )

        assert b.lca_block.data == blocks[2].header.data

        for i in range(1, len(blocks_reorg)):
            await b.receive_block(blocks_reorg[i])

        assert (
            find_fork_point_in_chain(
                b.headers, blocks[10].header, blocks_reorg[10].header
            )
            == 8
        )
        assert (
            find_fork_point_in_chain(
                b.headers, blocks_2[10].header, blocks_reorg[10].header
            )
            == 4
        )
        assert b.lca_block.data == blocks[4].header.data
        await connection.close()
        b.shut_down()
Ejemplo n.º 2
0
    async def _reconsider_lca(self, genesis: bool, sync_mode: bool):
        """
        Update the least common ancestor of the heads. This is useful, since we can just assume
        there is one block per height before the LCA (and use the height_to_hash dict).
        """
        cur: List[Header] = self.tips[:]
        old_lca: Optional[Header]
        try:
            old_lca = self.lca_block
        except AttributeError:
            old_lca = None
        while any(b.header_hash != cur[0].header_hash for b in cur):
            heights = [b.height for b in cur]
            i = heights.index(max(heights))
            cur[i] = self.headers[cur[i].prev_header_hash]
        if genesis:
            self._reconsider_heights(None, cur[0])
        else:
            self._reconsider_heights(self.lca_block, cur[0])
        self.lca_block = cur[0]
        await self.block_store.set_lca(self.lca_block.header_hash)

        if old_lca is None:
            full: Optional[FullBlock] = await self.block_store.get_block(
                self.lca_block.header_hash)
            assert full is not None
            await self.coin_store.new_lca(full)
            await self._create_diffs_for_tips(self.lca_block)
        # If LCA changed update the unspent store
        elif old_lca.header_hash != self.lca_block.header_hash:
            # New LCA is lower height but not the a parent of old LCA (Reorg)
            fork_h = find_fork_point_in_chain(self.headers, old_lca,
                                              self.lca_block)
            # Rollback to fork
            await self.coin_store.rollback_lca_to_block(fork_h)

            # Add blocks between fork point and new lca
            fork_hash = self.height_to_hash[fork_h]
            fork_head = self.headers[fork_hash]
            await self._from_fork_to_lca(fork_head, self.lca_block)
            if not sync_mode:
                await self.recreate_diff_stores()
        else:
            # If LCA has not changed just update the difference
            self.coin_store.nuke_diffs()
            # Create DiffStore
            await self._create_diffs_for_tips(self.lca_block)
    async def _reconsider_peak(
            self, sub_block: SubBlockRecord, genesis: bool,
            fork_point_with_peak: Optional[uint32]) -> Optional[uint32]:
        """
        When a new block is added, this is called, to check if the new block is the new peak of the chain.
        This also handles reorgs by reverting blocks which are not in the heaviest chain.
        It returns the height of the fork between the previous chain and the new chain, or returns
        None if there was no update to the heaviest chain.
        """
        peak = self.get_peak()
        if genesis:
            if peak is None:
                block: Optional[
                    HeaderBlockRecord] = await self.block_store.get_header_block_record(
                        sub_block.header_hash)
                assert block is not None
                self.__height_to_hash[uint32(0)] = block.header_hash
                for removed in block.removals:
                    self.log.debug(f"Removed: {removed.name()}")
                await self.coins_of_interest_received(block.removals,
                                                      block.additions,
                                                      block.height)
                self.peak_height = uint32(0)
                return uint32(0)
            return None

        assert peak is not None
        if sub_block.weight > peak.weight:
            # Find the fork. if the block is just being appended, it will return the peak
            # If no blocks in common, returns -1, and reverts all blocks
            if fork_point_with_peak is not None:
                fork_h: int = fork_point_with_peak
            else:
                fork_h = find_fork_point_in_chain(self, sub_block, peak)

            # Rollback to fork
            self.log.debug(
                f"fork_h: {fork_h}, SB: {sub_block.height}, peak: {peak.height}"
            )
            await self.reorg_rollback(fork_h)

            # Rollback sub_epoch_summaries
            heights_to_delete = []
            for ses_included_height in self.__sub_epoch_summaries.keys():
                if ses_included_height > fork_h:
                    heights_to_delete.append(ses_included_height)
            for height in heights_to_delete:
                del self.__sub_epoch_summaries[height]

            # Collect all blocks from fork point to new peak
            blocks_to_add: List[Tuple[HeaderBlockRecord, SubBlockRecord]] = []
            curr = sub_block.header_hash
            while fork_h < 0 or curr != self.height_to_hash(uint32(fork_h)):
                fetched_block: Optional[
                    HeaderBlockRecord] = await self.block_store.get_header_block_record(
                        curr)
                fetched_sub_block: Optional[
                    SubBlockRecord] = await self.block_store.get_sub_block_record(
                        curr)
                assert fetched_block is not None
                assert fetched_sub_block is not None
                blocks_to_add.append((fetched_block, fetched_sub_block))
                if fetched_block.height == 0:
                    # Doing a full reorg, starting at height 0
                    break
                curr = fetched_sub_block.prev_hash

            for fetched_block, fetched_sub_block in reversed(blocks_to_add):
                self.__height_to_hash[
                    fetched_sub_block.height] = fetched_sub_block.header_hash
                if fetched_sub_block.is_block:
                    await self.coins_of_interest_received(
                        fetched_block.removals,
                        fetched_block.additions,
                        fetched_block.height,
                    )
                if fetched_sub_block.sub_epoch_summary_included is not None:
                    self.__sub_epoch_summaries[
                        fetched_sub_block.
                        height] = fetched_sub_block.sub_epoch_summary_included

            # Changes the peak to be the new peak
            await self.block_store.set_peak(sub_block.header_hash)
            self.peak_height = sub_block.height
            return uint32(min(fork_h, 0))

        # This is not a heavier block than the heaviest we have seen, so we don't change the coin set
        return None
    async def get_filter_additions_removals(
        self, new_block: HeaderBlock, transactions_filter: bytes, fork_point_with_peak: Optional[uint32]
    ) -> Tuple[List[bytes32], List[bytes32]]:
        """ Returns a list of our coin ids, and a list of puzzle_hashes that positively match with provided filter. """
        # assert new_block.prev_header_hash in self.blockchain.blocks

        tx_filter = PyBIP158([b for b in transactions_filter])

        # Find fork point
        if fork_point_with_peak is not None:
            fork_h: int = fork_point_with_peak
        elif new_block.prev_header_hash != self.constants.GENESIS_CHALLENGE and self.peak is not None:
            # TODO: handle returning of -1
            fork_h = find_fork_point_in_chain(
                self.blockchain,
                self.blockchain.block_record(self.peak.header_hash),
                new_block,
            )
        else:
            fork_h = 0

        # Get all unspent coins
        my_coin_records: Set[WalletCoinRecord] = await self.coin_store.get_unspent_coins_at_height(uint32(fork_h))

        # Filter coins up to and including fork point
        unspent_coin_names: Set[bytes32] = set()
        for coin in my_coin_records:
            if coin.confirmed_block_height <= fork_h:
                unspent_coin_names.add(coin.name())

        # # Get all blocks after fork point up to but not including this block
        # curr: BlockRecord = self.lockchain.blocks[new_block.prev_header_hash]
        # reorg_blocks: List[HeaderBlockRecord] = []
        # while curr.height > fork_h:
        #     header_block_record = await self.block_store.get_header_block_record(
        #         curr.header_hash
        #     )
        #     reorg_blocks.append(header_block_record)
        #     curr = self.blockchain.blocks[curr.prev_header_hash]
        # reorg_blocks.reverse()

        # For each block, process additions to get all Coins, then process removals to get unspent coins
        # for reorg_block in reorg_blocks:
        #     for addition in reorg_block.additions:
        #         unspent_coin_names.add(addition.name())
        #     for removal in reorg_block.removals:
        #         record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(
        #             removal.puzzle_hash
        #         )
        #         if record is None:
        #             continue
        #         unspent_coin_names.remove(removal)

        my_puzzle_hashes = self.puzzle_store.all_puzzle_hashes

        removals_of_interest: bytes32 = []
        additions_of_interest: bytes32 = []

        (
            trade_removals,
            trade_additions,
        ) = await self.trade_manager.get_coins_of_interest()
        for name, trade_coin in trade_removals.items():
            if tx_filter.Match(bytearray(trade_coin.name())):
                removals_of_interest.append(trade_coin.name())

        for name, trade_coin in trade_additions.items():
            if tx_filter.Match(bytearray(trade_coin.puzzle_hash)):
                additions_of_interest.append(trade_coin.puzzle_hash)

        for coin_name in unspent_coin_names:
            if tx_filter.Match(bytearray(coin_name)):
                removals_of_interest.append(coin_name)

        for puzzle_hash in my_puzzle_hashes:
            if tx_filter.Match(bytearray(puzzle_hash)):
                additions_of_interest.append(puzzle_hash)

        return additions_of_interest, removals_of_interest
Ejemplo n.º 5
0
    async def _reconsider_peak(self, sub_block: SubBlockRecord,
                               genesis: bool) -> Optional[uint32]:
        """
        When a new block is added, this is called, to check if the new block is the new peak of the chain.
        This also handles reorgs by reverting blocks which are not in the heaviest chain.
        It returns the height of the fork between the previous chain and the new chain, or returns
        None if there was no update to the heaviest chain.
        """
        peak = self.get_peak()
        if genesis:
            if peak is None:
                block: Optional[
                    FullBlock] = await self.block_store.get_full_block(
                        sub_block.header_hash)
                assert block is not None
                await self.coin_store.new_block(block)
                self.sub_height_to_hash[uint32(0)] = block.header_hash
                self.peak_height = uint32(0)
                await self.block_store.set_peak(block.header_hash)
                return uint32(0)
            return None

        assert peak is not None
        if sub_block.weight > peak.weight:
            # Find the fork. if the block is just being appended, it will return the peak
            # If no blocks in common, returns -1, and reverts all blocks
            fork_sub_block_height: int = find_fork_point_in_chain(
                self.sub_blocks, sub_block, peak)
            if fork_sub_block_height == -1:
                coin_store_reorg_height = -1
            else:
                last_sb_in_common = self.sub_blocks[self.sub_height_to_hash[
                    uint32(fork_sub_block_height)]]
                if last_sb_in_common.is_block:
                    coin_store_reorg_height = last_sb_in_common.height
                else:
                    coin_store_reorg_height = last_sb_in_common.height - 1

            # Rollback to fork
            await self.coin_store.rollback_to_block(coin_store_reorg_height)

            # Rollback sub_epoch_summaries
            heights_to_delete = []
            for ses_included_height in self.sub_epoch_summaries.keys():
                if ses_included_height > fork_sub_block_height:
                    heights_to_delete.append(ses_included_height)
            for sub_height in heights_to_delete:
                del self.sub_epoch_summaries[sub_height]

            # Collect all blocks from fork point to new peak
            blocks_to_add: List[Tuple[FullBlock, SubBlockRecord]] = []
            curr = sub_block.header_hash
            while fork_sub_block_height < 0 or curr != self.sub_height_to_hash[
                    uint32(fork_sub_block_height)]:
                fetched_block: Optional[
                    FullBlock] = await self.block_store.get_full_block(curr)
                fetched_sub_block: Optional[
                    SubBlockRecord] = await self.block_store.get_sub_block_record(
                        curr)
                assert fetched_block is not None
                assert fetched_sub_block is not None
                blocks_to_add.append((fetched_block, fetched_sub_block))
                if fetched_block.sub_block_height == 0:
                    # Doing a full reorg, starting at height 0
                    break
                curr = fetched_sub_block.prev_hash

            for fetched_block, fetched_sub_block in reversed(blocks_to_add):
                self.sub_height_to_hash[
                    fetched_sub_block.
                    sub_block_height] = fetched_sub_block.header_hash
                if fetched_sub_block.is_block:
                    await self.coin_store.new_block(fetched_block)
                if fetched_sub_block.sub_epoch_summary_included is not None:
                    self.sub_epoch_summaries[
                        fetched_sub_block.
                        sub_block_height] = fetched_sub_block.sub_epoch_summary_included

            # Changes the peak to be the new peak
            await self.block_store.set_peak(sub_block.header_hash)
            self.peak_height = sub_block.sub_block_height
            return uint32(max(fork_sub_block_height, 0))

        # This is not a heavier block than the heaviest we have seen, so we don't change the coin set
        return None
Ejemplo n.º 6
0
    async def _reconsider_peak(
            self, block_record: BlockRecord, genesis: bool,
            fork_point_with_peak: Optional[uint32]) -> Optional[uint32]:
        """
        When a new block is added, this is called, to check if the new block is the new peak of the chain.
        This also handles reorgs by reverting blocks which are not in the heaviest chain.
        It returns the height of the fork between the previous chain and the new chain, or returns
        None if there was no update to the heaviest chain.
        """
        peak = self.get_peak()
        if genesis:
            if peak is None:
                block: Optional[
                    FullBlock] = await self.block_store.get_full_block(
                        block_record.header_hash)
                assert block is not None

                # Begins a transaction, because we want to ensure that the coin store and block store are only updated
                # in sync.
                await self.block_store.begin_transaction()
                try:
                    await self.coin_store.new_block(block)
                    self.__height_to_hash[uint32(0)] = block.header_hash
                    self._peak_height = uint32(0)
                    await self.block_store.set_peak(block.header_hash)
                    await self.block_store.commit_transaction()
                except Exception:
                    await self.block_store.rollback_transaction()
                    raise
                return uint32(0)
            return None

        assert peak is not None
        if block_record.weight > peak.weight:
            # Find the fork. if the block is just being appended, it will return the peak
            # If no blocks in common, returns -1, and reverts all blocks
            if fork_point_with_peak is not None:
                fork_height: int = fork_point_with_peak
            else:
                fork_height = find_fork_point_in_chain(self, block_record,
                                                       peak)

            # Begins a transaction, because we want to ensure that the coin store and block store are only updated
            # in sync.
            await self.block_store.begin_transaction()
            try:
                # Rollback to fork
                await self.coin_store.rollback_to_block(fork_height)
                # Rollback sub_epoch_summaries
                heights_to_delete = []
                for ses_included_height in self.__sub_epoch_summaries.keys():
                    if ses_included_height > fork_height:
                        heights_to_delete.append(ses_included_height)
                for height in heights_to_delete:
                    log.info(f"delete ses at height {height}")
                    del self.__sub_epoch_summaries[height]

                if len(heights_to_delete) > 0:
                    # remove segments from prev fork
                    log.info(f"remove segments for se above {fork_height}")
                    await self.block_store.delete_sub_epoch_challenge_segments(
                        uint32(fork_height))

                # Collect all blocks from fork point to new peak
                blocks_to_add: List[Tuple[FullBlock, BlockRecord]] = []
                curr = block_record.header_hash

                while fork_height < 0 or curr != self.height_to_hash(
                        uint32(fork_height)):
                    fetched_full_block: Optional[
                        FullBlock] = await self.block_store.get_full_block(curr
                                                                           )
                    fetched_block_record: Optional[
                        BlockRecord] = await self.block_store.get_block_record(
                            curr)
                    assert fetched_full_block is not None
                    assert fetched_block_record is not None
                    blocks_to_add.append(
                        (fetched_full_block, fetched_block_record))
                    if fetched_full_block.height == 0:
                        # Doing a full reorg, starting at height 0
                        break
                    curr = fetched_block_record.prev_hash

                for fetched_full_block, fetched_block_record in reversed(
                        blocks_to_add):
                    self.__height_to_hash[
                        fetched_block_record.
                        height] = fetched_block_record.header_hash
                    if fetched_block_record.is_transaction_block:
                        await self.coin_store.new_block(fetched_full_block)
                    if fetched_block_record.sub_epoch_summary_included is not None:
                        self.__sub_epoch_summaries[
                            fetched_block_record.
                            height] = fetched_block_record.sub_epoch_summary_included

                # Changes the peak to be the new peak
                await self.block_store.set_peak(block_record.header_hash)
                self._peak_height = block_record.height
                await self.block_store.commit_transaction()
            except Exception:
                await self.block_store.rollback_transaction()
                raise

            return uint32(max(fork_height, 0))

        # This is not a heavier block than the heaviest we have seen, so we don't change the coin set
        return None
async def validate_block_body(
    constants: ConsensusConstants,
    sub_blocks: Dict[bytes32, SubBlockRecord],
    sub_height_to_hash: Dict[uint32, bytes32],
    block_store: BlockStore,
    coin_store: CoinStore,
    peak: Optional[SubBlockRecord],
    block: Union[FullBlock, UnfinishedBlock],
    sub_height: uint32,
    height: Optional[uint32],
    cached_cost_result: Optional[CostResult] = None,
) -> Optional[Err]:
    """
    This assumes the header block has been completely validated.
    Validates the transactions and body of the block. Returns None if everything
    validates correctly, or an Err if something does not validate.
    """
    if isinstance(block, FullBlock):
        assert sub_height == block.sub_block_height
        if height is not None:
            assert height == block.height
            assert block.is_block()
        else:
            assert not block.is_block()

    # 1. For non block sub-blocks, foliage block, transaction filter, transactions info, and generator must be empty
    # If it is a sub block but not a block, there is no body to validate. Check that all fields are None
    if block.foliage_sub_block.foliage_block_hash is None:
        if (block.foliage_block is not None
                or block.transactions_info is not None
                or block.transactions_generator is not None):
            return Err.NOT_BLOCK_BUT_HAS_DATA
        return None  # This means the sub-block is valid

    # 2. For blocks, foliage block, transaction filter, transactions info must not be empty
    if block.foliage_block is None or block.foliage_block.filter_hash is None or block.transactions_info is None:
        return Err.IS_BLOCK_BUT_NO_DATA

    # keeps track of the reward coins that need to be incorporated
    expected_reward_coins: Set[Coin] = set()

    # 3. The transaction info hash in the Foliage block must match the transaction info
    if block.foliage_block.transactions_info_hash != std_hash(
            block.transactions_info):
        return Err.INVALID_TRANSACTIONS_INFO_HASH

    # 4. The foliage block hash in the foliage sub block must match the foliage block
    if block.foliage_sub_block.foliage_block_hash != std_hash(
            block.foliage_block):
        return Err.INVALID_FOLIAGE_BLOCK_HASH

    # 5. The prev generators root must be valid
    # TODO(straya): implement prev generators

    # 6. The generator root must be the tree-hash of the generator (or zeroes if no generator)
    if block.transactions_generator is not None:
        if block.transactions_generator.get_tree_hash(
        ) != block.transactions_info.generator_root:
            return Err.INVALID_TRANSACTIONS_GENERATOR_ROOT
    else:
        if block.transactions_info.generator_root != bytes([0] * 32):
            return Err.INVALID_TRANSACTIONS_GENERATOR_ROOT

    # 7. The reward claims must be valid for the previous sub-blocks, and current block fees
    if sub_height > 0:
        # Add reward claims for all sub-blocks from the prev prev block, until the prev block (including the latter)
        prev_block = sub_blocks[block.foliage_block.prev_block_hash]

        assert prev_block.fees is not None
        pool_coin = create_pool_coin(
            prev_block.sub_block_height,
            prev_block.pool_puzzle_hash,
            calculate_pool_reward(prev_block.height),
        )
        farmer_coin = create_farmer_coin(
            prev_block.sub_block_height,
            prev_block.farmer_puzzle_hash,
            uint64(
                calculate_base_farmer_reward(prev_block.height) +
                prev_block.fees),
        )
        # Adds the previous block
        expected_reward_coins.add(pool_coin)
        expected_reward_coins.add(farmer_coin)

        # For the second block in the chain, don't go back further
        if prev_block.sub_block_height > 0:
            curr_sb = sub_blocks[prev_block.prev_hash]
            curr_height = curr_sb.height
            while not curr_sb.is_block:
                expected_reward_coins.add(
                    create_pool_coin(
                        curr_sb.sub_block_height,
                        curr_sb.pool_puzzle_hash,
                        calculate_pool_reward(curr_height),
                    ))
                expected_reward_coins.add(
                    create_farmer_coin(
                        curr_sb.sub_block_height,
                        curr_sb.farmer_puzzle_hash,
                        calculate_base_farmer_reward(curr_height),
                    ))
                curr_sb = sub_blocks[curr_sb.prev_hash]

    if set(block.transactions_info.reward_claims_incorporated
           ) != expected_reward_coins:
        return Err.INVALID_REWARD_COINS

    removals: List[bytes32] = []
    coinbase_additions: List[Coin] = list(expected_reward_coins)
    additions: List[Coin] = []
    npc_list: List[NPC] = []
    removals_puzzle_dic: Dict[bytes32, bytes32] = {}
    cost: uint64 = uint64(0)

    if block.transactions_generator is not None:
        # Get List of names removed, puzzles hashes for removed coins and conditions crated
        if cached_cost_result is not None:
            result: CostResult = cached_cost_result
        else:
            result = calculate_cost_of_program(
                block.transactions_generator,
                constants.CLVM_COST_RATIO_CONSTANT)
        cost = result.cost
        npc_list = result.npc_list

        # 8. Check that cost <= MAX_BLOCK_COST_CLVM
        if cost > constants.MAX_BLOCK_COST_CLVM:
            return Err.BLOCK_COST_EXCEEDS_MAX
        if result.error is not None:
            return Err(result.error)

        for npc in npc_list:
            removals.append(npc.coin_name)
            removals_puzzle_dic[npc.coin_name] = npc.puzzle_hash

        additions = additions_for_npc(npc_list)

    # 9. Check that the correct cost is in the transactions info
    if block.transactions_info.cost != cost:
        return Err.INVALID_BLOCK_COST

    additions_dic: Dict[bytes32, Coin] = {}
    # 10. Check additions for max coin amount
    for coin in additions + coinbase_additions:
        additions_dic[coin.name()] = coin
        if coin.amount >= constants.MAX_COIN_AMOUNT:
            return Err.COIN_AMOUNT_EXCEEDS_MAXIMUM

    # 11. Validate addition and removal roots
    root_error = validate_block_merkle_roots(
        block.foliage_block.additions_root,
        block.foliage_block.removals_root,
        additions + coinbase_additions,
        removals,
    )
    if root_error:
        return root_error

    # 12. The additions and removals must result in the correct filter
    byte_array_tx: List[bytes32] = []

    for coin in additions + coinbase_additions:
        byte_array_tx.append(bytearray(coin.puzzle_hash))
    for coin_name in removals:
        byte_array_tx.append(bytearray(coin_name))

    bip158: PyBIP158 = PyBIP158(byte_array_tx)
    encoded_filter = bytes(bip158.GetEncoded())
    filter_hash = std_hash(encoded_filter)

    if filter_hash != block.foliage_block.filter_hash:
        return Err.INVALID_TRANSACTIONS_FILTER_HASH

    # 13. Check for duplicate outputs in additions
    addition_counter = collections.Counter(
        _.name() for _ in additions + coinbase_additions)
    for k, v in addition_counter.items():
        if v > 1:
            return Err.DUPLICATE_OUTPUT

    # 14. Check for duplicate spends inside block
    removal_counter = collections.Counter(removals)
    for k, v in removal_counter.items():
        if v > 1:
            return Err.DOUBLE_SPEND

    # 15. Check if removals exist and were not previously spent. (unspent_db + diff_store + this_block)
    if peak is None or sub_height == 0:
        fork_sub_h: int = -1
    else:
        fork_sub_h = find_fork_point_in_chain(
            sub_blocks, peak, sub_blocks[block.prev_header_hash])

    if fork_sub_h == -1:
        coin_store_reorg_height = -1
    else:
        last_sb_in_common = sub_blocks[sub_height_to_hash[uint32(fork_sub_h)]]
        if last_sb_in_common.is_block:
            coin_store_reorg_height = last_sb_in_common.height
        else:
            coin_store_reorg_height = last_sb_in_common.height - 1

    # Get additions and removals since (after) fork_h but not including this block
    additions_since_fork: Dict[bytes32, Tuple[Coin, uint32]] = {}
    removals_since_fork: Set[bytes32] = set()
    coinbases_since_fork: Dict[bytes32, uint32] = {}

    if sub_height > 0:
        curr: Optional[FullBlock] = await block_store.get_full_block(
            block.prev_header_hash)
        assert curr is not None

        while curr.sub_block_height > fork_sub_h:
            removals_in_curr, additions_in_curr = curr.tx_removals_and_additions(
            )
            for c_name in removals_in_curr:
                removals_since_fork.add(c_name)
            for c in additions_in_curr:
                additions_since_fork[c.name()] = (c, curr.sub_block_height)

            for coinbase_coin in curr.get_included_reward_coins():
                additions_since_fork[coinbase_coin.name()] = (
                    coinbase_coin, curr.sub_block_height)
                coinbases_since_fork[
                    coinbase_coin.name()] = curr.sub_block_height
            if curr.sub_block_height == 0:
                break
            curr = await block_store.get_full_block(curr.prev_header_hash)
            assert curr is not None

    removal_coin_records: Dict[bytes32, CoinRecord] = {}
    for rem in removals:
        if rem in additions_dic:
            # Ephemeral coin
            rem_coin: Coin = additions_dic[rem]
            new_unspent: CoinRecord = CoinRecord(
                rem_coin,
                sub_height,
                uint32(0),
                False,
                False,
                block.foliage_block.timestamp,
            )
            removal_coin_records[new_unspent.name] = new_unspent
        else:
            unspent = await coin_store.get_coin_record(rem)
            if unspent is not None and unspent.confirmed_block_index <= coin_store_reorg_height:
                # Spending something in the current chain, confirmed before fork
                # (We ignore all coins confirmed after fork)
                if unspent.spent == 1 and unspent.spent_block_index <= coin_store_reorg_height:
                    # Check for coins spent in an ancestor block
                    return Err.DOUBLE_SPEND
                removal_coin_records[unspent.name] = unspent
            else:
                # This coin is not in the current heaviest chain, so it must be in the fork
                if rem not in additions_since_fork:
                    # Check for spending a coin that does not exist in this fork
                    # TODO: fix this, there is a consensus bug here
                    return Err.UNKNOWN_UNSPENT
                new_coin, confirmed_height = additions_since_fork[rem]
                new_coin_record: CoinRecord = CoinRecord(
                    new_coin,
                    confirmed_height,
                    uint32(0),
                    False,
                    (rem in coinbases_since_fork),
                    block.foliage_block.timestamp,
                )
                removal_coin_records[new_coin_record.name] = new_coin_record

            # This check applies to both coins created before fork (pulled from coin_store),
            # and coins created after fork (additions_since_fork)>
            if rem in removals_since_fork:
                # This coin was spent in the fork
                return Err.DOUBLE_SPEND

    removed = 0
    for unspent in removal_coin_records.values():
        removed += unspent.coin.amount

    added = 0
    for coin in additions:
        added += coin.amount

    # 16. Check that the total coin amount for added is <= removed
    if removed < added:
        return Err.MINTING_COIN

    fees = removed - added
    assert_fee_sum: uint64 = uint64(0)

    for npc in npc_list:
        if ConditionOpcode.ASSERT_FEE in npc.condition_dict:
            fee_list: List[ConditionVarPair] = npc.condition_dict[
                ConditionOpcode.ASSERT_FEE]
            for cvp in fee_list:
                fee = int_from_bytes(cvp.vars[0])
                assert_fee_sum = assert_fee_sum + fee

    # 17. Check that the assert fee sum <= fees
    if fees < assert_fee_sum:
        return Err.ASSERT_FEE_CONDITION_FAILED

    # 18. Check that the computed fees are equal to the fees in the block header
    if block.transactions_info.fees != fees:
        return Err.INVALID_BLOCK_FEE_AMOUNT

    # 19. Verify that removed coin puzzle_hashes match with calculated puzzle_hashes
    for unspent in removal_coin_records.values():
        if unspent.coin.puzzle_hash != removals_puzzle_dic[unspent.name]:
            return Err.WRONG_PUZZLE_HASH

    # 20. Verify conditions
    # create hash_key list for aggsig check
    pairs_pks = []
    pairs_msgs = []
    for npc in npc_list:
        assert height is not None
        unspent = removal_coin_records[npc.coin_name]
        error = blockchain_check_conditions_dict(
            unspent,
            removal_coin_records,
            npc.condition_dict,
            height,
            block.foliage_block.timestamp,
        )
        if error:
            return error
        for pk, m in pkm_pairs_for_conditions_dict(npc.condition_dict,
                                                   npc.coin_name):
            pairs_pks.append(pk)
            pairs_msgs.append(m)

    # 21. Verify aggregated signature
    # TODO: move this to pre_validate_blocks_multiprocessing so we can sync faster
    if not block.transactions_info.aggregated_signature:
        return Err.BAD_AGGREGATE_SIGNATURE

    if len(pairs_pks) == 0:
        if len(
                pairs_msgs
        ) != 0 or block.transactions_info.aggregated_signature != G2Element.infinity(
        ):
            return Err.BAD_AGGREGATE_SIGNATURE
    else:
        # noinspection PyTypeChecker
        validates = AugSchemeMPL.aggregate_verify(
            pairs_pks, pairs_msgs,
            block.transactions_info.aggregated_signature)
        if not validates:
            return Err.BAD_AGGREGATE_SIGNATURE

    return None
Ejemplo n.º 8
0
    async def _validate_transactions(self, block: FullBlock,
                                     fee_base: uint64) -> Optional[Err]:
        # TODO(straya): review, further test the code, and number all the validation steps

        # 1. Check that transactions generator is present
        if not block.transactions_generator:
            return Err.UNKNOWN
        # Get List of names removed, puzzles hashes for removed coins and conditions crated
        error, npc_list, cost = calculate_cost_of_program(
            block.transactions_generator,
            self.constants.CLVM_COST_RATIO_CONSTANT)

        # 2. Check that cost <= MAX_BLOCK_COST_CLVM
        if cost > self.constants.MAX_BLOCK_COST_CLVM:
            return Err.BLOCK_COST_EXCEEDS_MAX
        if error:
            return error

        prev_header: Header
        if block.prev_header_hash in self.headers:
            prev_header = self.headers[block.prev_header_hash]
        else:
            return Err.EXTENDS_UNKNOWN_BLOCK

        removals: List[bytes32] = []
        removals_puzzle_dic: Dict[bytes32, bytes32] = {}
        for npc in npc_list:
            removals.append(npc.coin_name)
            removals_puzzle_dic[npc.coin_name] = npc.puzzle_hash

        additions: List[Coin] = additions_for_npc(npc_list)
        additions_dic: Dict[bytes32, Coin] = {}
        # Check additions for max coin amount
        for coin in additions:
            additions_dic[coin.name()] = coin
            if coin.amount >= self.constants.MAX_COIN_AMOUNT:
                return Err.COIN_AMOUNT_EXCEEDS_MAXIMUM

        # Validate addition and removal roots
        root_error = self._validate_merkle_root(block, additions, removals)
        if root_error:
            return root_error

        # Validate filter
        byte_array_tx: List[bytes32] = []

        for coin in additions:
            byte_array_tx.append(bytearray(coin.puzzle_hash))
        for coin_name in removals:
            byte_array_tx.append(bytearray(coin_name))

        byte_array_tx.append(
            bytearray(block.header.data.farmer_rewards_puzzle_hash))
        byte_array_tx.append(
            bytearray(block.header.data.pool_target.puzzle_hash))

        bip158: PyBIP158 = PyBIP158(byte_array_tx)
        encoded_filter = bytes(bip158.GetEncoded())
        filter_hash = std_hash(encoded_filter)

        if filter_hash != block.header.data.filter_hash:
            return Err.INVALID_TRANSACTIONS_FILTER_HASH

        # Watch out for duplicate outputs
        addition_counter = collections.Counter(_.name() for _ in additions)
        for k, v in addition_counter.items():
            if v > 1:
                return Err.DUPLICATE_OUTPUT

        # Check for duplicate spends inside block
        removal_counter = collections.Counter(removals)
        for k, v in removal_counter.items():
            if v > 1:
                return Err.DOUBLE_SPEND

        # Check if removals exist and were not previously spend. (unspent_db + diff_store + this_block)
        fork_h = find_fork_point_in_chain(self.headers, self.lca_block,
                                          block.header)

        # Get additions and removals since (after) fork_h but not including this block
        additions_since_fork: Dict[bytes32, Tuple[Coin, uint32]] = {}
        removals_since_fork: Set[bytes32] = set()
        coinbases_since_fork: Dict[bytes32, uint32] = {}
        curr: Optional[FullBlock] = await self.block_store.get_block(
            block.prev_header_hash)
        assert curr is not None

        while curr.height > fork_h:
            removals_in_curr, additions_in_curr = await curr.tx_removals_and_additions(
            )
            for c_name in removals_in_curr:
                removals_since_fork.add(c_name)
            for c in additions_in_curr:
                additions_since_fork[c.name()] = (c, curr.height)

            coinbase_coin = curr.get_coinbase()
            fees_coin = curr.get_fees_coin()
            additions_since_fork[coinbase_coin.name()] = (
                coinbase_coin,
                curr.height,
            )
            additions_since_fork[fees_coin.name()] = (
                fees_coin,
                curr.height,
            )
            coinbases_since_fork[coinbase_coin.name()] = curr.height
            coinbases_since_fork[fees_coin.name()] = curr.height
            curr = await self.block_store.get_block(curr.prev_header_hash)
            assert curr is not None

        removal_coin_records: Dict[bytes32, CoinRecord] = {}
        for rem in removals:
            if rem in additions_dic:
                # Ephemeral coin
                rem_coin: Coin = additions_dic[rem]
                new_unspent: CoinRecord = CoinRecord(rem_coin, block.height,
                                                     uint32(0), False, False)
                removal_coin_records[new_unspent.name] = new_unspent
            else:
                assert prev_header is not None
                unspent = await self.coin_store.get_coin_record(
                    rem, prev_header)
                if unspent is not None and unspent.confirmed_block_index <= fork_h:
                    # Spending something in the current chain, confirmed before fork
                    # (We ignore all coins confirmed after fork)
                    if unspent.spent == 1 and unspent.spent_block_index <= fork_h:
                        # Spend in an ancestor block, so this is a double spend
                        return Err.DOUBLE_SPEND
                    # If it's a coinbase, check that it's not frozen
                    if unspent.coinbase == 1:
                        if (block.height < unspent.confirmed_block_index +
                                self.coinbase_freeze):
                            return Err.COINBASE_NOT_YET_SPENDABLE
                    removal_coin_records[unspent.name] = unspent
                else:
                    # This coin is not in the current heaviest chain, so it must be in the fork
                    if rem not in additions_since_fork:
                        # This coin does not exist in the fork
                        # TODO: fix this, there is a consensus bug here
                        return Err.UNKNOWN_UNSPENT
                    if rem in coinbases_since_fork:
                        # This coin is a coinbase coin
                        if (block.height < coinbases_since_fork[rem] +
                                self.coinbase_freeze):
                            return Err.COINBASE_NOT_YET_SPENDABLE
                    new_coin, confirmed_height = additions_since_fork[rem]
                    new_coin_record: CoinRecord = CoinRecord(
                        new_coin,
                        confirmed_height,
                        uint32(0),
                        False,
                        (rem in coinbases_since_fork),
                    )
                    removal_coin_records[
                        new_coin_record.name] = new_coin_record

                # This check applies to both coins created before fork (pulled from coin_store),
                # and coins created after fork (additions_since_fork)>
                if rem in removals_since_fork:
                    # This coin was spent in the fork
                    return Err.DOUBLE_SPEND

        # Check fees
        removed = 0
        for unspent in removal_coin_records.values():
            removed += unspent.coin.amount

        added = 0
        for coin in additions:
            added += coin.amount

        if removed < added:
            return Err.MINTING_COIN

        fees = removed - added
        assert_fee_sum: uint64 = uint64(0)

        for npc in npc_list:
            if ConditionOpcode.ASSERT_FEE in npc.condition_dict:
                fee_list: List[ConditionVarPair] = npc.condition_dict[
                    ConditionOpcode.ASSERT_FEE]
                for cvp in fee_list:
                    fee = int_from_bytes(cvp.var1)
                    assert_fee_sum = assert_fee_sum + fee

        if fees < assert_fee_sum:
            return Err.ASSERT_FEE_CONDITION_FAILED

        # Check coinbase reward
        if fees + fee_base != block.header.data.total_transaction_fees:
            return Err.BAD_COINBASE_REWARD

        # Verify that removed coin puzzle_hashes match with calculated puzzle_hashes
        for unspent in removal_coin_records.values():
            if unspent.coin.puzzle_hash != removals_puzzle_dic[unspent.name]:
                return Err.WRONG_PUZZLE_HASH

        # Verify conditions, create hash_key list for aggsig check
        pool_target_m = bytes(block.header.data.pool_target)

        # The pool signature on the pool target is checked here as well, since the pool signature is
        # aggregated along with the transaction signatures
        pairs_pks = [block.proof_of_space.pool_public_key]
        pairs_msgs = [pool_target_m]
        for npc in npc_list:
            unspent = removal_coin_records[npc.coin_name]
            error = blockchain_check_conditions_dict(
                unspent,
                removal_coin_records,
                npc.condition_dict,
                block.header,
            )
            if error:
                return error
            for pk, m in pkm_pairs_for_conditions_dict(npc.condition_dict,
                                                       npc.coin_name):
                pairs_pks.append(pk)
                pairs_msgs.append(m)

        # Verify aggregated signature
        # TODO: move this to pre_validate_blocks_multiprocessing so we can sync faster
        if not block.header.data.aggregated_signature:
            return Err.BAD_AGGREGATE_SIGNATURE

        validates = AugSchemeMPL.aggregate_verify(
            pairs_pks, pairs_msgs, block.header.data.aggregated_signature)
        if not validates:
            return Err.BAD_AGGREGATE_SIGNATURE

        return None