Skip to content

Commit 60940ae

Browse files
authored
Allow to customize preferCandidate (#1063)
Closes #939 The core idea is to keep a total order (via `Ord`) for `SelectView`, but allowing to customize the logic of `preferCandidate` (previously, it was always defined in terms of `Ord`, i.e. `preferCandidate ours cand = cand > ours`), such that it becomes possible to eg implement the restricted VRF tiebreaker (#524) this way, which is fundamentally intransitive. This is in contrast to #1046 (an alternative to this PR), where `preferCandidate` is not customizable, and we instead live with the fact the ordering might not be transitive, which techically would require replacing our calls to `sortBy` with eg a topological sorting algorithm, or just assume/test that `sortBy` still works "well enough" with non-transitive orders. Also see #1047 as a follow-up PR that leverages the new flexibility offered by this PR. This PR undoes some of the changes of IntersectMBO/ouroboros-network#2743 and IntersectMBO/ouroboros-network#2732 which were back then merged to increase simplicity/ease of exposition in the report, but it now turns out that the flexibility they removed is actually useful for us today.
2 parents f25ea0b + f28b409 commit 60940ae

File tree

26 files changed

+528
-132
lines changed

26 files changed

+528
-132
lines changed

ouroboros-consensus-diffusion/changelog.d/20240425_110304_alexander.esgen_customize_prefer_candidate.md

Whitespace-only changes.

ouroboros-consensus-diffusion/src/unstable-diffusion-testlib/Test/ThreadNet/General.hs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import Test.Util.HardFork.Future (Future)
7474
import Test.Util.Orphans.Arbitrary ()
7575
import Test.Util.Orphans.IOLike ()
7676
import Test.Util.Orphans.NoThunks ()
77+
import Test.Util.QuickCheck
7778
import Test.Util.Range
7879
import Test.Util.Shrink (andId, dropId)
7980
import Test.Util.Slots (NumSlots (..))
@@ -674,10 +675,6 @@ prop_general_internal syncity pga testOutput =
674675
| ((s1, _, max1), (s2, min2, _)) <- orderedPairs extrema
675676
]
676677
where
677-
-- QuickCheck's @==>@ 'discard's the test if @p1@ fails; that's not
678-
-- what we want
679-
implies p1 p2 = not p1 .||. p2
680-
681678
-- all pairs @(x, y)@ where @x@ precedes @y@ in the given list
682679
orderedPairs :: [a] -> [(a, a)]
683680
orderedPairs = \case
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Non-Breaking
2+
3+
- Adapted to introduction of new `ChainOrder` type class.

ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ common common-lib
4343
if flag(asserts)
4444
ghc-options: -fno-ignore-asserts
4545

46+
common common-test
47+
import: common-lib
48+
ghc-options:
49+
-threaded
50+
-rtsopts
51+
4652
library
4753
import: common-lib
4854
hs-source-dirs: src/ouroboros-consensus-protocol
@@ -90,3 +96,24 @@ library unstable-protocol-testlib
9096
cardano-protocol-tpraos,
9197
cardano-slotting,
9298
ouroboros-consensus-protocol,
99+
100+
test-suite protocol-test
101+
import: common-test
102+
type: exitcode-stdio-1.0
103+
hs-source-dirs: test/protocol-test
104+
main-is: Main.hs
105+
other-modules:
106+
Test.Consensus.Protocol.Praos.SelectView
107+
108+
build-depends:
109+
QuickCheck,
110+
base,
111+
cardano-crypto-class,
112+
cardano-ledger-binary:testlib,
113+
cardano-ledger-core,
114+
containers,
115+
ouroboros-consensus:{ouroboros-consensus, unstable-consensus-testlib},
116+
ouroboros-consensus-protocol,
117+
serialise,
118+
tasty,
119+
tasty-quickcheck,

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
{-# LANGUAGE DeriveAnyClass #-}
33
{-# LANGUAGE DeriveGeneric #-}
44
{-# LANGUAGE DerivingStrategies #-}
5+
{-# LANGUAGE DerivingVia #-}
56
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7+
{-# LANGUAGE StandaloneDeriving #-}
68
{-# LANGUAGE TypeFamilies #-}
9+
{-# LANGUAGE UndecidableInstances #-}
710

811
-- | Various things common to iterations of the Praos protocol.
912
module Ouroboros.Consensus.Protocol.Praos.Common (
@@ -41,15 +44,7 @@ newtype MaxMajorProtVer = MaxMajorProtVer
4144
deriving (Eq, Show, Generic)
4245
deriving newtype NoThunks
4346

44-
-- | View of the ledger tip for chain selection.
45-
--
46-
-- We order between chains as follows:
47-
--
48-
-- 1. By chain length, with longer chains always preferred.
49-
-- 2. If the tip of each chain was issued by the same agent, then we prefer
50-
-- the chain whose tip has the highest ocert issue number.
51-
-- 3. By a VRF value from the chain tip, with lower values preferred. See
52-
-- @pTieBreakVRFValue@ for which one is used.
47+
-- | View of the tip of a header fragment for chain selection.
5348
data PraosChainSelectView c = PraosChainSelectView
5449
{ csvChainLength :: BlockNo,
5550
csvSlotNo :: SlotNo,
@@ -59,6 +54,13 @@ data PraosChainSelectView c = PraosChainSelectView
5954
}
6055
deriving (Show, Eq, Generic, NoThunks)
6156

57+
-- | We order between chains as follows:
58+
--
59+
-- 1. By chain length, with longer chains always preferred.
60+
-- 2. If the tip of each chain was issued by the same agent, then we prefer
61+
-- the chain whose tip has the highest ocert issue number.
62+
-- 3. By a VRF value from the chain tip, with lower values preferred. See
63+
-- @pTieBreakVRFValue@ for which one is used.
6264
instance Crypto c => Ord (PraosChainSelectView c) where
6365
compare =
6466
mconcat
@@ -80,6 +82,9 @@ instance Crypto c => Ord (PraosChainSelectView c) where
8082
| otherwise =
8183
EQ
8284

85+
deriving via SimpleChainOrder (PraosChainSelectView c)
86+
instance Crypto c => ChainOrder (PraosChainSelectView c)
87+
8388
data PraosCanBeLeader c = PraosCanBeLeader
8489
{ -- | Certificate delegating rights from the stake pool cold key (or
8590
-- genesis stakeholder delegate cold key) to the online KES key.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Main (main) where
2+
3+
import qualified Test.Consensus.Protocol.Praos.SelectView
4+
import Test.Tasty
5+
import Test.Util.TestEnv
6+
7+
main :: IO ()
8+
main = defaultMainWithTestEnv defaultTestEnvConfig tests
9+
10+
tests :: TestTree
11+
tests =
12+
testGroup "protocol"
13+
[ Test.Consensus.Protocol.Praos.SelectView.tests
14+
]
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE FlexibleInstances #-}
3+
{-# LANGUAGE LambdaCase #-}
4+
{-# LANGUAGE NamedFieldPuns #-}
5+
{-# LANGUAGE ScopedTypeVariables #-}
6+
{-# LANGUAGE TypeApplications #-}
7+
8+
{-# OPTIONS_GHC -Wno-orphans #-}
9+
10+
module Test.Consensus.Protocol.Praos.SelectView (tests) where
11+
12+
import qualified Cardano.Crypto.Hash as Crypto
13+
import qualified Cardano.Crypto.Util as Crypto
14+
import Cardano.Crypto.VRF (OutputVRF, mkTestOutputVRF)
15+
import Cardano.Ledger.Crypto (Crypto (..), StandardCrypto)
16+
import qualified Cardano.Ledger.Keys as SL
17+
import Codec.Serialise (encode)
18+
import Control.Monad
19+
import Data.Containers.ListUtils (nubOrdOn)
20+
import Ouroboros.Consensus.Block
21+
import Ouroboros.Consensus.Protocol.Praos.Common
22+
import Test.Cardano.Ledger.Binary.Arbitrary ()
23+
import Test.Ouroboros.Consensus.Protocol
24+
import Test.QuickCheck.Gen (Gen (..))
25+
import Test.QuickCheck.Random (mkQCGen)
26+
import Test.Tasty
27+
import Test.Tasty.QuickCheck hiding (elements)
28+
import Test.Util.QuickCheck
29+
import Test.Util.TestEnv
30+
31+
tests :: TestTree
32+
tests = testGroup "PraosChainSelectView"
33+
[ adjustQuickCheckTests (* 50)
34+
-- Use a small max size by default in order to have a decent chance to
35+
-- trigger the actual tiebreaker cases.
36+
$ adjustQuickCheckMaxSize (`div` 10)
37+
$ tests_chainOrder (Proxy @(PraosChainSelectView StandardCrypto))
38+
]
39+
40+
instance Crypto c => Arbitrary (PraosChainSelectView c) where
41+
arbitrary = do
42+
size <- fromIntegral <$> getSize
43+
csvChainLength <- BlockNo <$> choose (1, size)
44+
csvSlotNo <- SlotNo <$> choose (1, size)
45+
csvIssuer <- elements knownIssuers
46+
csvIssueNo <- genIssueNo
47+
pure PraosChainSelectView {
48+
csvChainLength
49+
, csvSlotNo
50+
, csvIssuer
51+
, csvIssueNo
52+
, csvTieBreakVRF = mkVRFFor csvIssuer csvSlotNo
53+
}
54+
where
55+
-- We want to draw from the same small set of issuer identities in order to
56+
-- have a chance to explore cases where the issuers of two 'SelectView's
57+
-- are identical.
58+
knownIssuers :: [SL.VKey SL.BlockIssuer c]
59+
knownIssuers =
60+
nubOrdOn SL.hashKey
61+
$ unGen (replicateM numIssuers (SL.VKey <$> arbitrary)) randomSeed 100
62+
where
63+
randomSeed = mkQCGen 4 -- chosen by fair dice roll
64+
numIssuers = 10
65+
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+
80+
-- The header VRF is a deterministic function of the issuer VRF key, the
81+
-- slot and the epoch nonce. Additionally, for any particular chain, the
82+
-- slot determines the epoch nonce.
83+
mkVRFFor :: SL.VKey SL.BlockIssuer c -> SlotNo -> OutputVRF (VRF c)
84+
mkVRFFor issuer slot =
85+
mkTestOutputVRF
86+
$ Crypto.bytesToNatural
87+
$ Crypto.hashToBytes
88+
$ Crypto.xor (Crypto.castHash issuerHash)
89+
$ Crypto.hashWithSerialiser encode slot
90+
where
91+
SL.KeyHash issuerHash = SL.hashKey issuer
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Breaking
2+
3+
- Introduced new `ChainOrder` (with `preferCandidate`) class for `SelectView`s,
4+
and add necessary instances. Adapted `preferAnchoredCandidate` to use
5+
`preferCandidate` instead of relying on `preferAnchoredFragment`.

ouroboros-consensus/ouroboros-consensus.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ library unstable-consensus-testlib
331331
Test.Ouroboros.Consensus.ChainGenerator.Slot
332332
Test.Ouroboros.Consensus.ChainGenerator.Some
333333
Test.Ouroboros.Consensus.DiffusionPipelining
334+
Test.Ouroboros.Consensus.Protocol
334335
Test.QuickCheck.Extras
335336
Test.Util.BoolProps
336337
Test.Util.ChainDB

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,13 @@ class ( GetHeader blk
3434
=> BlockConfig blk
3535
-> Header blk -> SelectView (BlockProtocol blk)
3636
selectView _ = blockNo
37+
38+
projectChainOrderConfig ::
39+
BlockConfig blk
40+
-> ChainOrderConfig (SelectView (BlockProtocol blk))
41+
42+
default projectChainOrderConfig ::
43+
ChainOrderConfig (SelectView (BlockProtocol blk)) ~ ()
44+
=> BlockConfig blk
45+
-> ChainOrderConfig (SelectView (BlockProtocol blk))
46+
projectChainOrderConfig _ = ()

0 commit comments

Comments
 (0)