Skip to content

Commit 0a4d529

Browse files
committed
Refactor VoteDB to enforce single-cert-per-round invariant
1 parent c93a0f2 commit 0a4d529

File tree

4 files changed

+315
-206
lines changed

4 files changed

+315
-206
lines changed

ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Block/SupportsPeras.hs

Lines changed: 41 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@ module Ouroboros.Consensus.Block.SupportsPeras
2727
, PerasCfg (..)
2828
, ValidatedPerasCert (..)
2929
, ValidatedPerasVote (..)
30-
, PerasVoteAggregate (..)
31-
, PerasVoteAggregateStatus (..)
32-
, pvasMaybeCert
33-
, emptyPerasVoteAggregateStatus
34-
, updatePerasVoteAggregate
35-
, updatePerasVoteAggregateStatus
3630
, makePerasCfg
3731
, HasId (..)
3832
, HasPerasCertRound (..)
@@ -132,6 +126,11 @@ instance Condense PerasWeight where
132126
boostPerCert :: PerasWeight
133127
boostPerCert = PerasWeight 15
134128

129+
-- | TODO: this may become a Ledger protocol parameter
130+
-- see https://github.com/tweag/cardano-peras/issues/119
131+
quorumThreshold :: PerasVoteStake
132+
quorumThreshold = PerasVoteStake 0.75
133+
135134
-- TODO using 'Validated' for extra safety? Or some @.Unsafe@ module?
136135
data ValidatedPerasCert blk = ValidatedPerasCert
137136
{ vpcCert :: !(PerasCert blk)
@@ -202,6 +201,8 @@ class
202201

203202
data PerasValidationErr blk
204203

204+
data PerasForgeErr blk
205+
205206
validatePerasCert ::
206207
PerasCfg blk ->
207208
PerasCert blk ->
@@ -213,105 +214,23 @@ class
213214
PerasVoteStakeDistr ->
214215
Either (PerasValidationErr blk) (ValidatedPerasVote blk)
215216

216-
const_PERAS_QUORUM_THRESHOLD :: PerasVoteStake
217-
const_PERAS_QUORUM_THRESHOLD = PerasVoteStake 0.75
218-
219-
data PerasVoteAggregate blk = PerasVoteAggregate
220-
{ pvaTarget :: !(PerasVoteTarget blk)
221-
, pvaVotes :: !(Map (IdOf (PerasVote blk)) (WithArrivalTime (ValidatedPerasVote blk)))
222-
, pvaTotalStake :: !PerasVoteStake
223-
}
224-
deriving stock (Generic, Eq, Ord, Show)
225-
deriving anyclass NoThunks
226-
227-
data PerasVoteAggregateStatus blk
228-
= PerasVoteAggregateQuorumNotReached {pvasVoteAggregate :: !(PerasVoteAggregate blk)}
229-
| PerasVoteAggregateQuorumReachedAlready
230-
{pvasVoteAggregate :: !(PerasVoteAggregate blk), pvasCert :: ValidatedPerasCert blk}
231-
deriving stock (Generic, Eq, Ord, Show)
232-
deriving anyclass NoThunks
233-
234-
pvasMaybeCert :: PerasVoteAggregateStatus blk -> Maybe (ValidatedPerasCert blk)
235-
pvasMaybeCert aggr = case aggr of
236-
PerasVoteAggregateQuorumNotReached{} -> Nothing
237-
PerasVoteAggregateQuorumReachedAlready{pvasCert} -> Just pvasCert
238-
239-
emptyPerasVoteAggregateStatus :: PerasVoteTarget blk -> PerasVoteAggregateStatus blk
240-
emptyPerasVoteAggregateStatus target =
241-
PerasVoteAggregateQuorumNotReached $
242-
PerasVoteAggregate
243-
{ pvaTotalStake = PerasVoteStake 0
244-
, pvaTarget = target
245-
, pvaVotes = Map.empty
246-
}
247-
248-
-- | Add a vote to an existing aggregate if it isn't already present, and update
249-
-- the stake accordingly.
250-
-- PRECONDITION: the vote's target must match the aggregate's target.
251-
updatePerasVoteAggregate ::
252-
StandardHash blk =>
253-
PerasVoteAggregate blk ->
254-
WithArrivalTime (ValidatedPerasVote blk) ->
255-
PerasVoteAggregate blk
256-
updatePerasVoteAggregate
257-
pva@PerasVoteAggregate
258-
{ pvaTarget = (roundNo, point)
259-
, pvaVotes = existingVotes
260-
, pvaTotalStake = initialStake
261-
}
262-
vote =
263-
if not (getPerasVoteRound vote == roundNo && getPerasVoteVotedBlock vote == point)
264-
then error "updatePerasVoteAggregate: vote target does not match aggregate target"
265-
else
266-
let (pvaVotes', pvaTotalStake') =
267-
case Map.insertLookupWithKey
268-
(\_k old _new -> old)
269-
(getId vote)
270-
vote
271-
existingVotes of
272-
(Nothing, votes') ->
273-
-- key was NOT present → inserted and stake updated
274-
(votes', initialStake + vpvVoteStake (forgetArrivalTime vote))
275-
(Just _, _) ->
276-
-- key WAS already present → votes and stake unchanged
277-
(existingVotes, initialStake)
278-
in pva{pvaVotes = pvaVotes', pvaTotalStake = pvaTotalStake'}
279-
280-
updatePerasVoteAggregateStatus ::
281-
StandardHash blk =>
282-
PerasVoteAggregateStatus blk ->
283-
WithArrivalTime (ValidatedPerasVote blk) ->
284-
PerasVoteAggregateStatus blk
285-
updatePerasVoteAggregateStatus aggr vote = case aggr of
286-
PerasVoteAggregateQuorumNotReached{pvasVoteAggregate} ->
287-
let aggr' = updatePerasVoteAggregate pvasVoteAggregate vote
288-
in if pvaTotalStake aggr' >= const_PERAS_QUORUM_THRESHOLD
289-
then
290-
PerasVoteAggregateQuorumReachedAlready
291-
{ pvasVoteAggregate = aggr'
292-
, pvasCert =
293-
ValidatedPerasCert
294-
{ vpcCertBoost = boostPerCert
295-
, vpcCert = uncurry PerasCert (pvaTarget aggr')
296-
}
297-
}
298-
else PerasVoteAggregateQuorumNotReached{pvasVoteAggregate = aggr'}
299-
PerasVoteAggregateQuorumReachedAlready{pvasVoteAggregate, pvasCert} ->
300-
PerasVoteAggregateQuorumReachedAlready
301-
{ pvasVoteAggregate = updatePerasVoteAggregate pvasVoteAggregate vote
302-
, pvasCert
303-
}
217+
forgePerasCert ::
218+
PerasCfg blk ->
219+
PerasVoteTarget blk ->
220+
[ValidatedPerasVote blk] ->
221+
Either (PerasForgeErr blk) (ValidatedPerasCert blk)
304222

305223
type PerasVoteTarget blk = (PerasRoundNo, Point blk)
306224

307225
-- TODO: degenerate instance for all blks to get things to compile
308226
-- see https://github.com/tweag/cardano-peras/issues/73
309227
instance StandardHash blk => BlockSupportsPeras blk where
310-
newtype PerasCfg blk = PerasCfg
228+
data PerasCfg blk = PerasCfg
311229
{ -- TODO: eventually, this will come from the
312230
-- protocol parameters from the ledger state
313231
-- see https://github.com/tweag/cardano-peras/issues/119
314232
perasCfgWeightBoost :: PerasWeight
233+
, perasCfgQuorumThreshold :: PerasVoteStake
315234
}
316235
deriving stock (Show, Eq)
317236

@@ -336,6 +255,11 @@ instance StandardHash blk => BlockSupportsPeras blk where
336255
= PerasValidationErr
337256
deriving stock (Show, Eq)
338257

258+
data PerasForgeErr blk
259+
= PerasForgeErrMismatchedTarget
260+
| PerasForgeErrInsufficientVotes
261+
deriving stock (Show, Eq)
262+
339263
-- TODO: perform actual validation against all
340264
-- possible 'PerasValidationErr' variants
341265
-- see https://github.com/tweag/cardano-peras/issues/120
@@ -350,6 +274,27 @@ instance StandardHash blk => BlockSupportsPeras blk where
350274
let stake = getPerasVoteStakeOf stakeDistr (pvVoteVoterId vote)
351275
in Right (ValidatedPerasVote{vpvVote = vote, vpvVoteStake = stake})
352276

277+
forgePerasCert cfg target votes =
278+
let allMatchTarget = all (\v -> getPerasVoteTarget v == target) votes
279+
hasSufficientStake =
280+
let totalStake = mconcat (map vpvVoteStake votes)
281+
in totalStake >= perasCfgQuorumThreshold cfg
282+
in if not allMatchTarget
283+
then Left PerasForgeErrMismatchedTarget
284+
else
285+
if not hasSufficientStake
286+
then Left PerasForgeErrInsufficientVotes
287+
else
288+
Right
289+
ValidatedPerasCert
290+
{ vpcCert =
291+
PerasCert
292+
{ pcCertRound = fst target
293+
, pcCertBoostedBlock = snd target
294+
}
295+
, vpcCertBoost = perasCfgWeightBoost cfg
296+
}
297+
353298
instance HasId (PerasCert blk) where
354299
type IdOf (PerasCert blk) = PerasRoundNo
355300
getId = pcCertRound
@@ -402,6 +347,7 @@ makePerasCfg :: Maybe (BlockConfig blk) -> PerasCfg blk
402347
makePerasCfg _ =
403348
PerasCfg
404349
{ perasCfgWeightBoost = boostPerCert
350+
, perasCfgQuorumThreshold = quorumThreshold
405351
}
406352

407353
-- | Extract the certificate round from a Peras certificate container

ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/MiniProtocol/ObjectDiffusion/PerasVote.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ module Ouroboros.Consensus.MiniProtocol.ObjectDiffusion.PerasVote
1414
) where
1515

1616
import Ouroboros.Consensus.Block
17-
import Ouroboros.Consensus.MiniProtocol.ObjectDiffusion.Inbound
18-
import Ouroboros.Consensus.MiniProtocol.ObjectDiffusion.Inbound.State
17+
import Ouroboros.Consensus.MiniProtocol.ObjectDiffusion.Inbound.V1
18+
import Ouroboros.Consensus.MiniProtocol.ObjectDiffusion.Inbound.V1.State
1919
import Ouroboros.Consensus.MiniProtocol.ObjectDiffusion.ObjectPool.API
2020
import Ouroboros.Consensus.MiniProtocol.ObjectDiffusion.Outbound
2121
import Ouroboros.Consensus.Storage.PerasVoteDB.API

ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Storage/PerasVoteDB/API.hs

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,11 @@ module Ouroboros.Consensus.Storage.PerasVoteDB.API
1111

1212
-- * 'PerasVoteSnapshot'
1313
, PerasVoteSnapshot (..)
14-
, PerasStakeSnapshot (..)
1514
, PerasVoteTicketNo
1615
, zeroPerasVoteTicketNo
17-
, getPerasCertsFromStakeSnapshot
1816
) where
1917

2018
import Data.Map (Map)
21-
import qualified Data.Map as Map
22-
import Data.Set (Set)
23-
import qualified Data.Set as Set
2419
import Data.Word (Word64)
2520
import GHC.Generics (Generic)
2621
import NoThunks.Class
@@ -29,19 +24,15 @@ import Ouroboros.Consensus.BlockchainTime.WallClock.Types (WithArrivalTime)
2924
import Ouroboros.Consensus.Util.MonadSTM.NormalForm
3025
( MonadSTM (STM)
3126
)
32-
import Ouroboros.Consensus.Util.STM (WithFingerprint (..))
3327

3428
data PerasVoteDB m blk = PerasVoteDB
3529
{ addVote :: WithArrivalTime (ValidatedPerasVote blk) -> m (AddPerasVoteResult blk)
3630
-- ^ Add a Peras vote to the database. The result indicates whether
3731
-- the vote was actually added, or if it was already present.
38-
, getStakeSnapshot :: STM m (PerasStakeSnapshot blk)
39-
-- ^ Return a view of accumulated vote stake per (point, round)
40-
--
41-
-- The underlying 'Fingerprint' is updated every time a new vote is added, but it
42-
-- stays the same when votes are garbage-collected.
4332
, getVoteSnapshot :: STM m (PerasVoteSnapshot blk)
4433
-- ^ Interface to read the known votes, mostly for diffusion
34+
, getForgedCertForRound :: PerasRoundNo -> STM m (Maybe (ValidatedPerasCert blk))
35+
-- ^ Get the certificate if quorum was reached for the given round.
4536
, garbageCollect :: PerasRoundNo -> m ()
4637
-- ^ Garbage-collect state strictly older than the given slot number.
4738
, closeDB :: m ()
@@ -55,18 +46,6 @@ data AddPerasVoteResult blk
5546
deriving stock (Generic, Eq, Ord, Show)
5647
deriving anyclass NoThunks
5748

58-
newtype PerasStakeSnapshot blk = PerasStakeSnapshot
59-
{unPerasStakeSnapshot :: WithFingerprint (Map (PerasVoteTarget blk) (PerasVoteAggregateStatus blk))}
60-
deriving Generic
61-
deriving newtype NoThunks
62-
63-
getPerasCertsFromStakeSnapshot ::
64-
StandardHash blk =>
65-
PerasStakeSnapshot blk ->
66-
Set (ValidatedPerasCert blk)
67-
getPerasCertsFromStakeSnapshot (PerasStakeSnapshot mp) =
68-
Set.fromList $ Map.elems $ Map.mapMaybe pvasMaybeCert (forgetFingerprint mp)
69-
7049
data PerasVoteSnapshot blk = PerasVoteSnapshot
7150
{ containsVote :: IdOf (PerasVote blk) -> Bool
7251
, getVotesAfter ::

0 commit comments

Comments
 (0)