Let's write a parameterised validator that is a simple deadline function. Before the deadline, all transactions are validated, and after the deadline, all are invalidated. The deadline will be our parameter for the validator that is baked in the script. This deadline needs to be expressed in the Plutus.POSIXTime type, which is measured as the number of milliseconds since 1970-01-01T00:00:00Z. Plutus always works with POSIXTime, while the Cardano chain works with slots. The reason for this discrepancy is that the slot length of the chain is a parameter that can change over time.
Writing the validator
Let's create a new file DeadlineParam.hs, define the module DeadlineParam and add the same imports as before. We can start off by creating the simple DeadlineValidator type for the validator. Since we don't need the datum and redeemer in this case, we can omit their definitions to apply the default () type for both of them.
dataDeadlineinstance Scripts.ValidatorTypesDeadline-- default types for datum and redeemer are ()
Since POSIXTime already is an instance of ToData, we don't need to worry about lifting values right now and can go straight to the mkValidator function. The signature will be:
Inside the function, we want to check that the txInfoValidRange is contained in its entirety in the interval of negative infinity to the deadline.
Important to note that the ENTIRE transaction validity range must fall into this interval because if the positive end of the valid range would go over the deadline, the transaction would be validated after the deadline even if the majority of the valid range is before the deadline!
We will need to use some functions from the Interval module to determine that, namely contains and to.
import Plutus.V1.Ledger.Interval (contains,to)...mkValidator:: Plutus.POSIXTime->()->()-> Plutus.ScriptContext->BoolmkValidator deadline _ _ ctx = traceIfFalse "Invalid tx range"$ to deadline `contains` txRangewhereinfo:: Plutus.TxInfo info = Plutus.scriptContextTxInfo ctxtxRange:: Plutus.POSIXTimeRange txRange = Plutus.txInfoValidRange info
We destructure our ctx as before to get to the txInfoValidRange field. Then we state the predicate to deadline `contains` txRange which reads as "the interval from negative infinity to our deadline (inclusive) contains the entire interval of the transaction validity range".
Now, we need to compile the parameterised validator. We do this with the PSU.V2.mkTypedValidatorParam instead of PSU.V2.mkTypedValidator. Here is the mkTypedValidatorParam definition:
-- | Make a 'TypedValidator' from the 'CompiledCode' of a parameterized validator script and its wrapper.mkTypedValidatorParam::forall a param.LiftDefaultUni param => -- | Validator script (compiled)CompiledCode (param ->ValidatorType a) -> -- | A wrapper for the compiled validatorCompiledCode (ValidatorType a ->UntypedValidator) -> -- | The extra paramater for the validator script param ->TypedValidator amkTypedValidatorParam vc wrapper param = mkTypedValidator (vc `applyCode` liftCode param) wrapper
This function takes similar arguments as before. CompiledCode (param -> ValidatorType a) is our parameterised mkValidator function, CompiledCode (ValidatorType a -> UntypedValidator) is our wrapper to BuiltinData -> BuiltinData -> BuiltinData -> (), and what we get as a result is param -> TypedValidator a which will be the type signature of our typedValidator function. This makes sense, as our validator accepts a parameter to be baked in the validator. The validator can only be completed once that parameter is received and applied, finally resulting in UntypedValidator. Our typedValidator function now looks like this:
The next step is to get the script from our validator. Before we used simply PSU.V2.validatorScript typedValidator and Plutus.unValidatorScript validator, but since typedValidator now accepts one argument, we need to compose these functions together, and the validator and script functions also must receive the deadline parameter:
validator:: Plutus.POSIXTime-> Plutus.Validatorvalidator = PSU.V2.validatorScript . typedValidatorscript:: Plutus.POSIXTime-> Plutus.Scriptscript = Plutus.unValidatorScript . validator-- Note: we could write it a different way, this is using ETA reduction.
The last step is to write the serialised script to a file. Our writing script functions change a bit as a result of the additional parameter the script function must receive. At this stage, we simply need to apply the parameter to the script function. We will write it in a way so that writeSerialisedDeadlineParamScript accepts the deadline parameter and passes it on to create a script with our specified deadline. So using this function we can create many disfferent scripts of the same family (same validation logic), but with different deadline parameters.
That's it! We can now load our new module in cabal repl and write the script to a file. Of course, we need to provide the actual POSIXTime parameter to specify the deadline. For testing, we can get the current time with date %s and add 20 minutes to it to give us time for testing:
Remember, we are always working with POSIXTime in miliseconds when it comes to Plutus.
expr $(date+%s000) +12000001692181336000
That gives us our deadline in POSIXTime one hour from now. Every transaction we submit until then will be validated!
ghci> writeSerialisedScript 1692181336000
Testing the validator
As always, we create the script address first in testnet/DeadlineParam/create-script-address.sh.
Time to send some funds to the script. Let's create two outputs, one that we intend to spend before the deadline and another to test that we cannot spend it after the deadline. Don't forget to attach a datum to both of these or they will be unspendable in any case!
Time to successfully spend one of the outputs before the deadline. We need to set the correct valid range for this transaction (remember the default is infinite which would be invalidated by our validator). We can get the current slot of the chain with:
cardano-cliquerytip--testnet-magic2--socket-path $CNODE_HOME/sockets/node0.socket{"block":1117738,"epoch":295,"era":"Babbage","hash":"f3c6f5cabd28845159c1044e4dffb4b7a18170352fe4d79671fc0181aca1e2be","slot":25524308,# This is our current slot"slotInEpoch":36308,"slotsToEpochEnd":50092,"syncProgress":"100.00"}
Using the --invalid-hereafter option in the transaction build command, we can set the upper limit of the transaction validity range to the current slot plus some time to allow the transaction to be processed, but not passed the deadline. For example, our current slot is 25524308 and we will add 300 seconds to it for the transaction to make it 25524608 in our spend-script-funds.sh for this validator:
Remember, we are always working with slots when it comes to cardano-cli or cardano-node.
To test that we cannot just spend a script UTxO with any --invalid-hereafter option, let's try to submit a transaction in which the right side of the validity range falls after the deadline. To make sure it is after the deadline, we can simply add one hour to the current slot, since we know that will be past the deadline. So instead of 25524608, let's use 25528208 (25524608 + 3600). We can call this file spend-script-funds-past-deadline.sh. We also have to specify the other UTxO on the script address, since we already spent the first one.