Skip to content

Commit

Permalink
Test for correctness of the supplied script (Python-Cardano#322)
Browse files Browse the repository at this point in the history
* 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.

* Fix qa and add test cases for missing/incorrect scripts

* Add test case for entirely missing script

* Remove redundant check

* Improve error message
  • Loading branch information
nielstron authored Mar 4, 2024
1 parent 0fec24a commit 4245029
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 12 deletions.
42 changes: 30 additions & 12 deletions pycardano/txbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,24 +259,42 @@ def add_script_input(
self._consolidate_redeemer(redeemer)
self._inputs_to_redeemers[utxo] = redeemer

input_script_hash = utxo.output.address.payment_part

# collect potential scripts to fulfill the input
candidate_scripts: List[
Tuple[Union[NativeScript, PlutusV1Script, PlutusV2Script], 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(
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:
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.input}."
"Supplied scripts do not match the payment part of the input address."
)

self.inputs.append(utxo)
return self
Expand Down
178 changes: 178 additions & 0 deletions test/pycardano/test_txbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -658,6 +684,158 @@ 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,
)

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)
tx_in1 = TransactionInput.from_primitive(
Expand Down

0 comments on commit 4245029

Please sign in to comment.