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 already
# in the ledger's canonical parameter order. The script integrity hash
# is computed over that canonical order, so it MUST be preserved. Key
# each entry by its zero-padded position rather than zipping against a
# name list: keying by name (and sorting those names) permutes the costs
# away from canonical order, producing a wrong 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
already in the ledger's canonical parameter order. The script integrity hash
is computed over that order, so ``_parse_cost_models`` must preserve it and
never drop entries — otherwise a transaction's script integrity hash is wrong
and is rejected on submit (while ``evaluateTransaction`` still passes)."""

def test_preserves_order_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