Skip to content

Check the Plutus script language against the era when scripts are deserialised - #1372

Closed
carbolymer wants to merge 8 commits into
mgalazyn/fix/unsupported-plutus-language-errorfrom
mgalazyn/fix/jordans-plutus-language-in-era
Closed

carbolymer wants to merge 8 commits into
mgalazyn/fix/unsupported-plutus-language-errorfrom
mgalazyn/fix/jordans-plutus-language-in-era

Conversation

@carbolymer

@carbolymer carbolymer commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on PR 1363.
The seven commits are Jordan's, from the branch he linked on that PR, rebased onto 1363.

PlutusScriptInEra now requires the type-level constraint PlutusLangInEra lang era, so a Plutus script in a language the era does not support cannot be built in the experimental API.
The check runs once, where the bytes enter: decodeAnyPlutusScript, deserialiseAnyPlutusScriptOfLanguage and the text envelope decoder reject an unsupported language with a decoder error.
fromPlutusRunnable is total again.
The Either L.Language results and MakeUnsignedTxPlutusLanguageNotSupportedInEra added by PR 1363 are removed.
The legacy shim turns the ScriptLanguageInEra proof of a legacy witness into the new evidence, so the legacy path needs no runtime check.
The createTransactionBody reference script check, the generator fix and the V4 fixture from PR 1363 stay.

Context

Jordan proposed this design in a comment on PR 1363:

Open points for review:

  • PlutusLangsInEra has no instances for Shelley, Allegra and Mary, so readFilePlutusScript in Cardano.CLI.Compatible.Read, which runs under a plain ShelleyBasedEra, does not compile against this branch.
  • The error for an unsupported language names the language but not the era, and the text envelope path reports an unknown envelope type instead.
  • The check-changelog job fails because the fragments carry pr: 1363; this PR needs its own fragment once the design is settled.
  • PlutusLangInEra is a third copy of the language per era table, next to ledger's mkPlutusScript and the legacy ScriptLanguageInEra.

How to trust this PR

The test that built a Plutus V4 inline witness in Conway no longer typechecks, which is the point of the change.
prop_deserialise_rejects_unsupported_plutus_language replaces it and checks that V4 bytes fail to deserialise in Conway.
prop_makeUnsignedTx_accepts_reference_plutus_v4_witness and prop_createTransactionBody_rejects_unsupported_reference_script_language from PR 1363 are kept.

Checklist

  • Commit sequence broadly makes sense and commits have useful messages
  • New tests are added if needed and existing tests are updated. See Running tests for more details
  • Self-reviewed the diff
  • Changelog fragment added in .changes/

@carbolymer
carbolymer changed the base branch from master to mgalazyn/fix/unsupported-plutus-language-error October 7, 2026 13:35
Comment thread cardano-api/src/Cardano/Api/Experimental/Plutus/Internal/Script.hs Fixed
@carbolymer carbolymer self-assigned this Oct 7, 2026
@carbolymer carbolymer changed the title Mgalazyn/fix/jordans plutus language in era Check the Plutus script language against the era when scripts are deserialised Oct 7, 2026
@carbolymer
carbolymer force-pushed the mgalazyn/fix/unsupported-plutus-language-error branch from b2764fd to 88b834f Compare October 8, 2026 08:12
@carbolymer
carbolymer force-pushed the mgalazyn/fix/jordans-plutus-language-in-era branch from 3db491a to 029efd1 Compare October 8, 2026 08:12
PlutusLangInEra lang era is a Constraint-kinded closed type family listing
the language/era pairs the ledger accepts, mirroring eraMaxLanguage. The
PlutusLangsInEra class reflects it back as runtime evidence for code that
only learns the language at runtime. Nothing uses it yet.

Compiles.
fromPlutusRunnable now demands PlutusLangInEra lang era and returns
PlutusScript era instead of Maybe. The only way ledger's mkPlutusScript
returned Nothing was an unsupported language/era pairing, which the
constraint now excludes, so the Nothing branch is unreachable.

With the Left gone, the Either L.Language plumbing through
getAnyWitnessScript, getTxScriptWitnessRequirements,
collectTxBodyScriptWitnessRequirements and makeUnsignedTx is dropped and
MakeUnsignedTxPlutusLanguageNotSupportedInEra is removed.

Does not compile. The constraint has to come from somewhere:

  AnyScriptWitness.hs:263:31: error:
    Could not deduce 'PlutusLangInEra lang era'
      arising from a use of 'fromPlutusRunnable'
    from the context: L.AlonzoEraScript era
    or from: L.PlutusLanguage lang
      bound by a pattern with constructor: PlutusScriptInEra

The caller only has the witness in hand, and lang is existential in
AnyWitness. The evidence must travel inside the value: the next commit
puts it on the PlutusScriptInEra constructor.
The constraint is packed into the constructor so that any pattern match on
PlutusScriptInEra brings it back into scope. This is what lets
resolvePlutusWitnessScript call fromPlutusRunnable even though lang is
existential in AnyWitness: the evidence travels with the value.

Within this module GHC then demanded the constraint, in order, at:

  Script.hs:141 deserialiseFromCBOR   Could not deduce 'PlutusLangInEra lang era'
                                      arising from a use of 'PlutusScriptInEra'
  Script.hs:90  HasTextEnvelope       arising from the superclasses of an instance
  Script.hs:153 deserialisePlutusScriptInEra  arising from 'deserialiseFromCBOR'
  Script.hs:214 decodeAnyPlutusScript        arising from 'deserialisePlutusScriptInEra'
  Script.hs:239 serialiseAnyPlutusScriptToTextEnvelope  arising from 'serialiseToTextEnvelope'
  Script.hs:258 deserialiseAnyPlutusScriptFromTextEnvelope  arising from 'FromSomeType'

Three kinds of fix, by what is in scope at each site:

  * lang and era are type variables of a signature or instance head:
    add the constraint to the context (CBOR instance, HasTextEnvelope
    instance, deserialisePlutusScriptInEra).
  * the value is already a PlutusScriptInEra: match the constructor
    (serialiseAnyPlutusScriptToTextEnvelope).
  * lang is existential, learned from an envelope type string at runtime:
    look up PlutusLangsInEra evidence and match it (decodeAnyPlutusScript,
    deserialiseAnyPlutusScriptFromTextEnvelope). An unsupported language is
    now a decoder error, or an envelope type that is not offered at all.

Does not compile. Next module:

  AnyScript.hs:107 tryPlutusScript  Could not deduce 'PlutusLangInEra l era'
  AnyScript.hs:133 deserialiseAnyPlutusScriptOfLanguage
                                    Could not deduce 'PlutusLangInEra lang (LedgerEra era)'
  AnyScript.hs:204 readTextEnvelopeScript
                                    Could not deduce 'PlutusLangsInEra (LedgerEra era)'
AnyScript is where cardano-cli reads script files for every era, so this
is where the runtime check pays once.

  AnyScript.hs:133 deserialiseAnyPlutusScriptOfLanguage
      lang and era are both type variables. Look up PlutusLangsInEra
      evidence; Nothing becomes a decoder error.
  AnyScript.hs:107 tryPlutusScript
      the ledger's withPlutusScript hands over an existential language
      with no era evidence. Look it up; Nothing falls through to the
      next decoder. Ledger only yields scripts valid for the era, so this
      Nothing is unreachable, but GHC cannot know that.
  AnyScript.hs:204 readTextEnvelopeScript
      needs PlutusLangsInEra (LedgerEra era) from obtainCommonConstraints,
      so EraCommonConstraints gains it.

The SerialiseAsCBOR (AnyScript era) instance gains PlutusLangsInEra era in
its context for the same reason.

Does not compile. GHC now stops at the function this chain started from:

  AnyScriptWitness.hs:273:6: error: [-Wredundant-constraints]
    Redundant constraint: PlutusLangInEra lang era
    In the type signature for: fromPlutusRunnable

Nothing in the body consumes the constraint. Ledger's mkPlutusScript is
value-level and still returns Maybe, so the constraint on the function
does no work GHC can see. The next commit addresses that.
…nt type

GHC rejected the previous signature:

  AnyScriptWitness.hs:273:6: error: [-Wredundant-constraints]
    Redundant constraint: PlutusLangInEra lang era
    In the type signature for: fromPlutusRunnable

The constraint did no work in the body because ledger's mkPlutusScript is
value-level and still returns Maybe. The restriction that actually holds
is on the constructor, so fromPlutusRunnable now takes the
PlutusScriptInEra lang era itself. That type is only inhabited for
pairings in the PlutusLangInEra table, so the signature still excludes
invalid pairings, and the Nothing branch stays unreachable.

Does not compile. The legacy shim is next. GHC stops at its first error,
the Either wrapper added by the PR around a function that no longer
returns Either:

  LegacyScripts.hs:401:12: error:
    Couldn't match expected type: Either a0 (TxScriptWitnessRequirements ...)
                with actual type: TxScriptWitnessRequirements ...

Behind it, mkPlutusScriptWitness builds PlutusScriptInEra four times with
lang and era abstract over AlonzoEraOnwards, and will demand
PlutusLangInEra plutuslang (ShelleyLedgerEra era).
…roof

The legacy Old.PlutusScriptWitness already carries a ScriptLanguageInEra
lang era proof that the shim was discarding with a wildcard. Matching its
ten Plutus constructors yields PlutusLangInEraEvidence for the ledger era;
the seven simple-script constructors are closed with EmptyCase on the
PlutusScriptVersion, which has no SimpleScript' inhabitant.

mkPlutusScriptWitness gains PlutusLangInEra plutuslang (ShelleyLedgerEra
era) in its context, discharged by matching the evidence in
toNewPlutusScriptWitness. The error call the PR added to
legacyWitnessToScriptRequirements is removed, since the Either it
guarded is gone.

This is why the PlutusLangInEra table has Alonzo and Babbage rows: the
legacy createTransactionBody path reaches this shim for every era from
Alonzo onwards.

The library compiles. Tests and generators do not yet.
Cardano.Api.Experimental and Cardano.Api.Experimental.Plutus export
PlutusLangInEra, PlutusLangInEraEvidence and PlutusLangsInEra.

assertValidPlutusScriptBytesExperimental is polymorphic in both lang and
era, so it looks up evidence before deserialising.

The PR's test that built a PlutusV4 inline witness in Conway no longer
typechecks, which is the point of the change. It is replaced by a test
that deserialising V4 bytes in Conway fails.

Compiles. cardano-api-test and cardano-api-golden pass.
@carbolymer
carbolymer force-pushed the mgalazyn/fix/unsupported-plutus-language-error branch from 88b834f to bec442d Compare October 8, 2026 08:38
@carbolymer
carbolymer force-pushed the mgalazyn/fix/jordans-plutus-language-in-era branch 9 times, most recently from ef10201 to fa793a8 Compare October 8, 2026 13:14
PlutusLangInEra lang era is now a single-constructor GADT holding the language singleton, with the ledger's EraPlutusTxInfo lang era class as its context, so the ledger's instances are the table and cardano-api keeps no copy.
An unsupported pairing has no ledger instance, so the closed type family, its TypeError and UndecidableInstances go.
plutusLangInShelleyBasedEra gives the proof at runtime from a ShelleyBasedEra witness by asking the ledger's mkSupportedLanguage, and HasPlutusLangInEra wraps it for code that only has the ledger era, with instances for all seven eras.
PlutusScriptInEra carries the ledger script next to the runnable, built by mkPlutusScriptInEra under the EraPlutusTxInfo constraint through the ledger's mkSupportedPlutusScript, so plutusScriptInEraToScript is a field read and fromPlutusRunnable goes.
The unsupported language error names the era, and the text envelope decoder recognises the envelope's language before the type lookup so that error is reported instead of an unknown envelope type.
The SerialiseAsCBOR and HasTextEnvelope instances of PlutusScriptInEra require EraPlutusTxInfo lang era, so they exist only for supported pairings and carry no runtime check.
@carbolymer
carbolymer force-pushed the mgalazyn/fix/jordans-plutus-language-in-era branch from fa793a8 to 0e243ec Compare October 8, 2026 13:22
@carbolymer carbolymer closed this Oct 8, 2026
@carbolymer
carbolymer deleted the mgalazyn/fix/jordans-plutus-language-in-era branch October 9, 2026 07:25
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.

3 participants