Lecture 3: Vesting example
Credits
Condensed version of Lecture #3 of the Plutus Pioneer Program by Lars Brünjes on Youtube Cloned from Reddit (u/RikAlexander)
Changes
To start this lecture, we'll discuss some changes to Plutus.
If we look at the week02 example:
plutus-pioneer-program/code/week01/src/Week02/IsData.hs
Specifically the Validator:
Lines 37 - 39 of IsData.hs
{-# INLINABLE mkValidator #-}
mkValidator :: () -> MySillyRedeemer -> ValidatorCtx -> Bool
mkValidator () (MySillyRedeemer r) _ = traceIfFalse "wrong redeemer" $ r == 42
What changed?
ValidatorCtx
was changed to: ScriptContext
{-# INLINABLE mkValidator #-}
mkValidator :: () -> MySillyRedeemer -> ScriptContext -> Bool
mkValidator () (MySillyRedeemer r) _ = traceIfFalse "wrong redeemer" $ r == 42
We'll discuss the ScriptContext
in a second.
Another change: Lines 53 - 60 of IsData.hs
validator :: Validator
validator = Scripts.validatorScript inst
valHash :: Ledger.ValidatorHash
valHash = Scripts.validatorHash validator
scrAddress :: Ledger.Address
scrAddress = ScriptAddress valHash
Instead of having to create the validator hash ourselves
to then generate our script address
We can now use the function scriptAddress
(notice the lowercase s
) with our validator as argument
Which will save us a few lines of code
validator :: Validator
validator = Scripts.validatorScript inst
scrAddress :: Ledger.Address
scrAddress = scriptAddress validator
ScriptContext
ScriptContext is defined in Contexts.hs of the Plutus Repo
We'll start with it's definition.
Line 116
data ScriptContext = ScriptContext {
scriptContextTxInfo :: TxInfo,
scriptContextPurpose :: ScriptPurpose
}
The ScriptContext constructor takes a scriptContextTxInfo
of type TxInfo
, and a scriptContextPurpose
of type ScriptPurpose
The ScriptPurpose
lines 94 - 98
data ScriptPurpose
= Minting CurrencySymbol
| Spending TxOutRef
| Rewarding StakingCredential
| Certifying DCert
The ScriptContext
can have a Minting, Spending, Rewarding or Certifying purpose.
For this lecture though we'll be using the Spending purpose.
Our scriptContextTxInfo
, TxInfo is quite extensive, and holds multiple values; which in turn can be used by our validator for use in the validation logic.
Lines 101 - 114
data TxInfo = TxInfo
{ txInfoInputs :: [TxInInfo] -- ^ Transaction inputs
, txInfoInputsFees :: [TxInInfo] -- ^ Transaction inputs designated to pay fees
, txInfoOutputs :: [TxOut] -- ^ Transaction outputs
, txInfoFee :: Value -- ^ The fee paid by this transaction.
, txInfoForge :: Value -- ^ The 'Value' forged by this transaction.
, txInfoDCert :: [DCert] -- ^ Digests of certificates included in this transaction
, txInfoWdrl :: [(StakingCredential, Integer)] -- ^ Withdrawals
, txInfoValidRange :: SlotRange -- ^ The valid range for the transaction.
, txInfoSignatories :: [PubKeyHash] -- ^ Signatures provided with the transaction, attested that they all signed the tx
, txInfoData :: [(DatumHash, Datum)]
, txInfoId :: TxId
-- ^ Hash of the pending transaction (excluding witnesses)
} deriving (Generic)
Most of which are clearly explained in the respective comment next to it.
Although we'll be having a closer look at the txInfoValidRange
and txInfoSignatories
.
txInfoSignatories
holds multiple PubKeyHashes of all wallets that are part of this contract transaction; this is important to check if everybody is who they say they are.
txInfoValidRange
holds a SlotRange
which defines at which time the Transaction is valid e.g. you might want to hold ADA until a specific time at which one might redeem it, or maybe you'd want to expire the option of redeeming, etc.
SlotRange
In our txInfoValidRange
we have a SlotRange
, this is defined in Slot.hs of the Plutus Repo
Defined on line 56
Slot
is nothing but a wrapper around Integer (in essence Slot is just a number)
(defined on line 42)
SlotRange
holds and Interval
(defined in Interval.hs of the Plutus Repo)
Line 58
It is basically nothing else but a range of slots. From -> To.
Note: The interval can be unbounded on either side, From
doesn't have to be a specific slot, it may also be the beginning of time (Line 63, NegInf) although in reality this wouldn't surpass our Genesis slot, as this is our "beginning" slot; same goes for To
which may also be defined as the End of time respectively. (Line 63, PosInf)
Interval
To construct an Interval there are some functions defined in Interval.hs, including:
Excerpts from Lines 186 - 217
@interval a b@ includes all values that are greater than or equal to @a@ and smaller than @b@. Therefore it includes @a@ but it does not include @b@.
As the name suggests, this is just the one slot (from @a@ to @a@)
@from a@ is an 'Interval' that includes all values that are greater than or equal to @a@. So from @a@ to infinity
@to a@ is an 'Interval' that includes all values that are smaller than @a@. (Beginning of time -> @to)
An 'Interval' that covers every slot.
An 'Interval' that is empty; hence the name never
.
All of the previous functions are there to assist with easy Interval
creation.
Now that we know how to create our Intervals, we want to be able to do some checks on it.
Excerpts from lines 219 - 264 of Interval.hs
Check whether a value is in an interval.
Check whether two intervals overlap, that is, whether there is a value that is a member of both intervals.
'intersection a b' is the largest interval that is contained in 'a' and in 'b', if it exists.
'hull a b' is the smallest interval containing 'a' and 'b'.
@a contains
b@ is true if the 'Interval' @b@ is entirely contained in @a@. That is, @a contains
b@ if for every entry @s@, if @member s b@ then @member s a@.
Check if an 'Interval' is empty.
Check if a value is earlier than the beginning of an 'Interval'.
Check if a value is later than the end of a 'Interval'.
Note: all of this can of course be found in Interval.hs, but for completeness i've written it all down in this article.
Interval usage / testing
For testing the Interval creation and the helper functions, go into your Terminal and cd into the week03 directory of the Plutus Pioneer Program repo.
and run
Which will then give us a interactive environment CLI where we can test Interval
.
First we'll need to import the Interval
module by running
Now we can test our Interval creation by for example running
which if displayed (just entering the variable name with \<enter>)
a -> an Interval from slot 10 to 20
b -> an Interval from slot 10 to infinity
Play around with all the possible constructor/helper functions! :)
Vesting
Open the file
/plutus-pioneer-program/code/week03/Vesting.hs
Copy all its contents into the Plutus Playground
Either use your own local running playground. Instructions located: Community Docs or my Reddit article for MacOS
You could use the Community Run Playground for week03.
https://playground-week3.plutus-community.com
In short what this contract does is it allows a wallet to send ada, which will be stored, and may only be grabbed after a specific amount of time (slots) and only by a specific wallet.
As of week03, the playground will now also add Fees for transactions. These are set to 10 Lovelaces (in the future they will probably be more accurately calculated).
So to be sure of having enough Lovelaces to cover the transaction fees, I'd always up my opening balances to something more than 10.
Next we'll set up an example like this:
As you can see the getPubKeyHash
is empty here.
This is where we'll need to input the Public key hash of Wallet 2; by which we will identify if its really wallet2 that tries to grab the funds after the deadline of 10 slots.
How do we get the public key hash for wallet 2? There are 2 ways. The easiest one would be to just Evaluate the script like this (leaving the pubkeyhash empty)
In the simulated transactions overview, in the genesis slot (0), on the right side where the outputs are located, we can see the pubkeyhash for both wallets.
The other way, uses the CLI we opened for Interval / testing.
Open it, and first import 2 dependencies:
Which will provide us with the walletPubKey
function, that takes a wallet and returns a public key for it.
Second import:
This gives us access to the pubKeyHash
function, that generates a hash based on a wallets public key
By chaining these two functions, we can easily retrieve the public key hash for wallet 2
Note: we use a dollar sign here, but we could of course also write this as:
Now that we have the public key hash for wallet 2, just copy it, paste it in the getPubKeyHash give action, and reevaluate the script.
The genesis slot (slot 0) just gives both wallets its initial 1000 Lovelaces
Slot 1 will put 100 Lovelaces up for grabs (but only for wallet 2, and only after the specified 10 slots have passed)
Also notice the 10 Lovelaces fee. Now wallet 1 has a balance of 1000 - fee 10 - vesting 100 = 890 Lovelaces.
And the Script has the 100 Lovelaces.
Now we've waited 10 Slots, so wallet 2 should be able to retrieve its 100 Lovelaces from the script.
The Grab action itself also has a Fee of 10 Lovelaces which will be subtracted from the final output. (We're left with 90 Lovelaces)
Vesting - The code
Great it works. But how does it work? Well, take a look at Vesting.hs (/plutus-pioneer-program/code/week03/Vesting.hs)
Lines 42 - 54
mkValidator :: VestingDatum -> () -> ScriptContext -> Bool
mkValidator dat () ctx =
traceIfFalse "beneficiary's signature missing" checkSig &&
traceIfFalse "deadline not reached" checkDeadline
where
info :: TxInfo
info = scriptContextTxInfo ctx
checkSig :: Bool
checkSig = beneficiary dat `elem` txInfoSignatories info
checkDeadline :: Bool
checkDeadline = from (deadline dat) `contains` txInfoValidRange info
Our new Validator.
As always the validator receives 3 parameters, first the datum, the redeemer, and the script context.
The datum, of type VestingDatum is defined on lines 34 - 38
And holds the beneficiary (public key hash of wallet 2) and the deadline (in our case this would be Slot 10).
These must be compared against the data in ScriptContext, which as previously discussed holds quite a lot of data, including txInfoSignatories and txInfoValidRange.
Our validator does 2 checks
mkValidator dat () ctx =
traceIfFalse "beneficiary's signature missing" checkSig &&
traceIfFalse "deadline not reached" checkDeadline
By combining them with &&
, if either one fails, the validator will return False (and thus the grab will fail).
The 2 functions checkSig
and checkDeadline
are implemented in the lines after the where
statement; both of them just return a Bool (True/False)
Note: If you're used to SQL, I like to look at the where
statement as prepared statement bindings. Where we can define the functions more or less after we've said what to do with it's results.
Lines 47 - 48
Sets info
to the TxInfo
values of our ScriptContext (ctx)
Note: for information about the ScriptContext
and TxInfo
check the ScriptContext chapter above.
Lines 50 - 51
Checks if the beneficiary is in our txInfoSignatories
list (elem
is an infix function for checking if an item exists in a list).
Note: we use an infix function here more for readability than anything else.
We could also write this as elem $ beneficiary dat $ txInfoSignatories info
or elem (beneficiary dat) (txInfoSignatories info)
Lines 53 - 54
Checks if the from (see chapter Interval) slot of the deadline Interval, is contained (see also chapter Interval) in our txInfoValidRange
.
Parameterized
Open the file
/plutus-pioneer-program/code/week03/Parameterized.hs
This script really does exactly the same as our previous script (Vesting).
Although its key difference is that instead of our VestingDatum
which hold our beneficiary and deadline.
We now call our validator with an extra parameter, in this case VestingParam
.
It contains the same data as previously in VestingDatum
, only now by parameterizing this, we get one key difference.
Without the parameterized functionality, there would only be one instance of this smart contract; now, we could easily have multiple instances of the same contract only with different parameters (our VestingParam
)
Line 44
What once was our VestingDatum
, is now just Unit (void), and another parameter is added VestingParam
.
Lines 58 - 61
data Vesting
instance Scripts.ScriptType Vesting where
type instance DatumType Vesting = ()
type instance RedeemerType Vesting = ()
Here our Datum is also just Unit (we no longer need it)
Lines 63 - 68
inst :: VestingParam -> Scripts.ScriptInstance Vesting
inst p = Scripts.validator @Vesting
($$(PlutusTx.compile [|| mkValidator ||]) `PlutusTx.applyCode` PlutusTx.liftCode p)
$$(PlutusTx.compile [|| wrap ||])
where
wrap = Scripts.wrapValidator @() @()
The important change here plutus compile of our validator
PlutusTx.compile compiles our validator into Plutus code; the problem solved here, is that if we'd just added p
(our VestingParam) to it, it would try to compile it into plutus code.
Which wouldn't do us any good, since the value of p
should be specified when running the script, not when compiling it.
This is solved with PlutusTx.applyCode
and PlutusTx.liftCode
.
Lars goes into this with a very good and more in depth explanation, which I would definitely recommend watching ( in the lecture at minute 53:00 )
Mostly though, this would probably be more or less boilerplate code. So for now I'll just leave it at that.
Lines 70 - 71
Here we receive our VestingParam, and use the .
to compose the two functions.
Same goes for generating our address
Lines 73 - 74
Difference between $
and .
?
The $
operator is for avoiding parentheses. Anything appearing after it will take precedence over anything that comes before.
The primary purpose of the .
operator is not to avoid parentheses, but to chain functions. It lets you tie the output of whatever appears on the right to the input of whatever appears on the left. This usually also results in fewer parentheses, but works differently.
(Source: StackOverflow Question)
Homework 1
Open the file
/plutus-pioneer-program/code/week03/Homework1.hs
Lines 45 - 46
Our assignment reads:
This should validate if either beneficiary1 has signed the transaction and the current slot is before or at the deadline
or if beneficiary2 has signed the transaction and the deadline has passed.
First have a look at our VestingDatum
data VestingDatum = VestingDatum
{ beneficiary1 :: PubKeyHash
, beneficiary2 :: PubKeyHash
, deadline :: Slot
} deriving Show
We now have 2 beneficiaries
beneficiary1
-> wallet 2 in this case, the wallet we are sending the ada to redeem it before the deadline has passed.
beneficiary2
-> wallet 1 in this case, the wallet that send the ada, if its not retrieved before the deadline has passed, we would like to get our ada back! (otherwise it would be lost forever in this contract)
The solution provided by Lars
/plutus-pioneer-program/code/week03/Solution1.hs
Implements new validation logic
mkValidator :: VestingDatum -> () -> ScriptContext -> Bool
mkValidator dat () ctx
| (beneficiary1 dat `elem` sigs) && (to (deadline dat) `contains` range) = True
| (beneficiary2 dat `elem` sigs) && (from (1 + deadline dat) `contains` range) = True
| otherwise = False
where
info :: TxInfo
info = scriptContextTxInfo ctx
sigs :: [PubKeyHash]
sigs = txInfoSignatories info
range :: SlotRange
range = txInfoValidRange info
It basically checks if the trying to grab the funds is beneficiary 1 or 2 first
(beneficiary1 dat `elem` sigs)
If this evaluates to True, for beneficiary1 (the one we hoped would grab the funds before the deadline), we check with to
(see chapter Interval) if we are still in time. (same as we did in the Vesting script with our checkDeadline
function).
For beneficiary2 we check if the deadline has already passed.
Homework 2
Homework 2 is really quite easy.
First I would always recommend trying to create your own solutions for the Homework.
But for the sake of this article, I'm just going to provide you with the Solution, and a small explanation.
Open the file
/plutus-pioneer-program/code/week03/Homework2.hs
Lines 36 - 51
mkValidator :: PubKeyHash -> Slot -> () -> ScriptContext -> Bool
mkValidator _ _ _ _ = False -- FIX ME!
data Vesting
instance Scripts.ScriptType Vesting where
type instance DatumType Vesting = Slot
type instance RedeemerType Vesting = ()
inst :: PubKeyHash -> Scripts.ScriptInstance Vesting
inst = undefined -- IMPLEMENT ME!
validator :: PubKeyHash -> Validator
validator = undefined -- IMPLEMENT ME!
scrAddress :: PubKeyHash -> Ledger.Address
scrAddress = undefined -- IMPLEMENT ME!
In essence we have to implement a validator with a parameter (PubKeyHash) and a Datum which contains the Slot.
In parameterized.hs both these values were located in VestingParam, the real difference is that now they are both individual values, one as parameter, and one as the datum.
So for the solution, we can use the Validator from parameterized.hs only with the PubKeyHash parameter and Slot datum instead of the VestingParam data type that contained both these values.
mkValidator :: PubKeyHash -> Slot -> () -> ScriptContext -> Bool
mkValidator pkh s () ctx =
traceIfFalse "beneficiary's signature missing" checkSig &&
traceIfFalse "deadline not reached" checkDeadline
where
info :: TxInfo
info = scriptContextTxInfo ctx
checkSig :: Bool
checkSig = pkh `elem` txInfoSignatories info
checkDeadline :: Bool
checkDeadline = from s `contains` txInfoValidRange info
For the inst implementation, we should not forget our datum (@Slot)
inst :: PubKeyHash -> Scripts.ScriptInstance Vesting
inst p = Scripts.validator @Vesting
($$(PlutusTx.compile [|| mkValidator ||]) `PlutusTx.applyCode` PlutusTx.liftCode p)
$$(PlutusTx.compile [|| wrap ||])
where
wrap = Scripts.wrapValidator @Slot @()
The Validator implementation is exactly the same as in Parameterized.hs
Same goes for our scrAddress
The rest of the wallet code is already prepared for us.
Footnote
Great! Week03 Done!
On to the next one.
Happy Coding! 😊
Credits
Condensed version of Lecture #3 of the Plutus Pioneer Program by Lars Brünjes on Youtube Cloned from Reddit (u/RikAlexander)