tests
Safe HaskellSafe-Inferred
LanguageGHC2021

Hydra.Chain.Direct.Contract.Mutation

Description

Provides building blocks for Mutation testing of Contracts.

Introduction

Traditional Mutation testing is a testing technique that introduces small modifications like changing a comparison operator, or modifying constants, into a program and checks whether or not the existing tests "kill" the produced mutants, eg. fail. Mutation testing requires somewhat complex tooling because it needs to modify the source code, in limited and semantically meaningful ways in order to generate code that won't be rejected by the compiler.

Recall that Plutus eUTxO validators are boolean expressions of the form:

validator : Datum -> Redeemer -> ScriptContext -> Bool

All things being equal, "mutating" a validator so that it returns False instead of True can be done:

  • Either by mutating the code of the validator implementation,
  • Or by mutating its arguments.

This simple idea lead to the following strategy to test-drive validator scripts:

  1. Start with a validator that always return True,
  2. Write a positive property test checking valid transactions are accepted by the validator(s),
  3. Write a negative property test checking invalid transactions are rejected. This is where mutations are introduced, each different mutation type representing some possible "attack",
  4. Watch one or the other properties fail and enhance the validators code to make them pass,
  5. Rinse and repeat.

Generic Property and Mutations

Given a transaction with some UTxO context, and a function that generates SomeMutation from a valid transaction and context pair, the generic propMutation property checks applying any generated mutation makes the mutated (hence expectedly invalid) transaction fail the validation stage.

propMutation :: (Tx, Utxo) -> ((Tx, Utxo) -> Gen SomeMutation) -> Property
propMutation (tx, utxo) genMutation =
  forAll _ Property (genMutation (tx, utxo)) $ SomeMutation{label, mutation} ->
    (tx, utxo)
      & applyMutation mutation
      & propTransactionFailsPhase2

To this basic property definition we add a checkCoverage that ensures the set of generated mutations covers a statistically significant share of each of the various possible mutations classified by their label.

The SomeMutation type is simply a wrapper that attaches a label to a proper Mutation which is the interesting bit here.

The Mutation type enumerates various possible "atomic" mutations which preserve the structural correctness of the transaction but should make a validator fail.

data Mutation
  = ChangeHeadRedeemer Head.Input
  | ChangeInputHeadDatum Head.State
  ...
  | Changes [Mutation]

The constructors should hopefully be self-explaining but for the last one. Some interesting mutations we want to make require more than one "atomic" change to represent a possible validator failure. For example, we wanted to check that the Commit validator, in the context of a CollectCom transaction, verifies the state (Input) of the Head validator is correct. But to be interesting, this mutation needs to ensure the transition verified by the Head state machine is valid, which requires changing both the datum and the redeemer of the consumed head output.

Transaction-specific Mutations

To be run the propMutation requires a starting "healthy" (valid) transaction and a specialised generating function. It is instantiated in the test runner by providing these two elements. For example, the ContractSpec module has the following property check:

describe CollectCom $ do
  prop "does not survive random adversarial mutations" $
    propMutation healthyCollectComTx genCollectComMutation

The interesting part is the genCollectComMutation (details of the Mutation generators are omitted):

genCollectComMutation :: (Tx, Utxo) -> Gen SomeMutation
genCollectComMutation (tx, utxo) =
  oneof
    [ SomeMutation Nothing MutateOpenOutputValue . ChangeOutput ...
    , SomeMutation Nothing MutateOpenUtxoHash . ChangeOutput ...
    , SomeMutation Nothing MutateHeadTransition $ do
        changeRedeemer ChangeHeadRedeemer <$ ...
        changeDatum ChangeInputHeadDatum <$ ...
        pure $ Changes [changeRedeemer, changeDatum]
    ]

Here we have defined four different type of mutations that are interesting for the CollectCom transaction and represent possible attack vectors:

  • Changing the Head output's value, which would imply some of the committed funds could be "stolen" by the party posting the transaction,
  • Tampering with the content of the UTxO committed to the Head,
  • Trying to collect commits without running the Head validator,
  • Trying to collect commits in another Head state machine transition.

Running Properties

When such a property test succeeds we get the following report which shows the distribution of the various mutations that were tested.

Hydra.Chain.Direct.Contract
  CollectCom
    does not survive random adversarial mutations
      +++ OK, passed 200 tests.

      CollectComMutation (100 in total):
      23% MutateNumberOfParties
      22% MutateHeadTransition
      21% MutateHeadId
      19% MutateOpenUTxOHash
      15% MutateRequiredSigner

Finished in 18.1146 seconds

In the case of a failure we get a detailed report on the context of the failure.

Synopsis

Properties

propMutation :: (Tx, UTxO) -> ((Tx, UTxO) -> Gen SomeMutation) -> Property Source #

A Property checking a mutation is not validated. This property takes an initial (transaction, UTxO) pair that is supposedly valid, passes it to a generator that produces some mutations, then assert the resulting (transaction', UTxO') pair fails the validation process.

Note that only "level 2" validation is run, e.g the transaction is assume to be structurally valid and having passed "level 1" checks.

propTransactionFailsPhase2 :: Maybe Text -> (Tx, UTxO) -> Property Source #

Expect a phase-2 evaluation failure of given Tx and UTxO'.

propTransactionEvaluates :: (Tx, UTxO) -> Property Source #

Expect a given Tx and UTxO' to pass evaluation.

propTransactionFailsEvaluation :: (Tx, UTxO) -> Property Source #

Expect a given Tx and UTxO' to fail phase 1 or phase 2 evaluation.

Mutations

data SomeMutation Source #

Existential wrapper SomeMutation and some label type. This type is useful to provide a "generic" classification of mutation that is controlled by some custom type. The $sel:label:SomeMutation field can be passed to the genericCoverTable function to construct and display a coverage table showing the percentage of each mutation that's been applied and ensure significant coverage of all possible mutations using checkCoverage.

Constructors

forall lbl.(Typeable lbl, Enum lbl, Bounded lbl, Show lbl) => SomeMutation 

data Mutation Source #

Basic mutations

Constructors

ChangeHeadRedeemer Input

Changes the Head script's redeemer to the given value.

ChangeInputHeadDatum State

Changes the spent Head script datum to the given value. This assumes inline datums are used and replaces the datum in all matching UTxOs (there should only be one head txOut).

PrependOutput (TxOut CtxTx)

Adds given output as first transaction output.

AppendOutput (TxOut CtxTx)

Adds given output as last transaction output.

RemoveOutput Word

Removes given output from the transaction's outputs.

RemoveInput TxIn

Drops the given input from the transaction's inputs

AddInput TxIn (TxOut CtxUTxO) (Maybe HashableScriptData)

Adds given UTxO to the transaction's inputs and UTxO context.

AddScript PlutusScript

Add witness for the given script to the transaction.

ChangeInput TxIn (TxOut CtxUTxO) (Maybe HashableScriptData)

Change an input's TxOut to something else. This mutation alters the redeemers of the transaction to ensure any matching redeemer for given input matches the new redeemer, otherwise the transaction would be invalid for the wrong reason (unused redeemer).

This expects Nothing if the new input is not locked by any script, and it expects Just with some potentially new redeemer if locked by a script.

XXX: This is super tricky to use. If passing Nothing although it's a script tx out, the validator will actually not be run!

XXX: Likely incomplete as it can not add the datum for given txout.

ChangeOutput Word (TxOut CtxTx)

Change the transaction's output at given index to something else.

ChangeMintedValue Value

Change the transaction's minted values if it is actually minting something. NOTE: If Value is mempty the redeemers will be wrong.

ChangeRequiredSigners [Hash PaymentKey]

Change required signers on a transaction'

ChangeValidityInterval (TxValidityLowerBound, TxValidityUpperBound)

Change the validity interval of the transaction.

ChangeValidityLowerBound TxValidityLowerBound 
ChangeValidityUpperBound TxValidityUpperBound 
ChangeMintingPolicy PlutusScript

Change the included minting policy (the first minted policy) and update minted/burned values of this policy.

Changes [Mutation]

Applies several mutations as a single atomic Mutation. This is useful to enable specific mutations that require consistent change of more than one thing in the transaction and/or UTxO set, for example to change consistently the Head script's redeemer and datum.

Instances

Instances details
Generic Mutation Source # 
Instance details

Defined in Hydra.Chain.Direct.Contract.Mutation

Associated Types

type Rep Mutation :: Type -> Type Source #

Show Mutation Source # 
Instance details

Defined in Hydra.Chain.Direct.Contract.Mutation

type Rep Mutation Source # 
Instance details

Defined in Hydra.Chain.Direct.Contract.Mutation

type Rep Mutation = D1 ('MetaData "Mutation" "Hydra.Chain.Direct.Contract.Mutation" "main" 'False) ((((C1 ('MetaCons "ChangeHeadRedeemer" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Input)) :+: C1 ('MetaCons "ChangeInputHeadDatum" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 State))) :+: (C1 ('MetaCons "PrependOutput" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (TxOut CtxTx))) :+: C1 ('MetaCons "AppendOutput" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (TxOut CtxTx))))) :+: ((C1 ('MetaCons "RemoveOutput" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Word)) :+: C1 ('MetaCons "RemoveInput" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 TxIn))) :+: (C1 ('MetaCons "AddInput" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 TxIn) :*: (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (TxOut CtxUTxO)) :*: S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (Maybe HashableScriptData)))) :+: C1 ('MetaCons "AddScript" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 PlutusScript))))) :+: (((C1 ('MetaCons "ChangeInput" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 TxIn) :*: (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (TxOut CtxUTxO)) :*: S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (Maybe HashableScriptData)))) :+: C1 ('MetaCons "ChangeOutput" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Word) :*: S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (TxOut CtxTx)))) :+: (C1 ('MetaCons "ChangeMintedValue" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Value)) :+: C1 ('MetaCons "ChangeRequiredSigners" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 [Hash PaymentKey])))) :+: ((C1 ('MetaCons "ChangeValidityInterval" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (TxValidityLowerBound, TxValidityUpperBound))) :+: C1 ('MetaCons "ChangeValidityLowerBound" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 TxValidityLowerBound))) :+: (C1 ('MetaCons "ChangeValidityUpperBound" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 TxValidityUpperBound)) :+: (C1 ('MetaCons "ChangeMintingPolicy" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 PlutusScript)) :+: C1 ('MetaCons "Changes" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 [Mutation])))))))

applyMutation :: Mutation -> (Tx, UTxO) -> (Tx, UTxO) Source #

Apply a single Mutation to the given (transaction, UTxO) pair. ''NOTE'': This function is partial, it can raise error when some preconditions are not met by the transaction or UTxO set, for example if there's no Head script input or no datums in the transaction.

Orphans

Helpers

isHeadOutput :: TxOut CtxUTxO -> Bool Source #

Identify Head script's output.

addDatum :: TxOutDatum CtxTx -> TxBodyScriptData -> TxBodyScriptData Source #

Adds given Datum and corresponding hash to the transaction's scripts. TODO: As we are creating the TxOutDatum from a known datum, passing a TxOutDatum is pointless and requires more work than needed to check impossible variants.

modifyInlineDatum :: (FromScriptData a, ToScriptData a) => (a -> a) -> TxOut CtxTx -> TxOut CtxTx Source #

addParticipationTokens :: [VerificationKey PaymentKey] -> TxOut CtxUTxO -> TxOut CtxUTxO Source #

ensureDatums :: [TxOut CtxTx] -> TxBodyScriptData -> TxBodyScriptData Source #

Ensures the included datums of given TxOuts are included in the transactions' TxBodyScriptData.

alterRedeemers :: (PlutusPurpose AsIndex LedgerEra -> (Data LedgerEra, ExUnits) -> (Data LedgerEra, ExUnits)) -> TxBodyScriptData -> TxBodyScriptData Source #

Alter a transaction's redeemers map given some mapping function.

alterTxIns :: ([(TxIn, Maybe HashableScriptData)] -> [(TxIn, Maybe HashableScriptData)]) -> Tx -> Tx Source #

Alter the tx inputs in such way that redeemer pointer stay consistent. A value of Nothing for the redeemr means that this is not a script input. NOTE: This will reset all the execution budgets to 0.

alterTxOuts :: ([TxOut CtxTx] -> [TxOut CtxTx]) -> Tx -> Tx Source #

Apply some mapping function over a transaction's outputs.

anyPayToPubKeyTxOut :: Gen (TxOut ctx) Source #

Generates an output that pays to some arbitrary pubkey.

headTxIn :: UTxO -> TxIn Source #

Finds the Head script's input in given UTxO' set. ''NOTE'': This function is partial, it assumes the UTxO' set contains a Head script output.

changeMintedValueQuantityFrom :: Tx -> Integer -> Gen Mutation Source #

A Mutation that changes the minted/burnt quantity of all tokens to a non-zero value different than the given one.

changeMintedTokens :: Tx -> Value -> Gen Mutation Source #

A Mutation that changes the minted/burned quantity of tokens like this: - when no value is being minted/burned -> add a value - when tx is minting or burning values -> add more values on top of that

addPTWithQuantity :: Tx -> Quantity -> Gen Mutation Source #

A Mutation that adds an Arbitrary participation token with some quantity. As usual the quantity can be positive for minting, or negative for burning.

replacePolicyIdWith :: PolicyId -> PolicyId -> TxOut a -> TxOut a Source #

Replace first given PolicyId with the second argument in the whole TxOut value.

replacePolicyInValue :: PolicyId -> PolicyId -> Value -> Value Source #

Replace first given PolicyId with the second argument in the whole Value.

replaceSnapshotNumber :: SnapshotNumber -> State -> State Source #

replaceParties :: [Party] -> State -> State Source #

replaceUtxoHash :: Hash -> State -> State Source #

replaceContestationDeadline :: POSIXTime -> State -> State Source #

replaceContestationPeriod :: ContestationPeriod -> State -> State Source #

replaceHeadId :: CurrencySymbol -> State -> State Source #

replaceContesters :: [PubKeyHash] -> State -> State Source #

removePTFromMintedValue :: TxOut CtxUTxO -> Tx -> Value Source #

Orphan instances

Arbitrary Input Source # 
Instance details

Methods

arbitrary :: Gen Input

shrink :: Input -> [Input]

Arbitrary State Source # 
Instance details

Methods

arbitrary :: Gen State

shrink :: State -> [State]

Eq Input Source # 
Instance details

Methods

(==) :: Input -> Input -> Bool Source #

(/=) :: Input -> Input -> Bool Source #