Esempio n. 1
0
    def sign_transaction(self, spends: List[Tuple[Program, CoinSolution]]):
        sigs = []
        solution: Program
        puzzle: Program
        for puzzle, solution in spends:  # type: ignore # noqa
            pubkey, secretkey = self.get_keys(solution.coin.puzzle_hash)
            secretkey = BLSPrivateKey(secretkey)
            code_ = [puzzle, solution.solution]
            sexp = Program.to(code_)
            err, con, cost = conditions_for_solution(sexp)
            if not con:
                return
            conditions_dict = conditions_by_opcode(con)

            for _ in hash_key_pairs_for_conditions_dict(
                    conditions_dict, bytes(solution.coin)):
                signature = secretkey.sign(_.message_hash)
                sigs.append(signature)
        aggsig = BLSSignature.aggregate(sigs)
        solution_list: List[CoinSolution] = [
            CoinSolution(coin_solution.coin,
                         clvm.to_sexp_f([puzzle, coin_solution.solution]))
            for (puzzle, coin_solution) in spends
        ]
        spend_bundle = SpendBundle(solution_list, aggsig)
        return spend_bundle
Esempio n. 2
0
 def signature_for_solution(self, solution, coin_name):
     signatures = []
     conditions = conditions_for_solution(solution)
     assert conditions[1] is not None
     conditions_dict = conditions_by_opcode(conditions[1])
     for _ in hash_key_pairs_for_conditions_dict(conditions_dict, coin_name):
         signature = self.sign(_)
         signatures.append(signature)
     return BLSSignature.aggregate(signatures)
Esempio n. 3
0
    async def test_agg_sig_condition(self, two_nodes):
        num_blocks = 2
        wallet_a = WalletTool()
        coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
        wallet_receiver = WalletTool()
        receiver_puzzlehash = wallet_receiver.get_new_puzzlehash()

        blocks = bt.get_consecutive_blocks(
            test_constants, num_blocks, [], 10, b"", coinbase_puzzlehash
        )
        full_node_1, full_node_2, server_1, server_2 = two_nodes

        block = blocks[1]
        async for _ in full_node_1.respond_block(
            full_node_protocol.RespondBlock(block)
        ):
            pass

        unsigned: List[
            Tuple[Program, CoinSolution]
        ] = wallet_a.generate_unsigned_transaction(
            1000, receiver_puzzlehash, block.header.data.coinbase, {}, 0
        )
        assert len(unsigned) == 1

        puzzle, solution = unsigned[0]
        code_ = [puzzle, solution.solution]
        sexp = Program.to(code_)

        err, con, cost = conditions_for_solution(sexp)
        assert con is not None

        conditions_dict = conditions_by_opcode(con)
        hash_key_pairs = hash_key_pairs_for_conditions_dict(
            conditions_dict, solution.coin.name()
        )
        assert len(hash_key_pairs) == 1

        pk_pair: BLSSignature.PkMessagePair = hash_key_pairs[0]
        assert pk_pair.message_hash == solution.solution.first().get_tree_hash()

        spend_bundle = wallet_a.sign_transaction(unsigned)
        assert spend_bundle is not None

        tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(
            spend_bundle
        )
        async for _ in full_node_1.respond_transaction(tx):
            outbound: OutboundMessage = _
            # Maybe transaction means that it's accepted in mempool
            assert outbound.message.function == "new_transaction"

        sb = full_node_1.mempool_manager.get_spendbundle(spend_bundle.name())
        assert sb is spend_bundle
Esempio n. 4
0
 async def get_sigs_for_innerpuz_with_innersol(
         self, innerpuz: Program, innersol: Program) -> List[BLSSignature]:
     puzzle_hash = innerpuz.get_tree_hash()
     pubkey, private = await self.wallet_state_manager.get_keys(puzzle_hash)
     private = BLSPrivateKey(private)
     sigs: List[BLSSignature] = []
     code_ = [innerpuz, innersol]
     sexp = Program.to(code_)
     error, conditions, cost = conditions_dict_for_solution(sexp)
     if conditions is not None:
         for _ in hash_key_pairs_for_conditions_dict(conditions):
             signature = private.sign(_.message_hash)
             sigs.append(signature)
     return sigs
Esempio n. 5
0
    async def sign_transaction(
        self, spends: List[Tuple[Program, CoinSolution]]
    ) -> Optional[SpendBundle]:
        signatures = []
        for puzzle, solution in spends:
            # Get keys
            keys = await self.wallet_state_manager.get_keys(solution.coin.puzzle_hash)
            if not keys:
                self.log.error(
                    f"Sign transaction failed, No Keys for puzzlehash {solution.coin.puzzle_hash}"
                )
                return None

            pubkey, secretkey = keys
            secretkey = BLSPrivateKey(secretkey)
            code_ = [puzzle, solution.solution]
            sexp = clvm.to_sexp_f(code_)

            # Get AGGSIG conditions
            err, con, cost = conditions_for_solution(sexp)
            if err or not con:
                self.log.error(f"Sign transcation failed, con:{con}, error: {err}")
                return None

            conditions_dict = conditions_by_opcode(con)

            # Create signature
            for pk_message in hash_key_pairs_for_conditions_dict(
                conditions_dict, bytes(solution.coin)
            ):
                signature = secretkey.sign(pk_message.message_hash)
                signatures.append(signature)

        # Aggregate signatures
        aggsig = BLSSignature.aggregate(signatures)
        solution_list: List[CoinSolution] = [
            CoinSolution(
                coin_solution.coin, clvm.to_sexp_f([puzzle, coin_solution.solution])
            )
            for (puzzle, coin_solution) in spends
        ]
        spend_bundle = SpendBundle(solution_list, aggsig)

        return spend_bundle
    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]
Esempio n. 7
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)

        # 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 >= uint64.from_bytes(
                    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))

        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:
                log.error(f"DOUBLE SPEND! 1 {k}")
                return Err.DOUBLE_SPEND

        # Check if removals exist and were not previously spend. (unspent_db + diff_store + this_block)
        fork_h = self._find_fork_point_in_chain(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
        log.info(f"curr.height is: {curr.height}, fork height is: {fork_h}")
        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)
            additions_since_fork[curr.header.data.coinbase.name()] = (
                curr.header.data.coinbase,
                curr.height,
            )
            additions_since_fork[curr.header.data.fees_coin.name()] = (
                curr.header.data.fees_coin,
                curr.height,
            )
            coinbases_since_fork[
                curr.header.data.coinbase.name()] = curr.height
            coinbases_since_fork[
                curr.header.data.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
                        log.error(f"DOUBLE SPEND! 2 {unspent}")
                        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
                        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
                    log.error(f"DOUBLE SPEND! 3 {rem}")
                    log.error(f"removals_since_fork: {removals_since_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.fees_coin.amount:
            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
        hash_key_pairs = []
        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
            hash_key_pairs.extend(
                hash_key_pairs_for_conditions_dict(npc.condition_dict,
                                                   npc.coin_name))

        # 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
        if not block.header.data.aggregated_signature.validate(hash_key_pairs):
            return Err.BAD_AGGREGATE_SIGNATURE

        return None