Plutus Pioneer Program – Lecture #3
Plutus 版本更新(版本連結)
影片中指定了新的 commit 需要更新 plutus。
新版 plutus 做了一些更新:
- 原本的
mkValidator
函數參數改變- 原本:datum, redeemer,
ValidatorCtx
- 更新:datum, redeemer,
ScriptContext
- 原本:datum, redeemer,
data ScriptContext = ScriptContext{
scriptContextTxInfo :: TxInfo,
scriptContextPurpose :: ScriptPurpose
}
- 建構 script address 呼叫的建構子改變,更新後不需要計算
valHash
valHashvalidator :: Validator
validator = Scripts.validatorScript inst
scrAddress :: Ledger.Address
scrAddress = scriptAddress validator
- 新增了手續費功能。但目前的手續費固定為 10 Lovelace,並不符合未來在區塊鏈上的真實情況。在未來,手續費會依記憶體用量,驗證者所耗費的執行時間來估算手續費。
介紹 Script context
Week 02 介紹中沒有提到 validation 中的第三個參數:合約內容(Script Context),只有提到前兩個參數 datum 還有 redeemer。
從 plutus-ledger-api/src/Plutus/V1/Ledger/Contexts.hs
可以看到
data ScriptContext =
ScriptContext{
scriptContextTxInfo :: TxInfo,
scriptContextPurpose :: ScriptPurpose
}
ScriptContext
有兩個參數。
ScriptPurpose
用來描述 script 的用途。目前我們所構造的 script 都是「支付用途(Spending purpose)」,另外還有其他種用途:- Minting:用來在區塊鏈上鑄造自己的地方貨幣
- Rewarding:用於質押(staking)獎勵
- Certifying:用於質押證明(delegation certificates)
data ScriptPurpose
= Minting CurrencySymbol
| Spending TxOutRef
| Rewarding StakingCredential
| Certifying DCert
TxInfo
用來記錄合約相關的基本參數。
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)
時間區間的管理 Time Handling
相較於以太坊智能合約,Cardano 智能合約最大的優勢就是合約不需要透過嘗試寫到區塊鏈上來判斷是否成功。Cardano 在錢包(wallet)就可以完成驗證(validation),驗證失敗的 transaction 不需要支付額外的 fee。
而時戳(Timestamp)是在判斷合約中一個很重要的參數。就像前幾個例子,都有對 time slot 的操作。所以在 TxInfo
之中就會給出一個 valid 的時間區間,在任何 script 被執行之前,當前時間是否符合 script 中所指定的時間區間就會先被檢查。
data TxInfo = TxInfo
{
-- ...
, txInfoValidRange :: SlotRange -- ^ The valid range for the transaction.
-- ...
}
再更詳細 trace 下去。可以在同一個資料夾下找到 Slot.hs
。
-- | The slot number. This is a good proxy for time, since on the Cardano blockchain
-- slots pass at a constant rate.
newtype Slot = Slot { getSlot :: Integer }
deriving stock (Haskell.Eq, Haskell.Ord, Show, Generic)
deriving anyclass (FromJSON, FromJSONKey, ToJSON, ToJSONKey, NFData)
deriving newtype (Haskell.Num, AdditiveSemigroup, AdditiveMonoid, AdditiveGroup, Enum, Eq, Ord, Real, Integral, Serialise, Hashable, PlutusTx.IsData)
SlotRange
是一個 Interval
-- | An 'Interval' of 'Slot's.
type SlotRange = Interval Slot
在 Interval.hs
中可以看到定義
-- | An interval of @a@s.
--
-- The interval may be either closed or open at either end, meaning
-- that the endpoints may or may not be included in the interval.
--
-- The interval can also be unbounded on either side.
data Interval a = Interval { ivFrom :: LowerBound a, ivTo :: UpperBound a }
deriving stock (Haskell.Eq, Haskell.Ord, Show, Generic)
deriving anyclass (FromJSON, ToJSON, Serialise, Hashable, NFData)
但是使用者不會需要更動資料結構的內部邏輯。想要建構時間區間的話只要呼叫建構子即可。(interval is inclusive)
{-# INLINABLE interval #-}
-- | @interval a b@ includes all values that are greater than or equal
-- to @a@ and smaller than @b@. Therefore it includes @a@ but not it
-- does not include @b@.
interval :: a -> a -> Interval a
interval s s' = Interval (lowerBound s) (upperBound s')
另外在下面還可以看到一些建構開區間的建構子:
{-# INLINABLE from #-}
-- | @from a@ is an 'Interval' that includes all values that are
-- greater than or equal to @a@.
from :: a -> Interval a
from s = Interval (lowerBound s) (UpperBound PosInf True)
{-# INLINABLE to #-}
-- | @to a@ is an 'Interval' that includes all values that are
-- smaller than @a@.
to :: a -> Interval a
to s = Interval (LowerBound NegInf True) (upperBound s)
{-# INLINABLE always #-}
-- | An 'Interval' that covers every slot.
always :: Interval a
always = Interval (LowerBound NegInf True) (UpperBound PosInf True)
{-# INLINABLE never #-}
-- | An 'Interval' that is empty.
never :: Interval a
never = Interval (LowerBound PosInf True) (UpperBound NegInf True)
並有其他相關函數可以判斷 intervals 之間或是 slot 對 interval 的關係。
{-# INLINABLE member #-}
-- | Check whether a value is in an interval.
member :: Ord a => a -> Interval a -> Bool
member a i = i `contains` singleton a
{-# INLINABLE overlaps #-}
-- | Check whether two intervals overlap, that is, whether there is a value that
-- is a member of both intervals.
overlaps :: Ord a => Interval a -> Interval a -> Bool
overlaps l r = isEmpty (l `intersection` r)
{-# INLINABLE intersection #-}
-- | 'intersection a b' is the largest interval that is contained in 'a' and in
-- 'b', if it exists.
intersection :: Ord a => Interval a -> Interval a -> Interval a
intersection (Interval l1 h1) (Interval l2 h2) = Interval (max l1 l2) (min h1 h2)
{-# INLINABLE hull #-}
-- | 'hull a b' is the smallest interval containing 'a' and 'b'.
hull :: Ord a => Interval a -> Interval a -> Interval a
hull (Interval l1 h1) (Interval l2 h2) = Interval (min l1 l2) (max h1 h2)
{-# INLINABLE contains #-}
-- | @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@.
contains :: Ord a => Interval a -> Interval a -> Bool
contains (Interval l1 h1) (Interval l2 h2) = l1 <= l2 && h2 <= h1
{-# INLINABLE isEmpty #-}
-- | Check if an 'Interval' is empty.
isEmpty :: Ord a => Interval a -> Bool
isEmpty (Interval (LowerBound v1 in1) (UpperBound v2 in2)) = case v1 `compare` v2 of
LT -> False
GT -> True
EQ -> not (in1 && in2)
{-# INLINABLE before #-}
-- | Check if a value is earlier than the beginning of an 'Interval'.
before :: Ord a => a -> Interval a -> Bool
before h (Interval f _) = lowerBound h < f
{-# INLINABLE after #-}
-- | Check if a value is later than the end of a 'Interval'.
after :: Ord a => a -> Interval a -> Bool
after h (Interval _ t) = upperBound h > t
基本練習
Vesting.hs
這個例子在描述腳本中的第一,三個參數 Datum
, ScriptContext
的操作。
使用者情境:你想幫自己小孩從小開始存 ada,你想要存到他 18 歲時「只有他」可以去動用這個 ada account。也就是到了某個時間點,被存到合約的 ada 會被釋放並且可以被使用。
定義自己的資料結構: VestingDatum
- 受益人(beneficiary):到時候可以取得腳本(script)中的金錢的錢包
- 到期期限(deadline):到了某個時刻腳本才被解鎖
data VestingDatum = VestingDatum
{ beneficiary :: PubKeyHash
, deadline :: Slot
} deriving Show
這裡逐漸看到 Haskell 的好處,函數式程式語言的邏輯真的是一目了然呢!
影片中的 coding style 挺良好的,使得邏輯具有相當的可讀性。Script 會檢查兩個條件:
- signature 檢查參與者是否為 datum 中紀錄的 beneficiary 本人
- deadline 是否已經到達
這裡取用了 ScriptContext
資料結構底下的 TxInfo
。
{-# INLINABLE mkValidator #-}
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 inf
檢查參與者是否為 datum 中紀錄的 beneficiary 本人
在 TxInfo 中有儲存一個 list of signature 在 txInfoSignatories
中。
elem
是用來檢查提供的元素是否在 list 之中。(http://zvon.org/other/haskell/Outputprelude/elem_f.html)
Eq a => a -> [a] -> Bool
參與者( txInfoSignatorie
)中如果有 beneficiary
的話,就 return True。
checkSig = beneficiary dat `elem` txInfoSignatories info
deadline 是否已經到達
這個就很簡單,呼叫 Interval
相關的函數即可。
再來看到實際動作(give, grab)那裡。
- 動作 give 讓 tx 持有 datum 以及
Ada.lovelaceValueOf $ gpAmount gp
大小的 Adatx = mustPayToTheScript dat $ Ada.lovelaceValueOf $ gpAmount gp
- 動作 grab 可以看到
isSuitable
函數中吃進去三個參數:PubKeyHash(呼叫 grab 動作的 wallet 的 public key), Slot(時間),需要進行檢視的 transaction output。- 第一個 if 檢查是否存在 datum
- 第二個 if 檢查存在的 datum 是否可以被 serialize 成想要的結構(VestingDatum)。(在最剛開始已經用 Haskell template programming 來生成
fromData
的建構子了) - 第三個 if 取用 Datum 中的參數
beneficiary
與deadline
來做邏輯判斷
isSuitable :: PubKeyHash -> Slot -> TxOutTx -> Bool isSuitable pkh now o = case txOutDatumHash $ txOutTxOut o of Nothing -> False Just h -> case Map.lookup h $ txData $ txOutTxTx o of Nothing -> False Just (Datum e) -> case PlutusTx.fromData e of Nothing -> False Just d -> beneficiary d == pkh && deadline d <= now
Parameterized.hs
這裡讓 mkValidator
比原本多拿一個參數(原本僅提供純資料:datum, redeemer, script),讓最開頭多一個 Parameter 結構來存屬於 mkValidator 這個動作的參數。
可以想像成是你會想要有同樣的資料被傳入,但你可以再額外指定對這些資料要做什麼動作。讓你的 mkValidator
函數可以在 runtime 做不一樣的互動。
首先修改 mkValidator
定義:
{-# INLINABLE mkValidator #-}
mkValidator :: VestingParam -> () -> () -> ScriptContext -> Bool
mkValidator p () () ctx =
-- ...
這時在宣告 script instance 時,變得也需要把參數傳來構造 script 。
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 @() @()
關於 “Lift” 這個動作,是讓程式可以在 runtime 把 Haskell value 提升為 Plutus script value,因為並不是所有 Haskell 資料值都可以在 Plutus 底下被使用,所以需要額外這個動作來讓資料可以被 Plutus 使用。( plutus-tx/src/PlutusTx/Lift/Class.hs
)
-- | Class for types which can be lifted into Plutus IR.
-- | Instances should be derived, do not write your
-- | own instance!
class Lift uni a where
-- | Get a Plutus IR term corresponding to the given value.
lift :: a -> RTCompile uni fun (Term TyName Name uni fun ())
PlutusTx.liftCode
這個動作以 Lift 為基礎( plutus-tx/src/PlutusTx/Lift.hs
),會是我們主要呼叫,把 Haskell value 提升為 Plutus script value 的轉換函數。
-- | Get a Plutus Core program corresponding to the given value
-- | as a 'CompiledCodeIn', throwing any errors that occur as
-- | exceptions and ignoring fresh names.
liftCode
:: (Lift.Lift uni a, Throwable uni fun, PLC.ToBuiltinMeaning uni fun)
=> a -> CompiledCodeIn uni fun a
liftCode x = unsafely $ safeLiftCode x
現在回去看到剛剛宣告的 script instance 中的動作。( Parameterized.hs
第 65 行)用 liftCode
來提昇參數為 Plutus script value 後,使用 applyCode
來把資料餵給 template 產生的 mkValidator
函數。
(
$$(PlutusTx.compile [|| mkValidator ||])
`PlutusTx.applyCode` PlutusTx.liftCode p
)
另外對自定義的資料結構,有 PlutusTx.makeLift
來做像是之前 PlutusTx.unstableMakeIsData
的功能,使用 template 方式幫新增的結構做建構子。( Parameterized.hs
第 41 行)
PlutusTx.makeLift ''VestingParam
而原本 validator 單純是讓 Scripts.validatorScript
宣告一個資料 inst
validator :: Validator
validator = Scripts.validatorScript inst
現在因為 inst
需要參數,所以需要 .
operator。 .
operator 是 function composition 的 operator。 視 Scripts.validatorScript 還有 inst 為兩個函數,並把他們 compose 起來。 ( Parameterized.hs
70 ~ 74 行)
validator :: VestingParam -> Validator
validator = Scripts.validatorScript . inst
scrAddress :: VestingParam -> Ledger.Address
scrAddress = scriptAddress . validator
可以在下面 give, grab 動作中,
- give 傳入
p
來創造 script instance (line 94) - grab
- 將
p
傳給utxoAt
來找到屬於該 deadline 的 UTXO script (line 112)
- 將
p
傳到validator
中來判斷是否符合checkSig
還有checkDeadline
(line 118)
- 將
把合約腳本參數化的好處,一方面可以在 runtime 接收不同動作,另一方面 validation 本身就不用另外構造函數做判斷,而是在建立合約時就檢查是否可以成功執行。(correct by construction)
Homework 1
練習判斷邏輯的部分,對 datum 做相應的操作。
Homework 2
練習 parameterized script,並實作 Vesting.hs 中的邏輯。
(文章歡迎轉載,請註明出處為 eopxd.com)