From 034e55c34fd5f523fe2bfdaf29345b9e87223231 Mon Sep 17 00:00:00 2001 From: fanghr Date: Wed, 1 Jun 2022 21:35:57 +0800 Subject: [PATCH] add samples and tests for unlocking stakes --- agora-specs/Sample/Proposal.hs | 292 ++++++++++++++++++++++++++++++++- agora-specs/Spec/Proposal.hs | 250 ++++++++++++++++++++++++++++ bench.csv | 4 + 3 files changed, 541 insertions(+), 5 deletions(-) diff --git a/agora-specs/Sample/Proposal.hs b/agora-specs/Sample/Proposal.hs index 458154f..1d352b9 100644 --- a/agora-specs/Sample/Proposal.hs +++ b/agora-specs/Sample/Proposal.hs @@ -19,6 +19,14 @@ module Sample.Proposal ( advanceFinishedPropsoal, advanceProposalInsufficientVotes, advancePropsoalWithInvalidOutputStake, + voterUnlockStakeAndRetractVotesWhile, + voterUnlockStakeWhile, + creatorRetractVotesWhile, + creatorUnlockStakeWhile, + unlockStakeAndRetractVotesUsingIrrelevantStakeWhile, + unlockStakeUsingIrrelevantStakeWhile, + unlockStakeProposalId, + unlockStake, ) where import Agora.Governor (GovernorDatum (..)) @@ -78,17 +86,14 @@ import PlutusLedgerApi.V1.Value qualified as Value ( assetClassValue, singleton, ) -import PlutusTx.AssocMap qualified as AssocMap ( - Map, - empty, - fromList, - ) +import PlutusTx.AssocMap qualified as AssocMap import Sample.Shared ( govValidatorHash, minAda, proposal, proposalPolicySymbol, proposalStartingTimeFromTimeRange, + proposalValidatorAddress, proposalValidatorHash, signer, signer2, @@ -808,3 +813,280 @@ advancePropsoalWithInvalidOutputStake = <> templateTxInfo.txInfoData , txInfoSignatories = [stakeOwner] } + +-------------------------------------------------------------------------------- + +-- | Create empty effects for every result tag given the votes. +emptyEffectFor :: + ProposalVotes -> + AssocMap.Map ResultTag (AssocMap.Map ValidatorHash DatumHash) +emptyEffectFor (ProposalVotes vs) = + AssocMap.fromList $ + map (,AssocMap.empty) (AssocMap.keys vs) + +-- | The proposal id shared by all the samples relate to unlocking stake. +unlockStakeProposalId :: ProposalId +unlockStakeProposalId = ProposalId 0 + +-- | A 'ProposalVotes' that has only two options, serves as a template for unlokcing stake samples. +unlockStakePropsoalVotesTemplate :: ProposalVotes +unlockStakePropsoalVotesTemplate = + ProposalVotes $ + AssocMap.fromList + [ (ResultTag 0, 0) + , (ResultTag 1, 0) + ] + +-- | Create a 'TxInfo' that unlocks a stake from a proposal. For internal use only. +mkUnlockStakeTxInfo :: + -- | The current state of the proposal. + ProposalStatus -> + -- | The votes of the input propsoal + ProposalVotes -> + -- | The votes of the output proposal. + ProposalVotes -> + -- | Stake amount. + Integer -> + -- | Retract from option. + [ProposalLock] -> + -- | The locks of output stake. + [ProposalLock] -> + TxInfo +mkUnlockStakeTxInfo + status + votesBefore + votesAfter + stakedAmount + locksBefore + locksAfter = + let stakeOwner = signer + + stakeInputDatum' :: StakeDatum + stakeInputDatum' = + StakeDatum + { stakedAmount = Tagged stakedAmount + , owner = stakeOwner + , lockedBy = locksBefore + } + + stakeOutputDatum' :: StakeDatum + stakeOutputDatum' = + stakeInputDatum' + { lockedBy = locksAfter + } + + --- + + effects = emptyEffectFor votesBefore + + proposalInputDatum' :: ProposalDatum + proposalInputDatum' = + ProposalDatum + { proposalId = unlockStakeProposalId + , effects = effects + , status = status + , cosigners = [signer] + , thresholds = def + , votes = votesBefore + , timingConfig = def + , startingTime = ProposalStartingTime 0 + } + + proposalOutputDatum' :: ProposalDatum + proposalOutputDatum' = + proposalInputDatum' + { votes = votesAfter + } + + --- + + sst = Value.assetClassValue stakeAssetClass 1 + + stakeInputDatum :: Datum + stakeInputDatum = Datum $ toBuiltinData stakeInputDatum' + stakeInput :: TxOut + stakeInput = + TxOut + { txOutAddress = stakeAddress + , txOutValue = + mconcat + [ sst + , Value.assetClassValue (untag stake.gtClassRef) stakedAmount + , minAda + ] + , txOutDatumHash = Just $ toDatumHash stakeInputDatum + } + + stakeOutputDatum :: Datum + stakeOutputDatum = Datum $ toBuiltinData stakeOutputDatum' + stakeOutput :: TxOut + stakeOutput = + stakeInput + { txOutDatumHash = Just $ toDatumHash stakeOutputDatum + } + + --- + + pst = Value.singleton proposalPolicySymbol "" 1 + + proposalInputDatum :: Datum + proposalInputDatum = Datum $ toBuiltinData proposalInputDatum' + proposalInput :: TxOut + proposalInput = + TxOut + { txOutAddress = proposalValidatorAddress + , txOutValue = pst + , txOutDatumHash = Just $ toDatumHash proposalInputDatum + } + + --- + + proposalOutputDatum :: Datum + proposalOutputDatum = Datum $ toBuiltinData proposalOutputDatum' + proposalOutput :: TxOut + proposalOutput = + proposalInput + { txOutValue = proposalInput.txOutValue <> minAda + , txOutDatumHash = Just $ toDatumHash proposalOutputDatum + } + in TxInfo + { txInfoInputs = [TxInInfo proposalRef proposalInput, TxInInfo stakeRef stakeInput] + , txInfoOutputs = [proposalOutput, stakeOutput] + , txInfoFee = Value.singleton "" "" 2 + , txInfoMint = mempty + , txInfoDCert = [] + , txInfoWdrl = [] + , -- Time doesn't matter int this case. + txInfoValidRange = closedBoundedInterval 0 100 + , txInfoSignatories = [signer] + , txInfoData = datumPair <$> [proposalInputDatum, proposalOutputDatum, stakeInputDatum, stakeOutputDatum] + , txInfoId = "95ba4015e30aef16a3461ea97a779f814aeea6b8009d99a94add4b8293be737a" + } + +-- | How a stake has been used on a particular proposal. +data StakeUsage + = -- | The stake was spent to vote for a paraticular option. + VotedFor ResultTag + | -- | The stake was used to created the proposal. + Created + | -- | The stake has nothing to do with the proposal. + DidNothing + +-- | Create a bunch of 'ProposalLock' given the 'StakeUsgae'. +mkStakeLocks :: StakeUsage -> [ProposalLock] +mkStakeLocks (VotedFor rt) = [ProposalLock rt unlockStakeProposalId] +mkStakeLocks Created = + map (`ProposalLock` unlockStakeProposalId) $ + AssocMap.keys $ getProposalVotes unlockStakePropsoalVotesTemplate +mkStakeLocks _ = [] + +-- | Assemble the votes of the input propsoal based on 'unlockStakePropsoalVotesTemplate'. +mkVotesBefore :: + StakeUsage -> + -- | The staked amount/votes. + Integer -> + ProposalVotes +mkVotesBefore (VotedFor rt) vc = + ProposalVotes $ + updateMap (Just . const vc) rt $ + getProposalVotes unlockStakePropsoalVotesTemplate +mkVotesBefore _ vc = mkVotesBefore (VotedFor $ ResultTag 0) vc + +{- | Create a 'TxInfo' that unlocks the stake from the proposal. + The last parameter controls whether votes should be retracted or not. +-} +unlockStake :: + -- | The status of both the input and output propsoals. + ProposalStatus -> + StakeUsage -> + -- | Staked amount/vote count. + Integer -> + -- | Should we retract votes? + Bool -> + TxInfo +unlockStake ps su staked shouldRetract = + let votesBefore = mkVotesBefore su staked + votesAfter = + if shouldRetract + then unlockStakePropsoalVotesTemplate + else votesBefore + + locksBefore = mkStakeLocks su + locksAfter = [] + in mkUnlockStakeTxInfo + ps + votesBefore + votesAfter + staked + locksBefore + locksAfter + +{- | Create a 'TxInfo' that unlocks a stake which is used to vote on the proposal. + Correct count of votes is also retracted. The 'TxInfo' is valid only if the given + proposal status is 'VotingReady'. +-} +voterUnlockStakeAndRetractVotesWhile :: ProposalStatus -> TxInfo +voterUnlockStakeAndRetractVotesWhile ps = + unlockStake + ps + (VotedFor $ ResultTag 0) + 42 + True + +{- | Create a 'TxInfo' that unlocks a stake which is used to vote on the proposal + without retracting the votes, given the status of the proposal. + + The 'TxInfo' is valid only if the status of the propsoal is either 'Locked' + or 'Finished'. +-} +voterUnlockStakeWhile :: ProposalStatus -> TxInfo +voterUnlockStakeWhile ps = + unlockStake + ps + (VotedFor $ ResultTag 0) + 42 + False + +{- | Create an invalid 'TxInfo' that retracts votes using the stake + that is used to create the proposal. +-} +creatorRetractVotesWhile :: ProposalStatus -> TxInfo +creatorRetractVotesWhile ps = + unlockStake + ps + Created + 42 + True + +{- | Create a 'TxInfo' to unlock the stake that is used to create the propsoal. + The 'TxInfo' is valid only if the given proposal status is 'Finished'. +-} +creatorUnlockStakeWhile :: ProposalStatus -> TxInfo +creatorUnlockStakeWhile ps = + unlockStake + ps + Created + 42 + False + +{- | Create an invalid 'TxInfo' that tries to retract votes and also unlock a stake + which is not locked by the proposal, given the status of the proposal. +-} +unlockStakeAndRetractVotesUsingIrrelevantStakeWhile :: ProposalStatus -> TxInfo +unlockStakeAndRetractVotesUsingIrrelevantStakeWhile ps = + unlockStake + ps + DidNothing + 42 + True + +{- | Create an invalid 'TxInfo' that tries to unlock a stake which is not locked by the proposal, + given the status of the proposal. +-} +unlockStakeUsingIrrelevantStakeWhile :: ProposalStatus -> TxInfo +unlockStakeUsingIrrelevantStakeWhile ps = + unlockStake + ps + DidNothing + 42 + False diff --git a/agora-specs/Spec/Proposal.hs b/agora-specs/Spec/Proposal.hs index 39250cc..579a8dc 100644 --- a/agora-specs/Spec/Proposal.hs +++ b/agora-specs/Spec/Proposal.hs @@ -53,10 +53,16 @@ import Sample.Proposal qualified as Proposal ( advanceProposalSuccess, advancePropsoalWithInvalidOutputStake, cosignProposal, + creatorRetractVotesWhile, + creatorUnlockStakeWhile, proposalCreation, proposalRef, stakeRef, + unlockStakeAndRetractVotesUsingIrrelevantStakeWhile, + unlockStakeUsingIrrelevantStakeWhile, voteOnProposal, + voterUnlockStakeAndRetractVotesWhile, + voterUnlockStakeWhile, ) import Sample.Shared (signer, signer2) import Sample.Shared qualified as Shared (proposal, stake) @@ -356,5 +362,249 @@ specs = (Spending Proposal.proposalRef) ) ] + , group + "unlocking" + [ group + "legal" + [ validatorSucceedsWith + "retract votes and unlock stake while voting" + (proposalValidator Shared.proposal) + ( ProposalDatum + { proposalId = ProposalId 0 + , effects = + AssocMap.fromList + [ (ResultTag 0, AssocMap.empty) + , (ResultTag 1, AssocMap.empty) + ] + , status = VotingReady + , cosigners = [signer] + , thresholds = def + , votes = + ProposalVotes + ( AssocMap.fromList + [ (ResultTag 0, 42) + , (ResultTag 1, 0) + ] + ) + , timingConfig = def + , startingTime = ProposalStartingTime 0 + } + ) + (Unlock (ResultTag 0)) + ( ScriptContext + (Proposal.voterUnlockStakeAndRetractVotesWhile VotingReady) + (Spending Proposal.proposalRef) + ) + , validatorSucceedsWith + "unlock the stake that has been used to create the proposal" + (proposalValidator Shared.proposal) + ( ProposalDatum + { proposalId = ProposalId 0 + , effects = + AssocMap.fromList + [ (ResultTag 0, AssocMap.empty) + , (ResultTag 1, AssocMap.empty) + ] + , status = Finished + , cosigners = [signer] + , thresholds = def + , votes = + ProposalVotes + ( AssocMap.fromList + [ (ResultTag 0, 42) + , (ResultTag 1, 0) + ] + ) + , timingConfig = def + , startingTime = ProposalStartingTime 0 + } + ) + (Unlock (ResultTag 0)) + ( ScriptContext + (Proposal.creatorUnlockStakeWhile Finished) + (Spending Proposal.proposalRef) + ) + , group "unlock stake after voting" $ + map + ( \ps -> + validatorSucceedsWith + (show ps) + (proposalValidator Shared.proposal) + ( ProposalDatum + { proposalId = ProposalId 0 + , effects = + AssocMap.fromList + [ (ResultTag 0, AssocMap.empty) + , (ResultTag 1, AssocMap.empty) + ] + , status = ps + , cosigners = [signer] + , thresholds = def + , votes = + ProposalVotes + ( AssocMap.fromList + [ (ResultTag 0, 42) + , (ResultTag 1, 0) + ] + ) + , timingConfig = def + , startingTime = ProposalStartingTime 0 + } + ) + (Unlock (ResultTag 0)) + ( ScriptContext + (Proposal.voterUnlockStakeWhile ps) + (Spending Proposal.proposalRef) + ) + ) + [Locked, Finished] + ] + , group + "illegal" + [ group "retract votes while the proposal is not voting ready" $ + map + ( \ps -> + validatorFailsWith + (show ps) + (proposalValidator Shared.proposal) + ( ProposalDatum + { proposalId = ProposalId 0 + , effects = + AssocMap.fromList + [ (ResultTag 0, AssocMap.empty) + , (ResultTag 1, AssocMap.empty) + ] + , status = ps + , cosigners = [signer] + , thresholds = def + , votes = + ProposalVotes + ( AssocMap.fromList + [ (ResultTag 0, 42) + , (ResultTag 1, 0) + ] + ) + , timingConfig = def + , startingTime = ProposalStartingTime 0 + } + ) + (Unlock (ResultTag 0)) + ( ScriptContext + (Proposal.voterUnlockStakeAndRetractVotesWhile ps) + (Spending Proposal.proposalRef) + ) + ) + [Draft, Locked, Finished] + , group + "irrelevant stake" + $ foldMap + ( \(f, s) -> + map + ( \ps -> + validatorFailsWith + (s <> " (" <> show ps <> ")") + (proposalValidator Shared.proposal) + ( ProposalDatum + { proposalId = ProposalId 0 + , effects = + AssocMap.fromList + [ (ResultTag 0, AssocMap.empty) + , (ResultTag 1, AssocMap.empty) + ] + , status = ps + , cosigners = [signer] + , thresholds = def + , votes = + ProposalVotes + ( AssocMap.fromList + [ (ResultTag 0, 42) + , (ResultTag 1, 0) + ] + ) + , timingConfig = def + , startingTime = ProposalStartingTime 0 + } + ) + (Unlock (ResultTag 0)) + ( ScriptContext + (f ps) + (Spending Proposal.proposalRef) + ) + ) + [Draft, VotingReady, Locked, Finished] + ) + [ (Proposal.unlockStakeAndRetractVotesUsingIrrelevantStakeWhile, "unlock stake + retract votes") + , (Proposal.unlockStakeUsingIrrelevantStakeWhile, "unlock stake") + ] + , group "unlock stake that has been used to create the proposal before finished" $ + map + ( \ps -> + validatorFailsWith + (show ps) + (proposalValidator Shared.proposal) + ( ProposalDatum + { proposalId = ProposalId 0 + , effects = + AssocMap.fromList + [ (ResultTag 0, AssocMap.empty) + , (ResultTag 1, AssocMap.empty) + ] + , status = ps + , cosigners = [signer] + , thresholds = def + , votes = + ProposalVotes + ( AssocMap.fromList + [ (ResultTag 0, 42) + , (ResultTag 1, 0) + ] + ) + , timingConfig = def + , startingTime = ProposalStartingTime 0 + } + ) + (Unlock (ResultTag 0)) + ( ScriptContext + (Proposal.creatorUnlockStakeWhile ps) + (Spending Proposal.proposalRef) + ) + ) + [Draft, VotingReady, Locked] + , group "creator stake retract votes" $ + map + ( \ps -> + validatorFailsWith + (show ps) + (proposalValidator Shared.proposal) + ( ProposalDatum + { proposalId = ProposalId 0 + , effects = + AssocMap.fromList + [ (ResultTag 0, AssocMap.empty) + , (ResultTag 1, AssocMap.empty) + ] + , status = ps + , cosigners = [signer] + , thresholds = def + , votes = + ProposalVotes + ( AssocMap.fromList + [ (ResultTag 0, 42) + , (ResultTag 1, 0) + ] + ) + , timingConfig = def + , startingTime = ProposalStartingTime 0 + } + ) + (Unlock (ResultTag 0)) + ( ScriptContext + (Proposal.creatorRetractVotesWhile ps) + (Spending Proposal.proposalRef) + ) + ) + [Draft, VotingReady, Locked, Finished] + ] + ] ] ] diff --git a/bench.csv b/bench.csv index 9630163..32fcd3f 100644 --- a/bench.csv +++ b/bench.csv @@ -18,6 +18,10 @@ Agora/Proposal/validator/advancing/successfully advance to next state/Locked -> Agora/Proposal/validator/advancing/successfully advance to failed state: timeout/Draft -> Finished,160888965,431112,6394 Agora/Proposal/validator/advancing/successfully advance to failed state: timeout/VotingReady -> Finished,159480054,428407,6395 Agora/Proposal/validator/advancing/successfully advance to failed state: timeout/Locked -> Finished,160611032,430811,6395 +Agora/Proposal/validator/unlocking/legal/retract votes and unlock stake while voting,171592676,462566,6467 +Agora/Proposal/validator/unlocking/legal/unlock the stake that has been used to create the proposal,149988973,407906,6474 +Agora/Proposal/validator/unlocking/legal/unlock stake after voting/Locked,149056062,408201,6468 +Agora/Proposal/validator/unlocking/legal/unlock stake after voting/Finished,149056062,408201,6468 Agora/AuthorityToken/singleAuthorityTokenBurned/Correct simple,21017788,55883,806 Agora/AuthorityToken/singleAuthorityTokenBurned/Correct many inputs,33204186,88241,900 Agora/Treasury/Validator/Positive/Allows for effect changes,29938856,79744,1390