diff --git a/cardano_node_tests/tests/test_plutus/__init__.py b/cardano_node_tests/tests/test_plutus/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cardano_node_tests/tests/test_plutus_mint_build.py b/cardano_node_tests/tests/test_plutus/mint_build.py similarity index 100% rename from cardano_node_tests/tests/test_plutus_mint_build.py rename to cardano_node_tests/tests/test_plutus/mint_build.py diff --git a/cardano_node_tests/tests/test_plutus_mint_raw.py b/cardano_node_tests/tests/test_plutus/mint_raw.py similarity index 100% rename from cardano_node_tests/tests/test_plutus_mint_raw.py rename to cardano_node_tests/tests/test_plutus/mint_raw.py diff --git a/cardano_node_tests/tests/test_plutus_spend_build.py b/cardano_node_tests/tests/test_plutus/spend_build.py similarity index 100% rename from cardano_node_tests/tests/test_plutus_spend_build.py rename to cardano_node_tests/tests/test_plutus/spend_build.py diff --git a/cardano_node_tests/tests/test_plutus_spend_raw.py b/cardano_node_tests/tests/test_plutus/spend_raw.py similarity index 100% rename from cardano_node_tests/tests/test_plutus_spend_raw.py rename to cardano_node_tests/tests/test_plutus/spend_raw.py diff --git a/cardano_node_tests/tests/test_plutus_delegation.py b/cardano_node_tests/tests/test_plutus/test_delegation.py similarity index 100% rename from cardano_node_tests/tests/test_plutus_delegation.py rename to cardano_node_tests/tests/test_plutus/test_delegation.py diff --git a/cardano_node_tests/tests/test_plutus_lobster.py b/cardano_node_tests/tests/test_plutus/test_lobster.py similarity index 100% rename from cardano_node_tests/tests/test_plutus_lobster.py rename to cardano_node_tests/tests/test_plutus/test_lobster.py diff --git a/cardano_node_tests/tests/test_plutus/test_mint_build.py b/cardano_node_tests/tests/test_plutus/test_mint_build.py new file mode 100644 index 000000000..79a05057c --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_mint_build.py @@ -0,0 +1,1321 @@ +"""Tests for minting with Plutus using `transaction build`.""" +import datetime +import json +import logging +import shutil +from pathlib import Path +from typing import List +from typing import Tuple + +import allure +import pytest +from cardano_clusterlib import clusterlib +from cardano_clusterlib import clusterlib_helpers + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + common.SKIPIF_PLUTUS_UNUSABLE, + common.SKIPIF_BUILD_UNUSABLE, + pytest.mark.smoke, +] + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment address.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(2)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return addrs + + +def _fund_issuer( + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + payment_addr: clusterlib.AddressRecord, + issuer_addr: clusterlib.AddressRecord, + minting_cost: plutus_common.ScriptCost, + amount: int, + collateral_utxo_num: int = 1, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund the token issuer.""" + single_collateral_amount = minting_cost.collateral // collateral_utxo_num + collateral_amounts = [single_collateral_amount for __ in range(collateral_utxo_num - 1)] + collateral_subtotal = sum(collateral_amounts) + collateral_amounts.append(minting_cost.collateral - collateral_subtotal) + + issuer_init_balance = cluster_obj.g_query.get_address_balance(issuer_addr.address) + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut( + address=issuer_addr.address, + amount=amount, + ), + *[clusterlib.TxOut(address=issuer_addr.address, amount=a) for a in collateral_amounts], + ] + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files, + txouts=txouts, + fee_buffer=2_000_000, + # don't join 'change' and 'collateral' txouts, we need separate UTxOs + join_txouts=False, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + issuer_balance = cluster_obj.g_query.get_address_balance(issuer_addr.address) + assert ( + issuer_balance == issuer_init_balance + amount + minting_cost.collateral + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster_obj.g_query.get_utxo(tx_raw_output=tx_output) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset(utxos=out_utxos, txouts=tx_output.txouts) + + mint_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + + txid = out_utxos[0].utxo_hash + collateral_utxos = [ + clusterlib.UTXOData(utxo_hash=txid, utxo_ix=idx, amount=a, address=issuer_addr.address) + for idx, a in enumerate(collateral_amounts, start=utxo_ix_offset + 1) + ] + + return mint_utxos, collateral_utxos, tx_output + + +class TestBuildMinting: + """Tests for minting using Plutus smart contracts and `transaction build`.""" + + @pytest.fixture + def past_horizon_funds( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Create UTxOs for `test_ttl_horizon`.""" + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + script_fund = 200_000_000 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_WITNESS_REDEEMER_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + mint_utxos, collateral_utxos, tx_raw_output = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=script_fund, + ) + + retval = mint_utxos, collateral_utxos, tx_raw_output + fixture_cache.value = retval + + return retval + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_minting_one_token( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: 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 + * 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 = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + script_fund = 200_000_000 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + issuer_init_balance = cluster.g_query.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.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + 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.g_transaction.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_costs = cluster.g_transaction.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.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance + minting_cost.collateral + lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=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 = 350_000 + assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_v_record.execution_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) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + 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.g_query.get_protocol_params(), + ) + + issuer_init_balance = cluster.g_query.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.g_query.get_slot_no() + slots_offset = 200 + timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 + + protocol_version = cluster.g_query.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.g_transaction.get_policyid(plutus_common.MINTING_TIME_RANGE_PLUTUS_V1) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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.g_transaction.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_costs = cluster.g_transaction.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.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance + minting_cost.collateral + lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=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_costs( + plutus_costs=plutus_costs, + expected_costs=[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) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_minting( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """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 = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + script_fund = 500_000_000 + + script_file1_v1 = plutus_common.MINTING_PLUTUS_V1 + script_file1_v2 = plutus_common.MINTING_PLUTUS_V2 + + # this is higher than `plutus_common.MINTING*_COST`, because the script context has changed + # to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + minting_cost1_v1 = plutus_common.ExecutionCost( + per_time=408_545_501, per_space=1_126_016, fixed_cost=94_428 + ) + minting_cost2_v2 = plutus_common.ExecutionCost( + per_time=427_707_230, per_space=1_188_952, fixed_cost=99_441 + ) + else: + minting_cost1_v1 = plutus_common.ExecutionCost( + per_time=297_744_405, per_space=1_126_016, fixed_cost=86_439 + ) + minting_cost2_v2 = plutus_common.ExecutionCost( + per_time=312_830_204, per_space=1_188_952, fixed_cost=91_158 + ) + + minting_cost1_v2 = plutus_common.ExecutionCost( + per_time=185_595_199, per_space=595_446, fixed_cost=47_739 + ) + + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = minting_cost1_v1 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = minting_cost1_v2 + else: + raise AssertionError("Unknown test variant.") + + script_file2 = plutus_common.MINTING_TIME_RANGE_PLUTUS_V1 + + protocol_params = cluster.g_query.get_protocol_params() + minting_cost1 = plutus_common.compute_cost( + execution_cost=execution_cost1, protocol_params=protocol_params + ) + minting_cost2 = plutus_common.compute_cost( + execution_cost=minting_cost2_v2, protocol_params=protocol_params + ) + + # 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.g_transaction.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.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step1, txins=tx_output_step1.txins) + + out_utxos_step1 = cluster.g_query.get_utxo(tx_raw_output=tx_output_step1) + + issuer_utxos_step1 = clusterlib.filter_utxos( + utxos=out_utxos_step1, address=issuer_addr.address + ) + assert ( + clusterlib.calculate_utxos_balance(utxos=issuer_utxos_step1) + == script_fund + minting_cost1.collateral + minting_cost2.collateral + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + # Step 2: mint the "qacoins" + + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos_step1, txouts=tx_output_step1.txouts + ) + mint_utxos = clusterlib.filter_utxos(utxos=out_utxos_step1, utxo_ix=utxo_ix_offset) + collateral_utxo1 = clusterlib.filter_utxos( + utxos=out_utxos_step1, utxo_ix=utxo_ix_offset + 1 + ) + collateral_utxo2 = clusterlib.filter_utxos( + utxos=out_utxos_step1, utxo_ix=utxo_ix_offset + 2 + ) + + slot_step2 = cluster.g_query.get_slot_no() + + # "anyone can mint" qacoin + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + policyid1 = cluster.g_transaction.get_policyid(script_file1) + asset_name1 = f"qacoina{clusterlib.get_rand_str(4)}".encode().hex() + token1 = f"{policyid1}.{asset_name1}" + mint_txouts1 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token1) + ] + + # "time range" qacoin + slots_offset = 200 + timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 + + protocol_version = cluster.g_query.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 + + policyid2 = cluster.g_transaction.get_policyid(script_file2) + asset_name2 = f"qacoint{clusterlib.get_rand_str(4)}".encode().hex() + token2 = f"{policyid2}.{asset_name2}" + mint_txouts2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token2) + ] + + # mint the tokens + plutus_mint_data = [ + clusterlib.Mint( + txouts=mint_txouts1, + script_file=script_file1, + collaterals=collateral_utxo2, + redeemer_cbor_file=redeemer_cbor_file, + ), + clusterlib.Mint( + txouts=mint_txouts2, + script_file=script_file2, + collaterals=collateral_utxo1, + redeemer_value=str(redeemer_value_timerange), + ), + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + txouts_step2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts1, + *mint_txouts2, + ] + tx_output_step2 = cluster.g_transaction.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_costs = cluster.g_transaction.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.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + out_utxos_step2 = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + + issuer_utxos_step2 = clusterlib.filter_utxos( + utxos=out_utxos_step2, address=issuer_addr.address + ) + assert ( + clusterlib.calculate_utxos_balance(utxos=issuer_utxos_step2) == lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + token_utxo1 = clusterlib.filter_utxos( + utxos=out_utxos_step2, address=issuer_addr.address, coin=token1 + ) + assert ( + token_utxo1 and token_utxo1[0].amount == token_amount + ), "The 'anyone' token was not minted" + + token_utxo2 = clusterlib.filter_utxos( + utxos=out_utxos_step2, address=issuer_addr.address, coin=token2 + ) + assert ( + token_utxo2 and token_utxo2[0].amount == token_amount + ), "The 'timerange' 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_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost1, minting_cost2_v2], + ) + + # 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) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_minting_context_equivalence( + self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord] + ): + """Test context equivalence while minting a token. + + 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 + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * mint the token using the derived redeemer + * check that the token was minted and collateral UTxO was not spent + * 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 = 200_000_000 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_CONTEXT_EQUIVALENCE_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + issuer_init_balance = cluster.g_query.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" + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + policyid = cluster.g_transaction.get_policyid( + plutus_common.MINTING_CONTEXT_EQUIVALENCE_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().hex() + token = f"{policyid}.{asset_name}" + mint_txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token) + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file, plutus_common.SIGNING_KEY_GOLDEN], + ) + txouts_step2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts, + ] + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_mint_data_dummy = [ + clusterlib.Mint( + txouts=mint_txouts, + script_file=plutus_common.MINTING_CONTEXT_EQUIVALENCE_PLUTUS_V1, + collaterals=collateral_utxos, + redeemer_file=redeemer_file_dummy, + ) + ] + + tx_output_dummy = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_dummy", + tx_files=tx_files_step2, + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data_dummy, + required_signers=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + ) + assert tx_output_dummy + + tx_file_dummy = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_dummy.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_dummy", + ) + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_mint_data = [plutus_mint_data_dummy[0]._replace(redeemer_file=redeemer_file)] + + tx_output_step2 = cluster.g_transaction.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=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + # calculate cost of Plutus script + plutus_costs_step2 = cluster.g_transaction.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=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + tx_signed_step2 = cluster.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance + minting_cost.collateral + lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token + ) + assert token_utxo and token_utxo[0].amount == token_amount, "The token was not minted" + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs_step2, + expected_costs=[plutus_common.MINTING_CONTEXT_EQUIVALENCE_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) + tx_db_record_step2 = dbsync_utils.check_tx( + cluster_obj=cluster, tx_raw_output=tx_output_step2 + ) + # compare cost of Plutus script with data from db-sync + if tx_db_record_step2: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_db_record_step2.redeemers, cost_records=plutus_costs_step2 + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @pytest.mark.parametrize( + "key", + ( + "normal", + "extended", + ), + ) + 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 = f"{common.get_test_id(cluster)}_{key}" + + 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.g_query.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.g_query.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.g_transaction.get_policyid( + plutus_common.MINTING_WITNESS_REDEEMER_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_WITNESS_REDEEMER_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.g_transaction.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_costs = cluster.g_transaction.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.g_transaction.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.g_transaction.sign_tx( + tx_file=tx_signed_step2, + signing_key_files=[signing_key_golden], + tx_name=f"{temp_template}_step2_sign1", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2_inc, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance + minting_cost.collateral + lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=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_costs( + plutus_costs=plutus_costs, + expected_costs=[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) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era < VERSIONS.BABBAGE, + reason="runs only with Babbage+ TX", + ) + @pytest.mark.parametrize( + "ttl_offset", + (100, 1_000, 3_000, 10_000, 100_000, 1000_000, -1, -2), + ) + @common.PARAM_PLUTUS_VERSION + def test_ttl_horizon( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + past_horizon_funds: Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput + ], + plutus_version: str, + ttl_offset: int, + ): + """Test minting a token with ttl far in the future. + + Uses `cardano-cli transaction build` command for building the transactions. + + * try to mint a token using a Plutus script when ttl is set far in the future + * check that minting failed because of 'PastHorizon' failure when ttl is too far + in the future + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{ttl_offset}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + mint_utxos, collateral_utxos, __ = past_horizon_funds + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=collateral_utxos, + redeemer_file=plutus_common.REDEEMER_42, + ) + ] + + tx_files = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts, + ] + + # calculate 3k/f + offset_3kf = round( + 3 * cluster.genesis["securityParam"] / cluster.genesis["activeSlotsCoeff"] + ) + + # use 3k/f + `epoch_length` slots for ttl - this will not meet the `expect_pass` condition + if ttl_offset == -1: + ttl_offset = offset_3kf + cluster.epoch_length + # use 3k/f - 100 slots for ttl - this will meet the `expect_pass` condition + elif ttl_offset == -2: + ttl_offset = offset_3kf - 100 + + cluster.wait_for_new_block() + + last_slot_init = cluster.g_query.get_slot_no() + slot_no_3kf = last_slot_init + offset_3kf + invalid_hereafter = last_slot_init + ttl_offset + + ttl_epoch_info = clusterlib_helpers.get_epoch_for_slot( + cluster_obj=cluster, slot_no=invalid_hereafter + ) + + # the TTL will pass if it's in epoch 'e' and the slot of the latest applied block + 3k/f + # is greater than the first slot of 'e' + expect_pass = slot_no_3kf >= ttl_epoch_info.first_slot + + err = "" + try: + cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_mint", + tx_files=tx_files, + txins=mint_utxos, + txouts=txouts, + mint=plutus_mint_data, + invalid_hereafter=invalid_hereafter, + ) + except clusterlib.CLIError as exc: + err = str(exc) + + last_slot_diff = cluster.g_query.get_slot_no() - last_slot_init + expect_pass_finish = slot_no_3kf + last_slot_diff >= ttl_epoch_info.first_slot + if expect_pass != expect_pass_finish: + # we have hit a boundary, and it is hard to say if the test should have passed or not + assert not err or "TimeTranslationPastHorizon" in err, err + pytest.skip("Boundary hit, skipping") + return + + if err: + assert not expect_pass, f"Valid TTL (offset {ttl_offset} slots) was rejected" + assert "TimeTranslationPastHorizon" in err, err + else: + assert ( + expect_pass + ), f"TTL too far in the future (offset {ttl_offset} slots) was accepted" + + +class TestBuildMintingNegative: + """Tests for minting with Plutus using `transaction build` that are expected to fail.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + def test_witness_redeemer_missing_signer( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test minting a token with a Plutus script with invalid signers. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint the token using a Plutus script and a TX with signing key missing for + the required signer + * check that the minting failed because the required signers were not provided + """ + # 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.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _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.g_transaction.get_policyid( + plutus_common.MINTING_WITNESS_REDEEMER_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_WITNESS_REDEEMER_PLUTUS_V1, + collaterals=collateral_utxos, + redeemer_file=plutus_common.DATUM_WITNESS_GOLDEN_NORMAL, + ) + ] + + 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.g_transaction.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=[plutus_common.SIGNING_KEY_GOLDEN], + ) + tx_signed_step2 = cluster.g_transaction.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", + ) + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "MissingRequiredSigners" in str(excinfo.value) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + def test_redeemer_with_simple_minting_script( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test minting a token passing a redeemer for a simple minting script. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * try to mint the token using a simple script passing a redeemer + * check that the minting failed because a Plutus script is expected + """ + 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.g_query.get_protocol_params(), + ) + + # Fund the token issuer and create UTXO for collaterals + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=script_fund, + ) + + # Create simple script + keyhash = cluster.g_address.get_payment_vkey_hash(issuer_addr.vkey_file) + script_content = {"keyHash": keyhash, "type": "sig"} + script = Path(f"{temp_template}.script") + + with open(script, "w", encoding="utf-8") as out_json: + json.dump(script_content, out_json) + + # Mint the "qacoin" + policyid = cluster.g_transaction.get_policyid(script) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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=script, + collaterals=collateral_utxos, + redeemer_file=plutus_common.REDEEMER_42, + ) + ] + + tx_files = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts, + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=mint_utxos, + txouts=txouts, + mint=plutus_mint_data, + ) + + err_str = str(excinfo.value) + assert ( + "expected a script in the Plutus script language, but it is actually " + "using SimpleScriptLanguage SimpleScriptV1" in err_str + ), err_str diff --git a/cardano_node_tests/tests/test_plutus/test_mint_negative_build.py b/cardano_node_tests/tests/test_plutus/test_mint_negative_build.py new file mode 100644 index 000000000..79a05057c --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_mint_negative_build.py @@ -0,0 +1,1321 @@ +"""Tests for minting with Plutus using `transaction build`.""" +import datetime +import json +import logging +import shutil +from pathlib import Path +from typing import List +from typing import Tuple + +import allure +import pytest +from cardano_clusterlib import clusterlib +from cardano_clusterlib import clusterlib_helpers + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + common.SKIPIF_PLUTUS_UNUSABLE, + common.SKIPIF_BUILD_UNUSABLE, + pytest.mark.smoke, +] + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment address.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(2)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return addrs + + +def _fund_issuer( + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + payment_addr: clusterlib.AddressRecord, + issuer_addr: clusterlib.AddressRecord, + minting_cost: plutus_common.ScriptCost, + amount: int, + collateral_utxo_num: int = 1, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund the token issuer.""" + single_collateral_amount = minting_cost.collateral // collateral_utxo_num + collateral_amounts = [single_collateral_amount for __ in range(collateral_utxo_num - 1)] + collateral_subtotal = sum(collateral_amounts) + collateral_amounts.append(minting_cost.collateral - collateral_subtotal) + + issuer_init_balance = cluster_obj.g_query.get_address_balance(issuer_addr.address) + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut( + address=issuer_addr.address, + amount=amount, + ), + *[clusterlib.TxOut(address=issuer_addr.address, amount=a) for a in collateral_amounts], + ] + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files, + txouts=txouts, + fee_buffer=2_000_000, + # don't join 'change' and 'collateral' txouts, we need separate UTxOs + join_txouts=False, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + issuer_balance = cluster_obj.g_query.get_address_balance(issuer_addr.address) + assert ( + issuer_balance == issuer_init_balance + amount + minting_cost.collateral + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster_obj.g_query.get_utxo(tx_raw_output=tx_output) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset(utxos=out_utxos, txouts=tx_output.txouts) + + mint_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + + txid = out_utxos[0].utxo_hash + collateral_utxos = [ + clusterlib.UTXOData(utxo_hash=txid, utxo_ix=idx, amount=a, address=issuer_addr.address) + for idx, a in enumerate(collateral_amounts, start=utxo_ix_offset + 1) + ] + + return mint_utxos, collateral_utxos, tx_output + + +class TestBuildMinting: + """Tests for minting using Plutus smart contracts and `transaction build`.""" + + @pytest.fixture + def past_horizon_funds( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Create UTxOs for `test_ttl_horizon`.""" + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + script_fund = 200_000_000 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_WITNESS_REDEEMER_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + mint_utxos, collateral_utxos, tx_raw_output = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=script_fund, + ) + + retval = mint_utxos, collateral_utxos, tx_raw_output + fixture_cache.value = retval + + return retval + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_minting_one_token( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: 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 + * 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 = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + script_fund = 200_000_000 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + issuer_init_balance = cluster.g_query.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.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + 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.g_transaction.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_costs = cluster.g_transaction.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.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance + minting_cost.collateral + lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=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 = 350_000 + assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_v_record.execution_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) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + 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.g_query.get_protocol_params(), + ) + + issuer_init_balance = cluster.g_query.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.g_query.get_slot_no() + slots_offset = 200 + timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 + + protocol_version = cluster.g_query.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.g_transaction.get_policyid(plutus_common.MINTING_TIME_RANGE_PLUTUS_V1) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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.g_transaction.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_costs = cluster.g_transaction.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.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance + minting_cost.collateral + lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=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_costs( + plutus_costs=plutus_costs, + expected_costs=[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) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_minting( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """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 = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + script_fund = 500_000_000 + + script_file1_v1 = plutus_common.MINTING_PLUTUS_V1 + script_file1_v2 = plutus_common.MINTING_PLUTUS_V2 + + # this is higher than `plutus_common.MINTING*_COST`, because the script context has changed + # to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + minting_cost1_v1 = plutus_common.ExecutionCost( + per_time=408_545_501, per_space=1_126_016, fixed_cost=94_428 + ) + minting_cost2_v2 = plutus_common.ExecutionCost( + per_time=427_707_230, per_space=1_188_952, fixed_cost=99_441 + ) + else: + minting_cost1_v1 = plutus_common.ExecutionCost( + per_time=297_744_405, per_space=1_126_016, fixed_cost=86_439 + ) + minting_cost2_v2 = plutus_common.ExecutionCost( + per_time=312_830_204, per_space=1_188_952, fixed_cost=91_158 + ) + + minting_cost1_v2 = plutus_common.ExecutionCost( + per_time=185_595_199, per_space=595_446, fixed_cost=47_739 + ) + + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = minting_cost1_v1 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = minting_cost1_v2 + else: + raise AssertionError("Unknown test variant.") + + script_file2 = plutus_common.MINTING_TIME_RANGE_PLUTUS_V1 + + protocol_params = cluster.g_query.get_protocol_params() + minting_cost1 = plutus_common.compute_cost( + execution_cost=execution_cost1, protocol_params=protocol_params + ) + minting_cost2 = plutus_common.compute_cost( + execution_cost=minting_cost2_v2, protocol_params=protocol_params + ) + + # 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.g_transaction.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.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step1, txins=tx_output_step1.txins) + + out_utxos_step1 = cluster.g_query.get_utxo(tx_raw_output=tx_output_step1) + + issuer_utxos_step1 = clusterlib.filter_utxos( + utxos=out_utxos_step1, address=issuer_addr.address + ) + assert ( + clusterlib.calculate_utxos_balance(utxos=issuer_utxos_step1) + == script_fund + minting_cost1.collateral + minting_cost2.collateral + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + # Step 2: mint the "qacoins" + + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos_step1, txouts=tx_output_step1.txouts + ) + mint_utxos = clusterlib.filter_utxos(utxos=out_utxos_step1, utxo_ix=utxo_ix_offset) + collateral_utxo1 = clusterlib.filter_utxos( + utxos=out_utxos_step1, utxo_ix=utxo_ix_offset + 1 + ) + collateral_utxo2 = clusterlib.filter_utxos( + utxos=out_utxos_step1, utxo_ix=utxo_ix_offset + 2 + ) + + slot_step2 = cluster.g_query.get_slot_no() + + # "anyone can mint" qacoin + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + policyid1 = cluster.g_transaction.get_policyid(script_file1) + asset_name1 = f"qacoina{clusterlib.get_rand_str(4)}".encode().hex() + token1 = f"{policyid1}.{asset_name1}" + mint_txouts1 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token1) + ] + + # "time range" qacoin + slots_offset = 200 + timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 + + protocol_version = cluster.g_query.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 + + policyid2 = cluster.g_transaction.get_policyid(script_file2) + asset_name2 = f"qacoint{clusterlib.get_rand_str(4)}".encode().hex() + token2 = f"{policyid2}.{asset_name2}" + mint_txouts2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token2) + ] + + # mint the tokens + plutus_mint_data = [ + clusterlib.Mint( + txouts=mint_txouts1, + script_file=script_file1, + collaterals=collateral_utxo2, + redeemer_cbor_file=redeemer_cbor_file, + ), + clusterlib.Mint( + txouts=mint_txouts2, + script_file=script_file2, + collaterals=collateral_utxo1, + redeemer_value=str(redeemer_value_timerange), + ), + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + txouts_step2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts1, + *mint_txouts2, + ] + tx_output_step2 = cluster.g_transaction.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_costs = cluster.g_transaction.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.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + out_utxos_step2 = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + + issuer_utxos_step2 = clusterlib.filter_utxos( + utxos=out_utxos_step2, address=issuer_addr.address + ) + assert ( + clusterlib.calculate_utxos_balance(utxos=issuer_utxos_step2) == lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + token_utxo1 = clusterlib.filter_utxos( + utxos=out_utxos_step2, address=issuer_addr.address, coin=token1 + ) + assert ( + token_utxo1 and token_utxo1[0].amount == token_amount + ), "The 'anyone' token was not minted" + + token_utxo2 = clusterlib.filter_utxos( + utxos=out_utxos_step2, address=issuer_addr.address, coin=token2 + ) + assert ( + token_utxo2 and token_utxo2[0].amount == token_amount + ), "The 'timerange' 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_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost1, minting_cost2_v2], + ) + + # 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) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_minting_context_equivalence( + self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord] + ): + """Test context equivalence while minting a token. + + 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 + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * mint the token using the derived redeemer + * check that the token was minted and collateral UTxO was not spent + * 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 = 200_000_000 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_CONTEXT_EQUIVALENCE_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + issuer_init_balance = cluster.g_query.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" + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + policyid = cluster.g_transaction.get_policyid( + plutus_common.MINTING_CONTEXT_EQUIVALENCE_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().hex() + token = f"{policyid}.{asset_name}" + mint_txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token) + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file, plutus_common.SIGNING_KEY_GOLDEN], + ) + txouts_step2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts, + ] + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_mint_data_dummy = [ + clusterlib.Mint( + txouts=mint_txouts, + script_file=plutus_common.MINTING_CONTEXT_EQUIVALENCE_PLUTUS_V1, + collaterals=collateral_utxos, + redeemer_file=redeemer_file_dummy, + ) + ] + + tx_output_dummy = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_dummy", + tx_files=tx_files_step2, + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data_dummy, + required_signers=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + ) + assert tx_output_dummy + + tx_file_dummy = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_dummy.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_dummy", + ) + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_mint_data = [plutus_mint_data_dummy[0]._replace(redeemer_file=redeemer_file)] + + tx_output_step2 = cluster.g_transaction.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=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + # calculate cost of Plutus script + plutus_costs_step2 = cluster.g_transaction.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=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + tx_signed_step2 = cluster.g_transaction.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.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance + minting_cost.collateral + lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token + ) + assert token_utxo and token_utxo[0].amount == token_amount, "The token was not minted" + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs_step2, + expected_costs=[plutus_common.MINTING_CONTEXT_EQUIVALENCE_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) + tx_db_record_step2 = dbsync_utils.check_tx( + cluster_obj=cluster, tx_raw_output=tx_output_step2 + ) + # compare cost of Plutus script with data from db-sync + if tx_db_record_step2: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_db_record_step2.redeemers, cost_records=plutus_costs_step2 + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @pytest.mark.parametrize( + "key", + ( + "normal", + "extended", + ), + ) + 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 = f"{common.get_test_id(cluster)}_{key}" + + 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.g_query.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.g_query.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.g_transaction.get_policyid( + plutus_common.MINTING_WITNESS_REDEEMER_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_WITNESS_REDEEMER_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.g_transaction.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_costs = cluster.g_transaction.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.g_transaction.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.g_transaction.sign_tx( + tx_file=tx_signed_step2, + signing_key_files=[signing_key_golden], + tx_name=f"{temp_template}_step2_sign1", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2_inc, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance + minting_cost.collateral + lovelace_amount + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=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_costs( + plutus_costs=plutus_costs, + expected_costs=[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) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era < VERSIONS.BABBAGE, + reason="runs only with Babbage+ TX", + ) + @pytest.mark.parametrize( + "ttl_offset", + (100, 1_000, 3_000, 10_000, 100_000, 1000_000, -1, -2), + ) + @common.PARAM_PLUTUS_VERSION + def test_ttl_horizon( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + past_horizon_funds: Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput + ], + plutus_version: str, + ttl_offset: int, + ): + """Test minting a token with ttl far in the future. + + Uses `cardano-cli transaction build` command for building the transactions. + + * try to mint a token using a Plutus script when ttl is set far in the future + * check that minting failed because of 'PastHorizon' failure when ttl is too far + in the future + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{ttl_offset}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + mint_utxos, collateral_utxos, __ = past_horizon_funds + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=collateral_utxos, + redeemer_file=plutus_common.REDEEMER_42, + ) + ] + + tx_files = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts, + ] + + # calculate 3k/f + offset_3kf = round( + 3 * cluster.genesis["securityParam"] / cluster.genesis["activeSlotsCoeff"] + ) + + # use 3k/f + `epoch_length` slots for ttl - this will not meet the `expect_pass` condition + if ttl_offset == -1: + ttl_offset = offset_3kf + cluster.epoch_length + # use 3k/f - 100 slots for ttl - this will meet the `expect_pass` condition + elif ttl_offset == -2: + ttl_offset = offset_3kf - 100 + + cluster.wait_for_new_block() + + last_slot_init = cluster.g_query.get_slot_no() + slot_no_3kf = last_slot_init + offset_3kf + invalid_hereafter = last_slot_init + ttl_offset + + ttl_epoch_info = clusterlib_helpers.get_epoch_for_slot( + cluster_obj=cluster, slot_no=invalid_hereafter + ) + + # the TTL will pass if it's in epoch 'e' and the slot of the latest applied block + 3k/f + # is greater than the first slot of 'e' + expect_pass = slot_no_3kf >= ttl_epoch_info.first_slot + + err = "" + try: + cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_mint", + tx_files=tx_files, + txins=mint_utxos, + txouts=txouts, + mint=plutus_mint_data, + invalid_hereafter=invalid_hereafter, + ) + except clusterlib.CLIError as exc: + err = str(exc) + + last_slot_diff = cluster.g_query.get_slot_no() - last_slot_init + expect_pass_finish = slot_no_3kf + last_slot_diff >= ttl_epoch_info.first_slot + if expect_pass != expect_pass_finish: + # we have hit a boundary, and it is hard to say if the test should have passed or not + assert not err or "TimeTranslationPastHorizon" in err, err + pytest.skip("Boundary hit, skipping") + return + + if err: + assert not expect_pass, f"Valid TTL (offset {ttl_offset} slots) was rejected" + assert "TimeTranslationPastHorizon" in err, err + else: + assert ( + expect_pass + ), f"TTL too far in the future (offset {ttl_offset} slots) was accepted" + + +class TestBuildMintingNegative: + """Tests for minting with Plutus using `transaction build` that are expected to fail.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + def test_witness_redeemer_missing_signer( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test minting a token with a Plutus script with invalid signers. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint the token using a Plutus script and a TX with signing key missing for + the required signer + * check that the minting failed because the required signers were not provided + """ + # 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.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _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.g_transaction.get_policyid( + plutus_common.MINTING_WITNESS_REDEEMER_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_WITNESS_REDEEMER_PLUTUS_V1, + collaterals=collateral_utxos, + redeemer_file=plutus_common.DATUM_WITNESS_GOLDEN_NORMAL, + ) + ] + + 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.g_transaction.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=[plutus_common.SIGNING_KEY_GOLDEN], + ) + tx_signed_step2 = cluster.g_transaction.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", + ) + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "MissingRequiredSigners" in str(excinfo.value) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + def test_redeemer_with_simple_minting_script( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test minting a token passing a redeemer for a simple minting script. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * try to mint the token using a simple script passing a redeemer + * check that the minting failed because a Plutus script is expected + """ + 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.g_query.get_protocol_params(), + ) + + # Fund the token issuer and create UTXO for collaterals + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=script_fund, + ) + + # Create simple script + keyhash = cluster.g_address.get_payment_vkey_hash(issuer_addr.vkey_file) + script_content = {"keyHash": keyhash, "type": "sig"} + script = Path(f"{temp_template}.script") + + with open(script, "w", encoding="utf-8") as out_json: + json.dump(script_content, out_json) + + # Mint the "qacoin" + policyid = cluster.g_transaction.get_policyid(script) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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=script, + collaterals=collateral_utxos, + redeemer_file=plutus_common.REDEEMER_42, + ) + ] + + tx_files = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts, + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=mint_utxos, + txouts=txouts, + mint=plutus_mint_data, + ) + + err_str = str(excinfo.value) + assert ( + "expected a script in the Plutus script language, but it is actually " + "using SimpleScriptLanguage SimpleScriptV1" in err_str + ), err_str diff --git a/cardano_node_tests/tests/test_plutus/test_mint_negative_raw.py b/cardano_node_tests/tests/test_plutus/test_mint_negative_raw.py new file mode 100644 index 000000000..848560545 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_mint_negative_raw.py @@ -0,0 +1,2037 @@ +"""Tests for minting with Plutus using `transaction build-raw`.""" +import datetime +import logging +import shutil +from pathlib import Path +from typing import List +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import SubRequest +from cardano_clusterlib import clusterlib +from cardano_clusterlib import clusterlib_helpers + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + common.SKIPIF_PLUTUS_UNUSABLE, + pytest.mark.smoke, +] + + +# approx. fee for Tx size +FEE_MINT_TXSIZE = 400_000 + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment address.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(2)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return addrs + + +def _check_pretty_utxo( + cluster_obj: clusterlib.ClusterLib, tx_raw_output: clusterlib.TxRawOutput +) -> str: + """Check that pretty printed `query utxo` output looks as expected.""" + err = "" + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + utxo_out = ( + cluster_obj.cli( + [ + "query", + "utxo", + "--tx-in", + f"{txid}#0", + *cluster_obj.magic_args, + ] + ) + .stdout.decode("utf-8") + .split() + ) + + expected_out = [ + "TxHash", + "TxIx", + "Amount", + "--------------------------------------------------------------------------------------", + txid, + "0", + str(tx_raw_output.txouts[0].amount), + tx_raw_output.txouts[0].coin, + "+", + str(tx_raw_output.txouts[1].amount), + tx_raw_output.txouts[1].coin, + "+", + str(tx_raw_output.txouts[2].amount), + tx_raw_output.txouts[2].coin, + "+", + "TxOutDatumNone", + ] + + if utxo_out != expected_out: + err = f"Pretty UTxO output doesn't match expected output:\n{utxo_out}\nvs\n{expected_out}" + + return err + + +def _fund_issuer( + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + payment_addr: clusterlib.AddressRecord, + issuer_addr: clusterlib.AddressRecord, + minting_cost: plutus_common.ScriptCost, + amount: int, + fee_txsize: int = FEE_MINT_TXSIZE, + collateral_utxo_num: int = 1, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund the token issuer.""" + single_collateral_amount = minting_cost.collateral // collateral_utxo_num + collateral_amounts = [single_collateral_amount for __ in range(collateral_utxo_num - 1)] + collateral_subtotal = sum(collateral_amounts) + collateral_amounts.append(minting_cost.collateral - collateral_subtotal) + + issuer_init_balance = cluster_obj.g_query.get_address_balance(issuer_addr.address) + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut( + address=issuer_addr.address, + amount=amount + minting_cost.fee + fee_txsize, + ), + *[clusterlib.TxOut(address=issuer_addr.address, amount=a) for a in collateral_amounts], + ] + + tx_raw_output = cluster_obj.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + txouts=txouts, + tx_files=tx_files, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + # don't join 'change' and 'collateral' txouts, we need separate UTxOs + join_txouts=False, + ) + + issuer_balance = cluster_obj.g_query.get_address_balance(issuer_addr.address) + assert ( + issuer_balance + == issuer_init_balance + amount + minting_cost.fee + fee_txsize + minting_cost.collateral + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + mint_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#0") + collateral_utxos = [ + clusterlib.UTXOData(utxo_hash=txid, utxo_ix=idx, amount=a, address=issuer_addr.address) + for idx, a in enumerate(collateral_amounts, start=1) + ] + + return mint_utxos, collateral_utxos, tx_raw_output + + +class TestMinting: + """Tests for minting using Plutus smart contracts.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_minting_two_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting two tokens with a single Plutus script. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * mint the tokens using a Plutus script + * check that the tokens were minted and collateral UTxO was not spent + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + fee_txsize = 600_000 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + fee_txsize=fee_txsize, + collateral_utxo_num=2, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name_a = f"qacoina{clusterlib.get_rand_str(4)}".encode().hex() + token_a = f"{policyid}.{asset_name_a}" + asset_name_b = f"qacoinb{clusterlib.get_rand_str(4)}".encode().hex() + token_b = f"{policyid}.{asset_name_b}" + mint_txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_a), + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_b), + ] + + plutus_mint_data = [ + clusterlib.Mint( + txouts=mint_txouts, + script_file=plutus_v_record.script_file, + collaterals=collateral_utxos, + execution_units=( + plutus_v_record.execution_cost.per_time, + plutus_v_record.execution_cost.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ) + ] + + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + fee_txsize, + # ttl is optional in this test + invalid_hereafter=cluster.g_query.get_slot_no() + 200, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + + token_utxo_a = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_a + ) + assert ( + token_utxo_a and token_utxo_a[0].amount == token_amount + ), "The 'token a' was not minted" + + token_utxo_b = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_b + ) + assert ( + token_utxo_b and token_utxo_b[0].amount == token_amount + ), "The 'token b' was not minted" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + utxo_err = _check_pretty_utxo(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + if utxo_err: + pytest.fail(utxo_err) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @pytest.mark.parametrize( + "key", + ( + "normal", + "extended", + ), + ) + def test_witness_redeemer( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + key: str, + ): + """Test minting a token with a Plutus script. + + * 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 + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{key}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_WITNESS_REDEEMER_COST, + protocol_params=cluster.g_query.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 + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid( + plutus_common.MINTING_WITNESS_REDEEMER_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_WITNESS_REDEEMER_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_WITNESS_REDEEMER_COST.per_time, + plutus_common.MINTING_WITNESS_REDEEMER_COST.per_space, + ), + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + required_signers=[signing_key_golden], + ) + # sign incrementally (just to check that it works) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=[issuer_addr.skey_file], + tx_name=f"{temp_template}_step2_sign0", + ) + tx_signed_step2_inc = cluster.g_transaction.sign_tx( + tx_file=tx_signed_step2, + signing_key_files=[signing_key_golden], + tx_name=f"{temp_template}_step2_sign1", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2_inc, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token + ) + assert token_utxo and token_utxo[0].amount == token_amount, "The token was not minted" + + # check tx_view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_time_range_minting( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test minting a token with a time constraints Plutus script. + + * 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 + * (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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_TIME_RANGE_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + slot_step2 = cluster.g_query.get_slot_no() + slots_offset = 200 + timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 + + protocol_version = cluster.g_query.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.g_transaction.get_policyid(plutus_common.MINTING_TIME_RANGE_PLUTUS_V1) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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, + execution_units=( + plutus_common.MINTING_TIME_RANGE_COST.per_time, + plutus_common.MINTING_TIME_RANGE_COST.per_space, + ), + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + invalid_before=slot_step2 - slots_offset, + invalid_hereafter=slot_step2 + slots_offset, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token + ) + assert token_utxo and token_utxo[0].amount == token_amount, "The token was not minted" + + # check tx_view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_minting( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting two tokens with two different Plutus scripts. + + * 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 + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + script_file1_v1 = plutus_common.MINTING_PLUTUS_V1 + script_file1_v2 = plutus_common.MINTING_PLUTUS_V2 + + # this is higher than `plutus_common.MINTING*_COST`, because the script context has changed + # to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + minting_cost1_v1 = plutus_common.ExecutionCost( + per_time=408_545_501, per_space=1_126_016, fixed_cost=94_428 + ) + minting_cost2_v1 = plutus_common.ExecutionCost( + per_time=427_707_230, per_space=1_188_952, fixed_cost=99_441 + ) + else: + minting_cost1_v1 = plutus_common.ExecutionCost( + per_time=297_744_405, per_space=1_126_016, fixed_cost=86_439 + ) + minting_cost2_v1 = plutus_common.ExecutionCost( + per_time=312_830_204, per_space=1_188_952, fixed_cost=91_158 + ) + + minting_cost1_v2 = plutus_common.ExecutionCost( + per_time=185_595_199, per_space=595_446, fixed_cost=47_739 + ) + + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = minting_cost1_v1 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = minting_cost1_v2 + else: + raise AssertionError("Unknown test variant.") + + script_file2 = plutus_common.MINTING_TIME_RANGE_PLUTUS_V1 + + protocol_params = cluster.g_query.get_protocol_params() + minting_cost1 = plutus_common.compute_cost( + execution_cost=execution_cost1, protocol_params=protocol_params + ) + minting_cost2 = plutus_common.compute_cost( + execution_cost=minting_cost2_v1, protocol_params=protocol_params + ) + + fee_step2_total = minting_cost1.fee + minting_cost2.fee + FEE_MINT_TXSIZE + + issuer_init_balance = cluster.g_query.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=lovelace_amount + fee_step2_total), + # for collaterals + clusterlib.TxOut(address=issuer_addr.address, amount=minting_cost1.collateral), + clusterlib.TxOut(address=issuer_addr.address, amount=minting_cost2.collateral), + ] + + tx_raw_output_step1 = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + txouts=txouts_step1, + tx_files=tx_files_step1, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + # don't join 'change' and 'collateral' txouts, we need separate UTxOs + join_txouts=False, + ) + + issuer_step1_balance = cluster.g_query.get_address_balance(issuer_addr.address) + assert ( + issuer_step1_balance + == issuer_init_balance + + lovelace_amount + + fee_step2_total + + minting_cost1.collateral + + minting_cost2.collateral + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + # Step 2: mint the "qacoins" + + txid_step1 = cluster.g_transaction.get_txid(tx_body_file=tx_raw_output_step1.out_file) + mint_utxos = cluster.g_query.get_utxo(txin=f"{txid_step1}#0") + collateral_utxo1 = cluster.g_query.get_utxo(txin=f"{txid_step1}#1") + collateral_utxo2 = cluster.g_query.get_utxo(txin=f"{txid_step1}#2") + + slot_step2 = cluster.g_query.get_slot_no() + + # "anyone can mint" qacoin + policyid1 = cluster.g_transaction.get_policyid(script_file1) + asset_name1 = f"qacoina{clusterlib.get_rand_str(4)}".encode().hex() + token1 = f"{policyid1}.{asset_name1}" + mint_txouts1 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token1) + ] + + # "timerange" qacoin + slots_offset = 200 + timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 + + protocol_version = cluster.g_query.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 + + policyid2 = cluster.g_transaction.get_policyid(script_file2) + asset_name2 = f"qacoint{clusterlib.get_rand_str(4)}".encode().hex() + token2 = f"{policyid2}.{asset_name2}" + mint_txouts2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token2) + ] + + # mint the tokens + plutus_mint_data = [ + clusterlib.Mint( + txouts=mint_txouts1, + script_file=script_file1, + collaterals=collateral_utxo1, + execution_units=( + execution_cost1.per_time, + execution_cost1.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ), + clusterlib.Mint( + txouts=mint_txouts2, + script_file=script_file2, + collaterals=collateral_utxo2, + execution_units=( + minting_cost2_v1.per_time, + minting_cost2_v1.per_space, + ), + redeemer_value=str(redeemer_value_timerange), + ), + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + txouts_step2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts1, + *mint_txouts2, + ] + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=fee_step2_total, + invalid_before=slot_step2 - slots_offset, + invalid_hereafter=slot_step2 + slots_offset, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.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}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + + token_utxo1 = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token1 + ) + assert ( + token_utxo1 and token_utxo1[0].amount == token_amount + ), "The 'anyone' token was not minted" + + token_utxo2 = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token2 + ) + assert ( + token_utxo2 and token_utxo2[0].amount == token_amount + ), "The 'timerange' token was not minted" + + # check tx_view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + # check transactions in db-sync + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_minting_policy_executed_once1( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test that minting policy is executed only once even when the same policy is used twice. + + Test by minting two tokens while using the same Plutus script twice + with two different redeemers. + + The Plutus script used in this test takes the expected token name as + redeemer. Even though the redeemer used for minting the first token + doesn't match the token name, the token gets minted anyway. That's + because only the last redeemer is used and all the other scripts with + identical minting policy (and corresponding redeemers) are ignored. So + it only matters that the last redeemer matches the last token name. + + * fund the token issuer and create a UTxO for collateral - funds for fees and collateral + are sufficient for just single minting script + * check that the expected amount was transferred to token issuer's address + * mint the tokens using two identical Plutus scripts and two redeemers, where the first + redeemer value is invalid + * check that the tokens were minted and collateral UTxOs were not spent, i.e. the first + script and its redeemer were ignored + * check transaction view output + * (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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_TOKENNAME_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + issuer_init_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoins" + + policyid_tokenname = cluster.g_transaction.get_policyid( + plutus_common.MINTING_TOKENNAME_PLUTUS_V1 + ) + + # qacoinA + asset_name_a_dec = f"qacoinA{clusterlib.get_rand_str(4)}" + asset_name_a = asset_name_a_dec.encode("utf-8").hex() + token_a = f"{policyid_tokenname}.{asset_name_a}" + mint_txouts_a = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_a) + ] + + # qacoinB + asset_name_b_dec = f"qacoinB{clusterlib.get_rand_str(4)}" + asset_name_b = asset_name_b_dec.encode("utf-8").hex() + token_b = f"{policyid_tokenname}.{asset_name_b}" + mint_txouts_b = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_b) + ] + + # mint the tokens + plutus_mint_data = [ + # First redeemer and first script are ignored when there are + # multiple scripts for the same minting policy. Even though we + # specified execution units for the script, these will not be used. + # That's why we were able to use the costs for just single script, + # even when we passed it twice. + clusterlib.Mint( + txouts=mint_txouts_a, + script_file=plutus_common.MINTING_TOKENNAME_PLUTUS_V1, + # execution units are too low, but it doesn't matter as they get ignored anyway + execution_units=(1, 1), + redeemer_value='"ignored_value"', + ), + clusterlib.Mint( + txouts=mint_txouts_b, + script_file=plutus_common.MINTING_TOKENNAME_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_TOKENNAME_COST.per_time, + plutus_common.MINTING_TOKENNAME_COST.per_space, + ), + redeemer_value=f'"{asset_name_b_dec}"', + ), + ] + + 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_a, + *mint_txouts_b, + ] + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + + token_utxo_a = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_a + ) + assert ( + token_utxo_a and token_utxo_a[0].amount == token_amount + ), f"The '{asset_name_a_dec}' token was not minted" + + token_utxo_b = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_b + ) + assert ( + token_utxo_b and token_utxo_b[0].amount == token_amount + ), f"The '{asset_name_b_dec}' token was not minted" + + # check tx_view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + # check transactions in db-sync + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_minting_policy_executed_once2( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test that minting policy is executed only once even when the same policy is used twice. + + Test minting two tokens while using one Plutus script and one redeemer. + + The Plutus script used in this test takes the expected token name as + redeemer. Even though the redeemer doesn't match name of the first + token, the token get's minted anyway. That's because it is only checked + that the last token name matches the redeemer, and redeemer for the + first token is not needed. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * mint the tokens using a redeemer value that doesn't match the name of the first token + * check that the tokens were minted and collateral UTxOs were not spent, i.e. redeemer for + the first token was not needed + * check transaction view output + * (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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_TOKENNAME_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + collateral_utxo_num=2, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_common.MINTING_TOKENNAME_PLUTUS_V1) + + # qacoinA + asset_name_a_dec = f"qacoinA{clusterlib.get_rand_str(4)}" + asset_name_a = asset_name_a_dec.encode("utf-8").hex() + token_a = f"{policyid}.{asset_name_a}" + + # qacoinB + asset_name_b_dec = f"qacoinB{clusterlib.get_rand_str(4)}" + asset_name_b = asset_name_b_dec.encode("utf-8").hex() + token_b = f"{policyid}.{asset_name_b}" + + mint_txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_a), + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_b), + ] + + plutus_mint_data = [ + clusterlib.Mint( + txouts=mint_txouts, + script_file=plutus_common.MINTING_TOKENNAME_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_TOKENNAME_COST.per_time, + plutus_common.MINTING_TOKENNAME_COST.per_space, + ), + # both tokens will be minted even though the redeemer value + # matches the name of only the second one + redeemer_value=f'"{asset_name_b_dec}"', + ) + ] + + 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, + ] + + plutus_costs = cluster.g_transaction.calculate_plutus_script_cost( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_step2, + txouts=txouts_step2, + mint=plutus_mint_data, + ) + + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + + token_utxo_a = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_a + ) + assert ( + token_utxo_a and token_utxo_a[0].amount == token_amount + ), f"The '{asset_name_a_dec}' was not minted" + + token_utxo_b = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_b + ) + assert ( + token_utxo_b and token_utxo_b[0].amount == token_amount + ), f"The '{asset_name_b_dec}' was not minted" + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.MINTING_TOKENNAME_COST], + ) + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_minting_context_equivalence( + self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord] + ): + """Test context equivalence while minting a token. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * mint the token using the derived redeemer + * check that the token was minted and collateral UTxO was not spent + * (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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_CONTEXT_EQUIVALENCE_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + policyid = cluster.g_transaction.get_policyid( + plutus_common.MINTING_CONTEXT_EQUIVALENCE_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().hex() + token = f"{policyid}.{asset_name}" + mint_txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token) + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file, plutus_common.SIGNING_KEY_GOLDEN], + ) + txouts_step2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts, + ] + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_mint_data_dummy = [ + clusterlib.Mint( + txouts=mint_txouts, + script_file=plutus_common.MINTING_CONTEXT_EQUIVALENCE_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_CONTEXT_EQUIVALENCE_COST.per_time, + plutus_common.MINTING_CONTEXT_EQUIVALENCE_COST.per_space, + ), + redeemer_file=redeemer_file_dummy, + ) + ] + + tx_output_dummy = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_dummy_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data_dummy, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + required_signers=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + ) + assert tx_output_dummy + + tx_file_dummy = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_dummy.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_dummy", + ) + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_mint_data = [plutus_mint_data_dummy[0]._replace(redeemer_file=redeemer_file)] + + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + required_signers=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token + ) + assert token_utxo and token_utxo[0].amount == token_amount, "The token was not minted" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era < VERSIONS.BABBAGE, + reason="runs only with Babbage+ TX", + ) + @pytest.mark.parametrize( + "ttl_offset", + (100, 1_000, 3_000, 10_000, 100_000, 1000_000, -1, -2), + ) + @common.PARAM_PLUTUS_VERSION + def test_ttl_horizon( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ttl_offset: int, + plutus_version: str, + ): + """Test minting a token with ttl far in the future. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint a token using a Plutus script when ttl is set far in the future + * check that minting failed because of 'PastHorizon' failure when ttl is too far + in the future + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{ttl_offset}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + fee_txsize = 600_000 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + fee_txsize=fee_txsize, + ) + + # Step 2: try to mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=collateral_utxos, + execution_units=( + plutus_v_record.execution_cost.per_time, + plutus_v_record.execution_cost.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ) + ] + + 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, + ] + + # calculate 3k/f + offset_3kf = round( + 3 * cluster.genesis["securityParam"] / cluster.genesis["activeSlotsCoeff"] + ) + + # use 3k/f + `epoch_length` slots for ttl - this will not meet the `expect_pass` condition + if ttl_offset == -1: + ttl_offset = offset_3kf + cluster.epoch_length + # use 3k/f - 100 slots for ttl - this will meet the `expect_pass` condition + elif ttl_offset == -2: + ttl_offset = offset_3kf - 100 + + cluster.wait_for_new_block() + + last_slot_init = cluster.g_query.get_slot_no() + slot_no_3kf = last_slot_init + offset_3kf + invalid_hereafter = last_slot_init + ttl_offset + + ttl_epoch_info = clusterlib_helpers.get_epoch_for_slot( + cluster_obj=cluster, slot_no=invalid_hereafter + ) + + # the TTL will pass if it's in epoch 'e' and the slot of the latest applied block + 3k/f + # is greater than the first slot of 'e' + expect_pass = slot_no_3kf >= ttl_epoch_info.first_slot + + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + fee_txsize, + invalid_hereafter=invalid_hereafter, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + err = "" + try: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + except clusterlib.CLIError as exc: + err = str(exc) + + last_slot_diff = cluster.g_query.get_slot_no() - last_slot_init + expect_pass_finish = slot_no_3kf + last_slot_diff >= ttl_epoch_info.first_slot + if expect_pass != expect_pass_finish: + # we have hit a boundary and it is hard to say if the test should have passed or not + assert not err or "TimeTranslationPastHorizon" in err, err + pytest.skip("Boundary hit, skipping") + return + + if err: + assert not expect_pass, f"Valid TTL (offset {ttl_offset} slots) was rejected" + assert "TimeTranslationPastHorizon" in err, err + else: + assert ( + expect_pass + ), f"TTL too far in the future (offset {ttl_offset} slots) was accepted" + + +class TestMintingNegative: + """Tests for minting with Plutus using `transaction build-raw` that are expected to fail.""" + + @pytest.fixture + def pparams(self, cluster: clusterlib.ClusterLib) -> dict: + return cluster.g_query.get_protocol_params() + + @pytest.fixture + def fund_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + pparams: dict, + request: SubRequest, + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp]: + plutus_version = request.param + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + # for mypy + assert plutus_op.execution_cost + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=pparams, + ) + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addrs[0], + issuer_addr=payment_addrs[1], + minting_cost=minting_cost, + amount=2_000_000, + ) + return mint_utxos, collateral_utxos, plutus_op + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + def test_witness_redeemer_missing_signer( + self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord] + ): + """Test minting a token with a Plutus script with invalid signers. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint the token using a Plutus script and a TX with signing key missing for + the required signer + * check that the minting failed because the required signers were not provided + """ + # 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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_WITNESS_REDEEMER_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid( + plutus_common.MINTING_WITNESS_REDEEMER_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_WITNESS_REDEEMER_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_WITNESS_REDEEMER_COST.per_time, + plutus_common.MINTING_WITNESS_REDEEMER_COST.per_space, + ), + redeemer_file=plutus_common.DATUM_WITNESS_GOLDEN_NORMAL, + ) + ] + + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + required_signers=[plutus_common.SIGNING_KEY_GOLDEN], + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "MissingRequiredSigners" in str(excinfo.value) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_low_budget( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting a token when budget is too low. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint the token using a Plutus script when execution units are set to half + of the expected values + * check that the minting failed because the budget was overspent + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: try to mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=collateral_utxos, + # set execution units too low - to half of the expected values + execution_units=( + plutus_v_record.execution_cost.per_time // 2, + plutus_v_record.execution_cost.per_space // 2, + ), + redeemer_file=plutus_common.DATUM_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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + err_str = str(excinfo.value) + assert ( + "The budget was overspent" in err_str or "due to overspending the budget" in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_low_fee( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting a token when fee is set too low. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint a token using a Plutus script when fee is set lower than is the computed fee + * check that minting failed because the fee amount was too low + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: try to mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=collateral_utxos, + execution_units=( + plutus_v_record.execution_cost.per_time, + plutus_v_record.execution_cost.per_space, + ), + redeemer_file=plutus_common.DATUM_42, + ) + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + + fee_subtract = 300_000 + txouts_step2 = [ + # add subtracted fee to the transferred Lovelace amount so the Tx remains balanced + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount + fee_subtract), + *mint_txouts, + ] + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=FEE_MINT_TXSIZE + minting_cost.fee - fee_subtract, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "FeeTooSmallUTxO" in str(excinfo.value) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @hypothesis.given(data=st.data()) + @common.hypothesis_settings(100) + @pytest.mark.parametrize( + "fund_execution_units_above_limit", + ("v1", pytest.param("v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE)), + ids=("plutus_v1", "plutus_v2"), + indirect=True, + ) + def test_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_execution_units_above_limit: Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp + ], + pparams: dict, + data: st.DataObject, + request: FixtureRequest, + ): + """Test minting a token when execution units are above the limit. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * try to mint the token when execution units are set above the limits + * check that the minting failed because the execution units were too big + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, plutus_op = fund_execution_units_above_limit + + # Step 2: try to mint the "qacoin" + + per_time = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["steps"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_time > pparams["maxTxExecutionUnits"]["steps"] + + per_space = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["memory"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_space > pparams["maxTxExecutionUnits"]["memory"] + + fixed_cost = pparams["txFeeFixed"] + + high_execution_cost = plutus_common.ExecutionCost( + per_time=per_time, per_space=per_space, fixed_cost=fixed_cost + ) + + plutus_op = plutus_op._replace(execution_cost=high_execution_cost) + + minting_cost = plutus_common.compute_cost( + execution_cost=high_execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # for mypy + assert plutus_op.execution_cost + + policyid = cluster.g_transaction.get_policyid(plutus_op.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_op.script_file, + collaterals=collateral_utxos, + execution_units=( + plutus_op.execution_cost.per_time, + plutus_op.execution_cost.per_space, + ), + redeemer_file=plutus_common.DATUM_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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + err_str = str(excinfo.value) + assert "ExUnitsTooBigUTxO" in err_str, err_str + + +class TestNegativeCollateral: + """Tests for collaterals that are expected to fail.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_minting_with_invalid_collaterals( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting a token with a Plutus script with invalid collaterals. + + Expect failure. + + * fund the token issuer and create an UTxO for collateral with insufficient funds + * check that the expected amount was transferred to token issuer's address + * mint the token using a Plutus script + * check that the minting failed because no valid collateral was provided + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, *__ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: mint the "qacoin" + + invalid_collateral_utxo = clusterlib.UTXOData( + utxo_hash=mint_utxos[0].utxo_hash, + utxo_ix=10, + amount=minting_cost.collateral, + address=issuer_addr.address, + ) + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=[invalid_collateral_utxo], + execution_units=( + plutus_v_record.execution_cost.per_time, + plutus_v_record.execution_cost.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ) + ] + + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + # it should NOT be possible to mint with an invalid collateral + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "NoCollateralInputs" in str(excinfo.value) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_minting_with_insufficient_collateral( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting a token with a Plutus script with insufficient collateral. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral with insufficient funds + * check that the expected amount was transferred to token issuer's address + * mint the token using a Plutus script + * check that the minting failed because a collateral with insufficient funds was provided + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + collateral_amount = 2_000_000 + token_amount = 5 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + # increase fixed cost so the required collateral is higher than minimum collateral of 2 ADA + execution_cost = plutus_v_record.execution_cost._replace(fixed_cost=2_000_000) + + minting_cost = plutus_common.compute_cost( + execution_cost=execution_cost, protocol_params=cluster.g_query.get_protocol_params() + ) + + # Step 1: fund the token issuer + + mint_utxos, *__ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: mint the "qacoin" + + invalid_collateral_utxo = clusterlib.UTXOData( + utxo_hash=mint_utxos[0].utxo_hash, + utxo_ix=1, + amount=collateral_amount, + address=issuer_addr.address, + ) + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=[invalid_collateral_utxo], + execution_units=( + execution_cost.per_time, + execution_cost.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ) + ] + + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + # it should NOT be possible to mint with a collateral with insufficient funds + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "InsufficientCollateral" in str(excinfo.value) diff --git a/cardano_node_tests/tests/test_plutus/test_mint_raw.py b/cardano_node_tests/tests/test_plutus/test_mint_raw.py new file mode 100644 index 000000000..848560545 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_mint_raw.py @@ -0,0 +1,2037 @@ +"""Tests for minting with Plutus using `transaction build-raw`.""" +import datetime +import logging +import shutil +from pathlib import Path +from typing import List +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import SubRequest +from cardano_clusterlib import clusterlib +from cardano_clusterlib import clusterlib_helpers + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + common.SKIPIF_PLUTUS_UNUSABLE, + pytest.mark.smoke, +] + + +# approx. fee for Tx size +FEE_MINT_TXSIZE = 400_000 + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment address.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(2)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return addrs + + +def _check_pretty_utxo( + cluster_obj: clusterlib.ClusterLib, tx_raw_output: clusterlib.TxRawOutput +) -> str: + """Check that pretty printed `query utxo` output looks as expected.""" + err = "" + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + utxo_out = ( + cluster_obj.cli( + [ + "query", + "utxo", + "--tx-in", + f"{txid}#0", + *cluster_obj.magic_args, + ] + ) + .stdout.decode("utf-8") + .split() + ) + + expected_out = [ + "TxHash", + "TxIx", + "Amount", + "--------------------------------------------------------------------------------------", + txid, + "0", + str(tx_raw_output.txouts[0].amount), + tx_raw_output.txouts[0].coin, + "+", + str(tx_raw_output.txouts[1].amount), + tx_raw_output.txouts[1].coin, + "+", + str(tx_raw_output.txouts[2].amount), + tx_raw_output.txouts[2].coin, + "+", + "TxOutDatumNone", + ] + + if utxo_out != expected_out: + err = f"Pretty UTxO output doesn't match expected output:\n{utxo_out}\nvs\n{expected_out}" + + return err + + +def _fund_issuer( + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + payment_addr: clusterlib.AddressRecord, + issuer_addr: clusterlib.AddressRecord, + minting_cost: plutus_common.ScriptCost, + amount: int, + fee_txsize: int = FEE_MINT_TXSIZE, + collateral_utxo_num: int = 1, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund the token issuer.""" + single_collateral_amount = minting_cost.collateral // collateral_utxo_num + collateral_amounts = [single_collateral_amount for __ in range(collateral_utxo_num - 1)] + collateral_subtotal = sum(collateral_amounts) + collateral_amounts.append(minting_cost.collateral - collateral_subtotal) + + issuer_init_balance = cluster_obj.g_query.get_address_balance(issuer_addr.address) + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut( + address=issuer_addr.address, + amount=amount + minting_cost.fee + fee_txsize, + ), + *[clusterlib.TxOut(address=issuer_addr.address, amount=a) for a in collateral_amounts], + ] + + tx_raw_output = cluster_obj.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + txouts=txouts, + tx_files=tx_files, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + # don't join 'change' and 'collateral' txouts, we need separate UTxOs + join_txouts=False, + ) + + issuer_balance = cluster_obj.g_query.get_address_balance(issuer_addr.address) + assert ( + issuer_balance + == issuer_init_balance + amount + minting_cost.fee + fee_txsize + minting_cost.collateral + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + mint_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#0") + collateral_utxos = [ + clusterlib.UTXOData(utxo_hash=txid, utxo_ix=idx, amount=a, address=issuer_addr.address) + for idx, a in enumerate(collateral_amounts, start=1) + ] + + return mint_utxos, collateral_utxos, tx_raw_output + + +class TestMinting: + """Tests for minting using Plutus smart contracts.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_minting_two_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting two tokens with a single Plutus script. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * mint the tokens using a Plutus script + * check that the tokens were minted and collateral UTxO was not spent + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + fee_txsize = 600_000 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + fee_txsize=fee_txsize, + collateral_utxo_num=2, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name_a = f"qacoina{clusterlib.get_rand_str(4)}".encode().hex() + token_a = f"{policyid}.{asset_name_a}" + asset_name_b = f"qacoinb{clusterlib.get_rand_str(4)}".encode().hex() + token_b = f"{policyid}.{asset_name_b}" + mint_txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_a), + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_b), + ] + + plutus_mint_data = [ + clusterlib.Mint( + txouts=mint_txouts, + script_file=plutus_v_record.script_file, + collaterals=collateral_utxos, + execution_units=( + plutus_v_record.execution_cost.per_time, + plutus_v_record.execution_cost.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ) + ] + + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + fee_txsize, + # ttl is optional in this test + invalid_hereafter=cluster.g_query.get_slot_no() + 200, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + + token_utxo_a = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_a + ) + assert ( + token_utxo_a and token_utxo_a[0].amount == token_amount + ), "The 'token a' was not minted" + + token_utxo_b = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_b + ) + assert ( + token_utxo_b and token_utxo_b[0].amount == token_amount + ), "The 'token b' was not minted" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + utxo_err = _check_pretty_utxo(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + if utxo_err: + pytest.fail(utxo_err) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @pytest.mark.parametrize( + "key", + ( + "normal", + "extended", + ), + ) + def test_witness_redeemer( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + key: str, + ): + """Test minting a token with a Plutus script. + + * 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 + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{key}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_WITNESS_REDEEMER_COST, + protocol_params=cluster.g_query.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 + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid( + plutus_common.MINTING_WITNESS_REDEEMER_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_WITNESS_REDEEMER_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_WITNESS_REDEEMER_COST.per_time, + plutus_common.MINTING_WITNESS_REDEEMER_COST.per_space, + ), + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + required_signers=[signing_key_golden], + ) + # sign incrementally (just to check that it works) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=[issuer_addr.skey_file], + tx_name=f"{temp_template}_step2_sign0", + ) + tx_signed_step2_inc = cluster.g_transaction.sign_tx( + tx_file=tx_signed_step2, + signing_key_files=[signing_key_golden], + tx_name=f"{temp_template}_step2_sign1", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2_inc, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token + ) + assert token_utxo and token_utxo[0].amount == token_amount, "The token was not minted" + + # check tx_view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_time_range_minting( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test minting a token with a time constraints Plutus script. + + * 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 + * (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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_TIME_RANGE_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + slot_step2 = cluster.g_query.get_slot_no() + slots_offset = 200 + timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 + + protocol_version = cluster.g_query.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.g_transaction.get_policyid(plutus_common.MINTING_TIME_RANGE_PLUTUS_V1) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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, + execution_units=( + plutus_common.MINTING_TIME_RANGE_COST.per_time, + plutus_common.MINTING_TIME_RANGE_COST.per_space, + ), + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + invalid_before=slot_step2 - slots_offset, + invalid_hereafter=slot_step2 + slots_offset, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token + ) + assert token_utxo and token_utxo[0].amount == token_amount, "The token was not minted" + + # check tx_view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_minting( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting two tokens with two different Plutus scripts. + + * 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 + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + script_file1_v1 = plutus_common.MINTING_PLUTUS_V1 + script_file1_v2 = plutus_common.MINTING_PLUTUS_V2 + + # this is higher than `plutus_common.MINTING*_COST`, because the script context has changed + # to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + minting_cost1_v1 = plutus_common.ExecutionCost( + per_time=408_545_501, per_space=1_126_016, fixed_cost=94_428 + ) + minting_cost2_v1 = plutus_common.ExecutionCost( + per_time=427_707_230, per_space=1_188_952, fixed_cost=99_441 + ) + else: + minting_cost1_v1 = plutus_common.ExecutionCost( + per_time=297_744_405, per_space=1_126_016, fixed_cost=86_439 + ) + minting_cost2_v1 = plutus_common.ExecutionCost( + per_time=312_830_204, per_space=1_188_952, fixed_cost=91_158 + ) + + minting_cost1_v2 = plutus_common.ExecutionCost( + per_time=185_595_199, per_space=595_446, fixed_cost=47_739 + ) + + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = minting_cost1_v1 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = minting_cost1_v2 + else: + raise AssertionError("Unknown test variant.") + + script_file2 = plutus_common.MINTING_TIME_RANGE_PLUTUS_V1 + + protocol_params = cluster.g_query.get_protocol_params() + minting_cost1 = plutus_common.compute_cost( + execution_cost=execution_cost1, protocol_params=protocol_params + ) + minting_cost2 = plutus_common.compute_cost( + execution_cost=minting_cost2_v1, protocol_params=protocol_params + ) + + fee_step2_total = minting_cost1.fee + minting_cost2.fee + FEE_MINT_TXSIZE + + issuer_init_balance = cluster.g_query.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=lovelace_amount + fee_step2_total), + # for collaterals + clusterlib.TxOut(address=issuer_addr.address, amount=minting_cost1.collateral), + clusterlib.TxOut(address=issuer_addr.address, amount=minting_cost2.collateral), + ] + + tx_raw_output_step1 = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + txouts=txouts_step1, + tx_files=tx_files_step1, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + # don't join 'change' and 'collateral' txouts, we need separate UTxOs + join_txouts=False, + ) + + issuer_step1_balance = cluster.g_query.get_address_balance(issuer_addr.address) + assert ( + issuer_step1_balance + == issuer_init_balance + + lovelace_amount + + fee_step2_total + + minting_cost1.collateral + + minting_cost2.collateral + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + # Step 2: mint the "qacoins" + + txid_step1 = cluster.g_transaction.get_txid(tx_body_file=tx_raw_output_step1.out_file) + mint_utxos = cluster.g_query.get_utxo(txin=f"{txid_step1}#0") + collateral_utxo1 = cluster.g_query.get_utxo(txin=f"{txid_step1}#1") + collateral_utxo2 = cluster.g_query.get_utxo(txin=f"{txid_step1}#2") + + slot_step2 = cluster.g_query.get_slot_no() + + # "anyone can mint" qacoin + policyid1 = cluster.g_transaction.get_policyid(script_file1) + asset_name1 = f"qacoina{clusterlib.get_rand_str(4)}".encode().hex() + token1 = f"{policyid1}.{asset_name1}" + mint_txouts1 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token1) + ] + + # "timerange" qacoin + slots_offset = 200 + timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 + + protocol_version = cluster.g_query.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 + + policyid2 = cluster.g_transaction.get_policyid(script_file2) + asset_name2 = f"qacoint{clusterlib.get_rand_str(4)}".encode().hex() + token2 = f"{policyid2}.{asset_name2}" + mint_txouts2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token2) + ] + + # mint the tokens + plutus_mint_data = [ + clusterlib.Mint( + txouts=mint_txouts1, + script_file=script_file1, + collaterals=collateral_utxo1, + execution_units=( + execution_cost1.per_time, + execution_cost1.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ), + clusterlib.Mint( + txouts=mint_txouts2, + script_file=script_file2, + collaterals=collateral_utxo2, + execution_units=( + minting_cost2_v1.per_time, + minting_cost2_v1.per_space, + ), + redeemer_value=str(redeemer_value_timerange), + ), + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + txouts_step2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts1, + *mint_txouts2, + ] + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=fee_step2_total, + invalid_before=slot_step2 - slots_offset, + invalid_hereafter=slot_step2 + slots_offset, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.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}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + + token_utxo1 = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token1 + ) + assert ( + token_utxo1 and token_utxo1[0].amount == token_amount + ), "The 'anyone' token was not minted" + + token_utxo2 = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token2 + ) + assert ( + token_utxo2 and token_utxo2[0].amount == token_amount + ), "The 'timerange' token was not minted" + + # check tx_view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + # check transactions in db-sync + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_minting_policy_executed_once1( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test that minting policy is executed only once even when the same policy is used twice. + + Test by minting two tokens while using the same Plutus script twice + with two different redeemers. + + The Plutus script used in this test takes the expected token name as + redeemer. Even though the redeemer used for minting the first token + doesn't match the token name, the token gets minted anyway. That's + because only the last redeemer is used and all the other scripts with + identical minting policy (and corresponding redeemers) are ignored. So + it only matters that the last redeemer matches the last token name. + + * fund the token issuer and create a UTxO for collateral - funds for fees and collateral + are sufficient for just single minting script + * check that the expected amount was transferred to token issuer's address + * mint the tokens using two identical Plutus scripts and two redeemers, where the first + redeemer value is invalid + * check that the tokens were minted and collateral UTxOs were not spent, i.e. the first + script and its redeemer were ignored + * check transaction view output + * (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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_TOKENNAME_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + issuer_init_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoins" + + policyid_tokenname = cluster.g_transaction.get_policyid( + plutus_common.MINTING_TOKENNAME_PLUTUS_V1 + ) + + # qacoinA + asset_name_a_dec = f"qacoinA{clusterlib.get_rand_str(4)}" + asset_name_a = asset_name_a_dec.encode("utf-8").hex() + token_a = f"{policyid_tokenname}.{asset_name_a}" + mint_txouts_a = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_a) + ] + + # qacoinB + asset_name_b_dec = f"qacoinB{clusterlib.get_rand_str(4)}" + asset_name_b = asset_name_b_dec.encode("utf-8").hex() + token_b = f"{policyid_tokenname}.{asset_name_b}" + mint_txouts_b = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_b) + ] + + # mint the tokens + plutus_mint_data = [ + # First redeemer and first script are ignored when there are + # multiple scripts for the same minting policy. Even though we + # specified execution units for the script, these will not be used. + # That's why we were able to use the costs for just single script, + # even when we passed it twice. + clusterlib.Mint( + txouts=mint_txouts_a, + script_file=plutus_common.MINTING_TOKENNAME_PLUTUS_V1, + # execution units are too low, but it doesn't matter as they get ignored anyway + execution_units=(1, 1), + redeemer_value='"ignored_value"', + ), + clusterlib.Mint( + txouts=mint_txouts_b, + script_file=plutus_common.MINTING_TOKENNAME_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_TOKENNAME_COST.per_time, + plutus_common.MINTING_TOKENNAME_COST.per_space, + ), + redeemer_value=f'"{asset_name_b_dec}"', + ), + ] + + 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_a, + *mint_txouts_b, + ] + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_init_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + + token_utxo_a = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_a + ) + assert ( + token_utxo_a and token_utxo_a[0].amount == token_amount + ), f"The '{asset_name_a_dec}' token was not minted" + + token_utxo_b = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_b + ) + assert ( + token_utxo_b and token_utxo_b[0].amount == token_amount + ), f"The '{asset_name_b_dec}' token was not minted" + + # check tx_view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + # check transactions in db-sync + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_minting_policy_executed_once2( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test that minting policy is executed only once even when the same policy is used twice. + + Test minting two tokens while using one Plutus script and one redeemer. + + The Plutus script used in this test takes the expected token name as + redeemer. Even though the redeemer doesn't match name of the first + token, the token get's minted anyway. That's because it is only checked + that the last token name matches the redeemer, and redeemer for the + first token is not needed. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * mint the tokens using a redeemer value that doesn't match the name of the first token + * check that the tokens were minted and collateral UTxOs were not spent, i.e. redeemer for + the first token was not needed + * check transaction view output + * (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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_TOKENNAME_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + collateral_utxo_num=2, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_common.MINTING_TOKENNAME_PLUTUS_V1) + + # qacoinA + asset_name_a_dec = f"qacoinA{clusterlib.get_rand_str(4)}" + asset_name_a = asset_name_a_dec.encode("utf-8").hex() + token_a = f"{policyid}.{asset_name_a}" + + # qacoinB + asset_name_b_dec = f"qacoinB{clusterlib.get_rand_str(4)}" + asset_name_b = asset_name_b_dec.encode("utf-8").hex() + token_b = f"{policyid}.{asset_name_b}" + + mint_txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_a), + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token_b), + ] + + plutus_mint_data = [ + clusterlib.Mint( + txouts=mint_txouts, + script_file=plutus_common.MINTING_TOKENNAME_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_TOKENNAME_COST.per_time, + plutus_common.MINTING_TOKENNAME_COST.per_space, + ), + # both tokens will be minted even though the redeemer value + # matches the name of only the second one + redeemer_value=f'"{asset_name_b_dec}"', + ) + ] + + 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, + ] + + plutus_costs = cluster.g_transaction.calculate_plutus_script_cost( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_step2, + txouts=txouts_step2, + mint=plutus_mint_data, + ) + + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + + token_utxo_a = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_a + ) + assert ( + token_utxo_a and token_utxo_a[0].amount == token_amount + ), f"The '{asset_name_a_dec}' was not minted" + + token_utxo_b = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token_b + ) + assert ( + token_utxo_b and token_utxo_b[0].amount == token_amount + ), f"The '{asset_name_b_dec}' was not minted" + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.MINTING_TOKENNAME_COST], + ) + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + @pytest.mark.testnets + def test_minting_context_equivalence( + self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord] + ): + """Test context equivalence while minting a token. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * mint the token using the derived redeemer + * check that the token was minted and collateral UTxO was not spent + * (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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_CONTEXT_EQUIVALENCE_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, tx_raw_output_step1 = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + issuer_fund_balance = cluster.g_query.get_address_balance(issuer_addr.address) + + # Step 2: mint the "qacoin" + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + policyid = cluster.g_transaction.get_policyid( + plutus_common.MINTING_CONTEXT_EQUIVALENCE_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().hex() + token = f"{policyid}.{asset_name}" + mint_txouts = [ + clusterlib.TxOut(address=issuer_addr.address, amount=token_amount, coin=token) + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file, plutus_common.SIGNING_KEY_GOLDEN], + ) + txouts_step2 = [ + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount), + *mint_txouts, + ] + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_mint_data_dummy = [ + clusterlib.Mint( + txouts=mint_txouts, + script_file=plutus_common.MINTING_CONTEXT_EQUIVALENCE_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_CONTEXT_EQUIVALENCE_COST.per_time, + plutus_common.MINTING_CONTEXT_EQUIVALENCE_COST.per_space, + ), + redeemer_file=redeemer_file_dummy, + ) + ] + + tx_output_dummy = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_dummy_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data_dummy, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + required_signers=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + ) + assert tx_output_dummy + + tx_file_dummy = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_dummy.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_dummy", + ) + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_mint_data = [plutus_mint_data_dummy[0]._replace(redeemer_file=redeemer_file)] + + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + required_signers=[plutus_common.SIGNING_KEY_GOLDEN], + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + + assert ( + cluster.g_query.get_address_balance(issuer_addr.address) + == issuer_fund_balance - tx_raw_output_step2.fee + ), f"Incorrect balance for token issuer address `{issuer_addr.address}`" + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output_step2) + token_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=issuer_addr.address, coin=token + ) + assert token_utxo and token_utxo[0].amount == token_amount, "The token was not minted" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step1) + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output_step2) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era < VERSIONS.BABBAGE, + reason="runs only with Babbage+ TX", + ) + @pytest.mark.parametrize( + "ttl_offset", + (100, 1_000, 3_000, 10_000, 100_000, 1000_000, -1, -2), + ) + @common.PARAM_PLUTUS_VERSION + def test_ttl_horizon( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ttl_offset: int, + plutus_version: str, + ): + """Test minting a token with ttl far in the future. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint a token using a Plutus script when ttl is set far in the future + * check that minting failed because of 'PastHorizon' failure when ttl is too far + in the future + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{ttl_offset}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + fee_txsize = 600_000 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + fee_txsize=fee_txsize, + ) + + # Step 2: try to mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=collateral_utxos, + execution_units=( + plutus_v_record.execution_cost.per_time, + plutus_v_record.execution_cost.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ) + ] + + 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, + ] + + # calculate 3k/f + offset_3kf = round( + 3 * cluster.genesis["securityParam"] / cluster.genesis["activeSlotsCoeff"] + ) + + # use 3k/f + `epoch_length` slots for ttl - this will not meet the `expect_pass` condition + if ttl_offset == -1: + ttl_offset = offset_3kf + cluster.epoch_length + # use 3k/f - 100 slots for ttl - this will meet the `expect_pass` condition + elif ttl_offset == -2: + ttl_offset = offset_3kf - 100 + + cluster.wait_for_new_block() + + last_slot_init = cluster.g_query.get_slot_no() + slot_no_3kf = last_slot_init + offset_3kf + invalid_hereafter = last_slot_init + ttl_offset + + ttl_epoch_info = clusterlib_helpers.get_epoch_for_slot( + cluster_obj=cluster, slot_no=invalid_hereafter + ) + + # the TTL will pass if it's in epoch 'e' and the slot of the latest applied block + 3k/f + # is greater than the first slot of 'e' + expect_pass = slot_no_3kf >= ttl_epoch_info.first_slot + + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + fee_txsize, + invalid_hereafter=invalid_hereafter, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + err = "" + try: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + except clusterlib.CLIError as exc: + err = str(exc) + + last_slot_diff = cluster.g_query.get_slot_no() - last_slot_init + expect_pass_finish = slot_no_3kf + last_slot_diff >= ttl_epoch_info.first_slot + if expect_pass != expect_pass_finish: + # we have hit a boundary and it is hard to say if the test should have passed or not + assert not err or "TimeTranslationPastHorizon" in err, err + pytest.skip("Boundary hit, skipping") + return + + if err: + assert not expect_pass, f"Valid TTL (offset {ttl_offset} slots) was rejected" + assert "TimeTranslationPastHorizon" in err, err + else: + assert ( + expect_pass + ), f"TTL too far in the future (offset {ttl_offset} slots) was accepted" + + +class TestMintingNegative: + """Tests for minting with Plutus using `transaction build-raw` that are expected to fail.""" + + @pytest.fixture + def pparams(self, cluster: clusterlib.ClusterLib) -> dict: + return cluster.g_query.get_protocol_params() + + @pytest.fixture + def fund_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + pparams: dict, + request: SubRequest, + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp]: + plutus_version = request.param + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + # for mypy + assert plutus_op.execution_cost + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=pparams, + ) + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addrs[0], + issuer_addr=payment_addrs[1], + minting_cost=minting_cost, + amount=2_000_000, + ) + return mint_utxos, collateral_utxos, plutus_op + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + def test_witness_redeemer_missing_signer( + self, cluster: clusterlib.ClusterLib, payment_addrs: List[clusterlib.AddressRecord] + ): + """Test minting a token with a Plutus script with invalid signers. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint the token using a Plutus script and a TX with signing key missing for + the required signer + * check that the minting failed because the required signers were not provided + """ + # 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 + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_common.MINTING_WITNESS_REDEEMER_COST, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid( + plutus_common.MINTING_WITNESS_REDEEMER_PLUTUS_V1 + ) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_WITNESS_REDEEMER_PLUTUS_V1, + collaterals=collateral_utxos, + execution_units=( + plutus_common.MINTING_WITNESS_REDEEMER_COST.per_time, + plutus_common.MINTING_WITNESS_REDEEMER_COST.per_space, + ), + redeemer_file=plutus_common.DATUM_WITNESS_GOLDEN_NORMAL, + ) + ] + + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + required_signers=[plutus_common.SIGNING_KEY_GOLDEN], + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "MissingRequiredSigners" in str(excinfo.value) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_low_budget( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting a token when budget is too low. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint the token using a Plutus script when execution units are set to half + of the expected values + * check that the minting failed because the budget was overspent + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: try to mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=collateral_utxos, + # set execution units too low - to half of the expected values + execution_units=( + plutus_v_record.execution_cost.per_time // 2, + plutus_v_record.execution_cost.per_space // 2, + ), + redeemer_file=plutus_common.DATUM_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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + err_str = str(excinfo.value) + assert ( + "The budget was overspent" in err_str or "due to overspending the budget" in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_low_fee( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting a token when fee is set too low. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * check that the expected amount was transferred to token issuer's address + * try to mint a token using a Plutus script when fee is set lower than is the computed fee + * check that minting failed because the fee amount was too low + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, __ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: try to mint the "qacoin" + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=collateral_utxos, + execution_units=( + plutus_v_record.execution_cost.per_time, + plutus_v_record.execution_cost.per_space, + ), + redeemer_file=plutus_common.DATUM_42, + ) + ] + + tx_files_step2 = clusterlib.TxFiles( + signing_key_files=[issuer_addr.skey_file], + ) + + fee_subtract = 300_000 + txouts_step2 = [ + # add subtracted fee to the transferred Lovelace amount so the Tx remains balanced + clusterlib.TxOut(address=issuer_addr.address, amount=lovelace_amount + fee_subtract), + *mint_txouts, + ] + tx_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=FEE_MINT_TXSIZE + minting_cost.fee - fee_subtract, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "FeeTooSmallUTxO" in str(excinfo.value) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @hypothesis.given(data=st.data()) + @common.hypothesis_settings(100) + @pytest.mark.parametrize( + "fund_execution_units_above_limit", + ("v1", pytest.param("v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE)), + ids=("plutus_v1", "plutus_v2"), + indirect=True, + ) + def test_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_execution_units_above_limit: Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp + ], + pparams: dict, + data: st.DataObject, + request: FixtureRequest, + ): + """Test minting a token when execution units are above the limit. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral + * try to mint the token when execution units are set above the limits + * check that the minting failed because the execution units were too big + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + # Step 1: fund the token issuer + + mint_utxos, collateral_utxos, plutus_op = fund_execution_units_above_limit + + # Step 2: try to mint the "qacoin" + + per_time = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["steps"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_time > pparams["maxTxExecutionUnits"]["steps"] + + per_space = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["memory"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_space > pparams["maxTxExecutionUnits"]["memory"] + + fixed_cost = pparams["txFeeFixed"] + + high_execution_cost = plutus_common.ExecutionCost( + per_time=per_time, per_space=per_space, fixed_cost=fixed_cost + ) + + plutus_op = plutus_op._replace(execution_cost=high_execution_cost) + + minting_cost = plutus_common.compute_cost( + execution_cost=high_execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # for mypy + assert plutus_op.execution_cost + + policyid = cluster.g_transaction.get_policyid(plutus_op.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_op.script_file, + collaterals=collateral_utxos, + execution_units=( + plutus_op.execution_cost.per_time, + plutus_op.execution_cost.per_space, + ), + redeemer_file=plutus_common.DATUM_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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + err_str = str(excinfo.value) + assert "ExUnitsTooBigUTxO" in err_str, err_str + + +class TestNegativeCollateral: + """Tests for collaterals that are expected to fail.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_minting_with_invalid_collaterals( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting a token with a Plutus script with invalid collaterals. + + Expect failure. + + * fund the token issuer and create an UTxO for collateral with insufficient funds + * check that the expected amount was transferred to token issuer's address + * mint the token using a Plutus script + * check that the minting failed because no valid collateral was provided + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + token_amount = 5 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + minting_cost = plutus_common.compute_cost( + execution_cost=plutus_v_record.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + # Step 1: fund the token issuer + + mint_utxos, *__ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: mint the "qacoin" + + invalid_collateral_utxo = clusterlib.UTXOData( + utxo_hash=mint_utxos[0].utxo_hash, + utxo_ix=10, + amount=minting_cost.collateral, + address=issuer_addr.address, + ) + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=[invalid_collateral_utxo], + execution_units=( + plutus_v_record.execution_cost.per_time, + plutus_v_record.execution_cost.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ) + ] + + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + # it should NOT be possible to mint with an invalid collateral + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "NoCollateralInputs" in str(excinfo.value) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @common.PARAM_PLUTUS_VERSION + def test_minting_with_insufficient_collateral( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test minting a token with a Plutus script with insufficient collateral. + + Expect failure. + + * fund the token issuer and create a UTxO for collateral with insufficient funds + * check that the expected amount was transferred to token issuer's address + * mint the token using a Plutus script + * check that the minting failed because a collateral with insufficient funds was provided + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + payment_addr = payment_addrs[0] + issuer_addr = payment_addrs[1] + + lovelace_amount = 2_000_000 + collateral_amount = 2_000_000 + token_amount = 5 + + plutus_v_record = plutus_common.MINTING_PLUTUS[plutus_version] + + # increase fixed cost so the required collateral is higher than minimum collateral of 2 ADA + execution_cost = plutus_v_record.execution_cost._replace(fixed_cost=2_000_000) + + minting_cost = plutus_common.compute_cost( + execution_cost=execution_cost, protocol_params=cluster.g_query.get_protocol_params() + ) + + # Step 1: fund the token issuer + + mint_utxos, *__ = _fund_issuer( + cluster_obj=cluster, + temp_template=temp_template, + payment_addr=payment_addr, + issuer_addr=issuer_addr, + minting_cost=minting_cost, + amount=lovelace_amount, + ) + + # Step 2: mint the "qacoin" + + invalid_collateral_utxo = clusterlib.UTXOData( + utxo_hash=mint_utxos[0].utxo_hash, + utxo_ix=1, + amount=collateral_amount, + address=issuer_addr.address, + ) + + policyid = cluster.g_transaction.get_policyid(plutus_v_record.script_file) + asset_name = f"qacoin{clusterlib.get_rand_str(4)}".encode().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_v_record.script_file, + collaterals=[invalid_collateral_utxo], + execution_units=( + execution_cost.per_time, + execution_cost.per_space, + ), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + ) + ] + + 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_raw_output_step2 = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=mint_utxos, + txouts=txouts_step2, + mint=plutus_mint_data, + tx_files=tx_files_step2, + fee=minting_cost.fee + FEE_MINT_TXSIZE, + ) + tx_signed_step2 = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output_step2.out_file, + signing_key_files=tx_files_step2.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + # it should NOT be possible to mint with a collateral with insufficient funds + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx(tx_file=tx_signed_step2, txins=mint_utxos) + assert "InsufficientCollateral" in str(excinfo.value) diff --git a/cardano_node_tests/tests/test_plutus/test_spend_build.py b/cardano_node_tests/tests/test_plutus/test_spend_build.py new file mode 100644 index 000000000..798b2c522 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_spend_build.py @@ -0,0 +1,2957 @@ +"""Tests for spending with Plutus using `transaction build`.""" +import json +import logging +import shutil +from pathlib import Path +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from cardano_clusterlib import clusterlib +from cardano_clusterlib import txtools + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + common.SKIPIF_BUILD_UNUSABLE, + pytest.mark.smoke, +] + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment addresses.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(3)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=1_000_000_000, + ) + + return addrs + + +@pytest.fixture +def pool_users( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.PoolUser]: + """Create new pool users.""" + test_id = common.get_test_id(cluster) + created_users = clusterlib_utils.create_pool_users( + cluster_obj=cluster, + name_template=f"{test_id}_pool_users", + no_of_addr=2, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + created_users[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return created_users + + +def _build_fund_script( + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + plutus_op: plutus_common.PlutusOp, + tokens: Optional[List[plutus_common.Token]] = None, # tokens must already be in `payment_addr` + tokens_collateral: Optional[ + List[plutus_common.Token] + ] = None, # tokens must already be in `payment_addr` + embed_datum: bool = False, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund a Plutus script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + assert plutus_op.execution_cost # for mypy + + script_fund = 200_000_000 + + stokens = tokens or () + ctokens = tokens_collateral or () + + script_address = cluster_obj.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + ) + + # create a Tx output with a datum hash at the script address + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + + script_txout = plutus_common.txout_factory( + address=script_address, + amount=script_fund, + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + txouts = [ + script_txout, + # for collateral + clusterlib.TxOut(address=dst_addr.address, amount=redeem_cost.collateral), + ] + + for token in stokens: + txouts.append(script_txout._replace(amount=token.amount, coin=token.coin)) + + for token in ctokens: + txouts.append( + clusterlib.TxOut( + address=dst_addr.address, + amount=token.amount, + coin=token.coin, + ) + ) + + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files, + txouts=txouts, + fee_buffer=2_000_000, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster_obj.g_query.get_utxo(tx_raw_output=tx_output) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset(utxos=out_utxos, txouts=tx_output.txouts) + + script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + assert script_utxos, "No script UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos) == script_fund + ), f"Incorrect balance for script address `{script_address}`" + + collateral_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + assert collateral_utxos, "No collateral UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos) == redeem_cost.collateral + ), f"Incorrect balance for collateral address `{dst_addr.address}`" + + for token in stokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos, coin=token.coin) == token.amount + ), f"Incorrect token balance for script address `{script_address}`" + + for token in ctokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos, coin=token.coin) + == token.amount + ), f"Incorrect token balance for address `{dst_addr.address}`" + + if VERSIONS.transaction_era >= VERSIONS.ALONZO: + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_output) + + return script_utxos, collateral_utxos, tx_output + + +def _build_spend_locked_txin( # noqa: C901 + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + plutus_op: plutus_common.PlutusOp, + amount: int, + deposit_amount: int = 0, + txins: clusterlib.OptionalUTXOData = (), + tx_files: Optional[clusterlib.TxFiles] = None, + invalid_hereafter: Optional[int] = None, + invalid_before: Optional[int] = None, + tokens: Optional[List[plutus_common.Token]] = None, + expect_failure: bool = False, + script_valid: bool = True, + submit_tx: bool = True, +) -> Tuple[str, Optional[clusterlib.TxRawOutput], list]: + """Spend the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + # pylint: disable=too-many-arguments,too-many-locals + tx_files = tx_files or clusterlib.TxFiles() + spent_tokens = tokens or () + + # Change that was calculated manually will be returned to address of the first script. + # The remaining change that is automatically handled by the `build` command will be returned + # to `payment_addr`, because it would be inaccessible on script address without proper + # datum hash (datum hash is not provided for change that is handled by `build` command). + script_change_rec = script_utxos[0] + + # spend the "locked" UTxO + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + datum_value=plutus_op.datum_value if plutus_op.datum_value else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file if plutus_op.redeemer_cbor_file else "", + redeemer_value=plutus_op.redeemer_value if plutus_op.redeemer_value else "", + ) + ] + tx_files = tx_files._replace( + signing_key_files=list({*tx_files.signing_key_files, dst_addr.skey_file}), + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + + lovelace_change_needed = False + for token in spent_tokens: + txouts.append( + clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin) + ) + # append change + script_token_balance = clusterlib.calculate_utxos_balance( + utxos=script_utxos, coin=token.coin + ) + if script_token_balance > token.amount: + lovelace_change_needed = True + txouts.append( + clusterlib.TxOut( + address=script_change_rec.address, + amount=script_token_balance - token.amount, + coin=token.coin, + datum_hash=script_change_rec.datum_hash, + ) + ) + # add minimum (+ some) required Lovelace to change Tx output + if lovelace_change_needed: + txouts.append( + clusterlib.TxOut( + address=script_change_rec.address, + amount=4_000_000, + coin=clusterlib.DEFAULT_COIN, + datum_hash=script_change_rec.datum_hash, + ) + ) + + if expect_failure: + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=txins, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + ) + return str(excinfo.value), None, [] + + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=txins, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + deposit=deposit_amount, + script_valid=script_valid, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + if not submit_tx: + return "", tx_output, [] + + dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address) + + script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN] + + if not script_valid: + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=collateral_utxos) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) + == dst_init_balance - collateral_utxos[0].amount + ), f"Collateral was NOT spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return "", tx_output, [] + + # calculate cost of Plutus script + plutus_costs = cluster_obj.g_transaction.calculate_plutus_script_cost( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + deposit=deposit_amount, + script_valid=script_valid, + ) + + cluster_obj.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_output.script_txins if t.txins] + ) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + for token in spent_tokens: + script_utxos_token = [u for u in script_utxos if u.coin == token.coin] + for u in script_utxos_token: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[token.coin] + ), f"Token inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster_obj, tx_raw_output=tx_output) + + tx_db_record = dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_output) + # compare cost of Plutus script with data from db-sync + if tx_db_record: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_db_record.redeemers, cost_records=plutus_costs + ) + + return "", tx_output, plutus_costs + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestBuildLocking: + """Tests for Tx output locking using Plutus smart contracts and `transaction build`.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Corresponds to Exercise 3 for Alonzo Blue. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + __, tx_output, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 170_782 + assert tx_output and helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + def test_context_equivalence( + self, + cluster: clusterlib.ClusterLib, + pool_users: List[clusterlib.PoolUser], + ): + """Test context equivalence while spending a locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * spend the locked UTxO using the derived redeemer + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 10_000_000 + deposit_amount = cluster.g_query.get_address_deposit() + + # create stake address registration cert + stake_addr_reg_cert_file = cluster.g_stake_address.gen_stake_addr_registration_cert( + addr_name=f"{temp_template}_addr2", + stake_vkey_file=pool_users[0].stake.vkey_file, + ) + + tx_files = clusterlib.TxFiles(certificate_files=[stake_addr_reg_cert_file]) + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_op_dummy = plutus_common.PlutusOp( + script_file=plutus_common.CONTEXT_EQUIVALENCE_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=redeemer_file_dummy, + execution_cost=plutus_common.CONTEXT_EQUIVALENCE_COST, + ) + + # fund the script address + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + plutus_op=plutus_op_dummy, + ) + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + __, tx_output_dummy, __ = _build_spend_locked_txin( + temp_template=f"{temp_template}_dummy", + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_dummy, + amount=amount, + deposit_amount=deposit_amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + submit_tx=False, + ) + assert tx_output_dummy + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + tx_file_dummy = Path(f"{tx_output_dummy.out_file.with_suffix('')}.signed") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_op = plutus_op_dummy._replace(redeemer_file=redeemer_file) + + __, tx_output, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + deposit_amount=deposit_amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + # check expected fees + if tx_output: + expected_fee = 372_438 + assert helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("embed_datum", (True, False), ids=("embedded_datum", "datum")) + @pytest.mark.parametrize( + "variant", + ("typed_json", "typed_cbor", "untyped_value", "untyped_json", "untyped_cbor"), + ) + @common.PARAM_PLUTUS_VERSION + def test_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + embed_datum: bool, + request: FixtureRequest, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "guessing game" scripts that expect specific datum and redeemer value. + Test both typed and untyped redeemer and datum. + Test passing datum and redeemer to `cardano-cli` as value, json file and cbor file. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + + datum_file: Optional[Path] = None + datum_cbor_file: Optional[Path] = None + datum_value: Optional[str] = None + redeemer_file: Optional[Path] = None + redeemer_cbor_file: Optional[Path] = None + redeemer_value: Optional[str] = None + + if variant == "typed_json": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "typed_cbor": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_TYPED_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_TYPED_CBOR + elif variant == "untyped_value": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_value = "42" + redeemer_value = "42" + elif variant == "untyped_json": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_file = plutus_common.DATUM_42 + redeemer_file = plutus_common.REDEEMER_42 + elif variant == "untyped_cbor": # noqa: SIM106 + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + else: + raise AssertionError("Unknown test variant.") + + execution_cost = plutus_common.GUESSING_GAME[plutus_version].execution_cost + if script_file == plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file: + execution_cost = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost + + plutus_op = plutus_common.PlutusOp( + script_file=script_file, + datum_file=datum_file, + datum_cbor_file=datum_cbor_file, + datum_value=datum_value, + redeemer_file=redeemer_file, + redeemer_cbor_file=redeemer_cbor_file, + redeemer_value=redeemer_value, + execution_cost=execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + __, __, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v1_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("plutus_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + script_fund = 200_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + script_file1_v1 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V1 + execution_cost1_v1 = plutus_common.ALWAYS_SUCCEEDS_COST + script_file2_v1 = plutus_common.GUESSING_GAME_PLUTUS_V1 + # this is higher than `plutus_common.GUESSING_GAME_COST`, because the script + # context has changed to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=388_458_303, per_space=1_031_312, fixed_cost=87_515 + ) + else: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=280_668_068, per_space=1_031_312, fixed_cost=79_743 + ) + + script_file1_v2 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V2 + execution_cost1_v2 = plutus_common.ALWAYS_SUCCEEDS_V2_COST + script_file2_v2 = plutus_common.GUESSING_GAME_PLUTUS_V2 + execution_cost2_v2 = plutus_common.ExecutionCost( + per_time=208_314_784, + per_space=662_274, + fixed_cost=53_233, + ) + + expected_fee_fund = 174_389 + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + expected_fee_redeem = 378_768 + elif plutus_version == "mix_v1_v2": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + expected_fee_redeem = 321_739 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + expected_fee_redeem = 378_584 + elif plutus_version == "plutus_v2": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + expected_fee_redeem = 321_378 + else: + raise AssertionError("Unknown test variant.") + + plutus_op1 = plutus_common.PlutusOp( + script_file=script_file1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=execution_cost1, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=script_file2, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_TYPED_CBOR, + execution_cost=execution_cost2, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=script_fund, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=script_fund, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + tx_output_fund = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files_fund, + txouts=txouts_fund, + fee_buffer=2_000_000, + join_txouts=False, + ) + tx_signed_fund = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_fund.out_file, + signing_key_files=tx_files_fund.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + + cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) + + fund_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=fund_utxos, txouts=tx_output_fund.txouts + ) + script_utxos1 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset) + script_utxos2 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 1) + collateral_utxos1 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 2) + collateral_utxos2 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 3) + + assert script_utxos1 and script_utxos2, "No script UTxOs" + assert collateral_utxos1 and collateral_utxos2, "No collateral UTxOs" + + assert ( + script_utxos1[0].amount == script_fund + ), f"Incorrect balance for script address `{script_utxos1[0].address}`" + assert ( + script_utxos2[0].amount == script_fund + ), f"Incorrect balance for script address `{script_utxos2[0].address}`" + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + tx_output_redeem = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + # calculate cost of Plutus script + plutus_costs = cluster.g_transaction.calculate_plutus_script_cost( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + dst_init_balance = cluster.g_query.get_address_balance(payment_addrs[1].address) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed_redeem, + txins=[t.txins[0] for t in tx_output_redeem.script_txins if t.txins], + ) + + assert ( + cluster.g_query.get_address_balance(payment_addrs[1].address) + == dst_init_balance + amount * 2 + ), f"Incorrect balance for destination address `{payment_addrs[1].address}`" + + script_utxos_lovelace = [ + u for u in [*script_utxos1, *script_utxos2] if u.coin == clusterlib.DEFAULT_COIN + ] + for u in script_utxos_lovelace: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + # check expected fees + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + assert helpers.is_in_interval(tx_output_redeem.fee, expected_fee_redeem, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost1, execution_cost2], + ) + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + # check transactions in db-sync + tx_redeem_record = dbsync_utils.check_tx( + cluster_obj=cluster, tx_raw_output=tx_output_redeem + ) + if tx_redeem_record: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_redeem_record.redeemers, cost_records=plutus_costs + ) + + @allure.link(helpers.get_vcs_link()) + def test_always_fails( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the expected error was raised + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err, __, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + expect_failure=True, + ) + assert "The Plutus script evaluation failed" in err, err + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + def test_script_invalid( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test failing script together with the `--script-invalid` argument - collateral is taken. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was spent + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + # include any payment txin + txins = [ + r + for r in cluster.g_query.get_utxo( + address=payment_addrs[0].address, coins=[clusterlib.DEFAULT_COIN] + ) + if not (r.datum_hash or r.inline_datum_hash) + ][:1] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + try: + __, tx_output, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + txins=txins, + tx_files=tx_files, + script_valid=False, + ) + except clusterlib.CLIError as err: + # TODO: broken on node 1.35.0 and 1.35.1 + if "ScriptWitnessIndexTxIn 0 is missing from the execution units" in str(err): + pytest.xfail("See cardano-node issue #4013") + else: + raise + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + if tx_output: + expected_fee = 171_309 + assert helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_token_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO + * check that the expected amounts of Lovelace and native tokens were spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=100, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens=tokens_rec, + ) + + __, tx_output_spend, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + tokens=tokens_rec, + ) + + # check expected fees + expected_fee_fund = 173_597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 175_710 + assert tx_output_spend and helpers.is_in_interval( + tx_output_spend.fee, expected_fee, frac=0.15 + ) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_partial_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending part of funds (Lovelace and native tokens) on a locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO and create new locked UTxO with change + * check that the expected amounts of Lovelace and native tokens were spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + token_rand = clusterlib.get_rand_str(5) + + amount_spend = 10_000_000 + token_amount_fund = 100 + token_amount_spend = 20 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount_fund, + ) + tokens_fund_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens=tokens_fund_rec, + ) + + tokens_spend_rec = [ + plutus_common.Token(coin=t.token, amount=token_amount_spend) for t in tokens + ] + + __, tx_output_spend, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_spend, + tokens=tokens_spend_rec, + ) + + # check that the expected amounts of Lovelace and native tokens were spent and change UTxOs + # with appropriate datum hash were created + + assert tx_output_spend + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_spend) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_spend.txouts + ) + + # UTxO we created for tokens and minimum required Lovelace + change_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + # UTxO that was created by `build` command for rest of the Lovelace change (this will not + # have the script's datum) + # TODO: change UTxO used to be first, now it's last + build_change_utxo = out_utxos[0] if utxo_ix_offset else out_utxos[-1] + + # Lovelace balance on original script UTxOs + script_lovelace_balance = clusterlib.calculate_utxos_balance(utxos=script_utxos) + # Lovelace balance on change UTxOs + change_lovelace_balance = clusterlib.calculate_utxos_balance( + utxos=[*change_utxos, build_change_utxo] + ) + + assert ( + change_lovelace_balance == script_lovelace_balance - tx_output_spend.fee - amount_spend + ) + + token_amount_exp = token_amount_fund - token_amount_spend + assert len(change_utxos) == len(tokens_spend_rec) + 1 + for u in change_utxos: + if u.coin != clusterlib.DEFAULT_COIN: + assert u.amount == token_amount_exp + assert u.datum_hash == script_utxos[0].datum_hash + + # check expected fees + expected_fee_fund = 173_597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 183_366 + assert tx_output_spend and helpers.is_in_interval( + tx_output_spend.fee, expected_fee, frac=0.15 + ) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_is_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using single UTxO for both collateral and Tx input. + + Uses `cardano-cli transaction build` command for building the transactions. + + Tests bug https://github.com/input-output-hk/cardano-db-sync/issues/750 + + * create a Tx output with a datum hash at the script address and a collateral UTxO + * check that the expected amount was locked at the script address + * spend the locked UTxO while using the collateral UTxO both as collateral and as + normal Tx input + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + # Step 1: fund the script address + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_step1 = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + ) + + # Step 2: spend the "locked" UTxO + + script_address = script_utxos[0].address + + dst_step1_balance = cluster.g_query.get_address_balance(dst_addr.address) + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file + if plutus_op.redeemer_cbor_file + else "", + ) + ] + tx_files = clusterlib.TxFiles( + signing_key_files=[dst_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + + tx_output_step2 = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + # `collateral_utxos` is used both as collateral and as normal Tx input + txins=collateral_utxos, + txouts=txouts, + script_txins=plutus_txins, + change_address=script_address, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_step2.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_output_step2.script_txins if t.txins] + ) + + assert ( + cluster.g_query.get_address_balance(dst_addr.address) + == dst_step1_balance + amount - collateral_utxos[0].amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{script_address}`" + + # check expected fees + expected_fee_step1 = 168_845 + assert helpers.is_in_interval(tx_output_step1.fee, expected_fee_step1, frac=0.15) + + expected_fee_step2 = 176_986 + assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) + + 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) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestDatum: + """Tests for datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + def test_datum_on_key_credential_address( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test creating UTxO with datum on address with key credentials (non-script address). + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount, + datum_hash_file=plutus_common.DATUM_42_TYPED, + ) + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=temp_template, + tx_files=tx_files, + txouts=txouts, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=temp_template, + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output) + datum_utxo = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0] + assert datum_utxo.datum_hash, f"UTxO should have datum hash: {datum_utxo}" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output) + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_embed_datum_without_pparams( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test 'build --tx-out-datum-embed' without providing protocol params file.""" + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + ) + + script_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + utxos = cluster.g_query.get_utxo(address=payment_addrs[0].address) + txin = txtools.filter_utxo_with_highest_amount(utxos=utxos) + + out_file = f"{temp_template}_tx.body" + + cli_args = [ + "transaction", + "build", + "--tx-in", + f"{txin.utxo_hash}#{txin.utxo_ix}", + "--tx-out", + f"{script_address}+2000000", + "--tx-out-datum-embed-file", + str(plutus_op.datum_file), + "--change-address", + payment_addrs[0].address, + "--out-file", + out_file, + "--testnet-magic", + str(cluster.network_magic), + *cluster.g_transaction.tx_era_arg, + ] + + cluster.cli(cli_args) + + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_signed", + ) + + try: + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=[txin]) + except clusterlib.CLIError as err: + if "PPViewHashesDontMatch" in str(err): + pytest.xfail("build cmd requires protocol params - see node issue #4058") + raise + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegative: + """Tests for Tx output locking using Plutus smart contracts that are expected to fail.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_w_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while collateral contains native tokens. + + Expect failure. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a collateral UTxO with native tokens + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + payment_addr = payment_addrs[0] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addr, + issuer_addr=payment_addr, + amount=100, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens_collateral=tokens_rec, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "CollateralContainsNonADA" in err_str, err_str + + # check expected fees + expected_fee_fund = 173597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_same_collateral_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using the same UTxO as collateral. + + Expect failure. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO while using the same UTxO as collateral + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, __, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=script_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert ( + "expected to be key witnessed but are actually script witnessed: " + f'["{script_utxos[0].utxo_hash}#{script_utxos[0].utxo_ix}"]' in err_str + # in 1.35.3 and older + or "Expected key witnessed collateral" in err_str + ), err_str + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "variant", + ( + "42_43", # correct datum, wrong redeemer + "43_42", # wrong datum, correct redeemer + "43_43", # wrong datum and redeemer + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_invalid_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "guessing game" script that expects specific datum and redeemer value. + Test negative scenarios where datum or redeemer value is different than expected. + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was not spent + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{variant}" + + if variant == "42_43": + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + elif variant == "43_42": + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "43_43": # noqa: SIM106 + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + else: + raise AssertionError("Unknown test variant.") + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME[plutus_version].script_file, + datum_file=datum_file, + redeemer_file=redeemer_file, + execution_cost=plutus_common.GUESSING_GAME[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_two_scripts_spending_one_fail( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx, one fails. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script addresses + * try to spend the locked UTxOs + * check that the expected error was raised + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 50_000_000 + + script_fund = 200_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + plutus_op1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=script_fund, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=script_fund, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + tx_output_fund = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files_fund, + txouts=txouts_fund, + fee_buffer=2_000_000, + join_txouts=False, + ) + tx_signed_fund = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_fund.out_file, + signing_key_files=tx_files_fund.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + + cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_fund.txouts + ) + + script_utxos1 = clusterlib.filter_utxos( + utxos=out_utxos, utxo_ix=utxo_ix_offset, coin=clusterlib.DEFAULT_COIN + ) + script_utxos2 = clusterlib.filter_utxos( + utxos=out_utxos, utxo_ix=utxo_ix_offset + 1, coin=clusterlib.DEFAULT_COIN + ) + collateral_utxos1 = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 2) + collateral_utxos2 = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 3) + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeRedeemer: + """Tests for Tx output locking using Plutus smart contracts with wrong redeemer.""" + + MIN_INT_VAL = -common.MAX_UINT64 + AMOUNT = 2_000_000 + + @pytest.fixture + def fund_script_guessing_game_v1( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]]: + """Fund a PlutusV1 script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED_PLUTUS_V1, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED_COST, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + return script_utxos, collateral_utxos + + @pytest.fixture + def fund_script_guessing_game_v2( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]]: + """Fund a PlutusV2 script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED_PLUTUS_V2, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED_V2_COST, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + return script_utxos, collateral_utxos + + def _int_out_of_range( + self, + cluster: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + redeemer_value: int, + plutus_version: str, + ): + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file) if redeemer_content else None, + redeemer_value=None if redeemer_content else str(redeemer_value), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "Value out of range within the script data" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given( + redeemer_value=st.integers(min_value=MIN_INT_VAL, max_value=common.MAX_UINT64) + ) + @hypothesis.example(redeemer_value=MIN_INT_VAL) + @hypothesis.example(redeemer_value=common.MAX_UINT64) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_inside_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value that is in the valid range. + + Expect failure. + """ + hypothesis.assume(redeemer_value != 42) + + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file) if redeemer_content else None, + redeemer_value=None if redeemer_content else str(redeemer_value), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(min_value=common.MAX_UINT64 + 1)) + @hypothesis.example(redeemer_value=common.MAX_UINT64 + 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_above_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value, above max value allowed. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + self._int_out_of_range( + cluster=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(max_value=MIN_INT_VAL - 1)) + @hypothesis.example(redeemer_value=MIN_INT_VAL - 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_bellow_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value, bellow min value allowed. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + self._int_out_of_range( + cluster=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO with a wrong redeemer type, try to use bytes. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value.hex()}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "Script debugging logs: Incorrect datum. Expected 42." in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO using redeemer that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": redeemer_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in typed format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"int": redeemer_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "int" does not have the type required by the schema.' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"int": redeemer_value.hex()}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "int" does not have the type required by the schema.' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in typed format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": redeemer_value}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "bytes" does not have the type required by the schema.' + in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "bytes" does not have the type required by the schema.' + in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_invalid_json( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: str, + ): + """Try to build a Tx using a redeemer value that is invalid JSON. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{redeemer_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_invalid_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON typed schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{redeemer_type: 42}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_invalid_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON untyped schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({redeemer_type: 42}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err_str + ), err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeDatum: + """Tests for Tx output locking using Plutus smart contracts with wrong datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.parametrize("address_type", ("script_address", "key_address")) + @common.PARAM_PLUTUS_VERSION + def test_no_datum_txout( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + address_type: str, + plutus_version: str, + ): + """Test using UTxO without datum hash in place of locked UTxO. + + Expect failure. + + * create a Tx output without a datum hash + * try to spend the UTxO like it was locked Plutus UTxO + * check that the expected error was raised + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{address_type}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + if address_type == "script_address": + redeem_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + else: + redeem_address = payment_addrs[2].address + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + txouts = [ + clusterlib.TxOut(address=redeem_address, amount=amount + redeem_cost.fee), + clusterlib.TxOut(address=payment_addr.address, amount=redeem_cost.collateral), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + join_txouts=False, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_fund.txouts + ) + + script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + collateral_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + + if address_type == "script_address": + assert "txin does not have a script datum" in err_str, err_str + else: + assert ( + "not a Plutus script witnessed tx input" in err_str + or "points to a script hash that is not known" in err_str + ), err_str + + # check expected fees + expected_fee_fund = 199_087 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_lock_tx_invalid_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: str, + plutus_version: str, + ): + """Test locking a Tx output with an invalid datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{datum_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_tx_wrong_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output and try to spend it with a wrong datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op_1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op_1, + ) + + # use a wrong datum to try to unlock the funds + plutus_op_2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_2, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert ( + "The Plutus script witness has the wrong datum (according to the UTxO)." in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_non_script_utxo( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend a non-script UTxO with datum as if it was script locked UTxO. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + amount_fund = 4_000_000 + amount_redeem = 2_000_000 + amount_collateral = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = plutus_common.DATUM_42_TYPED + + datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file=datum_file) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=datum_file, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + # create datum and collateral UTxOs + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount_fund, + datum_hash=datum_hash, + ), + clusterlib.TxOut( + address=payment_addr.address, + amount=amount_collateral, + ), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=temp_template, + tx_files=tx_files, + txouts=txouts, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=temp_template, + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output) + datum_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=dst_addr.address, datum_hash=datum_hash + )[0] + collateral_utxos = clusterlib.filter_utxos( + utxos=out_utxos, address=payment_addr.address, utxo_ix=datum_utxo.utxo_ix + 1 + ) + assert ( + datum_utxo.datum_hash == datum_hash + ), f"UTxO should have datum hash '{datum_hash}': {datum_utxo}" + + # try to spend the "locked" UTxO + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=[datum_utxo], + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_redeem, + ) + + err_str = str(excinfo.value) + assert "points to a script hash that is not known" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: bytes, + plutus_version: str, + ): + """Try to lock a UTxO with datum that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": datum_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + +@pytest.mark.testnets +class TestCompatibility: + """Tests for checking compatibility with previous Tx eras.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era > VERSIONS.ALONZO, + reason="runs only with Tx era <= Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv2_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV2 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v2"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v2"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV2 is not supported" in err_str, err_str diff --git a/cardano_node_tests/tests/test_plutus/test_spend_compat_build.py b/cardano_node_tests/tests/test_plutus/test_spend_compat_build.py new file mode 100644 index 000000000..798b2c522 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_spend_compat_build.py @@ -0,0 +1,2957 @@ +"""Tests for spending with Plutus using `transaction build`.""" +import json +import logging +import shutil +from pathlib import Path +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from cardano_clusterlib import clusterlib +from cardano_clusterlib import txtools + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + common.SKIPIF_BUILD_UNUSABLE, + pytest.mark.smoke, +] + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment addresses.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(3)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=1_000_000_000, + ) + + return addrs + + +@pytest.fixture +def pool_users( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.PoolUser]: + """Create new pool users.""" + test_id = common.get_test_id(cluster) + created_users = clusterlib_utils.create_pool_users( + cluster_obj=cluster, + name_template=f"{test_id}_pool_users", + no_of_addr=2, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + created_users[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return created_users + + +def _build_fund_script( + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + plutus_op: plutus_common.PlutusOp, + tokens: Optional[List[plutus_common.Token]] = None, # tokens must already be in `payment_addr` + tokens_collateral: Optional[ + List[plutus_common.Token] + ] = None, # tokens must already be in `payment_addr` + embed_datum: bool = False, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund a Plutus script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + assert plutus_op.execution_cost # for mypy + + script_fund = 200_000_000 + + stokens = tokens or () + ctokens = tokens_collateral or () + + script_address = cluster_obj.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + ) + + # create a Tx output with a datum hash at the script address + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + + script_txout = plutus_common.txout_factory( + address=script_address, + amount=script_fund, + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + txouts = [ + script_txout, + # for collateral + clusterlib.TxOut(address=dst_addr.address, amount=redeem_cost.collateral), + ] + + for token in stokens: + txouts.append(script_txout._replace(amount=token.amount, coin=token.coin)) + + for token in ctokens: + txouts.append( + clusterlib.TxOut( + address=dst_addr.address, + amount=token.amount, + coin=token.coin, + ) + ) + + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files, + txouts=txouts, + fee_buffer=2_000_000, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster_obj.g_query.get_utxo(tx_raw_output=tx_output) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset(utxos=out_utxos, txouts=tx_output.txouts) + + script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + assert script_utxos, "No script UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos) == script_fund + ), f"Incorrect balance for script address `{script_address}`" + + collateral_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + assert collateral_utxos, "No collateral UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos) == redeem_cost.collateral + ), f"Incorrect balance for collateral address `{dst_addr.address}`" + + for token in stokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos, coin=token.coin) == token.amount + ), f"Incorrect token balance for script address `{script_address}`" + + for token in ctokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos, coin=token.coin) + == token.amount + ), f"Incorrect token balance for address `{dst_addr.address}`" + + if VERSIONS.transaction_era >= VERSIONS.ALONZO: + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_output) + + return script_utxos, collateral_utxos, tx_output + + +def _build_spend_locked_txin( # noqa: C901 + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + plutus_op: plutus_common.PlutusOp, + amount: int, + deposit_amount: int = 0, + txins: clusterlib.OptionalUTXOData = (), + tx_files: Optional[clusterlib.TxFiles] = None, + invalid_hereafter: Optional[int] = None, + invalid_before: Optional[int] = None, + tokens: Optional[List[plutus_common.Token]] = None, + expect_failure: bool = False, + script_valid: bool = True, + submit_tx: bool = True, +) -> Tuple[str, Optional[clusterlib.TxRawOutput], list]: + """Spend the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + # pylint: disable=too-many-arguments,too-many-locals + tx_files = tx_files or clusterlib.TxFiles() + spent_tokens = tokens or () + + # Change that was calculated manually will be returned to address of the first script. + # The remaining change that is automatically handled by the `build` command will be returned + # to `payment_addr`, because it would be inaccessible on script address without proper + # datum hash (datum hash is not provided for change that is handled by `build` command). + script_change_rec = script_utxos[0] + + # spend the "locked" UTxO + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + datum_value=plutus_op.datum_value if plutus_op.datum_value else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file if plutus_op.redeemer_cbor_file else "", + redeemer_value=plutus_op.redeemer_value if plutus_op.redeemer_value else "", + ) + ] + tx_files = tx_files._replace( + signing_key_files=list({*tx_files.signing_key_files, dst_addr.skey_file}), + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + + lovelace_change_needed = False + for token in spent_tokens: + txouts.append( + clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin) + ) + # append change + script_token_balance = clusterlib.calculate_utxos_balance( + utxos=script_utxos, coin=token.coin + ) + if script_token_balance > token.amount: + lovelace_change_needed = True + txouts.append( + clusterlib.TxOut( + address=script_change_rec.address, + amount=script_token_balance - token.amount, + coin=token.coin, + datum_hash=script_change_rec.datum_hash, + ) + ) + # add minimum (+ some) required Lovelace to change Tx output + if lovelace_change_needed: + txouts.append( + clusterlib.TxOut( + address=script_change_rec.address, + amount=4_000_000, + coin=clusterlib.DEFAULT_COIN, + datum_hash=script_change_rec.datum_hash, + ) + ) + + if expect_failure: + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=txins, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + ) + return str(excinfo.value), None, [] + + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=txins, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + deposit=deposit_amount, + script_valid=script_valid, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + if not submit_tx: + return "", tx_output, [] + + dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address) + + script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN] + + if not script_valid: + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=collateral_utxos) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) + == dst_init_balance - collateral_utxos[0].amount + ), f"Collateral was NOT spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return "", tx_output, [] + + # calculate cost of Plutus script + plutus_costs = cluster_obj.g_transaction.calculate_plutus_script_cost( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + deposit=deposit_amount, + script_valid=script_valid, + ) + + cluster_obj.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_output.script_txins if t.txins] + ) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + for token in spent_tokens: + script_utxos_token = [u for u in script_utxos if u.coin == token.coin] + for u in script_utxos_token: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[token.coin] + ), f"Token inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster_obj, tx_raw_output=tx_output) + + tx_db_record = dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_output) + # compare cost of Plutus script with data from db-sync + if tx_db_record: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_db_record.redeemers, cost_records=plutus_costs + ) + + return "", tx_output, plutus_costs + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestBuildLocking: + """Tests for Tx output locking using Plutus smart contracts and `transaction build`.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Corresponds to Exercise 3 for Alonzo Blue. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + __, tx_output, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 170_782 + assert tx_output and helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + def test_context_equivalence( + self, + cluster: clusterlib.ClusterLib, + pool_users: List[clusterlib.PoolUser], + ): + """Test context equivalence while spending a locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * spend the locked UTxO using the derived redeemer + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 10_000_000 + deposit_amount = cluster.g_query.get_address_deposit() + + # create stake address registration cert + stake_addr_reg_cert_file = cluster.g_stake_address.gen_stake_addr_registration_cert( + addr_name=f"{temp_template}_addr2", + stake_vkey_file=pool_users[0].stake.vkey_file, + ) + + tx_files = clusterlib.TxFiles(certificate_files=[stake_addr_reg_cert_file]) + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_op_dummy = plutus_common.PlutusOp( + script_file=plutus_common.CONTEXT_EQUIVALENCE_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=redeemer_file_dummy, + execution_cost=plutus_common.CONTEXT_EQUIVALENCE_COST, + ) + + # fund the script address + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + plutus_op=plutus_op_dummy, + ) + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + __, tx_output_dummy, __ = _build_spend_locked_txin( + temp_template=f"{temp_template}_dummy", + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_dummy, + amount=amount, + deposit_amount=deposit_amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + submit_tx=False, + ) + assert tx_output_dummy + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + tx_file_dummy = Path(f"{tx_output_dummy.out_file.with_suffix('')}.signed") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_op = plutus_op_dummy._replace(redeemer_file=redeemer_file) + + __, tx_output, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + deposit_amount=deposit_amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + # check expected fees + if tx_output: + expected_fee = 372_438 + assert helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("embed_datum", (True, False), ids=("embedded_datum", "datum")) + @pytest.mark.parametrize( + "variant", + ("typed_json", "typed_cbor", "untyped_value", "untyped_json", "untyped_cbor"), + ) + @common.PARAM_PLUTUS_VERSION + def test_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + embed_datum: bool, + request: FixtureRequest, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "guessing game" scripts that expect specific datum and redeemer value. + Test both typed and untyped redeemer and datum. + Test passing datum and redeemer to `cardano-cli` as value, json file and cbor file. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + + datum_file: Optional[Path] = None + datum_cbor_file: Optional[Path] = None + datum_value: Optional[str] = None + redeemer_file: Optional[Path] = None + redeemer_cbor_file: Optional[Path] = None + redeemer_value: Optional[str] = None + + if variant == "typed_json": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "typed_cbor": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_TYPED_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_TYPED_CBOR + elif variant == "untyped_value": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_value = "42" + redeemer_value = "42" + elif variant == "untyped_json": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_file = plutus_common.DATUM_42 + redeemer_file = plutus_common.REDEEMER_42 + elif variant == "untyped_cbor": # noqa: SIM106 + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + else: + raise AssertionError("Unknown test variant.") + + execution_cost = plutus_common.GUESSING_GAME[plutus_version].execution_cost + if script_file == plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file: + execution_cost = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost + + plutus_op = plutus_common.PlutusOp( + script_file=script_file, + datum_file=datum_file, + datum_cbor_file=datum_cbor_file, + datum_value=datum_value, + redeemer_file=redeemer_file, + redeemer_cbor_file=redeemer_cbor_file, + redeemer_value=redeemer_value, + execution_cost=execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + __, __, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v1_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("plutus_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + script_fund = 200_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + script_file1_v1 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V1 + execution_cost1_v1 = plutus_common.ALWAYS_SUCCEEDS_COST + script_file2_v1 = plutus_common.GUESSING_GAME_PLUTUS_V1 + # this is higher than `plutus_common.GUESSING_GAME_COST`, because the script + # context has changed to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=388_458_303, per_space=1_031_312, fixed_cost=87_515 + ) + else: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=280_668_068, per_space=1_031_312, fixed_cost=79_743 + ) + + script_file1_v2 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V2 + execution_cost1_v2 = plutus_common.ALWAYS_SUCCEEDS_V2_COST + script_file2_v2 = plutus_common.GUESSING_GAME_PLUTUS_V2 + execution_cost2_v2 = plutus_common.ExecutionCost( + per_time=208_314_784, + per_space=662_274, + fixed_cost=53_233, + ) + + expected_fee_fund = 174_389 + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + expected_fee_redeem = 378_768 + elif plutus_version == "mix_v1_v2": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + expected_fee_redeem = 321_739 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + expected_fee_redeem = 378_584 + elif plutus_version == "plutus_v2": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + expected_fee_redeem = 321_378 + else: + raise AssertionError("Unknown test variant.") + + plutus_op1 = plutus_common.PlutusOp( + script_file=script_file1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=execution_cost1, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=script_file2, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_TYPED_CBOR, + execution_cost=execution_cost2, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=script_fund, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=script_fund, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + tx_output_fund = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files_fund, + txouts=txouts_fund, + fee_buffer=2_000_000, + join_txouts=False, + ) + tx_signed_fund = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_fund.out_file, + signing_key_files=tx_files_fund.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + + cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) + + fund_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=fund_utxos, txouts=tx_output_fund.txouts + ) + script_utxos1 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset) + script_utxos2 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 1) + collateral_utxos1 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 2) + collateral_utxos2 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 3) + + assert script_utxos1 and script_utxos2, "No script UTxOs" + assert collateral_utxos1 and collateral_utxos2, "No collateral UTxOs" + + assert ( + script_utxos1[0].amount == script_fund + ), f"Incorrect balance for script address `{script_utxos1[0].address}`" + assert ( + script_utxos2[0].amount == script_fund + ), f"Incorrect balance for script address `{script_utxos2[0].address}`" + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + tx_output_redeem = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + # calculate cost of Plutus script + plutus_costs = cluster.g_transaction.calculate_plutus_script_cost( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + dst_init_balance = cluster.g_query.get_address_balance(payment_addrs[1].address) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed_redeem, + txins=[t.txins[0] for t in tx_output_redeem.script_txins if t.txins], + ) + + assert ( + cluster.g_query.get_address_balance(payment_addrs[1].address) + == dst_init_balance + amount * 2 + ), f"Incorrect balance for destination address `{payment_addrs[1].address}`" + + script_utxos_lovelace = [ + u for u in [*script_utxos1, *script_utxos2] if u.coin == clusterlib.DEFAULT_COIN + ] + for u in script_utxos_lovelace: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + # check expected fees + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + assert helpers.is_in_interval(tx_output_redeem.fee, expected_fee_redeem, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost1, execution_cost2], + ) + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + # check transactions in db-sync + tx_redeem_record = dbsync_utils.check_tx( + cluster_obj=cluster, tx_raw_output=tx_output_redeem + ) + if tx_redeem_record: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_redeem_record.redeemers, cost_records=plutus_costs + ) + + @allure.link(helpers.get_vcs_link()) + def test_always_fails( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the expected error was raised + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err, __, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + expect_failure=True, + ) + assert "The Plutus script evaluation failed" in err, err + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + def test_script_invalid( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test failing script together with the `--script-invalid` argument - collateral is taken. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was spent + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + # include any payment txin + txins = [ + r + for r in cluster.g_query.get_utxo( + address=payment_addrs[0].address, coins=[clusterlib.DEFAULT_COIN] + ) + if not (r.datum_hash or r.inline_datum_hash) + ][:1] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + try: + __, tx_output, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + txins=txins, + tx_files=tx_files, + script_valid=False, + ) + except clusterlib.CLIError as err: + # TODO: broken on node 1.35.0 and 1.35.1 + if "ScriptWitnessIndexTxIn 0 is missing from the execution units" in str(err): + pytest.xfail("See cardano-node issue #4013") + else: + raise + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + if tx_output: + expected_fee = 171_309 + assert helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_token_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO + * check that the expected amounts of Lovelace and native tokens were spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=100, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens=tokens_rec, + ) + + __, tx_output_spend, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + tokens=tokens_rec, + ) + + # check expected fees + expected_fee_fund = 173_597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 175_710 + assert tx_output_spend and helpers.is_in_interval( + tx_output_spend.fee, expected_fee, frac=0.15 + ) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_partial_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending part of funds (Lovelace and native tokens) on a locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO and create new locked UTxO with change + * check that the expected amounts of Lovelace and native tokens were spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + token_rand = clusterlib.get_rand_str(5) + + amount_spend = 10_000_000 + token_amount_fund = 100 + token_amount_spend = 20 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount_fund, + ) + tokens_fund_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens=tokens_fund_rec, + ) + + tokens_spend_rec = [ + plutus_common.Token(coin=t.token, amount=token_amount_spend) for t in tokens + ] + + __, tx_output_spend, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_spend, + tokens=tokens_spend_rec, + ) + + # check that the expected amounts of Lovelace and native tokens were spent and change UTxOs + # with appropriate datum hash were created + + assert tx_output_spend + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_spend) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_spend.txouts + ) + + # UTxO we created for tokens and minimum required Lovelace + change_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + # UTxO that was created by `build` command for rest of the Lovelace change (this will not + # have the script's datum) + # TODO: change UTxO used to be first, now it's last + build_change_utxo = out_utxos[0] if utxo_ix_offset else out_utxos[-1] + + # Lovelace balance on original script UTxOs + script_lovelace_balance = clusterlib.calculate_utxos_balance(utxos=script_utxos) + # Lovelace balance on change UTxOs + change_lovelace_balance = clusterlib.calculate_utxos_balance( + utxos=[*change_utxos, build_change_utxo] + ) + + assert ( + change_lovelace_balance == script_lovelace_balance - tx_output_spend.fee - amount_spend + ) + + token_amount_exp = token_amount_fund - token_amount_spend + assert len(change_utxos) == len(tokens_spend_rec) + 1 + for u in change_utxos: + if u.coin != clusterlib.DEFAULT_COIN: + assert u.amount == token_amount_exp + assert u.datum_hash == script_utxos[0].datum_hash + + # check expected fees + expected_fee_fund = 173_597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 183_366 + assert tx_output_spend and helpers.is_in_interval( + tx_output_spend.fee, expected_fee, frac=0.15 + ) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_is_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using single UTxO for both collateral and Tx input. + + Uses `cardano-cli transaction build` command for building the transactions. + + Tests bug https://github.com/input-output-hk/cardano-db-sync/issues/750 + + * create a Tx output with a datum hash at the script address and a collateral UTxO + * check that the expected amount was locked at the script address + * spend the locked UTxO while using the collateral UTxO both as collateral and as + normal Tx input + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + # Step 1: fund the script address + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_step1 = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + ) + + # Step 2: spend the "locked" UTxO + + script_address = script_utxos[0].address + + dst_step1_balance = cluster.g_query.get_address_balance(dst_addr.address) + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file + if plutus_op.redeemer_cbor_file + else "", + ) + ] + tx_files = clusterlib.TxFiles( + signing_key_files=[dst_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + + tx_output_step2 = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + # `collateral_utxos` is used both as collateral and as normal Tx input + txins=collateral_utxos, + txouts=txouts, + script_txins=plutus_txins, + change_address=script_address, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_step2.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_output_step2.script_txins if t.txins] + ) + + assert ( + cluster.g_query.get_address_balance(dst_addr.address) + == dst_step1_balance + amount - collateral_utxos[0].amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{script_address}`" + + # check expected fees + expected_fee_step1 = 168_845 + assert helpers.is_in_interval(tx_output_step1.fee, expected_fee_step1, frac=0.15) + + expected_fee_step2 = 176_986 + assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) + + 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) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestDatum: + """Tests for datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + def test_datum_on_key_credential_address( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test creating UTxO with datum on address with key credentials (non-script address). + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount, + datum_hash_file=plutus_common.DATUM_42_TYPED, + ) + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=temp_template, + tx_files=tx_files, + txouts=txouts, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=temp_template, + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output) + datum_utxo = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0] + assert datum_utxo.datum_hash, f"UTxO should have datum hash: {datum_utxo}" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output) + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_embed_datum_without_pparams( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test 'build --tx-out-datum-embed' without providing protocol params file.""" + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + ) + + script_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + utxos = cluster.g_query.get_utxo(address=payment_addrs[0].address) + txin = txtools.filter_utxo_with_highest_amount(utxos=utxos) + + out_file = f"{temp_template}_tx.body" + + cli_args = [ + "transaction", + "build", + "--tx-in", + f"{txin.utxo_hash}#{txin.utxo_ix}", + "--tx-out", + f"{script_address}+2000000", + "--tx-out-datum-embed-file", + str(plutus_op.datum_file), + "--change-address", + payment_addrs[0].address, + "--out-file", + out_file, + "--testnet-magic", + str(cluster.network_magic), + *cluster.g_transaction.tx_era_arg, + ] + + cluster.cli(cli_args) + + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_signed", + ) + + try: + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=[txin]) + except clusterlib.CLIError as err: + if "PPViewHashesDontMatch" in str(err): + pytest.xfail("build cmd requires protocol params - see node issue #4058") + raise + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegative: + """Tests for Tx output locking using Plutus smart contracts that are expected to fail.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_w_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while collateral contains native tokens. + + Expect failure. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a collateral UTxO with native tokens + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + payment_addr = payment_addrs[0] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addr, + issuer_addr=payment_addr, + amount=100, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens_collateral=tokens_rec, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "CollateralContainsNonADA" in err_str, err_str + + # check expected fees + expected_fee_fund = 173597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_same_collateral_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using the same UTxO as collateral. + + Expect failure. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO while using the same UTxO as collateral + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, __, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=script_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert ( + "expected to be key witnessed but are actually script witnessed: " + f'["{script_utxos[0].utxo_hash}#{script_utxos[0].utxo_ix}"]' in err_str + # in 1.35.3 and older + or "Expected key witnessed collateral" in err_str + ), err_str + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "variant", + ( + "42_43", # correct datum, wrong redeemer + "43_42", # wrong datum, correct redeemer + "43_43", # wrong datum and redeemer + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_invalid_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "guessing game" script that expects specific datum and redeemer value. + Test negative scenarios where datum or redeemer value is different than expected. + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was not spent + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{variant}" + + if variant == "42_43": + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + elif variant == "43_42": + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "43_43": # noqa: SIM106 + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + else: + raise AssertionError("Unknown test variant.") + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME[plutus_version].script_file, + datum_file=datum_file, + redeemer_file=redeemer_file, + execution_cost=plutus_common.GUESSING_GAME[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_two_scripts_spending_one_fail( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx, one fails. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script addresses + * try to spend the locked UTxOs + * check that the expected error was raised + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 50_000_000 + + script_fund = 200_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + plutus_op1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=script_fund, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=script_fund, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + tx_output_fund = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files_fund, + txouts=txouts_fund, + fee_buffer=2_000_000, + join_txouts=False, + ) + tx_signed_fund = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_fund.out_file, + signing_key_files=tx_files_fund.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + + cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_fund.txouts + ) + + script_utxos1 = clusterlib.filter_utxos( + utxos=out_utxos, utxo_ix=utxo_ix_offset, coin=clusterlib.DEFAULT_COIN + ) + script_utxos2 = clusterlib.filter_utxos( + utxos=out_utxos, utxo_ix=utxo_ix_offset + 1, coin=clusterlib.DEFAULT_COIN + ) + collateral_utxos1 = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 2) + collateral_utxos2 = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 3) + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeRedeemer: + """Tests for Tx output locking using Plutus smart contracts with wrong redeemer.""" + + MIN_INT_VAL = -common.MAX_UINT64 + AMOUNT = 2_000_000 + + @pytest.fixture + def fund_script_guessing_game_v1( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]]: + """Fund a PlutusV1 script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED_PLUTUS_V1, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED_COST, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + return script_utxos, collateral_utxos + + @pytest.fixture + def fund_script_guessing_game_v2( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]]: + """Fund a PlutusV2 script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED_PLUTUS_V2, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED_V2_COST, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + return script_utxos, collateral_utxos + + def _int_out_of_range( + self, + cluster: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + redeemer_value: int, + plutus_version: str, + ): + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file) if redeemer_content else None, + redeemer_value=None if redeemer_content else str(redeemer_value), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "Value out of range within the script data" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given( + redeemer_value=st.integers(min_value=MIN_INT_VAL, max_value=common.MAX_UINT64) + ) + @hypothesis.example(redeemer_value=MIN_INT_VAL) + @hypothesis.example(redeemer_value=common.MAX_UINT64) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_inside_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value that is in the valid range. + + Expect failure. + """ + hypothesis.assume(redeemer_value != 42) + + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file) if redeemer_content else None, + redeemer_value=None if redeemer_content else str(redeemer_value), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(min_value=common.MAX_UINT64 + 1)) + @hypothesis.example(redeemer_value=common.MAX_UINT64 + 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_above_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value, above max value allowed. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + self._int_out_of_range( + cluster=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(max_value=MIN_INT_VAL - 1)) + @hypothesis.example(redeemer_value=MIN_INT_VAL - 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_bellow_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value, bellow min value allowed. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + self._int_out_of_range( + cluster=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO with a wrong redeemer type, try to use bytes. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value.hex()}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "Script debugging logs: Incorrect datum. Expected 42." in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO using redeemer that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": redeemer_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in typed format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"int": redeemer_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "int" does not have the type required by the schema.' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"int": redeemer_value.hex()}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "int" does not have the type required by the schema.' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in typed format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": redeemer_value}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "bytes" does not have the type required by the schema.' + in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "bytes" does not have the type required by the schema.' + in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_invalid_json( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: str, + ): + """Try to build a Tx using a redeemer value that is invalid JSON. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{redeemer_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_invalid_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON typed schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{redeemer_type: 42}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_invalid_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON untyped schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({redeemer_type: 42}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err_str + ), err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeDatum: + """Tests for Tx output locking using Plutus smart contracts with wrong datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.parametrize("address_type", ("script_address", "key_address")) + @common.PARAM_PLUTUS_VERSION + def test_no_datum_txout( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + address_type: str, + plutus_version: str, + ): + """Test using UTxO without datum hash in place of locked UTxO. + + Expect failure. + + * create a Tx output without a datum hash + * try to spend the UTxO like it was locked Plutus UTxO + * check that the expected error was raised + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{address_type}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + if address_type == "script_address": + redeem_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + else: + redeem_address = payment_addrs[2].address + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + txouts = [ + clusterlib.TxOut(address=redeem_address, amount=amount + redeem_cost.fee), + clusterlib.TxOut(address=payment_addr.address, amount=redeem_cost.collateral), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + join_txouts=False, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_fund.txouts + ) + + script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + collateral_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + + if address_type == "script_address": + assert "txin does not have a script datum" in err_str, err_str + else: + assert ( + "not a Plutus script witnessed tx input" in err_str + or "points to a script hash that is not known" in err_str + ), err_str + + # check expected fees + expected_fee_fund = 199_087 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_lock_tx_invalid_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: str, + plutus_version: str, + ): + """Test locking a Tx output with an invalid datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{datum_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_tx_wrong_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output and try to spend it with a wrong datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op_1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op_1, + ) + + # use a wrong datum to try to unlock the funds + plutus_op_2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_2, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert ( + "The Plutus script witness has the wrong datum (according to the UTxO)." in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_non_script_utxo( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend a non-script UTxO with datum as if it was script locked UTxO. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + amount_fund = 4_000_000 + amount_redeem = 2_000_000 + amount_collateral = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = plutus_common.DATUM_42_TYPED + + datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file=datum_file) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=datum_file, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + # create datum and collateral UTxOs + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount_fund, + datum_hash=datum_hash, + ), + clusterlib.TxOut( + address=payment_addr.address, + amount=amount_collateral, + ), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=temp_template, + tx_files=tx_files, + txouts=txouts, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=temp_template, + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output) + datum_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=dst_addr.address, datum_hash=datum_hash + )[0] + collateral_utxos = clusterlib.filter_utxos( + utxos=out_utxos, address=payment_addr.address, utxo_ix=datum_utxo.utxo_ix + 1 + ) + assert ( + datum_utxo.datum_hash == datum_hash + ), f"UTxO should have datum hash '{datum_hash}': {datum_utxo}" + + # try to spend the "locked" UTxO + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=[datum_utxo], + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_redeem, + ) + + err_str = str(excinfo.value) + assert "points to a script hash that is not known" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: bytes, + plutus_version: str, + ): + """Try to lock a UTxO with datum that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": datum_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + +@pytest.mark.testnets +class TestCompatibility: + """Tests for checking compatibility with previous Tx eras.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era > VERSIONS.ALONZO, + reason="runs only with Tx era <= Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv2_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV2 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v2"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v2"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV2 is not supported" in err_str, err_str diff --git a/cardano_node_tests/tests/test_plutus/test_spend_compat_raw.py b/cardano_node_tests/tests/test_plutus/test_spend_compat_raw.py new file mode 100644 index 000000000..a264ae894 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_spend_compat_raw.py @@ -0,0 +1,3052 @@ +"""Tests for spending with Plutus using `transaction build-raw`.""" +import itertools +import json +import logging +import shutil +import time +from pathlib import Path +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import SubRequest +from cardano_clusterlib import clusterlib + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import logfiles +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + pytest.mark.smoke, +] + + +FundTupleT = Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], List[clusterlib.AddressRecord] +] + +# approx. fee for Tx size +FEE_REDEEM_TXSIZE = 400_000 + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment addresses.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(3)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return addrs + + +@pytest.fixture +def pool_users( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.PoolUser]: + """Create new pool users.""" + test_id = common.get_test_id(cluster) + created_users = clusterlib_utils.create_pool_users( + cluster_obj=cluster, + name_template=f"{test_id}_pool_users", + no_of_addr=2, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + created_users[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return created_users + + +def _fund_script( + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + plutus_op: plutus_common.PlutusOp, + amount: int, + fee_txsize: int = FEE_REDEEM_TXSIZE, + deposit_amount: int = 0, + tokens: Optional[List[plutus_common.Token]] = None, # tokens must already be in `payment_addr` + tokens_collateral: Optional[ + List[plutus_common.Token] + ] = None, # tokens must already be in `payment_addr` + collateral_fraction_offset: float = 1.0, + embed_datum: bool = False, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund a Plutus script and create the locked UTxO and collateral UTxO.""" + # pylint: disable=too-many-locals,too-many-arguments + assert plutus_op.execution_cost # for mypy + + stokens = tokens or () + ctokens = tokens_collateral or () + + script_address = cluster_obj.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + collateral_fraction_offset=collateral_fraction_offset, + ) + + # create a Tx output with a datum hash at the script address + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + + script_txout = plutus_common.txout_factory( + address=script_address, + amount=amount + redeem_cost.fee + fee_txsize + deposit_amount, + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + txouts = [ + script_txout, + # for collateral + clusterlib.TxOut(address=dst_addr.address, amount=redeem_cost.collateral), + ] + + for token in stokens: + txouts.append(script_txout._replace(amount=token.amount, coin=token.coin)) + + for token in ctokens: + txouts.append( + clusterlib.TxOut( + address=dst_addr.address, + amount=token.amount, + coin=token.coin, + ) + ) + + tx_raw_output = cluster_obj.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + txouts=txouts, + tx_files=tx_files, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + ) + + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + script_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#0") + assert script_utxos, "No script UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos) == txouts[0].amount + ), f"Incorrect balance for script address `{script_address}`" + + collateral_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#1") + assert collateral_utxos, "No collateral UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos) == redeem_cost.collateral + ), f"Incorrect balance for collateral address `{dst_addr.address}`" + + for token in stokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos, coin=token.coin) == token.amount + ), f"Incorrect token balance for script address `{script_address}`" + + for token in ctokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos, coin=token.coin) + == token.amount + ), f"Incorrect token balance for address `{dst_addr.address}`" + + if VERSIONS.transaction_era >= VERSIONS.ALONZO: + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + return script_utxos, collateral_utxos, tx_raw_output + + +def _spend_locked_txin( # noqa: C901 + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + dst_addr: clusterlib.AddressRecord, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + plutus_op: plutus_common.PlutusOp, + amount: int, + fee_txsize: int = FEE_REDEEM_TXSIZE, + txins: clusterlib.OptionalUTXOData = (), + tx_files: Optional[clusterlib.TxFiles] = None, + invalid_hereafter: Optional[int] = None, + invalid_before: Optional[int] = None, + tokens: Optional[List[plutus_common.Token]] = None, + expect_failure: bool = False, + script_valid: bool = True, + submit_tx: bool = True, +) -> Tuple[str, clusterlib.TxRawOutput]: + """Spend the locked UTxO.""" + # pylint: disable=too-many-arguments,too-many-locals + assert plutus_op.execution_cost + + tx_files = tx_files or clusterlib.TxFiles() + spent_tokens = tokens or () + + # change will be returned to address of the first script + change_rec = script_utxos[0] + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + ) + + script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN] + script_lovelace_balance = clusterlib.calculate_utxos_balance( + utxos=[*script_utxos_lovelace, *txins] + ) + + # spend the "locked" UTxO + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + execution_units=(plutus_op.execution_cost.per_time, plutus_op.execution_cost.per_space), + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + datum_value=plutus_op.datum_value if plutus_op.datum_value else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file if plutus_op.redeemer_cbor_file else "", + redeemer_value=plutus_op.redeemer_value if plutus_op.redeemer_value else "", + ) + ] + + tx_files = tx_files._replace( + signing_key_files=list({*tx_files.signing_key_files, dst_addr.skey_file}), + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + # append change + if script_lovelace_balance > amount + redeem_cost.fee + fee_txsize: + txouts.append( + clusterlib.TxOut( + address=change_rec.address, + amount=script_lovelace_balance - amount - redeem_cost.fee - fee_txsize, + datum_hash=change_rec.datum_hash, + ) + ) + + for token in spent_tokens: + txouts.append( + clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin) + ) + # append change + script_token_balance = clusterlib.calculate_utxos_balance( + utxos=script_utxos, coin=token.coin + ) + if script_token_balance > token.amount: + txouts.append( + clusterlib.TxOut( + address=change_rec.address, + amount=script_token_balance - token.amount, + coin=token.coin, + datum_hash=change_rec.datum_hash, + ) + ) + + tx_raw_output = cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=txins, + txouts=txouts, + tx_files=tx_files, + fee=redeem_cost.fee + fee_txsize, + script_txins=plutus_txins, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + script_valid=script_valid, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + if not submit_tx: + return "", tx_raw_output + + dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address) + + if not script_valid: + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=collateral_utxos) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) + == dst_init_balance - collateral_utxos[0].amount + ), f"Collateral was NOT spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return "", tx_raw_output + + if expect_failure: + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed) + err = str(excinfo.value) + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + ), f"Collateral was spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return err, tx_raw_output + + cluster_obj.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_raw_output.script_txins if t.txins] + ) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + for token in spent_tokens: + script_utxos_token = [u for u in script_utxos if u.coin == token.coin] + for u in script_utxos_token: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[token.coin] + ), f"Token inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + return "", tx_raw_output + + +def _check_pretty_utxo( + cluster_obj: clusterlib.ClusterLib, tx_raw_output: clusterlib.TxRawOutput +) -> str: + """Check that pretty printed `query utxo` output looks as expected.""" + err = "" + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + utxo_out = ( + cluster_obj.cli( + [ + "query", + "utxo", + "--tx-in", + f"{txid}#0", + *cluster_obj.magic_args, + ] + ) + .stdout.decode("utf-8") + .split() + ) + + cluster_era = VERSIONS.cluster_era_name.title() + datum_hash = clusterlib_utils.datum_hash_from_txout( + cluster_obj=cluster_obj, txout=tx_raw_output.txouts[0] + ) + expected_out = [ + "TxHash", + "TxIx", + "Amount", + "--------------------------------------------------------------------------------------", + txid, + "0", + str(tx_raw_output.txouts[0].amount), + tx_raw_output.txouts[0].coin, + "+", + "TxOutDatumHash", + f"ScriptDataIn{cluster_era}Era", + f'"{datum_hash}"', + ] + + if utxo_out != expected_out: + err = f"Pretty UTxO output doesn't match expected output:\n{utxo_out}\nvs\n{expected_out}" + + return err + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestLocking: + """Tests for Tx output locking using Plutus smart contracts.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Corresponds to Exercise 3 for Alonzo Blue. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + utxo_err = _check_pretty_utxo(cluster_obj=cluster, tx_raw_output=tx_output_fund) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + if utxo_err: + pytest.fail(utxo_err) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + def test_context_equivalence( + self, + cluster: clusterlib.ClusterLib, + pool_users: List[clusterlib.PoolUser], + ): + """Test context equivalence while spending a locked UTxO. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * spend the locked UTxO using the derived redeemer + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 10_000_000 + deposit_amount = cluster.g_query.get_address_deposit() + + # create stake address registration cert + stake_addr_reg_cert_file = cluster.g_stake_address.gen_stake_addr_registration_cert( + addr_name=f"{temp_template}_addr0", + stake_vkey_file=pool_users[0].stake.vkey_file, + ) + + tx_files = clusterlib.TxFiles(certificate_files=[stake_addr_reg_cert_file]) + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_op_dummy = plutus_common.PlutusOp( + script_file=plutus_common.CONTEXT_EQUIVALENCE_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=redeemer_file_dummy, + execution_cost=plutus_common.CONTEXT_EQUIVALENCE_COST, + ) + + # fund the script address + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + plutus_op=plutus_op_dummy, + amount=amount, + deposit_amount=deposit_amount, + ) + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + __, tx_output_dummy = _spend_locked_txin( + temp_template=f"{temp_template}_dummy", + cluster_obj=cluster, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_dummy, + amount=amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + submit_tx=False, + ) + assert tx_output_dummy + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + tx_file_dummy = Path(f"{tx_output_dummy.out_file.with_suffix('')}.signed") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_op = plutus_op_dummy._replace(redeemer_file=redeemer_file) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("embed_datum", (True, False), ids=("embedded_datum", "datum")) + @pytest.mark.parametrize( + "variant", + ( + "typed_json", + "typed_cbor", + "untyped_value", + "untyped_json", + "untyped_cbor", + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + request: FixtureRequest, + embed_datum: bool, + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "guessing game" scripts that expect specific datum and redeemer value. + Test both typed and untyped redeemer and datum. + Test passing datum and redeemer to `cardano-cli` as value, json file and cbor file. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + amount = 2_000_000 + + datum_file: Optional[Path] = None + datum_cbor_file: Optional[Path] = None + datum_value: Optional[str] = None + redeemer_file: Optional[Path] = None + redeemer_cbor_file: Optional[Path] = None + redeemer_value: Optional[str] = None + + if variant == "typed_json": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "typed_cbor": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_TYPED_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_TYPED_CBOR + elif variant == "untyped_value": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_value = "42" + redeemer_value = "42" + elif variant == "untyped_json": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_file = plutus_common.DATUM_42 + redeemer_file = plutus_common.REDEEMER_42 + elif variant == "untyped_cbor": # noqa: SIM106 + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + else: + raise AssertionError("Unknown test variant.") + + execution_cost = plutus_common.GUESSING_GAME[plutus_version].execution_cost + if script_file == plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file: + execution_cost = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost + + plutus_op = plutus_common.PlutusOp( + script_file=script_file, + datum_file=datum_file, + datum_cbor_file=datum_cbor_file, + datum_value=datum_value, + redeemer_file=redeemer_file, + redeemer_cbor_file=redeemer_cbor_file, + redeemer_value=redeemer_value, + execution_cost=execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + embed_datum=embed_datum, + ) + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v1_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("plutus_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + script_file1_v1 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V1 + execution_cost1_v1 = plutus_common.ALWAYS_SUCCEEDS_COST + script_file2_v1 = plutus_common.GUESSING_GAME_PLUTUS_V1 + # this is higher than `plutus_common.GUESSING_GAME_COST`, because the script + # context has changed to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=388_458_303, per_space=1_031_312, fixed_cost=87_515 + ) + else: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=280_668_068, per_space=1_031_312, fixed_cost=79_743 + ) + + script_file1_v2 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V2 + execution_cost1_v2 = plutus_common.ALWAYS_SUCCEEDS_V2_COST + script_file2_v2 = plutus_common.GUESSING_GAME_PLUTUS_V2 + execution_cost2_v2 = plutus_common.ExecutionCost( + per_time=208_314_784, + per_space=662_274, + fixed_cost=53_233, + ) + + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + elif plutus_version == "mix_v1_v2": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + elif plutus_version == "plutus_v2": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + else: + raise AssertionError("Unknown test variant.") + + plutus_op1 = plutus_common.PlutusOp( + script_file=script_file1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=execution_cost1, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=script_file2, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_TYPED_CBOR, + execution_cost=execution_cost2, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=amount + redeem_cost1.fee, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + txouts=txouts_fund, + tx_files=tx_files_fund, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + join_txouts=False, + ) + + txid_fund = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos1 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#0", coins=[clusterlib.DEFAULT_COIN] + ) + script_utxos2 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#1", coins=[clusterlib.DEFAULT_COIN] + ) + collateral_utxos1 = cluster.g_query.get_utxo(txin=f"{txid_fund}#2") + collateral_utxos2 = cluster.g_query.get_utxo(txin=f"{txid_fund}#3") + + assert script_utxos1 and script_utxos2, "No script UTxOs" + assert collateral_utxos1 and collateral_utxos2, "No collateral UTxOs" + + assert ( + script_utxos1[0].amount == amount + redeem_cost1.fee + ), f"Incorrect balance for script address `{script_utxos1[0].address}`" + assert ( + script_utxos2[0].amount == amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE + ), f"Incorrect balance for script address `{script_utxos2[0].address}`" + + # Step 2: spend the "locked" UTxOs + + # for mypy + assert plutus_op1.execution_cost and plutus_op2.execution_cost + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + execution_units=( + plutus_op1.execution_cost.per_time, + plutus_op1.execution_cost.per_space, + ), + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + execution_units=( + plutus_op2.execution_cost.per_time, + plutus_op2.execution_cost.per_space, + ), + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + tx_output_redeem = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts_redeem, + tx_files=tx_files_redeem, + fee=redeem_cost1.fee + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + dst_init_balance = cluster.g_query.get_address_balance(payment_addrs[1].address) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed_redeem, + txins=[t.txins[0] for t in tx_output_redeem.script_txins if t.txins], + ) + + assert ( + cluster.g_query.get_address_balance(payment_addrs[1].address) + == dst_init_balance + amount * 2 + ), f"Incorrect balance for destination address `{payment_addrs[1].address}`" + + script_utxos_lovelace = [ + u for u in [*script_utxos1, *script_utxos2] if u.coin == clusterlib.DEFAULT_COIN + ] + for u in script_utxos_lovelace: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + @allure.link(helpers.get_vcs_link()) + def test_always_fails( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + worker_id: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred, collateral UTxO was not spent + and the expected error was raised + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + logfiles.add_ignore_rule( + files_glob="*.stdout", + regex="ValidationTagMismatch", + ignore_file_id=worker_id, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + err, __ = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + expect_failure=True, + ) + assert "PlutusFailure" in err, err + + # wait a bit so there's some time for error messages to appear in log file + time.sleep(1 if cluster.network_magic == configuration.NETWORK_MAGIC_LOCAL else 5) + + @allure.link(helpers.get_vcs_link()) + def test_script_invalid( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test failing script together with the `--script-invalid` argument - collateral is taken. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was spent + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + # include any payment txin + txins = [ + r + for r in cluster.g_query.get_utxo( + address=payment_addrs[0].address, coins=[clusterlib.DEFAULT_COIN] + ) + if not (r.datum_hash or r.inline_datum_hash) + ][:1] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + txins=txins, + tx_files=tx_files, + script_valid=False, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_token_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with native tokens and spending the locked UTxO. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO + * check that the expected amounts of Lovelace and native tokens were spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + amount = 2_000_000 + token_amount = 100 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + tokens=tokens_rec, + ) + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + tokens=tokens_rec, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_partial_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending part of funds (Lovelace and native tokens) on a locked UTxO. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO and create new locked UTxO with change + * check that the expected amounts of Lovelace and native tokens were spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + token_rand = clusterlib.get_rand_str(5) + + amount_fund = 6_000_000 + amount_spend = 2_000_000 + token_amount_fund = 100 + token_amount_spend = 20 + + # add extra fee for tokens + fee_redeem_txsize = FEE_REDEEM_TXSIZE + 5_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount_fund, + ) + tokens_fund_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount_fund, + fee_txsize=fee_redeem_txsize, + tokens=tokens_fund_rec, + ) + + tokens_spend_rec = [ + plutus_common.Token(coin=t.token, amount=token_amount_spend) for t in tokens + ] + + __, tx_output_spend = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_spend, + fee_txsize=fee_redeem_txsize, + tokens=tokens_spend_rec, + ) + + txid_spend = cluster.g_transaction.get_txid(tx_body_file=tx_output_spend.out_file) + change_utxos = cluster.g_query.get_utxo(txin=f"{txid_spend}#1") + + # check that the expected amounts of Lovelace and native tokens were spent and change UTxOs + # with appropriate datum hash were created + token_amount_exp = token_amount_fund - token_amount_spend + assert len(change_utxos) == len(tokens_spend_rec) + 1 + for u in change_utxos: + if u.coin == clusterlib.DEFAULT_COIN: + assert u.amount == amount_fund - amount_spend + else: + assert u.amount == token_amount_exp + assert u.datum_hash == script_utxos[0].datum_hash + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("scenario", ("max", "max+1", "none")) + @common.PARAM_PLUTUS_VERSION + def test_collaterals( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + scenario: str, + plutus_version: str, + ): + """Test dividing required collateral amount into multiple collateral UTxOs. + + Test 3 scenarios: + 1. maximum allowed number of collateral inputs + 2. more collateral inputs than what is allowed + 3. no collateral input + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * create multiple UTxOs for collateral + * spend the locked UTxO + * check that the expected amount was spent when success is expected + * OR check that the amount was not transferred and collateral UTxO was not spent + when failure is expected + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{scenario}" + amount = 2_000_000 + + max_collateral_ins = cluster.g_query.get_protocol_params()["maxCollateralInputs"] + collateral_utxos = [] + + if scenario == "max": + collateral_num = max_collateral_ins + exp_err = "" + collateral_fraction_offset = 250_000.0 + elif scenario == "max+1": + collateral_num = max_collateral_ins + 1 + exp_err = "TooManyCollateralInputs" + collateral_fraction_offset = 250_000.0 + else: + collateral_num = 0 + exp_err = "Transaction body has no collateral inputs" + collateral_fraction_offset = 1.0 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, fund_collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + collateral_fraction_offset=collateral_fraction_offset, + ) + + if collateral_num: + # instead of using the collateral UTxO created by `_fund_script`, create multiple new + # collateral UTxOs with the combined amount matching the original UTxO + collateral_amount_part = int(fund_collateral_utxos[0].amount // collateral_num) + 1 + txouts_collaterals = [ + clusterlib.TxOut(address=dst_addr.address, amount=collateral_amount_part) + for __ in range(collateral_num) + ] + tx_files_collaterals = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + tx_output_collaterals = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_collaterals", + txouts=txouts_collaterals, + tx_files=tx_files_collaterals, + join_txouts=False, + ) + txid_collaterals = cluster.g_transaction.get_txid( + tx_body_file=tx_output_collaterals.out_file + ) + _utxos_nested = [ + cluster.g_query.get_utxo(txin=f"{txid_collaterals}#{i}") + for i in range(collateral_num) + ] + collateral_utxos = list(itertools.chain.from_iterable(_utxos_nested)) + + if exp_err: + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert exp_err in err_str, err_str + else: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestDatum: + """Tests for datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + def test_datum_on_key_credential_address( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test creating UTxO with datum on address with key credentials (non-script address).""" + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount, + datum_hash_file=plutus_common.DATUM_42_TYPED, + ) + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_raw_output = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output) + datum_utxo = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0] + assert datum_utxo.datum_hash, f"UTxO should have datum hash: {datum_utxo}" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegative: + """Tests for Tx output locking using Plutus smart contracts that are expected to fail.""" + + @pytest.fixture + def pparams(self, cluster: clusterlib.ClusterLib) -> dict: + return cluster.g_query.get_protocol_params() + + @pytest.fixture + def fund_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + request: SubRequest, + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp]: + plutus_version = request.param + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=2_000_000, + ) + + return script_utxos, collateral_utxos, plutus_op + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @pytest.mark.parametrize( + "variant", + ( + "42_43", # correct datum, wrong redeemer + "43_42", # wrong datum, correct redeemer + "43_43", # wrong datum and redeemer + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_invalid_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "guessing game" script that expects specific datum and redeemer value. + Test negative scenarios where datum or redeemer value is different than expected. + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was not spent + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{variant}" + amount = 2_000_000 + + if variant == "42_43": + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + elif variant == "43_42": + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "43_43": # noqa: SIM106 + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + else: + raise AssertionError("Unknown test variant.") + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME[plutus_version].script_file, + datum_file=datum_file, + redeemer_file=redeemer_file, + execution_cost=plutus_common.GUESSING_GAME[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + err, __ = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + expect_failure=True, + ) + + assert "ValidationTagMismatch (IsValid True)" in err, err + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_w_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while collateral contains native tokens. + + Expect failure. + + * create a collateral UTxO with native tokens + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + amount = 2_000_000 + token_amount = 100 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + tokens_collateral=tokens_rec, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "CollateralContainsNonADA" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_same_collateral_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using the same UTxO as collateral. + + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO while using the same UTxO as collateral + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_CBOR, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, *__ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=script_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert "cardano-cli transaction submit" in err_str, err_str + assert "ScriptsNotPaidUTxO" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_percent( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend locked UTxO while collateral is less than required. + + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * create a collateral UTxO with amount of ADA less than required by `collateralPercentage` + * try to spend the UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + # increase fixed cost so the required collateral is higher than minimum collateral of 2 ADA + execution_cost = plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost + execution_cost_increased = execution_cost._replace(fixed_cost=2_000_000) + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=execution_cost_increased, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + collateral_fraction_offset=0.9, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "InsufficientCollateral" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_two_scripts_spending_one_fail( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx, one fails. + + Expect failure. + + * create a Tx output with a datum hash at the script addresses + * try to spend the locked UTxOs + * check that the expected error was raised + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + plutus_op1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + script2_hash = helpers.decode_bech32(bech32=script_address2)[2:] + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=amount + redeem_cost1.fee, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + txouts=txouts_fund, + tx_files=tx_files_fund, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + join_txouts=False, + ) + + txid_fund = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos1 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#0", coins=[clusterlib.DEFAULT_COIN] + ) + script_utxos2 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#1", coins=[clusterlib.DEFAULT_COIN] + ) + collateral_utxos1 = cluster.g_query.get_utxo(txin=f"{txid_fund}#2") + collateral_utxos2 = cluster.g_query.get_utxo(txin=f"{txid_fund}#3") + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + execution_units=( + plutus_op1.execution_cost.per_time, + plutus_op1.execution_cost.per_space, + ), + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + execution_units=( + plutus_op2.execution_cost.per_time, + plutus_op2.execution_cost.per_space, + ), + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + + tx_output_redeem = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts_redeem, + tx_files=tx_files_redeem, + fee=redeem_cost1.fee + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed_redeem) + + err_str = str(excinfo.value) + assert rf"ScriptHash \"{script2_hash}\") fails" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(data=st.data()) + @common.hypothesis_settings(100) + @pytest.mark.parametrize( + "fund_execution_units_above_limit", + ("v1", pytest.param("v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE)), + ids=("plutus_v1", "plutus_v2"), + indirect=True, + ) + def test_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_execution_units_above_limit: Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp + ], + pparams: dict, + data: st.DataObject, + request: FixtureRequest, + ): + """Test spending a locked UTxO with a Plutus script with execution units above the limit. + + Expect failure. + + * fund the script address and create a UTxO for collateral + * try to spend the locked UTxO when execution units are set above the limits + * check that failed because the execution units were too big + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + amount = 2_000_000 + + script_utxos, collateral_utxos, plutus_op = fund_execution_units_above_limit + + per_time = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["steps"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_time > pparams["maxTxExecutionUnits"]["steps"] + + per_space = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["memory"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_space > pparams["maxTxExecutionUnits"]["memory"] + + fixed_cost = pparams["txFeeFixed"] + + high_execution_cost = plutus_common.ExecutionCost( + per_time=per_time, per_space=per_space, fixed_cost=fixed_cost + ) + + plutus_op = plutus_op._replace(execution_cost=high_execution_cost) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert "ExUnitsTooBigUTxO" in err_str, err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeRedeemer: + """Tests for Tx output locking using Plutus smart contracts with wrong redeemer.""" + + MIN_INT_VAL = -common.MAX_UINT64 + AMOUNT = 2_000_000 + + def _fund_script_guessing_game( + self, + cluster_manager: cluster_management.ClusterManager, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + plutus_version: str, + ) -> FundTupleT: + """Fund a Plutus script and create the locked UTxO and collateral UTxO.""" + payment_addrs = clusterlib_utils.create_payment_addr_records( + *[f"{temp_template}_payment_addr_{i}" for i in range(2)], + cluster_obj=cluster_obj, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + payment_addrs[0], + cluster_obj=cluster_obj, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster_obj, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def fund_script_guessing_game_v1( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + ) -> FundTupleT: + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + + script_utxos, collateral_utxos, payment_addrs = self._fund_script_guessing_game( + cluster_manager=cluster_manager, + cluster_obj=cluster, + temp_template=temp_template, + plutus_version="v1", + ) + fixture_cache.value = script_utxos, collateral_utxos, payment_addrs + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def fund_script_guessing_game_v2( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + ) -> FundTupleT: + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + + script_utxos, collateral_utxos, payment_addrs = self._fund_script_guessing_game( + cluster_manager=cluster_manager, + cluster_obj=cluster, + temp_template=temp_template, + plutus_version="v2", + ) + fixture_cache.value = script_utxos, collateral_utxos, payment_addrs + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def cost_per_unit( + self, + cluster: clusterlib.ClusterLib, + ) -> plutus_common.ExecutionCost: + return plutus_common.get_cost_per_unit( + protocol_params=cluster.g_query.get_protocol_params() + ) + + def _failed_tx_build( + self, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + redeemer_content: str, + dst_addr: clusterlib.AddressRecord, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + ) -> str: + """Try to build a Tx and expect failure.""" + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + outfile.write(redeemer_content) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + ) + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + return str(excinfo.value) + + def _int_out_of_range( + self, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + redeemer_value: int, + dst_addr: clusterlib.AddressRecord, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + ): + """Try to spend a locked UTxO with redeemer int value that is not in allowed range.""" + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=redeemer_file if redeemer_content else "", + redeemer_value=str(redeemer_value) if not redeemer_content else "", + ) + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + err_str = str(excinfo.value) + assert "Value out of range within the script data" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given( + redeemer_value=st.integers(min_value=MIN_INT_VAL, max_value=common.MAX_UINT64) + ) + @hypothesis.example(redeemer_value=MIN_INT_VAL) + @hypothesis.example(redeemer_value=common.MAX_UINT64) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_inside_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with an unexpected redeemer value. + + Expect failure. + """ + hypothesis.assume(redeemer_value != 42) + + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + # try to spend the "locked" UTxO + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + dst_addr = payment_addrs[1] + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=redeemer_file if redeemer_content else "", + redeemer_value=str(redeemer_value) if not redeemer_content else "", + ) + ] + tx_raw_output = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed) + + err_str = str(excinfo.value) + assert "ValidationTagMismatch (IsValid True)" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(max_value=MIN_INT_VAL - 1)) + @hypothesis.example(redeemer_value=MIN_INT_VAL - 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_bellow_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a redeemer int value < minimum allowed value. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + self._int_out_of_range( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(min_value=common.MAX_UINT64 + 1)) + @hypothesis.example(redeemer_value=common.MAX_UINT64 + 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_above_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a redeemer int value > maximum allowed value. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + self._int_out_of_range( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO with an invalid redeemer type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value.hex()}, outfile) + + # try to spend the "locked" UTxO + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + dst_addr = payment_addrs[1] + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + ) + ] + + tx_raw_output = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed) + + err_str = str(excinfo.value) + assert "ValidationTagMismatch (IsValid True)" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO using redeemer that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = json.dumps( + {"constructor": 0, "fields": [{"bytes": redeemer_value.hex()}]} + ) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert "must consist of at most 64 bytes" in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in typed format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = json.dumps({"constructor": 0, "fields": [{"int": redeemer_value.hex()}]}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "int" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"int": redeemer_value.hex()}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "int" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in typed format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"constructor": 0, "fields": [{"bytes": redeemer_value}]}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "bytes" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"bytes": redeemer_value}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "bytes" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_invalid_json( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: str, + ): + """Try to build a Tx using a redeemer value that is invalid JSON. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = f'{{"{redeemer_value}"}}' + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert "Invalid JSON format" in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_invalid_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON typed schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({redeemer_type: 42}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err + ), err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_invalid_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON untyped schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({redeemer_type: 42}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err + ), err + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeDatum: + """Tests for Tx output locking using Plutus smart contracts with wrong datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.parametrize("address_type", ("script_address", "key_address")) + @common.PARAM_PLUTUS_VERSION + def test_no_datum_txout( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + address_type: str, + plutus_version: str, + ): + """Test using UTxO without datum hash in place of locked UTxO. + + Expect failure. + + * create a Tx output without a datum hash + * try to spend the UTxO like it was locked Plutus UTxO + * check that the expected error was raised + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{address_type}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + if address_type == "script_address": + redeem_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + else: + redeem_address = payment_addrs[2].address + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + txouts = [ + clusterlib.TxOut( + address=redeem_address, amount=amount + redeem_cost.fee + FEE_REDEEM_TXSIZE + ), + clusterlib.TxOut(address=payment_addr.address, amount=redeem_cost.collateral), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + join_txouts=False, + ) + txid = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos = cluster.g_query.get_utxo(txin=f"{txid}#0") + collateral_utxos = cluster.g_query.get_utxo(txin=f"{txid}#1") + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "NonOutputSupplimentaryDatums" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_lock_tx_invalid_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: str, + plutus_version: str, + ): + """Test locking a Tx output with an invalid datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{datum_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + with pytest.raises(clusterlib.CLIError) as excinfo: + _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_tx_wrong_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output and try to spend it with a wrong datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op_1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op_1.execution_cost # for mypy + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op_1, + amount=amount, + ) + + # use a wrong datum to try to unlock the funds + plutus_op_2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_2, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "NonOutputSupplimentaryDatums" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_non_script_utxo( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend a non-script UTxO with datum as if it was script locked UTxO. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + amount_fund = 4_000_000 + amount_redeem = 2_000_000 + amount_collateral = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = plutus_common.DATUM_42_TYPED + + datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file=datum_file) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=datum_file, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + # create datum and collateral UTxOs + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount_fund, + datum_hash=datum_hash, + ), + clusterlib.TxOut( + address=payment_addr.address, + amount=amount_collateral, + ), + ] + tx_files_fund = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_raw_output = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files_fund, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output) + datum_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=dst_addr.address, datum_hash=datum_hash + )[0] + collateral_utxos = clusterlib.filter_utxos( + utxos=out_utxos, address=payment_addr.address, utxo_ix=datum_utxo.utxo_ix + 1 + ) + assert ( + datum_utxo.datum_hash == datum_hash + ), f"UTxO should have datum hash '{datum_hash}': {datum_utxo}" + + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file, dst_addr.skey_file] + ) + + # try to spend the "locked" UTxO + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=[datum_utxo], + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_redeem, + tx_files=tx_files_redeem, + ) + + err_str = str(excinfo.value) + assert "ExtraneousScriptWitnessesUTXOW" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: bytes, + plutus_version: str, + ): + """Try to lock a UTxO with datum that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": datum_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + with pytest.raises(clusterlib.CLIError) as excinfo: + _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + +@pytest.mark.testnets +class TestCompatibility: + """Tests for checking compatibility with previous Tx eras.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era > VERSIONS.ALONZO, + reason="runs only with Tx era <= Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv2_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV2 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v2"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v2"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV2 is not supported" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era >= VERSIONS.ALONZO, + reason="runs only with Tx era < Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv1_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV1 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v1"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v1"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV1 is not supported" in err_str, err_str diff --git a/cardano_node_tests/tests/test_plutus/test_spend_datum_build.py b/cardano_node_tests/tests/test_plutus/test_spend_datum_build.py new file mode 100644 index 000000000..798b2c522 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_spend_datum_build.py @@ -0,0 +1,2957 @@ +"""Tests for spending with Plutus using `transaction build`.""" +import json +import logging +import shutil +from pathlib import Path +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from cardano_clusterlib import clusterlib +from cardano_clusterlib import txtools + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + common.SKIPIF_BUILD_UNUSABLE, + pytest.mark.smoke, +] + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment addresses.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(3)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=1_000_000_000, + ) + + return addrs + + +@pytest.fixture +def pool_users( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.PoolUser]: + """Create new pool users.""" + test_id = common.get_test_id(cluster) + created_users = clusterlib_utils.create_pool_users( + cluster_obj=cluster, + name_template=f"{test_id}_pool_users", + no_of_addr=2, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + created_users[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return created_users + + +def _build_fund_script( + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + plutus_op: plutus_common.PlutusOp, + tokens: Optional[List[plutus_common.Token]] = None, # tokens must already be in `payment_addr` + tokens_collateral: Optional[ + List[plutus_common.Token] + ] = None, # tokens must already be in `payment_addr` + embed_datum: bool = False, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund a Plutus script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + assert plutus_op.execution_cost # for mypy + + script_fund = 200_000_000 + + stokens = tokens or () + ctokens = tokens_collateral or () + + script_address = cluster_obj.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + ) + + # create a Tx output with a datum hash at the script address + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + + script_txout = plutus_common.txout_factory( + address=script_address, + amount=script_fund, + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + txouts = [ + script_txout, + # for collateral + clusterlib.TxOut(address=dst_addr.address, amount=redeem_cost.collateral), + ] + + for token in stokens: + txouts.append(script_txout._replace(amount=token.amount, coin=token.coin)) + + for token in ctokens: + txouts.append( + clusterlib.TxOut( + address=dst_addr.address, + amount=token.amount, + coin=token.coin, + ) + ) + + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files, + txouts=txouts, + fee_buffer=2_000_000, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster_obj.g_query.get_utxo(tx_raw_output=tx_output) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset(utxos=out_utxos, txouts=tx_output.txouts) + + script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + assert script_utxos, "No script UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos) == script_fund + ), f"Incorrect balance for script address `{script_address}`" + + collateral_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + assert collateral_utxos, "No collateral UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos) == redeem_cost.collateral + ), f"Incorrect balance for collateral address `{dst_addr.address}`" + + for token in stokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos, coin=token.coin) == token.amount + ), f"Incorrect token balance for script address `{script_address}`" + + for token in ctokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos, coin=token.coin) + == token.amount + ), f"Incorrect token balance for address `{dst_addr.address}`" + + if VERSIONS.transaction_era >= VERSIONS.ALONZO: + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_output) + + return script_utxos, collateral_utxos, tx_output + + +def _build_spend_locked_txin( # noqa: C901 + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + plutus_op: plutus_common.PlutusOp, + amount: int, + deposit_amount: int = 0, + txins: clusterlib.OptionalUTXOData = (), + tx_files: Optional[clusterlib.TxFiles] = None, + invalid_hereafter: Optional[int] = None, + invalid_before: Optional[int] = None, + tokens: Optional[List[plutus_common.Token]] = None, + expect_failure: bool = False, + script_valid: bool = True, + submit_tx: bool = True, +) -> Tuple[str, Optional[clusterlib.TxRawOutput], list]: + """Spend the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + # pylint: disable=too-many-arguments,too-many-locals + tx_files = tx_files or clusterlib.TxFiles() + spent_tokens = tokens or () + + # Change that was calculated manually will be returned to address of the first script. + # The remaining change that is automatically handled by the `build` command will be returned + # to `payment_addr`, because it would be inaccessible on script address without proper + # datum hash (datum hash is not provided for change that is handled by `build` command). + script_change_rec = script_utxos[0] + + # spend the "locked" UTxO + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + datum_value=plutus_op.datum_value if plutus_op.datum_value else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file if plutus_op.redeemer_cbor_file else "", + redeemer_value=plutus_op.redeemer_value if plutus_op.redeemer_value else "", + ) + ] + tx_files = tx_files._replace( + signing_key_files=list({*tx_files.signing_key_files, dst_addr.skey_file}), + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + + lovelace_change_needed = False + for token in spent_tokens: + txouts.append( + clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin) + ) + # append change + script_token_balance = clusterlib.calculate_utxos_balance( + utxos=script_utxos, coin=token.coin + ) + if script_token_balance > token.amount: + lovelace_change_needed = True + txouts.append( + clusterlib.TxOut( + address=script_change_rec.address, + amount=script_token_balance - token.amount, + coin=token.coin, + datum_hash=script_change_rec.datum_hash, + ) + ) + # add minimum (+ some) required Lovelace to change Tx output + if lovelace_change_needed: + txouts.append( + clusterlib.TxOut( + address=script_change_rec.address, + amount=4_000_000, + coin=clusterlib.DEFAULT_COIN, + datum_hash=script_change_rec.datum_hash, + ) + ) + + if expect_failure: + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=txins, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + ) + return str(excinfo.value), None, [] + + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=txins, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + deposit=deposit_amount, + script_valid=script_valid, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + if not submit_tx: + return "", tx_output, [] + + dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address) + + script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN] + + if not script_valid: + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=collateral_utxos) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) + == dst_init_balance - collateral_utxos[0].amount + ), f"Collateral was NOT spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return "", tx_output, [] + + # calculate cost of Plutus script + plutus_costs = cluster_obj.g_transaction.calculate_plutus_script_cost( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + deposit=deposit_amount, + script_valid=script_valid, + ) + + cluster_obj.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_output.script_txins if t.txins] + ) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + for token in spent_tokens: + script_utxos_token = [u for u in script_utxos if u.coin == token.coin] + for u in script_utxos_token: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[token.coin] + ), f"Token inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster_obj, tx_raw_output=tx_output) + + tx_db_record = dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_output) + # compare cost of Plutus script with data from db-sync + if tx_db_record: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_db_record.redeemers, cost_records=plutus_costs + ) + + return "", tx_output, plutus_costs + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestBuildLocking: + """Tests for Tx output locking using Plutus smart contracts and `transaction build`.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Corresponds to Exercise 3 for Alonzo Blue. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + __, tx_output, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 170_782 + assert tx_output and helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + def test_context_equivalence( + self, + cluster: clusterlib.ClusterLib, + pool_users: List[clusterlib.PoolUser], + ): + """Test context equivalence while spending a locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * spend the locked UTxO using the derived redeemer + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 10_000_000 + deposit_amount = cluster.g_query.get_address_deposit() + + # create stake address registration cert + stake_addr_reg_cert_file = cluster.g_stake_address.gen_stake_addr_registration_cert( + addr_name=f"{temp_template}_addr2", + stake_vkey_file=pool_users[0].stake.vkey_file, + ) + + tx_files = clusterlib.TxFiles(certificate_files=[stake_addr_reg_cert_file]) + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_op_dummy = plutus_common.PlutusOp( + script_file=plutus_common.CONTEXT_EQUIVALENCE_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=redeemer_file_dummy, + execution_cost=plutus_common.CONTEXT_EQUIVALENCE_COST, + ) + + # fund the script address + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + plutus_op=plutus_op_dummy, + ) + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + __, tx_output_dummy, __ = _build_spend_locked_txin( + temp_template=f"{temp_template}_dummy", + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_dummy, + amount=amount, + deposit_amount=deposit_amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + submit_tx=False, + ) + assert tx_output_dummy + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + tx_file_dummy = Path(f"{tx_output_dummy.out_file.with_suffix('')}.signed") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_op = plutus_op_dummy._replace(redeemer_file=redeemer_file) + + __, tx_output, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + deposit_amount=deposit_amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + # check expected fees + if tx_output: + expected_fee = 372_438 + assert helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("embed_datum", (True, False), ids=("embedded_datum", "datum")) + @pytest.mark.parametrize( + "variant", + ("typed_json", "typed_cbor", "untyped_value", "untyped_json", "untyped_cbor"), + ) + @common.PARAM_PLUTUS_VERSION + def test_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + embed_datum: bool, + request: FixtureRequest, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "guessing game" scripts that expect specific datum and redeemer value. + Test both typed and untyped redeemer and datum. + Test passing datum and redeemer to `cardano-cli` as value, json file and cbor file. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + + datum_file: Optional[Path] = None + datum_cbor_file: Optional[Path] = None + datum_value: Optional[str] = None + redeemer_file: Optional[Path] = None + redeemer_cbor_file: Optional[Path] = None + redeemer_value: Optional[str] = None + + if variant == "typed_json": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "typed_cbor": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_TYPED_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_TYPED_CBOR + elif variant == "untyped_value": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_value = "42" + redeemer_value = "42" + elif variant == "untyped_json": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_file = plutus_common.DATUM_42 + redeemer_file = plutus_common.REDEEMER_42 + elif variant == "untyped_cbor": # noqa: SIM106 + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + else: + raise AssertionError("Unknown test variant.") + + execution_cost = plutus_common.GUESSING_GAME[plutus_version].execution_cost + if script_file == plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file: + execution_cost = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost + + plutus_op = plutus_common.PlutusOp( + script_file=script_file, + datum_file=datum_file, + datum_cbor_file=datum_cbor_file, + datum_value=datum_value, + redeemer_file=redeemer_file, + redeemer_cbor_file=redeemer_cbor_file, + redeemer_value=redeemer_value, + execution_cost=execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + __, __, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v1_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("plutus_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + script_fund = 200_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + script_file1_v1 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V1 + execution_cost1_v1 = plutus_common.ALWAYS_SUCCEEDS_COST + script_file2_v1 = plutus_common.GUESSING_GAME_PLUTUS_V1 + # this is higher than `plutus_common.GUESSING_GAME_COST`, because the script + # context has changed to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=388_458_303, per_space=1_031_312, fixed_cost=87_515 + ) + else: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=280_668_068, per_space=1_031_312, fixed_cost=79_743 + ) + + script_file1_v2 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V2 + execution_cost1_v2 = plutus_common.ALWAYS_SUCCEEDS_V2_COST + script_file2_v2 = plutus_common.GUESSING_GAME_PLUTUS_V2 + execution_cost2_v2 = plutus_common.ExecutionCost( + per_time=208_314_784, + per_space=662_274, + fixed_cost=53_233, + ) + + expected_fee_fund = 174_389 + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + expected_fee_redeem = 378_768 + elif plutus_version == "mix_v1_v2": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + expected_fee_redeem = 321_739 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + expected_fee_redeem = 378_584 + elif plutus_version == "plutus_v2": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + expected_fee_redeem = 321_378 + else: + raise AssertionError("Unknown test variant.") + + plutus_op1 = plutus_common.PlutusOp( + script_file=script_file1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=execution_cost1, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=script_file2, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_TYPED_CBOR, + execution_cost=execution_cost2, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=script_fund, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=script_fund, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + tx_output_fund = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files_fund, + txouts=txouts_fund, + fee_buffer=2_000_000, + join_txouts=False, + ) + tx_signed_fund = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_fund.out_file, + signing_key_files=tx_files_fund.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + + cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) + + fund_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=fund_utxos, txouts=tx_output_fund.txouts + ) + script_utxos1 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset) + script_utxos2 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 1) + collateral_utxos1 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 2) + collateral_utxos2 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 3) + + assert script_utxos1 and script_utxos2, "No script UTxOs" + assert collateral_utxos1 and collateral_utxos2, "No collateral UTxOs" + + assert ( + script_utxos1[0].amount == script_fund + ), f"Incorrect balance for script address `{script_utxos1[0].address}`" + assert ( + script_utxos2[0].amount == script_fund + ), f"Incorrect balance for script address `{script_utxos2[0].address}`" + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + tx_output_redeem = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + # calculate cost of Plutus script + plutus_costs = cluster.g_transaction.calculate_plutus_script_cost( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + dst_init_balance = cluster.g_query.get_address_balance(payment_addrs[1].address) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed_redeem, + txins=[t.txins[0] for t in tx_output_redeem.script_txins if t.txins], + ) + + assert ( + cluster.g_query.get_address_balance(payment_addrs[1].address) + == dst_init_balance + amount * 2 + ), f"Incorrect balance for destination address `{payment_addrs[1].address}`" + + script_utxos_lovelace = [ + u for u in [*script_utxos1, *script_utxos2] if u.coin == clusterlib.DEFAULT_COIN + ] + for u in script_utxos_lovelace: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + # check expected fees + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + assert helpers.is_in_interval(tx_output_redeem.fee, expected_fee_redeem, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost1, execution_cost2], + ) + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + # check transactions in db-sync + tx_redeem_record = dbsync_utils.check_tx( + cluster_obj=cluster, tx_raw_output=tx_output_redeem + ) + if tx_redeem_record: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_redeem_record.redeemers, cost_records=plutus_costs + ) + + @allure.link(helpers.get_vcs_link()) + def test_always_fails( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the expected error was raised + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err, __, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + expect_failure=True, + ) + assert "The Plutus script evaluation failed" in err, err + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + def test_script_invalid( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test failing script together with the `--script-invalid` argument - collateral is taken. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was spent + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + # include any payment txin + txins = [ + r + for r in cluster.g_query.get_utxo( + address=payment_addrs[0].address, coins=[clusterlib.DEFAULT_COIN] + ) + if not (r.datum_hash or r.inline_datum_hash) + ][:1] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + try: + __, tx_output, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + txins=txins, + tx_files=tx_files, + script_valid=False, + ) + except clusterlib.CLIError as err: + # TODO: broken on node 1.35.0 and 1.35.1 + if "ScriptWitnessIndexTxIn 0 is missing from the execution units" in str(err): + pytest.xfail("See cardano-node issue #4013") + else: + raise + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + if tx_output: + expected_fee = 171_309 + assert helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_token_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO + * check that the expected amounts of Lovelace and native tokens were spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=100, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens=tokens_rec, + ) + + __, tx_output_spend, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + tokens=tokens_rec, + ) + + # check expected fees + expected_fee_fund = 173_597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 175_710 + assert tx_output_spend and helpers.is_in_interval( + tx_output_spend.fee, expected_fee, frac=0.15 + ) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_partial_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending part of funds (Lovelace and native tokens) on a locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO and create new locked UTxO with change + * check that the expected amounts of Lovelace and native tokens were spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + token_rand = clusterlib.get_rand_str(5) + + amount_spend = 10_000_000 + token_amount_fund = 100 + token_amount_spend = 20 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount_fund, + ) + tokens_fund_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens=tokens_fund_rec, + ) + + tokens_spend_rec = [ + plutus_common.Token(coin=t.token, amount=token_amount_spend) for t in tokens + ] + + __, tx_output_spend, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_spend, + tokens=tokens_spend_rec, + ) + + # check that the expected amounts of Lovelace and native tokens were spent and change UTxOs + # with appropriate datum hash were created + + assert tx_output_spend + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_spend) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_spend.txouts + ) + + # UTxO we created for tokens and minimum required Lovelace + change_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + # UTxO that was created by `build` command for rest of the Lovelace change (this will not + # have the script's datum) + # TODO: change UTxO used to be first, now it's last + build_change_utxo = out_utxos[0] if utxo_ix_offset else out_utxos[-1] + + # Lovelace balance on original script UTxOs + script_lovelace_balance = clusterlib.calculate_utxos_balance(utxos=script_utxos) + # Lovelace balance on change UTxOs + change_lovelace_balance = clusterlib.calculate_utxos_balance( + utxos=[*change_utxos, build_change_utxo] + ) + + assert ( + change_lovelace_balance == script_lovelace_balance - tx_output_spend.fee - amount_spend + ) + + token_amount_exp = token_amount_fund - token_amount_spend + assert len(change_utxos) == len(tokens_spend_rec) + 1 + for u in change_utxos: + if u.coin != clusterlib.DEFAULT_COIN: + assert u.amount == token_amount_exp + assert u.datum_hash == script_utxos[0].datum_hash + + # check expected fees + expected_fee_fund = 173_597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 183_366 + assert tx_output_spend and helpers.is_in_interval( + tx_output_spend.fee, expected_fee, frac=0.15 + ) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_is_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using single UTxO for both collateral and Tx input. + + Uses `cardano-cli transaction build` command for building the transactions. + + Tests bug https://github.com/input-output-hk/cardano-db-sync/issues/750 + + * create a Tx output with a datum hash at the script address and a collateral UTxO + * check that the expected amount was locked at the script address + * spend the locked UTxO while using the collateral UTxO both as collateral and as + normal Tx input + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + # Step 1: fund the script address + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_step1 = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + ) + + # Step 2: spend the "locked" UTxO + + script_address = script_utxos[0].address + + dst_step1_balance = cluster.g_query.get_address_balance(dst_addr.address) + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file + if plutus_op.redeemer_cbor_file + else "", + ) + ] + tx_files = clusterlib.TxFiles( + signing_key_files=[dst_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + + tx_output_step2 = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + # `collateral_utxos` is used both as collateral and as normal Tx input + txins=collateral_utxos, + txouts=txouts, + script_txins=plutus_txins, + change_address=script_address, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_step2.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_output_step2.script_txins if t.txins] + ) + + assert ( + cluster.g_query.get_address_balance(dst_addr.address) + == dst_step1_balance + amount - collateral_utxos[0].amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{script_address}`" + + # check expected fees + expected_fee_step1 = 168_845 + assert helpers.is_in_interval(tx_output_step1.fee, expected_fee_step1, frac=0.15) + + expected_fee_step2 = 176_986 + assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) + + 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) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestDatum: + """Tests for datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + def test_datum_on_key_credential_address( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test creating UTxO with datum on address with key credentials (non-script address). + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount, + datum_hash_file=plutus_common.DATUM_42_TYPED, + ) + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=temp_template, + tx_files=tx_files, + txouts=txouts, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=temp_template, + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output) + datum_utxo = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0] + assert datum_utxo.datum_hash, f"UTxO should have datum hash: {datum_utxo}" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output) + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_embed_datum_without_pparams( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test 'build --tx-out-datum-embed' without providing protocol params file.""" + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + ) + + script_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + utxos = cluster.g_query.get_utxo(address=payment_addrs[0].address) + txin = txtools.filter_utxo_with_highest_amount(utxos=utxos) + + out_file = f"{temp_template}_tx.body" + + cli_args = [ + "transaction", + "build", + "--tx-in", + f"{txin.utxo_hash}#{txin.utxo_ix}", + "--tx-out", + f"{script_address}+2000000", + "--tx-out-datum-embed-file", + str(plutus_op.datum_file), + "--change-address", + payment_addrs[0].address, + "--out-file", + out_file, + "--testnet-magic", + str(cluster.network_magic), + *cluster.g_transaction.tx_era_arg, + ] + + cluster.cli(cli_args) + + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_signed", + ) + + try: + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=[txin]) + except clusterlib.CLIError as err: + if "PPViewHashesDontMatch" in str(err): + pytest.xfail("build cmd requires protocol params - see node issue #4058") + raise + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegative: + """Tests for Tx output locking using Plutus smart contracts that are expected to fail.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_w_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while collateral contains native tokens. + + Expect failure. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a collateral UTxO with native tokens + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + payment_addr = payment_addrs[0] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addr, + issuer_addr=payment_addr, + amount=100, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens_collateral=tokens_rec, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "CollateralContainsNonADA" in err_str, err_str + + # check expected fees + expected_fee_fund = 173597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_same_collateral_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using the same UTxO as collateral. + + Expect failure. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO while using the same UTxO as collateral + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, __, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=script_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert ( + "expected to be key witnessed but are actually script witnessed: " + f'["{script_utxos[0].utxo_hash}#{script_utxos[0].utxo_ix}"]' in err_str + # in 1.35.3 and older + or "Expected key witnessed collateral" in err_str + ), err_str + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "variant", + ( + "42_43", # correct datum, wrong redeemer + "43_42", # wrong datum, correct redeemer + "43_43", # wrong datum and redeemer + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_invalid_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "guessing game" script that expects specific datum and redeemer value. + Test negative scenarios where datum or redeemer value is different than expected. + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was not spent + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{variant}" + + if variant == "42_43": + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + elif variant == "43_42": + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "43_43": # noqa: SIM106 + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + else: + raise AssertionError("Unknown test variant.") + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME[plutus_version].script_file, + datum_file=datum_file, + redeemer_file=redeemer_file, + execution_cost=plutus_common.GUESSING_GAME[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_two_scripts_spending_one_fail( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx, one fails. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script addresses + * try to spend the locked UTxOs + * check that the expected error was raised + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 50_000_000 + + script_fund = 200_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + plutus_op1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=script_fund, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=script_fund, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + tx_output_fund = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files_fund, + txouts=txouts_fund, + fee_buffer=2_000_000, + join_txouts=False, + ) + tx_signed_fund = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_fund.out_file, + signing_key_files=tx_files_fund.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + + cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_fund.txouts + ) + + script_utxos1 = clusterlib.filter_utxos( + utxos=out_utxos, utxo_ix=utxo_ix_offset, coin=clusterlib.DEFAULT_COIN + ) + script_utxos2 = clusterlib.filter_utxos( + utxos=out_utxos, utxo_ix=utxo_ix_offset + 1, coin=clusterlib.DEFAULT_COIN + ) + collateral_utxos1 = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 2) + collateral_utxos2 = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 3) + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeRedeemer: + """Tests for Tx output locking using Plutus smart contracts with wrong redeemer.""" + + MIN_INT_VAL = -common.MAX_UINT64 + AMOUNT = 2_000_000 + + @pytest.fixture + def fund_script_guessing_game_v1( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]]: + """Fund a PlutusV1 script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED_PLUTUS_V1, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED_COST, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + return script_utxos, collateral_utxos + + @pytest.fixture + def fund_script_guessing_game_v2( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]]: + """Fund a PlutusV2 script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED_PLUTUS_V2, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED_V2_COST, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + return script_utxos, collateral_utxos + + def _int_out_of_range( + self, + cluster: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + redeemer_value: int, + plutus_version: str, + ): + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file) if redeemer_content else None, + redeemer_value=None if redeemer_content else str(redeemer_value), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "Value out of range within the script data" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given( + redeemer_value=st.integers(min_value=MIN_INT_VAL, max_value=common.MAX_UINT64) + ) + @hypothesis.example(redeemer_value=MIN_INT_VAL) + @hypothesis.example(redeemer_value=common.MAX_UINT64) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_inside_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value that is in the valid range. + + Expect failure. + """ + hypothesis.assume(redeemer_value != 42) + + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file) if redeemer_content else None, + redeemer_value=None if redeemer_content else str(redeemer_value), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(min_value=common.MAX_UINT64 + 1)) + @hypothesis.example(redeemer_value=common.MAX_UINT64 + 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_above_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value, above max value allowed. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + self._int_out_of_range( + cluster=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(max_value=MIN_INT_VAL - 1)) + @hypothesis.example(redeemer_value=MIN_INT_VAL - 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_bellow_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value, bellow min value allowed. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + self._int_out_of_range( + cluster=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO with a wrong redeemer type, try to use bytes. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value.hex()}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "Script debugging logs: Incorrect datum. Expected 42." in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO using redeemer that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": redeemer_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in typed format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"int": redeemer_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "int" does not have the type required by the schema.' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"int": redeemer_value.hex()}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "int" does not have the type required by the schema.' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in typed format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": redeemer_value}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "bytes" does not have the type required by the schema.' + in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "bytes" does not have the type required by the schema.' + in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_invalid_json( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: str, + ): + """Try to build a Tx using a redeemer value that is invalid JSON. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{redeemer_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_invalid_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON typed schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{redeemer_type: 42}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_invalid_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON untyped schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({redeemer_type: 42}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err_str + ), err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeDatum: + """Tests for Tx output locking using Plutus smart contracts with wrong datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.parametrize("address_type", ("script_address", "key_address")) + @common.PARAM_PLUTUS_VERSION + def test_no_datum_txout( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + address_type: str, + plutus_version: str, + ): + """Test using UTxO without datum hash in place of locked UTxO. + + Expect failure. + + * create a Tx output without a datum hash + * try to spend the UTxO like it was locked Plutus UTxO + * check that the expected error was raised + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{address_type}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + if address_type == "script_address": + redeem_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + else: + redeem_address = payment_addrs[2].address + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + txouts = [ + clusterlib.TxOut(address=redeem_address, amount=amount + redeem_cost.fee), + clusterlib.TxOut(address=payment_addr.address, amount=redeem_cost.collateral), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + join_txouts=False, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_fund.txouts + ) + + script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + collateral_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + + if address_type == "script_address": + assert "txin does not have a script datum" in err_str, err_str + else: + assert ( + "not a Plutus script witnessed tx input" in err_str + or "points to a script hash that is not known" in err_str + ), err_str + + # check expected fees + expected_fee_fund = 199_087 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_lock_tx_invalid_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: str, + plutus_version: str, + ): + """Test locking a Tx output with an invalid datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{datum_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_tx_wrong_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output and try to spend it with a wrong datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op_1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op_1, + ) + + # use a wrong datum to try to unlock the funds + plutus_op_2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_2, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert ( + "The Plutus script witness has the wrong datum (according to the UTxO)." in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_non_script_utxo( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend a non-script UTxO with datum as if it was script locked UTxO. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + amount_fund = 4_000_000 + amount_redeem = 2_000_000 + amount_collateral = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = plutus_common.DATUM_42_TYPED + + datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file=datum_file) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=datum_file, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + # create datum and collateral UTxOs + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount_fund, + datum_hash=datum_hash, + ), + clusterlib.TxOut( + address=payment_addr.address, + amount=amount_collateral, + ), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=temp_template, + tx_files=tx_files, + txouts=txouts, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=temp_template, + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output) + datum_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=dst_addr.address, datum_hash=datum_hash + )[0] + collateral_utxos = clusterlib.filter_utxos( + utxos=out_utxos, address=payment_addr.address, utxo_ix=datum_utxo.utxo_ix + 1 + ) + assert ( + datum_utxo.datum_hash == datum_hash + ), f"UTxO should have datum hash '{datum_hash}': {datum_utxo}" + + # try to spend the "locked" UTxO + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=[datum_utxo], + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_redeem, + ) + + err_str = str(excinfo.value) + assert "points to a script hash that is not known" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: bytes, + plutus_version: str, + ): + """Try to lock a UTxO with datum that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": datum_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + +@pytest.mark.testnets +class TestCompatibility: + """Tests for checking compatibility with previous Tx eras.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era > VERSIONS.ALONZO, + reason="runs only with Tx era <= Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv2_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV2 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v2"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v2"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV2 is not supported" in err_str, err_str diff --git a/cardano_node_tests/tests/test_plutus/test_spend_datum_raw.py b/cardano_node_tests/tests/test_plutus/test_spend_datum_raw.py new file mode 100644 index 000000000..a264ae894 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_spend_datum_raw.py @@ -0,0 +1,3052 @@ +"""Tests for spending with Plutus using `transaction build-raw`.""" +import itertools +import json +import logging +import shutil +import time +from pathlib import Path +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import SubRequest +from cardano_clusterlib import clusterlib + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import logfiles +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + pytest.mark.smoke, +] + + +FundTupleT = Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], List[clusterlib.AddressRecord] +] + +# approx. fee for Tx size +FEE_REDEEM_TXSIZE = 400_000 + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment addresses.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(3)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return addrs + + +@pytest.fixture +def pool_users( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.PoolUser]: + """Create new pool users.""" + test_id = common.get_test_id(cluster) + created_users = clusterlib_utils.create_pool_users( + cluster_obj=cluster, + name_template=f"{test_id}_pool_users", + no_of_addr=2, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + created_users[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return created_users + + +def _fund_script( + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + plutus_op: plutus_common.PlutusOp, + amount: int, + fee_txsize: int = FEE_REDEEM_TXSIZE, + deposit_amount: int = 0, + tokens: Optional[List[plutus_common.Token]] = None, # tokens must already be in `payment_addr` + tokens_collateral: Optional[ + List[plutus_common.Token] + ] = None, # tokens must already be in `payment_addr` + collateral_fraction_offset: float = 1.0, + embed_datum: bool = False, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund a Plutus script and create the locked UTxO and collateral UTxO.""" + # pylint: disable=too-many-locals,too-many-arguments + assert plutus_op.execution_cost # for mypy + + stokens = tokens or () + ctokens = tokens_collateral or () + + script_address = cluster_obj.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + collateral_fraction_offset=collateral_fraction_offset, + ) + + # create a Tx output with a datum hash at the script address + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + + script_txout = plutus_common.txout_factory( + address=script_address, + amount=amount + redeem_cost.fee + fee_txsize + deposit_amount, + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + txouts = [ + script_txout, + # for collateral + clusterlib.TxOut(address=dst_addr.address, amount=redeem_cost.collateral), + ] + + for token in stokens: + txouts.append(script_txout._replace(amount=token.amount, coin=token.coin)) + + for token in ctokens: + txouts.append( + clusterlib.TxOut( + address=dst_addr.address, + amount=token.amount, + coin=token.coin, + ) + ) + + tx_raw_output = cluster_obj.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + txouts=txouts, + tx_files=tx_files, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + ) + + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + script_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#0") + assert script_utxos, "No script UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos) == txouts[0].amount + ), f"Incorrect balance for script address `{script_address}`" + + collateral_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#1") + assert collateral_utxos, "No collateral UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos) == redeem_cost.collateral + ), f"Incorrect balance for collateral address `{dst_addr.address}`" + + for token in stokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos, coin=token.coin) == token.amount + ), f"Incorrect token balance for script address `{script_address}`" + + for token in ctokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos, coin=token.coin) + == token.amount + ), f"Incorrect token balance for address `{dst_addr.address}`" + + if VERSIONS.transaction_era >= VERSIONS.ALONZO: + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + return script_utxos, collateral_utxos, tx_raw_output + + +def _spend_locked_txin( # noqa: C901 + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + dst_addr: clusterlib.AddressRecord, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + plutus_op: plutus_common.PlutusOp, + amount: int, + fee_txsize: int = FEE_REDEEM_TXSIZE, + txins: clusterlib.OptionalUTXOData = (), + tx_files: Optional[clusterlib.TxFiles] = None, + invalid_hereafter: Optional[int] = None, + invalid_before: Optional[int] = None, + tokens: Optional[List[plutus_common.Token]] = None, + expect_failure: bool = False, + script_valid: bool = True, + submit_tx: bool = True, +) -> Tuple[str, clusterlib.TxRawOutput]: + """Spend the locked UTxO.""" + # pylint: disable=too-many-arguments,too-many-locals + assert plutus_op.execution_cost + + tx_files = tx_files or clusterlib.TxFiles() + spent_tokens = tokens or () + + # change will be returned to address of the first script + change_rec = script_utxos[0] + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + ) + + script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN] + script_lovelace_balance = clusterlib.calculate_utxos_balance( + utxos=[*script_utxos_lovelace, *txins] + ) + + # spend the "locked" UTxO + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + execution_units=(plutus_op.execution_cost.per_time, plutus_op.execution_cost.per_space), + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + datum_value=plutus_op.datum_value if plutus_op.datum_value else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file if plutus_op.redeemer_cbor_file else "", + redeemer_value=plutus_op.redeemer_value if plutus_op.redeemer_value else "", + ) + ] + + tx_files = tx_files._replace( + signing_key_files=list({*tx_files.signing_key_files, dst_addr.skey_file}), + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + # append change + if script_lovelace_balance > amount + redeem_cost.fee + fee_txsize: + txouts.append( + clusterlib.TxOut( + address=change_rec.address, + amount=script_lovelace_balance - amount - redeem_cost.fee - fee_txsize, + datum_hash=change_rec.datum_hash, + ) + ) + + for token in spent_tokens: + txouts.append( + clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin) + ) + # append change + script_token_balance = clusterlib.calculate_utxos_balance( + utxos=script_utxos, coin=token.coin + ) + if script_token_balance > token.amount: + txouts.append( + clusterlib.TxOut( + address=change_rec.address, + amount=script_token_balance - token.amount, + coin=token.coin, + datum_hash=change_rec.datum_hash, + ) + ) + + tx_raw_output = cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=txins, + txouts=txouts, + tx_files=tx_files, + fee=redeem_cost.fee + fee_txsize, + script_txins=plutus_txins, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + script_valid=script_valid, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + if not submit_tx: + return "", tx_raw_output + + dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address) + + if not script_valid: + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=collateral_utxos) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) + == dst_init_balance - collateral_utxos[0].amount + ), f"Collateral was NOT spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return "", tx_raw_output + + if expect_failure: + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed) + err = str(excinfo.value) + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + ), f"Collateral was spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return err, tx_raw_output + + cluster_obj.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_raw_output.script_txins if t.txins] + ) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + for token in spent_tokens: + script_utxos_token = [u for u in script_utxos if u.coin == token.coin] + for u in script_utxos_token: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[token.coin] + ), f"Token inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + return "", tx_raw_output + + +def _check_pretty_utxo( + cluster_obj: clusterlib.ClusterLib, tx_raw_output: clusterlib.TxRawOutput +) -> str: + """Check that pretty printed `query utxo` output looks as expected.""" + err = "" + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + utxo_out = ( + cluster_obj.cli( + [ + "query", + "utxo", + "--tx-in", + f"{txid}#0", + *cluster_obj.magic_args, + ] + ) + .stdout.decode("utf-8") + .split() + ) + + cluster_era = VERSIONS.cluster_era_name.title() + datum_hash = clusterlib_utils.datum_hash_from_txout( + cluster_obj=cluster_obj, txout=tx_raw_output.txouts[0] + ) + expected_out = [ + "TxHash", + "TxIx", + "Amount", + "--------------------------------------------------------------------------------------", + txid, + "0", + str(tx_raw_output.txouts[0].amount), + tx_raw_output.txouts[0].coin, + "+", + "TxOutDatumHash", + f"ScriptDataIn{cluster_era}Era", + f'"{datum_hash}"', + ] + + if utxo_out != expected_out: + err = f"Pretty UTxO output doesn't match expected output:\n{utxo_out}\nvs\n{expected_out}" + + return err + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestLocking: + """Tests for Tx output locking using Plutus smart contracts.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Corresponds to Exercise 3 for Alonzo Blue. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + utxo_err = _check_pretty_utxo(cluster_obj=cluster, tx_raw_output=tx_output_fund) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + if utxo_err: + pytest.fail(utxo_err) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + def test_context_equivalence( + self, + cluster: clusterlib.ClusterLib, + pool_users: List[clusterlib.PoolUser], + ): + """Test context equivalence while spending a locked UTxO. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * spend the locked UTxO using the derived redeemer + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 10_000_000 + deposit_amount = cluster.g_query.get_address_deposit() + + # create stake address registration cert + stake_addr_reg_cert_file = cluster.g_stake_address.gen_stake_addr_registration_cert( + addr_name=f"{temp_template}_addr0", + stake_vkey_file=pool_users[0].stake.vkey_file, + ) + + tx_files = clusterlib.TxFiles(certificate_files=[stake_addr_reg_cert_file]) + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_op_dummy = plutus_common.PlutusOp( + script_file=plutus_common.CONTEXT_EQUIVALENCE_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=redeemer_file_dummy, + execution_cost=plutus_common.CONTEXT_EQUIVALENCE_COST, + ) + + # fund the script address + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + plutus_op=plutus_op_dummy, + amount=amount, + deposit_amount=deposit_amount, + ) + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + __, tx_output_dummy = _spend_locked_txin( + temp_template=f"{temp_template}_dummy", + cluster_obj=cluster, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_dummy, + amount=amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + submit_tx=False, + ) + assert tx_output_dummy + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + tx_file_dummy = Path(f"{tx_output_dummy.out_file.with_suffix('')}.signed") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_op = plutus_op_dummy._replace(redeemer_file=redeemer_file) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("embed_datum", (True, False), ids=("embedded_datum", "datum")) + @pytest.mark.parametrize( + "variant", + ( + "typed_json", + "typed_cbor", + "untyped_value", + "untyped_json", + "untyped_cbor", + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + request: FixtureRequest, + embed_datum: bool, + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "guessing game" scripts that expect specific datum and redeemer value. + Test both typed and untyped redeemer and datum. + Test passing datum and redeemer to `cardano-cli` as value, json file and cbor file. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + amount = 2_000_000 + + datum_file: Optional[Path] = None + datum_cbor_file: Optional[Path] = None + datum_value: Optional[str] = None + redeemer_file: Optional[Path] = None + redeemer_cbor_file: Optional[Path] = None + redeemer_value: Optional[str] = None + + if variant == "typed_json": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "typed_cbor": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_TYPED_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_TYPED_CBOR + elif variant == "untyped_value": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_value = "42" + redeemer_value = "42" + elif variant == "untyped_json": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_file = plutus_common.DATUM_42 + redeemer_file = plutus_common.REDEEMER_42 + elif variant == "untyped_cbor": # noqa: SIM106 + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + else: + raise AssertionError("Unknown test variant.") + + execution_cost = plutus_common.GUESSING_GAME[plutus_version].execution_cost + if script_file == plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file: + execution_cost = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost + + plutus_op = plutus_common.PlutusOp( + script_file=script_file, + datum_file=datum_file, + datum_cbor_file=datum_cbor_file, + datum_value=datum_value, + redeemer_file=redeemer_file, + redeemer_cbor_file=redeemer_cbor_file, + redeemer_value=redeemer_value, + execution_cost=execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + embed_datum=embed_datum, + ) + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v1_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("plutus_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + script_file1_v1 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V1 + execution_cost1_v1 = plutus_common.ALWAYS_SUCCEEDS_COST + script_file2_v1 = plutus_common.GUESSING_GAME_PLUTUS_V1 + # this is higher than `plutus_common.GUESSING_GAME_COST`, because the script + # context has changed to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=388_458_303, per_space=1_031_312, fixed_cost=87_515 + ) + else: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=280_668_068, per_space=1_031_312, fixed_cost=79_743 + ) + + script_file1_v2 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V2 + execution_cost1_v2 = plutus_common.ALWAYS_SUCCEEDS_V2_COST + script_file2_v2 = plutus_common.GUESSING_GAME_PLUTUS_V2 + execution_cost2_v2 = plutus_common.ExecutionCost( + per_time=208_314_784, + per_space=662_274, + fixed_cost=53_233, + ) + + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + elif plutus_version == "mix_v1_v2": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + elif plutus_version == "plutus_v2": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + else: + raise AssertionError("Unknown test variant.") + + plutus_op1 = plutus_common.PlutusOp( + script_file=script_file1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=execution_cost1, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=script_file2, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_TYPED_CBOR, + execution_cost=execution_cost2, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=amount + redeem_cost1.fee, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + txouts=txouts_fund, + tx_files=tx_files_fund, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + join_txouts=False, + ) + + txid_fund = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos1 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#0", coins=[clusterlib.DEFAULT_COIN] + ) + script_utxos2 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#1", coins=[clusterlib.DEFAULT_COIN] + ) + collateral_utxos1 = cluster.g_query.get_utxo(txin=f"{txid_fund}#2") + collateral_utxos2 = cluster.g_query.get_utxo(txin=f"{txid_fund}#3") + + assert script_utxos1 and script_utxos2, "No script UTxOs" + assert collateral_utxos1 and collateral_utxos2, "No collateral UTxOs" + + assert ( + script_utxos1[0].amount == amount + redeem_cost1.fee + ), f"Incorrect balance for script address `{script_utxos1[0].address}`" + assert ( + script_utxos2[0].amount == amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE + ), f"Incorrect balance for script address `{script_utxos2[0].address}`" + + # Step 2: spend the "locked" UTxOs + + # for mypy + assert plutus_op1.execution_cost and plutus_op2.execution_cost + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + execution_units=( + plutus_op1.execution_cost.per_time, + plutus_op1.execution_cost.per_space, + ), + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + execution_units=( + plutus_op2.execution_cost.per_time, + plutus_op2.execution_cost.per_space, + ), + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + tx_output_redeem = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts_redeem, + tx_files=tx_files_redeem, + fee=redeem_cost1.fee + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + dst_init_balance = cluster.g_query.get_address_balance(payment_addrs[1].address) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed_redeem, + txins=[t.txins[0] for t in tx_output_redeem.script_txins if t.txins], + ) + + assert ( + cluster.g_query.get_address_balance(payment_addrs[1].address) + == dst_init_balance + amount * 2 + ), f"Incorrect balance for destination address `{payment_addrs[1].address}`" + + script_utxos_lovelace = [ + u for u in [*script_utxos1, *script_utxos2] if u.coin == clusterlib.DEFAULT_COIN + ] + for u in script_utxos_lovelace: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + @allure.link(helpers.get_vcs_link()) + def test_always_fails( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + worker_id: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred, collateral UTxO was not spent + and the expected error was raised + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + logfiles.add_ignore_rule( + files_glob="*.stdout", + regex="ValidationTagMismatch", + ignore_file_id=worker_id, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + err, __ = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + expect_failure=True, + ) + assert "PlutusFailure" in err, err + + # wait a bit so there's some time for error messages to appear in log file + time.sleep(1 if cluster.network_magic == configuration.NETWORK_MAGIC_LOCAL else 5) + + @allure.link(helpers.get_vcs_link()) + def test_script_invalid( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test failing script together with the `--script-invalid` argument - collateral is taken. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was spent + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + # include any payment txin + txins = [ + r + for r in cluster.g_query.get_utxo( + address=payment_addrs[0].address, coins=[clusterlib.DEFAULT_COIN] + ) + if not (r.datum_hash or r.inline_datum_hash) + ][:1] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + txins=txins, + tx_files=tx_files, + script_valid=False, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_token_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with native tokens and spending the locked UTxO. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO + * check that the expected amounts of Lovelace and native tokens were spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + amount = 2_000_000 + token_amount = 100 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + tokens=tokens_rec, + ) + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + tokens=tokens_rec, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_partial_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending part of funds (Lovelace and native tokens) on a locked UTxO. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO and create new locked UTxO with change + * check that the expected amounts of Lovelace and native tokens were spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + token_rand = clusterlib.get_rand_str(5) + + amount_fund = 6_000_000 + amount_spend = 2_000_000 + token_amount_fund = 100 + token_amount_spend = 20 + + # add extra fee for tokens + fee_redeem_txsize = FEE_REDEEM_TXSIZE + 5_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount_fund, + ) + tokens_fund_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount_fund, + fee_txsize=fee_redeem_txsize, + tokens=tokens_fund_rec, + ) + + tokens_spend_rec = [ + plutus_common.Token(coin=t.token, amount=token_amount_spend) for t in tokens + ] + + __, tx_output_spend = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_spend, + fee_txsize=fee_redeem_txsize, + tokens=tokens_spend_rec, + ) + + txid_spend = cluster.g_transaction.get_txid(tx_body_file=tx_output_spend.out_file) + change_utxos = cluster.g_query.get_utxo(txin=f"{txid_spend}#1") + + # check that the expected amounts of Lovelace and native tokens were spent and change UTxOs + # with appropriate datum hash were created + token_amount_exp = token_amount_fund - token_amount_spend + assert len(change_utxos) == len(tokens_spend_rec) + 1 + for u in change_utxos: + if u.coin == clusterlib.DEFAULT_COIN: + assert u.amount == amount_fund - amount_spend + else: + assert u.amount == token_amount_exp + assert u.datum_hash == script_utxos[0].datum_hash + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("scenario", ("max", "max+1", "none")) + @common.PARAM_PLUTUS_VERSION + def test_collaterals( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + scenario: str, + plutus_version: str, + ): + """Test dividing required collateral amount into multiple collateral UTxOs. + + Test 3 scenarios: + 1. maximum allowed number of collateral inputs + 2. more collateral inputs than what is allowed + 3. no collateral input + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * create multiple UTxOs for collateral + * spend the locked UTxO + * check that the expected amount was spent when success is expected + * OR check that the amount was not transferred and collateral UTxO was not spent + when failure is expected + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{scenario}" + amount = 2_000_000 + + max_collateral_ins = cluster.g_query.get_protocol_params()["maxCollateralInputs"] + collateral_utxos = [] + + if scenario == "max": + collateral_num = max_collateral_ins + exp_err = "" + collateral_fraction_offset = 250_000.0 + elif scenario == "max+1": + collateral_num = max_collateral_ins + 1 + exp_err = "TooManyCollateralInputs" + collateral_fraction_offset = 250_000.0 + else: + collateral_num = 0 + exp_err = "Transaction body has no collateral inputs" + collateral_fraction_offset = 1.0 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, fund_collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + collateral_fraction_offset=collateral_fraction_offset, + ) + + if collateral_num: + # instead of using the collateral UTxO created by `_fund_script`, create multiple new + # collateral UTxOs with the combined amount matching the original UTxO + collateral_amount_part = int(fund_collateral_utxos[0].amount // collateral_num) + 1 + txouts_collaterals = [ + clusterlib.TxOut(address=dst_addr.address, amount=collateral_amount_part) + for __ in range(collateral_num) + ] + tx_files_collaterals = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + tx_output_collaterals = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_collaterals", + txouts=txouts_collaterals, + tx_files=tx_files_collaterals, + join_txouts=False, + ) + txid_collaterals = cluster.g_transaction.get_txid( + tx_body_file=tx_output_collaterals.out_file + ) + _utxos_nested = [ + cluster.g_query.get_utxo(txin=f"{txid_collaterals}#{i}") + for i in range(collateral_num) + ] + collateral_utxos = list(itertools.chain.from_iterable(_utxos_nested)) + + if exp_err: + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert exp_err in err_str, err_str + else: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestDatum: + """Tests for datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + def test_datum_on_key_credential_address( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test creating UTxO with datum on address with key credentials (non-script address).""" + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount, + datum_hash_file=plutus_common.DATUM_42_TYPED, + ) + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_raw_output = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output) + datum_utxo = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0] + assert datum_utxo.datum_hash, f"UTxO should have datum hash: {datum_utxo}" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegative: + """Tests for Tx output locking using Plutus smart contracts that are expected to fail.""" + + @pytest.fixture + def pparams(self, cluster: clusterlib.ClusterLib) -> dict: + return cluster.g_query.get_protocol_params() + + @pytest.fixture + def fund_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + request: SubRequest, + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp]: + plutus_version = request.param + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=2_000_000, + ) + + return script_utxos, collateral_utxos, plutus_op + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @pytest.mark.parametrize( + "variant", + ( + "42_43", # correct datum, wrong redeemer + "43_42", # wrong datum, correct redeemer + "43_43", # wrong datum and redeemer + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_invalid_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "guessing game" script that expects specific datum and redeemer value. + Test negative scenarios where datum or redeemer value is different than expected. + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was not spent + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{variant}" + amount = 2_000_000 + + if variant == "42_43": + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + elif variant == "43_42": + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "43_43": # noqa: SIM106 + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + else: + raise AssertionError("Unknown test variant.") + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME[plutus_version].script_file, + datum_file=datum_file, + redeemer_file=redeemer_file, + execution_cost=plutus_common.GUESSING_GAME[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + err, __ = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + expect_failure=True, + ) + + assert "ValidationTagMismatch (IsValid True)" in err, err + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_w_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while collateral contains native tokens. + + Expect failure. + + * create a collateral UTxO with native tokens + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + amount = 2_000_000 + token_amount = 100 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + tokens_collateral=tokens_rec, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "CollateralContainsNonADA" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_same_collateral_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using the same UTxO as collateral. + + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO while using the same UTxO as collateral + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_CBOR, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, *__ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=script_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert "cardano-cli transaction submit" in err_str, err_str + assert "ScriptsNotPaidUTxO" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_percent( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend locked UTxO while collateral is less than required. + + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * create a collateral UTxO with amount of ADA less than required by `collateralPercentage` + * try to spend the UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + # increase fixed cost so the required collateral is higher than minimum collateral of 2 ADA + execution_cost = plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost + execution_cost_increased = execution_cost._replace(fixed_cost=2_000_000) + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=execution_cost_increased, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + collateral_fraction_offset=0.9, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "InsufficientCollateral" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_two_scripts_spending_one_fail( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx, one fails. + + Expect failure. + + * create a Tx output with a datum hash at the script addresses + * try to spend the locked UTxOs + * check that the expected error was raised + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + plutus_op1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + script2_hash = helpers.decode_bech32(bech32=script_address2)[2:] + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=amount + redeem_cost1.fee, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + txouts=txouts_fund, + tx_files=tx_files_fund, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + join_txouts=False, + ) + + txid_fund = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos1 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#0", coins=[clusterlib.DEFAULT_COIN] + ) + script_utxos2 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#1", coins=[clusterlib.DEFAULT_COIN] + ) + collateral_utxos1 = cluster.g_query.get_utxo(txin=f"{txid_fund}#2") + collateral_utxos2 = cluster.g_query.get_utxo(txin=f"{txid_fund}#3") + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + execution_units=( + plutus_op1.execution_cost.per_time, + plutus_op1.execution_cost.per_space, + ), + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + execution_units=( + plutus_op2.execution_cost.per_time, + plutus_op2.execution_cost.per_space, + ), + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + + tx_output_redeem = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts_redeem, + tx_files=tx_files_redeem, + fee=redeem_cost1.fee + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed_redeem) + + err_str = str(excinfo.value) + assert rf"ScriptHash \"{script2_hash}\") fails" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(data=st.data()) + @common.hypothesis_settings(100) + @pytest.mark.parametrize( + "fund_execution_units_above_limit", + ("v1", pytest.param("v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE)), + ids=("plutus_v1", "plutus_v2"), + indirect=True, + ) + def test_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_execution_units_above_limit: Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp + ], + pparams: dict, + data: st.DataObject, + request: FixtureRequest, + ): + """Test spending a locked UTxO with a Plutus script with execution units above the limit. + + Expect failure. + + * fund the script address and create a UTxO for collateral + * try to spend the locked UTxO when execution units are set above the limits + * check that failed because the execution units were too big + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + amount = 2_000_000 + + script_utxos, collateral_utxos, plutus_op = fund_execution_units_above_limit + + per_time = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["steps"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_time > pparams["maxTxExecutionUnits"]["steps"] + + per_space = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["memory"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_space > pparams["maxTxExecutionUnits"]["memory"] + + fixed_cost = pparams["txFeeFixed"] + + high_execution_cost = plutus_common.ExecutionCost( + per_time=per_time, per_space=per_space, fixed_cost=fixed_cost + ) + + plutus_op = plutus_op._replace(execution_cost=high_execution_cost) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert "ExUnitsTooBigUTxO" in err_str, err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeRedeemer: + """Tests for Tx output locking using Plutus smart contracts with wrong redeemer.""" + + MIN_INT_VAL = -common.MAX_UINT64 + AMOUNT = 2_000_000 + + def _fund_script_guessing_game( + self, + cluster_manager: cluster_management.ClusterManager, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + plutus_version: str, + ) -> FundTupleT: + """Fund a Plutus script and create the locked UTxO and collateral UTxO.""" + payment_addrs = clusterlib_utils.create_payment_addr_records( + *[f"{temp_template}_payment_addr_{i}" for i in range(2)], + cluster_obj=cluster_obj, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + payment_addrs[0], + cluster_obj=cluster_obj, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster_obj, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def fund_script_guessing_game_v1( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + ) -> FundTupleT: + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + + script_utxos, collateral_utxos, payment_addrs = self._fund_script_guessing_game( + cluster_manager=cluster_manager, + cluster_obj=cluster, + temp_template=temp_template, + plutus_version="v1", + ) + fixture_cache.value = script_utxos, collateral_utxos, payment_addrs + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def fund_script_guessing_game_v2( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + ) -> FundTupleT: + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + + script_utxos, collateral_utxos, payment_addrs = self._fund_script_guessing_game( + cluster_manager=cluster_manager, + cluster_obj=cluster, + temp_template=temp_template, + plutus_version="v2", + ) + fixture_cache.value = script_utxos, collateral_utxos, payment_addrs + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def cost_per_unit( + self, + cluster: clusterlib.ClusterLib, + ) -> plutus_common.ExecutionCost: + return plutus_common.get_cost_per_unit( + protocol_params=cluster.g_query.get_protocol_params() + ) + + def _failed_tx_build( + self, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + redeemer_content: str, + dst_addr: clusterlib.AddressRecord, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + ) -> str: + """Try to build a Tx and expect failure.""" + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + outfile.write(redeemer_content) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + ) + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + return str(excinfo.value) + + def _int_out_of_range( + self, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + redeemer_value: int, + dst_addr: clusterlib.AddressRecord, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + ): + """Try to spend a locked UTxO with redeemer int value that is not in allowed range.""" + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=redeemer_file if redeemer_content else "", + redeemer_value=str(redeemer_value) if not redeemer_content else "", + ) + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + err_str = str(excinfo.value) + assert "Value out of range within the script data" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given( + redeemer_value=st.integers(min_value=MIN_INT_VAL, max_value=common.MAX_UINT64) + ) + @hypothesis.example(redeemer_value=MIN_INT_VAL) + @hypothesis.example(redeemer_value=common.MAX_UINT64) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_inside_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with an unexpected redeemer value. + + Expect failure. + """ + hypothesis.assume(redeemer_value != 42) + + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + # try to spend the "locked" UTxO + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + dst_addr = payment_addrs[1] + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=redeemer_file if redeemer_content else "", + redeemer_value=str(redeemer_value) if not redeemer_content else "", + ) + ] + tx_raw_output = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed) + + err_str = str(excinfo.value) + assert "ValidationTagMismatch (IsValid True)" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(max_value=MIN_INT_VAL - 1)) + @hypothesis.example(redeemer_value=MIN_INT_VAL - 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_bellow_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a redeemer int value < minimum allowed value. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + self._int_out_of_range( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(min_value=common.MAX_UINT64 + 1)) + @hypothesis.example(redeemer_value=common.MAX_UINT64 + 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_above_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a redeemer int value > maximum allowed value. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + self._int_out_of_range( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO with an invalid redeemer type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value.hex()}, outfile) + + # try to spend the "locked" UTxO + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + dst_addr = payment_addrs[1] + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + ) + ] + + tx_raw_output = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed) + + err_str = str(excinfo.value) + assert "ValidationTagMismatch (IsValid True)" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO using redeemer that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = json.dumps( + {"constructor": 0, "fields": [{"bytes": redeemer_value.hex()}]} + ) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert "must consist of at most 64 bytes" in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in typed format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = json.dumps({"constructor": 0, "fields": [{"int": redeemer_value.hex()}]}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "int" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"int": redeemer_value.hex()}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "int" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in typed format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"constructor": 0, "fields": [{"bytes": redeemer_value}]}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "bytes" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"bytes": redeemer_value}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "bytes" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_invalid_json( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: str, + ): + """Try to build a Tx using a redeemer value that is invalid JSON. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = f'{{"{redeemer_value}"}}' + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert "Invalid JSON format" in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_invalid_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON typed schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({redeemer_type: 42}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err + ), err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_invalid_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON untyped schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({redeemer_type: 42}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err + ), err + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeDatum: + """Tests for Tx output locking using Plutus smart contracts with wrong datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.parametrize("address_type", ("script_address", "key_address")) + @common.PARAM_PLUTUS_VERSION + def test_no_datum_txout( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + address_type: str, + plutus_version: str, + ): + """Test using UTxO without datum hash in place of locked UTxO. + + Expect failure. + + * create a Tx output without a datum hash + * try to spend the UTxO like it was locked Plutus UTxO + * check that the expected error was raised + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{address_type}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + if address_type == "script_address": + redeem_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + else: + redeem_address = payment_addrs[2].address + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + txouts = [ + clusterlib.TxOut( + address=redeem_address, amount=amount + redeem_cost.fee + FEE_REDEEM_TXSIZE + ), + clusterlib.TxOut(address=payment_addr.address, amount=redeem_cost.collateral), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + join_txouts=False, + ) + txid = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos = cluster.g_query.get_utxo(txin=f"{txid}#0") + collateral_utxos = cluster.g_query.get_utxo(txin=f"{txid}#1") + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "NonOutputSupplimentaryDatums" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_lock_tx_invalid_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: str, + plutus_version: str, + ): + """Test locking a Tx output with an invalid datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{datum_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + with pytest.raises(clusterlib.CLIError) as excinfo: + _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_tx_wrong_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output and try to spend it with a wrong datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op_1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op_1.execution_cost # for mypy + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op_1, + amount=amount, + ) + + # use a wrong datum to try to unlock the funds + plutus_op_2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_2, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "NonOutputSupplimentaryDatums" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_non_script_utxo( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend a non-script UTxO with datum as if it was script locked UTxO. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + amount_fund = 4_000_000 + amount_redeem = 2_000_000 + amount_collateral = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = plutus_common.DATUM_42_TYPED + + datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file=datum_file) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=datum_file, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + # create datum and collateral UTxOs + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount_fund, + datum_hash=datum_hash, + ), + clusterlib.TxOut( + address=payment_addr.address, + amount=amount_collateral, + ), + ] + tx_files_fund = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_raw_output = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files_fund, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output) + datum_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=dst_addr.address, datum_hash=datum_hash + )[0] + collateral_utxos = clusterlib.filter_utxos( + utxos=out_utxos, address=payment_addr.address, utxo_ix=datum_utxo.utxo_ix + 1 + ) + assert ( + datum_utxo.datum_hash == datum_hash + ), f"UTxO should have datum hash '{datum_hash}': {datum_utxo}" + + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file, dst_addr.skey_file] + ) + + # try to spend the "locked" UTxO + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=[datum_utxo], + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_redeem, + tx_files=tx_files_redeem, + ) + + err_str = str(excinfo.value) + assert "ExtraneousScriptWitnessesUTXOW" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: bytes, + plutus_version: str, + ): + """Try to lock a UTxO with datum that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": datum_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + with pytest.raises(clusterlib.CLIError) as excinfo: + _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + +@pytest.mark.testnets +class TestCompatibility: + """Tests for checking compatibility with previous Tx eras.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era > VERSIONS.ALONZO, + reason="runs only with Tx era <= Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv2_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV2 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v2"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v2"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV2 is not supported" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era >= VERSIONS.ALONZO, + reason="runs only with Tx era < Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv1_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV1 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v1"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v1"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV1 is not supported" in err_str, err_str diff --git a/cardano_node_tests/tests/test_plutus/test_spend_negative_build.py b/cardano_node_tests/tests/test_plutus/test_spend_negative_build.py new file mode 100644 index 000000000..798b2c522 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_spend_negative_build.py @@ -0,0 +1,2957 @@ +"""Tests for spending with Plutus using `transaction build`.""" +import json +import logging +import shutil +from pathlib import Path +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from cardano_clusterlib import clusterlib +from cardano_clusterlib import txtools + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + common.SKIPIF_BUILD_UNUSABLE, + pytest.mark.smoke, +] + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment addresses.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(3)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=1_000_000_000, + ) + + return addrs + + +@pytest.fixture +def pool_users( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.PoolUser]: + """Create new pool users.""" + test_id = common.get_test_id(cluster) + created_users = clusterlib_utils.create_pool_users( + cluster_obj=cluster, + name_template=f"{test_id}_pool_users", + no_of_addr=2, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + created_users[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return created_users + + +def _build_fund_script( + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + plutus_op: plutus_common.PlutusOp, + tokens: Optional[List[plutus_common.Token]] = None, # tokens must already be in `payment_addr` + tokens_collateral: Optional[ + List[plutus_common.Token] + ] = None, # tokens must already be in `payment_addr` + embed_datum: bool = False, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund a Plutus script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + assert plutus_op.execution_cost # for mypy + + script_fund = 200_000_000 + + stokens = tokens or () + ctokens = tokens_collateral or () + + script_address = cluster_obj.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + ) + + # create a Tx output with a datum hash at the script address + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + + script_txout = plutus_common.txout_factory( + address=script_address, + amount=script_fund, + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + txouts = [ + script_txout, + # for collateral + clusterlib.TxOut(address=dst_addr.address, amount=redeem_cost.collateral), + ] + + for token in stokens: + txouts.append(script_txout._replace(amount=token.amount, coin=token.coin)) + + for token in ctokens: + txouts.append( + clusterlib.TxOut( + address=dst_addr.address, + amount=token.amount, + coin=token.coin, + ) + ) + + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files, + txouts=txouts, + fee_buffer=2_000_000, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster_obj.g_query.get_utxo(tx_raw_output=tx_output) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset(utxos=out_utxos, txouts=tx_output.txouts) + + script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + assert script_utxos, "No script UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos) == script_fund + ), f"Incorrect balance for script address `{script_address}`" + + collateral_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + assert collateral_utxos, "No collateral UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos) == redeem_cost.collateral + ), f"Incorrect balance for collateral address `{dst_addr.address}`" + + for token in stokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos, coin=token.coin) == token.amount + ), f"Incorrect token balance for script address `{script_address}`" + + for token in ctokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos, coin=token.coin) + == token.amount + ), f"Incorrect token balance for address `{dst_addr.address}`" + + if VERSIONS.transaction_era >= VERSIONS.ALONZO: + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_output) + + return script_utxos, collateral_utxos, tx_output + + +def _build_spend_locked_txin( # noqa: C901 + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + plutus_op: plutus_common.PlutusOp, + amount: int, + deposit_amount: int = 0, + txins: clusterlib.OptionalUTXOData = (), + tx_files: Optional[clusterlib.TxFiles] = None, + invalid_hereafter: Optional[int] = None, + invalid_before: Optional[int] = None, + tokens: Optional[List[plutus_common.Token]] = None, + expect_failure: bool = False, + script_valid: bool = True, + submit_tx: bool = True, +) -> Tuple[str, Optional[clusterlib.TxRawOutput], list]: + """Spend the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + # pylint: disable=too-many-arguments,too-many-locals + tx_files = tx_files or clusterlib.TxFiles() + spent_tokens = tokens or () + + # Change that was calculated manually will be returned to address of the first script. + # The remaining change that is automatically handled by the `build` command will be returned + # to `payment_addr`, because it would be inaccessible on script address without proper + # datum hash (datum hash is not provided for change that is handled by `build` command). + script_change_rec = script_utxos[0] + + # spend the "locked" UTxO + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + datum_value=plutus_op.datum_value if plutus_op.datum_value else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file if plutus_op.redeemer_cbor_file else "", + redeemer_value=plutus_op.redeemer_value if plutus_op.redeemer_value else "", + ) + ] + tx_files = tx_files._replace( + signing_key_files=list({*tx_files.signing_key_files, dst_addr.skey_file}), + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + + lovelace_change_needed = False + for token in spent_tokens: + txouts.append( + clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin) + ) + # append change + script_token_balance = clusterlib.calculate_utxos_balance( + utxos=script_utxos, coin=token.coin + ) + if script_token_balance > token.amount: + lovelace_change_needed = True + txouts.append( + clusterlib.TxOut( + address=script_change_rec.address, + amount=script_token_balance - token.amount, + coin=token.coin, + datum_hash=script_change_rec.datum_hash, + ) + ) + # add minimum (+ some) required Lovelace to change Tx output + if lovelace_change_needed: + txouts.append( + clusterlib.TxOut( + address=script_change_rec.address, + amount=4_000_000, + coin=clusterlib.DEFAULT_COIN, + datum_hash=script_change_rec.datum_hash, + ) + ) + + if expect_failure: + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=txins, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + ) + return str(excinfo.value), None, [] + + tx_output = cluster_obj.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txins=txins, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + deposit=deposit_amount, + script_valid=script_valid, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + if not submit_tx: + return "", tx_output, [] + + dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address) + + script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN] + + if not script_valid: + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=collateral_utxos) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) + == dst_init_balance - collateral_utxos[0].amount + ), f"Collateral was NOT spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return "", tx_output, [] + + # calculate cost of Plutus script + plutus_costs = cluster_obj.g_transaction.calculate_plutus_script_cost( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + txouts=txouts, + script_txins=plutus_txins, + change_address=payment_addr.address, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + deposit=deposit_amount, + script_valid=script_valid, + ) + + cluster_obj.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_output.script_txins if t.txins] + ) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + for token in spent_tokens: + script_utxos_token = [u for u in script_utxos if u.coin == token.coin] + for u in script_utxos_token: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[token.coin] + ), f"Token inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster_obj, tx_raw_output=tx_output) + + tx_db_record = dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_output) + # compare cost of Plutus script with data from db-sync + if tx_db_record: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_db_record.redeemers, cost_records=plutus_costs + ) + + return "", tx_output, plutus_costs + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestBuildLocking: + """Tests for Tx output locking using Plutus smart contracts and `transaction build`.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Corresponds to Exercise 3 for Alonzo Blue. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + __, tx_output, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 170_782 + assert tx_output and helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + def test_context_equivalence( + self, + cluster: clusterlib.ClusterLib, + pool_users: List[clusterlib.PoolUser], + ): + """Test context equivalence while spending a locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * spend the locked UTxO using the derived redeemer + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 10_000_000 + deposit_amount = cluster.g_query.get_address_deposit() + + # create stake address registration cert + stake_addr_reg_cert_file = cluster.g_stake_address.gen_stake_addr_registration_cert( + addr_name=f"{temp_template}_addr2", + stake_vkey_file=pool_users[0].stake.vkey_file, + ) + + tx_files = clusterlib.TxFiles(certificate_files=[stake_addr_reg_cert_file]) + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_op_dummy = plutus_common.PlutusOp( + script_file=plutus_common.CONTEXT_EQUIVALENCE_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=redeemer_file_dummy, + execution_cost=plutus_common.CONTEXT_EQUIVALENCE_COST, + ) + + # fund the script address + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + plutus_op=plutus_op_dummy, + ) + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + __, tx_output_dummy, __ = _build_spend_locked_txin( + temp_template=f"{temp_template}_dummy", + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_dummy, + amount=amount, + deposit_amount=deposit_amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + submit_tx=False, + ) + assert tx_output_dummy + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + tx_file_dummy = Path(f"{tx_output_dummy.out_file.with_suffix('')}.signed") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_op = plutus_op_dummy._replace(redeemer_file=redeemer_file) + + __, tx_output, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + deposit_amount=deposit_amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + # check expected fees + if tx_output: + expected_fee = 372_438 + assert helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("embed_datum", (True, False), ids=("embedded_datum", "datum")) + @pytest.mark.parametrize( + "variant", + ("typed_json", "typed_cbor", "untyped_value", "untyped_json", "untyped_cbor"), + ) + @common.PARAM_PLUTUS_VERSION + def test_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + embed_datum: bool, + request: FixtureRequest, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "guessing game" scripts that expect specific datum and redeemer value. + Test both typed and untyped redeemer and datum. + Test passing datum and redeemer to `cardano-cli` as value, json file and cbor file. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + + datum_file: Optional[Path] = None + datum_cbor_file: Optional[Path] = None + datum_value: Optional[str] = None + redeemer_file: Optional[Path] = None + redeemer_cbor_file: Optional[Path] = None + redeemer_value: Optional[str] = None + + if variant == "typed_json": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "typed_cbor": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_TYPED_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_TYPED_CBOR + elif variant == "untyped_value": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_value = "42" + redeemer_value = "42" + elif variant == "untyped_json": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_file = plutus_common.DATUM_42 + redeemer_file = plutus_common.REDEEMER_42 + elif variant == "untyped_cbor": # noqa: SIM106 + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + else: + raise AssertionError("Unknown test variant.") + + execution_cost = plutus_common.GUESSING_GAME[plutus_version].execution_cost + if script_file == plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file: + execution_cost = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost + + plutus_op = plutus_common.PlutusOp( + script_file=script_file, + datum_file=datum_file, + datum_cbor_file=datum_cbor_file, + datum_value=datum_value, + redeemer_file=redeemer_file, + redeemer_cbor_file=redeemer_cbor_file, + redeemer_value=redeemer_value, + execution_cost=execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + __, __, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v1_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("plutus_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + script_fund = 200_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + script_file1_v1 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V1 + execution_cost1_v1 = plutus_common.ALWAYS_SUCCEEDS_COST + script_file2_v1 = plutus_common.GUESSING_GAME_PLUTUS_V1 + # this is higher than `plutus_common.GUESSING_GAME_COST`, because the script + # context has changed to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=388_458_303, per_space=1_031_312, fixed_cost=87_515 + ) + else: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=280_668_068, per_space=1_031_312, fixed_cost=79_743 + ) + + script_file1_v2 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V2 + execution_cost1_v2 = plutus_common.ALWAYS_SUCCEEDS_V2_COST + script_file2_v2 = plutus_common.GUESSING_GAME_PLUTUS_V2 + execution_cost2_v2 = plutus_common.ExecutionCost( + per_time=208_314_784, + per_space=662_274, + fixed_cost=53_233, + ) + + expected_fee_fund = 174_389 + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + expected_fee_redeem = 378_768 + elif plutus_version == "mix_v1_v2": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + expected_fee_redeem = 321_739 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + expected_fee_redeem = 378_584 + elif plutus_version == "plutus_v2": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + expected_fee_redeem = 321_378 + else: + raise AssertionError("Unknown test variant.") + + plutus_op1 = plutus_common.PlutusOp( + script_file=script_file1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=execution_cost1, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=script_file2, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_TYPED_CBOR, + execution_cost=execution_cost2, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=script_fund, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=script_fund, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + tx_output_fund = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files_fund, + txouts=txouts_fund, + fee_buffer=2_000_000, + join_txouts=False, + ) + tx_signed_fund = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_fund.out_file, + signing_key_files=tx_files_fund.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + + cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) + + fund_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=fund_utxos, txouts=tx_output_fund.txouts + ) + script_utxos1 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset) + script_utxos2 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 1) + collateral_utxos1 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 2) + collateral_utxos2 = clusterlib.filter_utxos(utxos=fund_utxos, utxo_ix=utxo_ix_offset + 3) + + assert script_utxos1 and script_utxos2, "No script UTxOs" + assert collateral_utxos1 and collateral_utxos2, "No collateral UTxOs" + + assert ( + script_utxos1[0].amount == script_fund + ), f"Incorrect balance for script address `{script_utxos1[0].address}`" + assert ( + script_utxos2[0].amount == script_fund + ), f"Incorrect balance for script address `{script_utxos2[0].address}`" + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + tx_output_redeem = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + # calculate cost of Plutus script + plutus_costs = cluster.g_transaction.calculate_plutus_script_cost( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + dst_init_balance = cluster.g_query.get_address_balance(payment_addrs[1].address) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed_redeem, + txins=[t.txins[0] for t in tx_output_redeem.script_txins if t.txins], + ) + + assert ( + cluster.g_query.get_address_balance(payment_addrs[1].address) + == dst_init_balance + amount * 2 + ), f"Incorrect balance for destination address `{payment_addrs[1].address}`" + + script_utxos_lovelace = [ + u for u in [*script_utxos1, *script_utxos2] if u.coin == clusterlib.DEFAULT_COIN + ] + for u in script_utxos_lovelace: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + # check expected fees + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + assert helpers.is_in_interval(tx_output_redeem.fee, expected_fee_redeem, frac=0.15) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[execution_cost1, execution_cost2], + ) + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + # check transactions in db-sync + tx_redeem_record = dbsync_utils.check_tx( + cluster_obj=cluster, tx_raw_output=tx_output_redeem + ) + if tx_redeem_record: + dbsync_utils.check_plutus_costs( + redeemer_records=tx_redeem_record.redeemers, cost_records=plutus_costs + ) + + @allure.link(helpers.get_vcs_link()) + def test_always_fails( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the expected error was raised + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err, __, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + expect_failure=True, + ) + assert "The Plutus script evaluation failed" in err, err + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + def test_script_invalid( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test failing script together with the `--script-invalid` argument - collateral is taken. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was spent + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + # include any payment txin + txins = [ + r + for r in cluster.g_query.get_utxo( + address=payment_addrs[0].address, coins=[clusterlib.DEFAULT_COIN] + ) + if not (r.datum_hash or r.inline_datum_hash) + ][:1] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + try: + __, tx_output, __ = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + txins=txins, + tx_files=tx_files, + script_valid=False, + ) + except clusterlib.CLIError as err: + # TODO: broken on node 1.35.0 and 1.35.1 + if "ScriptWitnessIndexTxIn 0 is missing from the execution units" in str(err): + pytest.xfail("See cardano-node issue #4013") + else: + raise + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + if tx_output: + expected_fee = 171_309 + assert helpers.is_in_interval(tx_output.fee, expected_fee, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_token_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO + * check that the expected amounts of Lovelace and native tokens were spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=100, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens=tokens_rec, + ) + + __, tx_output_spend, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + tokens=tokens_rec, + ) + + # check expected fees + expected_fee_fund = 173_597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 175_710 + assert tx_output_spend and helpers.is_in_interval( + tx_output_spend.fee, expected_fee, frac=0.15 + ) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_partial_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending part of funds (Lovelace and native tokens) on a locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO and create new locked UTxO with change + * check that the expected amounts of Lovelace and native tokens were spent + * check expected fees + * check expected Plutus cost + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + token_rand = clusterlib.get_rand_str(5) + + amount_spend = 10_000_000 + token_amount_fund = 100 + token_amount_spend = 20 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount_fund, + ) + tokens_fund_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens=tokens_fund_rec, + ) + + tokens_spend_rec = [ + plutus_common.Token(coin=t.token, amount=token_amount_spend) for t in tokens + ] + + __, tx_output_spend, plutus_costs = _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_spend, + tokens=tokens_spend_rec, + ) + + # check that the expected amounts of Lovelace and native tokens were spent and change UTxOs + # with appropriate datum hash were created + + assert tx_output_spend + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_spend) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_spend.txouts + ) + + # UTxO we created for tokens and minimum required Lovelace + change_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + # UTxO that was created by `build` command for rest of the Lovelace change (this will not + # have the script's datum) + # TODO: change UTxO used to be first, now it's last + build_change_utxo = out_utxos[0] if utxo_ix_offset else out_utxos[-1] + + # Lovelace balance on original script UTxOs + script_lovelace_balance = clusterlib.calculate_utxos_balance(utxos=script_utxos) + # Lovelace balance on change UTxOs + change_lovelace_balance = clusterlib.calculate_utxos_balance( + utxos=[*change_utxos, build_change_utxo] + ) + + assert ( + change_lovelace_balance == script_lovelace_balance - tx_output_spend.fee - amount_spend + ) + + token_amount_exp = token_amount_fund - token_amount_spend + assert len(change_utxos) == len(tokens_spend_rec) + 1 + for u in change_utxos: + if u.coin != clusterlib.DEFAULT_COIN: + assert u.amount == token_amount_exp + assert u.datum_hash == script_utxos[0].datum_hash + + # check expected fees + expected_fee_fund = 173_597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + expected_fee = 183_366 + assert tx_output_spend and helpers.is_in_interval( + tx_output_spend.fee, expected_fee, frac=0.15 + ) + + plutus_common.check_plutus_costs( + plutus_costs=plutus_costs, + expected_costs=[plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost], + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_is_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using single UTxO for both collateral and Tx input. + + Uses `cardano-cli transaction build` command for building the transactions. + + Tests bug https://github.com/input-output-hk/cardano-db-sync/issues/750 + + * create a Tx output with a datum hash at the script address and a collateral UTxO + * check that the expected amount was locked at the script address + * spend the locked UTxO while using the collateral UTxO both as collateral and as + normal Tx input + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + # Step 1: fund the script address + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_step1 = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + ) + + # Step 2: spend the "locked" UTxO + + script_address = script_utxos[0].address + + dst_step1_balance = cluster.g_query.get_address_balance(dst_addr.address) + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file + if plutus_op.redeemer_cbor_file + else "", + ) + ] + tx_files = clusterlib.TxFiles( + signing_key_files=[dst_addr.skey_file], + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + + tx_output_step2 = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files, + # `collateral_utxos` is used both as collateral and as normal Tx input + txins=collateral_utxos, + txouts=txouts, + script_txins=plutus_txins, + change_address=script_address, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_step2.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_output_step2.script_txins if t.txins] + ) + + assert ( + cluster.g_query.get_address_balance(dst_addr.address) + == dst_step1_balance + amount - collateral_utxos[0].amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{script_address}`" + + # check expected fees + expected_fee_step1 = 168_845 + assert helpers.is_in_interval(tx_output_step1.fee, expected_fee_step1, frac=0.15) + + expected_fee_step2 = 176_986 + assert helpers.is_in_interval(tx_output_step2.fee, expected_fee_step2, frac=0.15) + + 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) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestDatum: + """Tests for datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + def test_datum_on_key_credential_address( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test creating UTxO with datum on address with key credentials (non-script address). + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount, + datum_hash_file=plutus_common.DATUM_42_TYPED, + ) + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=temp_template, + tx_files=tx_files, + txouts=txouts, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=temp_template, + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output) + datum_utxo = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0] + assert datum_utxo.datum_hash, f"UTxO should have datum hash: {datum_utxo}" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output) + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_embed_datum_without_pparams( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test 'build --tx-out-datum-embed' without providing protocol params file.""" + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + ) + + script_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + utxos = cluster.g_query.get_utxo(address=payment_addrs[0].address) + txin = txtools.filter_utxo_with_highest_amount(utxos=utxos) + + out_file = f"{temp_template}_tx.body" + + cli_args = [ + "transaction", + "build", + "--tx-in", + f"{txin.utxo_hash}#{txin.utxo_ix}", + "--tx-out", + f"{script_address}+2000000", + "--tx-out-datum-embed-file", + str(plutus_op.datum_file), + "--change-address", + payment_addrs[0].address, + "--out-file", + out_file, + "--testnet-magic", + str(cluster.network_magic), + *cluster.g_transaction.tx_era_arg, + ] + + cluster.cli(cli_args) + + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_signed", + ) + + try: + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=[txin]) + except clusterlib.CLIError as err: + if "PPViewHashesDontMatch" in str(err): + pytest.xfail("build cmd requires protocol params - see node issue #4058") + raise + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegative: + """Tests for Tx output locking using Plutus smart contracts that are expected to fail.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_w_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while collateral contains native tokens. + + Expect failure. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a collateral UTxO with native tokens + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + payment_addr = payment_addrs[0] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addr, + issuer_addr=payment_addr, + amount=100, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + tokens_collateral=tokens_rec, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "CollateralContainsNonADA" in err_str, err_str + + # check expected fees + expected_fee_fund = 173597 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_same_collateral_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using the same UTxO as collateral. + + Expect failure. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO while using the same UTxO as collateral + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, __, tx_output_fund = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=script_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert ( + "expected to be key witnessed but are actually script witnessed: " + f'["{script_utxos[0].utxo_hash}#{script_utxos[0].utxo_ix}"]' in err_str + # in 1.35.3 and older + or "Expected key witnessed collateral" in err_str + ), err_str + + # check expected fees + expected_fee_fund = 168_845 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "variant", + ( + "42_43", # correct datum, wrong redeemer + "43_42", # wrong datum, correct redeemer + "43_43", # wrong datum and redeemer + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_invalid_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + + Test with "guessing game" script that expects specific datum and redeemer value. + Test negative scenarios where datum or redeemer value is different than expected. + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was not spent + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{variant}" + + if variant == "42_43": + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + elif variant == "43_42": + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "43_43": # noqa: SIM106 + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + else: + raise AssertionError("Unknown test variant.") + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME[plutus_version].script_file, + datum_file=datum_file, + redeemer_file=redeemer_file, + execution_cost=plutus_common.GUESSING_GAME[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_two_scripts_spending_one_fail( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx, one fails. + + Uses `cardano-cli transaction build` command for building the transactions. + + * create a Tx output with a datum hash at the script addresses + * try to spend the locked UTxOs + * check that the expected error was raised + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 50_000_000 + + script_fund = 200_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + plutus_op1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=script_fund, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=script_fund, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + tx_output_fund = cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + tx_files=tx_files_fund, + txouts=txouts_fund, + fee_buffer=2_000_000, + join_txouts=False, + ) + tx_signed_fund = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_fund.out_file, + signing_key_files=tx_files_fund.signing_key_files, + tx_name=f"{temp_template}_step1", + ) + + cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_fund.txouts + ) + + script_utxos1 = clusterlib.filter_utxos( + utxos=out_utxos, utxo_ix=utxo_ix_offset, coin=clusterlib.DEFAULT_COIN + ) + script_utxos2 = clusterlib.filter_utxos( + utxos=out_utxos, utxo_ix=utxo_ix_offset + 1, coin=clusterlib.DEFAULT_COIN + ) + collateral_utxos1 = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 2) + collateral_utxos2 = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 3) + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.build_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step2", + tx_files=tx_files_redeem, + txouts=txouts_redeem, + script_txins=plutus_txins, + change_address=payment_addrs[0].address, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeRedeemer: + """Tests for Tx output locking using Plutus smart contracts with wrong redeemer.""" + + MIN_INT_VAL = -common.MAX_UINT64 + AMOUNT = 2_000_000 + + @pytest.fixture + def fund_script_guessing_game_v1( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]]: + """Fund a PlutusV1 script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED_PLUTUS_V1, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED_COST, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + return script_utxos, collateral_utxos + + @pytest.fixture + def fund_script_guessing_game_v2( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]]: + """Fund a PlutusV2 script and create the locked UTxO and collateral UTxO. + + Uses `cardano-cli transaction build` command for building the transactions. + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED_PLUTUS_V2, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED_V2_COST, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + return script_utxos, collateral_utxos + + def _int_out_of_range( + self, + cluster: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + redeemer_value: int, + plutus_version: str, + ): + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file) if redeemer_content else None, + redeemer_value=None if redeemer_content else str(redeemer_value), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "Value out of range within the script data" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given( + redeemer_value=st.integers(min_value=MIN_INT_VAL, max_value=common.MAX_UINT64) + ) + @hypothesis.example(redeemer_value=MIN_INT_VAL) + @hypothesis.example(redeemer_value=common.MAX_UINT64) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_inside_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value that is in the valid range. + + Expect failure. + """ + hypothesis.assume(redeemer_value != 42) + + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file) if redeemer_content else None, + redeemer_value=None if redeemer_content else str(redeemer_value), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "The Plutus script evaluation failed" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(min_value=common.MAX_UINT64 + 1)) + @hypothesis.example(redeemer_value=common.MAX_UINT64 + 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_above_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value, above max value allowed. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + self._int_out_of_range( + cluster=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(max_value=MIN_INT_VAL - 1)) + @hypothesis.example(redeemer_value=MIN_INT_VAL - 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_bellow_range( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a wrong redeemer value, bellow min value allowed. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + self._int_out_of_range( + cluster=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO with a wrong redeemer type, try to use bytes. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value.hex()}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "Script debugging logs: Incorrect datum. Expected 42." in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO using redeemer that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": redeemer_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in typed format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"int": redeemer_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "int" does not have the type required by the schema.' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"int": redeemer_value.hex()}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "int" does not have the type required by the schema.' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in typed format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": redeemer_value}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "bytes" does not have the type required by the schema.' + in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'The value in the field "bytes" does not have the type required by the schema.' + in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_invalid_json( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_value: str, + ): + """Try to build a Tx using a redeemer value that is invalid JSON. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{redeemer_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_invalid_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON typed schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{redeemer_type: 42}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_invalid_type( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_script_guessing_game_v1: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + fund_script_guessing_game_v2: Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData]], + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON untyped schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({redeemer_type: 42}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + err_str = str(excinfo.value) + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err_str + ), err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeDatum: + """Tests for Tx output locking using Plutus smart contracts with wrong datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.parametrize("address_type", ("script_address", "key_address")) + @common.PARAM_PLUTUS_VERSION + def test_no_datum_txout( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + address_type: str, + plutus_version: str, + ): + """Test using UTxO without datum hash in place of locked UTxO. + + Expect failure. + + * create a Tx output without a datum hash + * try to spend the UTxO like it was locked Plutus UTxO + * check that the expected error was raised + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{address_type}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + if address_type == "script_address": + redeem_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + else: + redeem_address = payment_addrs[2].address + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + txouts = [ + clusterlib.TxOut(address=redeem_address, amount=amount + redeem_cost.fee), + clusterlib.TxOut(address=payment_addr.address, amount=redeem_cost.collateral), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + join_txouts=False, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) + utxo_ix_offset = clusterlib_utils.get_utxo_ix_offset( + utxos=out_utxos, txouts=tx_output_fund.txouts + ) + + script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset) + collateral_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset + 1) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + + if address_type == "script_address": + assert "txin does not have a script datum" in err_str, err_str + else: + assert ( + "not a Plutus script witnessed tx input" in err_str + or "points to a script hash that is not known" in err_str + ), err_str + + # check expected fees + expected_fee_fund = 199_087 + assert helpers.is_in_interval(tx_output_fund.fee, expected_fee_fund, frac=0.15) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_lock_tx_invalid_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: str, + plutus_version: str, + ): + """Test locking a Tx output with an invalid datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{datum_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_tx_wrong_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output and try to spend it with a wrong datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op_1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op_1, + ) + + # use a wrong datum to try to unlock the funds + plutus_op_2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_2, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert ( + "The Plutus script witness has the wrong datum (according to the UTxO)." in err_str + ), err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_non_script_utxo( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend a non-script UTxO with datum as if it was script locked UTxO. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + amount_fund = 4_000_000 + amount_redeem = 2_000_000 + amount_collateral = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = plutus_common.DATUM_42_TYPED + + datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file=datum_file) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=datum_file, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + # create datum and collateral UTxOs + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount_fund, + datum_hash=datum_hash, + ), + clusterlib.TxOut( + address=payment_addr.address, + amount=amount_collateral, + ), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output = cluster.g_transaction.build_tx( + src_address=payment_addr.address, + tx_name=temp_template, + tx_files=tx_files, + txouts=txouts, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=temp_template, + ) + cluster.g_transaction.submit_tx(tx_file=tx_signed, txins=tx_output.txins) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output) + datum_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=dst_addr.address, datum_hash=datum_hash + )[0] + collateral_utxos = clusterlib.filter_utxos( + utxos=out_utxos, address=payment_addr.address, utxo_ix=datum_utxo.utxo_ix + 1 + ) + assert ( + datum_utxo.datum_hash == datum_hash + ), f"UTxO should have datum hash '{datum_hash}': {datum_utxo}" + + # try to spend the "locked" UTxO + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + script_utxos=[datum_utxo], + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_redeem, + ) + + err_str = str(excinfo.value) + assert "points to a script hash that is not known" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: bytes, + plutus_version: str, + ): + """Try to lock a UTxO with datum that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": datum_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + +@pytest.mark.testnets +class TestCompatibility: + """Tests for checking compatibility with previous Tx eras.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era > VERSIONS.ALONZO, + reason="runs only with Tx era <= Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv2_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV2 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v2"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v2"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _build_fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _build_spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=2_000_000, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV2 is not supported" in err_str, err_str diff --git a/cardano_node_tests/tests/test_plutus/test_spend_negative_raw.py b/cardano_node_tests/tests/test_plutus/test_spend_negative_raw.py new file mode 100644 index 000000000..a264ae894 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_spend_negative_raw.py @@ -0,0 +1,3052 @@ +"""Tests for spending with Plutus using `transaction build-raw`.""" +import itertools +import json +import logging +import shutil +import time +from pathlib import Path +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import SubRequest +from cardano_clusterlib import clusterlib + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import logfiles +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + pytest.mark.smoke, +] + + +FundTupleT = Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], List[clusterlib.AddressRecord] +] + +# approx. fee for Tx size +FEE_REDEEM_TXSIZE = 400_000 + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment addresses.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(3)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return addrs + + +@pytest.fixture +def pool_users( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.PoolUser]: + """Create new pool users.""" + test_id = common.get_test_id(cluster) + created_users = clusterlib_utils.create_pool_users( + cluster_obj=cluster, + name_template=f"{test_id}_pool_users", + no_of_addr=2, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + created_users[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return created_users + + +def _fund_script( + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + plutus_op: plutus_common.PlutusOp, + amount: int, + fee_txsize: int = FEE_REDEEM_TXSIZE, + deposit_amount: int = 0, + tokens: Optional[List[plutus_common.Token]] = None, # tokens must already be in `payment_addr` + tokens_collateral: Optional[ + List[plutus_common.Token] + ] = None, # tokens must already be in `payment_addr` + collateral_fraction_offset: float = 1.0, + embed_datum: bool = False, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund a Plutus script and create the locked UTxO and collateral UTxO.""" + # pylint: disable=too-many-locals,too-many-arguments + assert plutus_op.execution_cost # for mypy + + stokens = tokens or () + ctokens = tokens_collateral or () + + script_address = cluster_obj.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + collateral_fraction_offset=collateral_fraction_offset, + ) + + # create a Tx output with a datum hash at the script address + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + + script_txout = plutus_common.txout_factory( + address=script_address, + amount=amount + redeem_cost.fee + fee_txsize + deposit_amount, + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + txouts = [ + script_txout, + # for collateral + clusterlib.TxOut(address=dst_addr.address, amount=redeem_cost.collateral), + ] + + for token in stokens: + txouts.append(script_txout._replace(amount=token.amount, coin=token.coin)) + + for token in ctokens: + txouts.append( + clusterlib.TxOut( + address=dst_addr.address, + amount=token.amount, + coin=token.coin, + ) + ) + + tx_raw_output = cluster_obj.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + txouts=txouts, + tx_files=tx_files, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + ) + + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + script_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#0") + assert script_utxos, "No script UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos) == txouts[0].amount + ), f"Incorrect balance for script address `{script_address}`" + + collateral_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#1") + assert collateral_utxos, "No collateral UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos) == redeem_cost.collateral + ), f"Incorrect balance for collateral address `{dst_addr.address}`" + + for token in stokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos, coin=token.coin) == token.amount + ), f"Incorrect token balance for script address `{script_address}`" + + for token in ctokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos, coin=token.coin) + == token.amount + ), f"Incorrect token balance for address `{dst_addr.address}`" + + if VERSIONS.transaction_era >= VERSIONS.ALONZO: + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + return script_utxos, collateral_utxos, tx_raw_output + + +def _spend_locked_txin( # noqa: C901 + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + dst_addr: clusterlib.AddressRecord, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + plutus_op: plutus_common.PlutusOp, + amount: int, + fee_txsize: int = FEE_REDEEM_TXSIZE, + txins: clusterlib.OptionalUTXOData = (), + tx_files: Optional[clusterlib.TxFiles] = None, + invalid_hereafter: Optional[int] = None, + invalid_before: Optional[int] = None, + tokens: Optional[List[plutus_common.Token]] = None, + expect_failure: bool = False, + script_valid: bool = True, + submit_tx: bool = True, +) -> Tuple[str, clusterlib.TxRawOutput]: + """Spend the locked UTxO.""" + # pylint: disable=too-many-arguments,too-many-locals + assert plutus_op.execution_cost + + tx_files = tx_files or clusterlib.TxFiles() + spent_tokens = tokens or () + + # change will be returned to address of the first script + change_rec = script_utxos[0] + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + ) + + script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN] + script_lovelace_balance = clusterlib.calculate_utxos_balance( + utxos=[*script_utxos_lovelace, *txins] + ) + + # spend the "locked" UTxO + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + execution_units=(plutus_op.execution_cost.per_time, plutus_op.execution_cost.per_space), + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + datum_value=plutus_op.datum_value if plutus_op.datum_value else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file if plutus_op.redeemer_cbor_file else "", + redeemer_value=plutus_op.redeemer_value if plutus_op.redeemer_value else "", + ) + ] + + tx_files = tx_files._replace( + signing_key_files=list({*tx_files.signing_key_files, dst_addr.skey_file}), + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + # append change + if script_lovelace_balance > amount + redeem_cost.fee + fee_txsize: + txouts.append( + clusterlib.TxOut( + address=change_rec.address, + amount=script_lovelace_balance - amount - redeem_cost.fee - fee_txsize, + datum_hash=change_rec.datum_hash, + ) + ) + + for token in spent_tokens: + txouts.append( + clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin) + ) + # append change + script_token_balance = clusterlib.calculate_utxos_balance( + utxos=script_utxos, coin=token.coin + ) + if script_token_balance > token.amount: + txouts.append( + clusterlib.TxOut( + address=change_rec.address, + amount=script_token_balance - token.amount, + coin=token.coin, + datum_hash=change_rec.datum_hash, + ) + ) + + tx_raw_output = cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=txins, + txouts=txouts, + tx_files=tx_files, + fee=redeem_cost.fee + fee_txsize, + script_txins=plutus_txins, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + script_valid=script_valid, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + if not submit_tx: + return "", tx_raw_output + + dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address) + + if not script_valid: + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=collateral_utxos) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) + == dst_init_balance - collateral_utxos[0].amount + ), f"Collateral was NOT spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return "", tx_raw_output + + if expect_failure: + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed) + err = str(excinfo.value) + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + ), f"Collateral was spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return err, tx_raw_output + + cluster_obj.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_raw_output.script_txins if t.txins] + ) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + for token in spent_tokens: + script_utxos_token = [u for u in script_utxos if u.coin == token.coin] + for u in script_utxos_token: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[token.coin] + ), f"Token inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + return "", tx_raw_output + + +def _check_pretty_utxo( + cluster_obj: clusterlib.ClusterLib, tx_raw_output: clusterlib.TxRawOutput +) -> str: + """Check that pretty printed `query utxo` output looks as expected.""" + err = "" + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + utxo_out = ( + cluster_obj.cli( + [ + "query", + "utxo", + "--tx-in", + f"{txid}#0", + *cluster_obj.magic_args, + ] + ) + .stdout.decode("utf-8") + .split() + ) + + cluster_era = VERSIONS.cluster_era_name.title() + datum_hash = clusterlib_utils.datum_hash_from_txout( + cluster_obj=cluster_obj, txout=tx_raw_output.txouts[0] + ) + expected_out = [ + "TxHash", + "TxIx", + "Amount", + "--------------------------------------------------------------------------------------", + txid, + "0", + str(tx_raw_output.txouts[0].amount), + tx_raw_output.txouts[0].coin, + "+", + "TxOutDatumHash", + f"ScriptDataIn{cluster_era}Era", + f'"{datum_hash}"', + ] + + if utxo_out != expected_out: + err = f"Pretty UTxO output doesn't match expected output:\n{utxo_out}\nvs\n{expected_out}" + + return err + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestLocking: + """Tests for Tx output locking using Plutus smart contracts.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Corresponds to Exercise 3 for Alonzo Blue. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + utxo_err = _check_pretty_utxo(cluster_obj=cluster, tx_raw_output=tx_output_fund) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + if utxo_err: + pytest.fail(utxo_err) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + def test_context_equivalence( + self, + cluster: clusterlib.ClusterLib, + pool_users: List[clusterlib.PoolUser], + ): + """Test context equivalence while spending a locked UTxO. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * spend the locked UTxO using the derived redeemer + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 10_000_000 + deposit_amount = cluster.g_query.get_address_deposit() + + # create stake address registration cert + stake_addr_reg_cert_file = cluster.g_stake_address.gen_stake_addr_registration_cert( + addr_name=f"{temp_template}_addr0", + stake_vkey_file=pool_users[0].stake.vkey_file, + ) + + tx_files = clusterlib.TxFiles(certificate_files=[stake_addr_reg_cert_file]) + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_op_dummy = plutus_common.PlutusOp( + script_file=plutus_common.CONTEXT_EQUIVALENCE_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=redeemer_file_dummy, + execution_cost=plutus_common.CONTEXT_EQUIVALENCE_COST, + ) + + # fund the script address + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + plutus_op=plutus_op_dummy, + amount=amount, + deposit_amount=deposit_amount, + ) + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + __, tx_output_dummy = _spend_locked_txin( + temp_template=f"{temp_template}_dummy", + cluster_obj=cluster, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_dummy, + amount=amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + submit_tx=False, + ) + assert tx_output_dummy + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + tx_file_dummy = Path(f"{tx_output_dummy.out_file.with_suffix('')}.signed") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_op = plutus_op_dummy._replace(redeemer_file=redeemer_file) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("embed_datum", (True, False), ids=("embedded_datum", "datum")) + @pytest.mark.parametrize( + "variant", + ( + "typed_json", + "typed_cbor", + "untyped_value", + "untyped_json", + "untyped_cbor", + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + request: FixtureRequest, + embed_datum: bool, + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "guessing game" scripts that expect specific datum and redeemer value. + Test both typed and untyped redeemer and datum. + Test passing datum and redeemer to `cardano-cli` as value, json file and cbor file. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + amount = 2_000_000 + + datum_file: Optional[Path] = None + datum_cbor_file: Optional[Path] = None + datum_value: Optional[str] = None + redeemer_file: Optional[Path] = None + redeemer_cbor_file: Optional[Path] = None + redeemer_value: Optional[str] = None + + if variant == "typed_json": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "typed_cbor": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_TYPED_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_TYPED_CBOR + elif variant == "untyped_value": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_value = "42" + redeemer_value = "42" + elif variant == "untyped_json": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_file = plutus_common.DATUM_42 + redeemer_file = plutus_common.REDEEMER_42 + elif variant == "untyped_cbor": # noqa: SIM106 + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + else: + raise AssertionError("Unknown test variant.") + + execution_cost = plutus_common.GUESSING_GAME[plutus_version].execution_cost + if script_file == plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file: + execution_cost = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost + + plutus_op = plutus_common.PlutusOp( + script_file=script_file, + datum_file=datum_file, + datum_cbor_file=datum_cbor_file, + datum_value=datum_value, + redeemer_file=redeemer_file, + redeemer_cbor_file=redeemer_cbor_file, + redeemer_value=redeemer_value, + execution_cost=execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + embed_datum=embed_datum, + ) + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v1_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("plutus_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + script_file1_v1 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V1 + execution_cost1_v1 = plutus_common.ALWAYS_SUCCEEDS_COST + script_file2_v1 = plutus_common.GUESSING_GAME_PLUTUS_V1 + # this is higher than `plutus_common.GUESSING_GAME_COST`, because the script + # context has changed to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=388_458_303, per_space=1_031_312, fixed_cost=87_515 + ) + else: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=280_668_068, per_space=1_031_312, fixed_cost=79_743 + ) + + script_file1_v2 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V2 + execution_cost1_v2 = plutus_common.ALWAYS_SUCCEEDS_V2_COST + script_file2_v2 = plutus_common.GUESSING_GAME_PLUTUS_V2 + execution_cost2_v2 = plutus_common.ExecutionCost( + per_time=208_314_784, + per_space=662_274, + fixed_cost=53_233, + ) + + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + elif plutus_version == "mix_v1_v2": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + elif plutus_version == "plutus_v2": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + else: + raise AssertionError("Unknown test variant.") + + plutus_op1 = plutus_common.PlutusOp( + script_file=script_file1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=execution_cost1, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=script_file2, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_TYPED_CBOR, + execution_cost=execution_cost2, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=amount + redeem_cost1.fee, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + txouts=txouts_fund, + tx_files=tx_files_fund, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + join_txouts=False, + ) + + txid_fund = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos1 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#0", coins=[clusterlib.DEFAULT_COIN] + ) + script_utxos2 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#1", coins=[clusterlib.DEFAULT_COIN] + ) + collateral_utxos1 = cluster.g_query.get_utxo(txin=f"{txid_fund}#2") + collateral_utxos2 = cluster.g_query.get_utxo(txin=f"{txid_fund}#3") + + assert script_utxos1 and script_utxos2, "No script UTxOs" + assert collateral_utxos1 and collateral_utxos2, "No collateral UTxOs" + + assert ( + script_utxos1[0].amount == amount + redeem_cost1.fee + ), f"Incorrect balance for script address `{script_utxos1[0].address}`" + assert ( + script_utxos2[0].amount == amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE + ), f"Incorrect balance for script address `{script_utxos2[0].address}`" + + # Step 2: spend the "locked" UTxOs + + # for mypy + assert plutus_op1.execution_cost and plutus_op2.execution_cost + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + execution_units=( + plutus_op1.execution_cost.per_time, + plutus_op1.execution_cost.per_space, + ), + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + execution_units=( + plutus_op2.execution_cost.per_time, + plutus_op2.execution_cost.per_space, + ), + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + tx_output_redeem = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts_redeem, + tx_files=tx_files_redeem, + fee=redeem_cost1.fee + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + dst_init_balance = cluster.g_query.get_address_balance(payment_addrs[1].address) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed_redeem, + txins=[t.txins[0] for t in tx_output_redeem.script_txins if t.txins], + ) + + assert ( + cluster.g_query.get_address_balance(payment_addrs[1].address) + == dst_init_balance + amount * 2 + ), f"Incorrect balance for destination address `{payment_addrs[1].address}`" + + script_utxos_lovelace = [ + u for u in [*script_utxos1, *script_utxos2] if u.coin == clusterlib.DEFAULT_COIN + ] + for u in script_utxos_lovelace: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + @allure.link(helpers.get_vcs_link()) + def test_always_fails( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + worker_id: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred, collateral UTxO was not spent + and the expected error was raised + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + logfiles.add_ignore_rule( + files_glob="*.stdout", + regex="ValidationTagMismatch", + ignore_file_id=worker_id, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + err, __ = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + expect_failure=True, + ) + assert "PlutusFailure" in err, err + + # wait a bit so there's some time for error messages to appear in log file + time.sleep(1 if cluster.network_magic == configuration.NETWORK_MAGIC_LOCAL else 5) + + @allure.link(helpers.get_vcs_link()) + def test_script_invalid( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test failing script together with the `--script-invalid` argument - collateral is taken. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was spent + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + # include any payment txin + txins = [ + r + for r in cluster.g_query.get_utxo( + address=payment_addrs[0].address, coins=[clusterlib.DEFAULT_COIN] + ) + if not (r.datum_hash or r.inline_datum_hash) + ][:1] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + txins=txins, + tx_files=tx_files, + script_valid=False, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_token_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with native tokens and spending the locked UTxO. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO + * check that the expected amounts of Lovelace and native tokens were spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + amount = 2_000_000 + token_amount = 100 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + tokens=tokens_rec, + ) + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + tokens=tokens_rec, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_partial_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending part of funds (Lovelace and native tokens) on a locked UTxO. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO and create new locked UTxO with change + * check that the expected amounts of Lovelace and native tokens were spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + token_rand = clusterlib.get_rand_str(5) + + amount_fund = 6_000_000 + amount_spend = 2_000_000 + token_amount_fund = 100 + token_amount_spend = 20 + + # add extra fee for tokens + fee_redeem_txsize = FEE_REDEEM_TXSIZE + 5_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount_fund, + ) + tokens_fund_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount_fund, + fee_txsize=fee_redeem_txsize, + tokens=tokens_fund_rec, + ) + + tokens_spend_rec = [ + plutus_common.Token(coin=t.token, amount=token_amount_spend) for t in tokens + ] + + __, tx_output_spend = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_spend, + fee_txsize=fee_redeem_txsize, + tokens=tokens_spend_rec, + ) + + txid_spend = cluster.g_transaction.get_txid(tx_body_file=tx_output_spend.out_file) + change_utxos = cluster.g_query.get_utxo(txin=f"{txid_spend}#1") + + # check that the expected amounts of Lovelace and native tokens were spent and change UTxOs + # with appropriate datum hash were created + token_amount_exp = token_amount_fund - token_amount_spend + assert len(change_utxos) == len(tokens_spend_rec) + 1 + for u in change_utxos: + if u.coin == clusterlib.DEFAULT_COIN: + assert u.amount == amount_fund - amount_spend + else: + assert u.amount == token_amount_exp + assert u.datum_hash == script_utxos[0].datum_hash + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("scenario", ("max", "max+1", "none")) + @common.PARAM_PLUTUS_VERSION + def test_collaterals( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + scenario: str, + plutus_version: str, + ): + """Test dividing required collateral amount into multiple collateral UTxOs. + + Test 3 scenarios: + 1. maximum allowed number of collateral inputs + 2. more collateral inputs than what is allowed + 3. no collateral input + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * create multiple UTxOs for collateral + * spend the locked UTxO + * check that the expected amount was spent when success is expected + * OR check that the amount was not transferred and collateral UTxO was not spent + when failure is expected + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{scenario}" + amount = 2_000_000 + + max_collateral_ins = cluster.g_query.get_protocol_params()["maxCollateralInputs"] + collateral_utxos = [] + + if scenario == "max": + collateral_num = max_collateral_ins + exp_err = "" + collateral_fraction_offset = 250_000.0 + elif scenario == "max+1": + collateral_num = max_collateral_ins + 1 + exp_err = "TooManyCollateralInputs" + collateral_fraction_offset = 250_000.0 + else: + collateral_num = 0 + exp_err = "Transaction body has no collateral inputs" + collateral_fraction_offset = 1.0 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, fund_collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + collateral_fraction_offset=collateral_fraction_offset, + ) + + if collateral_num: + # instead of using the collateral UTxO created by `_fund_script`, create multiple new + # collateral UTxOs with the combined amount matching the original UTxO + collateral_amount_part = int(fund_collateral_utxos[0].amount // collateral_num) + 1 + txouts_collaterals = [ + clusterlib.TxOut(address=dst_addr.address, amount=collateral_amount_part) + for __ in range(collateral_num) + ] + tx_files_collaterals = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + tx_output_collaterals = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_collaterals", + txouts=txouts_collaterals, + tx_files=tx_files_collaterals, + join_txouts=False, + ) + txid_collaterals = cluster.g_transaction.get_txid( + tx_body_file=tx_output_collaterals.out_file + ) + _utxos_nested = [ + cluster.g_query.get_utxo(txin=f"{txid_collaterals}#{i}") + for i in range(collateral_num) + ] + collateral_utxos = list(itertools.chain.from_iterable(_utxos_nested)) + + if exp_err: + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert exp_err in err_str, err_str + else: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestDatum: + """Tests for datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + def test_datum_on_key_credential_address( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test creating UTxO with datum on address with key credentials (non-script address).""" + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount, + datum_hash_file=plutus_common.DATUM_42_TYPED, + ) + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_raw_output = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output) + datum_utxo = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0] + assert datum_utxo.datum_hash, f"UTxO should have datum hash: {datum_utxo}" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegative: + """Tests for Tx output locking using Plutus smart contracts that are expected to fail.""" + + @pytest.fixture + def pparams(self, cluster: clusterlib.ClusterLib) -> dict: + return cluster.g_query.get_protocol_params() + + @pytest.fixture + def fund_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + request: SubRequest, + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp]: + plutus_version = request.param + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=2_000_000, + ) + + return script_utxos, collateral_utxos, plutus_op + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @pytest.mark.parametrize( + "variant", + ( + "42_43", # correct datum, wrong redeemer + "43_42", # wrong datum, correct redeemer + "43_43", # wrong datum and redeemer + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_invalid_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "guessing game" script that expects specific datum and redeemer value. + Test negative scenarios where datum or redeemer value is different than expected. + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was not spent + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{variant}" + amount = 2_000_000 + + if variant == "42_43": + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + elif variant == "43_42": + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "43_43": # noqa: SIM106 + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + else: + raise AssertionError("Unknown test variant.") + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME[plutus_version].script_file, + datum_file=datum_file, + redeemer_file=redeemer_file, + execution_cost=plutus_common.GUESSING_GAME[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + err, __ = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + expect_failure=True, + ) + + assert "ValidationTagMismatch (IsValid True)" in err, err + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_w_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while collateral contains native tokens. + + Expect failure. + + * create a collateral UTxO with native tokens + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + amount = 2_000_000 + token_amount = 100 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + tokens_collateral=tokens_rec, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "CollateralContainsNonADA" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_same_collateral_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using the same UTxO as collateral. + + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO while using the same UTxO as collateral + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_CBOR, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, *__ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=script_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert "cardano-cli transaction submit" in err_str, err_str + assert "ScriptsNotPaidUTxO" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_percent( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend locked UTxO while collateral is less than required. + + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * create a collateral UTxO with amount of ADA less than required by `collateralPercentage` + * try to spend the UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + # increase fixed cost so the required collateral is higher than minimum collateral of 2 ADA + execution_cost = plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost + execution_cost_increased = execution_cost._replace(fixed_cost=2_000_000) + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=execution_cost_increased, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + collateral_fraction_offset=0.9, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "InsufficientCollateral" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_two_scripts_spending_one_fail( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx, one fails. + + Expect failure. + + * create a Tx output with a datum hash at the script addresses + * try to spend the locked UTxOs + * check that the expected error was raised + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + plutus_op1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + script2_hash = helpers.decode_bech32(bech32=script_address2)[2:] + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=amount + redeem_cost1.fee, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + txouts=txouts_fund, + tx_files=tx_files_fund, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + join_txouts=False, + ) + + txid_fund = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos1 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#0", coins=[clusterlib.DEFAULT_COIN] + ) + script_utxos2 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#1", coins=[clusterlib.DEFAULT_COIN] + ) + collateral_utxos1 = cluster.g_query.get_utxo(txin=f"{txid_fund}#2") + collateral_utxos2 = cluster.g_query.get_utxo(txin=f"{txid_fund}#3") + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + execution_units=( + plutus_op1.execution_cost.per_time, + plutus_op1.execution_cost.per_space, + ), + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + execution_units=( + plutus_op2.execution_cost.per_time, + plutus_op2.execution_cost.per_space, + ), + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + + tx_output_redeem = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts_redeem, + tx_files=tx_files_redeem, + fee=redeem_cost1.fee + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed_redeem) + + err_str = str(excinfo.value) + assert rf"ScriptHash \"{script2_hash}\") fails" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(data=st.data()) + @common.hypothesis_settings(100) + @pytest.mark.parametrize( + "fund_execution_units_above_limit", + ("v1", pytest.param("v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE)), + ids=("plutus_v1", "plutus_v2"), + indirect=True, + ) + def test_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_execution_units_above_limit: Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp + ], + pparams: dict, + data: st.DataObject, + request: FixtureRequest, + ): + """Test spending a locked UTxO with a Plutus script with execution units above the limit. + + Expect failure. + + * fund the script address and create a UTxO for collateral + * try to spend the locked UTxO when execution units are set above the limits + * check that failed because the execution units were too big + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + amount = 2_000_000 + + script_utxos, collateral_utxos, plutus_op = fund_execution_units_above_limit + + per_time = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["steps"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_time > pparams["maxTxExecutionUnits"]["steps"] + + per_space = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["memory"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_space > pparams["maxTxExecutionUnits"]["memory"] + + fixed_cost = pparams["txFeeFixed"] + + high_execution_cost = plutus_common.ExecutionCost( + per_time=per_time, per_space=per_space, fixed_cost=fixed_cost + ) + + plutus_op = plutus_op._replace(execution_cost=high_execution_cost) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert "ExUnitsTooBigUTxO" in err_str, err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeRedeemer: + """Tests for Tx output locking using Plutus smart contracts with wrong redeemer.""" + + MIN_INT_VAL = -common.MAX_UINT64 + AMOUNT = 2_000_000 + + def _fund_script_guessing_game( + self, + cluster_manager: cluster_management.ClusterManager, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + plutus_version: str, + ) -> FundTupleT: + """Fund a Plutus script and create the locked UTxO and collateral UTxO.""" + payment_addrs = clusterlib_utils.create_payment_addr_records( + *[f"{temp_template}_payment_addr_{i}" for i in range(2)], + cluster_obj=cluster_obj, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + payment_addrs[0], + cluster_obj=cluster_obj, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster_obj, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def fund_script_guessing_game_v1( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + ) -> FundTupleT: + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + + script_utxos, collateral_utxos, payment_addrs = self._fund_script_guessing_game( + cluster_manager=cluster_manager, + cluster_obj=cluster, + temp_template=temp_template, + plutus_version="v1", + ) + fixture_cache.value = script_utxos, collateral_utxos, payment_addrs + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def fund_script_guessing_game_v2( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + ) -> FundTupleT: + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + + script_utxos, collateral_utxos, payment_addrs = self._fund_script_guessing_game( + cluster_manager=cluster_manager, + cluster_obj=cluster, + temp_template=temp_template, + plutus_version="v2", + ) + fixture_cache.value = script_utxos, collateral_utxos, payment_addrs + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def cost_per_unit( + self, + cluster: clusterlib.ClusterLib, + ) -> plutus_common.ExecutionCost: + return plutus_common.get_cost_per_unit( + protocol_params=cluster.g_query.get_protocol_params() + ) + + def _failed_tx_build( + self, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + redeemer_content: str, + dst_addr: clusterlib.AddressRecord, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + ) -> str: + """Try to build a Tx and expect failure.""" + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + outfile.write(redeemer_content) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + ) + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + return str(excinfo.value) + + def _int_out_of_range( + self, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + redeemer_value: int, + dst_addr: clusterlib.AddressRecord, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + ): + """Try to spend a locked UTxO with redeemer int value that is not in allowed range.""" + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=redeemer_file if redeemer_content else "", + redeemer_value=str(redeemer_value) if not redeemer_content else "", + ) + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + err_str = str(excinfo.value) + assert "Value out of range within the script data" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given( + redeemer_value=st.integers(min_value=MIN_INT_VAL, max_value=common.MAX_UINT64) + ) + @hypothesis.example(redeemer_value=MIN_INT_VAL) + @hypothesis.example(redeemer_value=common.MAX_UINT64) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_inside_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with an unexpected redeemer value. + + Expect failure. + """ + hypothesis.assume(redeemer_value != 42) + + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + # try to spend the "locked" UTxO + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + dst_addr = payment_addrs[1] + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=redeemer_file if redeemer_content else "", + redeemer_value=str(redeemer_value) if not redeemer_content else "", + ) + ] + tx_raw_output = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed) + + err_str = str(excinfo.value) + assert "ValidationTagMismatch (IsValid True)" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(max_value=MIN_INT_VAL - 1)) + @hypothesis.example(redeemer_value=MIN_INT_VAL - 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_bellow_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a redeemer int value < minimum allowed value. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + self._int_out_of_range( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(min_value=common.MAX_UINT64 + 1)) + @hypothesis.example(redeemer_value=common.MAX_UINT64 + 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_above_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a redeemer int value > maximum allowed value. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + self._int_out_of_range( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO with an invalid redeemer type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value.hex()}, outfile) + + # try to spend the "locked" UTxO + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + dst_addr = payment_addrs[1] + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + ) + ] + + tx_raw_output = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed) + + err_str = str(excinfo.value) + assert "ValidationTagMismatch (IsValid True)" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO using redeemer that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = json.dumps( + {"constructor": 0, "fields": [{"bytes": redeemer_value.hex()}]} + ) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert "must consist of at most 64 bytes" in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in typed format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = json.dumps({"constructor": 0, "fields": [{"int": redeemer_value.hex()}]}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "int" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"int": redeemer_value.hex()}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "int" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in typed format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"constructor": 0, "fields": [{"bytes": redeemer_value}]}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "bytes" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"bytes": redeemer_value}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "bytes" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_invalid_json( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: str, + ): + """Try to build a Tx using a redeemer value that is invalid JSON. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = f'{{"{redeemer_value}"}}' + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert "Invalid JSON format" in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_invalid_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON typed schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({redeemer_type: 42}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err + ), err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_invalid_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON untyped schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({redeemer_type: 42}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err + ), err + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeDatum: + """Tests for Tx output locking using Plutus smart contracts with wrong datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.parametrize("address_type", ("script_address", "key_address")) + @common.PARAM_PLUTUS_VERSION + def test_no_datum_txout( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + address_type: str, + plutus_version: str, + ): + """Test using UTxO without datum hash in place of locked UTxO. + + Expect failure. + + * create a Tx output without a datum hash + * try to spend the UTxO like it was locked Plutus UTxO + * check that the expected error was raised + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{address_type}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + if address_type == "script_address": + redeem_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + else: + redeem_address = payment_addrs[2].address + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + txouts = [ + clusterlib.TxOut( + address=redeem_address, amount=amount + redeem_cost.fee + FEE_REDEEM_TXSIZE + ), + clusterlib.TxOut(address=payment_addr.address, amount=redeem_cost.collateral), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + join_txouts=False, + ) + txid = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos = cluster.g_query.get_utxo(txin=f"{txid}#0") + collateral_utxos = cluster.g_query.get_utxo(txin=f"{txid}#1") + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "NonOutputSupplimentaryDatums" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_lock_tx_invalid_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: str, + plutus_version: str, + ): + """Test locking a Tx output with an invalid datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{datum_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + with pytest.raises(clusterlib.CLIError) as excinfo: + _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_tx_wrong_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output and try to spend it with a wrong datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op_1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op_1.execution_cost # for mypy + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op_1, + amount=amount, + ) + + # use a wrong datum to try to unlock the funds + plutus_op_2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_2, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "NonOutputSupplimentaryDatums" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_non_script_utxo( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend a non-script UTxO with datum as if it was script locked UTxO. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + amount_fund = 4_000_000 + amount_redeem = 2_000_000 + amount_collateral = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = plutus_common.DATUM_42_TYPED + + datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file=datum_file) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=datum_file, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + # create datum and collateral UTxOs + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount_fund, + datum_hash=datum_hash, + ), + clusterlib.TxOut( + address=payment_addr.address, + amount=amount_collateral, + ), + ] + tx_files_fund = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_raw_output = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files_fund, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output) + datum_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=dst_addr.address, datum_hash=datum_hash + )[0] + collateral_utxos = clusterlib.filter_utxos( + utxos=out_utxos, address=payment_addr.address, utxo_ix=datum_utxo.utxo_ix + 1 + ) + assert ( + datum_utxo.datum_hash == datum_hash + ), f"UTxO should have datum hash '{datum_hash}': {datum_utxo}" + + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file, dst_addr.skey_file] + ) + + # try to spend the "locked" UTxO + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=[datum_utxo], + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_redeem, + tx_files=tx_files_redeem, + ) + + err_str = str(excinfo.value) + assert "ExtraneousScriptWitnessesUTXOW" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: bytes, + plutus_version: str, + ): + """Try to lock a UTxO with datum that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": datum_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + with pytest.raises(clusterlib.CLIError) as excinfo: + _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + +@pytest.mark.testnets +class TestCompatibility: + """Tests for checking compatibility with previous Tx eras.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era > VERSIONS.ALONZO, + reason="runs only with Tx era <= Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv2_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV2 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v2"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v2"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV2 is not supported" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era >= VERSIONS.ALONZO, + reason="runs only with Tx era < Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv1_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV1 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v1"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v1"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV1 is not supported" in err_str, err_str diff --git a/cardano_node_tests/tests/test_plutus/test_spend_raw.py b/cardano_node_tests/tests/test_plutus/test_spend_raw.py new file mode 100644 index 000000000..a264ae894 --- /dev/null +++ b/cardano_node_tests/tests/test_plutus/test_spend_raw.py @@ -0,0 +1,3052 @@ +"""Tests for spending with Plutus using `transaction build-raw`.""" +import itertools +import json +import logging +import shutil +import time +from pathlib import Path +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import allure +import hypothesis +import hypothesis.strategies as st +import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import SubRequest +from cardano_clusterlib import clusterlib + +from cardano_node_tests.cluster_management import cluster_management +from cardano_node_tests.tests import common +from cardano_node_tests.tests import plutus_common +from cardano_node_tests.utils import clusterlib_utils +from cardano_node_tests.utils import configuration +from cardano_node_tests.utils import dbsync_utils +from cardano_node_tests.utils import helpers +from cardano_node_tests.utils import logfiles +from cardano_node_tests.utils import tx_view +from cardano_node_tests.utils.versions import VERSIONS + +LOGGER = logging.getLogger(__name__) + +# skip all tests if Tx era < alonzo +pytestmark = [ + pytest.mark.smoke, +] + + +FundTupleT = Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], List[clusterlib.AddressRecord] +] + +# approx. fee for Tx size +FEE_REDEEM_TXSIZE = 400_000 + + +@pytest.fixture +def payment_addrs( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.AddressRecord]: + """Create new payment addresses.""" + test_id = common.get_test_id(cluster) + addrs = clusterlib_utils.create_payment_addr_records( + *[f"{test_id}_payment_addr_{i}" for i in range(3)], + cluster_obj=cluster, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + addrs[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return addrs + + +@pytest.fixture +def pool_users( + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, +) -> List[clusterlib.PoolUser]: + """Create new pool users.""" + test_id = common.get_test_id(cluster) + created_users = clusterlib_utils.create_pool_users( + cluster_obj=cluster, + name_template=f"{test_id}_pool_users", + no_of_addr=2, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + created_users[0], + cluster_obj=cluster, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + return created_users + + +def _fund_script( + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + payment_addr: clusterlib.AddressRecord, + dst_addr: clusterlib.AddressRecord, + plutus_op: plutus_common.PlutusOp, + amount: int, + fee_txsize: int = FEE_REDEEM_TXSIZE, + deposit_amount: int = 0, + tokens: Optional[List[plutus_common.Token]] = None, # tokens must already be in `payment_addr` + tokens_collateral: Optional[ + List[plutus_common.Token] + ] = None, # tokens must already be in `payment_addr` + collateral_fraction_offset: float = 1.0, + embed_datum: bool = False, +) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], clusterlib.TxRawOutput]: + """Fund a Plutus script and create the locked UTxO and collateral UTxO.""" + # pylint: disable=too-many-locals,too-many-arguments + assert plutus_op.execution_cost # for mypy + + stokens = tokens or () + ctokens = tokens_collateral or () + + script_address = cluster_obj.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + collateral_fraction_offset=collateral_fraction_offset, + ) + + # create a Tx output with a datum hash at the script address + + tx_files = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file], + ) + + script_txout = plutus_common.txout_factory( + address=script_address, + amount=amount + redeem_cost.fee + fee_txsize + deposit_amount, + plutus_op=plutus_op, + embed_datum=embed_datum, + ) + + txouts = [ + script_txout, + # for collateral + clusterlib.TxOut(address=dst_addr.address, amount=redeem_cost.collateral), + ] + + for token in stokens: + txouts.append(script_txout._replace(amount=token.amount, coin=token.coin)) + + for token in ctokens: + txouts.append( + clusterlib.TxOut( + address=dst_addr.address, + amount=token.amount, + coin=token.coin, + ) + ) + + tx_raw_output = cluster_obj.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_step1", + txouts=txouts, + tx_files=tx_files, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + ) + + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + script_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#0") + assert script_utxos, "No script UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos) == txouts[0].amount + ), f"Incorrect balance for script address `{script_address}`" + + collateral_utxos = cluster_obj.g_query.get_utxo(txin=f"{txid}#1") + assert collateral_utxos, "No collateral UTxO" + + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos) == redeem_cost.collateral + ), f"Incorrect balance for collateral address `{dst_addr.address}`" + + for token in stokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=script_utxos, coin=token.coin) == token.amount + ), f"Incorrect token balance for script address `{script_address}`" + + for token in ctokens: + assert ( + clusterlib.calculate_utxos_balance(utxos=collateral_utxos, coin=token.coin) + == token.amount + ), f"Incorrect token balance for address `{dst_addr.address}`" + + if VERSIONS.transaction_era >= VERSIONS.ALONZO: + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + return script_utxos, collateral_utxos, tx_raw_output + + +def _spend_locked_txin( # noqa: C901 + temp_template: str, + cluster_obj: clusterlib.ClusterLib, + dst_addr: clusterlib.AddressRecord, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + plutus_op: plutus_common.PlutusOp, + amount: int, + fee_txsize: int = FEE_REDEEM_TXSIZE, + txins: clusterlib.OptionalUTXOData = (), + tx_files: Optional[clusterlib.TxFiles] = None, + invalid_hereafter: Optional[int] = None, + invalid_before: Optional[int] = None, + tokens: Optional[List[plutus_common.Token]] = None, + expect_failure: bool = False, + script_valid: bool = True, + submit_tx: bool = True, +) -> Tuple[str, clusterlib.TxRawOutput]: + """Spend the locked UTxO.""" + # pylint: disable=too-many-arguments,too-many-locals + assert plutus_op.execution_cost + + tx_files = tx_files or clusterlib.TxFiles() + spent_tokens = tokens or () + + # change will be returned to address of the first script + change_rec = script_utxos[0] + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster_obj.g_query.get_protocol_params(), + ) + + script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN] + script_lovelace_balance = clusterlib.calculate_utxos_balance( + utxos=[*script_utxos_lovelace, *txins] + ) + + # spend the "locked" UTxO + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_op.script_file, + collaterals=collateral_utxos, + execution_units=(plutus_op.execution_cost.per_time, plutus_op.execution_cost.per_space), + datum_file=plutus_op.datum_file if plutus_op.datum_file else "", + datum_cbor_file=plutus_op.datum_cbor_file if plutus_op.datum_cbor_file else "", + datum_value=plutus_op.datum_value if plutus_op.datum_value else "", + redeemer_file=plutus_op.redeemer_file if plutus_op.redeemer_file else "", + redeemer_cbor_file=plutus_op.redeemer_cbor_file if plutus_op.redeemer_cbor_file else "", + redeemer_value=plutus_op.redeemer_value if plutus_op.redeemer_value else "", + ) + ] + + tx_files = tx_files._replace( + signing_key_files=list({*tx_files.signing_key_files, dst_addr.skey_file}), + ) + txouts = [ + clusterlib.TxOut(address=dst_addr.address, amount=amount), + ] + # append change + if script_lovelace_balance > amount + redeem_cost.fee + fee_txsize: + txouts.append( + clusterlib.TxOut( + address=change_rec.address, + amount=script_lovelace_balance - amount - redeem_cost.fee - fee_txsize, + datum_hash=change_rec.datum_hash, + ) + ) + + for token in spent_tokens: + txouts.append( + clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin) + ) + # append change + script_token_balance = clusterlib.calculate_utxos_balance( + utxos=script_utxos, coin=token.coin + ) + if script_token_balance > token.amount: + txouts.append( + clusterlib.TxOut( + address=change_rec.address, + amount=script_token_balance - token.amount, + coin=token.coin, + datum_hash=change_rec.datum_hash, + ) + ) + + tx_raw_output = cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txins=txins, + txouts=txouts, + tx_files=tx_files, + fee=redeem_cost.fee + fee_txsize, + script_txins=plutus_txins, + invalid_hereafter=invalid_hereafter, + invalid_before=invalid_before, + script_valid=script_valid, + ) + tx_signed = cluster_obj.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + if not submit_tx: + return "", tx_raw_output + + dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address) + + if not script_valid: + cluster_obj.g_transaction.submit_tx(tx_file=tx_signed, txins=collateral_utxos) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) + == dst_init_balance - collateral_utxos[0].amount + ), f"Collateral was NOT spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return "", tx_raw_output + + if expect_failure: + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed) + err = str(excinfo.value) + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + ), f"Collateral was spent from `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were unexpectedly spent for `{u.address}`" + + return err, tx_raw_output + + cluster_obj.g_transaction.submit_tx( + tx_file=tx_signed, txins=[t.txins[0] for t in tx_raw_output.script_txins if t.txins] + ) + + assert ( + cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance + amount + ), f"Incorrect balance for destination address `{dst_addr.address}`" + + for u in script_utxos_lovelace: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + for token in spent_tokens: + script_utxos_token = [u for u in script_utxos if u.coin == token.coin] + for u in script_utxos_token: + assert not cluster_obj.g_query.get_utxo( + utxo=u, coins=[token.coin] + ), f"Token inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + dbsync_utils.check_tx(cluster_obj=cluster_obj, tx_raw_output=tx_raw_output) + + return "", tx_raw_output + + +def _check_pretty_utxo( + cluster_obj: clusterlib.ClusterLib, tx_raw_output: clusterlib.TxRawOutput +) -> str: + """Check that pretty printed `query utxo` output looks as expected.""" + err = "" + txid = cluster_obj.g_transaction.get_txid(tx_body_file=tx_raw_output.out_file) + + utxo_out = ( + cluster_obj.cli( + [ + "query", + "utxo", + "--tx-in", + f"{txid}#0", + *cluster_obj.magic_args, + ] + ) + .stdout.decode("utf-8") + .split() + ) + + cluster_era = VERSIONS.cluster_era_name.title() + datum_hash = clusterlib_utils.datum_hash_from_txout( + cluster_obj=cluster_obj, txout=tx_raw_output.txouts[0] + ) + expected_out = [ + "TxHash", + "TxIx", + "Amount", + "--------------------------------------------------------------------------------------", + txid, + "0", + str(tx_raw_output.txouts[0].amount), + tx_raw_output.txouts[0].coin, + "+", + "TxOutDatumHash", + f"ScriptDataIn{cluster_era}Era", + f'"{datum_hash}"', + ] + + if utxo_out != expected_out: + err = f"Pretty UTxO output doesn't match expected output:\n{utxo_out}\nvs\n{expected_out}" + + return err + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestLocking: + """Tests for Tx output locking using Plutus smart contracts.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Corresponds to Exercise 3 for Alonzo Blue. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, tx_output_fund = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + utxo_err = _check_pretty_utxo(cluster_obj=cluster, tx_raw_output=tx_output_fund) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + if utxo_err: + pytest.fail(utxo_err) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + not shutil.which("create-script-context"), + reason="cannot find `create-script-context` on the PATH", + ) + @pytest.mark.dbsync + def test_context_equivalence( + self, + cluster: clusterlib.ClusterLib, + pool_users: List[clusterlib.PoolUser], + ): + """Test context equivalence while spending a locked UTxO. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * generate a dummy redeemer and a dummy Tx + * derive the correct redeemer from the dummy Tx + * spend the locked UTxO using the derived redeemer + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 10_000_000 + deposit_amount = cluster.g_query.get_address_deposit() + + # create stake address registration cert + stake_addr_reg_cert_file = cluster.g_stake_address.gen_stake_addr_registration_cert( + addr_name=f"{temp_template}_addr0", + stake_vkey_file=pool_users[0].stake.vkey_file, + ) + + tx_files = clusterlib.TxFiles(certificate_files=[stake_addr_reg_cert_file]) + + # generate a dummy redeemer in order to create a txbody from which + # we can generate a tx and then derive the correct redeemer + redeemer_file_dummy = Path(f"{temp_template}_dummy_script_context.redeemer") + clusterlib_utils.create_script_context( + cluster_obj=cluster, plutus_version=1, redeemer_file=redeemer_file_dummy + ) + + plutus_op_dummy = plutus_common.PlutusOp( + script_file=plutus_common.CONTEXT_EQUIVALENCE_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=redeemer_file_dummy, + execution_cost=plutus_common.CONTEXT_EQUIVALENCE_COST, + ) + + # fund the script address + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=pool_users[0].payment, + dst_addr=pool_users[1].payment, + plutus_op=plutus_op_dummy, + amount=amount, + deposit_amount=deposit_amount, + ) + + invalid_hereafter = cluster.g_query.get_slot_no() + 200 + + __, tx_output_dummy = _spend_locked_txin( + temp_template=f"{temp_template}_dummy", + cluster_obj=cluster, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_dummy, + amount=amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + script_valid=False, + submit_tx=False, + ) + assert tx_output_dummy + + # generate the "real" redeemer + redeemer_file = Path(f"{temp_template}_script_context.redeemer") + tx_file_dummy = Path(f"{tx_output_dummy.out_file.with_suffix('')}.signed") + + try: + clusterlib_utils.create_script_context( + cluster_obj=cluster, + plutus_version=1, + redeemer_file=redeemer_file, + tx_file=tx_file_dummy, + ) + except AssertionError as err: + err_msg = str(err) + if "DeserialiseFailure" in err_msg: + pytest.xfail("DeserialiseFailure: see issue #944") + if "TextEnvelopeTypeError" in err_msg and cluster.use_cddl: # noqa: SIM106 + pytest.xfail( + "TextEnvelopeTypeError: `create-script-context` doesn't work with CDDL format" + ) + else: + raise + + plutus_op = plutus_op_dummy._replace(redeemer_file=redeemer_file) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=pool_users[1].payment, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + tx_files=tx_files, + invalid_before=1, + invalid_hereafter=invalid_hereafter, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("embed_datum", (True, False), ids=("embedded_datum", "datum")) + @pytest.mark.parametrize( + "variant", + ( + "typed_json", + "typed_cbor", + "untyped_value", + "untyped_json", + "untyped_cbor", + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + request: FixtureRequest, + embed_datum: bool, + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "guessing game" scripts that expect specific datum and redeemer value. + Test both typed and untyped redeemer and datum. + Test passing datum and redeemer to `cardano-cli` as value, json file and cbor file. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + amount = 2_000_000 + + datum_file: Optional[Path] = None + datum_cbor_file: Optional[Path] = None + datum_value: Optional[str] = None + redeemer_file: Optional[Path] = None + redeemer_cbor_file: Optional[Path] = None + redeemer_value: Optional[str] = None + + if variant == "typed_json": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "typed_cbor": + script_file = plutus_common.GUESSING_GAME[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_TYPED_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_TYPED_CBOR + elif variant == "untyped_value": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_value = "42" + redeemer_value = "42" + elif variant == "untyped_json": + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_file = plutus_common.DATUM_42 + redeemer_file = plutus_common.REDEEMER_42 + elif variant == "untyped_cbor": # noqa: SIM106 + script_file = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file + datum_cbor_file = plutus_common.DATUM_42_CBOR + redeemer_cbor_file = plutus_common.REDEEMER_42_CBOR + else: + raise AssertionError("Unknown test variant.") + + execution_cost = plutus_common.GUESSING_GAME[plutus_version].execution_cost + if script_file == plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file: + execution_cost = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost + + plutus_op = plutus_common.PlutusOp( + script_file=script_file, + datum_file=datum_file, + datum_cbor_file=datum_cbor_file, + datum_value=datum_value, + redeemer_file=redeemer_file, + redeemer_cbor_file=redeemer_cbor_file, + redeemer_value=redeemer_value, + execution_cost=execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + embed_datum=embed_datum, + ) + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize( + "plutus_version", + ( + "plutus_v1", + pytest.param("mix_v1_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("mix_v2_v1", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + pytest.param("plutus_v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE), + ), + ) + def test_two_scripts_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * spend the locked UTxO + * check that the expected amount was spent + * (optional) check transactions in db-sync + """ + # pylint: disable=too-many-locals,too-many-statements + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + script_file1_v1 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V1 + execution_cost1_v1 = plutus_common.ALWAYS_SUCCEEDS_COST + script_file2_v1 = plutus_common.GUESSING_GAME_PLUTUS_V1 + # this is higher than `plutus_common.GUESSING_GAME_COST`, because the script + # context has changed to include more stuff + if configuration.ALONZO_COST_MODEL or VERSIONS.cluster_era == VERSIONS.ALONZO: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=388_458_303, per_space=1_031_312, fixed_cost=87_515 + ) + else: + execution_cost2_v1 = plutus_common.ExecutionCost( + per_time=280_668_068, per_space=1_031_312, fixed_cost=79_743 + ) + + script_file1_v2 = plutus_common.ALWAYS_SUCCEEDS_PLUTUS_V2 + execution_cost1_v2 = plutus_common.ALWAYS_SUCCEEDS_V2_COST + script_file2_v2 = plutus_common.GUESSING_GAME_PLUTUS_V2 + execution_cost2_v2 = plutus_common.ExecutionCost( + per_time=208_314_784, + per_space=662_274, + fixed_cost=53_233, + ) + + if plutus_version == "plutus_v1": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + elif plutus_version == "mix_v1_v2": + script_file1 = script_file1_v1 + execution_cost1 = execution_cost1_v1 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + elif plutus_version == "mix_v2_v1": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v1 + execution_cost2 = execution_cost2_v1 + elif plutus_version == "plutus_v2": + script_file1 = script_file1_v2 + execution_cost1 = execution_cost1_v2 + script_file2 = script_file2_v2 + execution_cost2 = execution_cost2_v2 + else: + raise AssertionError("Unknown test variant.") + + plutus_op1 = plutus_common.PlutusOp( + script_file=script_file1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=execution_cost1, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=script_file2, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_TYPED_CBOR, + execution_cost=execution_cost2, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=amount + redeem_cost1.fee, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + txouts=txouts_fund, + tx_files=tx_files_fund, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + join_txouts=False, + ) + + txid_fund = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos1 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#0", coins=[clusterlib.DEFAULT_COIN] + ) + script_utxos2 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#1", coins=[clusterlib.DEFAULT_COIN] + ) + collateral_utxos1 = cluster.g_query.get_utxo(txin=f"{txid_fund}#2") + collateral_utxos2 = cluster.g_query.get_utxo(txin=f"{txid_fund}#3") + + assert script_utxos1 and script_utxos2, "No script UTxOs" + assert collateral_utxos1 and collateral_utxos2, "No collateral UTxOs" + + assert ( + script_utxos1[0].amount == amount + redeem_cost1.fee + ), f"Incorrect balance for script address `{script_utxos1[0].address}`" + assert ( + script_utxos2[0].amount == amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE + ), f"Incorrect balance for script address `{script_utxos2[0].address}`" + + # Step 2: spend the "locked" UTxOs + + # for mypy + assert plutus_op1.execution_cost and plutus_op2.execution_cost + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + execution_units=( + plutus_op1.execution_cost.per_time, + plutus_op1.execution_cost.per_space, + ), + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + execution_units=( + plutus_op2.execution_cost.per_time, + plutus_op2.execution_cost.per_space, + ), + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + tx_output_redeem = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts_redeem, + tx_files=tx_files_redeem, + fee=redeem_cost1.fee + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + dst_init_balance = cluster.g_query.get_address_balance(payment_addrs[1].address) + + cluster.g_transaction.submit_tx( + tx_file=tx_signed_redeem, + txins=[t.txins[0] for t in tx_output_redeem.script_txins if t.txins], + ) + + assert ( + cluster.g_query.get_address_balance(payment_addrs[1].address) + == dst_init_balance + amount * 2 + ), f"Incorrect balance for destination address `{payment_addrs[1].address}`" + + script_utxos_lovelace = [ + u for u in [*script_utxos1, *script_utxos2] if u.coin == clusterlib.DEFAULT_COIN + ] + for u in script_utxos_lovelace: + assert not cluster.g_query.get_utxo( + utxo=u, coins=[clusterlib.DEFAULT_COIN] + ), f"Inputs were NOT spent for `{u.address}`" + + # check tx view + tx_view.check_tx_view(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_output_redeem) + + @allure.link(helpers.get_vcs_link()) + def test_always_fails( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + worker_id: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred, collateral UTxO was not spent + and the expected error was raised + """ + __: Any # mypy workaround + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + logfiles.add_ignore_rule( + files_glob="*.stdout", + regex="ValidationTagMismatch", + ignore_file_id=worker_id, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + err, __ = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + expect_failure=True, + ) + assert "PlutusFailure" in err, err + + # wait a bit so there's some time for error messages to appear in log file + time.sleep(1 if cluster.network_magic == configuration.NETWORK_MAGIC_LOCAL else 5) + + @allure.link(helpers.get_vcs_link()) + def test_script_invalid( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test failing script together with the `--script-invalid` argument - collateral is taken. + + Test with "always fails" script that fails for all datum / redeemer values. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was spent + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + # include any payment txin + txins = [ + r + for r in cluster.g_query.get_utxo( + address=payment_addrs[0].address, coins=[clusterlib.DEFAULT_COIN] + ) + if not (r.datum_hash or r.inline_datum_hash) + ][:1] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addrs[0].skey_file]) + + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + txins=txins, + tx_files=tx_files, + script_valid=False, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_txout_token_locking( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output with native tokens and spending the locked UTxO. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO + * check that the expected amounts of Lovelace and native tokens were spent + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + amount = 2_000_000 + token_amount = 100 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + tokens=tokens_rec, + ) + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + tokens=tokens_rec, + ) + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_partial_spending( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending part of funds (Lovelace and native tokens) on a locked UTxO. + + * create a Tx output that contains native tokens with a datum hash at the script address + * check that expected amounts of Lovelace and native tokens were locked at the script + address + * spend the locked UTxO and create new locked UTxO with change + * check that the expected amounts of Lovelace and native tokens were spent + * (optional) check transactions in db-sync + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + token_rand = clusterlib.get_rand_str(5) + + amount_fund = 6_000_000 + amount_spend = 2_000_000 + token_amount_fund = 100 + token_amount_spend = 20 + + # add extra fee for tokens + fee_redeem_txsize = FEE_REDEEM_TXSIZE + 5_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount_fund, + ) + tokens_fund_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount_fund, + fee_txsize=fee_redeem_txsize, + tokens=tokens_fund_rec, + ) + + tokens_spend_rec = [ + plutus_common.Token(coin=t.token, amount=token_amount_spend) for t in tokens + ] + + __, tx_output_spend = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_spend, + fee_txsize=fee_redeem_txsize, + tokens=tokens_spend_rec, + ) + + txid_spend = cluster.g_transaction.get_txid(tx_body_file=tx_output_spend.out_file) + change_utxos = cluster.g_query.get_utxo(txin=f"{txid_spend}#1") + + # check that the expected amounts of Lovelace and native tokens were spent and change UTxOs + # with appropriate datum hash were created + token_amount_exp = token_amount_fund - token_amount_spend + assert len(change_utxos) == len(tokens_spend_rec) + 1 + for u in change_utxos: + if u.coin == clusterlib.DEFAULT_COIN: + assert u.amount == amount_fund - amount_spend + else: + assert u.amount == token_amount_exp + assert u.datum_hash == script_utxos[0].datum_hash + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @pytest.mark.parametrize("scenario", ("max", "max+1", "none")) + @common.PARAM_PLUTUS_VERSION + def test_collaterals( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + scenario: str, + plutus_version: str, + ): + """Test dividing required collateral amount into multiple collateral UTxOs. + + Test 3 scenarios: + 1. maximum allowed number of collateral inputs + 2. more collateral inputs than what is allowed + 3. no collateral input + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * create multiple UTxOs for collateral + * spend the locked UTxO + * check that the expected amount was spent when success is expected + * OR check that the amount was not transferred and collateral UTxO was not spent + when failure is expected + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{scenario}" + amount = 2_000_000 + + max_collateral_ins = cluster.g_query.get_protocol_params()["maxCollateralInputs"] + collateral_utxos = [] + + if scenario == "max": + collateral_num = max_collateral_ins + exp_err = "" + collateral_fraction_offset = 250_000.0 + elif scenario == "max+1": + collateral_num = max_collateral_ins + 1 + exp_err = "TooManyCollateralInputs" + collateral_fraction_offset = 250_000.0 + else: + collateral_num = 0 + exp_err = "Transaction body has no collateral inputs" + collateral_fraction_offset = 1.0 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, fund_collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + collateral_fraction_offset=collateral_fraction_offset, + ) + + if collateral_num: + # instead of using the collateral UTxO created by `_fund_script`, create multiple new + # collateral UTxOs with the combined amount matching the original UTxO + collateral_amount_part = int(fund_collateral_utxos[0].amount // collateral_num) + 1 + txouts_collaterals = [ + clusterlib.TxOut(address=dst_addr.address, amount=collateral_amount_part) + for __ in range(collateral_num) + ] + tx_files_collaterals = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + tx_output_collaterals = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=f"{temp_template}_collaterals", + txouts=txouts_collaterals, + tx_files=tx_files_collaterals, + join_txouts=False, + ) + txid_collaterals = cluster.g_transaction.get_txid( + tx_body_file=tx_output_collaterals.out_file + ) + _utxos_nested = [ + cluster.g_query.get_utxo(txin=f"{txid_collaterals}#{i}") + for i in range(collateral_num) + ] + collateral_utxos = list(itertools.chain.from_iterable(_utxos_nested)) + + if exp_err: + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert exp_err in err_str, err_str + else: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestDatum: + """Tests for datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + def test_datum_on_key_credential_address( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test creating UTxO with datum on address with key credentials (non-script address).""" + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount, + datum_hash_file=plutus_common.DATUM_42_TYPED, + ) + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_raw_output = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output) + datum_utxo = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0] + assert datum_utxo.datum_hash, f"UTxO should have datum hash: {datum_utxo}" + + dbsync_utils.check_tx(cluster_obj=cluster, tx_raw_output=tx_raw_output) + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegative: + """Tests for Tx output locking using Plutus smart contracts that are expected to fail.""" + + @pytest.fixture + def pparams(self, cluster: clusterlib.ClusterLib) -> dict: + return cluster.g_query.get_protocol_params() + + @pytest.fixture + def fund_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + request: SubRequest, + ) -> Tuple[List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp]: + plutus_version = request.param + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=2_000_000, + ) + + return script_utxos, collateral_utxos, plutus_op + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.testnets + @pytest.mark.parametrize( + "variant", + ( + "42_43", # correct datum, wrong redeemer + "43_42", # wrong datum, correct redeemer + "43_43", # wrong datum and redeemer + ), + ) + @common.PARAM_PLUTUS_VERSION + def test_invalid_guessing_game( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + variant: str, + plutus_version: str, + ): + """Test locking a Tx output with a Plutus script and spending the locked UTxO. + + Test with "guessing game" script that expects specific datum and redeemer value. + Test negative scenarios where datum or redeemer value is different than expected. + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO + * check that the amount was not transferred and collateral UTxO was not spent + """ + __: Any # mypy workaround + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{variant}" + amount = 2_000_000 + + if variant == "42_43": + datum_file = plutus_common.DATUM_42_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + elif variant == "43_42": + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_42_TYPED + elif variant == "43_43": # noqa: SIM106 + datum_file = plutus_common.DATUM_43_TYPED + redeemer_file = plutus_common.REDEEMER_43_TYPED + else: + raise AssertionError("Unknown test variant.") + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME[plutus_version].script_file, + datum_file=datum_file, + redeemer_file=redeemer_file, + execution_cost=plutus_common.GUESSING_GAME[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + err, __ = _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + expect_failure=True, + ) + + assert "ValidationTagMismatch (IsValid True)" in err, err + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_w_tokens( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while collateral contains native tokens. + + Expect failure. + + * create a collateral UTxO with native tokens + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + token_rand = clusterlib.get_rand_str(5) + + amount = 2_000_000 + token_amount = 100 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + tokens = clusterlib_utils.new_tokens( + *[f"qacoin{token_rand}{i}".encode().hex() for i in range(5)], + cluster_obj=cluster, + temp_template=f"{temp_template}_{token_rand}", + token_mint_addr=payment_addrs[0], + issuer_addr=payment_addrs[0], + amount=token_amount, + ) + tokens_rec = [plutus_common.Token(coin=t.token, amount=t.amount) for t in tokens] + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + tokens_collateral=tokens_rec, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "CollateralContainsNonADA" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_same_collateral_txin( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test spending the locked UTxO while using the same UTxO as collateral. + + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * try to spend the locked UTxO while using the same UTxO as collateral + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_cbor_file=plutus_common.DATUM_42_CBOR, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + script_utxos, *__ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=script_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert "cardano-cli transaction submit" in err_str, err_str + assert "ScriptsNotPaidUTxO" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.dbsync + @common.PARAM_PLUTUS_VERSION + def test_collateral_percent( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend locked UTxO while collateral is less than required. + + Expect failure. + + * create a Tx output with a datum hash at the script address + * check that the expected amount was locked at the script address + * create a collateral UTxO with amount of ADA less than required by `collateralPercentage` + * try to spend the UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + # increase fixed cost so the required collateral is higher than minimum collateral of 2 ADA + execution_cost = plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost + execution_cost_increased = execution_cost._replace(fixed_cost=2_000_000) + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=execution_cost_increased, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + collateral_fraction_offset=0.9, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "InsufficientCollateral" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_two_scripts_spending_one_fail( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking two Tx outputs with two different Plutus scripts in single Tx, one fails. + + Expect failure. + + * create a Tx output with a datum hash at the script addresses + * try to spend the locked UTxOs + * check that the expected error was raised + """ + # pylint: disable=too-many-locals + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + protocol_params = cluster.g_query.get_protocol_params() + + plutus_op1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + plutus_op2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_FAILS_PLUTUS_V1, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_FAILS_COST, + ) + + # Step 1: fund the Plutus scripts + + assert plutus_op1.execution_cost and plutus_op2.execution_cost # for mypy + + script_address1 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr1", payment_script_file=plutus_op1.script_file + ) + redeem_cost1 = plutus_common.compute_cost( + execution_cost=plutus_op1.execution_cost, protocol_params=protocol_params + ) + datum_hash1 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op1.datum_file + ) + + script_address2 = cluster.g_address.gen_payment_addr( + addr_name=f"{temp_template}_addr2", payment_script_file=plutus_op2.script_file + ) + script2_hash = helpers.decode_bech32(bech32=script_address2)[2:] + redeem_cost2 = plutus_common.compute_cost( + execution_cost=plutus_op2.execution_cost, protocol_params=protocol_params + ) + datum_hash2 = cluster.g_transaction.get_hash_script_data( + script_data_file=plutus_op2.datum_file + ) + + # create a Tx output with a datum hash at the script address + + tx_files_fund = clusterlib.TxFiles( + signing_key_files=[payment_addrs[0].skey_file], + ) + txouts_fund = [ + clusterlib.TxOut( + address=script_address1, + amount=amount + redeem_cost1.fee, + datum_hash=datum_hash1, + ), + clusterlib.TxOut( + address=script_address2, + amount=amount + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + datum_hash=datum_hash2, + ), + # for collateral + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost1.collateral), + clusterlib.TxOut(address=payment_addrs[1].address, amount=redeem_cost2.collateral), + ] + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addrs[0].address, + tx_name=f"{temp_template}_step1", + txouts=txouts_fund, + tx_files=tx_files_fund, + # TODO: workaround for https://github.com/input-output-hk/cardano-node/issues/1892 + witness_count_add=2, + join_txouts=False, + ) + + txid_fund = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos1 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#0", coins=[clusterlib.DEFAULT_COIN] + ) + script_utxos2 = cluster.g_query.get_utxo( + txin=f"{txid_fund}#1", coins=[clusterlib.DEFAULT_COIN] + ) + collateral_utxos1 = cluster.g_query.get_utxo(txin=f"{txid_fund}#2") + collateral_utxos2 = cluster.g_query.get_utxo(txin=f"{txid_fund}#3") + + # Step 2: spend the "locked" UTxOs + + assert plutus_op1.datum_file and plutus_op2.datum_file + assert plutus_op1.redeemer_cbor_file and plutus_op2.redeemer_cbor_file + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos1, + script_file=plutus_op1.script_file, + collaterals=collateral_utxos1, + execution_units=( + plutus_op1.execution_cost.per_time, + plutus_op1.execution_cost.per_space, + ), + datum_file=plutus_op1.datum_file, + redeemer_cbor_file=plutus_op1.redeemer_cbor_file, + ), + clusterlib.ScriptTxIn( + txins=script_utxos2, + script_file=plutus_op2.script_file, + collaterals=collateral_utxos2, + execution_units=( + plutus_op2.execution_cost.per_time, + plutus_op2.execution_cost.per_space, + ), + datum_file=plutus_op2.datum_file, + redeemer_cbor_file=plutus_op2.redeemer_cbor_file, + ), + ] + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addrs[1].skey_file], + ) + txouts_redeem = [ + clusterlib.TxOut(address=payment_addrs[1].address, amount=amount * 2), + ] + + tx_output_redeem = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts_redeem, + tx_files=tx_files_redeem, + fee=redeem_cost1.fee + redeem_cost2.fee + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed_redeem = cluster.g_transaction.sign_tx( + tx_body_file=tx_output_redeem.out_file, + signing_key_files=tx_files_redeem.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed_redeem) + + err_str = str(excinfo.value) + assert rf"ScriptHash \"{script2_hash}\") fails" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(data=st.data()) + @common.hypothesis_settings(100) + @pytest.mark.parametrize( + "fund_execution_units_above_limit", + ("v1", pytest.param("v2", marks=common.SKIPIF_PLUTUSV2_UNUSABLE)), + ids=("plutus_v1", "plutus_v2"), + indirect=True, + ) + def test_execution_units_above_limit( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + fund_execution_units_above_limit: Tuple[ + List[clusterlib.UTXOData], List[clusterlib.UTXOData], plutus_common.PlutusOp + ], + pparams: dict, + data: st.DataObject, + request: FixtureRequest, + ): + """Test spending a locked UTxO with a Plutus script with execution units above the limit. + + Expect failure. + + * fund the script address and create a UTxO for collateral + * try to spend the locked UTxO when execution units are set above the limits + * check that failed because the execution units were too big + """ + temp_template = f"{common.get_test_id(cluster)}_{request.node.callspec.id}" + amount = 2_000_000 + + script_utxos, collateral_utxos, plutus_op = fund_execution_units_above_limit + + per_time = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["steps"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_time > pparams["maxTxExecutionUnits"]["steps"] + + per_space = data.draw( + st.integers( + min_value=pparams["maxTxExecutionUnits"]["memory"] + 1, max_value=common.MAX_INT64 + ) + ) + assert per_space > pparams["maxTxExecutionUnits"]["memory"] + + fixed_cost = pparams["txFeeFixed"] + + high_execution_cost = plutus_common.ExecutionCost( + per_time=per_time, per_space=per_space, fixed_cost=fixed_cost + ) + + plutus_op = plutus_op._replace(execution_cost=high_execution_cost) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + err_str = str(excinfo.value) + assert "ExUnitsTooBigUTxO" in err_str, err_str + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeRedeemer: + """Tests for Tx output locking using Plutus smart contracts with wrong redeemer.""" + + MIN_INT_VAL = -common.MAX_UINT64 + AMOUNT = 2_000_000 + + def _fund_script_guessing_game( + self, + cluster_manager: cluster_management.ClusterManager, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + plutus_version: str, + ) -> FundTupleT: + """Fund a Plutus script and create the locked UTxO and collateral UTxO.""" + payment_addrs = clusterlib_utils.create_payment_addr_records( + *[f"{temp_template}_payment_addr_{i}" for i in range(2)], + cluster_obj=cluster_obj, + ) + + # fund source address + clusterlib_utils.fund_from_faucet( + payment_addrs[0], + cluster_obj=cluster_obj, + faucet_data=cluster_manager.cache.addrs_data["user1"], + amount=3_000_000_000, + ) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + execution_cost=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster_obj, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=self.AMOUNT, + ) + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def fund_script_guessing_game_v1( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + ) -> FundTupleT: + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + + script_utxos, collateral_utxos, payment_addrs = self._fund_script_guessing_game( + cluster_manager=cluster_manager, + cluster_obj=cluster, + temp_template=temp_template, + plutus_version="v1", + ) + fixture_cache.value = script_utxos, collateral_utxos, payment_addrs + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def fund_script_guessing_game_v2( + self, + cluster_manager: cluster_management.ClusterManager, + cluster: clusterlib.ClusterLib, + ) -> FundTupleT: + with cluster_manager.cache_fixture() as fixture_cache: + if fixture_cache.value: + return fixture_cache.value # type: ignore + + temp_template = common.get_test_id(cluster) + + script_utxos, collateral_utxos, payment_addrs = self._fund_script_guessing_game( + cluster_manager=cluster_manager, + cluster_obj=cluster, + temp_template=temp_template, + plutus_version="v2", + ) + fixture_cache.value = script_utxos, collateral_utxos, payment_addrs + + return script_utxos, collateral_utxos, payment_addrs + + @pytest.fixture + def cost_per_unit( + self, + cluster: clusterlib.ClusterLib, + ) -> plutus_common.ExecutionCost: + return plutus_common.get_cost_per_unit( + protocol_params=cluster.g_query.get_protocol_params() + ) + + def _failed_tx_build( + self, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + redeemer_content: str, + dst_addr: clusterlib.AddressRecord, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + ) -> str: + """Try to build a Tx and expect failure.""" + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + outfile.write(redeemer_content) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + ) + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + return str(excinfo.value) + + def _int_out_of_range( + self, + cluster_obj: clusterlib.ClusterLib, + temp_template: str, + script_utxos: List[clusterlib.UTXOData], + collateral_utxos: List[clusterlib.UTXOData], + redeemer_value: int, + dst_addr: clusterlib.AddressRecord, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + ): + """Try to spend a locked UTxO with redeemer int value that is not in allowed range.""" + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=redeemer_file if redeemer_content else "", + redeemer_value=str(redeemer_value) if not redeemer_content else "", + ) + ] + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster_obj.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + err_str = str(excinfo.value) + assert "Value out of range within the script data" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given( + redeemer_value=st.integers(min_value=MIN_INT_VAL, max_value=common.MAX_UINT64) + ) + @hypothesis.example(redeemer_value=MIN_INT_VAL) + @hypothesis.example(redeemer_value=common.MAX_UINT64) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_inside_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with an unexpected redeemer value. + + Expect failure. + """ + hypothesis.assume(redeemer_value != 42) + + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = {} + if redeemer_value % 2 == 0: + redeemer_content = {"int": redeemer_value} + + redeemer_file = f"{temp_template}.redeemer" + if redeemer_content: + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump(redeemer_content, outfile) + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + # try to spend the "locked" UTxO + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + dst_addr = payment_addrs[1] + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=redeemer_file if redeemer_content else "", + redeemer_value=str(redeemer_value) if not redeemer_content else "", + ) + ] + tx_raw_output = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed) + + err_str = str(excinfo.value) + assert "ValidationTagMismatch (IsValid True)" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(max_value=MIN_INT_VAL - 1)) + @hypothesis.example(redeemer_value=MIN_INT_VAL - 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_bellow_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a redeemer int value < minimum allowed value. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + self._int_out_of_range( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers(min_value=common.MAX_UINT64 + 1)) + @hypothesis.example(redeemer_value=common.MAX_UINT64 + 1) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_value_above_range( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to spend a locked UTxO with a redeemer int value > maximum allowed value. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + self._int_out_of_range( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_value=redeemer_value, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_wrong_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO with an invalid redeemer type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_file = f"{temp_template}.redeemer" + with open(redeemer_file, "w", encoding="utf-8") as outfile: + json.dump({"bytes": redeemer_value.hex()}, outfile) + + # try to spend the "locked" UTxO + + per_time = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_time + per_space = plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.per_space + + fee_redeem = ( + round(per_time * cost_per_unit.per_time + per_space * cost_per_unit.per_space) + + plutus_common.GUESSING_GAME_UNTYPED[plutus_version].execution_cost.fixed_cost + ) + + dst_addr = payment_addrs[1] + + tx_files = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) + txouts = [clusterlib.TxOut(address=dst_addr.address, amount=self.AMOUNT)] + + plutus_txins = [ + clusterlib.ScriptTxIn( + txins=script_utxos, + script_file=plutus_common.GUESSING_GAME_UNTYPED[plutus_version].script_file, + collaterals=collateral_utxos, + execution_units=( + per_time, + per_space, + ), + datum_file=plutus_common.DATUM_42, + redeemer_file=Path(redeemer_file), + ) + ] + + tx_raw_output = cluster.g_transaction.build_raw_tx_bare( + out_file=f"{temp_template}_step2_tx.body", + txouts=txouts, + tx_files=tx_files, + fee=fee_redeem + FEE_REDEEM_TXSIZE, + script_txins=plutus_txins, + ) + tx_signed = cluster.g_transaction.sign_tx( + tx_body_file=tx_raw_output.out_file, + signing_key_files=tx_files.signing_key_files, + tx_name=f"{temp_template}_step2", + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + cluster.g_transaction.submit_tx_bare(tx_file=tx_signed) + + err_str = str(excinfo.value) + assert "ValidationTagMismatch (IsValid True)" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to spend a locked UTxO using redeemer that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = json.dumps( + {"constructor": 0, "fields": [{"bytes": redeemer_value.hex()}]} + ) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert "must consist of at most 64 bytes" in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in typed format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + + redeemer_content = json.dumps({"constructor": 0, "fields": [{"int": redeemer_value.hex()}]}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "int" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.binary(max_size=64)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_int_bytes_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: bytes, + ): + """Try to build a Tx using byte string for redeemer when JSON schema specifies int. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"int": redeemer_value.hex()}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "int" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in typed format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"constructor": 0, "fields": [{"bytes": redeemer_value}]}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "bytes" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.integers()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_bytes_int_declared( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: int, + ): + """Try to build a Tx using int value for redeemer when JSON schema specifies byte string. + + Redeemer is in untyped format and the value doesn't comply to JSON schema. Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({"bytes": redeemer_value}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert 'field "bytes" does not have the type required by the schema' in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_invalid_json( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_value: str, + ): + """Try to build a Tx using a redeemer value that is invalid JSON. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = f'{{"{redeemer_value}"}}' + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + assert "Invalid JSON format" in err, err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_typed_invalid_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON typed schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({redeemer_type: 42}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err + ), err + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(redeemer_type=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_json_schema_untyped_invalid_type( + self, + cluster: clusterlib.ClusterLib, + fund_script_guessing_game_v1: FundTupleT, + fund_script_guessing_game_v2: FundTupleT, + cost_per_unit: plutus_common.ExecutionCost, + plutus_version: str, + redeemer_type: str, + ): + """Try to build a Tx using a JSON untyped schema that specifies an invalid type. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + fund_script_guessing_game = ( + fund_script_guessing_game_v1 if plutus_version == "v1" else fund_script_guessing_game_v2 + ) + + script_utxos, collateral_utxos, payment_addrs = fund_script_guessing_game + redeemer_content = json.dumps({redeemer_type: 42}) + + # try to build a Tx for spending the "locked" UTxO + err = self._failed_tx_build( + cluster_obj=cluster, + temp_template=temp_template, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + redeemer_content=redeemer_content, + dst_addr=payment_addrs[1], + cost_per_unit=cost_per_unit, + plutus_version=plutus_version, + ) + + assert ( + 'Expected a single field named "int", "bytes", "string", "list" or "map".' in err + ), err + + +@common.SKIPIF_PLUTUS_UNUSABLE +@pytest.mark.testnets +class TestNegativeDatum: + """Tests for Tx output locking using Plutus smart contracts with wrong datum.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.parametrize("address_type", ("script_address", "key_address")) + @common.PARAM_PLUTUS_VERSION + def test_no_datum_txout( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + address_type: str, + plutus_version: str, + ): + """Test using UTxO without datum hash in place of locked UTxO. + + Expect failure. + + * create a Tx output without a datum hash + * try to spend the UTxO like it was locked Plutus UTxO + * check that the expected error was raised + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}_{address_type}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_file=plutus_common.REDEEMER_42_TYPED, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + if address_type == "script_address": + redeem_address = cluster.g_address.gen_payment_addr( + addr_name=temp_template, payment_script_file=plutus_op.script_file + ) + else: + redeem_address = payment_addrs[2].address + + redeem_cost = plutus_common.compute_cost( + execution_cost=plutus_op.execution_cost, + protocol_params=cluster.g_query.get_protocol_params(), + ) + + txouts = [ + clusterlib.TxOut( + address=redeem_address, amount=amount + redeem_cost.fee + FEE_REDEEM_TXSIZE + ), + clusterlib.TxOut(address=payment_addr.address, amount=redeem_cost.collateral), + ] + tx_files = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_output_fund = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files, + join_txouts=False, + ) + txid = cluster.g_transaction.get_txid(tx_body_file=tx_output_fund.out_file) + script_utxos = cluster.g_query.get_utxo(txin=f"{txid}#0") + collateral_utxos = cluster.g_query.get_utxo(txin=f"{txid}#1") + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "NonOutputSupplimentaryDatums" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.text()) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_lock_tx_invalid_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: str, + plutus_version: str, + ): + """Test locking a Tx output with an invalid datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump(f'{{"{datum_value}"}}', outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + with pytest.raises(clusterlib.CLIError) as excinfo: + _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "JSON object expected. Unexpected value" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_tx_wrong_datum( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Test locking a Tx output and try to spend it with a wrong datum. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + plutus_op_1 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42_TYPED, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op_1.execution_cost # for mypy + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op_1, + amount=amount, + ) + + # use a wrong datum to try to unlock the funds + plutus_op_2 = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=plutus_common.DATUM_42, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op_2, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "NonOutputSupplimentaryDatums" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @common.PARAM_PLUTUS_VERSION + def test_unlock_non_script_utxo( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + plutus_version: str, + ): + """Try to spend a non-script UTxO with datum as if it was script locked UTxO. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + + amount_fund = 4_000_000 + amount_redeem = 2_000_000 + amount_collateral = 2_000_000 + + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = plutus_common.DATUM_42_TYPED + + datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file=datum_file) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=datum_file, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + # create datum and collateral UTxOs + + txouts = [ + clusterlib.TxOut( + address=dst_addr.address, + amount=amount_fund, + datum_hash=datum_hash, + ), + clusterlib.TxOut( + address=payment_addr.address, + amount=amount_collateral, + ), + ] + tx_files_fund = clusterlib.TxFiles(signing_key_files=[payment_addr.skey_file]) + + tx_raw_output = cluster.g_transaction.send_tx( + src_address=payment_addr.address, + tx_name=temp_template, + txouts=txouts, + tx_files=tx_files_fund, + ) + + out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_raw_output) + datum_utxo = clusterlib.filter_utxos( + utxos=out_utxos, address=dst_addr.address, datum_hash=datum_hash + )[0] + collateral_utxos = clusterlib.filter_utxos( + utxos=out_utxos, address=payment_addr.address, utxo_ix=datum_utxo.utxo_ix + 1 + ) + assert ( + datum_utxo.datum_hash == datum_hash + ), f"UTxO should have datum hash '{datum_hash}': {datum_utxo}" + + tx_files_redeem = clusterlib.TxFiles( + signing_key_files=[payment_addr.skey_file, dst_addr.skey_file] + ) + + # try to spend the "locked" UTxO + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=dst_addr, + script_utxos=[datum_utxo], + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount_redeem, + tx_files=tx_files_redeem, + ) + + err_str = str(excinfo.value) + assert "ExtraneousScriptWitnessesUTXOW" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @hypothesis.given(datum_value=st.binary(min_size=65)) + @common.hypothesis_settings(max_examples=200) + @common.PARAM_PLUTUS_VERSION + def test_too_big( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + datum_value: bytes, + plutus_version: str, + ): + """Try to lock a UTxO with datum that is too big. + + Expect failure. + """ + temp_template = f"{common.get_test_id(cluster)}_{plutus_version}" + amount = 2_000_000 + payment_addr = payment_addrs[0] + dst_addr = payment_addrs[1] + + datum_file = f"{temp_template}.datum" + with open(datum_file, "w", encoding="utf-8") as outfile: + json.dump({"constructor": 0, "fields": [{"bytes": datum_value.hex()}]}, outfile) + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS[plutus_version].script_file, + datum_file=Path(datum_file), + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS[plutus_version].execution_cost, + ) + assert plutus_op.execution_cost # for mypy + + with pytest.raises(clusterlib.CLIError) as excinfo: + _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addr, + dst_addr=dst_addr, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "must consist of at most 64 bytes" in err_str, err_str + + +@pytest.mark.testnets +class TestCompatibility: + """Tests for checking compatibility with previous Tx eras.""" + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era > VERSIONS.ALONZO, + reason="runs only with Tx era <= Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv2_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV2 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v2"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v2"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV2 is not supported" in err_str, err_str + + @allure.link(helpers.get_vcs_link()) + @pytest.mark.skipif( + VERSIONS.transaction_era >= VERSIONS.ALONZO, + reason="runs only with Tx era < Alonzo", + ) + @pytest.mark.dbsync + def test_plutusv1_old_tx_era( + self, + cluster: clusterlib.ClusterLib, + payment_addrs: List[clusterlib.AddressRecord], + ): + """Test spending a UTxO locked with PlutusV1 script using old Tx era. + + Expect failure. + + * try to spend the locked UTxO + * check that the expected error was raised + * (optional) check transactions in db-sync + """ + temp_template = common.get_test_id(cluster) + amount = 2_000_000 + + plutus_op = plutus_common.PlutusOp( + script_file=plutus_common.ALWAYS_SUCCEEDS["v1"].script_file, + datum_cbor_file=plutus_common.DATUM_42_TYPED_CBOR, + redeemer_cbor_file=plutus_common.REDEEMER_42_CBOR, + execution_cost=plutus_common.ALWAYS_SUCCEEDS["v1"].execution_cost, + ) + + script_utxos, collateral_utxos, __ = _fund_script( + temp_template=temp_template, + cluster_obj=cluster, + payment_addr=payment_addrs[0], + dst_addr=payment_addrs[1], + plutus_op=plutus_op, + amount=amount, + ) + + with pytest.raises(clusterlib.CLIError) as excinfo: + _spend_locked_txin( + temp_template=temp_template, + cluster_obj=cluster, + dst_addr=payment_addrs[1], + script_utxos=script_utxos, + collateral_utxos=collateral_utxos, + plutus_op=plutus_op, + amount=amount, + ) + + err_str = str(excinfo.value) + assert "PlutusScriptV1 is not supported" in err_str, err_str