From 9d18d7d4d3467b2633313558f16329c8df46f7d3 Mon Sep 17 00:00:00 2001 From: theeldermillenial Date: Sun, 18 Oct 2026 20:17:28 -0400 Subject: [PATCH] fix(ogmios): keep the full cost-model arrays in OgmiosV6 backend `OgmiosV6ChainContext._parse_cost_models` built the PlutusV1/V2 cost models by zipping the operation-cost array against `sorted(PLUTUS_V{1,2}_COST_MODEL.keys())`. The hardcoded name lists have a fixed length (V1 = 166, V2 = 175), but the ledger's cost models grow across protocol eras; mainnet PlutusV1 and PlutusV2 now each have 332 operation costs. `zip` stops at the shorter iterable, so every cost past the hardcoded count was dropped. The cost model feeds the language view in the script integrity (script-data) hash, so a truncated model yields a hash the node rejects on submit, even though `evaluateTransaction`, which does not check that hash, still passes. The names the zip attached were correct for the entries it kept: for the first 166 (V1) and 175 (V2) parameters the ledger's canonical order is the alphabetical order of the parameter names, so arrays no longer than the hardcoded lists encoded to the same bytes. The only defect was the truncation. Key each cost model by its zero-padded position over the full Ogmios array, the same approach the PlutusV3 branch already used. Drops the now-unused PLUTUS_V1/V2_COST_MODEL imports. Adds a regression test. Co-Authored-By: Claude Opus 5.5 (1M context) --- pycardano/backend/ogmios_v6.py | 39 +++++++++---------- test/pycardano/backend/test_ogmios_v6.py | 49 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 test/pycardano/backend/test_ogmios_v6.py diff --git a/pycardano/backend/ogmios_v6.py b/pycardano/backend/ogmios_v6.py index b0ff21cc..e7306371 100644 --- a/pycardano/backend/ogmios_v6.py +++ b/pycardano/backend/ogmios_v6.py @@ -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, ) @@ -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 diff --git a/test/pycardano/backend/test_ogmios_v6.py b/test/pycardano/backend/test_ogmios_v6.py new file mode 100644 index 00000000..102864bb --- /dev/null +++ b/test/pycardano/backend/test_ogmios_v6.py @@ -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({}) == {}