Skip to content
Open
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
39 changes: 18 additions & 21 deletions pycardano/backend/ogmios_v6.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
from pycardano.hash import DatumHash, ScriptHash
from pycardano.network import Network
from pycardano.plutus import (
PLUTUS_V1_COST_MODEL,
PLUTUS_V2_COST_MODEL,
ExecutionUnits,
PlutusScript,
)
Expand Down Expand Up @@ -364,26 +362,25 @@ def evaluate_tx_cbor(self, cbor: Union[bytes, str]) -> Dict[str, ExecutionUnits]
def _parse_cost_models(self, plutus_cost_models):
ogmios_cost_models = plutus_cost_models or {}

# Ogmios returns each cost model as an array of operation costs in the
# ledger's canonical parameter order, and the script integrity hash is
# computed over the full array. Key each entry by its zero-padded
# position over the whole array. Zipping against the hardcoded
# parameter-name lists (V1 166, V2 175 entries) would drop every cost
# past that length, producing a script integrity hash that passes
# `evaluateTransaction` but is rejected on submit.
cost_models = {}
if "plutus:v1" in ogmios_cost_models:
cost_models["PlutusV1"] = dict(
zip(
sorted(PLUTUS_V1_COST_MODEL.keys()),
ogmios_cost_models["plutus:v1"].copy(),
)
)
if "plutus:v2" in ogmios_cost_models:
cost_models["PlutusV2"] = dict(
zip(
sorted(PLUTUS_V2_COST_MODEL.keys()),
ogmios_cost_models["plutus:v2"].copy(),
)
)
if "plutus:v3" in ogmios_cost_models:
cost_models["PlutusV3"] = {}
width = len(f'{len(ogmios_cost_models["plutus:v3"])}')
for i, v in enumerate(ogmios_cost_models["plutus:v3"].copy()):
cost_models["PlutusV3"][f"{i:0{width}d}"] = v
for ogmios_key, pycardano_key in (
("plutus:v1", "PlutusV1"),
("plutus:v2", "PlutusV2"),
("plutus:v3", "PlutusV3"),
):
if ogmios_key in ogmios_cost_models:
op_costs = ogmios_cost_models[ogmios_key]
width = len(f"{len(op_costs)}")
cost_models[pycardano_key] = {
f"{i:0{width}d}": v for i, v in enumerate(op_costs)
}
return cost_models


Expand Down
49 changes: 49 additions & 0 deletions test/pycardano/backend/test_ogmios_v6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from pycardano.backend.ogmios_v6 import OgmiosV6ChainContext


def _parse(plutus_cost_models):
# _parse_cost_models needs no connection state; build a bare instance.
ctx = OgmiosV6ChainContext.__new__(OgmiosV6ChainContext)
return ctx._parse_cost_models(plutus_cost_models)


class TestParseCostModels:
"""The Plutus cost models returned by Ogmios are arrays of operation costs
in the ledger's canonical parameter order. The script integrity hash is
computed over the full array, so ``_parse_cost_models`` must keep every
entry — a truncated cost model gives a script integrity hash that is
rejected on submit (while ``evaluateTransaction`` still passes)."""

def test_keeps_full_array_for_every_language(self):
v1 = list(range(100, 100 + 166))
v2 = list(range(1000, 1000 + 332))
v3 = list(range(5, 5 + 251))
parsed = _parse({"plutus:v1": v1, "plutus:v2": v2, "plutus:v3": v3})
assert list(parsed["PlutusV1"].values()) == v1
assert list(parsed["PlutusV2"].values()) == v2
assert list(parsed["PlutusV3"].values()) == v3

def test_does_not_truncate_when_model_grows(self):
# The ledger's cost models grow over time (e.g. PlutusV2 grew past 300
# parameters at Conway). Every operation cost must be kept, regardless of
# how long the array is.
v2 = list(range(332))
parsed = _parse({"plutus:v2": v2})
assert len(parsed["PlutusV2"]) == len(v2)
assert list(parsed["PlutusV2"].values()) == v2

def test_keys_are_zero_padded_indices_that_sort_canonically(self):
# Keys are positional and zero-padded so a lexicographic ``sorted(keys)``
# (used when serializing the V1 language view) stays in canonical order.
parsed = _parse({"plutus:v2": list(range(15))})
keys = list(parsed["PlutusV2"].keys())
assert keys == sorted(keys)
assert keys[:3] == ["00", "01", "02"]

def test_absent_language_is_omitted(self):
parsed = _parse({"plutus:v2": [1, 2, 3]})
assert set(parsed) == {"PlutusV2"}

def test_empty_input(self):
assert _parse(None) == {}
assert _parse({}) == {}
Loading