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.
Here, we are looking at the plutus-apps repository Haddock documentation.
We can see that it consists of two fields:
scriptContextTxInfo :: TxInfo
scriptContextPurpose :: ScriptPurpose
The scriptContextTxInfo
field is a type of TxInfo
for which we can find further information in Haddock:
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:
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:
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
:
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:
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:
If we try to compile this script as is, we will get an error:
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:
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.
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.
Testing the validator
Compile the validator as before by launching a cabal repl
and calling the write function.
Firstly, create a script address for the validator. We will use src/testnet/ExploringScriptContext
as the testing directory.
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.
We need to fund the script with some tADA before testing it. Create a send-funds-to-script.sh
script.
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:
The transaction tries to spend the script UTxO by creating 4 or more outputs.
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.
Select the script UTxO for the --tx-in
and create a valid number of outputs (in the below example two).
Trying to build this transaction gives us a nice error message under Script debugging logs:
since we used traceIfFalse
.
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.
Again, we get a nice error message explaining the failure.
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.
With this transaction, we can successfully spend the UTxO from the script address.
Last updated