In this StakeValidator example, we will create a simple script that controls staking actions via secret codes that the script is parameterised with.
Writing the validator
First, we will use the same GHC extensions as with the parameterised shared wallet script and the same module imports (except we don't need txSignedBy here).
{-# LANGUAGENoImplicitPrelude #-}{-# LANGUAGETemplateHaskell #-}{-# LANGUAGEOverloadedStrings #-}{-# LANGUAGETypeApplications #-}{-# LANGUAGETypeFamilies #-}{-# LANGUAGEScopedTypeVariables #-}{-# LANGUAGEMultiParamTypeClasses #-}{-# LANGUAGEDataKinds #-}module StakingValidator (scriptSerialised,writeSerialisedScript,CodeParam (..) )whereimportqualified PlutusTximport PlutusTx.Preludeimportqualified Plutus.V2.Ledger.Api as Plutusimport Cardano.Api.Shelley (PlutusScript (PlutusScriptSerialised),PlutusScriptV2,writeFileTextEnvelope)import Cardano.Api (FileError)importqualified Data.ByteString.Lazy as LBSimportqualified Data.ByteString.Short as SBSimport Codec.Serialiseimportqualified Ledger.Typed.Scripts as Scriptsimportqualified Plutus.Script.Utils.V2.Typed.Scripts as PSU.V2importqualified Plutus.Script.Utils.Typed as PSUimport Prelude (IO)...
Then, we need to create a parameter for the script as before, calling it CodeParam, and making it an instance of Data and Lift classes.
The cert field will contain an Integer code that must be specified in the redeemer for script certificates to be validated, and the reward code will have to be specified in order to withdraw rewards. We will be writing this as a typed validator so we need a ValidatorTypes instance as well:
dataCodeValidatorinstance PSU.ValidatorTypesCodeValidatorwheretypeinstanceRedeemerTypeCodeValidator=Integer-- We only care about the redeemer type for the stake validator
We now have everything to create the mkStakingValidator function which will represent our stake validator logic:
We simply need to check that the redeemer matches the corresponding action. Any script purpose other than Certifying or Rewarding will always be False.
Next, we need to compile that function into a StakeValidator type with the end goal of the UntypedStakeValidator that we looked at earlier. Since there is still no interface TypedStakeValidator, we have to use somewhat explicit code with mkStakeValidatorScript and mkUntypedStakeValidator. This function is defined as:
mkUntypedStakeValidator:: PV1.UnsafeFromData r=> (r -> sc ->Bool)->UntypedStakeValidatormkUntypedStakeValidator f r p = check $ f (tracedUnsafeFrom "Redeemer decoded successfully" r) (tracedUnsafeFrom "Script context decoded successfully" p)
It receives a function (r -> sc -> Bool) and returns the UntypedStakeValidator which is the end result we want here. Since this is a parameterised contract, we have to apply our CodeParam to the mkStakingValidator function first in order to get just the (Integer -> ScriptContext -> Bool) function that is required here.
That is why we have to compose the two functions together and apply the CodeParam. However, since this is all being compiled to Plutus IR (intermediate Plutus Core), we also have to first lift the CodeParam value to Plutus IR for it to be applied.
We do this with PlutusTx.liftCode cp, and we are able to do it because we made CodeParam an instance of the Lift class. In the previous examples, this was abstracted for us via the mkTypedValidatorParam function, but since one is not available for stake validators (yet), we have to do it manually here:
The rest is the same as before, the only difference being that instead of unValidatorScript, we use unStakeValidatorScript to get the Script type of the validator:
All that is left to do is come up with a concrete instance of the CodeParam and pass it to the writeSerialisedScript function to compile the validator. For this example, we will just use the integers 1 and 2 as our secret codes. In the cabal repl of our nix-shell, we can do that with:
To test a staking validator, we will need to slightly change our usual way of creating a script address with create-script-address.sh. So far we have only been using the payment credentials part to create an address, either a verification key or a validator script hash. Here, we want to add the optional staking credential part and use our stake validator script to provide the validation logic.
We can first use the cardano-cli stake-address build to get the stake address for this validator. When we do that, this address will be just a "Reward account address" (terminology from Cardano docs), unable to receive any UTxO, but could be used as a rewards address for delegation.
As mentioned above, we usually want to combine payment and staking credentials into one address, so let's do that next. We will specify the 01.vkey for the payment credential, and use our stake validator script to provide the staking credential. The full Bash script looks like this:
#!/usr/bin/env bashNWMAGIC=2# preview testnet# Build script stake addresscardano-clistake-addressbuild \--testnet-magic ${NWMAGIC} \--stake-script-file../../compiled/StakingValidator.plutus \--out-fileStakingValidator.stakeecho"Script stake address: $(catStakingValidator.stake)"# Build address with 01.vkey for payment part and script stake partcardano-cliaddressbuild \--payment-verification-key-file../address/01.vkey \--stake-script-file../../compiled/StakingValidator.plutus \--testnet-magic ${NWMAGIC} \--out-fileStakingValidator.addrecho"Full address: $(catStakingValidator.addr)"
Running the script provides us with the two addresses, the first one being just the stake address and the second a fully combined address:
It's time to finally register and delegate this address to a pool. Besides submitting the registration certificate, we also have to find a pool to delegate to. We can get a list of stake pools from cardano-cli with:
We can use preview.cardanoscan.io to check pool information. Let's pick the second pool from the list pool1qxcz3zxnye8g9ejsqslhl0ljevdx895uc80lr89ulf92gcv40ng because it is regularly creating blocks so we know we will get rewards. We can submit both the address registration and delegation certificate in the same transaction. Our validator defined two secret integer codes (we used 1 and 2) that need to be specified as the redeemer for transactions, the first one required to validate certificates being submitted (registration, delegation, deregistration), and the second one for validating rewards withdrawals.
Let's create a register-and-delegate-script.sh script that will do that for us. We create the registration and delegation certificates and attach them to the transaction build command. Then we have to specify the --certificate-script-file argument since our certificates are validated by our script rather than regular keys. The script must receive a redeemer and since our redeemer is very simple (just the integer value 1), we can use --certificate-redeemer-value 1. Besides that, we can specify the --change-address $(cat StakingValidator.addr) to send any remaining funds after the transaction fees are substracted to the staking address as well to be delegated.
We can check Cardanoscan again to look for our script address and see that the transaction did what was expected. It is now registered and delegated to the pool we specified.
You are free to try redelegating now by creating a new delegation-certificate with the wrong redeemer or an absent redeemer but our staking validator will invalidate the transaction.
We can create a check-script-rewards.sh next, but it will be empty for a few days until the first rewards start coming in (epochs are 1 day long on the Preview testnet).
Actually, the pool we initially delegated pool1qxcz3zxnye8g9ejsqslhl0ljevdx895uc80lr89ulf92gcv40ng to is not producing blocks anymore so we need to redelegate to another pool. We can write a new redelegate.sh script for that.