def generate_unsigned_transaction( self, amount: uint64, new_puzzle_hash: bytes32, coin: Coin, condition_dic: Dict[ConditionOpcode, List[ConditionVarPair]], fee: int = 0, secret_key=None, ) -> List[CoinSolution]: spends = [] spend_value = coin.amount puzzle_hash = coin.puzzle_hash if secret_key is None: secret_key = self.get_private_key_for_puzzle_hash(puzzle_hash) pubkey = secret_key.get_g1() puzzle = puzzle_for_pk(bytes(pubkey)) if ConditionOpcode.CREATE_COIN not in condition_dic: condition_dic[ConditionOpcode.CREATE_COIN] = [] output = ConditionVarPair(ConditionOpcode.CREATE_COIN, [new_puzzle_hash, int_to_bytes(amount)]) condition_dic[output.opcode].append(output) amount_total = sum(int_from_bytes(cvp.vars[1]) for cvp in condition_dic[ConditionOpcode.CREATE_COIN]) change = spend_value - amount_total - fee if change > 0: change_puzzle_hash = self.get_new_puzzlehash() change_output = ConditionVarPair(ConditionOpcode.CREATE_COIN, [change_puzzle_hash, int_to_bytes(change)]) condition_dic[output.opcode].append(change_output) solution = self.make_solution(condition_dic) else: solution = self.make_solution(condition_dic) puzzle_solution_pair = Program.to([puzzle, solution]) spends.append(CoinSolution(coin, puzzle_solution_pair)) return spends
def blockchain_assert_time_exceeds(condition: ConditionVarPair, timestamp): """ Checks if current time in millis exceeds the time specified in condition """ try: expected_mili_time = int_from_bytes(condition.vars[0]) except ValueError: return Err.INVALID_CONDITION current_time = timestamp if current_time <= expected_mili_time: return Err.ASSERT_TIME_EXCEEDS_FAILED return None
def blockchain_assert_block_index_exceeds(condition: ConditionVarPair, height: uint32) -> Optional[Err]: """ Checks if the next block index exceeds the block index from the condition """ try: expected_block_index = int_from_bytes(condition.vars[0]) except ValueError: return Err.INVALID_CONDITION # + 1 because min block it can be included is +1 from current if height <= expected_block_index: return Err.ASSERT_BLOCK_INDEX_EXCEEDS_FAILED return None
def mempool_assert_block_index_exceeds( condition: ConditionVarPair, prev_transaction_block_height: uint32) -> Optional[Err]: """ Checks if the next block index exceeds the block index from the condition """ try: block_index_exceeds_this = int_from_bytes(condition.vars[0]) except ValueError: return Err.INVALID_CONDITION if prev_transaction_block_height < block_index_exceeds_this: return Err.ASSERT_HEIGHT_NOW_EXCEEDS_FAILED return None
def mempool_assert_relative_time_exceeds(condition: ConditionVarPair, unspent: CoinRecord): """ Check if the current time in millis exceeds the time specified by condition """ try: expected_mili_time = int_from_bytes(condition.vars[0]) except ValueError: return Err.INVALID_CONDITION current_time = uint64(int(time.time() * 1000)) if current_time <= expected_mili_time + unspent.timestamp: return Err.ASSERT_SECONDS_NOW_EXCEEDS_FAILED return None
def mempool_assert_time_exceeds(condition: ConditionVarPair): """ Check if the current time in millis exceeds the time specified by condition """ try: expected_mili_time = int_from_bytes(condition.var1) except ValueError: return Err.INVALID_CONDITION current_time = uint64(int(time.time() * 1000)) if current_time <= expected_mili_time: return Err.ASSERT_TIME_EXCEEDS_FAILED return None
def blockchain_assert_relative_time_exceeds(condition: ConditionVarPair, unspent: CoinRecord, timestamp): """ Checks if time since unspent creation in millis exceeds the time specified in condition """ try: expected_mili_time = int_from_bytes(condition.vars[0]) except ValueError: return Err.INVALID_CONDITION current_time = timestamp if current_time <= expected_mili_time + unspent.timestamp: return Err.ASSERT_RELATIVE_TIME_EXCEEDS_FAILED return None
def blockchain_assert_block_age_exceeds(condition: ConditionVarPair, unspent: CoinRecord, height: uint32) -> Optional[Err]: """ Checks if the coin age exceeds the age from the condition """ try: expected_block_age = int_from_bytes(condition.vars[0]) expected_block_index = expected_block_age + unspent.confirmed_block_index except ValueError: return Err.INVALID_CONDITION if height <= expected_block_index: return Err.ASSERT_BLOCK_AGE_EXCEEDS_FAILED return None
def mempool_assert_block_age_exceeds( condition: ConditionVarPair, unspent: CoinRecord, prev_transaction_block_height: uint32) -> Optional[Err]: """ Checks if the coin age exceeds the age from the condition """ try: expected_block_age = int_from_bytes(condition.vars[0]) block_index_exceeds_this = expected_block_age + unspent.confirmed_block_index except ValueError: return Err.INVALID_CONDITION if prev_transaction_block_height < block_index_exceeds_this: return Err.ASSERT_HEIGHT_AGE_EXCEEDS_FAILED return None
def blockchain_assert_block_index_exceeds( condition: ConditionVarPair, prev_transaction_block_height: uint32) -> Optional[Err]: """ Checks if the next block index exceeds the block index from the condition """ try: expected_block_index = int_from_bytes(condition.vars[0]) except ValueError: return Err.INVALID_CONDITION if prev_transaction_block_height < expected_block_index: return Err.ASSERT_BLOCK_INDEX_EXCEEDS_FAILED return None
def mempool_assert_block_index_exceeds(condition: ConditionVarPair, unspent: CoinRecord, mempool: Mempool) -> Optional[Err]: """ Checks if the next block index exceeds the block index from the condition """ try: expected_block_index = int_from_bytes(condition.var1) except ValueError: return Err.INVALID_CONDITION # + 1 because min block it can be included is +1 from current if mempool.header.height + 1 <= expected_block_index: return Err.ASSERT_BLOCK_INDEX_EXCEEDS_FAILED return None
def mempool_assert_block_age_exceeds(condition: ConditionVarPair, unspent: CoinRecord, mempool: Mempool) -> Optional[Err]: """ Checks if the coin age exceeds the age from the condition """ try: expected_block_age = int_from_bytes(condition.var1) expected_block_index = expected_block_age + unspent.confirmed_block_index except ValueError: return Err.INVALID_CONDITION if mempool.header.height + 1 <= expected_block_index: return Err.ASSERT_BLOCK_AGE_EXCEEDS_FAILED return None
def created_outputs_for_conditions_dict( conditions_dict: Dict[ConditionOpcode, List[ConditionVarPair]], input_coin_name: bytes32, ) -> List[Coin]: output_coins = [] for cvp in conditions_dict.get(ConditionOpcode.CREATE_COIN, []): # TODO: check condition very carefully # (ensure there are the correct number and type of parameters) # maybe write a type-checking framework for conditions # and don't just fail with asserts puzzle_hash, amount_bin = cvp.vars[0], cvp.vars[1] amount = int_from_bytes(amount_bin) coin = Coin(input_coin_name, puzzle_hash, amount) output_coins.append(coin) return output_coins
def generate_unsigned_transaction( self, amount, newpuzzlehash, coin: Coin, condition_dic: Dict[ConditionOpcode, List[ConditionVarPair]], fee: int = 0, secretkey=None, ): spends = [] spend_value = coin.amount puzzle_hash = coin.puzzle_hash if secretkey is None: pubkey, secretkey = self.get_keys(puzzle_hash) else: pubkey = secretkey.get_g1() puzzle = puzzle_for_pk(bytes(pubkey)) if ConditionOpcode.CREATE_COIN not in condition_dic: condition_dic[ConditionOpcode.CREATE_COIN] = [] output = ConditionVarPair( ConditionOpcode.CREATE_COIN, newpuzzlehash, int_to_bytes(amount) ) condition_dic[output.opcode].append(output) amount_total = sum( int_from_bytes(cvp.var2) for cvp in condition_dic[ConditionOpcode.CREATE_COIN] ) change = spend_value - amount_total - fee if change > 0: changepuzzlehash = self.get_new_puzzlehash() change_output = ConditionVarPair( ConditionOpcode.CREATE_COIN, changepuzzlehash, int_to_bytes(change) ) condition_dic[output.opcode].append(change_output) solution = self.make_solution(condition_dic) else: solution = self.make_solution(condition_dic) spends.append((puzzle, CoinSolution(coin, solution))) return spends
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
def from_bytes(cls, blob): parent_coin_info = blob[:32] puzzle_hash = blob[32:64] amount = int_from_bytes(blob[64:]) return Coin(parent_coin_info, puzzle_hash, uint64(amount))
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 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, self.constants.CLVM_COST_RATIO_CONSTANT, ) 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 # 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 # 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.coin_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 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 ( None, MempoolInclusionStatus.FAILED, Err.ASSERT_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 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 pks: List[G1Element] = [] msgs: List[G2Element] = [] 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 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 for pk, m in pkm_pairs_for_conditions_dict( npc.condition_dict, npc.coin_name ): pks.append(pk) msgs.append(m) if error: errors.append(error) continue # Verify aggregated signature validates = AugSchemeMPL.agg_verify( pks, msgs, new_spend.aggregated_signature ) if not validates: 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]