A Guessing Game Script

Exploring datum and redeemer in scripts

For our next script, we will use the datum and redeemer arguments instead of ignoring them. We will still ignore the third argument, the transaction context, for now. The goal of this script is to create a guessing game, where the UTxO sitting at the script address is unlocked if the submitting transaction sends a redeemer that matches the datum present at that UTxO. It is quite a simple re-rewrite from our first script - we just need to add a bit of logic to the mkValidator function and replace the function names accordingly. Create a new file src/GuessingGame.hs for this validator and paste the code from SimplestSuccess.hs into it. Our extensions and imports stay exactly the same, let's just remove the qualified from the PlutusTx.Prelude import so that we do not have to prefix every Prelude function with Prelude.:

import PlutusTx.Prelude

Writing the validator

We rename our module and exposed functions. Let's remove the script name from the exposed generic functions for serialising and writing the script to disk and just call them scriptSerialised and writeSerialisedScript:

module GuessingGame
  (
    scriptSerialised,
    writeSerialisedScript
  )
where

Our mkValidator function becomes:

{-# INLINABLE mkValidator #-}
mkValidator :: BuiltinData -> BuiltinData -> BuiltinData -> ()
mkValidator datum redeemer _ =
    if datum == redeemer then ()
    else error ()

We use the comparison function to check whether the received redeemer matches the datum sitting at the UTxO. If that's the case, we return () as a sign of successful validation. Otherwise, we use the error () from Prelude to signify failed validation. Our validator and script functions stay exactly the same, but we need to update the names of generic functions for serialising and writing the script to disk, and set the write filename to GuessingGame.plutus:

scriptShortBs :: SBS.ShortByteString
scriptShortBs = SBS.toShort . LBS.toStrict $ serialise script

scriptSerialised :: PlutusScript PlutusScriptV2
scriptSerialised = PlutusScriptSerialised scriptShortBs

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

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
                        , Helpers.Utils
...

Testing the validator

Serialising string-like datums

To test this script, we could use the compiled unit.json as our datum, but let's instead create a more interesting one. Again launch the cabal repl and load the Utils module. Let's say we want to create a secret in the String format. We can try:

Prelude> :l src/helpers/Utils.hs 
Ok, one module loaded.
Prelude Utils> writeJSONData "compiled/assets/secretGuess.json" "I am a secret"

But we will get the following error:

<interactive>:3:1: error:
     No instance for (PlutusTx.IsData.Class.ToData Char)
        arising from a use of writeJSONData

It seems that PlutusTx.toData class does not implement an instance for the String type. Indeed, if we check the documentation, we see that only a ToData BuiltinByteString is defined when it comes to string-like values. So we need to convert our Haskell String to a Plutus BuiltinByteString. Again we need to look through the documentation to find the function we need (located in the PlutusTx.Builtins.Class module):

stringToBuiltinByteString :: String -> BuiltinByteString

Let's load up this module and apply this function to our string before serialising it:

Prelude Utils> import PlutusTx.Builtins.Class
Prelude PlutusTx.Builtins.Class Utils> writeJSONData "compiled/assets/secretGuess.json" $ stringToBuiltinByteString "I am
 a secret"

No error message, and our datum is compiled under compiled/assets/secretGuess.json. It looks like this:

{"bytes":"4920616d206120736563726574"}

We are now ready to test the validator! Create a new directory testnet/GuessingGame for this purpose.

Let's compile the validator as well.

-- The following line is not necessary if the module was added to exposed-modules in the .cabal file
Prelude> :l src/GuessingGame.hs
Prelude> GuessingGame.writeSerialisedScript
Right ()

Now, we need to create an address for this validator like before:

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

#!/usr/bin/env bash

NWMAGIC=2 # preview testnet

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

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

Our check-utxos.sh script remains the same, but we updated the script address:

# testnet/GuessingGame/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 GuessingGame.addr) \
--testnet-magic $NWMAGIC)

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

echo "Script address:"
echo "${funds_script}"

Again, we will see existing UTxOs present on the script address, as someone has already compiled and used it. Let's send some value to the script along with our secret datum.

# testnet/GuessingGame/set-guess-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 ea340a31a9ad4dd059e6743274607e2cc7bdb7b12b5345be8bc81988d9a6ea86#1 \
    --tx-out $(cat GuessingGame.addr)+2000000 \
    --tx-out-datum-embed-file ../../compiled/assets/secretGuess.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

If we check the UTxOs again, we'll see a new UTxO sitting at the script address. Only transactions with the matching redeemer can spend it. Let's try to first spend it with an invalid redeemer. Since the datum of the UTxO we are trying to spend needs to be specified in the transaction regardless, this is slightly pointless. But to show that the validator works as it should, let's specify the correct datum, but the wrong redeemer:

# testnet/GuessingGame/spend-script-utxo-invalid.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#0 \
    --tx-in-script-file ../../compiled/GuessingGame.plutus \
    --tx-in-datum-file ../../compiled/assets/secretGuess.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

Trying to execute it gives us a script execution failure:

./spend-script-utxo-invalid.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:

The logs are empty as we have not configured any logging, nor did we give an error message. But we still know that the script failed to execute successfully for this transaction because the redeemer does not match the datum. Let's create a valid transaction this time. We just need to change the --tx-in-redeemer-file line to point to our secret guess:

4) spend-script-utxo.sh

# testnet/GuessingGame/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 8fc7a4fda80ad379811c44591a9fc4bae7fcd9a4ddda1574df910adc0143ac7a#0 \
    --tx-in-script-file ../../compiled/GuessingGame.plutus \
    --tx-in-datum-file ../../compiled/assets/secretGuess.json \
    --tx-in-redeemer-file ../../compiled/assets/secretGuess.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

We see that the transaction is successful this time, and we are able to spend the script UTxO!

./spend-script-utxo.sh
Estimated transaction fee: Lovelace 173085
Transaction successfully submitted.

Last updated