Skip to content

Commit 62246f8

Browse files
authored
Praos chain order: fix issue no-related transitivity edge case (#1086)
Closes #1075 Beware that this PR has a fairly small $\dfrac{\text{severity}}{\text{subtlety}}$ ratio. ### Current non-transitivity of the chain order related to issue numbers Before this PR, the `Ord PraosChainSelectView` instance is defined as the lexicographic-ish[^lexicographic-ish] combination of the following comparisons in descending order: - Chain length, preferring longer chains. - If the issuer identity is the same, compare by the issue/opcert number, preferring higher values, otherwise, no preference. - VRF tiebreaker, preferring lower values. To see why it is not transitive, consider the following three `SelectView`s: | | a | b | c | | ------------ | - | - | - | | Chain length | l | l | l | | Issuer | x | y | x | | Issue no | 2 | o | 1 | | VRF | 3 | 2 | 1 | With the current chain order, we have - `a < b` and `b < c` due to the VRF tiebreaker, and - `c < a` due to the issue number tiebreaker (as `a` and `c` have the same issuer). So we have have `a < b < c < a < ...`. Note that due to `VRF a /= VRF c` and `Issuer a == Issuer c`, we must have `Slot a /= Slot c`, even though `ChainLength a == ChainLength b`. This is because VRFs are collision-resistant, and are a deterministic function of the slot, the (cold) issuer identity and the epoch nonce (which is itself determined by the slot for any given chain). However, this case is not important for the motivating scenario of the issue number tiebreaker, namely when an attacker got hold of the hot key (but not the cold key) of issuer `x`, and the attacked SPO, the owner of the cold key of `x`, creates a new hot key with an incremented issue number, where the issue number tiebreaker is now supposed to "establish precedence"[^precedence]. In this scenario, the attacker minted `c`, and the attacked SPO minted `a`; this is however unrealistic as due to `Slot a /= Slot c`, either party could have minted on top of the other block, superseding the tiebreaker due to having a longer chain. ### Restoring transitivity The natural fix is hence to require `Slot x == Slot y` in addition to `Issuer x == Issuer y` as the condition on whether to compare issue numbers when comparing `SelectView`s `x` and `y`. In the example above, we then have - `a < b` and `b < c` due to the VRF tiebreaker (unchanged), and - `a < c` also due to the VRF tiebreaker (new), as the issue number tiebreaker is not armed. Note that as already mentioned above, the condition `Slot x == Slot y && Issuer x == Issuer y` is equivalent to `VRF x == VRF y`. We could therefore use this condition in this PR, and even remove the issuer from `PraosChainSelectView`. As a historical note, a very similar chain order was in place in the past before the current non-transitivity was accidentally introduced as a side effect in IntersectMBO/ouroboros-network#2348. The approach of this PR is slightly different than (but morally the same as) the one suggested in #891; I think it is nice that the issue numbers are still compared "before" the VRFs in this approach as that matches the high-level intuition. --- Based on top of #1047 [^lexicographic-ish]: Usually, one only considers the lexicographic order constructed out of orders that are at least partial. However, the order "compare opcert numbers when the issuers are identical, otherwise, consider equal" on pairs of issuer identities and opcert numbers is not a partial order as it is non-transitive. Still, the same general principle applies. [^precedence]: See ["Design Specification for Delegation and Incentives in Cardano"](https://github.com/IntersectMBO/cardano-ledger/blob/master/README.md), Section 3.7.
2 parents c0e282f + 8c956b8 commit 62246f8

File tree

3 files changed

+29
-21
lines changed

3 files changed

+29
-21
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
### Non-Breaking
2+
3+
- Changed the Praos chain order such that for two blocks `A` and `B` by the same
4+
issuer with the same block number, `A` is now preferred over `B` only if `A`
5+
has a higher issue number *and* (new) `A` and `B` are in the same slot.
6+
7+
This is in line with the motiviation for the issue number tiebreaker, and
8+
fixes the transitivity of the `Ord PraosChainSelectView` instance in a special
9+
case.

ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos/Common.hs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ comparePraos ::
9292
-> Ordering
9393
comparePraos tiebreakerFlavor =
9494
(compare `on` csvChainLength)
95-
<> when' ((==) `on` csvIssuer) (compare `on` csvIssueNo)
95+
<> when' issueNoArmed (compare `on` csvIssueNo)
9696
<> when' vrfArmed (compare `on` Down . csvTieBreakVRF)
9797
where
9898
-- When the predicate @p@ returns 'True', use the given comparison function,
@@ -104,6 +104,12 @@ comparePraos tiebreakerFlavor =
104104
when' p comp a1 a2 =
105105
if p a1 a2 then comp a1 a2 else EQ
106106

107+
-- Only compare the issue numbers when the issuers and slots are identical.
108+
-- Note that this case implies the VRFs also coincide.
109+
issueNoArmed v1 v2 =
110+
csvSlotNo v1 == csvSlotNo v2
111+
&& csvIssuer v1 == csvIssuer v2
112+
107113
-- Whether to do a VRF comparison.
108114
vrfArmed v1 v2 = case tiebreakerFlavor of
109115
UnrestrictedVRFTiebreaker -> True
@@ -120,8 +126,9 @@ comparePraos tiebreakerFlavor =
120126
--
121127
-- 1. By chain length, with longer chains always preferred.
122128
--
123-
-- 2. If the tip of each chain was issued by the same agent, then we prefer
124-
-- the chain whose tip has the highest ocert issue number.
129+
-- 2. If the tip of each chain was issued by the same agent and they have the
130+
-- same slot number, prefer the chain whose tip has the highest ocert issue
131+
-- number.
125132
--
126133
-- 3. By a VRF value from the chain tip, with lower values preferred. See
127134
-- @pTieBreakVRFValue@ for which one is used.
@@ -142,8 +149,13 @@ instance Crypto c => Ord (PraosChainSelectView c) where
142149
--
143150
-- 1. Chain length, with longer chains always preferred.
144151
--
145-
-- 2. If the tip of each chain was issued by the same agent, then we prefer the
146-
-- candidate if it has a higher ocert issue number.
152+
-- 2. If the tip of each chain was issued by the same agent and had the same
153+
-- slot number, then we prefer the candidate if it has a higher ocert issue
154+
-- number.
155+
--
156+
-- Note that this condition is equivalent to the VRFs being identical, as
157+
-- the VRF is a deterministic function of the issuer VRF key, the slot and
158+
-- the epoch nonce, and VRFs are collision-resistant.
147159
--
148160
-- 3. Depending on the 'VRFTiebreakerFlavor':
149161
--
@@ -182,7 +194,8 @@ instance Crypto c => Ord (PraosChainSelectView c) where
182194
-- block issuer can use their cold key to issue a new hot key with a higher
183195
-- opcert issue number and set up a new pool. Due to this tiebreaker rule,
184196
-- the blocks minted by that pool will take precedence (allowing the actual
185-
-- block issuer to decide on eg the block contents and the predecessor), and
197+
-- block issuer to decide on eg the block contents and the predecessor) over
198+
-- blocks with the same block and slot number minted by the attacker, and
186199
-- they will end up on the honest chain quickly, which means that the
187200
-- adversary can't extend any chain containing such a block as it would
188201
-- violate the monotonicity requirement on opcert issue numbers.

ouroboros-consensus-protocol/test/protocol-test/Test/Consensus/Protocol/Praos/SelectView.hs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ instance Crypto c => Arbitrary (PraosChainSelectView c) where
4343
csvChainLength <- BlockNo <$> choose (1, size)
4444
csvSlotNo <- SlotNo <$> choose (1, size)
4545
csvIssuer <- elements knownIssuers
46-
csvIssueNo <- genIssueNo
46+
csvIssueNo <- choose (1, 10)
4747
pure PraosChainSelectView {
4848
csvChainLength
4949
, csvSlotNo
@@ -63,20 +63,6 @@ instance Crypto c => Arbitrary (PraosChainSelectView c) where
6363
randomSeed = mkQCGen 4 -- chosen by fair dice roll
6464
numIssuers = 10
6565

66-
-- TODO Actually randomize this once the issue number tiebreaker has been
67-
-- fixed to be transitive. See the document in
68-
-- https://github.com/IntersectMBO/ouroboros-consensus/pull/891 for
69-
-- details.
70-
--
71-
-- TL;DR: In an edge case, the issue number tiebreaker prevents the
72-
-- chain order from being transitive. This could be fixed relatively
73-
-- easily, namely by swapping the issue number tiebreaker and the VRF
74-
-- tiebreaker. However, this is technically not backwards-compatible,
75-
-- impacting the current pre-Conway diffusion pipelining scheme.
76-
--
77-
-- See https://github.com/IntersectMBO/ouroboros-consensus/issues/1075.
78-
genIssueNo = pure 1
79-
8066
-- The header VRF is a deterministic function of the issuer VRF key, the
8167
-- slot and the epoch nonce. Additionally, for any particular chain, the
8268
-- slot determines the epoch nonce.

0 commit comments

Comments
 (0)