Skip to content

Commit 7954019

Browse files
committed
fix(testing-interface): detect signing wallet instead of hardcoding w1
1 parent 2c37ed5 commit 7954019

File tree

7 files changed

+195
-39
lines changed

7 files changed

+195
-39
lines changed

src/testing-interface/lib/Convex/TestingInterface.hs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ import Convex.MockChain (MockChainState (..), MockchainT, initialStateFor, runMo
7676
import Convex.MockChain.Defaults qualified as Defaults
7777
import Convex.MonadLog (MonadLog)
7878
import Convex.NodeParams (NodeParams (..))
79-
import Convex.ThreatModel (ExceptT, ThreatModel, ThreatModelOutcome (..), getThreatModelName, runExceptT, runThreatModelCheck, threatModelEnvs)
79+
import Convex.ThreatModel (ExceptT, SigningWallet (AutoSign), ThreatModel, ThreatModelOutcome (..), getThreatModelName, runExceptT, runThreatModelCheck, threatModelEnvs)
8080
import Convex.Wallet.MockWallet qualified as Wallet
8181
import Data.Aeson (ToJSON (..), (.=))
8282
import Data.Aeson qualified as Aeson
@@ -424,9 +424,36 @@ positiveTest opts mGetTmResultsRef tms evs = monadicIO $ do
424424
tmResultsWithCov <- liftIO $ forM allToRun $ \tm -> do
425425
let name = fromMaybe "Unnamed" (getThreatModelName tm)
426426
(outcome, tmFinalState) <-
427-
runMockchainIO (runThreatModelCheck Wallet.w1 tm envs) params state0
427+
runMockchainIO (runThreatModelCheck AutoSign tm envs) params state0
428428
pure (name, outcome, mcsCoverageData tmFinalState)
429429

430+
-- tmResultsWithCov <- case (lastTx, lastUtxoBefore, lastMockChainState) of
431+
-- (Just tx, Just utxo, Just mcState) -> do
432+
-- let pparams' = params ^. ledgerProtocolParameters
433+
-- env = ThreatModelEnv tx utxo pparams'
434+
-- -- Check which threat models have already failed (from previous QuickCheck iterations)
435+
-- existingResults <- case mGetTmResultsRef of
436+
-- Just getTmRef -> liftIO $ do
437+
-- tmRef <- getTmRef
438+
-- readIORef tmRef
439+
-- Nothing -> pure Map.empty
440+
-- let isTMFailed (TMFailed _) = True
441+
-- isTMFailed _ = False
442+
-- alreadyFailed name = any isTMFailed (fromMaybe [] (Map.lookup name existingResults))
443+
-- -- Only filter threat models (tms) for early-stop; expected vulnerabilities (evs) always run
444+
-- tmsToRun = filter (not . alreadyFailed . fromMaybe "Unnamed" . getThreatModelName) tms
445+
-- allToRun = tmsToRun <> evs -- evs always run, no filtering
446+
-- -- Run each threat model in an isolated MockchainT context
447+
-- liftIO $ forM allToRun $ \tm -> do
448+
-- let name = fromMaybe "Unnamed" (getThreatModelName tm)
449+
-- case detectSigningWallet tx of
450+
-- Left err -> pure (name, TMError err, mempty)
451+
-- Right wallet -> do
452+
-- (outcome, tmFinalState) <-
453+
-- runMockchainIO (runThreatModelCheck wallet tm [env]) params mcState
454+
-- pure (name, outcome, mcsCoverageData tmFinalState)
455+
-- _ -> pure []
456+
430457
-- Extract just the (name, outcome) pairs for downstream processing
431458
let tmResults = [(n, o) | (n, o, _) <- tmResultsWithCov]
432459
-- Aggregate coverage from all threat model runs

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

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ module Convex.ThreatModel (
112112
monitorThreatModel,
113113
monitorLocalThreatModel,
114114

115+
-- * Wallet selection
116+
SigningWallet (..),
117+
115118
-- * Cardano API helpers
116119
-- $cardanoHelpers
117120
projectAda,
@@ -153,7 +156,7 @@ import Convex.Class (MockChainState, MonadMockchain (..), coverageData, getUtxo,
153156
import Convex.MockChain (applyTransaction, runMockchain)
154157
import Convex.NodeParams (NodeParams, ledgerProtocolParameters)
155158
import Convex.ThreatModel.Cardano.Api
156-
import Convex.ThreatModel.Cardano.Api qualified as TM
159+
import Convex.ThreatModel.Cardano.Api qualified as TM (detectSigningWallet, rebalanceAndSign, txRequiredSigners)
157160
import Convex.ThreatModel.Pretty
158161
import Convex.ThreatModel.TxModifier
159162
import Convex.Wallet (Wallet)
@@ -178,6 +181,13 @@ data ThreatModelEnv = ThreatModelEnv
178181
, pparams :: LedgerProtocolParameters Era
179182
}
180183

184+
-- | How to determine the wallet for re-balancing and re-signing modified transactions.
185+
data SigningWallet
186+
= -- | Detect the signing wallet automatically from the transaction's witnesses.
187+
AutoSign
188+
| -- | Use the specified wallet for signing.
189+
SignWith Wallet
190+
181191
-- | Create `ThreatModelEnv`s by reapplying the given transactions in order, starting with the given chain state.
182192
threatModelEnvs :: NodeParams Era -> [Tx Era] -> MockChainState Era -> [ThreatModelEnv]
183193
threatModelEnvs params txs chainState0 = fst $ foldM go chainState0 txs
@@ -336,16 +346,22 @@ then performs full Phase 1 + Phase 2 validation via 'applyTransaction'.
336346
This catches vulnerabilities that would be masked by signature/fee failures
337347
in the simpler Phase 2-only validation.
338348
349+
The wallet parameter controls signing:
350+
- @SignWith wallet@ - use the specified wallet for signing
351+
- @AutoSign@ - detect the signing wallet from the transaction's witnesses
352+
339353
Usage:
340354
@
341355
result <- runMockchain0IOWith utxos params $ do
342356
-- ... run your actions to get a transaction ...
343-
runThreatModelM Wallet.w1 unprotectedScriptOutput [env]
357+
runThreatModelM (SignWith Wallet.w1) unprotectedScriptOutput [env]
358+
-- or auto-detect:
359+
runThreatModelM AutoSign unprotectedScriptOutput [env]
344360
@
345361
-}
346362
runThreatModelM
347363
:: (MonadMockchain Era m, MonadFail m, MonadIO m)
348-
=> Wallet
364+
=> SigningWallet
349365
-> ThreatModel a
350366
-> [ThreatModelEnv]
351367
-> m Property
@@ -359,10 +375,12 @@ output cluttering test results.
359375
360376
The property still succeeds/fails correctly based on shouldValidate/shouldNotValidate
361377
checks, but Monitor/MonitorLocal annotations (counterexampleTM, etc.) are ignored.
378+
379+
The wallet parameter controls signing (see 'runThreatModelM' for details).
362380
-}
363381
runThreatModelMQuiet
364382
:: (MonadMockchain Era m, MonadFail m, MonadIO m)
365-
=> Wallet
383+
=> SigningWallet
366384
-> ThreatModel a
367385
-> [ThreatModelEnv]
368386
-> m Property
@@ -373,14 +391,21 @@ runThreatModelM'
373391
:: (MonadMockchain Era m, MonadFail m, MonadIO m)
374392
=> Bool
375393
-- ^ quiet: suppress counterexample annotations
376-
-> Wallet
394+
-> SigningWallet
377395
-> ThreatModel a
378396
-> [ThreatModelEnv]
379397
-> m Property
380-
runThreatModelM' quiet wallet = go False
398+
runThreatModelM' quiet signingWallet = go False
381399
where
382400
go b _model [] = pure $ b ==> property True
383-
go b model (env : envs) = interpM initialMon model
401+
go b model (env : envs) = do
402+
-- Resolve wallet: use provided or detect from transaction
403+
let resolvedWallet = case signingWallet of
404+
SignWith w -> Right w
405+
AutoSign -> TM.detectSigningWallet (currentTx env)
406+
case resolvedWallet of
407+
Left err -> pure $ counterexample err False
408+
Right wallet -> interpM initialMon wallet model
384409
where
385410
initialMon = if quiet then id else counterexample (show info)
386411

@@ -398,7 +423,7 @@ runThreatModelM' quiet wallet = go False
398423
, ""
399424
]
400425

401-
interpM mon = \case
426+
interpM mon wallet = \case
402427
Validate mods k -> do
403428
let (modifiedTx, modifiedUtxo) = applyTxModifier (currentTx env) (currentUTxOs env) mods
404429
-- Re-balance and re-sign the modified transaction
@@ -408,20 +433,20 @@ runThreatModelM' quiet wallet = go False
408433
(report, covData) <- validateTxM params rebalancedTx modifiedUtxo
409434
-- Accumulate coverage into the running MockChainState
410435
modifyMockChainState $ \s -> ((), s & coverageData %~ (<> covData))
411-
interpM mon (k report)
436+
interpM mon wallet (k report)
412437
Generate gen _shr k -> do
413438
-- Use QuickCheck's generate in IO
414439
a <- liftIO $ QC.generate gen
415-
interpM mon (k a)
440+
interpM mon wallet (k a)
416441
GetCtx k ->
417-
interpM mon (k env)
442+
interpM mon wallet (k env)
418443
Skip -> go b model envs
419-
InPrecondition k -> interpM mon (k False)
444+
InPrecondition k -> interpM mon wallet (k False)
420445
Fail err -> pure $ if quiet then property False else mon $ counterexample err False
421-
Monitor m k -> if quiet then interpM mon k else m <$> interpM mon k
422-
MonitorLocal m k -> if quiet then interpM mon k else interpM (mon . m) k
446+
Monitor m k -> if quiet then interpM mon wallet k else m <$> interpM mon wallet k
447+
MonitorLocal m k -> if quiet then interpM mon wallet k else interpM (mon . m) wallet k
423448
Done{} -> go True model envs
424-
Named _n k -> interpM mon k
449+
Named _n k -> interpM mon wallet k
425450

426451
-- | Extract the name from a threat model, if it was defined with 'Named'.
427452
getThreatModelName :: ThreatModel a -> Maybe String
@@ -434,19 +459,30 @@ getThreatModelName _ = Nothing
434459
Rebalancing failures (e.g., "No change output found") are treated as skipped
435460
because they indicate the transaction modification cannot be applied to this
436461
particular transaction, similar to a precondition failure.
462+
463+
The wallet parameter controls signing:
464+
- @SignWith wallet@ - use the specified wallet for signing
465+
- @AutoSign@ - detect the signing wallet from the transaction's witnesses
437466
-}
438467
runThreatModelCheck
439468
:: (MonadMockchain Era m, MonadFail m, MonadIO m)
440-
=> Wallet
469+
=> SigningWallet
441470
-> ThreatModel a
442471
-> [ThreatModelEnv]
443472
-> m ThreatModelOutcome
444-
runThreatModelCheck wallet = go False
473+
runThreatModelCheck signingWallet = go False
445474
where
446475
go b _model [] = pure $ if b then TMPassed else TMSkipped
447-
go b model (env : envs) = checkInterp model
476+
go b model (env : envs) = do
477+
-- Resolve wallet: use provided or detect from transaction
478+
let resolvedWallet = case signingWallet of
479+
SignWith w -> Right w
480+
AutoSign -> TM.detectSigningWallet (currentTx env)
481+
case resolvedWallet of
482+
Left err -> pure (TMError err) -- Continue to next env would lose the error, so return it
483+
Right wallet -> checkInterp wallet model
448484
where
449-
checkInterp = \case
485+
checkInterp wallet = \case
450486
Validate mods k -> do
451487
let (modifiedTx, modifiedUtxo) = applyTxModifier (currentTx env) (currentUTxOs env) mods
452488
params <- askNodeParams
@@ -458,19 +494,19 @@ runThreatModelCheck wallet = go False
458494
Right rebalancedTx -> do
459495
(report, covData) <- validateTxM params rebalancedTx modifiedUtxo
460496
modifyMockChainState $ \s -> ((), s & coverageData %~ (<> covData))
461-
checkInterp (k report)
497+
checkInterp wallet (k report)
462498
Generate gen _shr k -> do
463499
a <- liftIO $ QC.generate gen
464-
checkInterp (k a)
500+
checkInterp wallet (k a)
465501
GetCtx k ->
466-
checkInterp (k env)
502+
checkInterp wallet (k env)
467503
Skip -> go b model envs
468-
InPrecondition k -> checkInterp (k False)
504+
InPrecondition k -> checkInterp wallet (k False)
469505
Fail err -> pure (TMFailed err)
470-
Monitor _m k -> checkInterp k -- No Property to wrap; drop monitoring
471-
MonitorLocal _m k -> checkInterp k -- No Property to wrap; drop monitoring
506+
Monitor _m k -> checkInterp wallet k -- No Property to wrap; drop monitoring
507+
MonitorLocal _m k -> checkInterp wallet k -- No Property to wrap; drop monitoring
472508
Done{} -> go True model envs
473-
Named _n k -> checkInterp k
509+
Named _n k -> checkInterp wallet k
474510

475511
{- | Check a precondition. If the argument threat model fails, the evaluation of the current
476512
transaction is skipped. If all transactions in an evaluation of `runThreatModel` are skipped

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

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,86 @@
44
{-# LANGUAGE NamedFieldPuns #-}
55
{-# LANGUAGE TypeApplications #-}
66

7-
module Convex.ThreatModel.Cardano.Api where
7+
module Convex.ThreatModel.Cardano.Api (
8+
-- * Types
9+
Era,
10+
LedgerEra,
11+
12+
-- * TxOut accessors
13+
addressOfTxOut,
14+
valueOfTxOut,
15+
datumOfTxOut,
16+
referenceScriptOfTxOut,
17+
18+
-- * Redeemer and script data
19+
redeemerOfTxIn,
20+
recomputeScriptData,
21+
emptyTxBodyScriptData,
22+
addScriptData,
23+
updateRedeemer,
24+
addMintingRedeemer,
25+
recomputeScriptDataForMint,
26+
addDatum,
27+
toMaryAssetName,
28+
29+
-- * Address utilities
30+
paymentCredentialToAddressAny,
31+
scriptAddressAny,
32+
keyAddressAny,
33+
isKeyAddressAny,
34+
35+
-- * Datum/Redeemer conversion
36+
toCtxUTxODatum,
37+
txOutDatum,
38+
toScriptData,
39+
40+
-- * Transaction utilities
41+
dummyTxId,
42+
makeTxOut,
43+
txSigners,
44+
mockWalletHashes,
45+
detectSigningWallet,
46+
txRequiredSigners,
47+
txInputs,
48+
txReferenceInputs,
49+
txOutputs,
50+
51+
-- * Value utilities
52+
leqValue,
53+
projectAda,
54+
55+
-- * Validation
56+
ValidityReport (..),
57+
validateTx,
58+
validateTxM,
59+
buildMockState,
60+
61+
-- * Rebalancing
62+
rebalanceAndSignM,
63+
rebalanceAndSign,
64+
updateExecutionUnits,
65+
updateTxRedeemersWithExUnits,
66+
updateScriptDataExUnits,
67+
recalculateScriptIntegrityHash,
68+
getScriptLanguage,
69+
getTxFeeCoin,
70+
setTxFeeCoin,
71+
setTxOutputsList,
72+
adjustChangeOutputM,
73+
adjustChangeOutput,
74+
replaceAt,
75+
76+
-- * Validity interval
77+
convValidityInterval,
78+
79+
-- * UTxO utilities
80+
restrictUTxO,
81+
82+
-- * Coverage
83+
extractCoverageFromValidationError,
84+
unescapeHaskellString,
85+
extractCoverageAnnotations,
86+
) where
887

988
import Cardano.Api
1089

@@ -48,7 +127,7 @@ import Data.ByteString.Short qualified as SBS
48127
import Data.Either (isRight)
49128
import Data.Foldable (foldrM)
50129
import Data.Map qualified as Map
51-
import Data.Maybe (listToMaybe)
130+
import Data.Maybe (listToMaybe, mapMaybe)
52131
import Data.Maybe.Strict
53132
import Data.SOP.NonEmpty (NonEmpty (NonEmptyOne))
54133
import Data.Sequence.Strict qualified as Seq
@@ -260,6 +339,18 @@ txSigners (Tx _ wits) = [toHash wit | ShelleyKeyWitness _ (WitVKey wit _) <- wit
260339
mockWalletHashes :: [(Hash PaymentKey, Wallet)]
261340
mockWalletHashes = map (\w -> (Wallet.verificationKeyHash w, w)) mockWallets
262341

342+
{- | Detect which mock wallet signed a transaction by examining its witnesses.
343+
Returns an error message if no known mock wallet is found among the signers.
344+
-}
345+
detectSigningWallet :: Tx Era -> Either String Wallet
346+
detectSigningWallet tx =
347+
case txSigners tx of
348+
[] -> Left "Transaction has no signers — cannot determine wallet for threat model"
349+
signers ->
350+
case mapMaybe (\h -> lookup h mockWalletHashes) signers of
351+
(w : _) -> Right w
352+
[] -> Left "Transaction signers do not match any known mock wallet"
353+
263354
-- | Get the required signers from the transaction body (not witnesses).
264355
txRequiredSigners :: Tx Era -> [Hash PaymentKey]
265356
txRequiredSigners (Tx (ShelleyTxBody _ body _ _ _ _) _) =

src/testing-interface/test/AikenKingOfCardanoSpec.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import Convex.TestingInterface (
6767
TestingInterface (..),
6868
propRunActionsWithOptions,
6969
)
70-
import Convex.ThreatModel (ThreatModelEnv (..), runThreatModelM)
70+
import Convex.ThreatModel (SigningWallet (SignWith), ThreatModelEnv (..), runThreatModelM)
7171
import Convex.ThreatModel.Cardano.Api (dummyTxId)
7272
import Convex.ThreatModel.LargeData (largeDataAttackWith)
7373
import Convex.ThreatModel.SelfReferenceInjection (selfReferenceInjection)
@@ -694,7 +694,7 @@ propKingUnprotectedOutput opts = monadicIO $ do
694694
}
695695

696696
-- Run the threat model INSIDE MockchainT with full Phase 1 + Phase 2 validation
697-
lift $ runThreatModelM Wallet.w1 unprotectedScriptOutput [env]
697+
lift $ runThreatModelM (SignWith Wallet.w1) unprotectedScriptOutput [env]
698698

699699
case result of
700700
(Left err, _) -> do

src/testing-interface/test/AikenLendingSpec.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ import Convex.TestingInterface (
7676
TestingInterface (..),
7777
propRunActionsWithOptions,
7878
)
79-
import Convex.ThreatModel (ThreatModelEnv (..), runThreatModelMQuiet)
79+
import Convex.ThreatModel (SigningWallet (SignWith), ThreatModelEnv (..), runThreatModelMQuiet)
8080
import Convex.ThreatModel.Cardano.Api ()
8181
import Convex.ThreatModel.InputDuplication (inputDuplication)
8282
import Convex.ThreatModel.UnprotectedScriptOutput (unprotectedScriptOutput)
@@ -709,7 +709,7 @@ propLendingVulnerableToInputDuplication opts = QC.expectFailure $ monadicIO $ do
709709

710710
-- Run inputDuplication threat model
711711
-- It should find the second loan UTxO and try adding it as another input
712-
lift $ runThreatModelMQuiet Wallet.w3 inputDuplication [env]
712+
lift $ runThreatModelMQuiet (SignWith Wallet.w3) inputDuplication [env]
713713
_ -> fail "Expected at least 2 loan request UTxOs"
714714

715715
case result of

0 commit comments

Comments
 (0)