Add support for Plutus scripts on compatible Tx - #1282
Conversation
7139bf6 to
d6488c0
Compare
…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.
5e5e6af to
75f0875
Compare
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.
75f0875 to
26aaf4a
Compare
There was a problem hiding this comment.
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 newCompatibleTxBodyContentrecord andCompatibleTxError. - 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.
| 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." |
| txIn1@(Api.TxIn txId (Api.TxIx txIx)) <- H.forAll genTxIn | ||
| let txIn2 = Api.TxIn txId (Api.TxIx (txIx + 1)) |
palas
left a comment
There was a problem hiding this comment.
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 👍
| -- 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 |
There was a problem hiding this comment.
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
| -- 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 | ||
| ] |
There was a problem hiding this comment.
| -- 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
| 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)] |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
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'] |
There was a problem hiding this comment.
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
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:
createCompatibleTxsupports script-witnessed inputs: spending redeemer pointers, witness datums, script integrity hash, collateral inputs, transaction metadata and validity upper bound.CompatibleTxBodyContentrecord, and failures are reported via the newCompatibleTxError.mkSpendingScriptDatumis exported for building spending-purpose datums.StakeCredentialfield fromWitTxCert.Note
The commit
TODROP: Fix redeemer pointer indexing in the experimental witness machineryis part of #1288 and will be removed from this PR once #1288 is merged.This branch is intentionally not rebased onto
masteryet; CI will start once the rebase lands after #1288.Checklist
.changes/20260807_cardano_api_compatible_plutus.yml)