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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//! How many independent voices the pool must hold, and how long it may hold the same ones.
//!
//! These are the sizing half of NC-12: peers that are UNTRUSTED, plural enough that agreement
//! between them means something, and CYCLED so that a set an attacker captured once does not
//! decide this node's view of the chain for the life of the process.
//!
//! They live beside the pool because the pool is what has to satisfy them. A consumer counting
//! agreeing answers cannot make a pool large enough after the fact.
use Duration;
/// The sample size an agreement ratio is expressed against: four distinct voices.
///
/// Fewer than three cannot express "a majority with one dissenter" at all — with two peers every
/// disagreement is a bare split that says nothing about which side is anomalous. Three leaves no
/// margin: one peer mid-reorg drops the round to a 2-1 that only just clears a supermajority, and
/// a second stalls it entirely. Four costs one more handshake and tolerates one dissenter.
///
/// It is deliberately not larger: every extra member widens the window in which an attacker
/// holding a slice of the discoverable set lands two members in one sample.
pub const QUORUM_SAMPLE: usize = 4;
/// The fewest INDEPENDENT peers, besides the one that answered, that must be available before a
/// corroborated read may be attempted at all.
///
/// Two, because one corroborator is a single second opinion — enough to catch a peer that is
/// simply wrong, not enough to survive one that is lying while another is unreachable.
///
/// **This counts AGREEING ANSWERS, and it is also the number of independent peers that must be
/// HELD before the read is attempted.** The distinction matters across the crate boundary:
/// `dig-node`'s `sage::quorum` uses the same name and the same value for answers received in one
/// round only. Held is the weaker of the two, because a held connection may have died silently
/// since it was last used, so a pool that clears this floor has not yet shown that this many peers
/// will ANSWER. Anything adopting both must not let one stand in for the other — see
/// [`PeerPool::corroboration_readiness`](super::pool::PeerPool::corroboration_readiness), which
/// answers the held question, and `corroborate_presence`, which answers the agreeing one.
///
/// The gate this feeds REFUSES rather than degrading (see
/// [`PeerPool::corroboration_readiness`](super::pool::PeerPool::corroboration_readiness)). A pool
/// that quietly corroborates against whoever happens to be present converts a four-voice quorum
/// into a three- or two-voice one while still reporting the answer as corroborated, and nothing
/// downstream can tell the difference.
///
/// Both denominators are recorded ecosystem-wide in the superproject `canonical` skill, under
/// `CORROBORATION_FLOOR`, which names the HELD and the ANSWERED counts separately and forbids
/// re-exporting either as the other. Read it before adopting this constant in another repo.
pub const CORROBORATION_FLOOR: usize = 2;
/// How long a peer may stay in the pool before it is rotated out.
///
/// Five minutes: long enough that a read and the walk behind it complete over one set of
/// connections, short enough that a captured set does not decide this node's view of the chain for
/// the life of the process. This is NC-12's cycling half; the corroboration floor above is its
/// plurality half, and neither substitutes for the other.
pub const PEER_LIFETIME: Duration = from_secs;
/// How many pool slots the PRIORITY path can occupy.
///
/// Two, because the dialler tries two priority addresses before discovery — an operator's
/// `TRUSTED_FULLNODE` and the loopback — and they are distinct `SocketAddr`s, so on a host running
/// both, BOTH are admitted, and both are admitted as
/// [`PeerOrigin::Priority`](super::connect::PeerOrigin::Priority).
///
/// A priority peer is an excellent peer to ASK and is never an independent voice: a co-resident
/// node is precisely the source a local attacker can supply (dig_ecosystem#2648). So each of these
/// slots is occupied by a connection that cannot corroborate anything, which is why the pool is
/// sized around them.
///
/// It is measured against the dialler rather than asserted — see
/// [`connect::priority_addresses_from`](super::connect::priority_addresses_from) and the test
/// below. Sizing the pool for ONE priority slot while the dialler offered two is what left a full
/// pool three corroborators mid-rotation, below [`QUORUM_SAMPLE`].
pub const PRIORITY_SLOTS: usize = 2;
/// The pool size that leaves [`QUORUM_SAMPLE`] independent voices standing in the normal case.
///
/// Derived, not chosen. Every term below is a slot that is occupied and is NOT an independent
/// corroborating voice:
///
/// 1. **[`PRIORITY_SLOTS`] priority slots**, the addresses tried ahead of discovery.
/// 2. **One slot for the session a subscriber is following.** The wallet replica holds a session
/// for its own frames; the peer it is reading from cannot corroborate itself.
/// 3. **[`QUORUM_SAMPLE`] independent voices** — the sample an agreement ratio is expressed
/// against.
/// 4. **One slot of slack**, so that losing a single peer to attrition, or having one out of the
/// pool mid-rotation, does not immediately drop the sample below its floor. Without it, cycling
/// — which necessarily removes a peer before its replacement connects — would itself be enough
/// to disarm corroboration.
///
/// The previous default of 5 left, in the normal case, **three** usable corroborators — below
/// [`QUORUM_SAMPLE`]. That is the exact shape of a silent regression: a four-voice quorum becomes
/// a three-voice one that still reports itself corroborated. The default of 7 that replaced it
/// counted only ONE priority slot and had the same defect one host short of the worst case.
pub const
const _: = assert!;