Skip to content

fix(js): preserve cost-model key order for Plutus builds#6

Open
matiwinnetou wants to merge 1 commit into
developfrom
feature/js-costmodel-ordering
Open

fix(js): preserve cost-model key order for Plutus builds#6
matiwinnetou wants to merge 1 commit into
developfrom
feature/js-costmodel-ordering

Conversation

@matiwinnetou

@matiwinnetou matiwinnetou commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Fixes a real JS-wrapper correctness bug, found while adding the JS Plutus-mint DevKit test in #5: passing provider-fetched cost models into a Plutus build() produced a transaction the node rejects with ConwayUtxowFailure (PPViewHashesDontMatch …).

Root cause

Yaci DevKit / Blockfrost-style backends return Plutus cost models as a map keyed by numeric indices, e.g. {"PlutusV2": {"000": 100788, …, "165": 10}}. JavaScript object semantics iterate canonical integer-string keys ("100") in ascending order before zero-padded ones ("000"), so JSON.stringify emits them out of order. CCL rebuilds each language's cost array in document order, so the scrambled order yields a wrong script-integrity (cost-model) hash → the node rejects the tx.

This is JS-specific:

  • Go is unaffected — json.Marshal sorts keys lexicographically, which for zero-padded keys equals numeric order.
  • Python is unaffected — json preserves the provider's order.

Fix

normalizeCostModels() in wrappers/js/src/index.js, applied in quicktx.build(): any numerically-keyed language is converted to CCL's ordered cost_models_raw array form (a List<Long> that CCL consumes directly in list order, ahead of the order-sensitive named map). Arrays serialize order-stably, so the cost-model order is now canonical regardless of JS key hoisting. Named-operation cost models (which JS does not reorder) are left as a cost_models map untouched, and params without cost models pass through unchanged.

Verification

  • Offline (normal CI): new normalizeCostModels unit tests (numeric → ordered cost_models_raw, named-op left as-is, no-cost-models pass-through). Full JS offline suite green locally (71 pass).
  • Node-level (DevKit CI): the Plutus-mint round-trip in quicktx.integration.test.js now submits with the devnet's real fetched cost models — the earlier workaround (dropping cost models to use the lib's built-in set) is removed, so this test now genuinely exercises the fix end-to-end.

Closes the P1 cost-model-ordering item in TODO.md §3. The §2c provider helpers should pass params through an equivalent normalization.

DevKit/Blockfrost-style providers return Plutus cost models as a map keyed by
numeric indices ("000"."165"). JS object iteration hoists canonical
integer-string keys ("100") ahead of zero-padded ones ("000"), so
JSON.stringify emitted them out of order; CCL rebuilds the per-language cost
array in document order, so the scrambled order produced a wrong
script-integrity hash and the node rejected Plutus txs with
PPViewHashesDontMatch. (Go's json.Marshal sorts keys; Python preserves provider
order — both unaffected.)

normalizeCostModels() converts any numerically-keyed language to CCL's ordered
cost_models_raw array form (List<Long>, consumed in order ahead of the
order-sensitive named map), which serializes order-stably. Named-operation cost
models are left as-is. The Plutus-mint DevKit round-trip now submits with the
devnet's real fetched cost models (dropping the earlier workaround), and offline
unit tests cover the conversion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant