Skip to content

Commit 4cfc488

Browse files
committed
website: add CivicTime.md
1 parent 907d1a4 commit 4cfc488

File tree

11 files changed

+259
-28
lines changed

11 files changed

+259
-28
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Civic Time in the Cardano Node
2+
3+
## Introduction
4+
5+
This document discusses how civic time (eg the wall clock) relates to the Cardano node, both the current design details and the higher-level fundamental needs.
6+
7+
It also raises the question of what behaviors the node should exhibit when the Praos security argument has failed.
8+
9+
## Pre-Ouroboros Chronos
10+
11+
The Ouroboros Chronos protocol has not been implemented, but some of its basic rules have already been implmemented.
12+
Today's node behaviors related to Chronos can be summarized as follows.
13+
14+
- The node trusts NTP.
15+
(A full Ouroboros Chronos implementatino would not.)
16+
17+
- The node silently ignores the wall clock moving backwards by a small amount.
18+
It crashes if the wall clock moves backward by a large amount.
19+
(This would be a NTP failure/attack vector.)
20+
21+
- The node enforces a small bound for acceptable clock skew with respect to some peer's apparent clock.
22+
23+
- The key indicator of a peer's apparent wall clock is the reception of a header from that peer whose slot onset is ahead of the local wall clock.
24+
If the header's earliness is beyond the acceptable clock skew, the peer is considered buggy or dishonest; the node disconnects with prejudice.
25+
If it's instead within bounds, the ChainSync client for that peer is paused until the header is no longer ahead of the local wall clock.
26+
27+
An honest node will not fetch a block before validating the corresponding header, so the above rule prevents a node from receiving a block before the wall clock has reached the onset of its slot.
28+
29+
*Aside*.
30+
It is possibly that a block from the future is already in the database when the node starts.
31+
That's a corner case that seems unlikely to matter in practice.
32+
33+
## Time Translations
34+
35+
The Ouroboros Praos protocol and the Cardano details built around it are almost exclusively defined in terms of slots rather than common civic measures of time, such as POSIX time, UTC, etc.
36+
Each block explicitly inhabits some slot.
37+
The same is true for block headers and the election proofs therein.
38+
For the sake of determinism, transactions are labeled with a range of slots in which the transaction can be valid.
39+
40+
The are only two exceptions.
41+
42+
- The node's (commodity) operating system cannot compute the current slot, merely the current civic time.
43+
44+
- The Plutus interface exposes the validity range as POSIX time, in the [`txInfoValidRange` field](https://plutus.cardano.intersectmbo.org/haddock/latest/plutus-ledger-api/PlutusLedgerApi-V1.html#t:TxInfo) (see this [blog post](https://iohk.io/en/blog/posts/2022/12/08/time-handling-on-cardano-part-2-use-cases/) for high-level background).
45+
The Consensus Layer design would have been simpler if the Plutus API provided the validity range in terms of slots, but that ship has sailed.
46+
(And developers writing Plutus scripts are almost certainly relieved that it did.)
47+
48+
Because of those exceptions, the Consensus Layer must use the wall clock and/or the translation back and forth between slots and civic.
49+
Those uses are listed in the following table.
50+
51+
- Every use involves some translation, whether it be from slot-to-civic or vice versa (which is always the wall clock as a slot).
52+
53+
- This table excludes some uses that the Conensus Team is in the processing of removing (eg those that could be replaced by annotating validated headers with their slot onset).
54+
55+
- The rightmost column of the table judges whether the use obviously involves some entity that is obviously chain-dependent.
56+
The column is for the benefit of a section below, but the key idea is that the translations are inherently chain-dependent, but users and most developers are blissfully unaware of that and, moreover, such a dependency is fundamentally contrary to human intuitions about time.
57+
58+
- The last few rows make explicit Consensus features that are not yet implemented but will also involve civic time.
59+
60+
| Component | Use | Reads wall clock (whether translated to slot) | Which slots are translated to civic | Whether it's "obvious" that some involved entity is chain-dependent |
61+
| - | - | - | - | - |
62+
| ChainSync | enforce the clock skew bound | Yes (raw) | header's slot | Yes, headers |
63+
| ChainSel | define Plutus `txInfoValidRange`s when validating a block | No | validity range | Yes, blocks |
64+
| The Mint | checking whether to mint | Yes (translated) | none | Yes, (the parent of) the new block |
65+
| The Mint, via `getSnapshotFor` | recheck validity range[^MempoolAdd] | Yes (translated) | none | Yes, (the parent of) the new block |
66+
| Genesis State Machine, aka GSM | detect that the selection is an old chain | Yes (raw) | selection's slot | Yes, the selection |
67+
| hypothetically[^MempoolAdd]: Mempool (add and resync) | enforce the validity range | Yes (translated) | none | No, dangling txs are not |
68+
| Mempool (only add[^NoResyncTranslation]) | define Plutus `txInfoValidRange` when validating a dangling tx | No | validity range | No, dangling txs are not |
69+
| LocalStateQuery | the `GetInterpreter` query | No | arbitrary slots | ⚠No⚠[^GetInterpreterWild] |
70+
| Ouroboros Peras | ? analogs of ChainSync + The Mint (+ Mempool?) for votes | ? Yes (both)| ? vote slot | ?[^GiorgosIdea] |
71+
| Ouroboros Leios | ? analogs of ChainSync + The Mint (+ Mempool?) for IBs and EBs | ? Yes (both) | ? header slot | ?[^GiorgosIdea] |
72+
| Mithril | ? analogs of ChainSync + The Mint (+ Mempool?) for votes | ? Yes (raw?) | ? | ? |
73+
74+
[^MempoolAdd]: Explanation.
75+
Today's Mempool doesn't actually use the wall clock to enforce the validity range; it instead uses the slot after the selection's tip.
76+
But it might be preferable to use the wall-clock.
77+
The recheck in the Mint means there's no risk of minting a block with a stale tx.
78+
79+
[^GetInterpreterWild]: Warning.
80+
The consumer of the `GetInterpeter` result could use it for anything.
81+
Remarkably, the fact that the LocalStateQuery mini protocol forced the consumer to explicitly acquire a concrete ledger state (aka a point) before it could even issue the query is not obvious to the user of the consumer.
82+
One notable known use is the CLI tool's computation of the upcoming the leadership schedule.
83+
That tool's UX does not currently loudly indicate that the result depends on which ledger state answered the query and that the output could therefore be different if the same question is asked, even during the same epoch.
84+
85+
This same thing is true of some other queries, but they all have arguments that are obviously chain-dependent.
86+
87+
[^NoResyncTranslation]: Explanation.
88+
Only Plutus scripts require `txInfoValidRange`, but resyncing the Mempool doesn't re-execute Plutus scripts.
89+
See "Two-Phase Transaction Validation for Phase-2 Scripts" in the Alonzo ledger spec.
90+
91+
[^GiorgosIdea]: Note.
92+
We briefly raised this concern/request with Giorgos Panagiotakos.
93+
He brainstormed an idea of perhaps including the relevant nonce in vote/header.
94+
95+
## Some Time Translations Cannot Be Predicted
96+
97+
Different eras of the chain can have different slot durations.
98+
Therefore it must be the responsibility of the Hard Fork Combinator (HFC) to define translation back and forth between slots and civic.
99+
Even with all available information, some translations involving the future cannot be predicted.
100+
101+
Recall that the Byron era of Cardano sets the slot duration to 20 seconds, while all other eras so far set it to one second.
102+
Because eras can have different slot durations, a ledger state is fundamentally unable to correctly translate times _arbitrarily_ ahead of its own slot.
103+
Even if that ledger state were to be perfectly recent (ie its slot is very near the wall clock), eras that have not yet been implemented/designed/even considered could have an unpredictable slot duration.
104+
105+
Using a ledger state that is perfectly recent but otherwise arbitrary, how far ahead could the HFC do translations that are necessarily correct?
106+
Ultimately, it depends on how quickly the net could fork to an era with a different slot duration.
107+
In practice, that's a matter of months on Cardano mainnet[^EmergencyRapidity].
108+
In theory --- assuming only that the Praos security argument holds and that honest stakeholders wouldn't vote for the hard fork unless their nodes were already ready for it --- it's one stability window less than the lower bound on the duration between the current era's voting deadline and the proposal being enacted.
109+
110+
- In Byron, that lower bound was originaly 2k slots = 4320 slots = 86400 seconds = one day, but was doubled to two days before the fork to Shelley happened.
111+
So the lower bound on translations was at least one day ahead, since the Byron stability window was also 2k slots.
112+
113+
- After Byron and before Conway, it was 6k/f slots = 259200 slots = 259200 seconds = 3 days.
114+
So the lower bound on translations was at least 1.5 days ahead, since the post-Byron stability window has always been 3k/f slots.
115+
116+
- Since Conway, it's been one epoch = 10k/f slots = 432000 slots = 432000 seconds = 5 days.
117+
So the lower bound on translations is currently at least 3.5 days ahead, since the stability window is still 3k/f slots.
118+
119+
Those lower bounds are for an arbitrary ledger state, and so they're the "worst-case".
120+
For specific ledger states, their detailed location in the epoch can increase the lower bound by up to one epoch, which has always been 5 days in every era so far.
121+
122+
The limit is one stability window less the the post-voting buffer because --- assuming only the Praos security argument --- that's the upper bound on the age of a block the honest Praos node might need to discard as part of a rollback.
123+
In particular, until the oldest such block is after the voting deadline, a rollback could switch to a chain with a different voting outcome in that epoch.
124+
For example, switching from a chain in which the next era is only a few slots away to one in which it is at least another epoch away.
125+
126+
*Aside*.
127+
On the other hand, when using a (non-recent) ledger state in a historical era, the HFC could theoretically use its knowledge about the upcoming eras in order to translate even further ahead.
128+
For example, the HFC could safely assume the one second duration for slots between some given Babbage ledger state and however many slots it might possibly take before that ledger state could transition from Babbage to Conway and from Conway to whatever comes after, because that mystery era is the first time the slot duration could change.
129+
The HFC does not do this in practice, since the extra complexity is not worthwhile; it's not necessary for syncing nodes to be smarter than caught-up nodes.
130+
131+
[^EmergencyRapidity]: Technicality.
132+
It could perhaps be weeks or days in an emergency, but a change to the slot duration would almost certainly be avoided in that case.
133+
134+
## What About Chain Growth Failures?
135+
136+
Roughly, Today's HFC was derived as follows.
137+
138+
- Because of humans' general assumptions about time, it would be prohibitively confusing if the node (even its internal interfaces) might give different translations of the same slots/civic times.
139+
At the very least, the node should refuse to do a translation that might be invalidated by on-chain governance in the meantime (eg several weeks into the future).
140+
141+
- However, the node should be able to translate slot/civic times from the near future.
142+
At the very least, some users presumably want some capacity to plan ahead (eg to smartly schedule their node's brief downtime).
143+
144+
- The node should even refuse to do translations that might be invalidated by a rollback.
145+
146+
- Even a violation of Praos's Chain Growth property would not excuse violating the above constraints.
147+
148+
The first two are simple to achieve without the third or fourth.
149+
150+
- Require that a governance outcome is enacted at least X slots after the corresponding voting deadline.
151+
152+
- Refuse to translate a slot/civic time that is after the enactment of a governance outcome if using a ledger state that is before the corresponding voting deadline.
153+
154+
The third can be be additionally supported with simple changes.
155+
156+
- Require that X = one stability window + Y.
157+
(Recall that the Header-Body Split already also requires at least one stability window here.)
158+
(Recall that that every Cardano era sets Y to at least one stability window.)
159+
160+
- Refuse to translate a slot/civic time that is after the enactment of a governance outcome if using a ledger state that is less than one stability window after the corresponding voting deadline.
161+
162+
*Aside*.
163+
So-called "double stability"/"stable stability"/etc merely requires Y is at least one stability window.
164+
However, that's not fundamentally necessary (see below), which is why the above allows Y to be 0.
165+
166+
Those suffice to ensure the following.
167+
168+
- A ledger state will not translate a slot/civic time that is subject to some governance outcome until Chain Growth ensures no rollback could change that outcome.
169+
170+
- A ledger state can always translate at least Y slots beyond its own slot, with only one exception: if the first governance outcome in the new era might be enacted in less than Y slots.
171+
(That exception has been impossible for all Cardano era transitions so far: each era's Y has been smaller than the epoch length of the next era.)
172+
173+
The fourth requirement is not simple to ensure.
174+
In order to prevent rollbacks from altering time translations even despite a Chain Growth violation, today's HFC does something radical.
175+
176+
- Silently ignore the on-chain governance --- ie the HFC continues with the current era _despite the on-chain governance outcome having signaled the transition to the next era_ --- if the stability window after the voting deadline contains less than k+1 blocks (ie violates Chain Growth).
177+
(TODO this is today's intended behavior, but a bug is counting all blocks after the voting deadline instead of only those in the subsequent stability window.)
178+
179+
The Consensus Team is considering removing the fourth requirement and the corresponding possibility of ignoring the on-chain governance for the following reasons.
180+
181+
- The occasional clarification to colleagues that "the HFC might ignore the on-chain governance" has always been met with (reasonable) alarm and confusion.
182+
183+
- It seems unlikely that the Ledger Team would agree that it is worthwhile to upstream the block counting logic such that Chain Growth violations prohibit the governance outcomes relevant to the HFC.
184+
Making the on-chain governance itself detect Chain Growth violation (and specifying as much in the community documentation about governance) seems more reasonable than enabling HFC ledger states that are incoherent in surprising ways (ie increased major protocol version but still in the same era).
185+
(For the record, the Consensus Team would be on board with this if others consider this option worthwhile.)
186+
187+
- The rightmost column in the table above indicates that the most fundamental elements of the node (headers and blocks) are inherently already sensitive to rollbacks and therefore can freely rely on time translations that are also sensitive to rollbacks.
188+
(Thus, double stability at least is not required by Praos itself.)
189+
190+
- The other node functions in that table (Mempool and user queries) could prevent sensitivity to rollbacks by merely translating times according to the node's immutable tip ledger state instead of ever overriding the on-chain governance.
191+
192+
It is not yet clear what all the resulting node behaviors would be during a Chain Growth violation --- nor whether it matters!
193+
The node's behavior outside of the Praos security argument is very rarely discussed or even considered.
194+
195+
## Footnotes

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,10 @@ import Ouroboros.Consensus.Util.RedundantConstraints
138138

139139
byronTransition :: PartialLedgerConfig ByronBlock
140140
-> Word16 -- ^ Shelley major protocol version
141+
-> Bool
141142
-> LedgerState ByronBlock
142143
-> Maybe EpochNo
143-
byronTransition ByronPartialLedgerConfig{..} shelleyMajorVersion state =
144+
byronTransition ByronPartialLedgerConfig{..} shelleyMajorVersion ticking state =
144145
takeAny
145146
. mapMaybe isTransitionToShelley
146147
. Byron.Inspect.protocolUpdates byronLedgerConfig
@@ -157,7 +158,7 @@ byronTransition ByronPartialLedgerConfig{..} shelleyMajorVersion state =
157158
case Byron.Inspect.protocolUpdateState update of
158159
Byron.Inspect.UpdateCandidate _becameCandidateSlotNo adoptedIn -> do
159160
becameCandidateBlockNo <- Map.lookup version transitionInfo
160-
guard $ isReallyStable becameCandidateBlockNo
161+
guard $ ticking || isReallyStable becameCandidateBlockNo
161162
return adoptedIn
162163
Byron.Inspect.UpdateStableCandidate adoptedIn ->
163164
-- If the Byron ledger thinks it's stable, it's _definitely_ stable
@@ -213,14 +214,15 @@ byronTransition ByronPartialLedgerConfig{..} shelleyMajorVersion state =
213214
-------------------------------------------------------------------------------}
214215

215216
instance SingleEraBlock ByronBlock where
216-
singleEraTransition pcfg _eraParams _eraStart ledgerState =
217+
singleEraTransition pcfg _eraParams _eraStart ticking ledgerState =
217218
case byronTriggerHardFork pcfg of
218219
TriggerHardForkNotDuringThisExecution -> Nothing
219220
TriggerHardForkAtEpoch epoch -> Just epoch
220221
TriggerHardForkAtVersion shelleyMajorVersion ->
221222
byronTransition
222223
pcfg
223224
shelleyMajorVersion
225+
ticking
224226
ledgerState
225227

226228
singleEraInfo _ = SingleEraInfo {

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,12 @@ shelleyTransition ::
141141
forall era proto. ShelleyCompatible proto era
142142
=> PartialLedgerConfig (ShelleyBlock proto era)
143143
-> Word16 -- ^ Next era's initial major protocol version
144+
-> Bool
144145
-> LedgerState (ShelleyBlock proto era)
145146
-> Maybe EpochNo
146147
shelleyTransition ShelleyPartialLedgerConfig{..}
147148
transitionMajorVersionRaw
149+
ticking
148150
state =
149151
isTransition
150152
. Shelley.Inspect.pparamsUpdate
@@ -166,14 +168,14 @@ shelleyTransition ShelleyPartialLedgerConfig{..}
166168
let protVer = pp ^. SL.ppProtocolVersionL
167169
transitionMajorVersion <- SL.mkVersion transitionMajorVersionRaw
168170
guard $ SL.pvMajor protVer == transitionMajorVersion
169-
guard $ shelleyAfterVoting >= fromIntegral k
171+
guard $ ticking || (shelleyAfterVoting >= fromIntegral k)
170172
return newPParamsEpochNo
171173

172174
instance ( ShelleyCompatible proto era
173175
, LedgerSupportsProtocol (ShelleyBlock proto era)
174176
, TxLimits (ShelleyBlock proto era)
175177
) => SingleEraBlock (ShelleyBlock proto era) where
176-
singleEraTransition pcfg _eraParams _eraStart ledgerState =
178+
singleEraTransition pcfg _eraParams _eraStart ticking ledgerState =
177179
-- TODO: We might be evaluating 'singleEraTransition' more than once when
178180
-- replaying blocks. We should investigate if this is the case, and if so,
179181
-- whether this is the desired behaviour. If it is not, then we need to
@@ -188,6 +190,7 @@ instance ( ShelleyCompatible proto era
188190
shelleyTransition
189191
pcfg
190192
shelleyMajorVersion
193+
ticking
191194
ledgerState
192195

193196
singleEraInfo _ = SingleEraInfo {

ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/HardFork/Combinator/Abstract/SingleEraBlock.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class ( LedgerSupportsProtocol blk
9393
singleEraTransition :: PartialLedgerConfig blk
9494
-> EraParams -- ^ Current era parameters
9595
-> Bound -- ^ Start of this era
96+
-> Bool -- ^ True <=> ticking rather than forecasting
9697
-> LedgerState blk
9798
-> Maybe EpochNo
9899

@@ -106,6 +107,7 @@ singleEraTransition' :: SingleEraBlock blk
106107
=> WrapPartialLedgerConfig blk
107108
-> EraParams
108109
-> Bound
110+
-> Bool
109111
-> LedgerState blk -> Maybe EpochNo
110112
singleEraTransition' = singleEraTransition . unwrapPartialLedgerConfig
111113

0 commit comments

Comments
 (0)