You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
All three transaction builders accept an input list that repeats the same TxIn with two different witnesses, and silently build a transaction the ledger can never accept:
the experimental builder (Cardano.Api.Experimental.Tx.Internal.BodyContent.New, extractWitnessableTxIns)
the deprecated legacy builder (Cardano.Api.Tx.Internal.Body, extractWitnessableTxIns)
All three nub the whole (TxIn, witness) pair, so two entries that share a TxIn but differ in witness both survive.
The redeemer pointer machinery then assigns consecutive indices by list position, while the transaction body stores inputs as a Set, which collapses the duplicate to a single slot.
Every spending redeemer after the duplicate points one slot too far.
Impact
This is reachable from cardano-cli today. --tx-in TXID#0 --tx-in-script-file a.plutus ... --tx-in TXID#0 --tx-in-script-file b.plutus passes the parser, and nothing between optparse and the builders validates uniqueness.
Collateral inputs, by contrast, are already deduplicated with nubOrd in the same code path.
The observable outcome depends on the command:
transaction build and build-estimate fail client side, during execution-unit evaluation, with ScriptErrorRedeemerPointsToUnknownScriptHash wrapped in a TxBodyScriptExecutionError.
transaction build-raw and the compatible builder serialise the transaction without complaint; it is then rejected at submission with ExtraRedeemers (UTXOW, phase 1).
One edge case validates: when the duplicated TxIn carries one script witness and one key placeholder and sorts last among the distinct inputs, the cardinalities line up, the transaction is accepted, and the other supplied witness is silently ignored. The index-set equality check guarantees the surviving redeemer is the correct one, so a redeemer can never execute against the wrong script.
This is a usability bug: confusing failures, or a silently ignored witness. Funds are not at risk.
Proposed fix
Two options, in order of preference:
Make duplicates unrepresentable: key the inputs by TxIn in an insertion-ordered map (OMap), the way TxCertificates and TxProposalProcedures already work.
Spending inputs are the only witnessable category still passed around as a plain association list.
Rejecting a duplicate with a different witness then happens at map construction, which is how mkTxVotingProcedures already treats a duplicate voter ("This would cause ignoring some of the votes").
Keep the list and validate before constructing the body: add an error constructor per builder error type (for example CompatibleTxDuplicateTxIn TxIn in CompatibleTxError), plus analogous constructors for the experimental and legacy builders.
Either way:
consider deduplicating or validating at the cardano-cli layer too, mirroring the existing nubOrd collateral handling
extend the redeemer pointer property suite (Test.Cardano.Api.Transaction.Body.Plutus.RedeemerIndex) with duplicate-input cases
pin compareWitnesses with a property while in there: the Fix plutus script redeemer pointer indexing #1288 review flagged that the WitTxCert comparator returns LT for every pair, which degenerates to insertion order under a stable sort. Insertion order happens to be what certificates need, but that should be a tested invariant rather than a coincidence.
Related PRs and issues
Positional indexing has broken once per witnessable category; this issue covers the last one:
Fix plutus script redeemer pointer indexing #1288 fixed proposal pointers (indexed by Ord instead of OSet insertion order) and unwitnessed certificate slots in the legacy builder, and added the ledger-oracle property suite
Problem
All three transaction builders accept an input list that repeats the same
TxInwith two different witnesses, and silently build a transaction the ledger can never accept:createCompatibleTx(Cardano.Api.Compatible.Tx,witnessableTxIns)Cardano.Api.Experimental.Tx.Internal.BodyContent.New,extractWitnessableTxIns)Cardano.Api.Tx.Internal.Body,extractWitnessableTxIns)All three
nubthe whole(TxIn, witness)pair, so two entries that share aTxInbut differ in witness both survive.The redeemer pointer machinery then assigns consecutive indices by list position, while the transaction body stores inputs as a
Set, which collapses the duplicate to a single slot.Every spending redeemer after the duplicate points one slot too far.
Impact
This is reachable from cardano-cli today.
--tx-in TXID#0 --tx-in-script-file a.plutus ... --tx-in TXID#0 --tx-in-script-file b.plutuspasses the parser, and nothing between optparse and the builders validates uniqueness.Collateral inputs, by contrast, are already deduplicated with
nubOrdin the same code path.The observable outcome depends on the command:
transaction buildandbuild-estimatefail client side, during execution-unit evaluation, withScriptErrorRedeemerPointsToUnknownScriptHashwrapped in aTxBodyScriptExecutionError.transaction build-rawand the compatible builder serialise the transaction without complaint; it is then rejected at submission withExtraRedeemers(UTXOW, phase 1).TxIncarries one script witness and one key placeholder and sorts last among the distinct inputs, the cardinalities line up, the transaction is accepted, and the other supplied witness is silently ignored. The index-set equality check guarantees the surviving redeemer is the correct one, so a redeemer can never execute against the wrong script.This is a usability bug: confusing failures, or a silently ignored witness. Funds are not at risk.
Proposed fix
Two options, in order of preference:
TxInin an insertion-ordered map (OMap), the wayTxCertificatesandTxProposalProceduresalready work.Spending inputs are the only witnessable category still passed around as a plain association list.
Rejecting a duplicate with a different witness then happens at map construction, which is how
mkTxVotingProceduresalready treats a duplicate voter ("This would cause ignoring some of the votes").CompatibleTxDuplicateTxIn TxIninCompatibleTxError), plus analogous constructors for the experimental and legacy builders.Either way:
nubOrdcollateral handlingTest.Cardano.Api.Transaction.Body.Plutus.RedeemerIndex) with duplicate-input casescompareWitnesseswith a property while in there: the Fix plutus script redeemer pointer indexing #1288 review flagged that theWitTxCertcomparator returnsLTfor every pair, which degenerates to insertion order under a stable sort. Insertion order happens to be what certificates need, but that should be a tested invariant rather than a coincidence.Related PRs and issues
Positional indexing has broken once per witnessable category; this issue covers the last one:
Ordinstead ofOSetinsertion order) and unwitnessed certificate slots in the legacy builder, and added the ledger-oracle property suitemapScriptWitnessesCertificatessilently dropping key-witnessed certificatesMissingRedeemerson Plutus stake delegation, a user-facing symptom from the same pointer machinerytransaction view, because pointers are opaque to usersContext
Split out of #1282 so the fix can cover the experimental, legacy and compatible APIs together.
Review comment: #1282 (comment)