1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Anti-bypass `group_id` verification for state-delta governance positions.
//!
//! A signed `governance_position` carries a `group_id` the *sender* chose.
//! This module is the single source of truth for "does that claimed group
//! match the context's owning group?", shared by the apply path and the
//! DAG-catchup paths in `sync::manager` / `sync::delta_request`.
use ContextId;
/// Outcome of the anti-bypass `group_id` check that runs at every
/// apply path consulting a state delta's `governance_position`.
///
/// Two bypasses this check closes:
///
/// 1. **Mismatched `group_id` on a signed position.** A delta with
/// `governance_position: Some(pos)` carries a `group_id` the *sender*
/// chose at sign time. Without verification, a malicious sender could
/// craft a delta for context X (owned by group A) carrying a position
/// with `group_id = B` (a group the sender IS a member of). The
/// cross-DAG membership check would succeed against group B and the
/// write would land in context X without verifying membership in
/// group A.
///
/// 2. **Lying about being a non-group context.** `governance_position:
/// None` skips the cross-DAG check entirely (legacy non-group
/// contexts have no governance DAG). A malicious sender could omit
/// the position on a group-context delta to bypass enforcement. The
/// `GroupContextNoPosition` variant catches this.
///
/// Each call site translates the outcome to its local error handling
/// (warn-message wording, return-value shape, metric labels).
///
/// `pub(crate)` because the DAG-catchup paths in `sync::manager` and
/// `sync::delta_request` now share the same anti-bypass logic — a
/// single source of truth for "does the claimed governance position's
/// group match this context's owning group?". A copy-paste of the
/// match table across modules drifted in review (the DAG-catchup
/// head-pull was running `membership_status_at` without first checking
/// the group_id, leaving the bypass gap open); centralising fixes that
/// for good. New consumers must respect the TOCTOU and forward-only
/// invariants documented on `verify_position_group_id_matches_context`.
pub
// Hand-written `Debug` (rather than `#[derive(Debug)]`) because the
// `LookupError` variant wraps an `eyre::Error`, which we want to render
// via its own `Debug` impl rather than expose the full backtrace.
// Available in production code (not just tests) so call sites can
// debug-print outcomes in tracing spans.
/// Anti-bypass check for the apply-path consumers of a state delta's
/// `governance_position`. The `claimed_group_id` argument is the
/// `group_id` from `Some(pos)` (the sender's signed claim), or `None`
/// when the delta has no position. Returns the outcome each call site
/// interprets to log + recover in its local idiom. See [`GroupIdCheck`]
/// for the bypasses this closes.
///
/// **TOCTOU note.** Each call site runs this check immediately before
/// `membership_status_at`, which internally walks the governance DAG
/// scoped to `pos.group_id`. Between the two calls no lock is held;
/// in principle a concurrent governance op could reassign the context
/// to a different group, leaving the bypass check satisfied against
/// the old group while the membership walk runs against the new one.
/// In practice the `ContextManager` actor applies governance ops
/// sequentially, so no concurrent reassignment can interleave between
/// the check and the membership walk. The actor isolation is the
/// invariant that mitigates the TOCTOU window; if that ever changes,
/// the check needs to be promoted to a snapshot read across both
/// lookups.
pub