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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
//! Per-user on-disk secret-byte quota tracking for the hosted-mode
//! inactive-user TTL and storage cap (P5 of #4381, #4561).
//!
//! The [`QuotaTracker`] is a process-global singleton (via [`USER_QUOTA_TRACKER`])
//! so all pooled `SecretsStore` instances sharing a `secrets_dir` see the same
//! per-user counters.
use ;
use DashMap;
use UserId;
/// Default per-user secret-storage quota: 4 MiB of on-disk ciphertext per
/// [`UserId`], summed across every delegate that user has secrets under
/// (#4561, P5 of #4381). Bounds a single hosted visitor's disk footprint so
/// they cannot fill the node's disk. Operator-overridable via
/// `--per-user-secret-quota=BYTES` / `per-user-secret-quota` in config; `0`
/// disables enforcement entirely.
pub const DEFAULT_PER_USER_SECRET_QUOTA_BYTES: usize = 4 * 1024 * 1024;
/// Default inactivity TTL for hosted users (#4561, P5 of #4381): 30 days in
/// seconds. After this much real-calendar inactivity a hosted user's entire
/// per-user footprint is reclaimed by the background sweep, keeping a public
/// "try Freenet" node a transient demo with bounded storage. Operator-
/// overridable via `--per-user-inactive-ttl=SECS` / `per-user-inactive-ttl`
/// in config; `0` disables the sweep. Single source of truth for the in-code
/// default so the operator-facing default and the config fallback never drift.
pub const DEFAULT_PER_USER_INACTIVE_TTL_SECS: u64 = 2_592_000;
/// Maximum users reclaimed in ONE sweep pass (#4561, clock-fault blast-radius
/// guard). The reclaim is destructive, and the per-user eligibility test trusts
/// wall-clock `now`; a forward clock jump (NTP correcting a bad RTC, a date
/// misconfig, a VM restored from a skewed snapshot) would make EVERY user's age
/// suddenly exceed the TTL, so an unbounded pass could silently delete the whole
/// user base at once. Capping the pass bounds that blast radius and — paired
/// with a LOUD warn when the cap is hit — surfaces the anomaly so an operator
/// can intervene before more is lost. Sized generously: a normal idle backlog
/// (hundreds of genuinely-abandoned users) drains over a few hourly sweeps,
/// while a clock fault can never wipe more than this many users per pass. The
/// remainder is simply deferred to the next sweep (the reclaim is idempotent and
/// crash-safe, so deferral is free).
pub const MAX_RECLAIMS_PER_SWEEP: usize = 256;
/// Multiplier on the sweep interval defining the largest plausible gap between
/// two consecutive sweeps (#4561, clock-fault detector). If wall-clock `now`
/// advanced by MORE than `sweep_interval * this` since the previous sweep, the
/// clock almost certainly jumped forward (or the node was suspended/restored
/// across a long gap) rather than the interval having elapsed normally, so the
/// sweep SKIPS reclaim for that pass and warns. This catches a forward jump
/// BETWEEN sweeps before ANY delete happens, and conservatively skips a single
/// pass after legitimate long downtime (the users restamp on reconnect and the
/// next pass proceeds normally). 8× an hourly interval is 8h — comfortably above
/// jitter/missed-tick delay, well below a real multi-day skew.
pub const SWEEP_MAX_GAP_INTERVAL_MULTIPLE: u64 = 8;
/// Process-wide tracker of per-[`UserId`] on-disk secret-byte totals.
///
/// # Why this is a process-global rather than a per-store field
///
/// Each pooled `Executor<Runtime>` builds its OWN [`super::store::SecretsStore`] with its
/// own in-memory index, but they all write to the SAME `secrets_dir`. A quota
/// counter living on one store would diverge across executors exactly the way
/// the per-executor secret index did before the export fix walked the disk
/// instead (see [`super::store::SecretsStore::walk_scope_secrets_on_disk`]). So the per-user
/// totals must live in ONE structure every executor's store references. A
/// process-global `LazyLock<DashMap>` is the established pattern in this
/// subsystem for precisely this "shared across all pooled executors" need —
/// see the several `LazyLock<DashMap<...>>` statics in `native_api.rs`. The
/// `UserId` key is a globally-unique 32-byte hash, so cross-node/cross-delegate
/// collisions are impossible and a single global namespace is correct.
///
/// # Concurrency — the no-CAS safety is NON-LOCAL
///
/// Delegate execution is SERIAL on the contract loop — only one `store_secret`
/// runs at a time node-wide — so the increment/decrement are never actually
/// contended and a plain map would suffice for correctness. The per-user value
/// is held as an [`AtomicU64`](std::sync::atomic::AtomicU64) as cheap insurance
/// (and so reads never need the `DashMap` write path), but no compare-and-swap
/// is required precisely because of that serial-execution guarantee.
///
/// WARNING: this safety rests on a property OUTSIDE this module. Two things
/// would race if delegate `process()` were ever offloaded to run concurrently
/// across pooled executors:
///
/// 1. the load-then-store decrement in [`Self::sub_saturating`] (lost update),
/// and
/// 2. the check→commit gap in `store_secret` — the quota admission check reads
/// the total, then the counter is bumped only AFTER the rename, so two
/// concurrent admissions could both pass against the same pre-write total
/// and jointly exceed the limit.
///
/// If delegate execution ever becomes concurrent, BOTH the decrement and the
/// admission/commit must move to a real compare-and-swap (or a per-user lock);
/// an `AtomicU64` alone is not enough. Keep this guarantee in mind before
/// parallelizing the contract loop.
///
/// # Keying — `(secrets_dir, UserId)`, NOT `UserId` alone
///
/// The counter is keyed by the OWNING STORE's `base_path` (its `secrets_dir`)
/// PLUS the `UserId`. Pooled executors of one node all build their stores
/// against the SAME `secrets_dir`, so they still share a user's counter (the
/// shared-counter property). But two INDEPENDENT stores with DIFFERENT
/// `base_path`s — e.g. several simulated nodes in one test process, or a future
/// multi-store host — that happen to see the same hosted token (same `UserId`)
/// must NOT collide: each enforces against its OWN on-disk tree. Keying on
/// `UserId` alone made store B reuse store A's count instead of seeding against
/// B's disk, causing false rejections / under-counts. The `base_path`
/// namespaces them.
///
/// # Seeding
///
/// A user's entry is seeded LAZILY on first touch by summing their on-disk blob
/// sizes (see [`super::store::SecretsStore::seeded_user_total`]); thereafter every op is O(1).
pub
/// The single process-wide quota tracker. Lazily initialized; shared by every
/// [`super::store::SecretsStore`] in the process (production pool executors and tests alike),
/// but namespaced per `secrets_dir` so stores over DIFFERENT trees don't collide.
pub static USER_QUOTA_TRACKER: LazyLock =
new;
/// Apply a signed byte delta to a `(base_path, user)` footprint total: positive
/// → add, negative → saturating subtract. Used by `store_secret` to fold in the
/// value-blob delta and the `.keys`-registry delta (either can be negative on
/// an overwrite/registry-shrink). `i128` cannot overflow for any realistic
/// `u64` byte size pair.
pub
/// Captured under the pre-write borrow in `store_secret` and consumed after the
/// rename + `register_key` commit to update the per-user footprint counter. We
/// stat the value blob's old size and the `.keys` registry's old size BEFORE
/// the write, then re-stat the real post-write sizes to fold in exact deltas.
pub
/// Test-only view of a `(base_path, user)`'s currently-tracked total (`None` if
/// unseeded). Lets a test observe the SHARED process-global counter directly —
/// including confirming a write through one store over a given `secrets_dir` is
/// visible to another store over the SAME dir (and isolated from a different
/// dir).
pub
/// Test-only reset of a single `(base_path, user)` tracker entry, so each quota
/// test starts from a clean slate against the process-global tracker regardless
/// of run order.
pub