add checks to cosign

This commit is contained in:
Emily Martins 2022-04-19 22:31:38 +02:00
parent 832e15fc16
commit 1e972f1402
7 changed files with 241 additions and 44 deletions

View file

@ -13,9 +13,26 @@ module Spec.Proposal (tests) where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
import Agora.Proposal (proposalPolicy) import Agora.Proposal (
ProposalDatum (ProposalDatum),
ProposalId (ProposalId),
ProposalRedeemer (Cosign),
ProposalStatus (Draft),
ProposalVotes (ProposalVotes),
ResultTag (ResultTag),
cosigners,
effects,
proposalId,
proposalPolicy,
proposalValidator,
status,
thresholds,
votes,
)
import PlutusTx.AssocMap qualified as AssocMap
import Spec.Sample.Proposal (propThresholds, signer, signer2)
import Spec.Sample.Proposal qualified as Proposal import Spec.Sample.Proposal qualified as Proposal
import Spec.Util (policySucceedsWith) import Spec.Util (policySucceedsWith, validatorSucceedsWith)
import Test.Tasty (TestTree, testGroup) import Test.Tasty (TestTree, testGroup)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -35,4 +52,25 @@ tests =
() ()
Proposal.proposalCreation Proposal.proposalCreation
] ]
, testGroup
"validator"
[ validatorSucceedsWith
"stakeCreation"
(proposalValidator Proposal.proposal)
( ProposalDatum
{ proposalId = ProposalId 0
, effects =
AssocMap.fromList
[ (ResultTag 0, [])
, (ResultTag 1, [])
]
, status = Draft
, cosigners = [signer]
, thresholds = propThresholds
, votes = ProposalVotes AssocMap.empty
}
)
(Cosign [signer2])
(Proposal.cosignProposal [signer2])
]
] ]

View file

@ -7,13 +7,15 @@ This module tests primarily the happy path for Proposal interactions
-} -}
module Spec.Sample.Proposal ( module Spec.Sample.Proposal (
proposal, proposal,
policy, propPolicy,
policySymbol, propPolicySymbol,
validatorHashTN, propThresholds,
signer, signer,
signer2,
-- * Script contexts -- * Script contexts
proposalCreation, proposalCreation,
cosignProposal,
) where ) where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -37,11 +39,9 @@ import Plutus.V1.Ledger.Api (
TxInfo (..), TxInfo (..),
TxOut (TxOut, txOutAddress, txOutDatumHash, txOutValue), TxOut (TxOut, txOutAddress, txOutDatumHash, txOutValue),
TxOutRef (TxOutRef), TxOutRef (TxOutRef),
ValidatorHash (ValidatorHash),
) )
import Plutus.V1.Ledger.Interval qualified as Interval import Plutus.V1.Ledger.Interval qualified as Interval
import Plutus.V1.Ledger.Scripts (Validator) import Plutus.V1.Ledger.Scripts (Validator, ValidatorHash)
import Plutus.V1.Ledger.Value (AssetClass (AssetClass), TokenName (TokenName))
import Plutus.V1.Ledger.Value qualified as Value import Plutus.V1.Ledger.Value qualified as Value
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -52,13 +52,35 @@ import Agora.Governor (
governorPolicy, governorPolicy,
governorValidator, governorValidator,
) )
import Agora.Proposal import Agora.Proposal (
Proposal (..),
ProposalDatum (..),
ProposalId (..),
ProposalStatus (..),
ProposalThresholds (..),
ProposalVotes (..),
ResultTag (..),
proposalPolicy,
proposalValidator,
)
import Agora.Stake (Stake (..), stakePolicy)
import Plutarch.SafeMoney import Plutarch.SafeMoney
import Plutus.V1.Ledger.Address (scriptHashAddress)
import PlutusTx.AssocMap qualified as AssocMap import PlutusTx.AssocMap qualified as AssocMap
import Spec.Util (datumPair, toDatumHash) import Spec.Util (datumPair, toDatumHash)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
stake :: Stake
stake =
Stake
{ gtClassRef = Tagged $ Value.assetClass govSymbol ""
, proposalSTClass = Value.assetClass propPolicySymbol ""
}
stakeSymbol :: CurrencySymbol
stakeSymbol = mintingPolicySymbol $ mkMintingPolicy $ stakePolicy stake.gtClassRef
governor :: Governor governor :: Governor
governor = Governor governor = Governor
@ -76,30 +98,35 @@ proposal =
Proposal Proposal
{ governorSTAssetClass = { governorSTAssetClass =
-- TODO: if we had a governor here -- TODO: if we had a governor here
AssetClass Value.assetClass govSymbol ""
( govSymbol , stakeSTAssetClass =
, "" Value.assetClass stakeSymbol ""
)
} }
-- | 'Proposal' policy instance. -- | 'Proposal' policy instance.
policy :: MintingPolicy propPolicy :: MintingPolicy
policy = mkMintingPolicy (proposalPolicy proposal) propPolicy = mkMintingPolicy (proposalPolicy proposal)
policySymbol :: CurrencySymbol propPolicySymbol :: CurrencySymbol
policySymbol = mintingPolicySymbol policy propPolicySymbol = mintingPolicySymbol propPolicy
-- | A sample 'PubKeyHash'. -- | A sample 'PubKeyHash'.
signer :: PubKeyHash signer :: PubKeyHash
signer = "8a30896c4fd5e79843e4ca1bd2cdbaa36f8c0bc3be7401214142019c" signer = "8a30896c4fd5e79843e4ca1bd2cdbaa36f8c0bc3be7401214142019c"
-- | 'Proposal' validator instance. -- | Another sample 'PubKeyHash'.
validator :: Validator signer2 :: PubKeyHash
validator = mkValidator (proposalValidator proposal) signer2 = "8a30896c4fd5e79843e4ca1bd2cdbaa36f8c0bc3be74012141420192"
-- | 'TokenName' that represents the hash of the 'Proposal' validator. -- | 'Proposal' validator instance.
validatorHashTN :: TokenName propValidator :: Validator
validatorHashTN = let ValidatorHash vh = validatorHash validator in TokenName vh propValidator = mkValidator (proposalValidator proposal)
propValidatorHash :: ValidatorHash
propValidatorHash = validatorHash propValidator
propValidatorAddress :: Address
propValidatorAddress = scriptHashAddress propValidatorHash
propThresholds :: ProposalThresholds propThresholds :: ProposalThresholds
propThresholds = propThresholds =
@ -112,7 +139,7 @@ propThresholds =
-- | This script context should be a valid transaction. -- | This script context should be a valid transaction.
proposalCreation :: ScriptContext proposalCreation :: ScriptContext
proposalCreation = proposalCreation =
let st = Value.singleton policySymbol "" 1 -- Proposal ST let st = Value.singleton propPolicySymbol "" 1 -- Proposal ST
proposalDatum :: Datum proposalDatum :: Datum
proposalDatum = proposalDatum =
Datum Datum
@ -163,7 +190,7 @@ proposalCreation =
] ]
, txInfoOutputs = , txInfoOutputs =
[ TxOut [ TxOut
{ txOutAddress = Address (ScriptCredential $ validatorHash validator) Nothing { txOutAddress = Address (ScriptCredential $ validatorHash propValidator) Nothing
, txOutValue = , txOutValue =
mconcat mconcat
[ st [ st
@ -194,5 +221,68 @@ proposalCreation =
] ]
, txInfoId = "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be" , txInfoId = "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be"
} }
, scriptContextPurpose = Minting policySymbol , scriptContextPurpose = Minting propPolicySymbol
}
-- | This script context should be a valid transaction.
cosignProposal :: [PubKeyHash] -> ScriptContext
cosignProposal newSigners =
let st = Value.singleton propPolicySymbol "" 1 -- Proposal ST
proposalBefore :: ProposalDatum
proposalBefore =
ProposalDatum
{ proposalId = ProposalId 0
, effects =
AssocMap.fromList
[ (ResultTag 0, [])
, (ResultTag 1, [])
]
, status = Draft
, cosigners = [signer]
, thresholds = propThresholds
, votes = ProposalVotes AssocMap.empty
}
proposalAfter :: ProposalDatum
proposalAfter = proposalBefore {cosigners = newSigners <> proposalBefore.cosigners}
proposalRef = (TxOutRef "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be" 1)
in ScriptContext
{ scriptContextTxInfo =
TxInfo
{ txInfoInputs =
[ TxInInfo
proposalRef
TxOut
{ txOutAddress = propValidatorAddress
, txOutValue =
mconcat
[ st
, Value.singleton "" "" 10_000_000
]
, txOutDatumHash = Just (toDatumHash proposalBefore)
}
]
, txInfoOutputs =
[ TxOut
{ txOutAddress = Address (ScriptCredential $ validatorHash propValidator) Nothing
, txOutValue =
mconcat
[ st
, Value.singleton "" "" 10_000_000
]
, txOutDatumHash = Just (toDatumHash . Datum $ toBuiltinData proposalAfter)
}
]
, txInfoFee = Value.singleton "" "" 2
, txInfoMint = st
, txInfoDCert = []
, txInfoWdrl = []
, txInfoValidRange = Interval.always
, txInfoSignatories = newSigners
, txInfoData =
[ datumPair . Datum $ toBuiltinData proposalBefore
, datumPair . Datum $ toBuiltinData proposalAfter
]
, txInfoId = "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be"
}
, scriptContextPurpose = Spending proposalRef
} }

View file

@ -69,11 +69,12 @@ stake =
, "LQ" , "LQ"
) )
) )
, proposalSTClass = AssetClass ("", "")
} }
-- | 'Stake' policy instance. -- | 'Stake' policy instance.
policy :: MintingPolicy policy :: MintingPolicy
policy = mkMintingPolicy (stakePolicy stake) policy = mkMintingPolicy (stakePolicy stake.gtClassRef)
policySymbol :: CurrencySymbol policySymbol :: CurrencySymbol
policySymbol = mintingPolicySymbol policy policySymbol = mintingPolicySymbol policy

View file

@ -19,7 +19,7 @@ import Test.Tasty (TestTree, testGroup)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
import Agora.Stake (StakeDatum (StakeDatum), StakeRedeemer (DepositWithdraw), stakePolicy, stakeValidator) import Agora.Stake (Stake (..), StakeDatum (StakeDatum), StakeRedeemer (DepositWithdraw), stakePolicy, stakeValidator)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -36,17 +36,17 @@ tests =
"policy" "policy"
[ policySucceedsWith [ policySucceedsWith
"stakeCreation" "stakeCreation"
(stakePolicy Stake.stake) (stakePolicy Stake.stake.gtClassRef)
() ()
Stake.stakeCreation Stake.stakeCreation
, policyFailsWith , policyFailsWith
"stakeCreationWrongDatum" "stakeCreationWrongDatum"
(stakePolicy Stake.stake) (stakePolicy Stake.stake.gtClassRef)
() ()
Stake.stakeCreationWrongDatum Stake.stakeCreationWrongDatum
, policyFailsWith , policyFailsWith
"stakeCreationUnsigned" "stakeCreationUnsigned"
(stakePolicy Stake.stake) (stakePolicy Stake.stake.gtClassRef)
() ()
Stake.stakeCreationUnsigned Stake.stakeCreationUnsigned
] ]

View file

@ -249,8 +249,9 @@ PlutusTx.makeIsDataIndexed
] ]
-- | Parameters that identify the Proposal validator script. -- | Parameters that identify the Proposal validator script.
newtype Proposal = Proposal data Proposal = Proposal
{ governorSTAssetClass :: AssetClass { governorSTAssetClass :: AssetClass
, stakeSTAssetClass :: AssetClass
} }
deriving stock (Show, Eq) deriving stock (Show, Eq)
@ -442,14 +443,23 @@ proposalValidator proposal =
PSpending ((pfield @"_0" #) -> txOutRef) <- pmatch $ pfromData ctx.purpose PSpending ((pfield @"_0" #) -> txOutRef) <- pmatch $ pfromData ctx.purpose
PJust txOut <- pmatch $ findTxOutByTxOutRef # txOutRef # txInfoF.inputs PJust txOut <- pmatch $ findTxOutByTxOutRef # txOutRef # txInfoF.inputs
txOutF <- pletFields @'["address"] $ txOut txOutF <- pletFields @'["address", "value"] $ txOut
let proposalDatum :: Term _ PProposalDatum let proposalDatum :: Term _ PProposalDatum
proposalDatum = pfromData $ punsafeCoerce datum proposalDatum = pfromData $ punsafeCoerce datum
proposalRedeemer :: Term _ PProposalRedeemer proposalRedeemer :: Term _ PProposalRedeemer
proposalRedeemer = pfromData $ punsafeCoerce redeemer proposalRedeemer = pfromData $ punsafeCoerce redeemer
proposalF <- pletFields @'["cosigners"] proposalDatum proposalF <-
pletFields
@'[ "id"
, "effects"
, "status"
, "cosigners"
, "thresholds"
, "votes"
]
proposalDatum
ownAddress <- plet $ txOutF.address ownAddress <- plet $ txOutF.address
@ -475,18 +485,35 @@ proposalValidator proposal =
passert "Signatures are correctly added to cosignature list" $ passert "Signatures are correctly added to cosignature list" $
anyOutput @PProposalDatum # ctx.txInfo anyOutput @PProposalDatum # ctx.txInfo
#$ plam #$ plam
$ \_value address newProposalDatum -> P.do $ \newValue address newProposalDatum -> P.do
newProposalF <- pletFields @'["cosigners"] newProposalDatum newProposalF <-
pletFields
@'[ "id"
, "effects"
, "status"
, "cosigners"
, "thresholds"
, "votes"
]
newProposalDatum
-- This is a little sad. Can we do better by
-- building a new ProposalDatum and then comparing?
let correctDatum = let correctDatum =
foldr1 foldr1
(#&&) (#&&)
[ newProposalF.cosigners #== proposalF.cosigners [ newProposalF.cosigners #== pconcat # newSigs # proposalF.cosigners
, newProposalF.id #== proposalF.id
, newProposalF.effects #== proposalF.effects
, newProposalF.status #== proposalF.status
, newProposalF.thresholds #== proposalF.thresholds
, newProposalF.votes #== proposalF.votes
] ]
foldr1 foldr1
(#&&) (#&&)
[ ptraceIfFalse "Datum must be correct" $ correctDatum [ ptraceIfFalse "Datum must be correct" $ correctDatum
, ptraceIfFalse "Value should be correct" $ pdata txOutF.value #== pdata newValue
, ptraceIfFalse "Must be sent to Proposal's address" $ ownAddress #== pdata address , ptraceIfFalse "Must be sent to Proposal's address" $ ownAddress #== pdata address
] ]

View file

@ -70,9 +70,11 @@ import Agora.Utils (
pnotNull, pnotNull,
psingletonValue, psingletonValue,
psymbolValueOf, psymbolValueOf,
ptokenSpent,
ptxSignedBy, ptxSignedBy,
pvalueSpent, pvalueSpent,
) )
import Plutarch.Api.V1.Extra (passetClass)
import Plutarch.Numeric import Plutarch.Numeric
import Plutarch.SafeMoney ( import Plutarch.SafeMoney (
PDiscrete, PDiscrete,
@ -85,9 +87,10 @@ import Plutarch.TryFrom (PTryFrom, ptryFrom)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | Parameters for creating Stake scripts. -- | Parameters for creating Stake scripts.
newtype Stake = Stake data Stake = Stake
{ gtClassRef :: Tagged GTTag AssetClass { gtClassRef :: Tagged GTTag AssetClass
-- ^ Used when inlining the AssetClass of a 'PDiscrete' in the script code. -- ^ Used when inlining the AssetClass of a 'PDiscrete' in the script code.
, proposalSTClass :: AssetClass
} }
{- | A lock placed on a Stake datum in order to prevent {- | A lock placed on a Stake datum in order to prevent
@ -147,6 +150,9 @@ data StakeRedeemer
-- always allowed to have votes retracted and won't affect the Proposal datum, -- always allowed to have votes retracted and won't affect the Proposal datum,
-- allowing 'Stake's to be unlocked. -- allowing 'Stake's to be unlocked.
RetractVotes [ProposalLock] RetractVotes [ProposalLock]
| -- | The owner can consume stake if nothing is changed about it.
-- If the proposal token moves, this is equivalent to the owner consuming it.
WitnessStake
deriving stock (Show, GHC.Generic) deriving stock (Show, GHC.Generic)
PlutusTx.makeIsDataIndexed PlutusTx.makeIsDataIndexed
@ -155,6 +161,7 @@ PlutusTx.makeIsDataIndexed
, ('Destroy, 1) , ('Destroy, 1)
, ('PermitVote, 2) , ('PermitVote, 2)
, ('RetractVotes, 3) , ('RetractVotes, 3)
, ('WitnessStake, 4)
] ]
-- | Haskell-level datum for Stake scripts. -- | Haskell-level datum for Stake scripts.
@ -207,6 +214,7 @@ data PStakeRedeemer (s :: S)
PDestroy (Term s (PDataRecord '[])) PDestroy (Term s (PDataRecord '[]))
| PPermitVote (Term s (PDataRecord '["lock" ':= PProposalLock])) | PPermitVote (Term s (PDataRecord '["lock" ':= PProposalLock]))
| PRetractVotes (Term s (PDataRecord '["locks" ':= PBuiltinList (PAsData PProposalLock)])) | PRetractVotes (Term s (PDataRecord '["locks" ':= PBuiltinList (PAsData PProposalLock)]))
| PWitnessStake (Term s (PDataRecord '[]))
deriving stock (GHC.Generic) deriving stock (GHC.Generic)
deriving anyclass (Generic) deriving anyclass (Generic)
deriving anyclass (PIsDataRepr) deriving anyclass (PIsDataRepr)
@ -263,8 +271,8 @@ deriving via (DerivePConstantViaData ProposalLock PProposalLock) instance (PCons
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | Policy for Stake state threads. -- | Policy for Stake state threads.
stakePolicy :: Stake -> ClosedTerm PMintingPolicy stakePolicy :: Tagged GTTag AssetClass -> ClosedTerm PMintingPolicy
stakePolicy stake = stakePolicy gtClassRef =
plam $ \_redeemer ctx' -> P.do plam $ \_redeemer ctx' -> P.do
ctx <- pletFields @'["txInfo", "purpose"] ctx' ctx <- pletFields @'["txInfo", "purpose"] ctx'
txInfo <- plet $ ctx.txInfo txInfo <- plet $ ctx.txInfo
@ -325,7 +333,7 @@ stakePolicy stake =
# 1 # 1
let expectedValue = let expectedValue =
paddValue paddValue
# (pdiscreteValue' stake.gtClassRef # stakeDatum.stakedAmount) # (pdiscreteValue' gtClassRef # stakeDatum.stakedAmount)
# stValue # stValue
let ownerSignsTransaction = let ownerSignsTransaction =
ptxSignedBy ptxSignedBy
@ -339,7 +347,7 @@ stakePolicy stake =
foldr1 foldr1
(#&&) (#&&)
[ pgeqByClass' (AssetClass ("", "")) # value # expectedValue [ pgeqByClass' (AssetClass ("", "")) # value # expectedValue
, pgeqByClass' (untag stake.gtClassRef) , pgeqByClass' (untag gtClassRef)
# value # value
# expectedValue # expectedValue
, pgeqByClass , pgeqByClass
@ -381,7 +389,7 @@ stakeValidator stake =
-- Whether the owner signs this transaction or not. -- Whether the owner signs this transaction or not.
ownerSignsTransaction <- plet $ ptxSignedBy # ctx.txInfo # stakeDatum.owner ownerSignsTransaction <- plet $ ptxSignedBy # ctx.txInfo # stakeDatum.owner
stCurrencySymbol <- plet $ pconstant $ mintingPolicySymbol $ mkMintingPolicy (stakePolicy stake) stCurrencySymbol <- plet $ pconstant $ mintingPolicySymbol $ mkMintingPolicy (stakePolicy stake.gtClassRef)
mintedST <- plet $ psymbolValueOf # stCurrencySymbol # txInfoF.mint mintedST <- plet $ psymbolValueOf # stCurrencySymbol # txInfoF.mint
spentST <- plet $ psymbolValueOf # stCurrencySymbol #$ pvalueSpent # txInfoF.inputs spentST <- plet $ psymbolValueOf # stCurrencySymbol #$ pvalueSpent # txInfoF.inputs
@ -414,6 +422,35 @@ stakeValidator stake =
-- TODO: check proposal constraints -- TODO: check proposal constraints
popaque (pconstant ()) popaque (pconstant ())
-------------------------------------------------------------------------- --------------------------------------------------------------------------
PWitnessStake _ -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
let AssetClass (propCs, propTn) = stake.proposalSTClass
propAssetClass = passetClass # pconstant propCs # pconstant propTn
proposalTokenMoved =
ptokenSpent
# propAssetClass
# txInfoF.inputs
passert
"Owner signs this transaction OR proposal token is spent"
(ownerSignsTransaction #|| proposalTokenMoved)
passert "A UTXO must exist with the correct output" $
anyOutput @PStakeDatum # txInfo
#$ plam
$ \value address newStakeDatum' -> P.do
let isScriptAddress = pdata address #== ownAddress
let correctOutputDatum = pdata newStakeDatum' #== pdata stakeDatum'
let valueCorrect = pdata continuingValue #== pdata value
foldr1
(#&&)
[ ptraceIfFalse "isScriptAddress" isScriptAddress
, ptraceIfFalse "correctOutputDatum" correctOutputDatum
, ptraceIfFalse "valueCorrect" valueCorrect
]
popaque (pconstant ())
PDepositWithdraw r -> P.do PDepositWithdraw r -> P.do
passert "ST at inputs must be 1" $ passert "ST at inputs must be 1" $
spentST #== 1 spentST #== 1

View file

@ -287,7 +287,11 @@ pfindTxInByTxOutRef = phoistAcyclic $
pnotNull :: forall list a. PIsListLike list a => Term _ (list a :--> PBool) pnotNull :: forall list a. PIsListLike list a => Term _ (list a :--> PBool)
pnotNull = phoistAcyclic $ plam $ pelimList (\_ _ -> pcon PTrue) (pcon PFalse) pnotNull = phoistAcyclic $ plam $ pelimList (\_ _ -> pcon PTrue) (pcon PFalse)
-- | Check if a particular asset class has been spent in the input list. {- | Check if a particular asset class has been spent in the input list.
When using this as an authority check, you __MUST__ ensure the authority
knows how to ensure its end of the contract.
-}
ptokenSpent :: forall {s :: S}. Term s (PAssetClass :--> PBuiltinList (PAsData PTxInInfo) :--> PBool) ptokenSpent :: forall {s :: S}. Term s (PAssetClass :--> PBuiltinList (PAsData PTxInInfo) :--> PBool)
ptokenSpent = ptokenSpent =
plam $ \tokenClass inputs -> plam $ \tokenClass inputs ->