@optimystic/db-p2p 0.22.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cluster/client.d.ts +10 -0
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +30 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +39 -14
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +226 -119
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/service.d.ts +32 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +43 -2
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +37 -7
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +77 -19
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +9 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +88 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +134 -28
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +25 -1
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +17 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -2
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/owned-block-seed.d.ts +6 -3
- package/dist/src/owned-block-seed.d.ts.map +1 -1
- package/dist/src/owned-block-seed.js +16 -3
- package/dist/src/owned-block-seed.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +72 -0
- package/dist/src/peer-address-book.d.ts.map +1 -0
- package/dist/src/peer-address-book.js +123 -0
- package/dist/src/peer-address-book.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +11 -2
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +30 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +95 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +62 -9
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +242 -73
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +3 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +3 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +83 -0
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/cached-raw-storage.js +152 -0
- package/dist/src/storage/cached-raw-storage.js.map +1 -0
- package/dist/src/storage/cached-store-driver.d.ts +186 -0
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
- package/dist/src/storage/cached-store-driver.js +775 -0
- package/dist/src/storage/cached-store-driver.js.map +1 -0
- package/dist/src/storage/i-raw-storage.d.ts +12 -5
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +234 -0
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
- package/dist/src/storage/shared-cache-pool.js +354 -0
- package/dist/src/storage/shared-cache-pool.js.map +1 -0
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +35 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -668
- package/src/cluster/block-transfer.ts +424 -424
- package/src/cluster/client.ts +119 -88
- package/src/cluster/cluster-error.ts +64 -64
- package/src/cluster/cluster-policy.ts +203 -203
- package/src/cluster/cluster-repo.ts +242 -122
- package/src/cluster/cluster-size-coupling.ts +45 -45
- package/src/cluster/commit-cert.ts +139 -139
- package/src/cluster/i-transaction-state-store.ts +43 -43
- package/src/cluster/memory-transaction-state-store.ts +56 -56
- package/src/cluster/peer-key-binding.ts +37 -37
- package/src/cluster/persistent-transaction-state-store.ts +92 -92
- package/src/cluster/quorum-restore.ts +223 -223
- package/src/cluster/reconcile-block.ts +203 -203
- package/src/cluster/service.ts +293 -241
- package/src/cluster/supermajority-coupling.ts +37 -37
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
- package/src/cohort-topic/change-bridge.ts +109 -109
- package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
- package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
- package/src/cohort-topic/fret-trust-anchor.ts +153 -153
- package/src/cohort-topic/host.ts +42 -11
- package/src/cohort-topic/index.ts +13 -13
- package/src/cohort-topic/membership-publish-sink.ts +20 -20
- package/src/cohort-topic/membership-source.ts +68 -68
- package/src/cohort-topic/peer-codec.ts +31 -31
- package/src/cohort-topic/peer-sig.ts +86 -86
- package/src/cohort-topic/protocols.ts +71 -71
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
- package/src/cohort-topic/size-estimator.ts +16 -16
- package/src/cohort-topic/stream-util.ts +79 -19
- package/src/cohort-topic/threshold-crypto.ts +239 -239
- package/src/cohort-topic/topic-router.ts +77 -77
- package/src/dispute/arbitrator-selection.ts +138 -138
- package/src/dispute/cascade.ts +524 -524
- package/src/dispute/dispute-service.ts +11 -5
- package/src/dispute/invalidation.ts +625 -625
- package/src/inbound-authorization.ts +190 -190
- package/src/index.ts +52 -49
- package/src/libp2p-key-network.ts +1120 -990
- package/src/libp2p-node-base.ts +1675 -1651
- package/src/libp2p-node-rn.ts +30 -30
- package/src/libp2p-node.ts +36 -36
- package/src/logger.ts +19 -2
- package/src/matchmaking/aggregate-counts.ts +104 -104
- package/src/matchmaking/index.ts +20 -20
- package/src/matchmaking/module.ts +363 -363
- package/src/matchmaking/protocols.ts +51 -51
- package/src/matchmaking/provider-manager.ts +95 -95
- package/src/matchmaking/query-handler.ts +88 -88
- package/src/matchmaking/query-transport.ts +3 -3
- package/src/matchmaking/seeker-manager.ts +64 -64
- package/src/matchmaking/seeker-walk-client.ts +293 -293
- package/src/matchmaking/traffic-validation.ts +195 -195
- package/src/optimystic-node.ts +36 -36
- package/src/owned-block-seed.ts +53 -40
- package/src/peer-address-book.ts +149 -0
- package/src/protocol-limits.ts +33 -33
- package/src/reactivity/forwarder-host.ts +438 -438
- package/src/reactivity/index.ts +19 -19
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/origination-manager.ts +192 -192
- package/src/reactivity/protocols.ts +61 -61
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +7 -3
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
- package/src/reactivity/subscriber-registry.ts +96 -96
- package/src/reactivity/subscription-manager.ts +450 -450
- package/src/reactivity/topic-bytes.ts +37 -37
- package/src/repo/client.ts +12 -2
- package/src/repo/cluster-coordinator.ts +99 -3
- package/src/repo/coordinator-repo.ts +281 -74
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -36
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- package/src/storage/i-kv-store.ts +8 -8
- package/src/storage/i-raw-storage.ts +12 -5
- package/src/storage/kv-raw-storage.ts +135 -135
- package/src/storage/memory-kv-store.ts +28 -28
- package/src/storage/memory-storage.ts +25 -25
- package/src/storage/memory-store-driver.ts +157 -157
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +80 -80
- package/src/storage/ring-selector.ts +317 -317
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/shared-cache-pool.ts +452 -0
- package/src/storage/storage-repo.ts +1014 -1014
- package/src/testing/cohort-topic-mesh-harness.ts +673 -663
- package/src/testing/index.ts +8 -8
- package/src/testing/matchmaking-mesh-harness.ts +475 -475
- package/src/testing/raw-storage-conformance.ts +453 -417
- package/src/testing/reactivity-mesh-harness.ts +922 -922
- package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
- package/dist/src/storage/restoration-coordinator-v2.js +0 -172
- package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Matchmaking — seeker-side adversarial traffic-reporting bounds + reputation cross-check hooks (db-p2p).
|
|
3
|
-
*
|
|
4
|
-
* `docs/matchmaking.md` §Adversarial cohort traffic reporting. A `QueryReplyV1.topicTraffic` (and the
|
|
5
|
-
* registration reply's traffic) is signed by the cohort **primary's single member key, not a threshold
|
|
6
|
-
* signature**, because the response is advisory — so a malicious primary can over- or under-report. The
|
|
7
|
-
* doc proves the harm is bounded either way; this module makes those bounds explicit for the seeker walk
|
|
8
|
-
* and emits the cross-check signals the reputation subsystem consumes.
|
|
9
|
-
*
|
|
10
|
-
* - **Over-reporting** (fake *hot* tier ⇒ seeker hangs out): bounded by the seeker's `patienceMs`. The
|
|
11
|
-
* worst case is wasted patience plus the one `register → walk` hop after timeout — there is **no
|
|
12
|
-
* spatial flood**, because the decision rule only ever walks *toward the root*, never speculatively
|
|
13
|
-
* outward. {@link boundReportedTraffic} therefore caps any hang-out to the seeker's remaining
|
|
14
|
-
* wall-clock patience ({@link TrafficBoundResult.capPatienceMs}).
|
|
15
|
-
* - **Under-reporting** (fake *cold* tier ⇒ seeker escalates): bounded to **one extra hop per affected
|
|
16
|
-
* tier**, terminating at the root where aggregated truth is hardest to fake
|
|
17
|
-
* ({@link TrafficBoundResult.escalateAfterTiers}).
|
|
18
|
-
* - **Cross-check via cohort gossip → reputation.** Other cohort members can detect a primary whose
|
|
19
|
-
* reported rate diverges from the gossip-derived view. Detection *routing* is the reputation
|
|
20
|
-
* subsystem's job (out of scope to implement); this module provides the **emission points**
|
|
21
|
-
* ({@link TrafficCrossCheckSignal}) and a thin bridge ({@link reportTrafficCrossCheck}) into the
|
|
22
|
-
* existing {@link IPeerReputation}. It scores nothing itself — the reputation subsystem owns the
|
|
23
|
-
* aggregation/decay policy that turns a stream of advisory signals into an actual penalty.
|
|
24
|
-
*
|
|
25
|
-
* **GROUNDING (matchmaking.md §Adversarial traffic reporting):** no threshold signature is added per
|
|
26
|
-
* `QueryReplyV1` — the single-member signature stands; the bounded worst-case here does not justify the
|
|
27
|
-
* per-reply threshold cost. This module enforces the bounds purely seeker-side instead.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { type QueryReplyV1 } from "@optimystic/db-core";
|
|
31
|
-
import { PenaltyReason, type IPeerReputation } from "../reputation/index.js";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* The seeker's running walk state at the moment a reply lands — enough to bound the reply's reported
|
|
35
|
-
* traffic and attribute a cross-check signal. The seeker walk client (or the public seeker session)
|
|
36
|
-
* supplies it per reply.
|
|
37
|
-
*/
|
|
38
|
-
export interface SeekerWalkState {
|
|
39
|
-
/** Tree tier `d` the reply came from. */
|
|
40
|
-
readonly currentTier: number;
|
|
41
|
-
/** Starting tier `d_max` (the walk only descends from here toward `0`). */
|
|
42
|
-
readonly dMax: number;
|
|
43
|
-
/** Register hops issued so far (probes + escalations). Bounded by `d_max + 1` — the walk never loops. */
|
|
44
|
-
readonly tiersWalked: number;
|
|
45
|
-
/** Total patience budget for the task (ms). */
|
|
46
|
-
readonly patienceMs: number;
|
|
47
|
-
/** Patience left on the wall-clock deadline (ms) — the hang-out cap. */
|
|
48
|
-
readonly patienceRemainingMs: number;
|
|
49
|
-
/** Filter-matched providers the seeker's *own* immediate `QueryV1` at this tier actually returned. */
|
|
50
|
-
readonly observedMatches: number;
|
|
51
|
-
/** The reply's cohort primary peer id, for reputation attribution (absent → no attribution). */
|
|
52
|
-
readonly primaryId?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Tunables for {@link boundReportedTraffic}. */
|
|
56
|
-
export interface TrafficBoundConfig {
|
|
57
|
-
/**
|
|
58
|
-
* Upper bound on extra register hops a single under-reported tier may cost. The doc fixes this at
|
|
59
|
-
* **1** ("one extra hop per affected tier"); exposed for tests / future policy, not meant to change.
|
|
60
|
-
*/
|
|
61
|
-
readonly maxExtraHopsPerTier: number;
|
|
62
|
-
/**
|
|
63
|
-
* Over-report plausibility ratio. When a reply claims `directParticipants` more than this multiple of
|
|
64
|
-
* the seeker's *own* query yield, the over-report is flagged *suspect* (a cross-check signal — never a
|
|
65
|
-
* hard reject; the patience cap already bounds the harm).
|
|
66
|
-
*/
|
|
67
|
-
readonly overReportSuspectRatio: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** The documented defaults (`maxExtraHopsPerTier = 1`; suspect ratio chosen to tolerate normal churn). */
|
|
71
|
-
export const DEFAULT_TRAFFIC_BOUND_CONFIG: TrafficBoundConfig = {
|
|
72
|
-
maxExtraHopsPerTier: 1,
|
|
73
|
-
overReportSuspectRatio: 8,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/** The kind of divergence a {@link TrafficCrossCheckSignal} reports. */
|
|
77
|
-
export type TrafficCrossCheckKind = "over-report-suspected" | "under-report-suspected";
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* One advisory cross-check observation about a cohort primary's reported traffic. Emitted by
|
|
81
|
-
* {@link boundReportedTraffic}; consumed by the reputation subsystem (via {@link reportTrafficCrossCheck}
|
|
82
|
-
* or a custom sink). Carries the raw discrepancy so the reputation policy — not this module — decides
|
|
83
|
-
* whether it warrants a penalty.
|
|
84
|
-
*/
|
|
85
|
-
export interface TrafficCrossCheckSignal {
|
|
86
|
-
readonly kind: TrafficCrossCheckKind;
|
|
87
|
-
/** The cohort primary peer id (the subject), if the walk state attributed one. */
|
|
88
|
-
readonly subjectId?: string;
|
|
89
|
-
/** The tier the reply came from. */
|
|
90
|
-
readonly tier: number;
|
|
91
|
-
/** `topicTraffic.directParticipants` as reported. */
|
|
92
|
-
readonly reportedDirectParticipants: number;
|
|
93
|
-
/** `topicTraffic.arrivalsPerMin` as reported. */
|
|
94
|
-
readonly reportedArrivalsPerMin: number;
|
|
95
|
-
/** The seeker's own immediate-query yield, which the report is cross-checked against. */
|
|
96
|
-
readonly observedMatches: number;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** The outcome of {@link boundReportedTraffic}. */
|
|
100
|
-
export interface TrafficBoundResult {
|
|
101
|
-
/** Whether the reported traffic is within plausible bounds (advisory; a flag, not an admission). */
|
|
102
|
-
readonly trusted: boolean;
|
|
103
|
-
/**
|
|
104
|
-
* Over-report bound: never hang out at this tier beyond the seeker's remaining wall-clock patience.
|
|
105
|
-
* Equals `max(0, patienceRemainingMs)` — so a fabricated hot tier wastes at most that, then the walk
|
|
106
|
-
* proceeds with its one register hop.
|
|
107
|
-
*/
|
|
108
|
-
readonly capPatienceMs: number;
|
|
109
|
-
/** Under-report bound: at most this many extra register hops attributable to one tier (== config). */
|
|
110
|
-
readonly escalateAfterTiers: number;
|
|
111
|
-
/** Cross-check emission points (possibly empty). Forward to the reputation subsystem if desired. */
|
|
112
|
-
readonly reputationSignals: TrafficCrossCheckSignal[];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Bound the harm of a (possibly adversarial) `topicTraffic` report and emit cross-check signals. Pure —
|
|
117
|
-
* no I/O, no clock, no reputation scoring. The seeker walk consults `capPatienceMs` before hanging out
|
|
118
|
-
* and treats `escalateAfterTiers` as the per-tier hop ceiling; the bounds it returns hold *by
|
|
119
|
-
* construction* of the toward-root-only walk, so this function documents + asserts them rather than
|
|
120
|
-
* changing the walk topology.
|
|
121
|
-
*/
|
|
122
|
-
export function boundReportedTraffic(
|
|
123
|
-
reply: QueryReplyV1,
|
|
124
|
-
walkState: SeekerWalkState,
|
|
125
|
-
cfg: TrafficBoundConfig = DEFAULT_TRAFFIC_BOUND_CONFIG,
|
|
126
|
-
): TrafficBoundResult {
|
|
127
|
-
const traffic = reply.topicTraffic;
|
|
128
|
-
const signals: TrafficCrossCheckSignal[] = [];
|
|
129
|
-
|
|
130
|
-
// Over-report: a primary advertising a hot tier (many directParticipants) whose own query yields far
|
|
131
|
-
// fewer matches is suspect. Harm is bounded regardless — the patience cap below means a fabricated hot
|
|
132
|
-
// tier can only waste the seeker's remaining patience before it walks on (no spatial flood, since the
|
|
133
|
-
// walk only steps toward the root).
|
|
134
|
-
const overReportSuspect =
|
|
135
|
-
traffic.directParticipants > cfg.overReportSuspectRatio * Math.max(walkState.observedMatches, 1);
|
|
136
|
-
if (overReportSuspect) {
|
|
137
|
-
signals.push(crossCheckSignal("over-report-suspected", traffic, walkState));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Under-report: a primary claiming a cold tier (zero arrivals) while the seeker's own query yields
|
|
141
|
-
// matches is suspect. Either way the escalation costs at most one extra hop for this tier and
|
|
142
|
-
// terminates at the root.
|
|
143
|
-
const underReportSuspect = traffic.arrivalsPerMin === 0 && walkState.observedMatches > 0;
|
|
144
|
-
if (underReportSuspect) {
|
|
145
|
-
signals.push(crossCheckSignal("under-report-suspected", traffic, walkState));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
trusted: !overReportSuspect && !underReportSuspect,
|
|
150
|
-
capPatienceMs: Math.max(0, walkState.patienceRemainingMs),
|
|
151
|
-
escalateAfterTiers: cfg.maxExtraHopsPerTier,
|
|
152
|
-
reputationSignals: signals,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function crossCheckSignal(kind: TrafficCrossCheckKind, traffic: QueryReplyV1["topicTraffic"], walkState: SeekerWalkState): TrafficCrossCheckSignal {
|
|
157
|
-
const signal: TrafficCrossCheckSignal = {
|
|
158
|
-
kind,
|
|
159
|
-
tier: walkState.currentTier,
|
|
160
|
-
reportedDirectParticipants: traffic.directParticipants,
|
|
161
|
-
reportedArrivalsPerMin: traffic.arrivalsPerMin,
|
|
162
|
-
observedMatches: walkState.observedMatches,
|
|
163
|
-
};
|
|
164
|
-
if (walkState.primaryId !== undefined) {
|
|
165
|
-
(signal as { subjectId: string }).subjectId = walkState.primaryId;
|
|
166
|
-
}
|
|
167
|
-
return signal;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* The total hop budget the toward-root-only walk can consume under any sequence of (honest or
|
|
172
|
-
* adversarial) traffic reports: at most `maxExtraHopsPerTier` per tier from `d_max` down to the root,
|
|
173
|
-
* plus the terminal hop — i.e. `(dMax + 1) * maxExtraHopsPerTier`. Exposed so the walk and its tests can
|
|
174
|
-
* assert the under-report bound holds (the walk never exceeds it, because escalation is monotone toward
|
|
175
|
-
* `d = 0`).
|
|
176
|
-
*/
|
|
177
|
-
export function maxWalkHops(dMax: number, cfg: TrafficBoundConfig = DEFAULT_TRAFFIC_BOUND_CONFIG): number {
|
|
178
|
-
return Math.max(0, dMax + 1) * cfg.maxExtraHopsPerTier;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Integration hook: forward cross-check signals into the reputation subsystem. This is the **emission
|
|
183
|
-
* point only** — it records a `ProtocolViolation` against the reporting primary; the reputation
|
|
184
|
-
* subsystem's own weighting, decay, and deprioritize/ban thresholds (`PeerReputationService`) decide
|
|
185
|
-
* whether an accumulation of these advisory signals actually penalizes the peer. Signals without an
|
|
186
|
-
* attributed `subjectId` are skipped.
|
|
187
|
-
*/
|
|
188
|
-
export function reportTrafficCrossCheck(reputation: IPeerReputation, signals: readonly TrafficCrossCheckSignal[]): void {
|
|
189
|
-
for (const signal of signals) {
|
|
190
|
-
if (signal.subjectId === undefined) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
reputation.reportPeer(signal.subjectId, PenaltyReason.ProtocolViolation, `matchmaking:${signal.kind}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Matchmaking — seeker-side adversarial traffic-reporting bounds + reputation cross-check hooks (db-p2p).
|
|
3
|
+
*
|
|
4
|
+
* `docs/matchmaking.md` §Adversarial cohort traffic reporting. A `QueryReplyV1.topicTraffic` (and the
|
|
5
|
+
* registration reply's traffic) is signed by the cohort **primary's single member key, not a threshold
|
|
6
|
+
* signature**, because the response is advisory — so a malicious primary can over- or under-report. The
|
|
7
|
+
* doc proves the harm is bounded either way; this module makes those bounds explicit for the seeker walk
|
|
8
|
+
* and emits the cross-check signals the reputation subsystem consumes.
|
|
9
|
+
*
|
|
10
|
+
* - **Over-reporting** (fake *hot* tier ⇒ seeker hangs out): bounded by the seeker's `patienceMs`. The
|
|
11
|
+
* worst case is wasted patience plus the one `register → walk` hop after timeout — there is **no
|
|
12
|
+
* spatial flood**, because the decision rule only ever walks *toward the root*, never speculatively
|
|
13
|
+
* outward. {@link boundReportedTraffic} therefore caps any hang-out to the seeker's remaining
|
|
14
|
+
* wall-clock patience ({@link TrafficBoundResult.capPatienceMs}).
|
|
15
|
+
* - **Under-reporting** (fake *cold* tier ⇒ seeker escalates): bounded to **one extra hop per affected
|
|
16
|
+
* tier**, terminating at the root where aggregated truth is hardest to fake
|
|
17
|
+
* ({@link TrafficBoundResult.escalateAfterTiers}).
|
|
18
|
+
* - **Cross-check via cohort gossip → reputation.** Other cohort members can detect a primary whose
|
|
19
|
+
* reported rate diverges from the gossip-derived view. Detection *routing* is the reputation
|
|
20
|
+
* subsystem's job (out of scope to implement); this module provides the **emission points**
|
|
21
|
+
* ({@link TrafficCrossCheckSignal}) and a thin bridge ({@link reportTrafficCrossCheck}) into the
|
|
22
|
+
* existing {@link IPeerReputation}. It scores nothing itself — the reputation subsystem owns the
|
|
23
|
+
* aggregation/decay policy that turns a stream of advisory signals into an actual penalty.
|
|
24
|
+
*
|
|
25
|
+
* **GROUNDING (matchmaking.md §Adversarial traffic reporting):** no threshold signature is added per
|
|
26
|
+
* `QueryReplyV1` — the single-member signature stands; the bounded worst-case here does not justify the
|
|
27
|
+
* per-reply threshold cost. This module enforces the bounds purely seeker-side instead.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { type QueryReplyV1 } from "@optimystic/db-core";
|
|
31
|
+
import { PenaltyReason, type IPeerReputation } from "../reputation/index.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The seeker's running walk state at the moment a reply lands — enough to bound the reply's reported
|
|
35
|
+
* traffic and attribute a cross-check signal. The seeker walk client (or the public seeker session)
|
|
36
|
+
* supplies it per reply.
|
|
37
|
+
*/
|
|
38
|
+
export interface SeekerWalkState {
|
|
39
|
+
/** Tree tier `d` the reply came from. */
|
|
40
|
+
readonly currentTier: number;
|
|
41
|
+
/** Starting tier `d_max` (the walk only descends from here toward `0`). */
|
|
42
|
+
readonly dMax: number;
|
|
43
|
+
/** Register hops issued so far (probes + escalations). Bounded by `d_max + 1` — the walk never loops. */
|
|
44
|
+
readonly tiersWalked: number;
|
|
45
|
+
/** Total patience budget for the task (ms). */
|
|
46
|
+
readonly patienceMs: number;
|
|
47
|
+
/** Patience left on the wall-clock deadline (ms) — the hang-out cap. */
|
|
48
|
+
readonly patienceRemainingMs: number;
|
|
49
|
+
/** Filter-matched providers the seeker's *own* immediate `QueryV1` at this tier actually returned. */
|
|
50
|
+
readonly observedMatches: number;
|
|
51
|
+
/** The reply's cohort primary peer id, for reputation attribution (absent → no attribution). */
|
|
52
|
+
readonly primaryId?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Tunables for {@link boundReportedTraffic}. */
|
|
56
|
+
export interface TrafficBoundConfig {
|
|
57
|
+
/**
|
|
58
|
+
* Upper bound on extra register hops a single under-reported tier may cost. The doc fixes this at
|
|
59
|
+
* **1** ("one extra hop per affected tier"); exposed for tests / future policy, not meant to change.
|
|
60
|
+
*/
|
|
61
|
+
readonly maxExtraHopsPerTier: number;
|
|
62
|
+
/**
|
|
63
|
+
* Over-report plausibility ratio. When a reply claims `directParticipants` more than this multiple of
|
|
64
|
+
* the seeker's *own* query yield, the over-report is flagged *suspect* (a cross-check signal — never a
|
|
65
|
+
* hard reject; the patience cap already bounds the harm).
|
|
66
|
+
*/
|
|
67
|
+
readonly overReportSuspectRatio: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The documented defaults (`maxExtraHopsPerTier = 1`; suspect ratio chosen to tolerate normal churn). */
|
|
71
|
+
export const DEFAULT_TRAFFIC_BOUND_CONFIG: TrafficBoundConfig = {
|
|
72
|
+
maxExtraHopsPerTier: 1,
|
|
73
|
+
overReportSuspectRatio: 8,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** The kind of divergence a {@link TrafficCrossCheckSignal} reports. */
|
|
77
|
+
export type TrafficCrossCheckKind = "over-report-suspected" | "under-report-suspected";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* One advisory cross-check observation about a cohort primary's reported traffic. Emitted by
|
|
81
|
+
* {@link boundReportedTraffic}; consumed by the reputation subsystem (via {@link reportTrafficCrossCheck}
|
|
82
|
+
* or a custom sink). Carries the raw discrepancy so the reputation policy — not this module — decides
|
|
83
|
+
* whether it warrants a penalty.
|
|
84
|
+
*/
|
|
85
|
+
export interface TrafficCrossCheckSignal {
|
|
86
|
+
readonly kind: TrafficCrossCheckKind;
|
|
87
|
+
/** The cohort primary peer id (the subject), if the walk state attributed one. */
|
|
88
|
+
readonly subjectId?: string;
|
|
89
|
+
/** The tier the reply came from. */
|
|
90
|
+
readonly tier: number;
|
|
91
|
+
/** `topicTraffic.directParticipants` as reported. */
|
|
92
|
+
readonly reportedDirectParticipants: number;
|
|
93
|
+
/** `topicTraffic.arrivalsPerMin` as reported. */
|
|
94
|
+
readonly reportedArrivalsPerMin: number;
|
|
95
|
+
/** The seeker's own immediate-query yield, which the report is cross-checked against. */
|
|
96
|
+
readonly observedMatches: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The outcome of {@link boundReportedTraffic}. */
|
|
100
|
+
export interface TrafficBoundResult {
|
|
101
|
+
/** Whether the reported traffic is within plausible bounds (advisory; a flag, not an admission). */
|
|
102
|
+
readonly trusted: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Over-report bound: never hang out at this tier beyond the seeker's remaining wall-clock patience.
|
|
105
|
+
* Equals `max(0, patienceRemainingMs)` — so a fabricated hot tier wastes at most that, then the walk
|
|
106
|
+
* proceeds with its one register hop.
|
|
107
|
+
*/
|
|
108
|
+
readonly capPatienceMs: number;
|
|
109
|
+
/** Under-report bound: at most this many extra register hops attributable to one tier (== config). */
|
|
110
|
+
readonly escalateAfterTiers: number;
|
|
111
|
+
/** Cross-check emission points (possibly empty). Forward to the reputation subsystem if desired. */
|
|
112
|
+
readonly reputationSignals: TrafficCrossCheckSignal[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Bound the harm of a (possibly adversarial) `topicTraffic` report and emit cross-check signals. Pure —
|
|
117
|
+
* no I/O, no clock, no reputation scoring. The seeker walk consults `capPatienceMs` before hanging out
|
|
118
|
+
* and treats `escalateAfterTiers` as the per-tier hop ceiling; the bounds it returns hold *by
|
|
119
|
+
* construction* of the toward-root-only walk, so this function documents + asserts them rather than
|
|
120
|
+
* changing the walk topology.
|
|
121
|
+
*/
|
|
122
|
+
export function boundReportedTraffic(
|
|
123
|
+
reply: QueryReplyV1,
|
|
124
|
+
walkState: SeekerWalkState,
|
|
125
|
+
cfg: TrafficBoundConfig = DEFAULT_TRAFFIC_BOUND_CONFIG,
|
|
126
|
+
): TrafficBoundResult {
|
|
127
|
+
const traffic = reply.topicTraffic;
|
|
128
|
+
const signals: TrafficCrossCheckSignal[] = [];
|
|
129
|
+
|
|
130
|
+
// Over-report: a primary advertising a hot tier (many directParticipants) whose own query yields far
|
|
131
|
+
// fewer matches is suspect. Harm is bounded regardless — the patience cap below means a fabricated hot
|
|
132
|
+
// tier can only waste the seeker's remaining patience before it walks on (no spatial flood, since the
|
|
133
|
+
// walk only steps toward the root).
|
|
134
|
+
const overReportSuspect =
|
|
135
|
+
traffic.directParticipants > cfg.overReportSuspectRatio * Math.max(walkState.observedMatches, 1);
|
|
136
|
+
if (overReportSuspect) {
|
|
137
|
+
signals.push(crossCheckSignal("over-report-suspected", traffic, walkState));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Under-report: a primary claiming a cold tier (zero arrivals) while the seeker's own query yields
|
|
141
|
+
// matches is suspect. Either way the escalation costs at most one extra hop for this tier and
|
|
142
|
+
// terminates at the root.
|
|
143
|
+
const underReportSuspect = traffic.arrivalsPerMin === 0 && walkState.observedMatches > 0;
|
|
144
|
+
if (underReportSuspect) {
|
|
145
|
+
signals.push(crossCheckSignal("under-report-suspected", traffic, walkState));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
trusted: !overReportSuspect && !underReportSuspect,
|
|
150
|
+
capPatienceMs: Math.max(0, walkState.patienceRemainingMs),
|
|
151
|
+
escalateAfterTiers: cfg.maxExtraHopsPerTier,
|
|
152
|
+
reputationSignals: signals,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function crossCheckSignal(kind: TrafficCrossCheckKind, traffic: QueryReplyV1["topicTraffic"], walkState: SeekerWalkState): TrafficCrossCheckSignal {
|
|
157
|
+
const signal: TrafficCrossCheckSignal = {
|
|
158
|
+
kind,
|
|
159
|
+
tier: walkState.currentTier,
|
|
160
|
+
reportedDirectParticipants: traffic.directParticipants,
|
|
161
|
+
reportedArrivalsPerMin: traffic.arrivalsPerMin,
|
|
162
|
+
observedMatches: walkState.observedMatches,
|
|
163
|
+
};
|
|
164
|
+
if (walkState.primaryId !== undefined) {
|
|
165
|
+
(signal as { subjectId: string }).subjectId = walkState.primaryId;
|
|
166
|
+
}
|
|
167
|
+
return signal;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The total hop budget the toward-root-only walk can consume under any sequence of (honest or
|
|
172
|
+
* adversarial) traffic reports: at most `maxExtraHopsPerTier` per tier from `d_max` down to the root,
|
|
173
|
+
* plus the terminal hop — i.e. `(dMax + 1) * maxExtraHopsPerTier`. Exposed so the walk and its tests can
|
|
174
|
+
* assert the under-report bound holds (the walk never exceeds it, because escalation is monotone toward
|
|
175
|
+
* `d = 0`).
|
|
176
|
+
*/
|
|
177
|
+
export function maxWalkHops(dMax: number, cfg: TrafficBoundConfig = DEFAULT_TRAFFIC_BOUND_CONFIG): number {
|
|
178
|
+
return Math.max(0, dMax + 1) * cfg.maxExtraHopsPerTier;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Integration hook: forward cross-check signals into the reputation subsystem. This is the **emission
|
|
183
|
+
* point only** — it records a `ProtocolViolation` against the reporting primary; the reputation
|
|
184
|
+
* subsystem's own weighting, decay, and deprioritize/ban thresholds (`PeerReputationService`) decide
|
|
185
|
+
* whether an accumulation of these advisory signals actually penalizes the peer. Signals without an
|
|
186
|
+
* attributed `subjectId` are skipped.
|
|
187
|
+
*/
|
|
188
|
+
export function reportTrafficCrossCheck(reputation: IPeerReputation, signals: readonly TrafficCrossCheckSignal[]): void {
|
|
189
|
+
for (const signal of signals) {
|
|
190
|
+
if (signal.subjectId === undefined) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
reputation.reportPeer(signal.subjectId, PenaltyReason.ProtocolViolation, `matchmaking:${signal.kind}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/optimystic-node.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import type { Libp2p, PrivateKey } from '@libp2p/interface';
|
|
2
|
-
import type { IBlockChangeNotifier, IRepo } from '@optimystic/db-core';
|
|
3
|
-
import type { DisputeService } from './dispute/dispute-service.js';
|
|
4
|
-
import type { Libp2pKeyPeerNetwork } from './libp2p-key-network.js';
|
|
5
|
-
import type { PeerReputationService } from './reputation/peer-reputation.js';
|
|
6
|
-
import type { StorageRepo } from './storage/storage-repo.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The handles `createLibp2pNodeBase` attaches to the libp2p node it returns. This is the
|
|
10
|
-
* sanctioned in-process surface a host reads — declared once here so reaching it does not
|
|
11
|
-
* require a cast, and so a host cannot silently rebuild a component the node already owns.
|
|
12
|
-
*
|
|
13
|
-
* Deliberately NOT the full set of `(node as any).*` attachments made in `libp2p-node-base.ts`:
|
|
14
|
-
* the churn/rebalance/ring-shift monitors, the cohort-topic host and the reactivity registries
|
|
15
|
-
* are node-internal wiring, not a host-facing surface, and typing them is a separate job.
|
|
16
|
-
*/
|
|
17
|
-
export interface OptimysticNodeAttachments {
|
|
18
|
-
/**
|
|
19
|
-
* The node's ONE key network — built from its resolved cluster policy, network-namespaced
|
|
20
|
-
* protocol prefix, reputation tracker and persistence. A host that needs key/coordinator
|
|
21
|
-
* lookup uses THIS; constructing a second one gives peer selection a different cohort
|
|
22
|
-
* width and coordinator than the node's own consensus path uses for the same key.
|
|
23
|
-
*/
|
|
24
|
-
keyNetwork: Libp2pKeyPeerNetwork;
|
|
25
|
-
coordinatedRepo: IRepo;
|
|
26
|
-
storageRepo: StorageRepo;
|
|
27
|
-
/** Per-collection change origin. Replaced by the cohort-topic bridge notifier when enabled. */
|
|
28
|
-
blockChangeNotifier: IBlockChangeNotifier;
|
|
29
|
-
reputation: PeerReputationService;
|
|
30
|
-
/** Present only when the dispute subsystem is configured. */
|
|
31
|
-
disputeService?: DisputeService;
|
|
32
|
-
/** The node's libp2p Ed25519 identity key, for hosts binding a client-transaction signer. */
|
|
33
|
-
peerPrivateKey: PrivateKey;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type OptimysticNode = Libp2p & OptimysticNodeAttachments;
|
|
1
|
+
import type { Libp2p, PrivateKey } from '@libp2p/interface';
|
|
2
|
+
import type { IBlockChangeNotifier, IRepo } from '@optimystic/db-core';
|
|
3
|
+
import type { DisputeService } from './dispute/dispute-service.js';
|
|
4
|
+
import type { Libp2pKeyPeerNetwork } from './libp2p-key-network.js';
|
|
5
|
+
import type { PeerReputationService } from './reputation/peer-reputation.js';
|
|
6
|
+
import type { StorageRepo } from './storage/storage-repo.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The handles `createLibp2pNodeBase` attaches to the libp2p node it returns. This is the
|
|
10
|
+
* sanctioned in-process surface a host reads — declared once here so reaching it does not
|
|
11
|
+
* require a cast, and so a host cannot silently rebuild a component the node already owns.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT the full set of `(node as any).*` attachments made in `libp2p-node-base.ts`:
|
|
14
|
+
* the churn/rebalance/ring-shift monitors, the cohort-topic host and the reactivity registries
|
|
15
|
+
* are node-internal wiring, not a host-facing surface, and typing them is a separate job.
|
|
16
|
+
*/
|
|
17
|
+
export interface OptimysticNodeAttachments {
|
|
18
|
+
/**
|
|
19
|
+
* The node's ONE key network — built from its resolved cluster policy, network-namespaced
|
|
20
|
+
* protocol prefix, reputation tracker and persistence. A host that needs key/coordinator
|
|
21
|
+
* lookup uses THIS; constructing a second one gives peer selection a different cohort
|
|
22
|
+
* width and coordinator than the node's own consensus path uses for the same key.
|
|
23
|
+
*/
|
|
24
|
+
keyNetwork: Libp2pKeyPeerNetwork;
|
|
25
|
+
coordinatedRepo: IRepo;
|
|
26
|
+
storageRepo: StorageRepo;
|
|
27
|
+
/** Per-collection change origin. Replaced by the cohort-topic bridge notifier when enabled. */
|
|
28
|
+
blockChangeNotifier: IBlockChangeNotifier;
|
|
29
|
+
reputation: PeerReputationService;
|
|
30
|
+
/** Present only when the dispute subsystem is configured. */
|
|
31
|
+
disputeService?: DisputeService;
|
|
32
|
+
/** The node's libp2p Ed25519 identity key, for hosts binding a client-transaction signer. */
|
|
33
|
+
peerPrivateKey: PrivateKey;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type OptimysticNode = Libp2p & OptimysticNodeAttachments;
|
package/src/owned-block-seed.ts
CHANGED
|
@@ -1,40 +1,53 @@
|
|
|
1
|
-
import type { IRawStorage } from './storage/i-raw-storage.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Seed the resilience monitors' shared owned-block set from blocks already
|
|
5
|
-
* durable in raw storage from a previous process run.
|
|
6
|
-
*
|
|
7
|
-
* The live owned-block feed (`storageRepo.onAnyCollectionChange`, wired in
|
|
8
|
-
* `createLibp2pNodeBase`) only fires on NEW commits and RECEIVED replicas, so a
|
|
9
|
-
* block that was already durable on disk from a previous run is not tracked
|
|
10
|
-
* after a restart until it happens to be committed or replicated again — a
|
|
11
|
-
* freshly restarted node would under-protect exactly the data it already holds.
|
|
12
|
-
* This scan closes that gap by enumerating the metadata store
|
|
13
|
-
* with
|
|
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
|
-
|
|
1
|
+
import type { IRawStorage } from './storage/i-raw-storage.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Seed the resilience monitors' shared owned-block set from blocks already
|
|
5
|
+
* durable in raw storage from a previous process run.
|
|
6
|
+
*
|
|
7
|
+
* The live owned-block feed (`storageRepo.onAnyCollectionChange`, wired in
|
|
8
|
+
* `createLibp2pNodeBase`) only fires on NEW commits and RECEIVED replicas, so a
|
|
9
|
+
* block that was already durable on disk from a previous run is not tracked
|
|
10
|
+
* after a restart until it happens to be committed or replicated again — a
|
|
11
|
+
* freshly restarted node would under-protect exactly the data it already holds.
|
|
12
|
+
* This scan closes that gap by enumerating the metadata store — one id per block
|
|
13
|
+
* with ANY durable metadata — and adding each id to `ownedBlocks`.
|
|
14
|
+
*
|
|
15
|
+
* That population is a superset of what the live feed tracks, because metadata is
|
|
16
|
+
* also written on a plain pend — see the NOTE on the function body for why the
|
|
17
|
+
* resulting over-inclusion is accepted.
|
|
18
|
+
*
|
|
19
|
+
* Called AFTER the live feed is already subscribed, so a block committed/replicated
|
|
20
|
+
* mid-scan is independently caught by the feed; `Set.add` is idempotent, so the
|
|
21
|
+
* overlap is harmless. `isStopping()` is checked each iteration so a scan over a
|
|
22
|
+
* huge store aborts promptly when the node is stopping (the `for await` then calls
|
|
23
|
+
* the iterator's `return()` to release the backend cursor). A cooperative yield
|
|
24
|
+
* every `yieldEvery` ids keeps a tight add-loop from monopolizing an event-loop tick.
|
|
25
|
+
*
|
|
26
|
+
* Backends that omit `listBlockIds` (or an in-memory backend with nothing durable
|
|
27
|
+
* across a restart) yield no seed — the monitors still populate over time via the
|
|
28
|
+
* live feed.
|
|
29
|
+
*/
|
|
30
|
+
export async function seedOwnedBlocksFromStorage(
|
|
31
|
+
rawStorage: Pick<IRawStorage, 'listBlockIds'>,
|
|
32
|
+
ownedBlocks: Set<string>,
|
|
33
|
+
isStopping: () => boolean,
|
|
34
|
+
yieldEvery = 1000,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
// NOTE: accepted over-inclusion — this seeds pend-only blocks (metadata exists, no committed
|
|
37
|
+
// revision) alongside committed/replicated ones. Filtering to committed-only would mean reading and
|
|
38
|
+
// decoding metadata for every id at startup — a per-block read on the fs backend, exactly the cost
|
|
39
|
+
// the streamed key enumeration exists to avoid. It is benign today because every consumer of the
|
|
40
|
+
// shared set re-checks local data before acting: `SpreadOnChurnMonitor.spreadCheck` untracks a
|
|
41
|
+
// tracked block whose `repo.get` returns nothing, and `BlockTransferCoordinator.confirmReplicated`
|
|
42
|
+
// reports a no-local-data block as unconfirmed, so it is never released and never becomes
|
|
43
|
+
// GC-eligible. REVISIT IF any consumer of the shared owned-block set ever takes a destructive or
|
|
44
|
+
// irreversible action keyed on membership alone, without a local-data check — then this scan must
|
|
45
|
+
// filter to committed blocks. Asserted as-is by `test/owned-block-seed-node-wiring.spec.ts`.
|
|
46
|
+
if (typeof rawStorage.listBlockIds !== 'function') return;
|
|
47
|
+
let n = 0;
|
|
48
|
+
for await (const blockId of rawStorage.listBlockIds()) {
|
|
49
|
+
if (isStopping()) break;
|
|
50
|
+
ownedBlocks.add(blockId);
|
|
51
|
+
if (++n % yieldEvery === 0) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
52
|
+
}
|
|
53
|
+
}
|