async def get_max_send_amount(self, records=None): spendable: List[WalletCoinRecord] = list( await self.wallet_state_manager.get_spendable_coins_for_wallet(self.id(), records) ) if len(spendable) == 0: return 0 spendable.sort(reverse=True, key=lambda record: record.coin.amount) if self.cost_of_single_tx is None: coin = spendable[0].coin tx = await self.generate_signed_transaction( [coin.amount], [coin.puzzle_hash], coins={coin}, ignore_max_send_amount=True ) program: BlockGenerator = simple_solution_generator(tx.spend_bundle) # npc contains names of the coins removed, puzzle_hashes and their spend conditions result: NPCResult = get_name_puzzle_conditions(program, True) cost_result: uint64 = calculate_cost_of_program( program.program, result, self.wallet_state_manager.constants.COST_PER_BYTE ) self.cost_of_single_tx = cost_result self.log.info(f"Cost of a single tx for standard wallet: {self.cost_of_single_tx}") max_cost = self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 2 # avoid full block TXs current_cost = 0 total_amount = 0 total_coin_count = 0 for record in spendable: current_cost += self.cost_of_single_tx total_amount += record.coin.amount total_coin_count += 1 if current_cost + self.cost_of_single_tx > max_cost: break return total_amount
def get_npc_multiprocess(spend_bundle_bytes: bytes, max_cost: int, cost_per_byte: int) -> bytes: program = simple_solution_generator( SpendBundle.from_bytes(spend_bundle_bytes)) # npc contains names of the coins removed, puzzle_hashes and their spend conditions return bytes( get_name_puzzle_conditions(program, max_cost, cost_per_byte=cost_per_byte, safe_mode=True, rust_checker=True))
async def test_basics(self, rust_checker: bool): wallet_tool = bt.get_pool_wallet_tool() ph = wallet_tool.get_new_puzzlehash() num_blocks = 3 blocks = bt.get_consecutive_blocks(num_blocks, [], guarantee_transaction_block=True, pool_reward_puzzle_hash=ph, farmer_reward_puzzle_hash=ph) coinbase = None for coin in blocks[2].get_included_reward_coins(): if coin.puzzle_hash == ph and coin.amount == 250000000000: coinbase = coin break assert coinbase is not None spend_bundle = wallet_tool.generate_signed_transaction( coinbase.amount, BURN_PUZZLE_HASH, coinbase, ) assert spend_bundle is not None program: BlockGenerator = simple_solution_generator(spend_bundle) npc_result: NPCResult = get_name_puzzle_conditions( program, test_constants.MAX_BLOCK_COST_CLVM, cost_per_byte=test_constants.COST_PER_BYTE, safe_mode=False, rust_checker=rust_checker, ) cost = calculate_cost_of_program(program.program, npc_result, test_constants.COST_PER_BYTE) assert npc_result.error is None assert len(bytes(program.program)) == 433 coin_name = npc_result.npc_list[0].coin_name error, puzzle, solution = get_puzzle_and_solution_for_coin( program, coin_name, test_constants.MAX_BLOCK_COST_CLVM) assert error is None assert npc_result.clvm_cost == 404560 # Create condition + agg_sig_condition + length + cpu_cost assert (cost == ConditionCost.CREATE_COIN.value + ConditionCost.AGG_SIG.value + len(bytes(program.program)) * test_constants.COST_PER_BYTE + 404560 # clvm_cost )
def test_compressed_block_results(self): sb: SpendBundle = make_spend_bundle(1) start, end = match_standard_transaction_at_any_index( original_generator) ca = CompressorArg(uint32(0), SerializedProgram.from_bytes(original_generator), start, end) c = compressed_spend_bundle_solution(ca, sb) s = simple_solution_generator(sb) assert c != s cost_c, result_c = run_generator(c) cost_s, result_s = run_generator(s) print(result_c) assert result_c is not None assert result_s is not None assert result_c == result_s
def validate_spend_bundle( self, spend_bundle: SpendBundle, now: CoinTimestamp, max_cost: int, cost_per_byte: int, ) -> int: # this should use blockchain consensus code program = simple_solution_generator(spend_bundle) result: NPCResult = get_name_puzzle_conditions( program, max_cost, cost_per_byte=cost_per_byte, mempool_mode=True ) if result.error is not None: raise BadSpendBundleError(f"condition validation failure {Err(result.error)}") ephemeral_db = dict(self._db) for npc in result.npc_list: for coin in created_outputs_for_conditions_dict(npc.condition_dict, npc.coin_name): name = coin.name() ephemeral_db[name] = CoinRecord( coin, uint32(now.height), uint32(0), False, uint64(now.seconds), ) for npc in result.npc_list: prev_transaction_block_height = uint32(now.height) timestamp = uint64(now.seconds) coin_record = ephemeral_db.get(npc.coin_name) if coin_record is None: raise BadSpendBundleError(f"coin not found for id 0x{npc.coin_name.hex()}") # noqa err = mempool_check_conditions_dict( coin_record, npc.condition_dict, prev_transaction_block_height, timestamp, ) if err is not None: raise BadSpendBundleError(f"condition validation failure {Err(err)}") return 0
def validate_clvm_and_signature( spend_bundle_bytes: bytes, max_cost: int, cost_per_byte: int, additional_data: bytes ) -> Tuple[Optional[Err], bytes, Dict[bytes, bytes]]: """ Validates CLVM and aggregate signature for a spendbundle. This is meant to be called under a ProcessPoolExecutor in order to validate the heavy parts of a transction in a different thread. Returns an optional error, the NPCResult and a cache of the new pairings validated (if not error) """ try: bundle: SpendBundle = SpendBundle.from_bytes(spend_bundle_bytes) program = simple_solution_generator(bundle) # npc contains names of the coins removed, puzzle_hashes and their spend conditions result: NPCResult = get_name_puzzle_conditions( program, max_cost, cost_per_byte=cost_per_byte, mempool_mode=True) if result.error is not None: return Err(result.error), b"", {} pks: List[G1Element] = [] msgs: List[bytes32] = [] # TODO: address hint error and remove ignore # error: Incompatible types in assignment (expression has type "List[bytes]", variable has type # "List[bytes32]") [assignment] pks, msgs = pkm_pairs(result.npc_list, additional_data) # type: ignore[assignment] # Verify aggregated signature cache: LRUCache = LRUCache(10000) if not cached_bls.aggregate_verify( pks, msgs, bundle.aggregated_signature, True, cache): return Err.BAD_AGGREGATE_SIGNATURE, b"", {} new_cache_entries: Dict[bytes, bytes] = {} for k, v in cache.cache.items(): new_cache_entries[k] = bytes(v) except ValidationError as e: return e.code, b"", {} except Exception: return Err.UNKNOWN, b"", {} return None, bytes(result), new_cache_entries
def test_get_removals_for_single_coin(self): sb: SpendBundle = make_spend_bundle(1) start, end = match_standard_transaction_at_any_index( original_generator) ca = CompressorArg(uint32(0), SerializedProgram.from_bytes(original_generator), start, end) c = compressed_spend_bundle_solution(ca, sb) removal = sb.coin_spends[0].coin.name() error, puzzle, solution = get_puzzle_and_solution_for_coin( c, removal, INFINITE_COST) assert error is None assert bytes(puzzle) == bytes(sb.coin_spends[0].puzzle_reveal) assert bytes(solution) == bytes(sb.coin_spends[0].solution) # Test non compressed generator as well s = simple_solution_generator(sb) error, puzzle, solution = get_puzzle_and_solution_for_coin( s, removal, INFINITE_COST) assert error is None assert bytes(puzzle) == bytes(sb.coin_spends[0].puzzle_reveal) assert bytes(solution) == bytes(sb.coin_spends[0].solution)
def compute_coin_hints(cs: CoinSpend) -> List[bytes]: bundle = SpendBundle([cs], G2Element()) generator = simple_solution_generator(bundle) npc_result = get_name_puzzle_conditions( generator, INFINITE_COST, cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, mempool_mode=False, height=DEFAULT_CONSTANTS.SOFT_FORK_HEIGHT, ) h_list = [] for npc in npc_result.npc_list: for opcode, conditions in npc.conditions: if opcode == ConditionOpcode.CREATE_COIN: for condition in conditions: if len(condition.vars) > 2 and condition.vars[2] != b"": h_list.append(condition.vars[2]) return h_list
async def add_spendbundle( self, new_spend: SpendBundle, npc_result: NPCResult, spend_name: bytes32, program: Optional[SerializedProgram] = None, ) -> Tuple[Optional[uint64], MempoolInclusionStatus, Optional[Err]]: """ Tries to add spend bundle to the mempool Returns the cost (if SUCCESS), the result (MempoolInclusion status), and an optional error """ start_time = time.time() if self.peak is None: return None, MempoolInclusionStatus.FAILED, Err.MEMPOOL_NOT_INITIALIZED npc_list = npc_result.npc_list assert npc_result.error is None if program is None: program = simple_solution_generator(new_spend).program cost = calculate_cost_of_program(program, npc_result, self.constants.COST_PER_BYTE) log.debug(f"Cost: {cost}") if cost > int(self.limit_factor * self.constants.MAX_BLOCK_COST_CLVM): # we shouldn't ever end up here, since the cost is limited when we # execute the CLVM program. return None, MempoolInclusionStatus.FAILED, Err.BLOCK_COST_EXCEEDS_MAX # build removal list removal_names: List[bytes32] = [npc.coin_name for npc in npc_list] if set(removal_names) != set([s.name() for s in new_spend.removals()]): return None, MempoolInclusionStatus.FAILED, Err.INVALID_SPEND_BUNDLE 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 < 0: return ( None, MempoolInclusionStatus.FAILED, Err.COIN_AMOUNT_NEGATIVE, ) 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] = {} 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: return None, MempoolInclusionStatus.FAILED, Err.UNKNOWN_UNSPENT elif name in additions_dict: removal_coin = additions_dict[name] # TODO(straya): what timestamp to use here? assert self.peak.timestamp is not None removal_record = CoinRecord( removal_coin, uint32( self.peak.height + 1), # In mempool, so will be included in next height uint32(0), False, uint64(self.peak.timestamp + 1), ) 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 removals: List[Coin] = [coin for coin in removal_coin_dict.values()] if addition_amount > removal_amount: print(addition_amount, removal_amount) return None, MempoolInclusionStatus.FAILED, Err.MINTING_COIN fees = uint64(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[ConditionWithArgs] = npc.condition_dict[ ConditionOpcode.RESERVE_FEE] for cvp in fee_list: fee = int_from_bytes(cvp.vars[0]) if fee < 0: return None, MempoolInclusionStatus.FAILED, Err.RESERVE_FEE_CONDITION_FAILED 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(cost): if fees_per_cost < self.nonzero_fee_minimum_fpc: return None, MempoolInclusionStatus.FAILED, Err.INVALID_FEE_TOO_CLOSE_TO_ZERO if fees_per_cost <= self.mempool.get_min_fee_rate(cost): 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 if not self.can_replace(conflicting_pool_items, removal_record_dict, fees, fees_per_cost): potential = MempoolItem(new_spend, uint64(fees), npc_result, cost, spend_name, additions, removals, program) self.potential_cache.add(potential) 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 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) assert self.peak.timestamp is not None error = mempool_check_conditions_dict( coin_record, npc.condition_dict, uint32(chialisp_height), self.peak.timestamp, ) if error: if error is Err.ASSERT_HEIGHT_ABSOLUTE_FAILED or error is Err.ASSERT_HEIGHT_RELATIVE_FAILED: potential = MempoolItem(new_spend, uint64(fees), npc_result, cost, spend_name, additions, removals, program) self.potential_cache.add(potential) return uint64(cost), MempoolInclusionStatus.PENDING, error break if error: return None, MempoolInclusionStatus.FAILED, error # Remove all conflicting Coins and SpendBundles if fail_reason: mempool_item: MempoolItem for mempool_item in conflicting_pool_items.values(): self.mempool.remove_from_pool(mempool_item) new_item = MempoolItem(new_spend, uint64(fees), npc_result, cost, spend_name, additions, removals, program) self.mempool.add_to_pool(new_item) now = time.time() log.log( logging.DEBUG, f"add_spendbundle {spend_name} took {now - start_time:0.2f} seconds. " f"Cost: {cost} ({round(100.0 * cost/self.constants.MAX_BLOCK_COST_CLVM, 3)}% of max block cost)", ) return uint64(cost), MempoolInclusionStatus.SUCCESS, None
async def add_spendbundle( self, new_spend: SpendBundle, npc_result: NPCResult, spend_name: bytes32, validate_signature=True, program: Optional[SerializedProgram] = 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. """ start_time = time.time() if self.peak is None: return None, MempoolInclusionStatus.FAILED, Err.MEMPOOL_NOT_INITIALIZED npc_list = npc_result.npc_list if program is None: program = simple_solution_generator(new_spend).program cost = calculate_cost_of_program(program, npc_result, self.constants.COST_PER_BYTE) log.debug(f"Cost: {cost}") if cost > int(self.limit_factor * self.constants.MAX_BLOCK_COST_CLVM): return None, MempoolInclusionStatus.FAILED, Err.BLOCK_COST_EXCEEDS_MAX if npc_result.error is not None: return None, MempoolInclusionStatus.FAILED, Err(npc_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? assert self.peak.timestamp is not None removal_record = CoinRecord( removal_coin, uint32(self.peak.height + 1), # In mempool, so will be included in next height uint32(0), False, False, uint64(self.peak.timestamp + 1), ) 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 removals: List[Coin] = [coin for coin in removal_coin_dict.values()] 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 = uint64(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[ConditionWithArgs] = npc.condition_dict[ConditionOpcode.RESERVE_FEE] for cvp in fee_list: fee = int_from_bytes(cvp.vars[0]) if fee < 0: return None, MempoolInclusionStatus.FAILED, Err.RESERVE_FEE_CONDITION_FAILED 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(cost): if fees_per_cost < self.nonzero_fee_minimum_fpc: return None, MempoolInclusionStatus.FAILED, Err.INVALID_FEE_TOO_CLOSE_TO_ZERO if fees_per_cost <= self.mempool.get_min_fee_rate(cost): 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 if not self.can_replace(conflicting_pool_items, removal_record_dict, fees, fees_per_cost): potential = MempoolItem( new_spend, uint64(fees), npc_result, cost, spend_name, additions, removals, program ) self.add_to_potential_tx_set(potential) 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 coin_announcements_in_spend: Set[bytes32] = coin_announcements_names_for_npc(npc_list) puzzle_announcements_in_spend: Set[bytes32] = puzzle_announcements_names_for_npc(npc_list) 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 ) assert self.peak.timestamp is not None error = mempool_check_conditions_dict( coin_record, coin_announcements_in_spend, puzzle_announcements_in_spend, npc.condition_dict, uint32(chialisp_height), self.peak.timestamp, ) if error: if error is Err.ASSERT_HEIGHT_ABSOLUTE_FAILED or error is Err.ASSERT_HEIGHT_RELATIVE_FAILED: potential = MempoolItem( new_spend, uint64(fees), npc_result, cost, spend_name, additions, removals, program ) self.add_to_potential_tx_set(potential) 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, self.constants.AGG_SIG_ME_ADDITIONAL_DATA ): 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_from_pool(mempool_item) new_item = MempoolItem(new_spend, uint64(fees), npc_result, cost, spend_name, additions, removals, program) 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 generate_transaction_generator( self, bundle: Optional[SpendBundle]) -> Optional[BlockGenerator]: if bundle is None: return None return simple_solution_generator(bundle)
def cost_of_spend_bundle(spend_bundle: SpendBundle) -> int: program: BlockGenerator = simple_solution_generator(spend_bundle) npc_result: NPCResult = get_name_puzzle_conditions( program, INFINITE_COST, cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, mempool_mode=True ) return npc_result.cost
def make_block_generator(count: int) -> BlockGenerator: spend_bundle = make_spend_bundle(count) return simple_solution_generator(spend_bundle)
def get_npc_multiprocess(spend_bundle_bytes: bytes, ) -> bytes: program = simple_solution_generator( SpendBundle.from_bytes(spend_bundle_bytes)) # npc contains names of the coins removed, puzzle_hashes and their spend conditions return bytes(get_name_puzzle_conditions(program, True))