Exploring Script Context

Introduction to Script Context

We will now take a closer look at the third argument to Plutus validator functions which is the script context. As mentioned before, the script context contains the entire transaction being validated including all its inputs and outputs, fees, and certificates.

You may hear script context being called transaction context. Don't be confused as they mean the same thing.

In Plutus, this script context corresponds to the ScriptContext type. The best way to explore the structure of the ScriptContext type is through Haddock documentation.

We can see that it consists of two fields:

  1. scriptContextTxInfo :: TxInfo

  2. scriptContextPurpose :: ScriptPurpose

The scriptContextTxInfo field is a type of TxInfo for which we can find further information in Haddock:

txInfoInputs :: [TxInInfo]	                 -- Transaction inputs

txInfoReferenceInputs :: [TxInInfo]              -- Transaction reference inputs

txInfoOutputs :: [TxOut]	                 -- Transaction outputs

txInfoFee :: Value	                         -- The fee paid by this transaction.

txInfoMint :: Value                              -- The Value minted by this transaction.

txInfoDCert :: [DCert]	                         -- Digests of certificates included in this transaction

txInfoWdrl :: Map StakingCredential Integer	 -- Withdrawals

txInfoValidRange :: POSIXTimeRange	         -- The valid range for the transaction.

txInfoSignatories :: [PubKeyHash]	         -- Signatures provided with the transaction, attested that they all signed the tx

txInfoRedeemers :: Map ScriptPurpose             -- Redeemer

txInfoData :: Map DatumHash                      -- Datum
 
txInfoId :: TxId	                         -- Hash of the pending transaction (excluding witnesses)

In other words, it contains everything that we attached to the transaction when we built it.

The other field scriptContextPurpose is of type ScriptPurpose which can be one of:

Minting    CurrencySymbol
Spending   TxOutRef
Rewarding  StakingCredential
Certifying DCert

A ScriptContext Exploration Script

Now that we have a theoretical overview of the script context, let's write a simple script utilising it in practice. It will be just a simple example with the sole purpose of exploring the use of script context inside the validator. We will create a script that validates only if the transaction attempts to create less than or exactly three outputs and if its valid time range is infinite (this is the default range applied if no valid range is specified during the transaction building stage).

A note on Cardano transaction validity ranges

Cardano transactions contain a txInfoValidRange, which defines the range of slots between which the transaction is valid. There are two layers of checking the valid range for a transaction. The first one happens when a cardano-node receives the transaction. The first thing the node does when considering a transaction is check its valid range - if the current slot does not fall into the valid range of the transaction, it is immediately discarded without doing anything else (including running a possible validator script in the transaction). The second layer is optional and can be specified in the validator script itself. This is done by accessing the txInfoValidRange from the ScriptContext and performing arbitrary checks against it.

Writing the validator

Create a new file src/ExploringScriptContext.hs for this validator. Our imports stay the same as in the previous scripts so copy those in. Firstly, we need to change the way we look at arguments in the mkValidator function. We are not interested in the datum and redeemer fields so we can ignore them now, and instead, look only at the context field:

mkValidator :: BuiltinData -> BuiltinData -> BuiltinData -> ()
mkValidator _ _ ctx = ...

We want to check two things in the validation as mentioned above, so we will create helper functions that check each one individually, checkOutputs and checkRange. The main function will check that both of these helper functions evaluate to True:

mkValidator _ _ ctx =
  if checkOutputs && checkRange 
    then ()
    else error ()

Now we need to write the helper functions which is a bit more complicated as they have to work with the script context. Firstly, to even access the context in the structure of ScriptContext we explored, we have to build that structure from the third argument ctx, which is of type BuiltinData. We can do this transformation by using unsafeFromBuiltinData function on ctx (the difference between unsafeFromBuiltinData and fromBuiltinData is that the former will error if it fails which is faster, while the latter will return Nothing). This leads to our first helper function:

valCtx = Plutus.unsafeFromBuiltinData ctx

We now have the valCtx of type ScriptContext, so we can destructure it accordingly. Remember that it consists of two fields scriptContextTxInfo :: TxInfo and scriptContextPurpose :: ScriptPurpose. For our use case, we are only interested in the TxInfo, and we can get it with our second helper function:

info = Plutus.scriptContextTxInfo valCtx

The info is now of type TxInfo, which contains all the information we need for our validation. Specifically, we are interested in the fields txInfoOutputs :: [TxOut] and txInfoValidRange :: POSIXTimeRange. To create the validation logic for the number of UTxOs we can simply use the length function to count the number of UTxOs in the transaction since txInfoOutputs is a [List]. We also combine that with the Plutus Prelude traceIfFalse function to provide us with debugging info in case of invalid transactions:

checkOutputs = traceIfFalse "4 or more outputs in tx!" $ length (Plutus.txInfoOutputs info) <= 3

The final part of our validator logic is to check that the submitted transaction's valid range is infinite. We first destruct the txInfoValidRange field of TxInfo, and compare it with the Plutus.always pre-defined time interval which corresponds to the infinite time range. Again, we use the traceIfFalse as before:

checkRange = traceIfFalse "Tx does not have infinite range!" $ Plutus.txInfoValidRange info == Plutus.always

Now we have all the pieces of the validation done and the full validator looks like this:

mkValidator :: BuiltinData -> BuiltinData -> BuiltinData -> ()
mkValidator _ _ ctx =
  if checkOutputs P.&& checkRange 
    then ()
    else error ()
  where
    valCtx = Plutus.unsafeFromBuiltinData ctx
    info = Plutus.scriptContextTxInfo valCtx
    checkOutputs = traceIfFalse "4 or more outputs in tx!" $ length (Plutus.txInfoOutputs info) <= 3
    checkRange = traceIfFalse "Tx does not have infinite range!" $ Plutus.txInfoValidRange info == Plutus.always

The rest of the functions for serialising and writing the script stay the same as with previous scripts (validator, script, scriptShortBs, scriptSerialised). We just need to change the compilation result destination in the writeSerialisedScript function:

writeSerialisedScript :: IO (Either (FileError ()) ())
writeSerialisedScript = writeFileTextEnvelope "compiled/ExploringScriptContext.plutus" Nothing scriptSerialised

If we try to compile this script as is, we will get an error:

Couldn't match expected type BuiltinString
    with actual type [ghc-prim-0.6.1:GHC.Types.Char]
     In the first argument of traceIfFalse, namely
        "Tx does not have infinite range!"

The traceIfFalse function is expecting a BuiltinString but we are passing it a regular Haskell string. We can solve this with the stringToBuiltinString function from PlutusTx.Builtins.Class. Add an import for this function and apply it to the trace message string in the checkOutputs and checkRange functions:

import PlutusTx.Builtins.Class (stringToBuiltinString)

...

checkOutputs = traceIfFalse (stringToBuiltinString "4 or more outputs in tx!")
    $ length (Plutus.txInfoOutputs txInfo) <= 3

checkRange = traceIfFalse (stringToBuiltinString "Tx does not have infinite range!")
    $ Plutus.txInfoValidRange txInfo == Plutus.always
    
...

The module will compile okay now. However, there is another way to get through this issue, with a GHC extension called OverloadedStrings. This extension lets GHC try to transform regular Haskell strings into the required types. We can remove the PlutusTx.Builtins.Class import and revert our functions to just using a normal Haskell string as before, with the addition of this extension to the top of the file.

...
{-# LANGUAGE OverloadedStrings #-}
...

Every time we want to automatically load a module we write when launching a cabal repl, we can add them to our .cabal file in the exposed-modules field.

-- hpm-validators.cabal

...
library
    hs-source-dirs:       src
    exposed-modules:      SimplestSuccess
                        , GuessingGame
                        , ExploringScriptContext
                        , Helpers.Utils
...

Testing the validator

Compile the validator as before by launching a cabal repl and calling the write function.

Prelude> ExploringScriptContext.writeSerialisedScript
Right ()

Firstly, create a script address for the validator. We will use src/testnet/ExploringScriptContext as the testing directory.

# testnet/ExploringScriptContext/create-script-address.sh

#!/usr/bin/env bash

NWMAGIC=2 # preview testnet

# Build script address
cardano-cli address build \
--payment-script-file ../../compiled/ExploringScriptContext.plutus \
--testnet-magic $NWMAGIC \
--out-file ExploringScriptContext.addr

echo "Script address: $(cat ExploringScriptContext.addr)"

As before, we need a way to check the UTxOs. It's likely that this script address will not have any UTxOs on it when checked.

# testnet/ExploringScriptContext/check-utxos.sh

#!/usr/bin/env bash

NWMAGIC=2 # preview testnet
export CARDANO_NODE_SOCKET_PATH=$CNODE_HOME/sockets/node0.socket

funds_normal=$(cardano-cli query utxo \
--address $(cat ../address/01.addr) \
--testnet-magic $NWMAGIC)

funds_script=$(cardano-cli query utxo \
--address $(cat ExploringScriptContext.addr) \
--testnet-magic $NWMAGIC)

echo "Normal address:"
echo "${funds_normal}"

echo "Script address:"
echo "${funds_script}"
./check-utxos.sh

Normal address:
                           TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------
5dc5111e257f8e68b0978c9619e57bbb12d365c0ec45d879115866bb674156ae     0        1826915 lovelace + TxOutDatumNone
8fc7a4fda80ad379811c44591a9fc4bae7fcd9a4ddda1574df910adc0143ac7a     1        9995661298 lovelace + TxOutDatumNone
ee346be463426509daec07aba24a8905c5f55965daebb39f842a49191d83f9e1     0        1829006 lovelace + TxOutDatumNone
Script address:
                           TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------

We need to fund the script with some tADA before testing it. Create a send-funds-to-script.sh script.

# testnet/ExploringScriptContext/send-funds-to-script.sh

#!/usr/bin/env bash

NWMAGIC=2 # preview testnet
export CARDANO_NODE_SOCKET_PATH=$CNODE_HOME/sockets/node0.socket


cardano-cli transaction build \
    --testnet-magic $NWMAGIC \
    --change-address $(cat ../address/01.addr) \
    --tx-in 8fc7a4fda80ad379811c44591a9fc4bae7fcd9a4ddda1574df910adc0143ac7a#1 \
    --tx-out $(cat ExploringScriptContext.addr)+10000000 \
    --tx-out-datum-embed-file ../../compiled/assets/unit.json \
    --out-file tx.body

cardano-cli transaction sign \
    --tx-body-file tx.body \
    --signing-key-file ../address/01.skey \
    --testnet-magic $NWMAGIC \
    --out-file tx.signed

cardano-cli transaction submit \
    --testnet-magic $NWMAGIC \
    --tx-file tx.signed
./send-funds-to-script.sh
Estimated transaction fee: Lovelace 169109
Transaction successfully submitted.

The funds are on the script, and we want to start actually testing the validator logic. Let's create the two transactions that should fail. These two cases are:

  1. The transaction tries to spend the script UTxO by creating 4 or more outputs.

  2. The transaction validity range is not infinite.

Let's start with the validity range case. We can query the tip of the chain with cardano-cli to get the current slot, and we can add an --invalid-before argument to the transaction build command with any slot before the current tip. This will define the transaction validity range from that slot to infinity. This will make the transaction pass the first validity range check that the node performs, as the current slot will fall into the transactions' valid range, but our logic inside the validator regarding the transaction range should fail.

cardano-cli query tip --testnet-magic 2
{
    "block": 1099151,
    "epoch": 290,
    "era": "Babbage",
    "hash": "31323e6507cf03e5668ab714be923535b01aee73b04ebb175c9c744472d573a4",
    "slot": 25086665,  # This is our current chain tip
    "slotInEpoch": 30665,
    "slotsToEpochEnd": 55735,
    "syncProgress": "100.00"
}

Select the script UTxO for the --tx-in and create a valid number of outputs (in the below example two).

# testnet/ExploringScriptContext/spend-script-funds-invalid-range.sh

#!/usr/bin/env bash

NWMAGIC=2 # preview testnet
export CARDANO_NODE_SOCKET_PATH=$CNODE_HOME/sockets/node0.socket


cardano-cli transaction build \
    --testnet-magic $NWMAGIC \
    --change-address $(cat ../address/01.addr) \
    --invalid-before 25086665 \
    --tx-in e4a68d9cb4e58d47085c74426441445eaa30c2a60a9d102217d27ec0b0664db8#0 \
    --tx-in-script-file ../../compiled/ExploringScriptContext.plutus \
    --tx-in-datum-file ../../compiled/assets/unit.json \
    --tx-in-redeemer-file ../../compiled/assets/unit.json \
    --tx-in-collateral ee346be463426509daec07aba24a8905c5f55965daebb39f842a49191d83f9e1#0 \
    --tx-out $(cat ../address/01.addr)+1500000 \
    --tx-out $(cat ../address/01.addr)+1500000 \
    --out-file tx.body

cardano-cli transaction sign \
    --tx-body-file tx.body \
    --signing-key-file ../address/01.skey \
    --testnet-magic $NWMAGIC \
    --out-file tx.signed

cardano-cli transaction submit \
    --testnet-magic $NWMAGIC \
    --tx-file tx.signed

Trying to build this transaction gives us a nice error message under Script debugging logs: since we used traceIfFalse.

./spend-script-funds-invalid-range.sh

Command failed: transaction build  Error: The following scripts have execution failures:
the script for transaction input 0 (in ascending order of the TxIds) failed with: 
The Plutus script evaluation failed: An error has occurred:  User error:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Script debugging logs: Tx does not have infinite range!

Okay, now let's see if the script fails when we try to create an invalid number of outputs. Create a spend-script-utxo-invalid-utxos.sh script to test this. This time, we omit the --invalid-before as we want the transaction to have infinite range to make sure the failure is from the number of outputs trying to be created.

# testnet/ExploringScriptContext/spend-script-utxo-invalid-utxos.sh

#!/usr/bin/env bash

NWMAGIC=2 # preview testnet
export CARDANO_NODE_SOCKET_PATH=$CNODE_HOME/sockets/node0.socket


cardano-cli transaction build \
    --testnet-magic $NWMAGIC \
    --change-address $(cat ../normal_address/01.addr) \
    --tx-in 1886d7f5af349199fd8fb74f37372b473c888cd40f19f5885ba470d8e25fb571#0 \
    --tx-in-script-file exploringContext.plutus \
    --tx-in-datum-file ../assets/unit.json \
    --tx-in-redeemer-file ../assets/unit.json \
    --tx-in-collateral 45337a0fb353dadc7e31f865378885207553b4471814384421e0fa1607271bf6#1 \
    --tx-out $(cat ../normal_address/01.addr)+1500000 \
    --tx-out $(cat ../normal_address/01.addr)+1500000 \
    --tx-out $(cat ../normal_address/01.addr)+1500000 \
    --tx-out $(cat ../normal_address/01.addr)+1500000 \
    --protocol-params-file ../normal_address/protocol.json \
    --out-file tx.body

cardano-cli transaction sign \
    --tx-body-file tx.body \
    --signing-key-file ../normal_address/01.skey \
    --testnet-magic $NWMAGIC \
    --out-file tx.signed

cardano-cli transaction submit \
    --testnet-magic $NWMAGIC \
    --tx-file tx.signed

Again, we get a nice error message explaining the failure.

./spend-script-funds-invalid-utxos.sh

Command failed: transaction build  Error: The following scripts have execution failures:
the script for transaction input 0 (in ascending order of the TxIds) failed with: 
The Plutus script evaluation failed: An error has occurred:  User error:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Script debugging logs: 4 or more outputs in tx!

The only thing left to test is whether a valid transaction will work. That is one with infinite range and less than four outputs. Let's create a spend-script-funds.sh to test it. The below example has just one output to be created specified via --change-address. With no other outputs present, all the tADA will go to this address after the transaction fees are paid.

# testnet/ExploringScriptContext/spend-script-utxo.sh

#!/usr/bin/env bash

NWMAGIC=2 # preview testnet
export CARDANO_NODE_SOCKET_PATH=$CNODE_HOME/sockets/node0.socket


cardano-cli transaction build \
    --testnet-magic $NWMAGIC \
    --change-address $(cat ../address/01.addr) \
    --tx-in e4a68d9cb4e58d47085c74426441445eaa30c2a60a9d102217d27ec0b0664db8#0 \
    --tx-in-script-file ../../compiled/ExploringScriptContext.plutus \
    --tx-in-datum-file ../../compiled/assets/unit.json \
    --tx-in-redeemer-file ../../compiled/assets/unit.json \
    --tx-in-collateral ee346be463426509daec07aba24a8905c5f55965daebb39f842a49191d83f9e1#0 \
    --out-file tx.body

cardano-cli transaction sign \
    --tx-body-file tx.body \
    --signing-key-file ../address/01.skey \
    --testnet-magic $NWMAGIC \
    --out-file tx.signed

cardano-cli transaction submit \
    --testnet-magic $NWMAGIC \
    --tx-file tx.signed

With this transaction, we can successfully spend the UTxO from the script address.

./spend-script-funds.sh

Estimated transaction fee: Lovelace 350224
Transaction successfully submitted.

Last updated