Fix plutus script redeemer pointer indexing - #1288
Conversation
604df2f to
ab72b80
Compare
There was a problem hiding this comment.
Pull request overview
Fixes Plutus redeemer pointer indexing to match ledger resolution across both the experimental transaction builder and the deprecated createTransactionBody path, and adds regression/property tests to prevent pointer-index drift (including a documented breaking change to WitTxCert).
Changes:
- Align proposal redeemer indexing with transaction insertion order; ensure certificate indexing counts unwitnessed certificates as index slots.
- Harden vote extraction against missing voter witnesses so indices cannot shift due to dropped entries.
- Add new property tests that compare API-assigned redeemer pointers against the ledger’s
Indexable/redeemerPointerbehavior, and record the breaking change in a changelog fragment.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 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 redeemer pointer/index agreement with the ledger across all witnessable categories (incl. deprecated path). |
| cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs | Adds focused regression properties for proposal insertion-order indexing and certificate “count unwitnessed” indexing in makeUnsignedTx. |
| cardano-api/test/cardano-api-test/cardano-api-test.hs | Registers the new redeemer-index test group. |
| cardano-api/src/Cardano/Api/Tx/Internal/Body.hs | Old-API path: keep unwitnessed certificates as index-slot placeholders when extracting witnessables. |
| cardano-api/src/Cardano/Api/Tx.hs | Adjusts export positioning/comment for extractWitnessableCertificates. |
| cardano-api/src/Cardano/Api/Experimental/Tx/Internal/BodyContent/New.hs | Experimental path: include unwitnessed cert placeholders and make vote extraction total (no gaps). |
| cardano-api/src/Cardano/Api/Experimental/Plutus/Internal/IndexedPlutusScriptWitness.hs | Updates ordering to preserve insertion order for certs/proposals and removes unused StakeCredential from WitTxCert (breaking). |
| cardano-api/cardano-api.cabal | Adds test-suite dependency and test module entry for new property suite. |
| .changes/20260811_cardano_api_redeemer_pointer_indexing.yml | Changelog fragment classifying the change as bugfix + breaking + tests. |
Suppressed comments (1)
cardano-api/src/Cardano/Api/Experimental/Plutus/Internal/IndexedPlutusScriptWitness.hs:121
- Same issue as certificates: returning
LTfor any twoWitProposalvalues violates ordering laws required bysortBy. If you want to preserve insertion order for proposals, returnEQand rely on the stability ofsortByto keep the original order.
-- Proposals are also stored in an `OSet` and resolved positionally
-- (`StrictSeq.findIndexL`), same as `WitTxCert` above.
(WitProposal{}, WitProposal{}) -> LT
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| (WitProposal propA, WitProposal propB) -> compare propA propB | ||
| -- Proposals are also stored in an `OSet` and resolved positionally | ||
| -- (`StrictSeq.findIndexL`), same as `WitTxCert` above. | ||
| (WitProposal{}, WitProposal{}) -> LT |
| , wit | ||
| ) | ||
| | (Exp.Certificate cert, Just (stakeCred, wit)) <- getCertificates txCerts | ||
| [ (WitTxCert cert, maybe AnyKeyWitnessPlaceholder snd mCredAndWit) |
There was a problem hiding this comment.
So in when we transition to Dijsktra this is when all certificates need a witness. In Conway stake registration certificates don't need any witnesses.
There was a problem hiding this comment.
Thanks for checking that. To make the types more precise, without overcomplicating things I think the path forward here is to turn our current definition
newtype TxCertificates era
= TxCertificates
{unTxCertificates :: OMap (Exp.Certificate era) (Maybe (StakeCredential, AnyWitness era))}
deriving (Show, Eq)into GADT:
data TxCertificates era where
TxCertificatesConway
:: OMap (Exp.Certificate ConwayEra) (Maybe (StakeCredential, AnyWitness ConwayEra))
-> TxCertificates ConwayEra
TxCertificatesDijkstra
:: OMap (Exp.Certificate DijkstraEra) (StakeCredential, AnyWitness DijkstraEra)
-> TxCertificates DijkstraEra
After hardforking to Dijkstra we could drop the old era constructor and go back to newtype.
TxCertificates era is however used in compatible API, so this would mean duplicating the newtype definition there.
There was a problem hiding this comment.
Another alternative would be to add a type family:
type family CertWitness era where
CertWitness ShelleyEra = Maybe (StakeCredential, AnyWitness ConwayEra)
...
CertWitness ConwayEra = Maybe (StakeCredential, AnyWitness ConwayEra)
CertWitness DijkstraEra = (StakeCredential, AnyWitness ConwayEra)But it would pollute experimental API with older eras information, which is exactly what we want to avoid in experimental api.
ab72b80 to
3bde01c
Compare
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.
2eb11a2 to
e238d9e
Compare
Fix plutus script redeemer pointer indexing: proposal pointers now follow the transaction's insertion order and certificate pointers count unwitnessed certificates, in both the experimental and the deprecated transaction builders. Remove the unused StakeCredential field from the WitTxCert constructor. Add property tests checking every redeemer pointer against the ledger's own resolution.
Context
Redeemer pointers for certificates and proposals were computed against the wrong order. Proposals were sorted by their
Ordinstance before index assignment, but the ledger resolvesProposingpurposes by the transaction's insertion order (OSet->StrictSeqpositional lookup). Unwitnessed certificates (e.g. plain stake registrations) were dropped before index assignment, but the ledger counts every certificate's position in the full sequence, so a plutus-witnessed certificate placed after an unwitnessed one received a shifted pointer and failed phase-2 validation. The certificate bug existed in both the experimental builder and the deprecatedcreateTransactionBodypath (reachable viamakeTransactionBodyAutoBalance).Also hardened
getVotesagainst gaps in the voter map: it now uses a total lookup with a placeholder witness on a miss, rather than a lookup that silently skips a missing voter and shifts every later voter's redeemer index.How this was fixed
compareWitnessesfor proposals.StakeCredentialfield fromWitTxCert(breaking).indexTxProposalProcedures/indexWitnessedTxProposalProcedures.How to trust this PR
Property tests assert every witnessable category's redeemer pointer against the ledger's own
Indexable/redeemerPointerresolution: container-level oracles for all six categories, old-API bridge twins, and an end-to-endcreateTransactionBodyproperty with per-certificate redeemer binding. Each fix was mutation-verified (reverting the fix makes the corresponding property fail).Checklist
.changes/