Skip to content

Commit 51cc1f2

Browse files
authored
Leios: Prototype voting (#1963)
First PR to add voting capabilities to the `leios-prototype`: - Naiive N^2 vote diffusion via `LeiosNotify` - A new `VoteState` that allows to `addVote` and `subscribeVotes` - A voting thread that votes on completed EB closures - Missing: EB too old to vote check - Missing: EB validation - Mocked voting key registration (re-uses KES key bytes) - No vote validation (yet) - Threadnet properties that drove the implementation so far - Fixed re-trigger of EB notifications for already completed closures Roughly the components that were implemented in this PR: <img width="2872" height="904" alt="Leios voting (Current)(1)" src="https://github.com/user-attachments/assets/7e9e032c-d581-4dd1-b30d-9ced05967edc" /> **NOTE**: The failing test with seed `(SMGen 15462600834920737326 9156690725779514573,20)` is actually a lucky one that conflates two `VoterId` from the hacked KES key bytes. It will drive the follow-up PR that explores commitee selection paths.
2 parents 4107a8e + 9cad3f7 commit 51cc1f2

40 files changed

Lines changed: 623 additions & 229 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "cardano-blueprint"]
2+
path = cardano-blueprint
3+
url = git@github.com:cardano-scaling/cardano-blueprint.git

cardano-blueprint

Submodule cardano-blueprint added at ebabbf1

ouroboros-consensus-cardano/src/byron/Ouroboros/Consensus/Byron/Node.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ protocolInfoByron
217217
, topLevelConfigCodec = mkByronCodecConfig compactedGenesisConfig
218218
, topLevelConfigStorage = ByronStorageConfig blockConfig
219219
, topLevelConfigCheckpoints = emptyCheckpointsMap
220+
, topLevelConfigVotingKey = Nothing
220221
}
221222
, pInfoInitLedger =
222223
ExtLedgerState

ouroboros-consensus-cardano/src/ouroboros-consensus-cardano/Ouroboros/Consensus/Cardano/Node.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module Ouroboros.Consensus.Cardano.Node
4747

4848
import Cardano.Binary (DecoderError (..), enforceSize)
4949
import Cardano.Chain.Slotting (EpochSlots)
50+
import Cardano.Crypto.KES.Class (rawSerialiseUnsoundPureSignKeyKES)
5051
import qualified Cardano.Ledger.Api.Era as L
5152
import qualified Cardano.Ledger.Api.Transition as L
5253
import qualified Cardano.Ledger.BaseTypes as SL
@@ -812,6 +813,12 @@ protocolInfoCardano paramsCardano
812813
(Shelley.ShelleyStorageConfig tpraosSlotsPerKESPeriod k)
813814
(Shelley.ShelleyStorageConfig tpraosSlotsPerKESPeriod k)
814815
, topLevelConfigCheckpoints = cardanoCheckpoints
816+
, -- FIXME: REMOVE THIS. Accesses and re-uses KES signing key material.
817+
topLevelConfigVotingKey =
818+
Just
819+
. rawSerialiseUnsoundPureSignKeyKES
820+
. shelleyLeaderCredentialsInitSignKey
821+
$ credssShelleyBased !! 0
815822
}
816823

817824
-- When the initial ledger state is not in the Byron era, register various

ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Node/TPraos.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ protocolInfoTPraosShelleyBased
232232
, topLevelConfigCodec = ShelleyCodecConfig
233233
, topLevelConfigStorage = storageConfig
234234
, topLevelConfigCheckpoints = emptyCheckpointsMap
235+
, topLevelConfigVotingKey = Nothing
235236
}
236237

237238
consensusConfig :: ConsensusConfig (BlockProtocol (ShelleyBlock (TPraos c) era))

ouroboros-consensus-cardano/src/unstable-byron-testlib/Ouroboros/Consensus/ByronDual/Node.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ protocolInfoDualByron abstractGenesis@ByronSpecGenesis{..} params credss =
125125
, dualStorageConfigAux = ByronSpecStorageConfig
126126
}
127127
, topLevelConfigCheckpoints = emptyCheckpointsMap
128+
, topLevelConfigVotingKey = Nothing
128129
}
129130
, pInfoInitLedger =
130131
ExtLedgerState

ouroboros-consensus-cardano/test/cardano-test/Test/ThreadNet/Leios.hs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,14 @@ import Data.Sequence.Strict ((|>))
5252
import qualified Data.Set as Set
5353
import Data.Word (Word64)
5454
import LeiosDemoDb (LeiosDbConnection, newLeiosDBInMemoryWith, withLeiosDb)
55-
import LeiosDemoTypes (LeiosPoint (..), TraceLeiosKernel (..), hashLeiosEb, minCertificationGap)
56-
import Lens.Micro (each, (%~), (^.), (^..))
55+
import LeiosDemoTypes
56+
( LeiosPoint (..)
57+
, LeiosVote (..)
58+
, TraceLeiosKernel (..)
59+
, hashLeiosEb
60+
, minCertificationGap
61+
)
62+
import Lens.Micro ((%~), (^.))
5763
import Ouroboros.Consensus.Block (SlotNo (..), blockSlot)
5864
import Ouroboros.Consensus.Cardano
5965
( CardanoBlock
@@ -127,9 +133,8 @@ import Test.ThreadNet.Network
127133
( LeiosState (..)
128134
, NodeOutput (..)
129135
, TestNodeInitialization (..)
130-
, _FromLeios
131-
, _FromMempool
132-
, _nodeEvent
136+
, TraceThreadNet (FromNode)
137+
, TraceThreadNetNode (FromLeios, FromLeiosPeer, FromMempool)
133138
)
134139
import Test.ThreadNet.TxGen.Cardano (CardanoTxGenExtra (..))
135140
import Test.ThreadNet.Util.NodeJoinPlan (trivialNodeJoinPlan)
@@ -170,9 +175,11 @@ prop_leios seed =
170175
, propConsistentChains
171176
, certificationGapIsCorrect
172177
.||. length certifyingBlocks <= 1
178+
, propVoting
173179
]
174180
where
175-
numNodes = 3 :: Integer
181+
numNodes = 3 :: Int
182+
176183
numSlots = 200 :: Word64
177184

178185
(testOutput, ProtocolInfo{pInfoConfig, pInfoInitLedger}) =
@@ -184,7 +191,7 @@ prop_leios seed =
184191

185192
includedTxCounts = length . extractTxs <$> forgedBlocks
186193

187-
leiosTraces = traces ^.. each . _nodeEvent . _FromLeios
194+
leiosTraces = [ev | FromNode _ (FromLeios ev) <- traces]
188195

189196
forgedPoints = Map.keysSet forgedEBs
190197

@@ -196,7 +203,39 @@ prop_leios seed =
196203
TraceLeiosBlockTxsAcquired point -> Just point
197204
_ -> Nothing
198205

199-
mempoolTraces = traces ^.. each . _nodeEvent . _FromMempool
206+
propVoting =
207+
conjoin
208+
[ length votedPoints > 0
209+
& counterexample "never voted"
210+
, acquiredPoints `Set.isSubsetOf` votedPoints
211+
& counterexample "not voted on all acquired EBs"
212+
& prettyCounterexampleList "acquired leios EBs" 120 acquiredPoints
213+
& prettyCounterexampleList "voted on EBs" 120 votedPoints
214+
, ( Map.keysSet acquiredVotes === votedPoints
215+
.&&. all (\voters -> length voters == numNodes) acquiredVotes
216+
)
217+
& counterexample "created votes not diffused"
218+
& prettyCounterexampleMap "acquired votes" 120 acquiredVotes
219+
& prettyCounterexampleList "voted on EBs" 120 votedPoints
220+
& counterexample
221+
( "peer traces: "
222+
<> unlines [show (nid, ev) | FromNode nid (FromLeiosPeer ev) <- traces]
223+
)
224+
& counterexample
225+
( "kernel traces: "
226+
<> unlines [show (nid, ev) | FromNode nid (FromLeios ev) <- traces]
227+
)
228+
]
229+
230+
votedPoints = Set.fromList . flip mapMaybe leiosTraces $ \case
231+
TraceLeiosVoted{vote} -> Just vote.point
232+
_ -> Nothing
233+
234+
acquiredVotes = Map.fromListWith mappend . flip mapMaybe leiosTraces $ \case
235+
TraceLeiosVoteAcquired{vote} -> Just (vote.point, Set.singleton vote.voterId)
236+
_ -> Nothing
237+
238+
mempoolTraces = [ev | FromNode _ (FromMempool ev) <- traces]
200239

201240
mempoolAddedTxs = flip mapMaybe mempoolTraces $ \case
202241
TraceMempoolAddedTx tx _ _ -> Just tx
@@ -579,9 +618,13 @@ prettyCounterexampleMap title maxLength m prop =
579618
where
580619
prettyMap =
581620
Map.toList m
582-
& map (\(a, b) -> indented 2 . elided maxLength $ show a <> " -> " <> show b)
621+
& map (\(a, b) -> indented 2 $ elided kvLength (show a) <> arrowStr <> elided kvLength (show b))
583622
& unlines
584623

624+
arrowStr = " -> "
625+
626+
kvLength = (maxLength - length arrowStr) `div` 2
627+
585628
-- | Pretty print a list of counterexamples, one on each row and eliding long
586629
-- entries to given maxLength. If maxLength is 0 or negative, no elision is
587630
-- performed.

ouroboros-consensus-diffusion/ouroboros-consensus-diffusion.cabal

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ library unstable-diffusion-testlib
139139
graphviz >=2999.20.1.0,
140140
io-classes,
141141
io-sim,
142-
microlens,
143142
mtl,
144143
ouroboros-consensus:{ouroboros-consensus, unstable-consensus-testlib},
145144
ouroboros-consensus-diffusion,

ouroboros-consensus-diffusion/src/ouroboros-consensus-diffusion/Ouroboros/Consensus/Network/NodeToNode.hs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ import qualified Codec.CBOR.Decoding as CBOR
5151
import Codec.CBOR.Encoding (Encoding)
5252
import qualified Codec.CBOR.Encoding as CBOR
5353
import Codec.CBOR.Read (DeserialiseFailure)
54+
import Control.Applicative ((<|>))
5455
import qualified Control.Concurrent.Class.MonadMVar as MVar
5556
import Control.Concurrent.Class.MonadSTM.Strict (readTChan)
5657
import qualified Control.Concurrent.Class.MonadSTM.Strict.TVar as TVar.Unchecked
58+
import Control.Monad (forM_)
5759
import Control.Monad.Class.MonadTime.SI (MonadTime)
5860
import Control.Monad.Class.MonadTimer.SI (MonadTimer)
5961
import Control.ResourceRegistry
@@ -65,19 +67,23 @@ import qualified Data.Map as Map
6567
import Data.Map.Strict (Map)
6668
import qualified Data.Set as Set
6769
import Data.Void (Void)
68-
import LeiosDemoDb (LeiosDbHandle (subscribeEbNotifications))
70+
import LeiosDemoDb
71+
( LeiosDbHandle (subscribeEbNotifications)
72+
, LeiosEbNotification (..)
73+
)
6974
import qualified LeiosDemoDb as LeiosDb
7075
import qualified LeiosDemoLogic as Leios
7176
import LeiosDemoOnlyTestFetch
7277
import LeiosDemoOnlyTestNotify
7378
import LeiosDemoTypes
7479
( LeiosEb
75-
, LeiosNotification (..)
7680
, LeiosPoint (..)
7781
, LeiosTx
82+
, TraceLeiosKernel (..)
7883
, TraceLeiosPeer (..)
7984
)
8085
import qualified LeiosDemoTypes as Leios
86+
import LeiosVoteState (LeiosVoteState (..), LeiosVoteSubscription (..), subscribeVotes)
8187
import qualified Network.Mux as Mux
8288
import Network.TypedProtocol.Codec
8389
import Network.TypedProtocol.Peer (Peer (Effect))
@@ -96,6 +102,7 @@ import Ouroboros.Consensus.Node.ExitPolicy
96102
import Ouroboros.Consensus.Node.NetworkProtocolVersion
97103
import Ouroboros.Consensus.Node.Run
98104
import Ouroboros.Consensus.Node.Serialisation
105+
import Ouroboros.Consensus.Node.Tracers (leiosKernelTracer)
99106
import qualified Ouroboros.Consensus.Node.Tracers as Node
100107
import Ouroboros.Consensus.NodeKernel
101108
import qualified Ouroboros.Consensus.Storage.ChainDB.API as ChainDB
@@ -332,6 +339,8 @@ mkHandlers
332339
, hPeerSharingServer = \_version _peer -> peerSharingServer getPeerSharingAPI
333340
, hLeiosNotifyClient = \_version controlMessageSTM peer -> toLeiosNotifyClientPeerPipelined $ Effect $ do
334341
let tracer = leiosPeerTracer peer
342+
kernelTracer = leiosKernelTracer tracers
343+
LeiosVoteState{addVote} = leiosVoteState
335344
MVar.modifyMVar_ getLeiosPeersVars $ \leiosPeersVars -> do
336345
x <- Leios.newLeiosPeerVars
337346
let !leiosPeersVars' = Map.insert (Leios.MkPeerId peer) x leiosPeersVars
@@ -340,7 +349,7 @@ mkHandlers
340349
leiosNotifyClientPeerPipelined
341350
( atomically controlMessageSTM <&> \case
342351
Terminate -> Left ()
343-
_ -> Right 300 {- TODO magic number -}
352+
_ -> Right 10 {- TODO magic number -}
344353
)
345354
( pure $ \case
346355
MsgLeiosBlockAnnouncement{} -> error "Demo does not send EB announcements!"
@@ -377,15 +386,31 @@ mkHandlers
377386
let !offers2' = Set.insert ebHash offers2
378387
pure (offers1, offers2')
379388
void $ MVar.tryPutMVar getLeiosReady ()
389+
MsgLeiosVotes vs -> do
390+
-- TODO: reduce this trace a bit to the essential
391+
traceWith tracer $ MkTraceLeiosPeer $ "MsgLeiosVotes " <> show vs
392+
mapM_ addVote vs
393+
forM_ vs $ \vote ->
394+
traceWith kernelTracer TraceLeiosVoteAcquired{vote}
380395
)
381396
, hLeiosNotifyServer = \_version _peer -> Effect $ do
382397
chan <- subscribeEbNotifications leiosDB
383-
pure . leiosNotifyServerPeer $ do
384-
atomically (readTChan chan) >>= \case
385-
LeiosOfferBlock point ebSize ->
386-
pure $ MsgLeiosBlockOffer point ebSize
387-
LeiosOfferBlockTxs point ->
388-
pure $ MsgLeiosBlockTxsOffer point
398+
let processEbNotification =
399+
readTChan chan >>= \case
400+
AcquiredEb point ebSize ->
401+
pure $ MsgLeiosBlockOffer point ebSize
402+
AcquiredEbTxs point ->
403+
pure $ MsgLeiosBlockTxsOffer point
404+
405+
LeiosVoteSubscription{getNextVote} <- subscribeVotes leiosVoteState
406+
let processVote = do
407+
vote <- getNextVote
408+
-- FIXME: Yields each vote separately
409+
pure $ MsgLeiosVotes [vote]
410+
411+
pure . leiosNotifyServerPeer $
412+
atomically $
413+
processEbNotification <|> processVote
389414
, hLeiosFetchClient = \_version controlMessageSTM peer -> toLeiosFetchClientPeerPipelined $ Effect $ do
390415
reqVar <-
391416
let loop = do
@@ -427,6 +452,7 @@ mkHandlers
427452
} = nodeKernel
428453
NodeKernel
429454
{ leiosDB
455+
, leiosVoteState
430456
, getLeiosPeersVars
431457
, getLeiosOutstanding
432458
, getLeiosReady

ouroboros-consensus-diffusion/src/ouroboros-consensus-diffusion/Ouroboros/Consensus/NodeKernel.hs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,14 @@ import System.Random (StdGen)
161161

162162
import Control.Concurrent.Class.MonadMVar (MVar)
163163
import qualified Control.Concurrent.Class.MonadMVar as MVar
164+
import Control.Concurrent.Class.MonadSTM.Strict (readTChan)
165+
import qualified Data.ByteString as BS
164166
import Data.Map (Map)
165167
import qualified Data.Map as Map
166168
import LeiosDemoDb
167169
( LeiosDbConnection
168170
, LeiosDbHandle (..)
171+
, LeiosEbNotification (..)
169172
, leiosDbInsertEbBody
170173
, leiosDbInsertEbPoint
171174
, leiosDbInsertTxs
@@ -176,9 +179,12 @@ import LeiosDemoTypes
176179
( ForgedLeiosEb
177180
, LeiosOutstanding
178181
, LeiosPeerVars
182+
, LeiosVote (..)
179183
, TraceLeiosKernel (..)
184+
, VoterId (..)
180185
)
181186
import qualified LeiosDemoTypes as Leios
187+
import LeiosVoteState (LeiosVoteState (..), newLeiosVoteState)
182188
import Ouroboros.Consensus.Mempool.TxSeq (mSize)
183189
import qualified Ouroboros.Consensus.Mempool.TxSeq as TxSeq
184190

@@ -228,6 +234,7 @@ data NodeKernel m addrNTN addrNTC blk = NodeKernel
228234

229235
-- See 'LeiosPeerVars' for the write patterns
230236
leiosDB :: LeiosDbHandle m
237+
, leiosVoteState :: LeiosVoteState m
231238
, getLeiosPeersVars :: MVar m (Map (Leios.PeerId (ConnectionId addrNTN)) (LeiosPeerVars m))
232239
, -- Written to by the fetch logic, by the LeiosNotify&LeiosFetch, and by LeiosCopier
233240
-- clients (TODO and by eviction).
@@ -407,6 +414,7 @@ initNodeKernel
407414
fetchClientRegistry
408415
blockFetchConfiguration
409416

417+
leiosVoteState <- newLeiosVoteState
410418
getLeiosPeersVars <- MVar.newMVar Map.empty
411419
getLeiosOutstanding <- MVar.newMVar Leios.emptyLeiosOutstanding -- TODO init from DB
412420
getLeiosReady <- MVar.newEmptyMVar
@@ -448,6 +456,47 @@ initNodeKernel
448456
traceWith tracer $ MkTraceLeiosKernel $ "leiosFetchLogic: duration " ++ show duration
449457
threadDelay $ loopInterval - duration
450458

459+
void $ forkLinkedThread registry "NodeKernel.leiosVoting" $ do
460+
let tracer = leiosKernelTracer tracers
461+
LeiosVoteState{addVote} = leiosVoteState
462+
-- Access voting key and derive voter id
463+
case topLevelConfigVotingKey cfg of
464+
Nothing ->
465+
traceWith tracer $
466+
MkTraceLeiosKernel $
467+
"NodeKernel.leiosVoting: disabled because no topLevelConfigVotingKey"
468+
Just votingKey -> do
469+
-- TODO: derive from committee within ledger state, also move within loop (changes across epochs)
470+
-- FIXME: Lucky seed has two identical voter ids. Fix via proper
471+
-- committee selection from all registered keys.
472+
-- :main -p Leios --quickcheck-replay "(SMGen 15462600834920737326 9156690725779514573,20)"
473+
let me = MkVoterId . fromIntegral $ BS.head votingKey
474+
-- Subscribe to EBs for which we have the full closure
475+
chan <- subscribeEbNotifications leiosDB
476+
let getNext f =
477+
atomically (readTChan chan) >>= \case
478+
AcquiredEb{} -> pure ()
479+
AcquiredEbTxs point -> f point
480+
forever $ do
481+
-- TODO: Need to poll available EBs instead? Otherwise we would not vote
482+
-- when we switch to a chain only later
483+
getNext $ \point -> do
484+
let tooOld = const False -- TODO: check not too old; use tip or wall clock time?
485+
unless (tooOld point) $ do
486+
-- TODO: check whether already voted
487+
-- TODO: validate EB closures against selected chain
488+
-- TODO: create vote (sign the eb hash)
489+
let vote =
490+
MkLeiosVote
491+
{ point
492+
, voterId = me
493+
, voteSignature = True
494+
}
495+
-- Store vote in memory and notify downstream peers
496+
addVote vote
497+
traceWith tracer TraceLeiosVoted{vote}
498+
traceWith tracer TraceLeiosVoteAcquired{vote}
499+
451500
return
452501
NodeKernel
453502
{ getChainDB = chainDB
@@ -466,6 +515,7 @@ initNodeKernel
466515
, getDiffusionPipeliningSupport
467516
, getBlockchainTime = btime
468517
, leiosDB
518+
, leiosVoteState
469519
, getLeiosPeersVars
470520
, getLeiosOutstanding
471521
, getLeiosReady

0 commit comments

Comments
 (0)