Skip to content

Commit 8a299a9

Browse files
committed
ChainDB q-s-m: test weighted chain selection
1 parent 7df2c92 commit 8a299a9

File tree

5 files changed

+155
-55
lines changed

5 files changed

+155
-55
lines changed

ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Util/AnchoredFragment.hs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module Ouroboros.Consensus.Util.AnchoredFragment
1010
( compareAnchoredFragments
1111
, compareHeadBlockNo
1212
, cross
13-
, forksAtMostKBlocks
13+
, forksAtMostKWeight
1414
, preferAnchoredCandidate
1515
, stripCommonPrefix
1616
) where
@@ -19,7 +19,6 @@ import Data.Foldable (toList)
1919
import qualified Data.Foldable1 as F1
2020
import Data.Function (on)
2121
import qualified Data.List.NonEmpty as NE
22-
import Data.Word (Word64)
2322
import GHC.Stack
2423
import Ouroboros.Consensus.Block
2524
import Ouroboros.Consensus.Peras.SelectView
@@ -55,20 +54,32 @@ compareHeadBlockNo ::
5554
Ordering
5655
compareHeadBlockNo = compare `on` AF.headBlockNo
5756

58-
forksAtMostKBlocks ::
59-
HasHeader b =>
60-
-- | How many blocks can it fork?
61-
Word64 ->
62-
-- | Our chain.
57+
-- | Check that we can switch from @ours@ to @theirs@ by rolling back our chain
58+
-- by at most @k@ weight.
59+
--
60+
-- If @ours@ and @cand@ do not intersect, this returns 'False'. If they do
61+
-- intersect, then we check that the suffix of @ours@ after the intersection has
62+
-- total weight at most @k@.
63+
forksAtMostKWeight ::
64+
( StandardHash blk
65+
, HasHeader b
66+
, HeaderHash blk ~ HeaderHash b
67+
) =>
68+
PerasWeightSnapshot blk ->
69+
-- | By how much weight can we roll back our chain at most?
70+
PerasWeight ->
71+
-- | Our chain @ours@.
6372
AnchoredFragment b ->
64-
-- | Their chain
73+
-- | Their chain @theirs@.
6574
AnchoredFragment b ->
66-
-- | Indicates whether their chain forks at most the
67-
-- specified number of blocks.
75+
-- | Indicates whether their chain forks at most the given the amount of
76+
-- weight. Returns 'False' if the two fragments do not intersect.
6877
Bool
69-
forksAtMostKBlocks k ours theirs = case ours `AF.intersect` theirs of
70-
Nothing -> False
71-
Just (_, _, ourSuffix, _) -> fromIntegral (AF.length ourSuffix) <= k
78+
forksAtMostKWeight weights maxWeight ours theirs =
79+
case ours `AF.intersect` theirs of
80+
Nothing -> False
81+
Just (_, _, ourSuffix, _) ->
82+
totalWeightOfFragment weights ourSuffix <= maxWeight
7283

7384
-- | Compare two (potentially empty!) 'AnchoredFragment's.
7485
--

ouroboros-consensus/src/unstable-consensus-testlib/Test/Util/Orphans/ToExpr.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ instance ToExpr FsError where
119119

120120
deriving instance ToExpr a => ToExpr (LoE a)
121121

122+
deriving anyclass instance ToExpr PerasRoundNo
123+
124+
deriving anyclass instance ToExpr PerasWeight
125+
126+
deriving anyclass instance ToExpr (HeaderHash blk) => ToExpr (PerasCert blk)
127+
122128
{-------------------------------------------------------------------------------
123129
si-timers
124130
--------------------------------------------------------------------------------}

ouroboros-consensus/test/storage-test/Test/Ouroboros/Storage/ChainDB/Model.hs

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Test.Ouroboros.Storage.ChainDB.Model
2525
, addBlock
2626
, addBlockPromise
2727
, addBlocks
28+
, addPerasCert
2829
, empty
2930

3031
-- * Queries
@@ -44,7 +45,7 @@ module Test.Ouroboros.Storage.ChainDB.Model
4445
, invalid
4546
, isOpen
4647
, isValid
47-
, lastK
48+
, maxPerasRoundNo
4849
, tipBlock
4950
, tipPoint
5051
, volatileChain
@@ -90,6 +91,7 @@ import Control.Monad.Except (runExcept)
9091
import Data.Bifunctor (first)
9192
import qualified Data.ByteString.Lazy as Lazy
9293
import Data.Containers.ListUtils (nubOrdOn)
94+
import Data.Foldable (foldMap')
9395
import Data.Function (on, (&))
9496
import Data.Functor (($>), (<&>))
9597
import Data.List (isInfixOf, isPrefixOf, sortBy)
@@ -100,7 +102,6 @@ import Data.Proxy
100102
import Data.Set (Set)
101103
import qualified Data.Set as Set
102104
import Data.TreeDiff
103-
import Data.Word (Word64)
104105
import GHC.Generics (Generic)
105106
import Ouroboros.Consensus.Block
106107
import Ouroboros.Consensus.Config
@@ -147,6 +148,7 @@ data Model blk = Model
147148
-- ^ The VolatileDB
148149
, immutableDbChain :: Chain blk
149150
-- ^ The ImmutableDB
151+
, perasCerts :: Map PerasRoundNo (PerasCert blk)
150152
, cps :: CPS.ChainProducerState blk
151153
, currentLedger :: ExtLedgerState blk EmptyMK
152154
, initLedger :: ExtLedgerState blk EmptyMK
@@ -233,72 +235,78 @@ tipPoint = maybe GenesisPoint blockPoint . tipBlock
233235
getMaxSlotNo :: HasHeader blk => Model blk -> MaxSlotNo
234236
getMaxSlotNo = foldMap (MaxSlotNo . blockSlot) . blocks
235237

236-
lastK ::
237-
HasHeader a =>
238-
SecurityParam ->
239-
-- | Provided since `AnchoredFragment` is not a functor
240-
(blk -> a) ->
241-
Model blk ->
242-
AnchoredFragment a
243-
lastK (SecurityParam k) f =
244-
Fragment.anchorNewest (unNonZero k)
245-
. Chain.toAnchoredFragment
246-
. fmap f
247-
. currentChain
248-
249-
-- | Actual number of blocks that can be rolled back. Equal to @k@, except
250-
-- when:
238+
-- | Actual amount of weight that can be rolled back. This can non-trivially
239+
-- smaller than @k@ in the following cases:
251240
--
252-
-- * Near genesis, the chain might not be @k@ blocks long yet.
253-
-- * After VolatileDB corruption, the whole chain might be >= @k@ blocks, but
254-
-- the tip of the ImmutableDB might be closer than @k@ blocks away from the
255-
-- current chain's tip.
256-
maxActualRollback :: HasHeader blk => SecurityParam -> Model blk -> Word64
241+
-- * Near genesis, the chain might not have grown sufficiently yet.
242+
-- * After VolatileDB corruption, the whole chain might have more than weight
243+
-- @k@, but the tip of the ImmutableDB might be buried under significantly
244+
-- less than weight @k@ worth of blocks.
245+
maxActualRollback :: HasHeader blk => SecurityParam -> Model blk -> PerasWeight
257246
maxActualRollback k m =
258-
fromIntegral
259-
. length
247+
foldMap' (weightBoostOfPoint weights)
260248
. takeWhile (/= immutableTipPoint)
261249
. map blockPoint
262250
. Chain.toNewestFirst
263251
. currentChain
264252
$ m
265253
where
254+
weights = perasWeights m
255+
266256
immutableTipPoint = Chain.headPoint (immutableChain k m)
267257

268258
-- | Return the immutable prefix of the current chain.
269259
--
270260
-- This is the longest of the given two chains:
271261
--
272-
-- 1. The current chain with the last @k@ blocks dropped.
262+
-- 1. The current chain with the longest suffix of weight at most @k@ dropped.
273263
-- 2. The chain formed by the blocks in 'immutableDbChain', i.e., the
274264
-- \"ImmutableDB\". We need to take this case in consideration because the
275265
-- VolatileDB might have been wiped.
276266
--
277-
-- We need this because we do not allow rolling back more than @k@ blocks, but
267+
-- We need this because we do not allow rolling back more than weight @k@, but
278268
-- the background thread copying blocks from the VolatileDB to the ImmutableDB
279269
-- might not have caught up yet. This means we cannot use the tip of the
280270
-- ImmutableDB to know the most recent \"immutable\" block.
281271
immutableChain ::
272+
forall blk.
273+
HasHeader blk =>
282274
SecurityParam ->
283275
Model blk ->
284276
Chain blk
285-
immutableChain (SecurityParam k) m =
277+
immutableChain k m =
286278
maxBy
279+
-- As one of the two chains is a prefix of the other, Peras weight doesn't
280+
-- matter here.
287281
Chain.length
288-
(Chain.drop (fromIntegral $ unNonZero k) (currentChain m))
282+
(dropAtMostWeight (maxRollbackWeight k) (currentChain m))
289283
(immutableDbChain m)
290284
where
291285
maxBy f a b
292286
| f a >= f b = a
293287
| otherwise = b
294288

289+
weights = perasWeights m
290+
291+
-- Drop the longest suffix with at most the given weight.
292+
dropAtMostWeight :: PerasWeight -> Chain blk -> Chain blk
293+
dropAtMostWeight budget = go mempty
294+
where
295+
go w = \case
296+
Genesis -> Genesis
297+
c@(c' :> b)
298+
| w' <= budget -> go w' c'
299+
| otherwise -> c
300+
where
301+
w' = w <> PerasWeight 1 <> weightBoostOfPoint weights (blockPoint b)
302+
295303
-- | Return the volatile suffix of the current chain.
296304
--
297305
-- The opposite of 'immutableChain'.
298306
--
299307
-- This is the shortest of the given two chain fragments:
300308
--
301-
-- 1. The last @k@ blocks of the current chain.
309+
-- 1. The longest suffix of the current chain with weight at most @k@.
302310
-- 2. The suffix of the current chain not part of the 'immutableDbChain', i.e.,
303311
-- the \"ImmutableDB\".
304312
volatileChain ::
@@ -370,6 +378,17 @@ isValid = flip getIsValid
370378
getLoEFragment :: Model blk -> LoE (AnchoredFragment blk)
371379
getLoEFragment = loeFragment
372380

381+
perasWeights :: StandardHash blk => Model blk -> PerasWeightSnapshot blk
382+
perasWeights =
383+
mkPerasWeightSnapshot
384+
-- TODO make boost per cert configurable
385+
. fmap (\c -> (perasCertBoostedBlock c, boostPerCert))
386+
. Map.elems
387+
. perasCerts
388+
389+
maxPerasRoundNo :: Model blk -> Maybe PerasRoundNo
390+
maxPerasRoundNo m = fst <$> Map.lookupMax (perasCerts m)
391+
373392
{-------------------------------------------------------------------------------
374393
Construction
375394
-------------------------------------------------------------------------------}
@@ -383,6 +402,7 @@ empty loe initLedger =
383402
Model
384403
{ volatileDbBlocks = Map.empty
385404
, immutableDbChain = Chain.Genesis
405+
, perasCerts = Map.empty
386406
, cps = CPS.initChainProducerState Chain.Genesis
387407
, currentLedger = initLedger
388408
, initLedger = initLedger
@@ -422,6 +442,23 @@ addBlock cfg blk m
422442
-- If it's an invalid block we've seen before, ignore it.
423443
Map.member (blockHash blk) (invalid m)
424444

445+
addPerasCert ::
446+
forall blk.
447+
(LedgerSupportsProtocol blk, LedgerTablesAreTrivial (ExtLedgerState blk)) =>
448+
TopLevelConfig blk ->
449+
PerasCert blk ->
450+
Model blk ->
451+
Model blk
452+
addPerasCert cfg cert m
453+
-- Do not alter the model when a certificate for that round already exists.
454+
| Map.member certRound (perasCerts m) = m
455+
| otherwise =
456+
chainSelection
457+
cfg
458+
m{perasCerts = Map.insert certRound cert (perasCerts m)}
459+
where
460+
certRound = perasCertRound cert
461+
425462
chainSelection ::
426463
forall blk.
427464
( LedgerTablesAreTrivial (ExtLedgerState blk)
@@ -434,6 +471,7 @@ chainSelection cfg m =
434471
Model
435472
{ volatileDbBlocks = volatileDbBlocks m
436473
, immutableDbChain = immutableDbChain m
474+
, perasCerts = perasCerts m
437475
, cps = CPS.switchFork newChain (cps m)
438476
, currentLedger = newLedger
439477
, initLedger = initLedger m
@@ -533,15 +571,12 @@ chainSelection cfg m =
533571
. selectChain
534572
(Proxy @(BlockProtocol blk))
535573
(projectChainOrderConfig (configBlock cfg))
536-
( weightedSelectView (configBlock cfg) weights
574+
( weightedSelectView (configBlock cfg) (perasWeights m)
537575
. Chain.toAnchoredFragment
538576
. fmap getHeader
539577
)
540578
(currentChain m)
541579
$ consideredCandidates
542-
where
543-
-- TODO enrich with Peras weights/certs
544-
weights = emptyPerasWeightSnapshot
545580

546581
-- We update the set of valid blocks with all valid blocks on all candidate
547582
-- chains that are considered by the modeled chain selection. This ensures
@@ -871,12 +906,9 @@ validChains cfg m bs =
871906
sortChains =
872907
sortBy $
873908
flip
874-
( Fragment.compareAnchoredFragments (configBlock cfg) weights
909+
( Fragment.compareAnchoredFragments (configBlock cfg) (perasWeights m)
875910
`on` (Chain.toAnchoredFragment . fmap getHeader)
876911
)
877-
where
878-
-- TODO enrich with Peras weights/certs
879-
weights = emptyPerasWeightSnapshot
880912

881913
classify ::
882914
ValidatedChain blk ->
@@ -910,7 +942,11 @@ between k from to m = do
910942
fork <- errFork
911943
-- See #871.
912944
if partOfCurrentChain fork
913-
|| Fragment.forksAtMostKBlocks (maxActualRollback k m) currentFrag fork
945+
|| Fragment.forksAtMostKWeight
946+
(perasWeights m)
947+
(maxActualRollback k m)
948+
currentFrag
949+
fork
914950
then return $ Fragment.toOldestFirst fork
915951
-- We cannot stream from an old fork
916952
else Left $ ForkTooOld from
@@ -1050,6 +1086,7 @@ garbageCollect ::
10501086
garbageCollect secParam m@Model{..} =
10511087
m
10521088
{ volatileDbBlocks = Map.filter (not . collectable) volatileDbBlocks
1089+
-- TODO garbage collection Peras certs?
10531090
}
10541091
where
10551092
-- TODO what about iterators that will stream garbage collected blocks?
@@ -1101,6 +1138,14 @@ wipeVolatileDB cfg m =
11011138
m' =
11021139
(closeDB m)
11031140
{ volatileDbBlocks = Map.empty
1141+
, -- TODO: Currently, the SUT has no persistence of Peras certs across
1142+
-- restarts, but this will change. There are at least two options:
1143+
--
1144+
-- * Change this command to mean "wipe volatile state" (including
1145+
-- volatile certificates)
1146+
--
1147+
-- * Add a separate "Wipe volatile certs".
1148+
perasCerts = Map.empty
11041149
, cps = CPS.switchFork newChain (cps m)
11051150
, currentLedger = newLedger
11061151
, invalid = Map.empty

ouroboros-consensus/test/storage-test/Test/Ouroboros/Storage/ChainDB/Model/Test.hs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
-- chain DB, we always pick the most preferred chain.
2323
module Test.Ouroboros.Storage.ChainDB.Model.Test (tests) where
2424

25-
import Cardano.Ledger.BaseTypes (unNonZero)
2625
import GHC.Stack
2726
import Ouroboros.Consensus.Block
2827
import Ouroboros.Consensus.Config
@@ -97,13 +96,13 @@ prop_alwaysPickPreferredChain bt p =
9796

9897
curFragment = Chain.toAnchoredFragment (getHeader <$> current)
9998

100-
SecurityParam k = configSecurityParam singleNodeTestConfig
99+
k = configSecurityParam singleNodeTestConfig
101100

102101
bcfg = configBlock singleNodeTestConfig
103102

104103
preferCandidate' candidate =
105104
AF.preferAnchoredCandidate bcfg weights curFragment candFragment
106-
&& AF.forksAtMostKBlocks (unNonZero k) curFragment candFragment
105+
&& AF.forksAtMostKWeight weights (maxRollbackWeight k) curFragment candFragment
107106
where
108107
candFragment = Chain.toAnchoredFragment (getHeader <$> candidate)
109108

0 commit comments

Comments
 (0)