Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cardano_node_tests/tests/test_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2659,6 +2659,12 @@ def test_spend_reference_script(
signing_key_files=[reference_addr.skey_file],
)

# The reference script is pulled into the Tx by spending the UTxO that holds it, so its
# size must be accounted for in the fee.
reference_script_size = clusterlib_utils.get_reference_script_size(
script_file=multisig_script
)

# The `tx_out_spend`
clusterlib_utils.build_and_submit_tx(
cluster_obj=cluster,
Expand All @@ -2671,6 +2677,7 @@ def test_spend_reference_script(
tx_files=tx_files,
witness_override=3,
byron_witness_count=1 if address_type == "byron" else 0,
reference_script_size=reference_script_size,
)

# Check that the reference UTxO was spent
Expand Down
58 changes: 58 additions & 0 deletions cardano_node_tests/utils/clusterlib_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,64 @@ def create_reference_utxo(
return reference_utxo, tx_raw_output


def _encode_simple_script(*, script: dict) -> list:
"""Encode a simple (native) script into the structure the ledger serializes.

Args:
script: A dict with the JSON representation of a simple script.

Returns:
list: A structure that is CBOR encoded the same way as the ledger encodes the script.
"""
script_type = script["type"]

if script_type == "sig":
return [0, bytes.fromhex(script["keyHash"])]
if script_type == clusterlib.MultiSigTypeArgs.ALL:
return [1, [_encode_simple_script(script=s) for s in script["scripts"]]]
if script_type == clusterlib.MultiSigTypeArgs.ANY:
return [2, [_encode_simple_script(script=s) for s in script["scripts"]]]
if script_type == clusterlib.MultiSigTypeArgs.AT_LEAST:
return [
3,
script["required"],
[_encode_simple_script(script=s) for s in script["scripts"]],
]
if script_type == clusterlib.MultiSlotTypeArgs.AFTER:
return [4, script["slot"]]
if script_type == clusterlib.MultiSlotTypeArgs.BEFORE:
return [5, script["slot"]]

err = f"Unsupported simple script type: {script_type}"
raise ValueError(err)


def get_reference_script_size(*, script_file: cl_types.FileType) -> int:
"""Get the size of a script as it is accounted for when used as a reference script.

Since Conway, the ledger charges `minFeeRefScriptCostPerByte` for every byte of every
reference script a transaction pulls in, be it through a reference input or through a spent
UTxO that holds the script. The size is the size of the script as serialized by the ledger,
which is the bare script for Plutus scripts and the CBOR encoded structure for simple
scripts.

Args:
script_file: A path to the script file.

Returns:
int: A size of the reference script in bytes.
"""
with open(script_file, encoding="utf-8") as fp_in:
script: dict = json.load(fp_in)

# A Plutus script file is a text envelope that carries the script in `cborHex`
cbor_hex = script.get("cborHex")
if cbor_hex:
return len(cbor2.loads(bytes.fromhex(cbor_hex)))

return len(cbor2.dumps(_encode_simple_script(script=script)))


def get_utxo_ix_offset(*, utxos: list[clusterlib.UTXOData], txouts: list[clusterlib.TxOut]) -> int:
"""Get offset of index of the first user-defined txout.

Expand Down
114 changes: 114 additions & 0 deletions framework_tests/test_clusterlib_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Unit tests for `cardano_node_tests.utils.clusterlib_utils`.

The tests must not depend on project-specific binaries (`cardano-cli`, ...) being present.
"""

import json
import pathlib as pl

import cbor2
import pytest

from cardano_node_tests.utils import clusterlib_utils

KEY_HASH1 = "9e1156acae8bd72bc1815d0be9fcb64e2d50e61f4204c45b901dad6b"
KEY_HASH2 = "7c2086ea4ebaa880c6e6c70604c0deb37ffbaa0567aec0bea8564055"


def write_script(*, script: dict, dest_dir: pl.Path) -> pl.Path:
"""Write a script into a file and return its path."""
script_file = dest_dir / "script.json"
with open(script_file, "w", encoding="utf-8") as fp_out:
json.dump(script, fp_out, indent=4)
return script_file


class TestGetReferenceScriptSize:
"""Tests for `get_reference_script_size`.

The expected sizes are the sizes of the scripts as serialized by the ledger. The `sig` and
`any` sizes were confirmed against `FeeTooSmallUTxO` ledger errors on the Preview testnet,
where `minFeeRefScriptCostPerByte` is 15: the fee was short by exactly 32 * 15 for the `sig`
script and by 167 * 15 for the `any` script below.
"""

def test_sig(self, tmp_path: pl.Path):
"""Get size of a `sig` script."""
script_file = write_script(script={"keyHash": KEY_HASH1, "type": "sig"}, dest_dir=tmp_path)
assert clusterlib_utils.get_reference_script_size(script_file=script_file) == 32

def test_any_with_slot(self, tmp_path: pl.Path):
"""Get size of an `any` script that nests `sig` scripts and a slot condition."""
script_file = write_script(
script={
"scripts": [
{"keyHash": KEY_HASH1, "type": "sig"},
{"keyHash": KEY_HASH2, "type": "sig"},
{"keyHash": KEY_HASH1, "type": "sig"},
{"keyHash": KEY_HASH2, "type": "sig"},
{"keyHash": KEY_HASH1, "type": "sig"},
{"slot": 100, "type": "after"},
],
"type": "any",
},
dest_dir=tmp_path,
)
assert clusterlib_utils.get_reference_script_size(script_file=script_file) == 167

def test_all(self, tmp_path: pl.Path):
"""Get size of an `all` script."""
script_file = write_script(
script={
"scripts": [
{"keyHash": KEY_HASH1, "type": "sig"},
{"keyHash": KEY_HASH2, "type": "sig"},
],
"type": "all",
},
dest_dir=tmp_path,
)
# 2 bytes for the outer array and tag, 1 byte for the inner array, 2 * 32 bytes for the
# nested `sig` scripts
assert clusterlib_utils.get_reference_script_size(script_file=script_file) == 67

def test_at_least(self, tmp_path: pl.Path):
"""Get size of an `atLeast` script."""
script_file = write_script(
script={
"required": 2,
"scripts": [
{"keyHash": KEY_HASH1, "type": "sig"},
{"keyHash": KEY_HASH2, "type": "sig"},
],
"type": "atLeast",
},
dest_dir=tmp_path,
)
# One more byte than the `all` script above, for the `required` value
assert clusterlib_utils.get_reference_script_size(script_file=script_file) == 68

def test_before(self, tmp_path: pl.Path):
"""Get size of a `before` script."""
script_file = write_script(script={"slot": 100, "type": "before"}, dest_dir=tmp_path)
assert clusterlib_utils.get_reference_script_size(script_file=script_file) == 4

def test_plutus(self, tmp_path: pl.Path):
"""Get size of a Plutus script, which is the size of the bare script."""
plutus_bytes = b"\x01\x02\x03\x04\x05"
script_file = write_script(
script={
"type": "PlutusScriptV3",
"description": "",
"cborHex": cbor2.dumps(plutus_bytes).hex(),
},
dest_dir=tmp_path,
)
assert clusterlib_utils.get_reference_script_size(script_file=script_file) == len(
plutus_bytes
)

def test_unsupported_type(self, tmp_path: pl.Path):
"""Fail on an unknown simple script type."""
script_file = write_script(script={"type": "unknown"}, dest_dir=tmp_path)
with pytest.raises(ValueError, match="Unsupported simple script type: unknown"):
clusterlib_utils.get_reference_script_size(script_file=script_file)
Loading