-
Notifications
You must be signed in to change notification settings - Fork 33
Refactor: SelectView = BlockNo × TiebreakerView
#1591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| instance NoThunks (TiebreakerView p) => NoThunks (SelectView p) | ||
|
|
||
| -- | First compare block numbers, then compare the 'TiebreakerView'. | ||
| deriving stock instance Ord (TiebreakerView p) => Ord (SelectView p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be an explicitly written-out Ord instance? it would be bad if someone decided to reorder the fields in SelectView and catastrophically broke chain selection lol
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, pushed 👍
Irrational grudge of mine: I always found it a bit depressing to write such instances by hand; motivated by #549, I wrote https://gist.github.com/amesgen/0ddcbe0e2bae458ba8b40d4799d1b6e4 (essentially a "surgery" in the sense of generic-data) that allowed you to permute the constructors of a data type in the Generic representation for DerivingVia. The same thing of course works for fields instead of constructors; with that, we could write
deriving
via Generically (PermuteFields ["svBlockNo", "svTiebreakerView"] (SelectView p))
instance Ord (TiebreakerView p) => Ord (SelectView p)which is now resistant to syntactic reordering in the data declaration of data SelectView. All that to avoid writing
instance Ord (TiebreakerView p) => Ord (SelectView p) where
compare =
mconcat
[ compare `on` svBlockNo
, compare `on` svTiebreakerView
]by hand 💪 💪 💪
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. I left a couple of comments. Let me know when the TODOs are addressed and a new review round is required 👍
...consensus-cardano/src/ouroboros-consensus-cardano/Ouroboros/Consensus/Cardano/CanHardFork.hs
Show resolved
Hide resolved
ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Protocol/Abstract.hs
Show resolved
Hide resolved
| { svBlockNo :: !BlockNo | ||
| , svTiebreakerView :: !(TiebreakerView p) | ||
| } | ||
| deriving stock Generic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably unrelated to this PR, but I was wondering if we could elaborate on why the block number (which will become a weigth) and the notion of a tie-breaker cannot be unified into a single weight notion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to capture this in the Haddocks 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, it's still unclear to me (the answer to the question above, the haddocks comment is clear :D). It seems that ultimately, we could write a function to sort two select views which take the information of the tie-breaker into consideration. Do we want to keep the block number and tiebreaker separately because of the separation that we currently have between sorting candidates and preferring a chain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not exactly sure what unification you have in mind, is it something different than what we have in current main, where everybody defines SelectView as a "single" thing determining the chain order for themselves? Or sth else entirely?
we could write a function to sort two select views which take the information of the tie-breaker into consideration.
Are you expecting something different than in the Ord/ChainOrder instances for SelectView in this PR?
Do we want to keep the block number and tiebreaker separately because of the separation that we currently have between sorting candidates and preferring a chain?
It is unrelated to that distinction; the motivation is
- (minor) to make it explicit that
BlockNomust be the primary way of comparing chains; even before this PR,ConsensusProtocols didn't have the freedom to use arbitrarySelectViews, and - nicely supports weighted chain comparisons, see "Primary motivation" in the PR description, as well as Introduce weighted chain comparisons #1594.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FTR, we discussed this offline with @amesgen. The key constraint we have is that we rely on a transitive Ord instance when selecting candidates. Therefore, we cannot unify the sorting of Views in a single function. I think that we have is indeed two comparison functions that operate on SelectViews. It's just that we're explicit about the part of the field that is used for breaking ties (which I think makes sense)
05b7cb1 to
adb6891
Compare
adb6891 to
13c868f
Compare
...nsensus/src/ouroboros-consensus/Ouroboros/Consensus/HardFork/Combinator/Protocol/ChainSel.hs
Outdated
Show resolved
Hide resolved
13c868f to
9b07e31
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great.
ouroboros-consensus-cardano/changelog.d/20250716_190309_alexander.esgen_tiebreaker_view.md
Show resolved
Hide resolved
|
|
||
| -- | The chain order of some type; in the Consensus layer, this will always be | ||
| -- the 'SelectView' of some 'ConsensusProtocol'. | ||
| -- the 'TiebreakerView'/'SelectView' of some 'ConsensusProtocol'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we mention both in this comment? Would it make sense to clarify when we use one and when the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we have instances for both of them, where ChainOrder SelectView uses ChainOrder TiebreakerView. I explicitly mentioned this also here 👍
| { svBlockNo :: !BlockNo | ||
| , svTiebreakerView :: !(TiebreakerView p) | ||
| } | ||
| deriving stock Generic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, it's still unclear to me (the answer to the question above, the haddocks comment is clear :D). It seems that ultimately, we could write a function to sort two select views which take the information of the tie-breaker into consideration. Do we want to keep the block number and tiebreaker separately because of the separation that we currently have between sorting candidates and preferring a chain?
9b07e31 to
5cdc439
Compare
This PR does not change any behavior; most of the diff is purely
mechanical.
Previously, the `SelectView`s of all `ConsensusProtocol`s contained the
`BlockNo` as the most important criterion for comparing chains, with
only tiebreaking behavior (among chains of equal length) being different
across different protocols.
This PR makes this explicit: `SelectView` is now longer an associated
type family of `ConsensusProtocol`, but rather an ordinary data type
```haskell
data SelectView p = SelectView
{ svBlockNo :: !BlockNo
, svTiebreakerView :: !(TiebreakerView p)
}
```
where `TiebreakerView` is now an associated type family of
`ConsensusProtocol`.
This PR also removes the `WithBlockNo` type that the HFC was already
using to always attach a `BlockNo` to its `HardForkSelectView`. The code
can be simpler now that `SelectView`s always automatically contain an
explicit `BlockNo`.
Also see
#1542 (comment)
## Primary motivation
The main motivation for this is to enable weighted chain selection in
Peras (tweag/cardano-peras#62), which can now
be easily modeled by adding the weight of a chain to the block number
component, at least morally; we probably want to eventually have more
type safety here to make it hard to "forget" to account for the weight,
such as defining
```haskell
data WeightedSelectView p = WeightedSelectView
{ wsvWeight :: !PerasWeight
, wsvTiebreakerView :: !(TiebreakerView p)
}
```
If the only goal were to minimize the diff, then it would also be
possible to add a function `(BlockNo -> BlockNo) -> SelectView p ->
SelectView p`, but it seems more honest to "properly" extract out the
block number.
…ed chain selection (#1678) # Description This PR introduces: - **Weighted chain comparisons**: We compare chains based on their *weight* instead of their length. Non-trivial weight is given by Peras certificates as introduced in #1673. - **Weighted chain selection**: Select the *weightiest* chain instead of the *longest* chain. - **Weighted immutability criterion**: Define the immutable tip in terms of *weight* instead of length. ### Commits The commits are intended to be reviewed individually: - *ChainDB: expose PerasCertDB functionality* The ChainDB maintains a PerasCertDB internally, and exposes most of its functionality through its public API. This is analogous to how the LedgerDB is managed by the ChainDB. - *`SecurityParam`: mention weighted nature* Purely a documentation update to mention the richer dynamics under Peras, plus a small helper function. - *O.C.Peras.Weight: add `takeVolatileSuffix`* In Praos, the volatile suffix of a chain is defined to be the `k` most recent blocks. Analogously, in Peras, the volatile suffix of a chain is defined to be the longest suffix with weight at most `k`. This commits adds an appropriate function (via a binary search), as well as documentation and tests. - *ChainDB: define `getCurrentChain` in terms of weight* This makes use of the previous commit in the ChainDB. Note that #1619 guarantees that the LedgerDB automatically uses the same notion of immutability. Note that this means that the immutable tip will be less than `k` blocks behind the tip when Peras is working well, ie every Peras round is giving rise to a certificate. Concretely, for plausible parameters, ie a round length of $U = 90$ slots and a Peras boost of $B = 15$, the length of the volatile suffix decreases from $k=2160$ to $$\frac{k}{1+\frac{B}{U\cdot f}} \approx 499$$ on average. - *ChainDB.StateMachine: check immutable tip monotonicity* To make sure that the more refined immutability criterion doesn't introduce any surprises, we add a postcondition to the ChainDB q-s-m test that the immutable tip never recedes. - *GSM: allow `candidateOverSelection` to be stateful* and *ChainSel: make `rollbackExceedsSuffix` weight-aware* Preparatory refactorings for when the chain comparison will become weighted and hence additionally depend on the `PerasWeightSnapshot`. - *Peras.SelectView: initialize, expose `WeightedSelectView`* This introduces the notion of a *weighted* `SelectView`, making use of #1591 https://github.com/IntersectMBO/ouroboros-consensus/blob/90131b282b4e002e9ddc0d7196043c50b255a1d4/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Peras/SelectView.hs#L35-L48 This will used in place of the "normal" `SelectView`. Eventually, we could also remove the normal `SelectView` and replace it with `WeightedSelectView`. However, to keep this already big PR more focused, we propose to do this in the future. - *Introduce weighted chain comparisons* The core change in this commit is in the `O.C.Util.AnchoredFragment` to the `preferAnchoredCandidate` and `compareAnchoredFragments` functions. These now - have a slightly strengthened precondition, namely that they intersect (as we otherwise can't meaningfully compare their weight), that we ensure is satisfied anywhere, and - take `PerasWeightSnapshot` as an additional argument. We keep the old implementation in case the `PerasWeightSnapshot` is empty; which is semantically unnecessary, but is a trivial way to make sure that no performance regression is introduced. The rest of the diff of this commit is simply due to adapting the respective call sites in a rather straightforward fashion. - *Integrate weighted BlockFetch decision logic* This commit plugs the weighted chain comparison logic into the BlockFetch decision logic. This relies on a (merged, but not yet released, hence the s-r-p) Network change, see IntersectMBO/ouroboros-network#5161 for a detailed description. - *ChainDB: implement chain selection for Peras certificates* We allow new Peras certificates to be added to the ChainDB (and therefore to the managed PerasCertDB). If the certificate is boosting a block that is not on the current selection, we perform chain selection for it, potentially switching to a fork containing it if it now is weightier than our selection. - *MockChainSel: switch to weighted chain selection* This logic is for example used in tests. - *ChainDB q-s-m: test weighted chain selection* We enrich the model ChainDB implementation with weighted chain selection, and add a new command with a simple generator for adding certificates. We also enrich labelling with the `TagSwitchedToShorterChain` tag that shows that we sometimes *do* switch to a shorter chain; which would be a bug without Peras. A follow-up PR (#1670) will further improve the generators of this test. --- ### **Regression** As of today, there is no way to generate certificates, so `PerasCertDB` is always empty, and the change to the chain selection algorithm is therefore invisible: in the absence of certificates, the Peras weight of a chain is equal to its length. Therefore, this PR does not introduce any semantic changes. Furthermore, from a performance standpoint, care is taken to ensure that if there are no certificates, we use the unweighted Praos logic, meaning that there is no change in performance when Peras is disabled.
This PR does not change any behavior; most of the diff is purely mechanical.
Previously, the
SelectViews of allConsensusProtocols contained theBlockNoas the most important criterion for comparing chains, with only tiebreaking behavior (among chains of equal length) being different across different protocols.This PR makes this explicit:
SelectViewis now longer an associated type family ofConsensusProtocol, but rather an ordinary data typewhere
TiebreakerViewis now an associated type family ofConsensusProtocol.This PR also removes the
WithBlockNotype that the HFC was already using to always attach aBlockNoto itsHardForkSelectView. The code can be simpler now thatSelectViews always automatically contain an explicitBlockNo.Also see #1542 (comment)
Primary motivation
The main motivation for this is to enable weighted chain selection in Peras (tweag/cardano-peras#62), which can now be easily modeled by adding the weight of a chain to the block number component, at least morally; we probably want to eventually have more type safety here to make it hard to "forget" to account for the weight, such as defining
If the only goal were to minimize the diff, then it would also be possible to add a function
(BlockNo -> BlockNo) -> SelectView p -> SelectView p, but it seems more honest to "properly" extract out the block number.