def check_plutus_cost(plutus_cost: List[dict], expected_cost: List[ExecutionCost]): """Check plutus transaction cost. units: the time is in picoseconds and the space is in bytes. """ # sort records by total cost sorted_plutus = sorted( plutus_cost, key=lambda x: x["executionUnits"]["memory"] # type: ignore + x["executionUnits"]["steps"] + x["lovelaceCost"], ) sorted_expected = sorted( expected_cost, key=lambda x: x.per_space + x.per_time + x.fixed_cost) errors = [] for costs, expected_values in zip(sorted_plutus, sorted_expected): tx_time = costs["executionUnits"]["steps"] tx_space = costs["executionUnits"]["memory"] lovelace_cost = costs["lovelaceCost"] if not helpers.is_in_interval( tx_time, expected_values.per_time, frac=0.15): errors.append(f"time: {tx_time} vs {expected_values.per_time}") if not helpers.is_in_interval( tx_space, expected_values.per_space, frac=0.15): errors.append(f"space: {tx_space} vs {expected_values.per_space}") if not helpers.is_in_interval( lovelace_cost, expected_values.fixed_cost, frac=0.15): errors.append( f"fixed cost: {lovelace_cost} vs {expected_values.fixed_cost}") if errors: raise AssertionError("\n".join(errors))
def test_addr_deregistration_fees( self, cluster: clusterlib.ClusterLib, pool_users: List[clusterlib.PoolUser], addr_fee: Tuple[int, int], ): """Test stake address deregistration fees.""" no_of_addr, expected_fee = addr_fee temp_template = f"{helpers.get_func_name()}_{no_of_addr}" src_address = pool_users[0].payment.address selected_users = pool_users[:no_of_addr] stake_addr_dereg_certs = [ cluster.gen_stake_addr_deregistration_cert( addr_name=f"{temp_template}_addr{i}", stake_vkey_file=p.stake.vkey_file) for i, p in enumerate(selected_users) ] # create TX data tx_files = clusterlib.TxFiles( certificate_files=[*stake_addr_dereg_certs], signing_key_files=[ *[p.payment.skey_file for p in selected_users], *[p.stake.skey_file for p in selected_users], ], ) # calculate TX fee tx_fee = cluster.calculate_tx_fee(src_address=src_address, tx_name=temp_template, tx_files=tx_files) assert helpers.is_in_interval( tx_fee, expected_fee), "Expected fee doesn't match the actual fee"
def _from_to_transactions( self, cluster_obj: clusterlib.ClusterLib, tx_name: str, pool_users: List[clusterlib.PoolUser], from_num: int, to_num: int, amount_expected: Tuple[int, int], ): """Check fees for 1 tx from `from_num` payment addresses to `to_num` payment addresses.""" amount, expected_fee = amount_expected src_address = pool_users[0].payment.address # addr1..addr<from_num+1> from_addr_recs = [p.payment for p in pool_users[1 : from_num + 1]] # addr<from_num+1>..addr<from_num+to_num+1> dst_addresses = [ pool_users[i].payment.address for i in range(from_num + 1, from_num + to_num + 1) ] # create TX data _txins = [cluster_obj.get_utxo(address=r.address) for r in from_addr_recs] # flatten the list of lists that is _txins txins = list(itertools.chain.from_iterable(_txins)) txouts = [clusterlib.TxOut(address=addr, amount=amount) for addr in dst_addresses] tx_files = clusterlib.TxFiles(signing_key_files=[r.skey_file for r in from_addr_recs]) # calculate TX fee tx_fee = cluster_obj.calculate_tx_fee( src_address=src_address, tx_name=tx_name, txins=txins, txouts=txouts, tx_files=tx_files ) assert helpers.is_in_interval( tx_fee, expected_fee ), "Expected fee doesn't match the actual fee"
def test_pool_registration_fees( self, cluster: clusterlib.ClusterLib, temp_dir: Path, pool_users: List[clusterlib.PoolUser], addr_fee: Tuple[int, int], ): """Test pool registration fees.""" no_of_addr, expected_fee = addr_fee rand_str = clusterlib.get_rand_str(4) temp_template = f"{helpers.get_func_name()}_{rand_str}_{no_of_addr}" pool_name = f"pool_{rand_str}" pool_metadata = { "name": pool_name, "description": "Shelley QA E2E test Test", "ticker": "QA1", "homepage": "www.test1.com", } pool_metadata_file = helpers.write_json( temp_dir / f"{pool_name}_registration_metadata.json", pool_metadata) pool_data = clusterlib.PoolData( pool_name=pool_name, pool_pledge=1000, pool_cost=15, pool_margin=0.2, pool_metadata_url="https://www.where_metadata_file_is_located.com", pool_metadata_hash=cluster.gen_pool_metadata_hash( pool_metadata_file), ) # create pool owners selected_owners = pool_users[:no_of_addr] # create certificates src_address, tx_files = self._create_pool_certificates( cluster_obj=cluster, pool_owners=selected_owners, temp_template=temp_template, pool_data=pool_data, ) # calculate TX fee tx_fee = cluster.calculate_tx_fee(src_address=src_address, tx_name=temp_template, tx_files=tx_files) assert helpers.is_in_interval( tx_fee, expected_fee), "Expected fee doesn't match the actual fee"
def test_oversaturated( # noqa: C901 self, cluster_manager: cluster_management.ClusterManager, cluster_lock_pools: clusterlib.ClusterLib, ): """Check diminished rewards when stake pool is oversaturated. The stake pool continues to operate normally and those who delegate to that pool receive rewards, but the rewards are proportionally lower than those received from stake pool that is not oversaturated. * register and delegate stake address in "init epoch", for all available pools * in "init epoch" + 2, saturate all available pools (block distribution remains balanced among pools) * in "init epoch" + 3, oversaturate one pool * in "init epoch" + 5, for all available pools, withdraw rewards and transfer funds from delegated addresses so pools are no longer (over)saturated * while doing the steps above, collect rewards data for 9 epochs * compare proportionality of rewards in epochs where pools were non-saturated, saturated and oversaturated """ # pylint: disable=too-many-statements,too-many-locals,too-many-branches epoch_saturate = 2 epoch_oversaturate = 4 epoch_withdrawal = 6 cluster = cluster_lock_pools temp_template = common.get_test_id(cluster) initial_balance = 1_000_000_000 faucet_rec = cluster_manager.cache.addrs_data["byron000"] pool_records: Dict[int, PoolRecord] = {} # make sure we have enough time to finish the delegation in one epoch clusterlib_utils.wait_for_epoch_interval(cluster_obj=cluster, start=5, stop=-40) init_epoch = cluster.get_epoch() # submit registration certificates and delegate to pools for idx, res in enumerate( [ cluster_management.Resources.POOL1, cluster_management.Resources.POOL2, cluster_management.Resources.POOL3, ], start=1, ): pool_addrs_data = cluster_manager.cache.addrs_data[res] reward_addr = clusterlib.PoolUser( payment=pool_addrs_data["payment"], stake=pool_addrs_data["reward"]) pool_id = delegation.get_pool_id( cluster_obj=cluster, addrs_data=cluster_manager.cache.addrs_data, pool_name=res, ) pool_id_dec = helpers.decode_bech32(bech32=pool_id) delegation_out = delegation.delegate_stake_addr( cluster_obj=cluster, addrs_data=cluster_manager.cache.addrs_data, temp_template=f"{temp_template}_pool{idx}", pool_id=pool_id, amount=initial_balance, ) pool_records[idx] = PoolRecord( name=res, id=pool_id, id_dec=pool_id_dec, reward_addr=reward_addr, delegation_out=delegation_out, user_rewards=[], owner_rewards=[], blocks_minted={}, saturation_amounts={}, ) # record initial reward balance for each pool for pool_rec in pool_records.values(): user_payment_balance = cluster.get_address_balance( pool_rec.delegation_out.pool_user.payment.address) owner_payment_balance = cluster.get_address_balance( pool_rec.reward_addr.payment.address) pool_rec.user_rewards.append( RewardRecord( epoch_no=init_epoch, reward_total=0, reward_per_epoch=0, stake_total=user_payment_balance, )) pool_rec.owner_rewards.append( RewardRecord( epoch_no=init_epoch, reward_total=cluster.get_stake_addr_info( pool_rec.reward_addr.stake.address). reward_account_balance, reward_per_epoch=0, stake_total=owner_payment_balance, )) assert ( cluster.get_epoch() == init_epoch ), "Delegation took longer than expected and would affect other checks" LOGGER.info("Checking rewards for 10 epochs.") for __ in range(10): # wait for new epoch if cluster.get_epoch( ) == pool_records[2].owner_rewards[-1].epoch_no: cluster.wait_for_new_epoch() # sleep till the end of epoch clusterlib_utils.wait_for_epoch_interval(cluster_obj=cluster, start=-50, stop=-40, force_epoch=True) this_epoch = cluster.get_epoch() ledger_state = clusterlib_utils.get_ledger_state( cluster_obj=cluster) clusterlib_utils.save_ledger_state( cluster_obj=cluster, state_name=f"{temp_template}_{this_epoch}", ledger_state=ledger_state, ) for pool_rec in pool_records.values(): # reward balance in previous epoch prev_user_reward = pool_rec.user_rewards[-1].reward_total prev_owner_reward = pool_rec.owner_rewards[-1].reward_total pool_rec.blocks_minted[this_epoch - 1] = (ledger_state["blocksBefore"].get( pool_rec.id_dec) or 0) # current reward balance user_reward = cluster.get_stake_addr_info( pool_rec.delegation_out.pool_user.stake.address ).reward_account_balance owner_reward = cluster.get_stake_addr_info( pool_rec.reward_addr.stake.address).reward_account_balance # total reward amounts received this epoch owner_reward_epoch = owner_reward - prev_owner_reward # We cannot compare with previous rewards in epochs where # `this_epoch >= init_epoch + epoch_withdrawal`. # There's a withdrawal of rewards at the end of these epochs. if this_epoch > init_epoch + epoch_withdrawal: user_reward_epoch = user_reward else: user_reward_epoch = user_reward - prev_user_reward # store collected rewards info user_payment_balance = cluster.get_address_balance( pool_rec.delegation_out.pool_user.payment.address) owner_payment_balance = cluster.get_address_balance( pool_rec.reward_addr.payment.address) pool_rec.user_rewards.append( RewardRecord( epoch_no=this_epoch, reward_total=user_reward, reward_per_epoch=user_reward_epoch, stake_total=user_payment_balance + user_reward, )) pool_rec.owner_rewards.append( RewardRecord( epoch_no=this_epoch, reward_total=owner_reward, reward_per_epoch=owner_reward_epoch, stake_total=owner_payment_balance, )) pool_rec.saturation_amounts[ this_epoch] = _get_saturation_threshold( cluster_obj=cluster, ledger_state=ledger_state, pool_id=pool_rec.id) # fund the delegated addresses - saturate all pools if this_epoch == init_epoch + epoch_saturate: clusterlib_utils.fund_from_faucet( *[ p.delegation_out.pool_user.payment for p in pool_records.values() ], cluster_obj=cluster, faucet_data=faucet_rec, amount=[ p.saturation_amounts[this_epoch] - 100_000_000_000 for p in pool_records.values() ], tx_name=f"{temp_template}_saturate_pools_ep{this_epoch}", force=True, ) with cluster_manager.restart_on_failure(): # Fund the address delegated to "pool2" to oversaturate the pool. # New stake amount will be current (saturated) stake * 2. if this_epoch == init_epoch + epoch_oversaturate: assert (pool_records[2].saturation_amounts[this_epoch] > 0), "Pool is already saturated" current_stake = int( cluster.get_stake_snapshot( pool_records[2].id)["poolStakeMark"]) overstaturate_amount = current_stake * 2 saturation_threshold = pool_records[2].saturation_amounts[ this_epoch] assert overstaturate_amount > saturation_threshold, ( f"{overstaturate_amount} Lovelace is not enough to oversature the pool " f"({saturation_threshold} is needed)") clusterlib_utils.fund_from_faucet( pool_records[2].delegation_out.pool_user.payment, cluster_obj=cluster, faucet_data=faucet_rec, amount=overstaturate_amount, tx_name=f"{temp_template}_oversaturate_pool2", force=True, ) # wait 4 epochs for first rewards if this_epoch >= init_epoch + 4: assert (owner_reward > prev_owner_reward ), "New reward was not received by pool owner" # transfer funds back to faucet so the pools are no longer (over)saturated # and staked amount is +- same as the `initial_balance` if this_epoch >= init_epoch + epoch_withdrawal: _withdraw_rewards( *[ p.delegation_out.pool_user for p in pool_records.values() ], cluster_obj=cluster, tx_name=f"{temp_template}_ep{this_epoch}", ) return_to_addrs = [] return_amounts = [] for idx, pool_rec in pool_records.items(): deleg_payment_balance = cluster.get_address_balance( pool_rec.delegation_out.pool_user.payment.address) if deleg_payment_balance > initial_balance + 10_000_000: return_to_addrs.append( pool_rec.delegation_out.pool_user.payment) return_amounts.append(deleg_payment_balance - initial_balance) clusterlib_utils.return_funds_to_faucet( *return_to_addrs, cluster_obj=cluster, faucet_addr=faucet_rec["payment"].address, amount=return_amounts, tx_name=f"{temp_template}_ep{this_epoch}", ) for return_addr in return_to_addrs: deleg_payment_balance = cluster.get_address_balance( return_addr.address) assert ( deleg_payment_balance <= initial_balance ), "Unexpected funds in payment address '{return_addr}'" assert ( cluster.get_epoch() == this_epoch ), "Failed to finish actions in single epoch, it would affect other checks" pool1_user_rewards_per_block = _get_reward_per_block(pool_records[1]) pool2_user_rewards_per_block = _get_reward_per_block(pool_records[2]) pool3_user_rewards_per_block = _get_reward_per_block(pool_records[3]) pool1_owner_rewards_per_block = _get_reward_per_block( pool_records[1], owner_rewards=True) pool2_owner_rewards_per_block = _get_reward_per_block( pool_records[2], owner_rewards=True) pool3_owner_rewards_per_block = _get_reward_per_block( pool_records[3], owner_rewards=True) oversaturated_epoch = max( e for e, r in pool_records[2].saturation_amounts.items() if r < 0) saturated_epoch = oversaturated_epoch - 2 nonsaturated_epoch = oversaturated_epoch - 4 try: # check that rewards per block per stake for "pool2" in the epoch where the pool is # oversaturated is lower than in epochs where pools are not oversaturated assert (pool1_user_rewards_per_block[nonsaturated_epoch] > pool2_user_rewards_per_block[oversaturated_epoch]) assert (pool2_user_rewards_per_block[nonsaturated_epoch] > pool2_user_rewards_per_block[oversaturated_epoch]) assert (pool3_user_rewards_per_block[nonsaturated_epoch] > pool2_user_rewards_per_block[oversaturated_epoch]) assert (pool1_user_rewards_per_block[saturated_epoch] > pool2_user_rewards_per_block[oversaturated_epoch]) assert (pool2_user_rewards_per_block[saturated_epoch] > pool2_user_rewards_per_block[oversaturated_epoch]) assert (pool3_user_rewards_per_block[saturated_epoch] > pool2_user_rewards_per_block[oversaturated_epoch]) # check that oversaturated pool doesn't lead to increased rewards for pool owner # when compared to saturated pool, i.e. total pool margin amount is not increased pool1_rew_fraction_sat = pool1_owner_rewards_per_block[ saturated_epoch] pool2_rew_fraction_sat = pool2_owner_rewards_per_block[ saturated_epoch] pool3_rew_fraction_sat = pool3_owner_rewards_per_block[ saturated_epoch] pool2_rew_fraction_over = pool2_owner_rewards_per_block[ oversaturated_epoch] assert pool2_rew_fraction_sat > pool2_rew_fraction_over or helpers.is_in_interval( pool2_rew_fraction_sat, pool2_rew_fraction_over, frac=0.4, ) assert pool1_rew_fraction_sat > pool2_rew_fraction_over or helpers.is_in_interval( pool1_rew_fraction_sat, pool2_rew_fraction_over, frac=0.4, ) assert pool3_rew_fraction_sat > pool2_rew_fraction_over or helpers.is_in_interval( pool3_rew_fraction_sat, pool2_rew_fraction_over, frac=0.4, ) # Compare rewards in last (non-saturated) epoch to rewards in next-to-last # (saturated / over-saturated) epoch. # This way check that staked amount for each pool was restored to `initial_balance` # and that rewards correspond to the restored amounts. for pool_rec in pool_records.values(): assert (pool_rec.user_rewards[-1].reward_per_epoch * 100 < pool_rec.user_rewards[-2].reward_per_epoch) except Exception: # save debugging data in case of test failure with open(f"{temp_template}_pool_records.pickle", "wb") as out_data: pickle.dump(pool_records, out_data) raise
def test_pool_deregistration_fees( self, cluster: clusterlib.ClusterLib, temp_dir: Path, pool_users: List[clusterlib.PoolUser], addr_fee: Tuple[int, int], ): """Test pool deregistration fees.""" no_of_addr, expected_fee = addr_fee rand_str = clusterlib.get_rand_str(4) temp_template = f"{helpers.get_func_name()}_{rand_str}_{no_of_addr}" src_address = pool_users[0].payment.address pool_name = f"pool_{rand_str}" pool_metadata = { "name": pool_name, "description": "Shelley QA E2E test Test", "ticker": "QA1", "homepage": "www.test1.com", } pool_metadata_file = helpers.write_json( temp_dir / f"{pool_name}_registration_metadata.json", pool_metadata) pool_data = clusterlib.PoolData( pool_name=pool_name, pool_pledge=222, pool_cost=123, pool_margin=0.512, pool_metadata_url="https://www.where_metadata_file_is_located.com", pool_metadata_hash=cluster.gen_pool_metadata_hash( pool_metadata_file), ) # create pool owners selected_owners = pool_users[:no_of_addr] # create node cold key pair and counter node_cold = cluster.gen_cold_key_pair_and_counter( node_name=pool_data.pool_name) # create deregistration certificate pool_dereg_cert_file = cluster.gen_pool_deregistration_cert( pool_name=pool_data.pool_name, cold_vkey_file=node_cold.vkey_file, epoch=cluster.get_epoch() + 1, ) tx_files = clusterlib.TxFiles( certificate_files=[pool_dereg_cert_file], signing_key_files=[ *[p.payment.skey_file for p in selected_owners], *[p.stake.skey_file for p in selected_owners], node_cold.skey_file, ], ) # calculate TX fee tx_fee = cluster.calculate_tx_fee(src_address=src_address, tx_name=temp_template, tx_files=tx_files) assert helpers.is_in_interval( tx_fee, expected_fee), "Expected fee doesn't match the actual fee"
def test_valid_policy_before( self, cluster: clusterlib.ClusterLib, issuers_addrs: List[clusterlib.AddressRecord]): """Test minting and burning tokens before given slot.""" expected_fee = 351093 temp_template = helpers.get_func_name() rand = clusterlib.get_rand_str(4) amount = 5 token_mint_addr = issuers_addrs[0] payment_vkey_files = [p.vkey_file for p in issuers_addrs] before_slot = cluster.get_slot_no() + 10_000 # create multisig script multisig_script = cluster.build_multisig_script( script_name=temp_template, script_type_arg=clusterlib.MultiSigTypeArgs.ALL, payment_vkey_files=payment_vkey_files[1:], slot=before_slot, slot_type_arg=clusterlib.MultiSlotTypeArgs.BEFORE, ) policyid = cluster.get_policyid(multisig_script) tokens_to_mint = [] for tnum in range(5): asset_name = f"couttscoin{rand}{tnum}" token = f"{policyid}.{asset_name}" assert not cluster.get_utxo(token_mint_addr.address, coins=[ token ]), "The token already exists" tokens_to_mint.append( clusterlib_utils.TokenRecord( token=token, asset_name=asset_name, amount=amount, issuers_addrs=issuers_addrs, token_mint_addr=token_mint_addr, script=multisig_script, )) # token minting tx_out_mint = _mint_or_burn_witness( cluster_obj=cluster, new_tokens=tokens_to_mint, temp_template=f"{temp_template}_mint", invalid_before=100, invalid_hereafter=cluster.get_slot_no() + 1000, ) for t in tokens_to_mint: token_utxo = cluster.get_utxo(token_mint_addr.address, coins=[t.token]) assert token_utxo and token_utxo[ 0].amount == amount, "The token was not minted" # token burning tokens_to_burn = [t._replace(amount=-amount) for t in tokens_to_mint] tx_out_burn = _mint_or_burn_witness( cluster_obj=cluster, new_tokens=tokens_to_burn, temp_template=f"{temp_template}_burn", invalid_before=100, invalid_hereafter=cluster.get_slot_no() + 1000, ) for t in tokens_to_burn: token_utxo = cluster.get_utxo(token_mint_addr.address, coins=[t.token]) assert not token_utxo, "The token was not burnt" # check expected fees assert helpers.is_in_interval( tx_out_mint.fee, expected_fee, frac=0.15) and helpers.is_in_interval( tx_out_burn.fee, expected_fee, frac=0.15), "TX fee doesn't fit the expected interval"
def test_minting_and_partial_burning( self, cluster: clusterlib.ClusterLib, issuers_addrs: List[clusterlib.AddressRecord]): """Test minting and partial burning of tokens.""" expected_fee = 201141 temp_template = helpers.get_func_name() asset_name = f"couttscoin{clusterlib.get_rand_str(4)}" amount = 50 payment_vkey_files = [p.vkey_file for p in issuers_addrs] token_mint_addr = issuers_addrs[0] # create multisig script multisig_script = cluster.build_multisig_script( script_name=temp_template, script_type_arg=clusterlib.MultiSigTypeArgs.ALL, payment_vkey_files=payment_vkey_files[1:], ) policyid = cluster.get_policyid(multisig_script) token = f"{policyid}.{asset_name}" assert not cluster.get_utxo(token_mint_addr.address, coins=[token]), "The token already exists" token_mint = clusterlib_utils.TokenRecord( token=token, asset_name=asset_name, amount=amount, issuers_addrs=issuers_addrs, token_mint_addr=token_mint_addr, script=multisig_script, ) # token minting tx_out_mint = _mint_or_burn_witness( cluster_obj=cluster, new_tokens=[token_mint], temp_template=f"{temp_template}_mint", ) token_utxo = cluster.get_utxo(token_mint_addr.address, coins=[token]) assert token_utxo and token_utxo[ 0].amount == amount, "The token was not minted" # token burning burn_amount = amount - 10 token_burn = token_mint._replace(amount=-burn_amount) _mint_or_burn_witness( cluster_obj=cluster, new_tokens=[token_burn], temp_template=f"{temp_template}_burn", ) token_utxo = cluster.get_utxo(token_mint_addr.address, coins=[token]) assert (token_utxo and token_utxo[0].amount == amount - burn_amount), "The token was not burned" # burn the rest of tokens final_burn = token_mint._replace(amount=-10) _mint_or_burn_witness( cluster_obj=cluster, new_tokens=[final_burn], temp_template=f"{temp_template}_burn", ) # check expected fee assert helpers.is_in_interval( tx_out_mint.fee, expected_fee, frac=0.15), "TX fee doesn't fit the expected interval"
def test_multi_minting_and_burning_sign( self, cluster: clusterlib.ClusterLib, issuers_addrs: List[clusterlib.AddressRecord], simple_script_policyid: Tuple[Path, str], tokens_db: Tuple[int, int], ): """Test minting and burning multiple different tokens, sign the TX using skeys.""" temp_template = helpers.get_func_name() rand = clusterlib.get_rand_str(8) amount = 5 tokens_num, expected_fee = tokens_db token_mint_addr = issuers_addrs[0] issuer_addr = issuers_addrs[1] script, policyid = simple_script_policyid tokens_to_mint = [] for tnum in range(tokens_num): asset_name = f"couttscoin{rand}{tnum}" token = f"{policyid}.{asset_name}" tokens_to_mint.append( clusterlib_utils.TokenRecord( token=token, asset_name=asset_name, amount=amount, issuers_addrs=[issuer_addr], token_mint_addr=token_mint_addr, script=script, )) # token minting minting_args = { "cluster_obj": cluster, "new_tokens": tokens_to_mint, "temp_template": f"{temp_template}_mint", } if tokens_num >= 500: with pytest.raises(clusterlib.CLIError) as excinfo: clusterlib_utils.mint_or_burn_sign( **minting_args) # type: ignore if tokens_num >= 1000: assert "(UtxoFailure (MaxTxSizeUTxO" in str(excinfo.value) else: assert "(UtxoFailure (OutputTooBigUTxO" in str(excinfo.value) return tx_out_mint = clusterlib_utils.mint_or_burn_sign( **minting_args) # type: ignore for t in tokens_to_mint: token_utxo = cluster.get_utxo(token_mint_addr.address, coins=[t.token]) assert token_utxo and token_utxo[ 0].amount == amount, "The token was not minted" # token burning tokens_to_burn = [t._replace(amount=-amount) for t in tokens_to_mint] tx_out_burn = clusterlib_utils.mint_or_burn_sign( cluster_obj=cluster, new_tokens=tokens_to_burn, temp_template=f"{temp_template}_burn", ) for t in tokens_to_burn: token_utxo = cluster.get_utxo(token_mint_addr.address, coins=[t.token]) assert not token_utxo, "The token was not burnt" # check expected fees assert helpers.is_in_interval( tx_out_mint.fee, expected_fee, frac=0.15) and helpers.is_in_interval( tx_out_burn.fee, expected_fee, frac=0.15), "TX fee doesn't fit the expected interval"
def test_minting_and_burning_sign( self, cluster: clusterlib.ClusterLib, issuers_addrs: List[clusterlib.AddressRecord]): """Test minting and burning of tokens, sign the transaction using skeys.""" expected_fee = 188821 temp_template = helpers.get_func_name() asset_name = f"couttscoin{clusterlib.get_rand_str(4)}" amount = 5 token_mint_addr = issuers_addrs[0] issuer_addr = issuers_addrs[1] # create simple script keyhash = cluster.get_payment_vkey_hash(issuer_addr.vkey_file) script_content = {"keyHash": keyhash, "type": "sig"} script = Path(f"{temp_template}.script") with open(f"{temp_template}.script", "w") as out_json: json.dump(script_content, out_json) policyid = cluster.get_policyid(script) token = f"{policyid}.{asset_name}" assert not cluster.get_utxo(token_mint_addr.address, coins=[token]), "The token already exists" token_mint = clusterlib_utils.TokenRecord( token=token, asset_name=asset_name, amount=amount, issuers_addrs=[issuer_addr], token_mint_addr=token_mint_addr, script=script, ) # token minting tx_out_mint = clusterlib_utils.mint_or_burn_sign( cluster_obj=cluster, new_tokens=[token_mint], temp_template=f"{temp_template}_mint", ) token_utxo = cluster.get_utxo(token_mint_addr.address, coins=[token]) assert token_utxo and token_utxo[ 0].amount == amount, "The token was not minted" # token burning token_burn = token_mint._replace(amount=-amount) tx_out_burn = clusterlib_utils.mint_or_burn_sign( cluster_obj=cluster, new_tokens=[token_burn], temp_template=f"{temp_template}_burn", ) token_utxo = cluster.get_utxo(token_mint_addr.address, coins=[token]) assert not token_utxo, "The token was not burnt" # check expected fees assert helpers.is_in_interval( tx_out_mint.fee, expected_fee, frac=0.15) and helpers.is_in_interval( tx_out_burn.fee, expected_fee, frac=0.15), "TX fee doesn't fit the expected interval"
def test_witness_redeemer( self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord], key: str, ): """Test minting a token with a Plutus script. Uses `cardano-cli transaction build` command for building the transactions. * fund the token issuer and create a UTxO for collateral * check that the expected amount was transferred to token issuer's address * mint the token using a Plutus script with required signer * check that the token was minted and collateral UTxO was not spent * check expected fees * check expected Plutus cost * (optional) check transactions in db-sync """ # pylint: disable=too-many-locals temp_template = common.get_test_id(cluster) payment_addr = payment_addrs[0] issuer_addr = payment_addrs[1] lovelace_amount = 2_000_000 token_amount = 5 script_fund = 200_000_000 minting_cost = plutus_common.compute_cost( execution_cost=plutus_common.MINTING_WITNESS_REDEEMER_COST, protocol_params=cluster.get_protocol_params(), ) if key == "normal": redeemer_file = plutus_common.DATUM_WITNESS_GOLDEN_NORMAL signing_key_golden = plutus_common.SIGNING_KEY_GOLDEN else: redeemer_file = plutus_common.DATUM_WITNESS_GOLDEN_EXTENDED signing_key_golden = plutus_common.SIGNING_KEY_GOLDEN_EXTENDED issuer_init_balance = cluster.get_address_balance(issuer_addr.address) # Step 1: fund the token issuer mint_utxos, collateral_utxos, tx_output_step1 = _fund_issuer( cluster_obj=cluster, temp_template=temp_template, payment_addr=payment_addr, issuer_addr=issuer_addr, minting_cost=minting_cost, amount=script_fund, ) # Step 2: mint the "qacoin" policyid = cluster.get_policyid(plutus_common.MINTING_PLUTUS_V1) asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode( "utf-8").hex() token = f"{policyid}.{asset_name}" mint_txouts = [ clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token) ] plutus_mint_data = [ clusterlib.Mint( txouts=mint_txouts, script_file=plutus_common.MINTING_PLUTUS_V1, collaterals=collateral_utxos, redeemer_file=redeemer_file, ) ] tx_files_step2 = clusterlib.TxFiles( signing_key_files=[issuer_addr.skey_file, signing_key_golden], ) txouts_step2 = [ clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), *mint_txouts, ] tx_output_step2 = cluster.build_tx( src_address=payment_addr.address, tx_name=f"{temp_template}_step2", tx_files=tx_files_step2, txins=mint_utxos, txouts=txouts_step2, mint=plutus_mint_data, required_signers=[signing_key_golden], ) plutus_cost = cluster.calculate_plutus_script_cost( src_address=payment_addr.address, tx_name=f"{temp_template}_step2", tx_files=tx_files_step2, txins=mint_utxos, txouts=txouts_step2, mint=plutus_mint_data, required_signers=[signing_key_golden], ) # sign incrementally (just to check that it works) tx_signed_step2 = cluster.sign_tx( tx_body_file=tx_output_step2.out_file, signing_key_files=[issuer_addr.skey_file], tx_name=f"{temp_template}_step2_sign0", ) tx_signed_step2_inc = cluster.sign_tx( tx_file=tx_signed_step2, signing_key_files=[signing_key_golden], tx_name=f"{temp_template}_step2_sign1", ) cluster.submit_tx(tx_file=tx_signed_step2_inc, txins=mint_utxos) assert ( cluster.get_address_balance( issuer_addr.address) == issuer_init_balance + minting_cost.collateral + lovelace_amount ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" token_utxo = cluster.get_utxo(address=issuer_addr.address, coins=[token]) assert token_utxo and token_utxo[ 0].amount == token_amount, "The token was not minted" # check expected fees expected_fee_step1 = 167_349 assert helpers.is_in_interval(tx_output_step1.fee, expected_fee_step1, frac=0.15) expected_fee_step2 = 372_438 assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) plutus_common.check_plutus_cost( plutus_cost=plutus_cost, expected_cost=[plutus_common.MINTING_WITNESS_REDEEMER_COST], ) dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_step1) dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_step2)
def test_two_scripts_minting( self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord]): """Test minting two tokens with two different Plutus scripts. Uses `cardano-cli transaction build` command for building the transactions. * fund the token issuer and create a UTxO for collaterals * check that the expected amount was transferred to token issuer's address * mint the tokens using two different Plutus scripts * check that the tokens were minted and collateral UTxOs were not spent * check transaction view output * check expected fees * check expected Plutus cost * (optional) check transactions in db-sync """ # pylint: disable=too-many-locals,too-many-statements temp_template = common.get_test_id(cluster) payment_addr = payment_addrs[0] issuer_addr = payment_addrs[1] lovelace_amount = 2_000_000 token_amount = 5 script_fund = 500_000_000 # this is higher than `plutus_common.MINTING*_COST`, because the script context has changed # to include more stuff minting_cost_two = plutus_common.ExecutionCost(per_time=408_545_501, per_space=1_126_016, fixed_cost=94_428) minting_time_range_cost_two = plutus_common.ExecutionCost( per_time=427_707_230, per_space=1_188_952, fixed_cost=99_441) protocol_params = cluster.get_protocol_params() minting_cost1 = plutus_common.compute_cost( execution_cost=minting_cost_two, protocol_params=protocol_params) minting_cost2 = plutus_common.compute_cost( execution_cost=minting_time_range_cost_two, protocol_params=protocol_params) issuer_init_balance = cluster.get_address_balance(issuer_addr.address) # Step 1: fund the token issuer tx_files_step1 = clusterlib.TxFiles( signing_key_files=[payment_addr.skey_file], ) txouts_step1 = [ clusterlib.TxOut(address=issuer_addr.address, amount=script_fund), # for collaterals clusterlib.TxOut(address=issuer_addr.address, amount=minting_cost1.collateral), clusterlib.TxOut(address=issuer_addr.address, amount=minting_cost2.collateral), ] tx_output_step1 = cluster.build_tx( src_address=payment_addr.address, tx_name=f"{temp_template}_step1", tx_files=tx_files_step1, txouts=txouts_step1, fee_buffer=2_000_000, # don't join 'change' and 'collateral' txouts, we need separate UTxOs join_txouts=False, ) tx_signed_step1 = cluster.sign_tx( tx_body_file=tx_output_step1.out_file, signing_key_files=tx_files_step1.signing_key_files, tx_name=f"{temp_template}_step1", ) cluster.submit_tx(tx_file=tx_signed_step1, txins=tx_output_step1.txins) issuer_step1_balance = cluster.get_address_balance(issuer_addr.address) assert ( issuer_step1_balance == issuer_init_balance + script_fund + minting_cost1.collateral + minting_cost2.collateral ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" # Step 2: mint the "qacoins" txid_step1 = cluster.get_txid(tx_body_file=tx_output_step1.out_file) mint_utxos = cluster.get_utxo(txin=f"{txid_step1}#1") collateral_utxo1 = cluster.get_utxo(txin=f"{txid_step1}#2") collateral_utxo2 = cluster.get_utxo(txin=f"{txid_step1}#3") slot_step2 = cluster.get_slot_no() # "time range" qacoin slots_offset = 200 timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 protocol_version = cluster.get_protocol_params( )["protocolVersion"]["major"] if protocol_version > 5: # POSIX timestamp + offset redeemer_value_timerange = ( int(datetime.datetime.now().timestamp() * 1_000) + timestamp_offset_ms) else: # BUG: https://github.com/input-output-hk/cardano-node/issues/3090 redeemer_value_timerange = 1_000_000_000_000 policyid_timerange = cluster.get_policyid( plutus_common.MINTING_TIME_RANGE_PLUTUS_V1) asset_name_timerange = f"qacoint{clusterlib.get_rand_str(4)}".encode( "utf-8").hex() token_timerange = f"{policyid_timerange}.{asset_name_timerange}" mint_txouts_timerange = [ clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_timerange) ] # "anyone can mint" qacoin redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR policyid_anyone = cluster.get_policyid(plutus_common.MINTING_PLUTUS_V1) asset_name_anyone = f"qacoina{clusterlib.get_rand_str(4)}".encode( "utf-8").hex() token_anyone = f"{policyid_anyone}.{asset_name_anyone}" mint_txouts_anyone = [ clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_anyone) ] # mint the tokens plutus_mint_data = [ clusterlib.Mint( txouts=mint_txouts_timerange, script_file=plutus_common.MINTING_TIME_RANGE_PLUTUS_V1, collaterals=collateral_utxo1, redeemer_value=str(redeemer_value_timerange), ), clusterlib.Mint( txouts=mint_txouts_anyone, script_file=plutus_common.MINTING_PLUTUS_V1, collaterals=collateral_utxo2, redeemer_cbor_file=redeemer_cbor_file, ), ] tx_files_step2 = clusterlib.TxFiles( signing_key_files=[issuer_addr.skey_file], ) txouts_step2 = [ clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), *mint_txouts_timerange, *mint_txouts_anyone, ] tx_output_step2 = cluster.build_tx( src_address=payment_addr.address, tx_name=f"{temp_template}_step2", tx_files=tx_files_step2, txins=mint_utxos, txouts=txouts_step2, mint=plutus_mint_data, invalid_before=slot_step2 - slots_offset, invalid_hereafter=slot_step2 + slots_offset, ) plutus_cost = cluster.calculate_plutus_script_cost( src_address=payment_addr.address, tx_name=f"{temp_template}_step2", tx_files=tx_files_step2, txins=mint_utxos, txouts=txouts_step2, mint=plutus_mint_data, invalid_before=slot_step2 - slots_offset, invalid_hereafter=slot_step2 + slots_offset, ) tx_signed_step2 = cluster.sign_tx( tx_body_file=tx_output_step2.out_file, signing_key_files=tx_files_step2.signing_key_files, tx_name=f"{temp_template}_step2", ) cluster.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) assert ( cluster.get_address_balance( issuer_addr.address) == issuer_init_balance + minting_cost1.collateral + minting_cost2.collateral + lovelace_amount ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" token_utxo_timerange = cluster.get_utxo(address=issuer_addr.address, coins=[token_timerange]) assert (token_utxo_timerange and token_utxo_timerange[0].amount == token_amount), "The 'timerange' token was not minted" token_utxo_anyone = cluster.get_utxo(address=issuer_addr.address, coins=[token_anyone]) assert (token_utxo_anyone and token_utxo_anyone[0].amount == token_amount), "The 'anyone' token was not minted" # check expected fees expected_fee_step1 = 168_977 assert helpers.is_in_interval(tx_output_step1.fee, expected_fee_step1, frac=0.15) expected_fee_step2 = 633_269 assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) plutus_common.check_plutus_cost( plutus_cost=plutus_cost, expected_cost=[minting_cost_two, minting_time_range_cost_two], ) # check tx_view tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_step2) # check transactions in db-sync dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_step1) dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_step2)
def test_time_range_minting(self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord]): """Test minting a token with a time constraints Plutus script. Uses `cardano-cli transaction build` command for building the transactions. * fund the token issuer and create a UTxO for collateral * check that the expected amount was transferred to token issuer's address * mint the token using a Plutus script * check that the token was minted and collateral UTxO was not spent * check expected fees * check expected Plutus cost * (optional) check transactions in db-sync """ # pylint: disable=too-many-locals temp_template = common.get_test_id(cluster) payment_addr = payment_addrs[0] issuer_addr = payment_addrs[1] lovelace_amount = 2_000_000 token_amount = 5 script_fund = 200_000_000 minting_cost = plutus_common.compute_cost( execution_cost=plutus_common.MINTING_TIME_RANGE_COST, protocol_params=cluster.get_protocol_params(), ) issuer_init_balance = cluster.get_address_balance(issuer_addr.address) # Step 1: fund the token issuer mint_utxos, collateral_utxos, tx_output_step1 = _fund_issuer( cluster_obj=cluster, temp_template=temp_template, payment_addr=payment_addr, issuer_addr=issuer_addr, minting_cost=minting_cost, amount=script_fund, ) # Step 2: mint the "qacoin" slot_step2 = cluster.get_slot_no() slots_offset = 200 timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 protocol_version = cluster.get_protocol_params( )["protocolVersion"]["major"] if protocol_version > 5: # POSIX timestamp + offset redeemer_value = int(datetime.datetime.now().timestamp() * 1_000) + timestamp_offset_ms else: # BUG: https://github.com/input-output-hk/cardano-node/issues/3090 redeemer_value = 1_000_000_000_000 policyid = cluster.get_policyid( plutus_common.MINTING_TIME_RANGE_PLUTUS_V1) asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode( "utf-8").hex() token = f"{policyid}.{asset_name}" mint_txouts = [ clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token) ] plutus_mint_data = [ clusterlib.Mint( txouts=mint_txouts, script_file=plutus_common.MINTING_TIME_RANGE_PLUTUS_V1, collaterals=collateral_utxos, redeemer_value=str(redeemer_value), ) ] tx_files_step2 = clusterlib.TxFiles( signing_key_files=[issuer_addr.skey_file], ) txouts_step2 = [ clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), *mint_txouts, ] tx_output_step2 = cluster.build_tx( src_address=payment_addr.address, tx_name=f"{temp_template}_step2", tx_files=tx_files_step2, txins=mint_utxos, txouts=txouts_step2, mint=plutus_mint_data, invalid_before=slot_step2 - slots_offset, invalid_hereafter=slot_step2 + slots_offset, ) plutus_cost = cluster.calculate_plutus_script_cost( src_address=payment_addr.address, tx_name=f"{temp_template}_step2", tx_files=tx_files_step2, txins=mint_utxos, txouts=txouts_step2, mint=plutus_mint_data, invalid_before=slot_step2 - slots_offset, invalid_hereafter=slot_step2 + slots_offset, ) tx_signed_step2 = cluster.sign_tx( tx_body_file=tx_output_step2.out_file, signing_key_files=tx_files_step2.signing_key_files, tx_name=f"{temp_template}_step2", ) cluster.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) assert ( cluster.get_address_balance( issuer_addr.address) == issuer_init_balance + minting_cost.collateral + lovelace_amount ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" token_utxo = cluster.get_utxo(address=issuer_addr.address, coins=[token]) assert token_utxo and token_utxo[ 0].amount == token_amount, "The token was not minted" # check expected fees expected_fee_step1 = 167_349 assert helpers.is_in_interval(tx_output_step1.fee, expected_fee_step1, frac=0.15) expected_fee_step2 = 411_175 assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) plutus_common.check_plutus_cost( plutus_cost=plutus_cost, expected_cost=[plutus_common.MINTING_TIME_RANGE_COST], ) # check tx_view tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_step2) dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_step1) dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_step2)
def test_minting_one_token(self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord]): """Test minting a token with a Plutus script. Uses `cardano-cli transaction build` command for building the transactions. * fund the token issuer and create a UTxO for collateral * check that the expected amount was transferred to token issuer's address * mint the token using a Plutus script * check that the token was minted and collateral UTxO was not spent * check expected fees * check expected Plutus cost * (optional) check transactions in db-sync """ # pylint: disable=too-many-locals temp_template = common.get_test_id(cluster) payment_addr = payment_addrs[0] issuer_addr = payment_addrs[1] lovelace_amount = 2_000_000 token_amount = 5 script_fund = 200_000_000 minting_cost = plutus_common.compute_cost( execution_cost=plutus_common.MINTING_COST, protocol_params=cluster.get_protocol_params()) issuer_init_balance = cluster.get_address_balance(issuer_addr.address) # Step 1: fund the token issuer and create UTXO for collaterals mint_utxos, collateral_utxos, tx_output_step1 = _fund_issuer( cluster_obj=cluster, temp_template=temp_template, payment_addr=payment_addr, issuer_addr=issuer_addr, minting_cost=minting_cost, amount=script_fund, collateral_utxo_num=2, ) # Step 2: mint the "qacoin" policyid = cluster.get_policyid(plutus_common.MINTING_PLUTUS_V1) asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode( "utf-8").hex() token = f"{policyid}.{asset_name}" mint_txouts = [ clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token) ] plutus_mint_data = [ clusterlib.Mint( txouts=mint_txouts, script_file=plutus_common.MINTING_PLUTUS_V1, collaterals=collateral_utxos, redeemer_file=plutus_common.REDEEMER_42, ) ] tx_files_step2 = clusterlib.TxFiles( signing_key_files=[issuer_addr.skey_file], ) txouts_step2 = [ clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), *mint_txouts, ] tx_output_step2 = cluster.build_tx( src_address=payment_addr.address, tx_name=f"{temp_template}_step2", tx_files=tx_files_step2, txins=mint_utxos, txouts=txouts_step2, mint=plutus_mint_data, ) plutus_cost = cluster.calculate_plutus_script_cost( src_address=payment_addr.address, tx_name=f"{temp_template}_step2", tx_files=tx_files_step2, txins=mint_utxos, txouts=txouts_step2, mint=plutus_mint_data, ) tx_signed_step2 = cluster.sign_tx( tx_body_file=tx_output_step2.out_file, signing_key_files=tx_files_step2.signing_key_files, tx_name=f"{temp_template}_step2", ) cluster.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) assert ( cluster.get_address_balance( issuer_addr.address) == issuer_init_balance + minting_cost.collateral + lovelace_amount ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" token_utxo = cluster.get_utxo(address=issuer_addr.address, coins=[token]) assert token_utxo and token_utxo[ 0].amount == token_amount, "The token was not minted" # check expected fees expected_fee_step1 = 168_977 assert helpers.is_in_interval(tx_output_step1.fee, expected_fee_step1, frac=0.15) expected_fee_step2 = 371_111 assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) plutus_common.check_plutus_cost( plutus_cost=plutus_cost, expected_cost=[plutus_common.MINTING_COST], ) # check tx_view tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_step2) dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_step1) dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_step2)