Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!--
A new scriv changelog fragment.

Uncomment the section that is right (remove the HTML comment wrapper).
For top level release notes, leave all the headers commented out.
-->

<!--
### Patch

- A bullet item for the Patch category.

-->
<!--
### Non-Breaking

- A bullet item for the Non-Breaking category.

-->

### Breaking

- Add modules `Ouroboros.Consensus.Storage.PerasCertDB{,.API,.Impl}`, notably defining the types`PerasCertDB`, `PerasCertSnapshot` (read-only snapshot of certs contained in the DB), and `AddPerasCertResult`; alongside their respective methods
- Add modules `Test.Ouroboros.Storage.PerasCertDB{,.StateMachine,.Model}` for q-s-m testing of the `PerasCertDB` datatype. The corresponding tests are included in the test suite defined by `Test.Ouroboros.Storage`
6 changes: 6 additions & 0 deletions ouroboros-consensus/ouroboros-consensus.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ library
Ouroboros.Consensus.Storage.LedgerDB.V2.Forker
Ouroboros.Consensus.Storage.LedgerDB.V2.InMemory
Ouroboros.Consensus.Storage.LedgerDB.V2.LedgerSeq
Ouroboros.Consensus.Storage.PerasCertDB
Ouroboros.Consensus.Storage.PerasCertDB.API
Ouroboros.Consensus.Storage.PerasCertDB.Impl
Ouroboros.Consensus.Storage.Serialisation
Ouroboros.Consensus.Storage.VolatileDB
Ouroboros.Consensus.Storage.VolatileDB.API
Expand Down Expand Up @@ -720,6 +723,9 @@ test-suite storage-test
Test.Ouroboros.Storage.LedgerDB.V1.DbChangelog
Test.Ouroboros.Storage.LedgerDB.V1.LMDB
Test.Ouroboros.Storage.Orphans
Test.Ouroboros.Storage.PerasCertDB
Test.Ouroboros.Storage.PerasCertDB.Model
Test.Ouroboros.Storage.PerasCertDB.StateMachine
Test.Ouroboros.Storage.VolatileDB
Test.Ouroboros.Storage.VolatileDB.Mock
Test.Ouroboros.Storage.VolatileDB.Model
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Ouroboros.Consensus.Storage.PerasCertDB (module X) where

import Ouroboros.Consensus.Storage.PerasCertDB.API as X
import Ouroboros.Consensus.Storage.PerasCertDB.Impl as X
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Ouroboros.Consensus.Storage.PerasCertDB.API
( PerasCertDB (..)
, AddPerasCertResult (..)

-- * 'PerasCertSnapshot'
, PerasCertSnapshot (..)
, PerasCertTicketNo
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need ticket numbers for Peras certificates?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We allocate PerasCertTicketNos in monotonically increasing fashion as new certificates arrive, just as we do for transactions in the mempool (we should add this in a Haddock comment). This way, anyone can subscribe to certificates as they arrive, by asking for more certificates after the last ticket number they know of, using getCertsAfter.

One thing to note here is that Peras round numbers almost could play the role of ticket numbers: Apart from edge cases, we will never receive a Peras certificate for round r after we received one for round s if r < s. However, because of these edge cases and a general concern for robustness, we are not using round numbers as "almost ticket numbers".


Lastly, we could also hide the ticket numbers by exposing a more monadic API (essentially a dequeue interface), where the PerasCertNo is maintained "internally". (This would be possible both here and in the Mempool API)

Status quo (simplified) in this PR:

getCertSnapshot :: STM m (PerasCertSnapshot blk)

data PerasCertSnapshot blk = PerasCertSnapshot
  { getCertsAfter :: PerasCertTicketNo -> [(ValidatedPerasCert blk, PerasCertTicketNo)]
  }

Here, the caller is responsible for maintaining the PerasCertTicketNo across calls to getCertsAfter/getCertSnapshot.

More monadic option without ticket numbers:

getCertFollower :: STM m (PerasCertFollower blk)

data PerasCertFollower blk = PerasCertFollower
  { getNextPerasCert :: STM m (ValidatedPerasCert blk)]
  }

Here, getNextPerasCert would internally maintain the PerasCertTicketNo in a TVar (or we could change the implementation to be based on a TChan 🤔 ).

Let us know if you find this appealing, we can explore that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, because of these edge cases and a general concern for robustness, we are not using round numbers as "almost ticket numbers".

That makes sense. Should we document this properly (if it's not documented already). Thanks!

Let us know if you find this appealing, we can explore that.

I can't say I have a preference, but the current design seems simpler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we document this properly (if it's not documented already). Thanks!

I enriched the Haddocks of PerasCertTicketNo appropriately 👍

, zeroPerasCertTicketNo
) where

import Data.Map (Map)
import Data.Word (Word64)
import NoThunks.Class
import Ouroboros.Consensus.Block
import Ouroboros.Consensus.Peras.Weight
import Ouroboros.Consensus.Util.IOLike
import Ouroboros.Consensus.Util.STM (WithFingerprint (..))

data PerasCertDB m blk = PerasCertDB
{ addCert :: ValidatedPerasCert blk -> m AddPerasCertResult
-- ^ Add a Peras certificate to the database. The result indicates whether
-- the certificate was actually added, or if it was already present.
, getWeightSnapshot :: STM m (WithFingerprint (PerasWeightSnapshot blk))
-- ^ Return the Peras weights in order compare the current selection against
-- potential candidate chains, namely the weights for blocks not older than
-- the current immutable tip. It might contain weights for even older blocks
-- if they have not yet been garbage-collected.
--
-- The 'Fingerprint' is updated every time a new certificate is added, but it
-- stays the same when certificates are garbage-collected.
, getCertSnapshot :: STM m (PerasCertSnapshot blk)
, garbageCollect :: SlotNo -> m ()
-- ^ Garbage-collect state older than the given slot number.
, closeDB :: m ()
}
deriving NoThunks via OnlyCheckWhnfNamed "PerasCertDB" (PerasCertDB m blk)

data AddPerasCertResult = AddedPerasCertToDB | PerasCertAlreadyInDB
deriving stock (Show, Eq)

data PerasCertSnapshot blk = PerasCertSnapshot
{ containsCert :: PerasRoundNo -> Bool
-- ^ Do we have the certificate for this round?
, getCertsAfter :: PerasCertTicketNo -> Map PerasCertTicketNo (ValidatedPerasCert blk)
-- ^ Get certificates after the given ticket number (excluded).
-- The result is a map of ticket numbers to validated certificates.
}

-- | A sequence number, incremented every time we receive a new certificate.
--
-- Note that we will /usually/ receive certificates monotonically by round
-- number, so round numbers could /almost/ fulfill the role of ticket numbers.
-- However, in certain edge cases (while catching up, or during cooldowns), this
-- might not be true, such as during syncing or during cooldown periods.
-- Therefore, for robustness, we choose to maintain dedicated ticket numbers
-- separately.
newtype PerasCertTicketNo = PerasCertTicketNo Word64
deriving stock Show
deriving newtype (Eq, Ord, Enum, NoThunks)

zeroPerasCertTicketNo :: PerasCertTicketNo
zeroPerasCertTicketNo = PerasCertTicketNo 0
Loading
Loading