示例#1
0
    async def add_diffs(
        self,
        removals: List[bytes32],
        additions: List[Coin],
        block: FullBlock,
        diff_store: DiffStore,
    ):

        for coin in additions:
            added: CoinRecord = CoinRecord(coin, block.height, 0, 0, 0)  # type: ignore # noqa
            diff_store.diffs[added.name.hex()] = added

        coinbase: CoinRecord = CoinRecord(block.header.data.coinbase, block.height, 0, 0, 1)  # type: ignore # noqa
        diff_store.diffs[coinbase.name.hex()] = coinbase
        fees_coin: CoinRecord = CoinRecord(block.header.data.fees_coin, block.height, 0, 0, 1)  # type: ignore # noqa
        diff_store.diffs[fees_coin.name.hex()] = fees_coin

        for coin_name in removals:
            removed: Optional[CoinRecord] = None
            if coin_name.hex() in diff_store.diffs:
                removed = diff_store.diffs[coin_name.hex()]
            if removed is None:
                removed = await self.get_coin_record(coin_name)
            if removed is None:
                raise Exception
            spent = CoinRecord(
                removed.coin,
                removed.confirmed_block_index,
                block.height,
                True,
                removed.coinbase,
            )  # type: ignore # noqa
            diff_store.diffs[spent.name.hex()] = spent
示例#2
0
 async def get_additions_and_removals(self, header_hash: bytes32) -> Tuple[List[CoinRecord], List[CoinRecord]]:
     try:
         response = await self.fetch("get_additions_and_removals", {"header_hash": header_hash.hex()})
     except Exception:
         return [], []
     removals = []
     additions = []
     for coin_record in response["removals"]:
         removals.append(CoinRecord.from_json_dict(coin_record))
     for coin_record in response["additions"]:
         additions.append(CoinRecord.from_json_dict(coin_record))
     return additions, removals
示例#3
0
    async def rollback_to_block(self, block_index: int):
        """
        Note that block_index can be negative, in which case everything is rolled back
        """
        # Update memory cache
        delete_queue: bytes32 = []
        for coin_name, coin_record in self.coin_record_cache.items():
            if int(coin_record.spent_block_index) > block_index:
                new_record = CoinRecord(
                    coin_record.coin,
                    coin_record.confirmed_block_index,
                    coin_record.spent_block_index,
                    False,
                    coin_record.coinbase,
                    coin_record.timestamp,
                )
                self.coin_record_cache[
                    coin_record.coin.name().hex()] = new_record
            if int(coin_record.confirmed_block_index) > block_index:
                delete_queue.append(coin_name)

        for coin_name in delete_queue:
            del self.coin_record_cache[coin_name]

        # Delete from storage
        c1 = await self.coin_record_db.execute(
            "DELETE FROM coin_record WHERE confirmed_index>?", (block_index, ))
        await c1.close()
        c2 = await self.coin_record_db.execute(
            "UPDATE coin_record SET spent_index = 0, spent = 0 WHERE spent_index>?",
            (block_index, ),
        )
        await c2.close()
示例#4
0
    async def rollback_lca_to_block(self, block_index):
        # Update memory cache
        delete_queue: bytes32 = []
        for coin_name, coin_record in self.lca_coin_records.items():
            if coin_record.spent_block_index > block_index:
                new_record = CoinRecord(
                    coin_record.coin,
                    coin_record.confirmed_block_index,
                    coin_record.spent_block_index,
                    False,
                    coin_record.coinbase,
                )
                self.lca_coin_records[
                    coin_record.coin.name().hex()] = new_record
            if coin_record.confirmed_block_index > block_index:
                delete_queue.append(coin_name)

        for coin_name in delete_queue:
            del self.lca_coin_records[coin_name]

        # Delete from storage
        c1 = await self.coin_record_db.execute(
            "DELETE FROM coin_record WHERE confirmed_index>?", (block_index, ))
        await c1.close()
        c2 = await self.coin_record_db.execute(
            "UPDATE coin_record SET spent_index = 0, spent = 0 WHERE spent_index>?",
            (block_index, ),
        )
        await c2.close()
        await self.coin_record_db.commit()
示例#5
0
 async def set_spent(self, coin_name: bytes32, index: uint32):
     current: Optional[CoinRecord] = await self.get_coin_record(coin_name)
     if current is None:
         return
     spent: CoinRecord = CoinRecord(
         current.coin, current.confirmed_block_index, index, True, current.coinbase,
     )  # type: ignore # noqa
     await self.add_coin_record(spent)
示例#6
0
    async def new_lca(self, block: FullBlock):
        removals, additions = await block.tx_removals_and_additions()

        for coin in additions:
            record: CoinRecord = CoinRecord(coin, block.height, uint32(0),
                                            False, False)
            await self.add_coin_record(record)

        for coin_name in removals:
            await self.set_spent(coin_name, block.height)

        coinbase: CoinRecord = CoinRecord(block.header.data.coinbase,
                                          block.height, uint32(0), False, True)
        fees_coin: CoinRecord = CoinRecord(block.header.data.fees_coin,
                                           block.height, uint32(0), False,
                                           True)

        await self.add_coin_record(coinbase)
        await self.add_coin_record(fees_coin)
 async def get_unspent_coins(
     self, puzzle_hash: bytes32, header_hash: Optional[bytes32] = None
 ) -> List:
     if header_hash is not None:
         d = {"puzzle_hash": puzzle_hash.hex(), "header_hash": header_hash.hex()}
     else:
         d = {"puzzle_hash": puzzle_hash.hex()}
     return [
         CoinRecord.from_json_dict(coin)
         for coin in ((await self.fetch("get_unspent_coins", d))["coin_records"])
     ]
示例#8
0
 async def get_unspent_coin_records(self) -> List[CoinRecord]:
     coins = set()
     cursor = await self.coin_record_db.execute(
         "SELECT * from coin_record WHERE spent=0")
     rows = await cursor.fetchall()
     await cursor.close()
     for row in rows:
         coin = Coin(bytes32(bytes.fromhex(row[6])),
                     bytes32(bytes.fromhex(row[5])), row[7])
         coins.add(CoinRecord(coin, row[1], row[2], row[3], row[4], row[8]))
     return list(coins)
示例#9
0
    async def new_block(self, block: FullBlock):
        """
        Only called for blocks which are blocks (and thus have rewards and transactions)
        """
        if block.is_transaction_block() is False:
            return
        assert block.foliage_transaction_block is not None
        removals, additions = block.tx_removals_and_additions()

        for coin in additions:
            record: CoinRecord = CoinRecord(
                coin,
                block.height,
                uint32(0),
                False,
                False,
                block.foliage_transaction_block.timestamp,
            )
            await self._add_coin_record(record)

        for coin_name in removals:
            await self._set_spent(coin_name, block.height)

        included_reward_coins = block.get_included_reward_coins()
        if block.height == 0:
            assert len(included_reward_coins) == 0
        else:
            assert len(included_reward_coins) >= 2

        for coin in included_reward_coins:
            reward_coin_r: CoinRecord = CoinRecord(
                coin,
                block.height,
                uint32(0),
                False,
                True,
                block.foliage_transaction_block.timestamp,
            )
            await self._add_coin_record(reward_coin_r)
示例#10
0
 async def get_coin_record(self,
                           coin_name: bytes32) -> Optional[CoinRecord]:
     if coin_name.hex() in self.coin_record_cache:
         return self.coin_record_cache[coin_name.hex()]
     cursor = await self.coin_record_db.execute(
         "SELECT * from coin_record WHERE coin_name=?", (coin_name.hex(), ))
     row = await cursor.fetchone()
     await cursor.close()
     if row is not None:
         coin = Coin(bytes32(bytes.fromhex(row[6])),
                     bytes32(bytes.fromhex(row[5])), row[7])
         return CoinRecord(coin, row[1], row[2], row[3], row[4], row[8])
     return None
示例#11
0
    async def get_coin_records_by_puzzle_hash(
            self, puzzle_hash: bytes32) -> List[CoinRecord]:
        coins = set()
        cursor = await self.coin_record_db.execute(
            "SELECT * from coin_record WHERE puzzle_hash=?",
            (puzzle_hash.hex(), ))
        rows = await cursor.fetchall()

        await cursor.close()
        for row in rows:
            coin = Coin(bytes32(bytes.fromhex(row[6])),
                        bytes32(bytes.fromhex(row[5])), row[7])
            coins.add(CoinRecord(coin, row[1], row[2], row[3], row[4], row[8]))
        return list(coins)
示例#12
0
 async def get_coins_removed_at_height(self,
                                       height: uint32) -> List[CoinRecord]:
     cursor = await self.coin_record_db.execute(
         "SELECT * from coin_record WHERE spent_index=? and spent=1",
         (height, ))
     rows = await cursor.fetchall()
     await cursor.close()
     coins = []
     for row in rows:
         coin = Coin(bytes32(bytes.fromhex(row[6])),
                     bytes32(bytes.fromhex(row[5])),
                     uint64.from_bytes(row[7]))
         coins.append(
             CoinRecord(coin, row[1], row[2], row[3], row[4], row[8]))
     return coins
示例#13
0
 async def get_coin_record(self,
                           coin_name: bytes32,
                           header: Header = None) -> Optional[CoinRecord]:
     if header is not None and header.header_hash in self.head_diffs:
         diff_store = self.head_diffs[header.header_hash]
         if coin_name.hex() in diff_store.diffs:
             return diff_store.diffs[coin_name.hex()]
     if coin_name.hex() in self.lca_coin_records:
         return self.lca_coin_records[coin_name.hex()]
     cursor = await self.coin_record_db.execute(
         "SELECT * from coin_record WHERE coin_name=?", (coin_name.hex(), ))
     row = await cursor.fetchone()
     await cursor.close()
     if row is not None:
         coin = Coin(bytes32(bytes.fromhex(row[6])),
                     bytes32(bytes.fromhex(row[5])), row[7])
         return CoinRecord(coin, row[1], row[2], row[3], row[4])
     return None
示例#14
0
 async def get_unspent_coin_records(self,
                                    header: Header = None
                                    ) -> List[CoinRecord]:
     coins = set()
     if header is not None and header.header_hash in self.head_diffs:
         diff_store = self.head_diffs[header.header_hash]
         for _, record in diff_store.diffs.items():
             if not record.spent:
                 coins.add(record)
     cursor = await self.coin_record_db.execute(
         "SELECT * from coin_record WHERE spent=0")
     rows = await cursor.fetchall()
     await cursor.close()
     for row in rows:
         coin = Coin(bytes32(bytes.fromhex(row[6])),
                     bytes32(bytes.fromhex(row[5])), row[7])
         coins.add(CoinRecord(coin, row[1], row[2], row[3], row[4]))
     return list(coins)
 async def get_coin_records_by_puzzle_hash(
     self,
     puzzle_hash: bytes32,
     include_spent_coins: bool = True,
     start_height: Optional[int] = None,
     end_height: Optional[int] = None,
 ) -> List:
     d = {
         "puzzle_hash": puzzle_hash.hex(),
         "include_spent_coins": include_spent_coins
     }
     if start_height is not None:
         d["start_height"] = start_height
     if end_height is not None:
         d["end_height"] = end_height
     return [
         CoinRecord.from_json_dict(coin) for coin in ((await self.fetch(
             "get_coin_records_by_puzzle_hash", d))["coin_records"])
     ]
示例#16
0
    async def get_coin_records_by_puzzle_hash(
            self,
            include_spent_coins: bool,
            puzzle_hash: bytes32,
            start_height: uint32 = uint32(0),
            end_height: uint32 = uint32((2**32) - 1),
    ) -> List[CoinRecord]:

        coins = set()
        cursor = await self.coin_record_db.execute(
            f"SELECT * from coin_record WHERE puzzle_hash=? AND confirmed_index>=? AND confirmed_index<? "
            f"{'' if include_spent_coins else 'AND spent=0'}",
            (puzzle_hash.hex(), start_height, end_height),
        )
        rows = await cursor.fetchall()

        await cursor.close()
        for row in rows:
            coin = Coin(bytes32(bytes.fromhex(row[6])),
                        bytes32(bytes.fromhex(row[5])),
                        uint64.from_bytes(row[7]))
            coins.add(CoinRecord(coin, row[1], row[2], row[3], row[4], row[8]))
        return list(coins)
    async def add_spendbundle(
        self, new_spend: SpendBundle, to_pool: Mempool = None
    ) -> Tuple[Optional[uint64], MempoolInclusionStatus, Optional[Err]]:
        """
        Tries to add spendbundle to either self.mempools or to_pool if it's specified.
        Returns true if it's added in any of pools, Returns error if it fails.
        """
        self.seen_bundle_hashes[new_spend.name()] = new_spend.name()
        self.maybe_pop_seen()

        # Calculate the cost and fees
        program = best_solution_program(new_spend)
        # npc contains names of the coins removed, puzzle_hashes and their spend conditions
        fail_reason, npc_list, cost = calculate_cost_of_program(program)
        if fail_reason:
            return None, MempoolInclusionStatus.FAILED, fail_reason

        # build removal list
        removal_names: List[bytes32] = new_spend.removal_names()

        additions = new_spend.additions()
        additions_dict: Dict[bytes32, Coin] = {}
        for add in additions:
            additions_dict[add.name()] = add

        addition_amount = uint64(0)

        # Check additions for max coin amount
        for coin in additions:
            if coin.amount >= uint64.from_bytes(self.constants["MAX_COIN_AMOUNT"]):
                return (
                    None,
                    MempoolInclusionStatus.FAILED,
                    Err.COIN_AMOUNT_EXCEEDS_MAXIMUM,
                )
            addition_amount = uint64(addition_amount + coin.amount)

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

        # Spend might be valid for one pool but not for other
        added_count = 0
        errors: List[Err] = []
        targets: List[Mempool]

        # If the transaction is added to potential set (to be retried), this is set.
        added_to_potential: bool = False
        potential_error: Optional[Err] = None

        if to_pool is not None:
            targets = [to_pool]
        else:
            targets = list(self.mempools.values())

        for pool in targets:
            # Skip if already added
            if new_spend.name() in pool.spends:
                added_count += 1
                continue

            removal_record_dict: Dict[bytes32, CoinRecord] = {}
            removal_coin_dict: Dict[bytes32, Coin] = {}

            unknown_unspent_error: bool = False
            removal_amount = uint64(0)
            for name in removal_names:
                removal_record = await self.unspent_store.get_coin_record(
                    name, pool.header
                )
                if removal_record is None and name not in additions_dict:
                    unknown_unspent_error = True
                    break
                elif name in additions_dict:
                    removal_coin = additions_dict[name]
                    removal_record = CoinRecord(
                        removal_coin,
                        uint32(pool.header.height + 1),
                        uint32(0),
                        False,
                        False,
                    )

                assert removal_record is not None
                removal_amount = uint64(removal_amount + removal_record.coin.amount)
                removal_record_dict[name] = removal_record
                removal_coin_dict[name] = removal_record.coin

            if unknown_unspent_error:
                errors.append(Err.UNKNOWN_UNSPENT)
                continue

            if addition_amount > removal_amount:
                return None, MempoolInclusionStatus.FAILED, Err.MINTING_COIN

            fees = removal_amount - addition_amount

            if cost == 0:
                return None, MempoolInclusionStatus.FAILED, Err.UNKNOWN

            fees_per_cost: float = fees / cost

            # If pool is at capacity check the fee, if not then accept even without the fee
            if pool.at_full_capacity():
                if fees == 0:
                    errors.append(Err.INVALID_FEE_LOW_FEE)
                    continue
                if fees_per_cost < pool.get_min_fee_rate():
                    errors.append(Err.INVALID_FEE_LOW_FEE)
                    continue

            # Check removals against UnspentDB + DiffStore + Mempool + SpendBundle
            # Use this information later when constructing a block
            fail_reason, conflicts = await self.check_removals(
                removal_record_dict, pool
            )
            # If there is a mempool conflict check if this spendbundle has a higher fee per cost than all others
            tmp_error: Optional[Err] = None
            conflicting_pool_items: Dict[bytes32, MempoolItem] = {}
            if fail_reason is Err.MEMPOOL_CONFLICT:
                for conflicting in conflicts:
                    sb: MempoolItem = pool.removals[conflicting.name()]
                    conflicting_pool_items[sb.name] = sb
                for item in conflicting_pool_items.values():
                    if item.fee_per_cost >= fees_per_cost:
                        tmp_error = Err.MEMPOOL_CONFLICT
                        self.add_to_potential_tx_set(new_spend)
                        added_to_potential = True
                        potential_error = Err.MEMPOOL_CONFLICT
                        break
            elif fail_reason:
                errors.append(fail_reason)
                continue

            if tmp_error:
                errors.append(tmp_error)
                continue

            # Verify conditions, create hash_key list for aggsig check
            hash_key_pairs = []
            error: Optional[Err] = None
            for npc in npc_list:
                coin_record: CoinRecord = removal_record_dict[npc.coin_name]
                # Check that the revealed removal puzzles actually match the puzzle hash
                if npc.puzzle_hash != coin_record.coin.puzzle_hash:
                    log.warning(
                        f"Mempool rejecting transaction because of wrong puzzle_hash"
                    )
                    log.warning(f"{npc.puzzle_hash} != {coin_record.coin.puzzle_hash}")
                    return None, MempoolInclusionStatus.FAILED, Err.WRONG_PUZZLE_HASH

                error = mempool_check_conditions_dict(
                    coin_record, new_spend, npc.condition_dict, pool
                )

                if error:
                    if (
                        error is Err.ASSERT_BLOCK_INDEX_EXCEEDS_FAILED
                        or error is Err.ASSERT_BLOCK_AGE_EXCEEDS_FAILED
                    ):
                        self.add_to_potential_tx_set(new_spend)
                        added_to_potential = True
                        potential_error = error
                    break
                hash_key_pairs.extend(
                    hash_key_pairs_for_conditions_dict(
                        npc.condition_dict, npc.coin_name
                    )
                )
            if error:
                errors.append(error)
                continue

            # Verify aggregated signature
            if not new_spend.aggregated_signature.validate(hash_key_pairs):
                return None, MempoolInclusionStatus.FAILED, Err.BAD_AGGREGATE_SIGNATURE

            # Remove all conflicting Coins and SpendBundles
            if fail_reason:
                mitem: MempoolItem
                for mitem in conflicting_pool_items.values():
                    pool.remove_spend(mitem)

            new_item = MempoolItem(new_spend, fees_per_cost, uint64(fees), uint64(cost))
            pool.add_to_pool(new_item, additions, removal_coin_dict)

            added_count += 1

        if added_count > 0:
            return uint64(cost), MempoolInclusionStatus.SUCCESS, None
        elif added_to_potential:
            return uint64(cost), MempoolInclusionStatus.PENDING, potential_error
        else:
            return None, MempoolInclusionStatus.FAILED, errors[0]
示例#18
0
    async def add_spendbundle(
        self,
        new_spend: SpendBundle,
        cost_result: CostResult,
        spend_name: bytes32,
        validate_signature=True,
    ) -> Tuple[Optional[uint64], MempoolInclusionStatus, Optional[Err]]:
        """
        Tries to add spendbundle to either self.mempools or to_pool if it's specified.
        Returns true if it's added in any of pools, Returns error if it fails.
        """
        start_time = time.time()
        if self.peak is None:
            return None, MempoolInclusionStatus.FAILED, Err.MEMPOOL_NOT_INITIALIZED

        npc_list = cost_result.npc_list
        cost = cost_result.cost

        log.debug(f"Cost: {cost}")

        if cost > self.constants.MAX_BLOCK_COST_CLVM:
            return None, MempoolInclusionStatus.FAILED, Err.BLOCK_COST_EXCEEDS_MAX

        if cost_result.error is not None:
            return None, MempoolInclusionStatus.FAILED, Err(cost_result.error)
        # build removal list
        removal_names: List[bytes32] = new_spend.removal_names()

        additions = additions_for_npc(npc_list)

        additions_dict: Dict[bytes32, Coin] = {}
        for add in additions:
            additions_dict[add.name()] = add

        addition_amount = uint64(0)
        # Check additions for max coin amount
        for coin in additions:
            if coin.amount > self.constants.MAX_COIN_AMOUNT:
                return (
                    None,
                    MempoolInclusionStatus.FAILED,
                    Err.COIN_AMOUNT_EXCEEDS_MAXIMUM,
                )
            addition_amount = uint64(addition_amount + coin.amount)
        # Check for duplicate outputs
        addition_counter = collections.Counter(_.name() for _ in additions)
        for k, v in addition_counter.items():
            if v > 1:
                return None, MempoolInclusionStatus.FAILED, Err.DUPLICATE_OUTPUT
        # Check for duplicate inputs
        removal_counter = collections.Counter(name for name in removal_names)
        for k, v in removal_counter.items():
            if v > 1:
                return None, MempoolInclusionStatus.FAILED, Err.DOUBLE_SPEND
        # Skip if already added
        if spend_name in self.mempool.spends:
            return uint64(cost), MempoolInclusionStatus.SUCCESS, None

        removal_record_dict: Dict[bytes32, CoinRecord] = {}
        removal_coin_dict: Dict[bytes32, Coin] = {}
        unknown_unspent_error: bool = False
        removal_amount = uint64(0)
        for name in removal_names:
            removal_record = await self.coin_store.get_coin_record(name)
            if removal_record is None and name not in additions_dict:
                unknown_unspent_error = True
                break
            elif name in additions_dict:
                removal_coin = additions_dict[name]
                # TODO(straya): what timestamp to use here?
                removal_record = CoinRecord(
                    removal_coin,
                    uint32(self.peak.height + 1),  # In mempool, so will be included in next height
                    uint32(0),
                    False,
                    False,
                    uint64(int(time.time())),
                )

            assert removal_record is not None
            removal_amount = uint64(removal_amount + removal_record.coin.amount)
            removal_record_dict[name] = removal_record
            removal_coin_dict[name] = removal_record.coin
        if unknown_unspent_error:
            return None, MempoolInclusionStatus.FAILED, Err.UNKNOWN_UNSPENT

        if addition_amount > removal_amount:
            print(addition_amount, removal_amount)
            return None, MempoolInclusionStatus.FAILED, Err.MINTING_COIN

        fees = removal_amount - addition_amount
        assert_fee_sum: uint64 = uint64(0)

        for npc in npc_list:
            if ConditionOpcode.RESERVE_FEE in npc.condition_dict:
                fee_list: List[ConditionVarPair] = npc.condition_dict[ConditionOpcode.RESERVE_FEE]
                for cvp in fee_list:
                    fee = int_from_bytes(cvp.vars[0])
                    assert_fee_sum = assert_fee_sum + fee
        if fees < assert_fee_sum:
            return (
                None,
                MempoolInclusionStatus.FAILED,
                Err.RESERVE_FEE_CONDITION_FAILED,
            )

        if cost == 0:
            return None, MempoolInclusionStatus.FAILED, Err.UNKNOWN

        fees_per_cost: float = fees / cost
        # If pool is at capacity check the fee, if not then accept even without the fee
        if self.mempool.at_full_capacity():
            if fees == 0:
                return None, MempoolInclusionStatus.FAILED, Err.INVALID_FEE_LOW_FEE
            if fees_per_cost < self.mempool.get_min_fee_rate():
                return None, MempoolInclusionStatus.FAILED, Err.INVALID_FEE_LOW_FEE
        # Check removals against UnspentDB + DiffStore + Mempool + SpendBundle
        # Use this information later when constructing a block
        fail_reason, conflicts = await self.check_removals(removal_record_dict)
        # If there is a mempool conflict check if this spendbundle has a higher fee per cost than all others
        tmp_error: Optional[Err] = None
        conflicting_pool_items: Dict[bytes32, MempoolItem] = {}
        if fail_reason is Err.MEMPOOL_CONFLICT:
            for conflicting in conflicts:
                sb: MempoolItem = self.mempool.removals[conflicting.name()]
                conflicting_pool_items[sb.name] = sb
            for item in conflicting_pool_items.values():
                if item.fee_per_cost >= fees_per_cost:
                    self.add_to_potential_tx_set(new_spend, spend_name, cost_result)
                    return (
                        uint64(cost),
                        MempoolInclusionStatus.PENDING,
                        Err.MEMPOOL_CONFLICT,
                    )

        elif fail_reason:
            return None, MempoolInclusionStatus.FAILED, fail_reason

        if tmp_error:
            return None, MempoolInclusionStatus.FAILED, tmp_error

        # Verify conditions, create hash_key list for aggsig check
        pks: List[G1Element] = []
        msgs: List[bytes32] = []
        error: Optional[Err] = None
        for npc in npc_list:
            coin_record: CoinRecord = removal_record_dict[npc.coin_name]
            # Check that the revealed removal puzzles actually match the puzzle hash
            if npc.puzzle_hash != coin_record.coin.puzzle_hash:
                log.warning("Mempool rejecting transaction because of wrong puzzle_hash")
                log.warning(f"{npc.puzzle_hash} != {coin_record.coin.puzzle_hash}")
                return None, MempoolInclusionStatus.FAILED, Err.WRONG_PUZZLE_HASH

            chialisp_height = (
                self.peak.prev_transaction_block_height if not self.peak.is_transaction_block else self.peak.height
            )
            error = mempool_check_conditions_dict(coin_record, new_spend, npc.condition_dict, uint32(chialisp_height))

            if error:
                if error is Err.ASSERT_HEIGHT_NOW_EXCEEDS_FAILED or error is Err.ASSERT_HEIGHT_AGE_EXCEEDS_FAILED:
                    self.add_to_potential_tx_set(new_spend, spend_name, cost_result)
                    return uint64(cost), MempoolInclusionStatus.PENDING, error
                break

            if validate_signature:
                for pk, message in pkm_pairs_for_conditions_dict(npc.condition_dict, npc.coin_name):
                    pks.append(pk)
                    msgs.append(message)
        if error:
            return None, MempoolInclusionStatus.FAILED, error

        if validate_signature:
            # Verify aggregated signature
            if not AugSchemeMPL.aggregate_verify(pks, msgs, new_spend.aggregated_signature):
                log.warning(f"Aggsig validation error {pks} {msgs} {new_spend}")
                return None, MempoolInclusionStatus.FAILED, Err.BAD_AGGREGATE_SIGNATURE
        # Remove all conflicting Coins and SpendBundles
        if fail_reason:
            mempool_item: MempoolItem
            for mempool_item in conflicting_pool_items.values():
                self.mempool.remove_spend(mempool_item)

        removals: List[Coin] = [coin for coin in removal_coin_dict.values()]
        new_item = MempoolItem(new_spend, uint64(fees), cost_result, spend_name, additions, removals)
        self.mempool.add_to_pool(new_item, additions, removal_coin_dict)
        log.info(f"add_spendbundle took {time.time() - start_time} seconds")
        return uint64(cost), MempoolInclusionStatus.SUCCESS, 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
示例#20
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
示例#21
0
 def _add_coin_entry(self, coin: Coin, birthday: CoinTimestamp) -> None:
     name = coin.name()
     assert name not in self._db
     self._db[name] = CoinRecord(coin, uint32(birthday.height), uint32(0),
                                 False, False, uint64(birthday.seconds))
     self._ph_index[coin.puzzle_hash].append(name)
 async def get_unspent_coins(self, puzzle_hash: bytes32) -> List:
     d = {"puzzle_hash": puzzle_hash.hex()}
     return [
         CoinRecord.from_json_dict(coin) for coin in ((
             await self.fetch("get_unspent_coins", d))["coin_records"])
     ]