Skip to content

Add support for Plutus scripts on compatible Tx - #1282

Open
carbolymer wants to merge 2 commits into
masterfrom
mgalazyn/feature/compatible-plutus
Open

Add support for Plutus scripts on compatible Tx#1282
carbolymer wants to merge 2 commits into
masterfrom
mgalazyn/feature/compatible-plutus

Conversation

@carbolymer

@carbolymer carbolymer commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

Adds plutus spending script support to the compatible transaction API, so pre-Conway script transactions (PlutusV1/V2, Alonzo/Babbage) can be built without the deprecated API.
Motivated by the tx-generator migration in IntersectMBO/cardano-node#6602.

Changes:

  • createCompatibleTx supports script-witnessed inputs: spending redeemer pointers, witness datums, script integrity hash, collateral inputs, transaction metadata and validity upper bound.
  • Breaking: all transaction content is now passed as a single CompatibleTxBodyContent record, and failures are reported via the new CompatibleTxError.
  • Breaking: mkSpendingScriptDatum is exported for building spending-purpose datums.
  • Fixes: certificate and proposal script witnesses were missing from the witness set; proposal reference inputs were overwritten.
  • Removes the redundant StakeCredential field from WitTxCert.

Note

The commit TODROP: Fix redeemer pointer indexing in the experimental witness machinery is part of #1288 and will be removed from this PR once #1288 is merged.
This branch is intentionally not rebased onto master yet; CI will start once the rebase lands after #1288.

Checklist

  • New tests: ledger-oracle redeemer pointer properties cover the compatible builder paths
  • Changelog fragment (.changes/20260807_cardano_api_compatible_plutus.yml)
  • Self-reviewed

@carbolymer carbolymer self-assigned this Aug 8, 2026
@carbolymer
carbolymer force-pushed the mgalazyn/feature/compatible-plutus branch 7 times, most recently from 7139bf6 to d6488c0 Compare August 13, 2026 08:58
…hinery

Proposal redeemer pointers were computed against Ord-sorted order
instead of the ledger's OSet insertion order, and certifying pointers
did not count unwitnessed certificates' index slots. Both now follow
the ledger's positional resolution, with regression tests pinning the
behaviour and property tests checking every witnessable category's
pointer against the ledger's own Indexable resolution.

The same fix is applied to the deprecated legacy transaction builder
(createTransactionBody); end-to-end and legacy-bridge regression
properties now cover the deprecated path too.

Also harden getVotes against voter-map gaps, remove the unused
StakeCredential field from the Witnessable WitTxCert constructor, and
correct the redeemer-index ordering documentation.
@carbolymer
carbolymer force-pushed the mgalazyn/feature/compatible-plutus branch 3 times, most recently from 5e5e6af to 75f0875 Compare August 13, 2026 10:35
createCompatibleTx now takes a CompatibleTxBodyContent record with
per-input AnyWitness values, and computes spending redeemer pointers,
witness datums, the script integrity hash, collateral inputs,
transaction metadata and the validity upper bound. Witness indexing is
shared with the experimental transaction builder. Also export the
mkSpendingScriptDatum smart constructor from Cardano.Api.Experimental.
@carbolymer
carbolymer force-pushed the mgalazyn/feature/compatible-plutus branch from 75f0875 to 26aaf4a Compare August 13, 2026 10:40
@carbolymer
carbolymer marked this pull request as ready for review August 13, 2026 15:33
Copilot AI lite review requested due to automatic review settings August 13, 2026 15:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the Cardano.Api.Compatible.Tx builder to support Plutus spending scripts (and related era features) so callers can build pre-Conway Plutus script transactions without relying on deprecated APIs, while also fixing redeemer-pointer indexing behavior and adding regression/property coverage.

Changes:

  • Add Plutus spending-witness support and additional threading (collateral, metadata, validity upper bound, script integrity hash) to createCompatibleTx, with a new CompatibleTxBodyContent record and CompatibleTxError.
  • Fix witness/indexing behavior for proposals/certificates/votes by preserving insertion order (OSet/OMap) and ensuring unwitnessed items occupy redeemer index slots.
  • Add/extend property tests to validate redeemer pointer indexing against ledger resolution and cover new compatible-builder behavior.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cardano-api/test/cardano-api-test/Test/Cardano/Api/Transaction/Body/Plutus/RedeemerIndex.hs New property suite asserting API redeemer indices match ledger Indexable/redeemerPointer resolution.
cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental/Fee.hs Updates compatible-tx tests and adds new properties for spending redeemer order, witness set inclusion, pparams requirement, and extra content threading.
cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs Adds properties verifying proposal insertion-order indexing and certificate indexing that counts unwitnessed certs.
cardano-api/test/cardano-api-test/cardano-api-test.hs Wires the new redeemer-index test group into the test runner.
cardano-api/src/Cardano/Api/Tx/Internal/Body.hs Keeps unwitnessed certificates in witnessable extraction to preserve redeemer index slots; updates proposal-indexing docs.
cardano-api/src/Cardano/Api/Tx.hs Reorders/clarifies export comment for witnessable extractors.
cardano-api/src/Cardano/Api/Internal/Orphans/Serialisation.hs Adds ToJSON for DijkstraLedgerPredFailure.
cardano-api/src/Cardano/Api/Experimental/Tx/Internal/BodyContent/New.hs Includes unwitnessed cert placeholders; makes vote extraction total to avoid index shifts on misses.
cardano-api/src/Cardano/Api/Experimental/Plutus/Internal/ScriptWitness.hs Exports mkSpendingScriptDatum helper for spending-purpose datum construction.
cardano-api/src/Cardano/Api/Experimental/Plutus/Internal/IndexedPlutusScriptWitness.hs Preserves insertion order for certificates/proposals during stable sort by returning EQ intra-category.
cardano-api/src/Cardano/Api/Experimental/Plutus.hs Re-exports mkSpendingScriptDatum.
cardano-api/src/Cardano/Api/Experimental.hs Re-exports mkSpendingScriptDatum.
cardano-api/src/Cardano/Api/Compatible/Tx.hs Major rewrite: createCompatibleTx now takes CompatibleTxBodyContent, supports Plutus spending witnesses, computes script integrity hash, threads metadata/ttl/collateral, and fixes witness inclusion.
cardano-api/cardano-api.cabal Adds cardano-data to test-suite dependencies and includes the new test module.
.changes/20260811_cardano_api_redeemer_pointer_indexing.yml Changelog fragment for redeemer pointer indexing fixes (includes breaking + tests).
.changes/20260807_cardano_api_compatible_plutus.yml Changelog fragment for compatible builder Plutus support and breaking API changes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +136 to +153
data CompatibleTxError
= -- | The supplied protocol parameters could not be converted to the
-- ledger representation needed to compute the script integrity hash.
CompatibleTxProtocolParametersConversionError ProtocolParametersConversionError
| -- | Plutus script witnesses are present (non-empty redeemers, datums
-- or plutus languages), but 'compatibleTxProtocolParams' is
-- 'Nothing', so the ledger's required script integrity hash cannot
-- be computed.
CompatibleTxMissingScriptIntegrityPParams
deriving Show

instance Error CompatibleTxError where
prettyError err =
case err of
CompatibleTxProtocolParametersConversionError e -> prettyError e
CompatibleTxMissingScriptIntegrityPParams ->
"Plutus script witnesses are present but no protocol parameters were supplied "
<> "to compute the script integrity hash."
Comment on lines +901 to +902
txIn1@(Api.TxIn txId (Api.TxIx txIx)) <- H.forAll genTxIn
let txIn2 = Api.TxIn txId (Api.TxIx (txIx + 1))

@palas palas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are a couple of issues that should be resolved before merging, and I also included a couple of potential readability improvement opportunities that I saw.

I still haven't had time to look at the tests yet too.

But it is looking good 👍

Comment on lines +179 to +186
-- Order must stay OMap insertion order; the shared
-- Witnessable-based indexing (via 'extractWitnessableProposals') preserves it.
proposalWitnesses
:: [(Witnessable ProposalItem (ShelleyLedgerEra era), AnyWitness (ShelleyLedgerEra era))]
proposalWitnesses =
[ (ix, witness)
| (_, (ix, witness)) <-
indexWitnessedTxProposalProcedures conwayOnwards proposalProcedures
]
Exp.obtainCommonConstraints (convert conwayOnwards) $
Exp.extractWitnessableProposals $
Just proposalProcedures

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the function is getting too large (even ignoring the where bits).

And proposalWitnesses doesn't need to be in the case, it could be moved to the where (having its own small case), and that would allow moving allWitnesses and sData to the where too (txAuxData can moved to the where already), and I think that would make the body of the function much more manageable.

Actually, updateTxBody wouldn't need to be here either, because I think it doesn't make use of the Either monad, so it wouldn't need to be monadic, it could be moved to the where too, but that is out of scope of the PR

Comment on lines +286 to +297
-- certificates and witnessed spending inputs.
setRefInputs :: Endo (L.TxBody L.TopTx (ShelleyLedgerEra era))
setRefInputs = do
let refInputs =
[ toShelleyTxIn refInput
| (_, _, Just (_, wit)) <- indexedTxCerts
| wit <- witnessedCertWitnesses
, refInput <- maybeToList $ getAnyWitnessReferenceInput wit
]
++ [ toShelleyTxIn refInput
| (_, wit) <- ins
, refInput <- maybeToList $ getAnyWitnessReferenceInput wit
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- certificates and witnessed spending inputs.
setRefInputs :: Endo (L.TxBody L.TopTx (ShelleyLedgerEra era))
setRefInputs = do
let refInputs =
[ toShelleyTxIn refInput
| (_, _, Just (_, wit)) <- indexedTxCerts
| wit <- witnessedCertWitnesses
, refInput <- maybeToList $ getAnyWitnessReferenceInput wit
]
++ [ toShelleyTxIn refInput
| (_, wit) <- ins
, refInput <- maybeToList $ getAnyWitnessReferenceInput wit
]
-- certificates and witnessed spending inputs.
setRefInputs :: Endo (L.TxBody L.TopTx (ShelleyLedgerEra era))
setRefInputs = do
let refInputs =
[ toShelleyTxIn refInput
| wit <- witnessedCertWitnesses <> map snd ins
, refInput <- maybeToList $ getAnyWitnessReferenceInput wit
]

This can be done with a single list comprehension

Comment on lines +394 to +435
setScriptWitnesses sData scriptWitnesses =
appEndos
[ monoidForEraInEon
era
( \aeo -> alonzoEraOnwardsConstraints aeo $ Endo $ do
let sData = convScriptData' sbe extraDatums scriptWitnesses
let (datums, redeemers) = case sData of
TxBodyScriptData _ ds rs -> (ds, rs)
TxBodyNoScriptData -> (mempty, L.Redeemers mempty)
(L.datsTxWitsL .~ datums) . (L.rdmrsTxWitsL %~ (<> redeemers))
( \aeo ->
alonzoEraOnwardsConstraints aeo $
obtainAlonzoScriptPurposeConstraints aeo $
Endo $ do
let (datums, redeemers) =
fromMaybe mempty sData
:: (L.TxDats (ShelleyLedgerEra era), L.Redeemers (ShelleyLedgerEra era))
-- 'getAnyWitnessScript' covers both plutus and simple
-- scripts. The simple ones overlap harmlessly (same
-- hash, same script) with the allegra-onwards branch
-- below, which is still needed for pre-Alonzo eras
-- that this branch does not run in.
let plutusAndSimpleScripts =
mapMaybe getAnyWitnessScript scriptWitnesses
(L.datsTxWitsL .~ datums)
. (L.rdmrsTxWitsL %~ (<> redeemers))
. ( L.scriptTxWitsL
%~ (<> Map.fromList [(L.hashScript sw, sw) | sw <- plutusAndSimpleScripts])
)
)
, monoidForEraInEon
era
( \aeo -> allegraEraOnwardsConstraints aeo $ Endo $ do
let ledgerScripts = convSimpleScripts sbe scriptWitnesses
L.scriptTxWitsL
.~ Map.fromList
[ (L.hashScript sw, sw)
| sw <- ledgerScripts
]
%~ ( <>
Map.fromList
[ (L.hashScript sw, sw)
| sw <- ledgerScripts
]
)
)
]

convSimpleScripts
:: ShelleyLedgerEra era ~ ledgerera
=> ShelleyBasedEra era
-> [(ScriptWitnessIndex, Exp.AnyWitness (ShelleyLedgerEra era))]
-> [Exp.AnyWitness (ShelleyLedgerEra era)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
setScriptWitnesses sData scriptWitnesses =
appEndos
[ monoidForEraInEon
era
( \aeo -> alonzoEraOnwardsConstraints aeo $ Endo $ do
let sData = convScriptData' sbe extraDatums scriptWitnesses
let (datums, redeemers) = case sData of
TxBodyScriptData _ ds rs -> (ds, rs)
TxBodyNoScriptData -> (mempty, L.Redeemers mempty)
(L.datsTxWitsL .~ datums) . (L.rdmrsTxWitsL %~ (<> redeemers))
( \aeo ->
alonzoEraOnwardsConstraints aeo $
obtainAlonzoScriptPurposeConstraints aeo $
Endo $ do
let (datums, redeemers) =
fromMaybe mempty sData
:: (L.TxDats (ShelleyLedgerEra era), L.Redeemers (ShelleyLedgerEra era))
-- 'getAnyWitnessScript' covers both plutus and simple
-- scripts. The simple ones overlap harmlessly (same
-- hash, same script) with the allegra-onwards branch
-- below, which is still needed for pre-Alonzo eras
-- that this branch does not run in.
let plutusAndSimpleScripts =
mapMaybe getAnyWitnessScript scriptWitnesses
(L.datsTxWitsL .~ datums)
. (L.rdmrsTxWitsL %~ (<> redeemers))
. ( L.scriptTxWitsL
%~ (<> Map.fromList [(L.hashScript sw, sw) | sw <- plutusAndSimpleScripts])
)
)
, monoidForEraInEon
era
( \aeo -> allegraEraOnwardsConstraints aeo $ Endo $ do
let ledgerScripts = convSimpleScripts sbe scriptWitnesses
L.scriptTxWitsL
.~ Map.fromList
[ (L.hashScript sw, sw)
| sw <- ledgerScripts
]
%~ ( <>
Map.fromList
[ (L.hashScript sw, sw)
| sw <- ledgerScripts
]
)
)
]
convSimpleScripts
:: ShelleyLedgerEra era ~ ledgerera
=> ShelleyBasedEra era
-> [(ScriptWitnessIndex, Exp.AnyWitness (ShelleyLedgerEra era))]
-> [Exp.AnyWitness (ShelleyLedgerEra era)]
setScriptWitnesses sData scriptWitnesses =
plutusAdditions . simpleScriptAdditions
where
plutusAdditions :: L.TxWits (ShelleyLedgerEra era) -> L.TxWits (ShelleyLedgerEra era)
plutusAdditions =
forEraInEon era id $ \aeo ->
alonzoEraOnwardsConstraints aeo $
obtainAlonzoScriptPurposeConstraints aeo $
let (datums, redeemers) =
fromMaybe mempty sData
:: (L.TxDats (ShelleyLedgerEra era), L.Redeemers (ShelleyLedgerEra era))
-- 'getAnyWitnessScript' covers both plutus and simple
-- scripts. The simple ones overlap harmlessly (same
-- hash, same script) with 'simpleScriptAdditions',
-- which is still needed for pre-Alonzo eras where this
-- addition is 'id'.
plutusAndSimpleScripts =
mapMaybe getAnyWitnessScript scriptWitnesses
in (L.datsTxWitsL .~ datums)
. (L.rdmrsTxWitsL %~ (<> redeemers))
. ( L.scriptTxWitsL
%~ (<> Map.fromList [(L.hashScript sw, sw) | sw <- plutusAndSimpleScripts])
)
simpleScriptAdditions :: L.TxWits (ShelleyLedgerEra era) -> L.TxWits (ShelleyLedgerEra era)
simpleScriptAdditions =
forEraInEon era id $ \aeo ->
allegraEraOnwardsConstraints aeo $
L.scriptTxWitsL
%~ ( <>
Map.fromList
[ (L.hashScript sw, sw)
| sw <- convSimpleScripts sbe scriptWitnesses
]
)
convSimpleScripts
:: ShelleyLedgerEra era ~ ledgerera
=> ShelleyBasedEra era
-> [Exp.AnyWitness (ShelleyLedgerEra era)]

I think Endos in this function only add complexity. They were already here before the PR, but since we are changing most of the function...

-- languages from. Only the redeemer pointer map (built inside
-- 'convScriptData'') needs per-category indexing.
allWitnesses :: [AnyWitness (ShelleyLedgerEra era)]
allWitnesses = witnessedCertWitnesses <> map snd proposalWitnesses <> map snd ins

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are missing TxVotingProcedures witnesses

:: L.AlonzoEraScript ledgerera
=> [(TxIn, AnyWitness ledgerera)]
-> [(Witnessable TxInItem ledgerera, AnyWitness ledgerera)]
witnessableTxIns txIns' = L.nub [(WitTxIn txIn, wit) | (txIn, wit) <- txIns']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should validate there are no repeated TxIns in the list. Or at least that there are no two TxIns with different witnesses, because that would mess up the redeemer pointers

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