convex-testing-interface
Safe HaskellSafe-Inferred
LanguageHaskell2010

Convex.ThreatModel

Description

The threat modelling framework allows you to write down and test properties of modifications of valid transactions.

A threat model is represented by a value in the ThreatModel monad, and is evaluated in the context of a single valid transaction and the chain state at the point it validated (a ThreatModelEnv). Transactions and chain states can most easily be obtained using a ContractModelResult from runContractModel, but they can in principle come from anywhere.

As an example, here is a ThreatModel that checks that any interaction with myScript requires theToken to be present:

    tokenThreatModel :: ThreatModel ()
    tokenThreatModel = do
      ensureHasInputAt myScript

      let hasToken out = theToken `leqValue` valueOf out
      i <- anyInputSuchThat  hasToken
      o <- anyOutputSuchThat hasToken

      shouldNotValidate $ changeValueOf i (valueOf i <> negateValue theToken)
                       <> changeValueOf o (valueOf o <> negateValue theToken)

For a more complex example see Test.QuickCheck.ThreatModel.DoubleSatisfaction.

Synopsis

Transaction modifiers

Types

data TxModifier Source #

The type of transaction modifiers. When combined using the monoid instance, individual modifications are applied in left-to-right order.

data Input Source #

A transaction input reference togheter with the corresponding TxOut from the UTxO set.

Constructors

Input 

type Datum = TxOutDatum CtxTx Era Source #

Type synonym for datums. The CtxTx context means that the actual datum value can be present, not just the hash.

type Redeemer = ScriptData Source #

Redeemers are plain ScriptData.

Modifiers

class IsInputOrOutput t where Source #

Functions common to both Inputs and Outputs.

Methods

changeAddressOf :: t -> AddressAny -> TxModifier Source #

Change the target address of an input or an output. For outputs this means redirecting an output to a different address, and for inputs it means modifying the UTxO set, changing the owner of the given input.

Note: Does not work for script inputs.

changeValueOf :: t -> Value -> TxModifier Source #

Change the value of an input or an output.

changeDatumOf :: t -> Datum -> TxModifier Source #

Change the datum on an input or an output.

changeRefScriptOf :: t -> ReferenceScript Era -> TxModifier Source #

Change the reference script on an input or an output

addressOf :: t -> AddressAny Source #

Get the address (pubkey or script address) of an input or an output.

valueOf :: t -> Value Source #

Get the value at an input or an output.

refScriptOf :: t -> ReferenceScript Era Source #

Get the reference script at an input or an output.

addOutput :: AddressAny -> Value -> Datum -> ReferenceScript Era -> TxModifier Source #

Add a new output of any type (public key or script)

removeOutput :: Output -> TxModifier Source #

Remove an output of any type.

addKeyInput :: AddressAny -> Value -> Datum -> ReferenceScript Era -> TxModifier Source #

Add a new public key input.

addPlutusScriptInput :: IsPlutusScriptInEra lang => PlutusScript lang -> Value -> Datum -> Redeemer -> ReferenceScript Era -> TxModifier Source #

Add a plutus V2 script input.

addReferenceScriptInput :: ScriptHash -> Value -> Datum -> Redeemer -> TxModifier Source #

Add a reference script input

addKeyReferenceInput :: AddressAny -> Value -> Datum -> ReferenceScript Era -> TxModifier Source #

Add a new public key reference input.

addPlutusScriptReferenceInput :: IsPlutusScriptInEra lang => PlutusScript lang -> Value -> Datum -> ReferenceScript Era -> TxModifier Source #

Add a plutus script reference input.

addSimpleScriptReferenceInput :: SimpleScript -> Value -> ReferenceScript Era -> TxModifier Source #

Add a simple script reference input.

removeInput :: Input -> TxModifier Source #

Remove an input of any type.

changeRedeemerOf :: Input -> Redeemer -> TxModifier Source #

Change the redeemer of a script input.

changeValidityRange :: (TxValidityLowerBound Era, TxValidityUpperBound Era) -> TxModifier Source #

Change the validity range of the transaction.

changeValidityLowerBound :: TxValidityLowerBound Era -> TxModifier Source #

Change the validity lower bound of the transaction.

changeValidityUpperBound :: TxValidityUpperBound Era -> TxModifier Source #

Change the validity upper bound of the transaction.

replaceTx :: Tx Era -> UTxO Era -> TxModifier Source #

The most general transaction modifier. Simply replace the original transaction and UTxO set by the given values. In most cases the modifiers above should be sufficient.

Threat models

data ThreatModel a where Source #

The threat model monad is how you construct threat models. It works in the context of a given transaction and the UTxO set at the point where the transaction was validated (see ThreatModelEnv) and lets you construct properties about the validatity of modifications of the original transaction.

Constructors

Named 

Fields

Instances

Instances details
MonadFail ThreatModel Source # 
Instance details

Defined in Convex.ThreatModel

Methods

fail :: String -> ThreatModel a #

Applicative ThreatModel Source # 
Instance details

Defined in Convex.ThreatModel

Methods

pure :: a -> ThreatModel a #

(<*>) :: ThreatModel (a -> b) -> ThreatModel a -> ThreatModel b #

liftA2 :: (a -> b -> c) -> ThreatModel a -> ThreatModel b -> ThreatModel c #

(*>) :: ThreatModel a -> ThreatModel b -> ThreatModel b #

(<*) :: ThreatModel a -> ThreatModel b -> ThreatModel a #

Functor ThreatModel Source # 
Instance details

Defined in Convex.ThreatModel

Methods

fmap :: (a -> b) -> ThreatModel a -> ThreatModel b #

(<$) :: a -> ThreatModel b -> ThreatModel a #

Monad ThreatModel Source # 
Instance details

Defined in Convex.ThreatModel

Methods

(>>=) :: ThreatModel a -> (a -> ThreatModel b) -> ThreatModel b #

(>>) :: ThreatModel a -> ThreatModel b -> ThreatModel b #

return :: a -> ThreatModel a #

data ThreatModelEnv Source #

The context in which a ThreatModel is executed: a transaction and the chain state it validated against. See getThreatModelEnv and originalTx to access this information in a threat model; the UTxO set and protocol parameters are derived from the chain state via currentUTxOs and pparams.

mkThreatModelEnv :: Tx Era -> MockChainState Era -> ThreatModelEnv Source #

Construct a ThreatModelEnv from a transaction and the chain state it validated against (captured *before* the transaction was submitted - a state captured afterwards no longer resolves the transaction's inputs).

pparams :: ThreatModelEnv -> LedgerProtocolParameters Era Source #

The protocol parameters, derived from currentChainState.

data ThreatModelOutcome Source #

Structured outcome of running a threat model against a transaction.

Constructors

TMPassed

At least one transaction was tested, all checks passed

TMFailed String

A check failed, with error details

TMSkipped

Preconditions were never met (all transactions skipped)

TMSkippedPhase1

No transaction ran to completion, and at least one was skipped for an environmental reason rather than an unmet precondition: the modified transaction was Phase 1 invalid, or rebalancing failed (the modification couldn't be realized as a well-formed transaction, see rebalanceAndSign). Other transactions may additionally have been skipped because of unmet preconditions. Distinguished from TMSkipped so an all-skipped run of an explicitly listed model only counts as vacuous - and fails the suite - when every skip was a precondition miss.

TMError String

Threat model crashed with an exception

threatModelEnvs :: NodeParams Era -> [Tx Era] -> MockChainState Era -> [ThreatModelEnv] Source #

Create ThreatModelEnvs by reapplying the given transactions in order, starting with the given chain state.

runThreatModelM :: (MonadMockchain Era m, MonadIO m) => SigningWallet -> ThreatModel a -> [ThreatModelEnv] -> m Property Source #

Run threat model inside MockchainT with full Phase 1 + Phase 2 validation.

Unlike runThreatModel which only validates Phase 2 (script execution), this version uses re-balancing and re-signing for modified transactions, then performs full Phase 1 + Phase 2 validation via applyTransaction.

This catches vulnerabilities that would be masked by signature/fee failures in the simpler Phase 2-only validation.

The wallet parameter controls signing: - SignWith wallet - use the specified wallet for signing - AutoSign - detect the signing wallet from the transaction's witnesses

Usage: result <- runMockchain0IOWith utxos params $ do -- ... run your actions to get a transaction ... runThreatModelM (SignWith Wallet.w1) unprotectedScriptOutput [env] -- or auto-detect: runThreatModelM AutoSign unprotectedScriptOutput [env]

runThreatModelMQuiet :: (MonadMockchain Era m, MonadIO m) => SigningWallet -> ThreatModel a -> [ThreatModelEnv] -> m Property Source #

Like runThreatModelM but suppresses verbose counterexample annotations.

This is useful for expectFailure tests where you want the test to fail (proving vulnerability exists) but don't want the lengthy counterexample output cluttering test results.

The property still succeedsfails correctly based on shouldValidateshouldNotValidate checks, but Monitor/MonitorLocal annotations (counterexampleTM, etc.) are ignored.

The wallet parameter controls signing (see runThreatModelM for details).

runThreatModelCheck :: (MonadMockchain Era m, MonadFail m, MonadIO m) => SigningWallet -> ThreatModel a -> [ThreatModelEnv] -> m (ThreatModelOutcome, Property -> Property) Source #

Run a threat model and return a structured outcome instead of a Property. Coverage is still accumulated in MockChainState as a side effect.

Rebalancing failures (e.g., "No change output found") are treated as skipped because they indicate the transaction modification cannot be applied to this particular transaction, similar to a precondition failure.

The wallet parameter controls signing: - SignWith wallet - use the specified wallet for signing - AutoSign - detect the signing wallet from the transaction's witnesses

runThreatModelCheckTraced :: (MonadMockchain Era m, MonadFail m, MonadIO m) => SigningWallet -> ThreatModel a -> [ThreatModelEnv] -> m (ThreatModelOutcome, [ThreatModelCheckEntry], Property -> Property) Source #

Like runThreatModelCheck but additionally accumulates trace data. Each Validate call in the threat model produces a ThreatModelCheckEntry. Coverage is still accumulated in MockChainState as a side effect.

data ThreatModelCheckEntry Source #

A single trace entry from a threat model check against one ThreatModelEnv.

Constructors

ThreatModelCheckEntry 

Fields

finalOutcome :: Bool -> Bool -> Maybe String -> ThreatModelOutcome Source #

The outcome of a whole model run, most informative first: a pass beats everything else, an environmental skip at least proves the precondition held, and an error only speaks when nothing else happened. Mirrors zeroCoverageKind, which ranks the same three when deciding whether zero coverage should fail a test case - so TMError there can be trusted to mean "no attack was ever carried out".

assertThreatModel :: ThreatModel a -> [(Tx Era, MockChainState Era)] -> Property Source #

Evaluate a ThreatModel on a list of transactions, each paired with the chain state it validated against.

getThreatModelName :: ThreatModel a -> Maybe String Source #

Extract the name from a threat model, if it was defined with Named.

Preconditions

threatPrecondition :: ThreatModel a -> ThreatModel a Source #

Check a precondition. If the argument threat model fails, the evaluation of the current transaction is skipped. If all transactions in an evaluation of runThreatModel are skipped it is considered a discarded test for QuickCheck.

Having the argument to threatPrecondition be a threat model computation instead of a plain boolean allows you do express preconditions talking about the validation of modified transactions (using shouldValidate and shouldNotValidate). See ensure for the boolean version.

inPrecondition :: ThreatModel Bool Source #

Returns True if evaluated under a threatPrecondition and False otherwise.

ensure :: Bool -> ThreatModel () Source #

Same as threatPrecondition but takes a boolean and skips the test if the argument is False.

ensureHasInputAt :: AddressAny -> ThreatModel () Source #

Precondition that check that the original transaction has an input at a given address. Useful, for example, to ensure that you only consider transactions that trie to spend a script output from the script under test.

requireScriptExecution :: ThreatModel () Source #

Precondition that requires the original transaction to run at least one Plutus script, of any purpose: a spending validator, a minting policy, a withdraw-zero staking validator, ... Attacks that mutate a script output and then check whether the transaction still validates need this: on a transaction that runs no script at all (e.g. a setup transaction that just pays a key input into a script for the first time), no validator ever gets a chance to reject the mutation, so "the mutated transaction still validates" is vacuous and proves nothing about the contract under test.

The check is based on the transaction's redeemer set, not on its input addresses, because the script that guards a contract's outputs is not always a spending validator - the withdraw-zero pattern, for instance, validates the whole transaction from a Rewarding script while every input is key-owned. Conversely, an input at a native-script address runs no Plutus code and does not count.

Not every attack needs this - one that tests a minting policy in isolation (e.g. Convex.ThreatModel.TokenForgery) is still meaningful on a transaction that spends no script - so this is opt-in per attack rather than applied globally to every ThreatModelEnv.

guardedScriptOutputs :: ThreatModel [Output] Source #

The transaction's outputs whose payment credential is the hash of a Plutus script that runs in this transaction.

Stronger than requireScriptExecution, and what the output-mutation attacks actually want: degrading an output is only evidence against a contract if a script plausibly responsible for it was executing and failed to object. Under the transaction-wide check, a transaction whose only script is an unrelated minting policy, paying into a validator for the first time, let those attacks degrade the new output and report a finding.

Hash identity is a proxy, not proof, and it cuts both ways. A script running under any purpose counts, so the withdraw-zero pattern's Rewarding script guards outputs at its own payment address - which is the point, and restricting this to Spending purposes would break that pattern, as there are no script inputs there at all. By the same token a multi-validator whose minting policy id equals its spending hash counts as guarding its own outputs. Conversely an output policed by some other running script is not matched and those attacks skip it, which is a known and accepted gap.

Empty when no Plutus script runs at all, which is what lets anyGuardedOutputSuchThat subsume requireScriptExecution.

anyGuardedOutputSuchThat :: (Output -> Bool) -> ThreatModel Output Source #

Pick any output that guardedScriptOutputs admits and that satisfies the predicate, recording a precondition miss when there is none.

The output-mutation attacks all want exactly this, so the emptiness convention lives here rather than being restated at each call site. Since guardedScriptOutputs is empty whenever no Plutus script runs, this subsumes requireScriptExecution and no separate call is needed. Attacks that corrupt a redeemer of any running script (e.g. Convex.ThreatModel.InvalidScriptPurpose, which targets a key output) still want the weaker, transaction-wide check.

anyGuardedOutputWithInlineDatum :: ThreatModel (Output, ScriptData) Source #

Pick any output that guardedScriptOutputs admits and that carries an inline datum, returning the output together with that datum.

Every datum-mutating attack starts here, so the predicate and the extraction are one operation: splitting them left each call site with a Nothing branch the predicate had already ruled out.

hasInlineDatum :: Output -> Bool Source #

Does this output carry an inline datum?

getInlineDatum :: Output -> Maybe ScriptData Source #

The output's inline datum, if it has one.

toInlineDatum :: ScriptData -> Datum Source #

Wrap a ScriptData as an inline datum, for use with changeDatumOf.

Validation

shouldValidate :: TxModifier -> ThreatModel () Source #

Check that a given modification of the original transaction validates. The modified transaction is printed in counterexample when this fails, or if it succeeds in a precondition and the test fails later.

shouldNotValidate :: TxModifier -> ThreatModel () Source #

Check that a given modification of the original transaction does not validate. The modified transaction is printed in counterexample when it does validate, or if it doesn't in a satisfied precondition and the test fails later.

data TxValidity Source #

The outcome of validating a transaction, mirroring the ledger's two-phase validation: Phase 2 (script execution) only runs when Phase 1 (structural ledger rules) passes, so no other combinations exist.

Constructors

Valid

Phase 1 and Phase 2 both passed

Phase1Invalid

Rejected by Phase 1 ledger rules; scripts were never executed

Phase2Invalid

Phase 1 passed, but a script rejected the transaction

validate :: TxModifier -> ThreatModel ValidityReport Source #

The most low-level way to validate a modified transaction. In most cases shouldValidate and shouldNotValidate are preferred.

Querying the environment

originalTx :: ThreatModel (Tx Era) Source #

Get the original transaction from the context.

getTxInputs :: ThreatModel [Input] Source #

Get the inputs from the original transaction.

getTxReferenceInputs :: ThreatModel [Input] Source #

Get the reference inputs from the original transaction.

getTxOutputs :: ThreatModel [Output] Source #

Get the outputs from the original transaction.

getRedeemer :: Input -> ThreatModel (Maybe Redeemer) Source #

Get the redeemer (if any) for an input of the original transaction.

getTxRequiredSigners :: ThreatModel [Hash PaymentKey] Source #

Get the required signers from the original transaction body.

Random generation

forAllTM :: Show a => Gen a -> (a -> [a]) -> ThreatModel a Source #

Generate a random value. Takes a QuickCheck generator and a shrink function.

shrinkPositive :: Int -> [Int] Source #

Shrink towards 1, never below it: the counts these attacks generate are sizes, and a size of zero is not a smaller failing case but a different (no-op) one.

pickAny :: Show a => [a] -> ThreatModel a Source #

Pick a random value from a list. Skips the test if the list is empty.

anySigner :: ThreatModel (Hash PaymentKey) Source #

Pick a random signer of the original transaction.

anyInput :: ThreatModel Input Source #

Pick a random input

anyReferenceInput :: ThreatModel Input Source #

Pick a random reference input

anyOutput :: ThreatModel Output Source #

Pick a random output

anyInputSuchThat :: (Input -> Bool) -> ThreatModel Input Source #

Pick a random input satisfying the given predicate.

anyReferenceInputSuchThat :: (Input -> Bool) -> ThreatModel Input Source #

Pick a random reference input satisfying the given predicate.

anyOutputSuchThat :: (Output -> Bool) -> ThreatModel Output Source #

Pick a random output satisfying the given predicate.

Monitoring

counterexampleTM :: String -> ThreatModel () Source #

Print the given string in case this threat model fails. Threat model counterpart of the QuickCheck counterexample function.

tabulateTM :: String -> [String] -> ThreatModel () Source #

Threat model counterpart of QuickCheck's tabulate function.

collectTM :: Show a => a -> ThreatModel () Source #

Threat model counterpart of QuickCheck's collect function.

classifyTM :: Bool -> String -> ThreatModel () Source #

Threat model counterpart of QuickCheck's classify function.

monitorThreatModel :: (Property -> Property) -> ThreatModel () Source #

Monitoring that's shared between all transactions evaluated. Avoid this in favour of tabulateTM, collectTM and classifyTM when possible.

monitorLocalThreatModel :: (Property -> Property) -> ThreatModel () Source #

Monitoring that's local to the current transaction. Use counterexampleTM when possible.

Wallet selection

data SigningWallet Source #

How to determine the wallet for re-balancing and re-signing modified transactions.

Constructors

AutoSign

Detect the signing wallet automatically from the transaction's witnesses.

SignWith Wallet

Use the specified wallet for signing.

Cardano API helpers

Some convenience functions making it easier to work with Cardano API.

projectAda :: Value -> Value Source #

Keep only the Ada part of a value.

leqValue :: Value -> Value -> Bool Source #

Check if a value is less or equal than another value.

Addresses

keyAddressAny :: Hash PaymentKey -> AddressAny Source #

Construct a public key address.

scriptAddressAny :: ScriptHash -> AddressAny Source #

Construct a script address.

isKeyAddressAny :: AddressAny -> Bool Source #

Check if an address is a public key address — i.e. has no script payment credential. Byron addresses count as key addresses, as they have no script credentials at all.

Datums

txOutDatum :: ScriptData -> TxOutDatum CtxTx Era Source #

Convert ScriptData to a Datum.

toScriptData :: ToData a => a -> ScriptData Source #

Convert a Haskell value to ScriptData for use as a Redeemer or convert to a Datum with txOutDatum.

datumOfTxOut :: TxOut ctx Era -> TxOutDatum ctx Era Source #

Get the datum from a transaction output.

Pretty printing

The framework already prints the original transaction and the results of validating modified transactions in counterexamples. To include more information you can use counterexampleTM with the functions below.

paragraph :: [String] -> String Source #

Format a list of strings as a paragraph. The structure of the list is not considered other than inserting whitespace between consecutive elements. Use with counterexampleTM when printing longer texts.