The Simplest Script
As mentioned in the EUTxO overview, the validator script receives three arguments:
Datum
Redeemer
Context
The Haddock documentation for Plutus specifies the main modules (https://intersectMBO.github.io/plutus/master/):
The two modules that we will be importing into our Haskell files are PlutusTx
and PlutusTx.Prelude
. We start with a new SimplestSuccess.hs
file. We will write the simplest contract that successfully validates every attempt to spend its funds.
Writing the validator
First, we will need to add some GHC extensions at the start of the file:
The DataKinds
extension is needed for some Template Haskell features or the PlutusTx
compilation will fail. NoImplicitPrelude
states not to import the Haskell Prelude. We always need to specify this as PlutusTx
has its own Prelude that we have to use. When writing the validator to a file, we still need to use the IO
monad from the original Prelude, which we can import explicitly. The TemplateHaskell
extension is simply to allow us to write Template Haskell expressions to be able to properly use PlutusTx.compile
inside our module.
Next, we define our module name (same as the filename):
Next, we need to import the packages required for the compilation of our script. Note that these must be defined in our .cabal
file in order to be imported here. For now, these will be:
In general, we want to write three major parts of our Haskell file:
The
mkValidator
function that contains our validation logic.Compilation of that function to a Plutus Core script (the on-chain language). This is done by using template Haskell.
Serialise and write the script to a
.plutus
file.
When we write a validator we define a function that receives the three aforementioned arguments and returns a unit ()
if successful. Not returning a ()
means that the validation failed and the transaction will be invalidated. With that in mind, we can start to think about the type signature of the validator function, something like:
mkValidator :: Datum -> Redeemer -> Context -> ()
.
But what are the types of Datum
, Redeemer
and Context
? It turns out that in Plutus, all three of the validation arguments need to come in a type of Data
. We can explore the Haddock pages to learn more about it: https://intersectMBO.github.io/plutus/master/plutus-core/html/PlutusCore-Data.html#t:Data.
We see that the Data
type comes with several constructors, but the main takeaway is that it is a generic data type that can represent various things such as integers, byte strings, lists, and maps. Plutus also features a BuiltinData
type (https://intersectMBO.github.io/plutus/master/plutus-tx/html/PlutusTx-Builtins.html#g:4) that can be used directly in the on-chain code.
So we can now write the type signature of our validator function using the BuiltinData
type for its arguments and returning ()
:
mkValidator :: BuiltinData -> BuiltinData -> BuiltinData -> ()
Since this function always returns ()
regardless of its arguments, any UTxO belonging to the script will be spendable by any transaction. We add the inlinable pragma just above the function to be able to later use it with the PlutusTx compiler directly in our Haskell code.
We now need to do part 2 of our three steps, compiling this validator function to Plutus Core:
The unusual syntax above is template Haskell: $$([|| ||])
. The function Plutus.mkValidatorScript
requires a Plutus Core argument so the mkValidator
is first compiled to Plutus Core. In order for this to work, the compiled function mkValidator
must be made inlinable with {-# INLINABLE mkValidator #-}
that we specified earlier.
Part 3 of our three steps is arguably the simplest. We need to unwrap the validator to get the script. This is just a necessary step to conform with the expected types. Since Plutus.Validator
is a wrapper around Plutus.Script
which is used as the actual validator in the ledger, we need to unwrap it.
We can now serialise the script to a ShortByteString
:
The next step is just a type conversion again:
Finally, we expose a function that writes the Plutus
script to a file that we will use with the actual blockchain:
We can load up a cabal repl
, and compile the script. Make sure you create the compiled/
directory first.
Serialising a datum object
We now have the compiled script in compiled/simplestSuccess.plutus
. Another thing we need is to serialise a datum
. We need to use datums on script outputs as any UTxO without a datum hash attached will be unspendable as we mentioned before. We need to write a utility function for converting Plutus data to JSON because cardano-cli
expects JSON values. Create a new file under src/Helpers/Utils.hs
:
This is mostly boilerplate code that we don't need to think too much about. It simply takes some data of the ToData
class and serialises it to a JSON that cardano-cli
expects. We can now load and use this function anytime we need to write a datum file. Here, we just want to write a unit ()
datum file. Make sure you create the compiled/assets/
directory before running the code below first.
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
To start testing our validators, we will need to create some regular Cardano addresses on the testnet and use the faucet to get some tADA. We will use these to pay the fees for the transactions we create as well as the collateral inputs. We will build two addresses now and use them throughout the course with different validators. Let's place all our testing files in the testnet/
directory of the project root. Below is a bash
script that creates the addresses for us (you can also use cardano-cli
directly in the terminal). Make sure you create the testnet/addresses/
directory beforehand.
We always test our validators from OUTSIDE the**** nix-shell
, i.e. with our local node that is synced. The**** nix-shell
****provides ONLY a development environment for writing and serialising Plutus validators.
To run the script, we first have to make it an executable:
We will always need to make any bash
scripts we intend to run executable first with the above command chmod +x <script-name>.sh
.
Once done, we need to request funds to our new address from the faucet: https://docs.cardano.org/cardano-testnet/tools/faucet/. Make sure you select the right network for the transaction, we are using preview
in this course.
The two addresses we created will be shared among all the validators we test. Now we need to create a script address for our SimplestSuccess
validator. For each validator we test, we will place the testing resources under a new directory specific to that validator. For SimplestSuccess
, that will be testnet/SimplestSuccess/
. After creating the directory, let's build the script address. Note that here we do not specify a key pair for the address, but instead a script file that acts as the validator for that address.
We can also build a convenience script to check the UTxOs at our addresses.
We will see something interesting when we check the UTxOs. Our normal address has just a single UTxO on it, which is the transaction from the faucet, and that is expected. But the script address has a lot of UTxOs on it. You might have expected it to have no UTxOs as we just created it. But it turns out, this is a common script whose address was already created and used on this testnet. Of course, if two or more people write scripts with the same compilation result (the same Plutus Core code that is executed on-chain), then the address created from that script will also be the same. This is because a script address is simply the hash of the script code.
Since we know there are already UTxOs sitting at the script address, we do not really need to send any funds to it in order to test that we can spend them back. We can just use any of the existing UTxOs since the script allows any UTxO sitting on it to be spent. We will still create a script for sending funds to the script for completeness. Note that as we mentioned before, any script UTxO without a datum attached is UNSPENDABLE (go ahead and try spending one), so never forget to attach a datum when sending funds to a script. The --tx-in
argument will be the UTxO from our normal address so you need to change it accordingly. For datum, we will simply embed the unit.json
that we created earlier. Finally, we need to sign the transaction with the private key of the 01.addr
.
Note that for all the bash scripts in this course, you will need to change the --tx-in arguments to match your own UTxOs. If you have not maintained the same directory structure as outlined in the course, you will need to change those paths accordingly as well.
Now we want to see if we can spend a UTxO from the script. Good luck finding the one you just sent to it. You can just pick a random one that has a datum from the UTxO list instead. This time, we must include a collateral UTxO, which must be from a regular address as we mentioned before. Change the --tx-in
and --tx-in-collateral
accordingly.
After the transaction is successfully submitted and processed, we can confirm that our 01.addr
received the funds from the script and our collateral was not spent.
Practice!
It is important to practice on your own. Try using the materials above to write and test a Plutus script that always fails (a sort of token-burning script) from scratch.
Last updated