1#![forbid(unsafe_code)]
9
10use std::error::Error;
11use std::fmt;
12
13pub const RUNTIME_SCHEMA_VERSION: u16 = 2;
15
16#[repr(transparent)]
22#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
23pub struct RuntimeSessionId([u8; 16]);
24
25impl RuntimeSessionId {
26 pub const fn new(bytes: [u8; 16]) -> Result<Self, RuntimeError> {
32 if u128::from_be_bytes(bytes) == 0 {
33 Err(RuntimeError::ZeroSession)
34 } else {
35 Ok(Self(bytes))
36 }
37 }
38
39 #[must_use]
41 pub const fn into_bytes(self) -> [u8; 16] {
42 self.0
43 }
44
45 #[must_use]
47 pub const fn as_bytes(&self) -> &[u8; 16] {
48 &self.0
49 }
50}
51
52impl TryFrom<[u8; 16]> for RuntimeSessionId {
53 type Error = RuntimeError;
54
55 fn try_from(value: [u8; 16]) -> Result<Self, Self::Error> {
56 Self::new(value)
57 }
58}
59
60#[repr(u8)]
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum AuthorityState {
64 Uninitialized = 0,
66 LocalStateOpened = 1,
68 HeaderSyncing = 2,
70 HeaderCurrent = 3,
72 ProofReady = 4,
74 ResolutionTransportReady = 5,
76 DnssecVerified = 6,
78 DaneOriginVerified = 7,
80 BrowserBridgeReady = 8,
82 Active = 9,
84 Degraded = 10,
86 Revoked = 11,
88 Stopped = 12,
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub struct RuntimeSnapshot {
95 schema_version: u16,
97 session: RuntimeSessionId,
99 generation: u64,
101 event_sequence: u64,
103 authority_state: AuthorityState,
105}
106
107impl RuntimeSnapshot {
108 #[must_use]
110 pub const fn schema_version(&self) -> u16 {
111 self.schema_version
112 }
113
114 #[must_use]
116 pub const fn session_id(&self) -> RuntimeSessionId {
117 self.session
118 }
119
120 #[must_use]
122 pub const fn session_bytes(&self) -> [u8; 16] {
123 self.session.into_bytes()
124 }
125
126 #[must_use]
128 pub const fn generation(&self) -> u64 {
129 self.generation
130 }
131
132 #[must_use]
134 pub const fn event_sequence(&self) -> u64 {
135 self.event_sequence
136 }
137
138 #[must_use]
140 pub const fn authority_state(&self) -> AuthorityState {
141 self.authority_state
142 }
143}
144
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub struct RuntimeStamp {
148 session: RuntimeSessionId,
149 generation: u64,
150 event_sequence: u64,
151}
152
153impl RuntimeStamp {
154 #[must_use]
156 pub const fn session(self) -> [u8; 16] {
157 self.session.into_bytes()
158 }
159
160 #[must_use]
162 pub const fn generation(self) -> u64 {
163 self.generation
164 }
165
166 #[must_use]
168 pub const fn event_sequence(self) -> u64 {
169 self.event_sequence
170 }
171}
172
173#[derive(Debug, Eq, PartialEq)]
178pub struct BrowserRuntime {
179 session: RuntimeSessionId,
180 generation: u64,
181 event_sequence: u64,
182 invalidation_sequence: u64,
183 authority_state: AuthorityState,
184}
185
186impl BrowserRuntime {
187 #[must_use]
189 pub const fn new(session: RuntimeSessionId) -> Self {
190 Self {
191 session,
192 generation: 1,
193 event_sequence: 0,
194 invalidation_sequence: 0,
195 authority_state: AuthorityState::Uninitialized,
196 }
197 }
198
199 #[must_use]
201 pub const fn snapshot(&self) -> RuntimeSnapshot {
202 RuntimeSnapshot {
203 schema_version: RUNTIME_SCHEMA_VERSION,
204 session: self.session,
205 generation: self.generation,
206 event_sequence: self.event_sequence,
207 authority_state: self.authority_state,
208 }
209 }
210
211 #[must_use]
213 pub const fn authority_state(&self) -> AuthorityState {
214 self.authority_state
215 }
216
217 pub const fn ensure_policy_change_capacity(&self) -> Result<(), RuntimeError> {
224 if self.generation == u64::MAX || self.event_sequence == u64::MAX {
225 return Err(RuntimeError::CounterExhausted);
226 }
227 Ok(())
228 }
229
230 pub fn policy_changed(&mut self) -> Result<RuntimeSnapshot, RuntimeError> {
237 self.ensure_policy_change_capacity()?;
238 self.generation += 1;
239 self.event_sequence += 1;
240 self.invalidation_sequence = self.event_sequence;
241 if self.authority_state != AuthorityState::Stopped {
242 self.authority_state = AuthorityState::Revoked;
243 }
244 Ok(self.snapshot())
245 }
246
247 pub fn transition(&mut self, next: AuthorityState) -> Result<RuntimeSnapshot, RuntimeError> {
255 if !valid_authority_transition(self.authority_state, next) {
256 return Err(RuntimeError::InvalidAuthorityTransition);
257 }
258 let event_sequence = self
259 .event_sequence
260 .checked_add(1)
261 .ok_or(RuntimeError::CounterExhausted)?;
262 self.event_sequence = event_sequence;
263 if matches!(
264 next,
265 AuthorityState::Degraded | AuthorityState::Revoked | AuthorityState::Stopped
266 ) {
267 self.invalidation_sequence = event_sequence;
268 }
269 self.authority_state = next;
270 Ok(self.snapshot())
271 }
272
273 pub fn admit_event(&mut self) -> Result<RuntimeStamp, RuntimeError> {
281 if self.authority_state == AuthorityState::Stopped {
282 return Err(RuntimeError::Stopped);
283 }
284 if !matches!(
285 self.authority_state,
286 AuthorityState::ResolutionTransportReady
287 | AuthorityState::DnssecVerified
288 | AuthorityState::DaneOriginVerified
289 | AuthorityState::BrowserBridgeReady
290 | AuthorityState::Active
291 ) {
292 return Err(RuntimeError::AuthorityNotReady);
293 }
294 self.event_sequence = self
295 .event_sequence
296 .checked_add(1)
297 .ok_or(RuntimeError::CounterExhausted)?;
298 Ok(RuntimeStamp {
299 session: self.session,
300 generation: self.generation,
301 event_sequence: self.event_sequence,
302 })
303 }
304
305 #[must_use]
307 pub fn admits(&self, stamp: RuntimeStamp) -> bool {
308 authority_admits_work(self.authority_state)
309 && stamp.session == self.session
310 && stamp.generation == self.generation
311 && stamp.event_sequence > self.invalidation_sequence
312 && stamp.event_sequence <= self.event_sequence
313 }
314}
315
316const fn authority_admits_work(state: AuthorityState) -> bool {
317 matches!(
318 state,
319 AuthorityState::ResolutionTransportReady
320 | AuthorityState::DnssecVerified
321 | AuthorityState::DaneOriginVerified
322 | AuthorityState::BrowserBridgeReady
323 | AuthorityState::Active
324 )
325}
326
327const fn valid_authority_transition(current: AuthorityState, next: AuthorityState) -> bool {
328 use AuthorityState::{
329 Active, BrowserBridgeReady, DaneOriginVerified, Degraded, DnssecVerified, HeaderCurrent,
330 HeaderSyncing, LocalStateOpened, ProofReady, ResolutionTransportReady, Revoked, Stopped,
331 Uninitialized,
332 };
333 matches!(
334 (current, next),
335 (Uninitialized, LocalStateOpened)
336 | (LocalStateOpened | Degraded | Revoked, HeaderSyncing)
337 | (HeaderSyncing, HeaderCurrent)
338 | (HeaderCurrent, ProofReady)
339 | (ProofReady, ResolutionTransportReady)
340 | (
341 ResolutionTransportReady,
342 DnssecVerified | BrowserBridgeReady
343 )
344 | (DnssecVerified, DaneOriginVerified | BrowserBridgeReady)
345 | (DaneOriginVerified, BrowserBridgeReady)
346 | (BrowserBridgeReady, Active)
347 | (
348 Uninitialized
349 | LocalStateOpened
350 | HeaderSyncing
351 | HeaderCurrent
352 | ProofReady
353 | ResolutionTransportReady
354 | DnssecVerified
355 | DaneOriginVerified
356 | BrowserBridgeReady
357 | Active,
358 Degraded | Revoked | Stopped
359 )
360 | (Degraded | Revoked, Stopped)
361 )
362}
363
364#[derive(Clone, Copy, Debug, Eq, PartialEq)]
366pub enum RuntimeError {
367 ZeroSession,
369 InvalidAuthorityTransition,
371 CounterExhausted,
373 Stopped,
375 AuthorityNotReady,
377}
378
379impl fmt::Display for RuntimeError {
380 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381 match self {
382 Self::ZeroSession => formatter.write_str("runtime session must be nonzero"),
383 Self::InvalidAuthorityTransition => {
384 formatter.write_str("invalid browser authority state transition")
385 }
386 Self::CounterExhausted => formatter.write_str("browser runtime counter exhausted"),
387 Self::Stopped => formatter.write_str("browser runtime is stopped"),
388 Self::AuthorityNotReady => {
389 formatter.write_str("browser runtime authority is not ready")
390 }
391 }
392 }
393}
394
395impl Error for RuntimeError {}
396
397#[cfg(test)]
398#[allow(
399 clippy::unwrap_used,
400 reason = "tests fail immediately on invalid lifecycle fixtures"
401)]
402mod tests {
403 use super::*;
404
405 const ALL_STATES: [AuthorityState; 13] = [
406 AuthorityState::Uninitialized,
407 AuthorityState::LocalStateOpened,
408 AuthorityState::HeaderSyncing,
409 AuthorityState::HeaderCurrent,
410 AuthorityState::ProofReady,
411 AuthorityState::ResolutionTransportReady,
412 AuthorityState::DnssecVerified,
413 AuthorityState::DaneOriginVerified,
414 AuthorityState::BrowserBridgeReady,
415 AuthorityState::Active,
416 AuthorityState::Degraded,
417 AuthorityState::Revoked,
418 AuthorityState::Stopped,
419 ];
420
421 const ALLOWED_TRANSITIONS: &[(AuthorityState, AuthorityState)] = &[
422 (
423 AuthorityState::Uninitialized,
424 AuthorityState::LocalStateOpened,
425 ),
426 (
427 AuthorityState::LocalStateOpened,
428 AuthorityState::HeaderSyncing,
429 ),
430 (AuthorityState::HeaderSyncing, AuthorityState::HeaderCurrent),
431 (AuthorityState::HeaderCurrent, AuthorityState::ProofReady),
432 (
433 AuthorityState::ProofReady,
434 AuthorityState::ResolutionTransportReady,
435 ),
436 (
437 AuthorityState::ResolutionTransportReady,
438 AuthorityState::DnssecVerified,
439 ),
440 (
441 AuthorityState::ResolutionTransportReady,
442 AuthorityState::BrowserBridgeReady,
443 ),
444 (
445 AuthorityState::DnssecVerified,
446 AuthorityState::DaneOriginVerified,
447 ),
448 (
449 AuthorityState::DnssecVerified,
450 AuthorityState::BrowserBridgeReady,
451 ),
452 (
453 AuthorityState::DaneOriginVerified,
454 AuthorityState::BrowserBridgeReady,
455 ),
456 (AuthorityState::BrowserBridgeReady, AuthorityState::Active),
457 (AuthorityState::Degraded, AuthorityState::HeaderSyncing),
458 (AuthorityState::Revoked, AuthorityState::HeaderSyncing),
459 (AuthorityState::Degraded, AuthorityState::Stopped),
460 (AuthorityState::Revoked, AuthorityState::Stopped),
461 ];
462
463 fn session(byte: u8) -> RuntimeSessionId {
464 RuntimeSessionId::new([byte; 16]).unwrap()
465 }
466
467 fn make_resolution_ready(runtime: &mut BrowserRuntime) {
468 for state in [
469 AuthorityState::LocalStateOpened,
470 AuthorityState::HeaderSyncing,
471 AuthorityState::HeaderCurrent,
472 AuthorityState::ProofReady,
473 AuthorityState::ResolutionTransportReady,
474 ] {
475 runtime.transition(state).unwrap();
476 }
477 }
478
479 #[test]
480 fn advances_only_through_the_required_authority_graph() {
481 let mut runtime = BrowserRuntime::new(session(1));
482 for state in [
483 AuthorityState::LocalStateOpened,
484 AuthorityState::HeaderSyncing,
485 AuthorityState::HeaderCurrent,
486 AuthorityState::ProofReady,
487 AuthorityState::ResolutionTransportReady,
488 AuthorityState::DnssecVerified,
489 AuthorityState::DaneOriginVerified,
490 AuthorityState::BrowserBridgeReady,
491 AuthorityState::Active,
492 ] {
493 runtime.transition(state).unwrap();
494 }
495 assert_eq!(runtime.authority_state(), AuthorityState::Active);
496 assert_eq!(runtime.snapshot().event_sequence, 9);
497 assert_eq!(
498 runtime.transition(AuthorityState::HeaderCurrent),
499 Err(RuntimeError::InvalidAuthorityTransition)
500 );
501 assert_eq!(runtime.authority_state(), AuthorityState::Active);
502 }
503
504 #[test]
505 fn failure_paths_are_explicit_and_stopped_is_terminal() {
506 let mut runtime = BrowserRuntime::new(session(2));
507 assert_eq!(runtime.admit_event(), Err(RuntimeError::AuthorityNotReady));
508 runtime.transition(AuthorityState::Degraded).unwrap();
509 runtime.transition(AuthorityState::HeaderSyncing).unwrap();
510 runtime.transition(AuthorityState::Revoked).unwrap();
511 runtime.transition(AuthorityState::Stopped).unwrap();
512 assert_eq!(
513 runtime.transition(AuthorityState::HeaderSyncing),
514 Err(RuntimeError::InvalidAuthorityTransition)
515 );
516 assert_eq!(runtime.admit_event(), Err(RuntimeError::Stopped));
517 }
518
519 #[test]
520 fn policy_change_revokes_and_advances_both_clocks() {
521 let mut runtime = BrowserRuntime::new(session(3));
522 runtime
523 .transition(AuthorityState::LocalStateOpened)
524 .unwrap();
525 let before = runtime.snapshot();
526 let after = runtime.policy_changed().unwrap();
527 assert_eq!(after.generation, before.generation + 1);
528 assert_eq!(after.event_sequence, before.event_sequence + 1);
529 assert_eq!(after.authority_state, AuthorityState::Revoked);
530 }
531
532 #[test]
533 fn admission_stamps_reject_other_sessions_generations_and_future_events() {
534 let mut first = BrowserRuntime::new(session(4));
535 let mut second = BrowserRuntime::new(session(5));
536 make_resolution_ready(&mut first);
537 make_resolution_ready(&mut second);
538 let first_stamp = first.admit_event().unwrap();
539 let second_stamp = second.admit_event().unwrap();
540 assert!(first.admits(first_stamp));
541 assert!(!first.admits(second_stamp));
542
543 first.policy_changed().unwrap();
544 assert!(!first.admits(first_stamp));
545
546 let future = RuntimeStamp {
547 session: first.snapshot().session,
548 generation: first.snapshot().generation,
549 event_sequence: first.snapshot().event_sequence + 1,
550 };
551 assert!(!first.admits(future));
552 }
553
554 #[test]
555 fn rejects_zero_runtime_session() {
556 assert_eq!(
557 RuntimeSessionId::new([0; 16]),
558 Err(RuntimeError::ZeroSession)
559 );
560 assert_eq!(
561 RuntimeSessionId::try_from([0; 16]),
562 Err(RuntimeError::ZeroSession)
563 );
564 assert_eq!(session(9).into_bytes(), [9; 16]);
565 }
566
567 #[test]
568 fn authority_discriminants_remain_stable() {
569 for (index, state) in ALL_STATES.into_iter().enumerate() {
570 assert_eq!(usize::from(state as u8), index);
571 }
572 }
573
574 #[test]
575 fn transition_matrix_is_exhaustive() {
576 for current in ALL_STATES {
577 for next in ALL_STATES {
578 let expected = ALLOWED_TRANSITIONS.contains(&(current, next))
579 || (matches!(
580 current,
581 AuthorityState::Uninitialized
582 | AuthorityState::LocalStateOpened
583 | AuthorityState::HeaderSyncing
584 | AuthorityState::HeaderCurrent
585 | AuthorityState::ProofReady
586 | AuthorityState::ResolutionTransportReady
587 | AuthorityState::DnssecVerified
588 | AuthorityState::DaneOriginVerified
589 | AuthorityState::BrowserBridgeReady
590 | AuthorityState::Active
591 ) && matches!(
592 next,
593 AuthorityState::Degraded
594 | AuthorityState::Revoked
595 | AuthorityState::Stopped
596 ));
597 assert_eq!(
598 valid_authority_transition(current, next),
599 expected,
600 "unexpected transition result for {current:?} -> {next:?}"
601 );
602 }
603 }
604 }
605
606 #[test]
607 fn bridge_can_start_before_navigation_and_after_icann_authenticated_absence() {
608 let mut startup = BrowserRuntime::new(session(6));
609 make_resolution_ready(&mut startup);
610 startup
611 .transition(AuthorityState::BrowserBridgeReady)
612 .unwrap();
613 startup.transition(AuthorityState::Active).unwrap();
614 assert_eq!(startup.authority_state(), AuthorityState::Active);
615
616 let mut webpki = BrowserRuntime::new(session(7));
617 make_resolution_ready(&mut webpki);
618 webpki.transition(AuthorityState::DnssecVerified).unwrap();
619 webpki
620 .transition(AuthorityState::BrowserBridgeReady)
621 .unwrap();
622 webpki.transition(AuthorityState::Active).unwrap();
623 assert_eq!(webpki.authority_state(), AuthorityState::Active);
624 }
625
626 #[test]
627 fn admitted_stamp_is_rejected_while_degraded_revoked_or_stopped() {
628 for terminal in [
629 AuthorityState::Degraded,
630 AuthorityState::Revoked,
631 AuthorityState::Stopped,
632 ] {
633 let mut runtime = BrowserRuntime::new(session(8));
634 make_resolution_ready(&mut runtime);
635 let stamp = runtime.admit_event().unwrap();
636 assert!(runtime.admits(stamp));
637 runtime.transition(terminal).unwrap();
638 assert!(!runtime.admits(stamp), "stamp survived {terminal:?}");
639 }
640 }
641
642 #[test]
643 fn admitted_stamp_cannot_resurrect_after_failure_recovery() {
644 for failure in [AuthorityState::Degraded, AuthorityState::Revoked] {
645 let mut runtime = BrowserRuntime::new(session(10));
646 make_resolution_ready(&mut runtime);
647 let stale = runtime.admit_event().unwrap();
648 runtime.transition(failure).unwrap();
649 for state in [
650 AuthorityState::HeaderSyncing,
651 AuthorityState::HeaderCurrent,
652 AuthorityState::ProofReady,
653 AuthorityState::ResolutionTransportReady,
654 ] {
655 runtime.transition(state).unwrap();
656 }
657 assert!(
658 !runtime.admits(stale),
659 "stamp resurrected after {failure:?} recovery"
660 );
661 let current = runtime.admit_event().unwrap();
662 assert!(runtime.admits(current));
663 }
664 }
665
666 #[test]
667 fn maximum_event_sequence_is_a_valid_final_snapshot() {
668 let mut runtime = BrowserRuntime::new(session(11));
669 runtime.event_sequence = u64::MAX - 1;
670 let snapshot = runtime
671 .transition(AuthorityState::LocalStateOpened)
672 .unwrap();
673 assert_eq!(snapshot.event_sequence(), u64::MAX);
674 assert_eq!(
675 runtime.transition(AuthorityState::HeaderSyncing),
676 Err(RuntimeError::CounterExhausted)
677 );
678 }
679}