From 86297a77b60581aaf73d28f4b1243a857bb0c9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Fri, 1 Mar 2024 09:19:49 +0100 Subject: [PATCH 1/5] Test for correctness of the supplied script This is to avoid users accidentally adding script inputs with the wrong script and then get cryptic errors from ogmios. --- pycardano/txbuilder.py | 47 +++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 52a0f989..aa32042c 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -250,24 +250,47 @@ def add_script_input( self._consolidate_redeemer(redeemer) self._inputs_to_redeemers[utxo] = redeemer + input_script_hash = utxo.output.address.payment_part + if not isinstance(input_script_hash, ScriptHash): + raise InvalidArgumentException( + f"Expect the payment part of the address to be of a script (type ScriptHash), " + f"but got {type(input_script_hash)} instead." + ) + + # collect potential scripts to fulfill the input + candidate_scripts: List[ + Tuple[ScriptType, Optional[UTxO]] + ] = [] if utxo.output.script: - self._inputs_to_scripts[utxo] = utxo.output.script - self.reference_inputs.add(utxo) - self._reference_scripts.append(utxo.output.script) + candidate_scripts.append((utxo.output.script, utxo)) elif not script: for i in self.context.utxos(utxo.output.address): if i.output.script: - self._inputs_to_scripts[utxo] = i.output.script - self.reference_inputs.add(i) - self._reference_scripts.append(i.output.script) - break + candidate_scripts.append((i.output.script, i)) elif isinstance(script, UTxO): - assert script.output.script is not None - self._inputs_to_scripts[utxo] = script.output.script - self.reference_inputs.add(script) - self._reference_scripts.append(script.output.script) + if script.output.script is None: + raise InvalidArgumentException( + "Expect the output of the UTxO to have a script, " + "but got None instead." + ) + candidate_scripts.append((script.output.script, script)) else: - self._inputs_to_scripts[utxo] = script + candidate_scripts.append((script, None)) + + found_valid_script = False + for candidate_script, candidate_utxo in candidate_scripts: + if script_hash(candidate_script) != input_script_hash: + continue + found_valid_script = True + self._inputs_to_scripts[utxo] = candidate_script + if candidate_utxo is not None: + self.reference_inputs.add(candidate_utxo) + self._reference_scripts.append(candidate_script) + if not found_valid_script: + raise InvalidArgumentException( + f"Cannot find a valid script to fulfill the input UTxO: {utxo}." + "Supplied scripts do not match the payment part of the input address." + ) self.inputs.append(utxo) return self From d7002180a8cffe05912a6a29bbed1476a9fcc54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Fri, 1 Mar 2024 09:32:33 +0100 Subject: [PATCH 2/5] Fix qa and add test cases for missing/incorrect scripts --- pycardano/txbuilder.py | 2 +- test/pycardano/test_txbuilder.py | 134 +++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index aa32042c..b83fa1bf 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -259,7 +259,7 @@ def add_script_input( # collect potential scripts to fulfill the input candidate_scripts: List[ - Tuple[ScriptType, Optional[UTxO]] + Tuple[Union[NativeScript, PlutusV1Script, PlutusV2Script], Optional[UTxO]] ] = [] if utxo.output.script: candidate_scripts.append((utxo.output.script, utxo)) diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 7ba70819..926ce85f 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -658,6 +658,140 @@ def test_add_script_input_with_script_from_specified_utxo(chain_context): assert [existing_script_utxo.input] == tx_body.reference_inputs +def test_add_script_input_incorrect_script(chain_context): + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + tx_in2 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 1] + ) + plutus_script = PlutusV1Script(b"dummy test script") + script_hash = plutus_script_hash(plutus_script) + incorrect_plutus_script = PlutusV2Script(b"dummy test script2") + script_address = Address(script_hash) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash()) + ) + mint = MultiAsset.from_primitive({script_hash.payload: {b"TestToken": 1}}) + UTxO( + tx_in2, + TransactionOutput( + script_address, Value(10000000, mint), datum_hash=datum.hash() + ), + ) + redeemer1 = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + script=incorrect_plutus_script, + datum=datum, + redeemer=redeemer1, + ) + + +def test_add_script_input_no_script_no_attached_script(chain_context): + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + plutus_script = PlutusV1Script(b"dummy test script") + script_hash = plutus_script_hash(plutus_script) + script_address = Address(script_hash) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, + TransactionOutput(script_address, 10000000, datum_hash=datum.hash()), + ) + redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + datum=datum, + redeemer=redeemer, + ) + + +def test_add_script_input_find_incorrect_script(chain_context): + original_utxos = chain_context.utxos( + "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + ) + with patch.object(chain_context, "utxos") as mock_utxos: + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + plutus_script = PlutusV1Script(b"dummy test script") + incorrect_plutus_script = PlutusV2Script(b"dummy test script2") + script_hash = plutus_script_hash(plutus_script) + script_address = Address(script_hash) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash()) + ) + + existing_script_utxo = UTxO( + TransactionInput.from_primitive( + [ + "41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", + 1, + ] + ), + TransactionOutput(script_address, 1234567, script=incorrect_plutus_script), + ) + + mock_utxos.return_value = original_utxos + [existing_script_utxo] + + redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + datum=datum, + redeemer=redeemer, + ) + + +def test_add_script_input_with_script_from_specified_utxo_with_incorrect_script( + chain_context, +): + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + plutus_script = PlutusV2Script(b"dummy test script") + incorrect_plutus_script = PlutusV1Script(b"dummy test script2") + script_hash = plutus_script_hash(plutus_script) + script_address = Address(script_hash) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash()) + ) + + existing_script_utxo = UTxO( + TransactionInput.from_primitive( + [ + "41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", + 1, + ] + ), + TransactionOutput(script_address, 1234567, script=incorrect_plutus_script), + ) + + redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + script=existing_script_utxo, + datum=datum, + redeemer=redeemer, + ) + + def test_add_script_input_multiple_redeemers(chain_context): tx_builder = TransactionBuilder(chain_context) tx_in1 = TransactionInput.from_primitive( From 971c1dd0f0a767a1346bd07510d011aa8e5aa606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Fri, 1 Mar 2024 09:34:42 +0100 Subject: [PATCH 3/5] Add test case for entirely missing script --- pycardano/txbuilder.py | 4 ++-- test/pycardano/test_txbuilder.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index b83fa1bf..7e66e5cb 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -270,8 +270,8 @@ def add_script_input( elif isinstance(script, UTxO): if script.output.script is None: raise InvalidArgumentException( - "Expect the output of the UTxO to have a script, " - "but got None instead." + f"Expect the output of the reference UTxO {utxo}" + " to have a script, but got None instead." ) candidate_scripts.append((script.output.script, script)) else: diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 926ce85f..2d6bccae 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -791,6 +791,24 @@ def test_add_script_input_with_script_from_specified_utxo_with_incorrect_script( redeemer=redeemer, ) + existing_script_utxo = UTxO( + TransactionInput.from_primitive( + [ + "41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", + 1, + ] + ), + TransactionOutput(script_address, 1234567, script=None), + ) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + script=existing_script_utxo, + datum=datum, + redeemer=redeemer, + ) + def test_add_script_input_multiple_redeemers(chain_context): tx_builder = TransactionBuilder(chain_context) From 399c32094be6e5929c63d2b984c3b64da1648152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Fri, 1 Mar 2024 09:43:45 +0100 Subject: [PATCH 4/5] Remove redundant check --- pycardano/txbuilder.py | 5 ----- test/pycardano/test_txbuilder.py | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 7e66e5cb..e8c2eb28 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -251,11 +251,6 @@ def add_script_input( self._inputs_to_redeemers[utxo] = redeemer input_script_hash = utxo.output.address.payment_part - if not isinstance(input_script_hash, ScriptHash): - raise InvalidArgumentException( - f"Expect the payment part of the address to be of a script (type ScriptHash), " - f"but got {type(input_script_hash)} instead." - ) # collect potential scripts to fulfill the input candidate_scripts: List[ diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 2d6bccae..3363cb19 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -576,6 +576,32 @@ def test_add_script_input_no_script(chain_context): assert witness.plutus_v1_script is None +def test_add_script_input_payment_script(chain_context): + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + plutus_script = PlutusV1Script(b"dummy test script") + vk1 = VerificationKey.from_cbor( + "58206443a101bdb948366fc87369336224595d36d8b0eee5602cba8b81a024e58473" + ) + script_address = Address(vk1.hash()) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, + TransactionOutput(script_address, 10000000, datum_hash=datum.hash()), + ) + redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + datum=datum, + redeemer=redeemer, + script=plutus_script, + ) + + def test_add_script_input_find_script(chain_context): original_utxos = chain_context.utxos( "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" From 7cf1a00a188091758b5924b07d1c26fec2886722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sat, 2 Mar 2024 16:53:40 +0100 Subject: [PATCH 5/5] Improve error message --- pycardano/txbuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index e8c2eb28..b2923593 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -283,7 +283,7 @@ def add_script_input( self._reference_scripts.append(candidate_script) if not found_valid_script: raise InvalidArgumentException( - f"Cannot find a valid script to fulfill the input UTxO: {utxo}." + f"Cannot find a valid script to fulfill the input UTxO: {utxo.input}." "Supplied scripts do not match the payment part of the input address." )