Skip to content

Commit d7b37d2

Browse files
committed
fix(threat-model): fix Phase 1 validation for doubleSatisfaction
- Fix inline datum handling: TxOutDatumInline should not add datums to witness set (fixes NotAllowedSupplementalDatums error) - Add recalculateScriptIntegrityHash to recompute PPViewHash after transaction modifications (fixes PPViewHashesDontMatch error) - All 4 threat model tests now pass correctly
1 parent 1362db6 commit d7b37d2

File tree

3 files changed

+74
-18
lines changed

3 files changed

+74
-18
lines changed

src/coin-selection/test/Spec.hs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -576,22 +576,19 @@ This test runs the doubleSatisfaction threat model against the VULNERABLE
576576
bounty validator. The threat model attempts to bundle a "safe script" input
577577
that satisfies the vulnerable script's output requirement.
578578
579-
The bounty script only checks "some output pays to beneficiary" without
580-
uniquely identifying which output belongs to this spend.
579+
Since the vulnerable bounty only checks "some output pays to beneficiary"
580+
without uniquely identifying which output belongs to this spend, the threat
581+
model WILL find a vulnerability.
581582
582-
NOTE: In Conway era, the doubleSatisfaction attack is blocked by Phase 1
583-
ledger rules (PPViewHashesDontMatch, NotAllowedSupplementalDatums) before the
584-
script even runs. The threat model needs to be updated to properly handle
585-
Conway-era datum and protocol parameter rules for the attack to work.
586-
587-
For now, this test verifies that the attack is rejected (by Phase 1 rules),
588-
which is still a secure outcome even if not for the intended reason.
583+
We use 'expectFailure' because finding the vulnerability means the
584+
QuickCheck property fails (which is the expected behavior for a vulnerable
585+
script).
589586
590587
NOTE: This test uses runThreatModelM which runs INSIDE MockchainT for full
591588
Phase 1 + Phase 2 validation with re-balancing and re-signing.
592589
-}
593590
propBountyVulnerableToDoubleSatisfaction :: RunOptions -> Property
594-
propBountyVulnerableToDoubleSatisfaction opts = monadicIO $ do
591+
propBountyVulnerableToDoubleSatisfaction opts = QC.expectFailure $ monadicIO $ do
595592
let Options{params} = mcOptions opts
596593

597594
-- Run the scenario AND the threat model INSIDE MockchainT

src/testing-interface/lib/Convex/ThreatModel/Cardano/Api.hs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ module Convex.ThreatModel.Cardano.Api where
99
import Cardano.Api
1010

1111
import Cardano.Ledger.Allegra.Scripts (ValidityInterval (..))
12+
import Cardano.Ledger.Alonzo.PParams (getLanguageView)
1213
import Cardano.Ledger.Alonzo.Scripts qualified as Ledger
14+
import Cardano.Ledger.Alonzo.Tx (hashScriptIntegrity)
1315
import Cardano.Ledger.Alonzo.TxBody qualified as Ledger
1416
import Cardano.Ledger.Alonzo.TxWits qualified as Ledger
1517
import Cardano.Ledger.Api.Era qualified as Ledger (eraProtVerLow)
@@ -18,6 +20,7 @@ import Cardano.Ledger.Binary qualified as CBOR
1820
import Cardano.Ledger.Conway.Scripts qualified as Conway
1921
import Cardano.Ledger.Conway.TxBody qualified as Conway
2022
import Cardano.Ledger.Keys (WitVKey (..), coerceKeyRole, hashKey)
23+
import Cardano.Ledger.Plutus.Language qualified as Plutus
2124
import Cardano.Slotting.Slot ()
2225
import Cardano.Slotting.Time (SlotLength, mkSlotLength)
2326
import Control.Lens ((&), (.~), _1)
@@ -41,6 +44,7 @@ import Data.Maybe (listToMaybe)
4144
import Data.Maybe.Strict
4245
import Data.SOP.NonEmpty (NonEmpty (NonEmptyOne))
4346
import Data.Sequence.Strict qualified as Seq
47+
import Data.Set qualified as Set
4448
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
4549
import Data.Word
4650
import GHC.Exts (toList)
@@ -354,9 +358,6 @@ This function:
354358
1. Calculates the new required fee
355359
2. Adjusts the change output (last output to wallet address) to compensate
356360
3. Re-signs the transaction with the wallet's key
357-
358-
Note: This works at the ledger level to preserve the transaction structure
359-
created by TxModifier operations.
360361
-}
361362
rebalanceAndSign
362363
:: (MonadMockchain Era m, MonadFail m)
@@ -392,13 +393,66 @@ rebalanceAndSign wallet tx utxo = do
392393
adjustedOutputs <- adjustChangeOutput walletAddr feeDiff currentOuts
393394

394395
-- Apply the changes: new fee and adjusted outputs
395-
let finalTx = setTxOutputsList adjustedOutputs $ setTxFeeCoin newFee tx
396+
let modifiedTx = setTxOutputsList adjustedOutputs $ setTxFeeCoin newFee tx
397+
398+
-- Recalculate script integrity hash
399+
let finalTx = recalculateScriptIntegrityHash pparams modifiedTx
396400

397401
-- Re-sign (strip old signatures and add new one)
398402
let Tx finalBody _ = finalTx
399403
unsignedTx = makeSignedTransaction [] finalBody
400404
pure $ Wallet.signTx wallet unsignedTx
401405

406+
{- | Recalculate and update the script integrity hash in a transaction.
407+
408+
The script integrity hash commits to:
409+
- The redeemers in the transaction
410+
- The datums in the witness set
411+
- The cost models for languages used (from protocol parameters)
412+
413+
After modifying a transaction (adding/removing inputs, changing redeemers/datums),
414+
this hash becomes stale and must be recalculated.
415+
-}
416+
recalculateScriptIntegrityHash :: LedgerProtocolParameters Era -> Tx Era -> Tx Era
417+
recalculateScriptIntegrityHash pparams (Tx (ShelleyTxBody era body scripts scriptData auxData validity) wits) =
418+
let
419+
-- Extract redeemers and datums from scriptData
420+
(redeemers, datums) = case scriptData of
421+
TxBodyNoScriptData -> (Ledger.Redeemers mempty, Ledger.TxDats mempty)
422+
TxBodyScriptData _ dats rdmrs -> (rdmrs, dats)
423+
424+
-- Get the protocol parameters
425+
pp = unLedgerProtocolParameters pparams
426+
427+
-- Determine which languages are used by examining the scripts in the transaction
428+
usedLangs =
429+
Set.fromList
430+
[ lang
431+
| script <- scripts
432+
, Just lang <- [getScriptLanguage script]
433+
]
434+
435+
-- Get LangDepView for each used language
436+
langs =
437+
Set.fromList
438+
[ getLanguageView pp lang
439+
| lang <- Set.toList usedLangs
440+
]
441+
442+
-- Compute new script integrity hash
443+
newHash = hashScriptIntegrity langs redeemers datums
444+
445+
-- Update the body with new hash
446+
body' = body{Conway.ctbScriptIntegrityHash = newHash}
447+
in
448+
Tx (ShelleyTxBody era body' scripts scriptData auxData validity) wits
449+
450+
-- | Extract the Plutus language from a ledger script, if it's a Plutus script
451+
getScriptLanguage :: Ledger.AlonzoScript LedgerEra -> Maybe Plutus.Language
452+
getScriptLanguage script = case script of
453+
Ledger.TimelockScript{} -> Nothing
454+
Ledger.PlutusScript ps -> Just $ Ledger.plutusScriptLanguage ps
455+
402456
-- | Get the fee from a transaction
403457
getTxFeeCoin :: Tx Era -> Coin
404458
getTxFeeCoin (Tx (ShelleyTxBody _ body _ _ _ _) _) = Conway.ctbTxfee body

src/testing-interface/lib/Convex/ThreatModel/TxModifier.hs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,13 @@ applyTxMod tx utxos (AddOutput addr value datum refscript) =
247247
toShelleyTxOut
248248
shelleyBasedEra
249249
(makeTxOut addr value datum refscript)
250+
-- Note: Inline datums are embedded in the output itself, NOT in the witness set.
251+
-- Only supplemental datums (for TxOutDatumHash outputs) go in the witness set.
250252
scriptData' = case datum of
251253
TxOutDatumNone -> scriptData
252254
TxOutDatumHash{} -> scriptData
253255
TxOutSupplementalDatum _ d -> addDatum (toAlonzoData d) scriptData
254-
TxOutDatumInline _ d -> addDatum (toAlonzoData d) scriptData
256+
TxOutDatumInline{} -> scriptData
255257
applyTxMod tx utxos (AddInput addr value datum rscript False) =
256258
( Tx (ShelleyTxBody era body{Conway.ctbSpendInputs = inputs'} scripts scriptData'' auxData validity) wits
257259
, utxos'
@@ -271,11 +273,12 @@ applyTxMod tx utxos (AddInput addr value datum rscript False) =
271273
| idx' >= idx = idx' + 1
272274
| otherwise = idx'
273275

276+
-- Note: Inline datums are embedded in the output itself, NOT in the witness set.
274277
scriptData'' = case datum of
275278
TxOutDatumNone -> scriptData'
276279
TxOutDatumHash{} -> scriptData'
277280
TxOutSupplementalDatum _ d -> addDatum (toAlonzoData d) scriptData'
278-
TxOutDatumInline _ d -> addDatum (toAlonzoData d) scriptData'
281+
TxOutDatumInline{} -> scriptData'
279282

280283
scriptData' = recomputeScriptData Nothing idxUpdate scriptData
281284
applyTxMod tx utxos (AddInput addr value datum rscript True) =
@@ -455,13 +458,14 @@ applyTxMod tx utxos (ChangeOutput ix maddr mvalue mdatum mrscript) =
455458
(fromMaybe datum mdatum)
456459
(fromMaybe rscript mrscript)
457460

461+
-- Note: Inline datums are embedded in the output itself, NOT in the witness set.
458462
scriptData' = case mdatum of
459463
Nothing -> scriptData
460464
Just d -> case d of
461465
TxOutDatumNone -> scriptData
462466
TxOutDatumHash{} -> scriptData
463467
TxOutSupplementalDatum _ d' -> addDatum (toAlonzoData d') scriptData
464-
TxOutDatumInline _ d' -> addDatum (toAlonzoData d') scriptData
468+
TxOutDatumInline{} -> scriptData
465469
applyTxMod tx utxos (ChangeInput txIn maddr mvalue mdatum mrscript) =
466470
(Tx (ShelleyTxBody era body scripts scriptData' auxData validity) wits, utxos')
467471
where
@@ -479,12 +483,13 @@ applyTxMod tx utxos (ChangeInput txIn maddr mvalue mdatum mrscript) =
479483
(fromMaybe rscript mrscript)
480484
utxos' = UTxO . Map.insert txIn txOut . unUTxO $ utxos
481485

486+
-- Note: Inline datums are embedded in the output itself, NOT in the witness set.
482487
scriptData' = case mdatum of
483488
Nothing -> scriptData
484489
Just TxOutDatumNone -> scriptData
485490
Just TxOutDatumHash{} -> scriptData
486491
Just (TxOutSupplementalDatum _ d) -> addDatum (toAlonzoData d) scriptData
487-
Just (TxOutDatumInline _ d) -> addDatum (toAlonzoData d) scriptData
492+
Just TxOutDatumInline{} -> scriptData
488493
applyTxMod tx utxos (ChangeScriptInput txIn mvalue mdatum mredeemer mrscript) =
489494
(Tx (ShelleyTxBody era body scripts scriptData' auxData validity) wits, utxos')
490495
where

0 commit comments

Comments
 (0)