@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,153 +1,153 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cohort-topic substrate — the **FRET-ring direct trust anchor** (db-p2p side of `IMembershipTrustAnchor`).
|
|
3
|
-
*
|
|
4
|
-
* `cohort-topic-trust-anchor-core` added the db-core trust gate (`membership/verifier.ts`): a (re)fetched
|
|
5
|
-
* `MembershipCertV1` is believed only if it is self-consistent **and** anchored by a trust root, the
|
|
6
|
-
* injected direct anchor, or the attestation chain — else it falls to interim trust-on-first-use. db-core
|
|
7
|
-
* ships only `noAuthorityTrustAnchor` (every coord `"unknown"`), so until db-p2p binds a real anchor the
|
|
8
|
-
* gate stays at TOFU. This module binds the **direct anchor** to the one coord→keyset authority FRET
|
|
9
|
-
* exposes today.
|
|
10
|
-
*
|
|
11
|
-
* **What authority FRET offers.** p2p-fret 0.5.0 has **no transferable stabilization proof** — there is no
|
|
12
|
-
* membership-cert/attestation API and the cert's `fretAttestation` field is never populated. The only
|
|
13
|
-
* coord→keyset authority FRET offers is `assembleCohort(coord, wants)` — the ring's two-sided closest-`k`
|
|
14
|
-
* selection — which is **local**: it answers correctly only for coords the node's routing table actually
|
|
15
|
-
* covers (coords the node is near / serves). That is exactly the amplification-exposed `promote`-handler
|
|
16
|
-
* path (`verifyAndApplyNotice` verifies against `target.servedCoord`, a coord the node serves), so binding
|
|
17
|
-
* the anchor to `assembleCohort` rejects the forged-cert attack precisely where the core ticket calls it out.
|
|
18
|
-
*
|
|
19
|
-
* **The rule (`directAnchor(cert, tier)`):**
|
|
20
|
-
*
|
|
21
|
-
* - **Committed tiers (T0/T1)** route to the tx-log commit certificate, not the FRET ring, so this anchor
|
|
22
|
-
* returns `"unknown"` for them — it composes with (does not fight) the future tx-log anchor.
|
|
23
|
-
* - **No local authority** — the node cannot cover the coord (cold/partitioned table, or a distant coord the
|
|
24
|
-
* node is nowhere near, so `assembleCohort` does not yield a populated neighborhood the node is part of) →
|
|
25
|
-
* `"unknown"`. The db-core gate then falls through to the chain / interim TOFU, so distant verification is
|
|
26
|
-
* never broken or falsely rejected.
|
|
27
|
-
* - **Covered coord** — compare the cert's **signing quorum** (`cert.signers`, the `≥ minSigs` that actually
|
|
28
|
-
* signed) against the ring-expected cohort, widened by a small {@link FretTrustAnchorOptions.churnSlack} to
|
|
29
|
-
* tolerate stabilization skew (membership stabilizes slightly behind the live ring):
|
|
30
|
-
* - every signer is in the slack-widened ring view → `"anchored"`;
|
|
31
|
-
* - the quorum is **disjoint** from the ring view (the ring says a wholly different cohort owns this coord)
|
|
32
|
-
* → `"rejected"` (a forgery — fatal even though it is internally self-consistent);
|
|
33
|
-
* - partial overlap beyond the slack (genuinely ambiguous within churn tolerance) → `"unknown"` (do not
|
|
34
|
-
* over-reject on transient skew; defer to the chain / TOFU).
|
|
35
|
-
*
|
|
36
|
-
* **Why anchoring on `signers` (not exact member-set equality) is sound and sufficient.** A forged cert
|
|
37
|
-
* must sign its threshold multisig with keys the adversary controls; those keys are not in the legitimate
|
|
38
|
-
* ring cohort, so a forged quorum is disjoint from `assembleCohort(coord)` → `"rejected"`. A legitimate
|
|
39
|
-
* cert's signers are real cohort members — present in the ring view (within slack) → `"anchored"`. The
|
|
40
|
-
* adversary cannot list real members as `signers` without their keys (the message multisig would not
|
|
41
|
-
* verify), so the quorum-subset test has teeth without demanding brittle full-set equality across churn.
|
|
42
|
-
*
|
|
43
|
-
* **db-core never imports FRET.** This adapter lives in db-p2p and depends only on the narrow
|
|
44
|
-
* {@link FretRingView} (satisfied by `FretService`), keeping the db-core trust gate transport-agnostic.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
import type { IMembershipTrustAnchor, MembershipCertV1, RingCoord, TrustAnchorVerdict } from "@optimystic/db-core";
|
|
48
|
-
import { b64urlToBytes, DEFAULT_MAX_NO_POW_TIER } from "@optimystic/db-core";
|
|
49
|
-
import { bytesToPeerIdString } from "./peer-codec.js";
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The minimal slice of `FretService` the trust anchor needs: the ring's local two-sided cohort assembly
|
|
53
|
-
* around a coord, plus an optional partition signal. `FretService` satisfies it directly; a test (and the
|
|
54
|
-
* mock-mesh harness facade) can supply a stub with just `assembleCohort`.
|
|
55
|
-
*/
|
|
56
|
-
export interface FretRingView {
|
|
57
|
-
/** Two-sided closest-`wants` selection around `coord` from the node's routing table (peer-id strings). */
|
|
58
|
-
assembleCohort(coord: Uint8Array, wants: number, exclude?: Set<string>): string[];
|
|
59
|
-
/** Optional: `true` when the node believes it is partitioned (table unreliable → never reject). */
|
|
60
|
-
detectPartition?(): boolean;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Configuration for a {@link FretTrustAnchor}. */
|
|
64
|
-
export interface FretTrustAnchorOptions {
|
|
65
|
-
/**
|
|
66
|
-
* Requested cohort size — must match the host's `wantK`, so the ring view the anchor computes lines up
|
|
67
|
-
* with the cohort the cohort-side `cohortAround` published the cert over.
|
|
68
|
-
*/
|
|
69
|
-
readonly k: number;
|
|
70
|
-
/** This node's own peer-id string — used for the coverage check (the node must be in the coord's cohort). */
|
|
71
|
-
readonly selfPeerId: string;
|
|
72
|
-
/**
|
|
73
|
-
* Stabilization-skew slack: the ring-expected cohort is widened to `k + churnSlack` members before the
|
|
74
|
-
* quorum-subset test, so a legit cert whose signing quorum lags the live ring by a rotation or two is
|
|
75
|
-
* still `"anchored"`. Kept small so a disjoint forged keyset cannot hide in the slack. Default
|
|
76
|
-
* {@link DEFAULT_CHURN_SLACK}.
|
|
77
|
-
*/
|
|
78
|
-
readonly churnSlack?: number;
|
|
79
|
-
/**
|
|
80
|
-
* Highest tier whose membership is anchored in the tx-log commit certificate (T0/T1), for which this
|
|
81
|
-
* FRET-ring anchor has no authority and returns `"unknown"`. Mirrors the membership-source dispatch
|
|
82
|
-
* (`createMembershipSourceRouter` treats tier 0/1 as committed). Default {@link DEFAULT_MAX_NO_POW_TIER}.
|
|
83
|
-
*/
|
|
84
|
-
readonly maxCommittedTier?: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Default stabilization-skew slack (`churn_slack ≈ 2`): a legit cert may lag the live ring by ~1–2 members
|
|
89
|
-
* (one rotates in, one rotates out) between stabilization and the verifier's table view; widening to
|
|
90
|
-
* `k + 2` admits that skew while still rejecting a wholly-disjoint forged keyset.
|
|
91
|
-
*/
|
|
92
|
-
export const DEFAULT_CHURN_SLACK = 2;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* The FRET-ring {@link IMembershipTrustAnchor}: judges a cert's `coord → keyset` binding against the
|
|
96
|
-
* node's local FRET cohort assembly. See the module header for the full rule.
|
|
97
|
-
*/
|
|
98
|
-
export class FretTrustAnchor implements IMembershipTrustAnchor {
|
|
99
|
-
private readonly k: number;
|
|
100
|
-
private readonly selfPeerId: string;
|
|
101
|
-
private readonly churnSlack: number;
|
|
102
|
-
private readonly maxCommittedTier: number;
|
|
103
|
-
|
|
104
|
-
constructor(private readonly fret: FretRingView, options: FretTrustAnchorOptions) {
|
|
105
|
-
this.k = options.k;
|
|
106
|
-
this.selfPeerId = options.selfPeerId;
|
|
107
|
-
this.churnSlack = options.churnSlack ?? DEFAULT_CHURN_SLACK;
|
|
108
|
-
this.maxCommittedTier = options.maxCommittedTier ?? DEFAULT_MAX_NO_POW_TIER;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
directAnchor(cert: MembershipCertV1, tier: number): TrustAnchorVerdict {
|
|
112
|
-
// Committed tiers (T0/T1) are the tx-log anchor's job, not the FRET ring's — defer.
|
|
113
|
-
if (tier <= this.maxCommittedTier) {
|
|
114
|
-
return "unknown";
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
// A partitioned table is unreliable: never reject a legit cert during a partition — defer to TOFU.
|
|
118
|
-
if (this.fret.detectPartition?.() === true) {
|
|
119
|
-
return "unknown";
|
|
120
|
-
}
|
|
121
|
-
const coord: RingCoord = b64urlToBytes(cert.cohortCoord);
|
|
122
|
-
const expected = this.fret.assembleCohort(coord, this.k);
|
|
123
|
-
// Coverage / local authority: a populated neighborhood the node is itself part of. A cold or
|
|
124
|
-
// partitioned table yields `< k`; a distant coord the node is nowhere near omits self. Either way
|
|
125
|
-
// the node cannot judge → `"unknown"` (no regression on coords nothing can anchor).
|
|
126
|
-
if (expected.length < this.k || !expected.includes(this.selfPeerId)) {
|
|
127
|
-
return "unknown";
|
|
128
|
-
}
|
|
129
|
-
const widened = new Set(this.fret.assembleCohort(coord, this.k + this.churnSlack));
|
|
130
|
-
// Decode the cert's signing quorum into FRET peer-id strings (the same form `assembleCohort` yields).
|
|
131
|
-
const signers = cert.signers.map((s) => bytesToPeerIdString(b64urlToBytes(s)));
|
|
132
|
-
if (signers.length === 0) {
|
|
133
|
-
return "unknown"; // nothing to judge (a self-consistent cert always has signers; defensive)
|
|
134
|
-
}
|
|
135
|
-
let inRing = 0;
|
|
136
|
-
for (const signer of signers) {
|
|
137
|
-
if (widened.has(signer)) {
|
|
138
|
-
inRing++;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (inRing === signers.length) {
|
|
142
|
-
return "anchored"; // the whole signing quorum is a subset of a reasonable ring view
|
|
143
|
-
}
|
|
144
|
-
if (inRing === 0) {
|
|
145
|
-
return "rejected"; // a wholly-disjoint quorum — the ring knows a different cohort owns this coord
|
|
146
|
-
}
|
|
147
|
-
return "unknown"; // partial overlap beyond the slack — ambiguous churn; defer rather than over-reject
|
|
148
|
-
} catch {
|
|
149
|
-
// Any decode failure on attacker-supplied bytes → the ring cannot judge it. Total, never throws.
|
|
150
|
-
return "unknown";
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — the **FRET-ring direct trust anchor** (db-p2p side of `IMembershipTrustAnchor`).
|
|
3
|
+
*
|
|
4
|
+
* `cohort-topic-trust-anchor-core` added the db-core trust gate (`membership/verifier.ts`): a (re)fetched
|
|
5
|
+
* `MembershipCertV1` is believed only if it is self-consistent **and** anchored by a trust root, the
|
|
6
|
+
* injected direct anchor, or the attestation chain — else it falls to interim trust-on-first-use. db-core
|
|
7
|
+
* ships only `noAuthorityTrustAnchor` (every coord `"unknown"`), so until db-p2p binds a real anchor the
|
|
8
|
+
* gate stays at TOFU. This module binds the **direct anchor** to the one coord→keyset authority FRET
|
|
9
|
+
* exposes today.
|
|
10
|
+
*
|
|
11
|
+
* **What authority FRET offers.** p2p-fret 0.5.0 has **no transferable stabilization proof** — there is no
|
|
12
|
+
* membership-cert/attestation API and the cert's `fretAttestation` field is never populated. The only
|
|
13
|
+
* coord→keyset authority FRET offers is `assembleCohort(coord, wants)` — the ring's two-sided closest-`k`
|
|
14
|
+
* selection — which is **local**: it answers correctly only for coords the node's routing table actually
|
|
15
|
+
* covers (coords the node is near / serves). That is exactly the amplification-exposed `promote`-handler
|
|
16
|
+
* path (`verifyAndApplyNotice` verifies against `target.servedCoord`, a coord the node serves), so binding
|
|
17
|
+
* the anchor to `assembleCohort` rejects the forged-cert attack precisely where the core ticket calls it out.
|
|
18
|
+
*
|
|
19
|
+
* **The rule (`directAnchor(cert, tier)`):**
|
|
20
|
+
*
|
|
21
|
+
* - **Committed tiers (T0/T1)** route to the tx-log commit certificate, not the FRET ring, so this anchor
|
|
22
|
+
* returns `"unknown"` for them — it composes with (does not fight) the future tx-log anchor.
|
|
23
|
+
* - **No local authority** — the node cannot cover the coord (cold/partitioned table, or a distant coord the
|
|
24
|
+
* node is nowhere near, so `assembleCohort` does not yield a populated neighborhood the node is part of) →
|
|
25
|
+
* `"unknown"`. The db-core gate then falls through to the chain / interim TOFU, so distant verification is
|
|
26
|
+
* never broken or falsely rejected.
|
|
27
|
+
* - **Covered coord** — compare the cert's **signing quorum** (`cert.signers`, the `≥ minSigs` that actually
|
|
28
|
+
* signed) against the ring-expected cohort, widened by a small {@link FretTrustAnchorOptions.churnSlack} to
|
|
29
|
+
* tolerate stabilization skew (membership stabilizes slightly behind the live ring):
|
|
30
|
+
* - every signer is in the slack-widened ring view → `"anchored"`;
|
|
31
|
+
* - the quorum is **disjoint** from the ring view (the ring says a wholly different cohort owns this coord)
|
|
32
|
+
* → `"rejected"` (a forgery — fatal even though it is internally self-consistent);
|
|
33
|
+
* - partial overlap beyond the slack (genuinely ambiguous within churn tolerance) → `"unknown"` (do not
|
|
34
|
+
* over-reject on transient skew; defer to the chain / TOFU).
|
|
35
|
+
*
|
|
36
|
+
* **Why anchoring on `signers` (not exact member-set equality) is sound and sufficient.** A forged cert
|
|
37
|
+
* must sign its threshold multisig with keys the adversary controls; those keys are not in the legitimate
|
|
38
|
+
* ring cohort, so a forged quorum is disjoint from `assembleCohort(coord)` → `"rejected"`. A legitimate
|
|
39
|
+
* cert's signers are real cohort members — present in the ring view (within slack) → `"anchored"`. The
|
|
40
|
+
* adversary cannot list real members as `signers` without their keys (the message multisig would not
|
|
41
|
+
* verify), so the quorum-subset test has teeth without demanding brittle full-set equality across churn.
|
|
42
|
+
*
|
|
43
|
+
* **db-core never imports FRET.** This adapter lives in db-p2p and depends only on the narrow
|
|
44
|
+
* {@link FretRingView} (satisfied by `FretService`), keeping the db-core trust gate transport-agnostic.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { IMembershipTrustAnchor, MembershipCertV1, RingCoord, TrustAnchorVerdict } from "@optimystic/db-core";
|
|
48
|
+
import { b64urlToBytes, DEFAULT_MAX_NO_POW_TIER } from "@optimystic/db-core";
|
|
49
|
+
import { bytesToPeerIdString } from "./peer-codec.js";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The minimal slice of `FretService` the trust anchor needs: the ring's local two-sided cohort assembly
|
|
53
|
+
* around a coord, plus an optional partition signal. `FretService` satisfies it directly; a test (and the
|
|
54
|
+
* mock-mesh harness facade) can supply a stub with just `assembleCohort`.
|
|
55
|
+
*/
|
|
56
|
+
export interface FretRingView {
|
|
57
|
+
/** Two-sided closest-`wants` selection around `coord` from the node's routing table (peer-id strings). */
|
|
58
|
+
assembleCohort(coord: Uint8Array, wants: number, exclude?: Set<string>): string[];
|
|
59
|
+
/** Optional: `true` when the node believes it is partitioned (table unreliable → never reject). */
|
|
60
|
+
detectPartition?(): boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Configuration for a {@link FretTrustAnchor}. */
|
|
64
|
+
export interface FretTrustAnchorOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Requested cohort size — must match the host's `wantK`, so the ring view the anchor computes lines up
|
|
67
|
+
* with the cohort the cohort-side `cohortAround` published the cert over.
|
|
68
|
+
*/
|
|
69
|
+
readonly k: number;
|
|
70
|
+
/** This node's own peer-id string — used for the coverage check (the node must be in the coord's cohort). */
|
|
71
|
+
readonly selfPeerId: string;
|
|
72
|
+
/**
|
|
73
|
+
* Stabilization-skew slack: the ring-expected cohort is widened to `k + churnSlack` members before the
|
|
74
|
+
* quorum-subset test, so a legit cert whose signing quorum lags the live ring by a rotation or two is
|
|
75
|
+
* still `"anchored"`. Kept small so a disjoint forged keyset cannot hide in the slack. Default
|
|
76
|
+
* {@link DEFAULT_CHURN_SLACK}.
|
|
77
|
+
*/
|
|
78
|
+
readonly churnSlack?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Highest tier whose membership is anchored in the tx-log commit certificate (T0/T1), for which this
|
|
81
|
+
* FRET-ring anchor has no authority and returns `"unknown"`. Mirrors the membership-source dispatch
|
|
82
|
+
* (`createMembershipSourceRouter` treats tier 0/1 as committed). Default {@link DEFAULT_MAX_NO_POW_TIER}.
|
|
83
|
+
*/
|
|
84
|
+
readonly maxCommittedTier?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Default stabilization-skew slack (`churn_slack ≈ 2`): a legit cert may lag the live ring by ~1–2 members
|
|
89
|
+
* (one rotates in, one rotates out) between stabilization and the verifier's table view; widening to
|
|
90
|
+
* `k + 2` admits that skew while still rejecting a wholly-disjoint forged keyset.
|
|
91
|
+
*/
|
|
92
|
+
export const DEFAULT_CHURN_SLACK = 2;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The FRET-ring {@link IMembershipTrustAnchor}: judges a cert's `coord → keyset` binding against the
|
|
96
|
+
* node's local FRET cohort assembly. See the module header for the full rule.
|
|
97
|
+
*/
|
|
98
|
+
export class FretTrustAnchor implements IMembershipTrustAnchor {
|
|
99
|
+
private readonly k: number;
|
|
100
|
+
private readonly selfPeerId: string;
|
|
101
|
+
private readonly churnSlack: number;
|
|
102
|
+
private readonly maxCommittedTier: number;
|
|
103
|
+
|
|
104
|
+
constructor(private readonly fret: FretRingView, options: FretTrustAnchorOptions) {
|
|
105
|
+
this.k = options.k;
|
|
106
|
+
this.selfPeerId = options.selfPeerId;
|
|
107
|
+
this.churnSlack = options.churnSlack ?? DEFAULT_CHURN_SLACK;
|
|
108
|
+
this.maxCommittedTier = options.maxCommittedTier ?? DEFAULT_MAX_NO_POW_TIER;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
directAnchor(cert: MembershipCertV1, tier: number): TrustAnchorVerdict {
|
|
112
|
+
// Committed tiers (T0/T1) are the tx-log anchor's job, not the FRET ring's — defer.
|
|
113
|
+
if (tier <= this.maxCommittedTier) {
|
|
114
|
+
return "unknown";
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
// A partitioned table is unreliable: never reject a legit cert during a partition — defer to TOFU.
|
|
118
|
+
if (this.fret.detectPartition?.() === true) {
|
|
119
|
+
return "unknown";
|
|
120
|
+
}
|
|
121
|
+
const coord: RingCoord = b64urlToBytes(cert.cohortCoord);
|
|
122
|
+
const expected = this.fret.assembleCohort(coord, this.k);
|
|
123
|
+
// Coverage / local authority: a populated neighborhood the node is itself part of. A cold or
|
|
124
|
+
// partitioned table yields `< k`; a distant coord the node is nowhere near omits self. Either way
|
|
125
|
+
// the node cannot judge → `"unknown"` (no regression on coords nothing can anchor).
|
|
126
|
+
if (expected.length < this.k || !expected.includes(this.selfPeerId)) {
|
|
127
|
+
return "unknown";
|
|
128
|
+
}
|
|
129
|
+
const widened = new Set(this.fret.assembleCohort(coord, this.k + this.churnSlack));
|
|
130
|
+
// Decode the cert's signing quorum into FRET peer-id strings (the same form `assembleCohort` yields).
|
|
131
|
+
const signers = cert.signers.map((s) => bytesToPeerIdString(b64urlToBytes(s)));
|
|
132
|
+
if (signers.length === 0) {
|
|
133
|
+
return "unknown"; // nothing to judge (a self-consistent cert always has signers; defensive)
|
|
134
|
+
}
|
|
135
|
+
let inRing = 0;
|
|
136
|
+
for (const signer of signers) {
|
|
137
|
+
if (widened.has(signer)) {
|
|
138
|
+
inRing++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (inRing === signers.length) {
|
|
142
|
+
return "anchored"; // the whole signing quorum is a subset of a reasonable ring view
|
|
143
|
+
}
|
|
144
|
+
if (inRing === 0) {
|
|
145
|
+
return "rejected"; // a wholly-disjoint quorum — the ring knows a different cohort owns this coord
|
|
146
|
+
}
|
|
147
|
+
return "unknown"; // partial overlap beyond the slack — ambiguous churn; defer rather than over-reject
|
|
148
|
+
} catch {
|
|
149
|
+
// Any decode failure on attacker-supplied bytes → the ring cannot judge it. Total, never throws.
|
|
150
|
+
return "unknown";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/cohort-topic/host.ts
CHANGED
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
import type { Libp2p } from "libp2p";
|
|
58
58
|
import type { Connection, PeerId, PrivateKey, Stream } from "@libp2p/interface";
|
|
59
59
|
import type { FretService } from "p2p-fret";
|
|
60
|
-
import { hashPeerId,
|
|
60
|
+
import { hashPeerId, readFramed, sendFramed } from "p2p-fret";
|
|
61
61
|
import {
|
|
62
62
|
RingHash,
|
|
63
63
|
createRegistrationStore,
|
|
@@ -2810,7 +2810,7 @@ async function registerProtocolHandlers(
|
|
|
2810
2810
|
await Promise.all([
|
|
2811
2811
|
// register: a direct dial carries a RenewV1 (ping), a ChildLinkV1 (a child cohort registering with this
|
|
2812
2812
|
// parent), or a RegisterV1 (re-attach walk fallback). The three shapes are disjoint, so try each in turn.
|
|
2813
|
-
node.handle(protocols.register,
|
|
2813
|
+
node.handle(protocols.register, makeRequestHandler(async (frame): Promise<Uint8Array> => {
|
|
2814
2814
|
const decoded = decodeCohortMessage(frame, maxBytes);
|
|
2815
2815
|
const renew = tryValidate(() => validateRenewV1(decoded));
|
|
2816
2816
|
if (renew !== undefined) {
|
|
@@ -2830,23 +2830,21 @@ async function registerProtocolHandlers(
|
|
|
2830
2830
|
// every coord engine's bus; per-bus epoch matching governs which engine merges the record deltas.
|
|
2831
2831
|
// First, if this is a verified co-member frame for a coord we hold no engine for, instantiate that
|
|
2832
2832
|
// engine (§Cold-start instantiation) so its freshly-subscribed bus merges this very frame on `deliver`.
|
|
2833
|
-
node.handle(protocols.gossip,
|
|
2833
|
+
node.handle(protocols.gossip, makeOneWayHandler(async (frame, from): Promise<void> => {
|
|
2834
2834
|
maybeInstantiateColdSibling(frame);
|
|
2835
2835
|
gossipTransport.deliver(from.toString(), frame);
|
|
2836
|
-
return undefined;
|
|
2837
2836
|
}, maxBytes)),
|
|
2838
2837
|
|
|
2839
2838
|
// promote: threshold-signed promotion/demotion notices (one-way, gossip-style fan-out). The dialing
|
|
2840
2839
|
// peer arrives as `from`, so the handler can gate per-(peer, topic) before any expensive work. The
|
|
2841
2840
|
// full pipeline (rate limit → findServing → effectiveAt high-water → verify+apply, bounded
|
|
2842
2841
|
// refetch) lives in the exported `handleInboundNotice`; it logs and never throws on the stream.
|
|
2843
|
-
node.handle(protocols.promote,
|
|
2842
|
+
node.handle(protocols.promote, makeOneWayHandler(async (frame, from): Promise<void> => {
|
|
2844
2843
|
await handleInboundNotice(frame, peerIdToBytes(from), registry, verifier, promoteGate, Date.now(), maxBytes);
|
|
2845
|
-
return undefined; // one-way: match the gossip-style fan-out (no ack frame)
|
|
2846
2844
|
}, maxBytes)),
|
|
2847
2845
|
|
|
2848
2846
|
// membership: serve this node's latest published cert; cache any cert the requester returns.
|
|
2849
|
-
node.handle(protocols.membership,
|
|
2847
|
+
node.handle(protocols.membership, makeRequestHandler(async (frame): Promise<Uint8Array> => {
|
|
2850
2848
|
void frame; // request frame is the raw coord; this node serves its own cohort cert
|
|
2851
2849
|
const latest = publishSink.latest();
|
|
2852
2850
|
if (latest !== undefined) {
|
|
@@ -2858,7 +2856,7 @@ async function registerProtocolHandlers(
|
|
|
2858
2856
|
// sign: per-member endorsement for threshold-signature assembly. Validate the request, run the
|
|
2859
2857
|
// endorsement policy, and reply with this node's peer-key signature over the request payload (or a
|
|
2860
2858
|
// refusal). One Ed25519 sign and nothing more — the cohort + epoch gate bounds who we sign for.
|
|
2861
|
-
node.handle(protocols.sign,
|
|
2859
|
+
node.handle(protocols.sign, makeRequestHandler(async (frame, from): Promise<Uint8Array> => {
|
|
2862
2860
|
const request = validateSignRequestV1(decodeCohortMessage(frame, maxBytes));
|
|
2863
2861
|
const reply = await signEndorse(request, from.toString());
|
|
2864
2862
|
return encodeCohortMessage(reply, maxBytes);
|
|
@@ -2866,7 +2864,40 @@ async function registerProtocolHandlers(
|
|
|
2866
2864
|
]);
|
|
2867
2865
|
}
|
|
2868
2866
|
|
|
2869
|
-
/**
|
|
2867
|
+
/**
|
|
2868
|
+
* Serve a request/reply protocol: read one framed request, reply with exactly one framed response.
|
|
2869
|
+
*
|
|
2870
|
+
* `handle` cannot return `undefined`, and that is the point. The dialer's `readFramed` treats bare
|
|
2871
|
+
* end-of-stream as a truncation error rather than an empty reply, so a reply-reading protocol
|
|
2872
|
+
* (register, membership, sign) must always put a frame on the wire — an empty one for "no result".
|
|
2873
|
+
* Making that a separate constructor from {@link makeOneWayHandler} puts the invariant in the type,
|
|
2874
|
+
* where a later edit cannot silently opt out of it. (`stream-util.ts#handleRequestResponse` takes
|
|
2875
|
+
* the other route for the same reason: every consumer of it is a reply-reading protocol, so it maps
|
|
2876
|
+
* a handler's `undefined` onto an explicit zero-length frame itself.)
|
|
2877
|
+
*/
|
|
2878
|
+
function makeRequestHandler(
|
|
2879
|
+
handle: (frame: Uint8Array, from: PeerId) => Promise<Uint8Array>,
|
|
2880
|
+
maxBytes: number,
|
|
2881
|
+
): (stream: Stream, connection: Connection) => void {
|
|
2882
|
+
return makeFrameHandler(handle, maxBytes);
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
/**
|
|
2886
|
+
* Serve a one-way protocol: read one framed message and send nothing back (cohort-gossip, promote).
|
|
2887
|
+
* Their dialers use `sendOneWay`, never read, and have typically already closed the stream — a reply
|
|
2888
|
+
* would be wasted at best and a write into a closed stream at worst.
|
|
2889
|
+
*/
|
|
2890
|
+
function makeOneWayHandler(
|
|
2891
|
+
handle: (frame: Uint8Array, from: PeerId) => Promise<void>,
|
|
2892
|
+
maxBytes: number,
|
|
2893
|
+
): (stream: Stream, connection: Connection) => void {
|
|
2894
|
+
return makeFrameHandler(async (frame, from): Promise<undefined> => {
|
|
2895
|
+
await handle(frame, from);
|
|
2896
|
+
return undefined;
|
|
2897
|
+
}, maxBytes);
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
/** Shared read-one / maybe-reply-one libp2p stream lifecycle behind the two constructors above. */
|
|
2870
2901
|
function makeFrameHandler(
|
|
2871
2902
|
handle: (frame: Uint8Array, from: PeerId) => Promise<Uint8Array | undefined>,
|
|
2872
2903
|
maxBytes: number,
|
|
@@ -2874,10 +2905,10 @@ function makeFrameHandler(
|
|
|
2874
2905
|
return (stream: Stream, connection: Connection): void => {
|
|
2875
2906
|
void (async (): Promise<void> => {
|
|
2876
2907
|
try {
|
|
2877
|
-
const frame = await
|
|
2908
|
+
const frame = await readFramed(stream, maxBytes);
|
|
2878
2909
|
const reply = await handle(frame, connection.remotePeer);
|
|
2879
2910
|
if (reply !== undefined) {
|
|
2880
|
-
stream
|
|
2911
|
+
sendFramed(stream, reply);
|
|
2881
2912
|
}
|
|
2882
2913
|
await stream.close();
|
|
2883
2914
|
} catch {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export * from "./protocols.js";
|
|
2
|
-
export * from "./peer-codec.js";
|
|
3
|
-
export * from "./peer-sig.js";
|
|
4
|
-
export * from "./stream-util.js";
|
|
5
|
-
export * from "./topic-router.js";
|
|
6
|
-
export * from "./cohort-gossip-transport.js";
|
|
7
|
-
export * from "./cohort-gossip-driver.js";
|
|
8
|
-
export * from "./membership-source.js";
|
|
9
|
-
export * from "./membership-publish-sink.js";
|
|
10
|
-
export * from "./threshold-crypto.js";
|
|
11
|
-
export * from "./size-estimator.js";
|
|
12
|
-
export * from "./host.js";
|
|
13
|
-
export * from "./change-bridge.js";
|
|
1
|
+
export * from "./protocols.js";
|
|
2
|
+
export * from "./peer-codec.js";
|
|
3
|
+
export * from "./peer-sig.js";
|
|
4
|
+
export * from "./stream-util.js";
|
|
5
|
+
export * from "./topic-router.js";
|
|
6
|
+
export * from "./cohort-gossip-transport.js";
|
|
7
|
+
export * from "./cohort-gossip-driver.js";
|
|
8
|
+
export * from "./membership-source.js";
|
|
9
|
+
export * from "./membership-publish-sink.js";
|
|
10
|
+
export * from "./threshold-crypto.js";
|
|
11
|
+
export * from "./size-estimator.js";
|
|
12
|
+
export * from "./host.js";
|
|
13
|
+
export * from "./change-bridge.js";
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import type { IMembershipPublishSink } from "@optimystic/db-core";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* FRET-backed {@link IMembershipPublishSink}: holds the cohort's latest threshold-signed
|
|
5
|
-
* `MembershipCertV1` (already encoded) and serves it over `/optimystic/cohort-topic/1.0.0/membership`.
|
|
6
|
-
* A node publishes only its own cohort's cert, so the most-recent encoded cert is all the host's
|
|
7
|
-
* membership protocol handler needs to answer inbound requests.
|
|
8
|
-
*/
|
|
9
|
-
export class FretMembershipPublishSink implements IMembershipPublishSink {
|
|
10
|
-
private latestCert: Uint8Array | undefined;
|
|
11
|
-
|
|
12
|
-
publish(encodedCert: Uint8Array): void {
|
|
13
|
-
this.latestCert = encodedCert;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** The most recently published encoded cert, or `undefined` if none yet (host serves this). */
|
|
17
|
-
latest(): Uint8Array | undefined {
|
|
18
|
-
return this.latestCert;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
1
|
+
import type { IMembershipPublishSink } from "@optimystic/db-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FRET-backed {@link IMembershipPublishSink}: holds the cohort's latest threshold-signed
|
|
5
|
+
* `MembershipCertV1` (already encoded) and serves it over `/optimystic/cohort-topic/1.0.0/membership`.
|
|
6
|
+
* A node publishes only its own cohort's cert, so the most-recent encoded cert is all the host's
|
|
7
|
+
* membership protocol handler needs to answer inbound requests.
|
|
8
|
+
*/
|
|
9
|
+
export class FretMembershipPublishSink implements IMembershipPublishSink {
|
|
10
|
+
private latestCert: Uint8Array | undefined;
|
|
11
|
+
|
|
12
|
+
publish(encodedCert: Uint8Array): void {
|
|
13
|
+
this.latestCert = encodedCert;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The most recently published encoded cert, or `undefined` if none yet (host serves this). */
|
|
17
|
+
latest(): Uint8Array | undefined {
|
|
18
|
+
return this.latestCert;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import type { IMembershipSource, RingCoord } from "@optimystic/db-core";
|
|
2
|
-
import { bytesToB64url } from "@optimystic/db-core";
|
|
3
|
-
import { peerIdFromString } from "@libp2p/peer-id";
|
|
4
|
-
import type { Libp2p } from "libp2p";
|
|
5
|
-
import type { CohortPeerResolver } from "./cohort-gossip-transport.js";
|
|
6
|
-
import { requestResponse, DEFAULT_STREAM_MAX_BYTES } from "./stream-util.js";
|
|
7
|
-
import { PROTOCOL_COHORT_MEMBERSHIP } from "./protocols.js";
|
|
8
|
-
|
|
9
|
-
export interface FretMembershipSourceOptions {
|
|
10
|
-
readonly membershipProtocol?: string;
|
|
11
|
-
/** Cohort fan-out probed on a `fetch`. Default 16. */
|
|
12
|
-
readonly wants?: number;
|
|
13
|
-
readonly maxBytes?: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* FRET-backed {@link IMembershipSource}: cohort membership snapshots over
|
|
18
|
-
* `/optimystic/cohort-topic/1.0.0/membership`. `current` serves the locally-cached encoded
|
|
19
|
-
* `MembershipCertV1`; `fetch` forces one refresh by requesting the cert from an assembled cohort
|
|
20
|
-
* member (the stale-cache retry the participant-side verifier drives). The host feeds inbound certs
|
|
21
|
-
* into the cache via {@link cache}.
|
|
22
|
-
*/
|
|
23
|
-
export class FretMembershipSource implements IMembershipSource {
|
|
24
|
-
private readonly byCoord = new Map<string, Uint8Array>();
|
|
25
|
-
private readonly membershipProtocol: string;
|
|
26
|
-
private readonly wants: number;
|
|
27
|
-
private readonly maxBytes: number;
|
|
28
|
-
|
|
29
|
-
constructor(private readonly node: Libp2p, private readonly resolver: CohortPeerResolver, options: FretMembershipSourceOptions = {}) {
|
|
30
|
-
this.membershipProtocol = options.membershipProtocol ?? PROTOCOL_COHORT_MEMBERSHIP;
|
|
31
|
-
this.wants = options.wants ?? 16;
|
|
32
|
-
this.maxBytes = options.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
current(coord: RingCoord): Promise<Uint8Array | undefined> {
|
|
36
|
-
return Promise.resolve(this.byCoord.get(bytesToB64url(coord)));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Synchronous local existence read over the in-memory cache: true iff a `MembershipCertV1` is cached for
|
|
41
|
-
* `coord` (no network I/O). The parent-reference bootstrap-evidence verifier's existence view consults
|
|
42
|
-
* this — an admission gate must never dial — so it reads the same `byCoord` map `current()` resolves from.
|
|
43
|
-
*/
|
|
44
|
-
has(coord: RingCoord): boolean {
|
|
45
|
-
return this.byCoord.has(bytesToB64url(coord));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async fetch(coord: RingCoord): Promise<Uint8Array | undefined> {
|
|
49
|
-
const request = coord; // the membership request frame is the raw coord bytes
|
|
50
|
-
for (const peerStr of this.resolver.cohortPeers(coord, this.wants)) {
|
|
51
|
-
try {
|
|
52
|
-
const reply = await requestResponse(this.node, peerIdFromString(peerStr), this.membershipProtocol, request, this.maxBytes);
|
|
53
|
-
if (reply.length > 0) {
|
|
54
|
-
this.cache(coord, reply);
|
|
55
|
-
return reply;
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
// Try the next member; a stale-cache refetch tolerates an unreachable holder.
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Cache an encoded cert for its coord (host feeds inbound/served certs here). */
|
|
65
|
-
cache(coord: RingCoord, encodedCert: Uint8Array): void {
|
|
66
|
-
this.byCoord.set(bytesToB64url(coord), encodedCert);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
import type { IMembershipSource, RingCoord } from "@optimystic/db-core";
|
|
2
|
+
import { bytesToB64url } from "@optimystic/db-core";
|
|
3
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
4
|
+
import type { Libp2p } from "libp2p";
|
|
5
|
+
import type { CohortPeerResolver } from "./cohort-gossip-transport.js";
|
|
6
|
+
import { requestResponse, DEFAULT_STREAM_MAX_BYTES } from "./stream-util.js";
|
|
7
|
+
import { PROTOCOL_COHORT_MEMBERSHIP } from "./protocols.js";
|
|
8
|
+
|
|
9
|
+
export interface FretMembershipSourceOptions {
|
|
10
|
+
readonly membershipProtocol?: string;
|
|
11
|
+
/** Cohort fan-out probed on a `fetch`. Default 16. */
|
|
12
|
+
readonly wants?: number;
|
|
13
|
+
readonly maxBytes?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* FRET-backed {@link IMembershipSource}: cohort membership snapshots over
|
|
18
|
+
* `/optimystic/cohort-topic/1.0.0/membership`. `current` serves the locally-cached encoded
|
|
19
|
+
* `MembershipCertV1`; `fetch` forces one refresh by requesting the cert from an assembled cohort
|
|
20
|
+
* member (the stale-cache retry the participant-side verifier drives). The host feeds inbound certs
|
|
21
|
+
* into the cache via {@link cache}.
|
|
22
|
+
*/
|
|
23
|
+
export class FretMembershipSource implements IMembershipSource {
|
|
24
|
+
private readonly byCoord = new Map<string, Uint8Array>();
|
|
25
|
+
private readonly membershipProtocol: string;
|
|
26
|
+
private readonly wants: number;
|
|
27
|
+
private readonly maxBytes: number;
|
|
28
|
+
|
|
29
|
+
constructor(private readonly node: Libp2p, private readonly resolver: CohortPeerResolver, options: FretMembershipSourceOptions = {}) {
|
|
30
|
+
this.membershipProtocol = options.membershipProtocol ?? PROTOCOL_COHORT_MEMBERSHIP;
|
|
31
|
+
this.wants = options.wants ?? 16;
|
|
32
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
current(coord: RingCoord): Promise<Uint8Array | undefined> {
|
|
36
|
+
return Promise.resolve(this.byCoord.get(bytesToB64url(coord)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Synchronous local existence read over the in-memory cache: true iff a `MembershipCertV1` is cached for
|
|
41
|
+
* `coord` (no network I/O). The parent-reference bootstrap-evidence verifier's existence view consults
|
|
42
|
+
* this — an admission gate must never dial — so it reads the same `byCoord` map `current()` resolves from.
|
|
43
|
+
*/
|
|
44
|
+
has(coord: RingCoord): boolean {
|
|
45
|
+
return this.byCoord.has(bytesToB64url(coord));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async fetch(coord: RingCoord): Promise<Uint8Array | undefined> {
|
|
49
|
+
const request = coord; // the membership request frame is the raw coord bytes
|
|
50
|
+
for (const peerStr of this.resolver.cohortPeers(coord, this.wants)) {
|
|
51
|
+
try {
|
|
52
|
+
const reply = await requestResponse(this.node, peerIdFromString(peerStr), this.membershipProtocol, request, this.maxBytes);
|
|
53
|
+
if (reply.length > 0) {
|
|
54
|
+
this.cache(coord, reply);
|
|
55
|
+
return reply;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Try the next member; a stale-cache refetch tolerates an unreachable holder.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Cache an encoded cert for its coord (host feeds inbound/served certs here). */
|
|
65
|
+
cache(coord: RingCoord, encodedCert: Uint8Array): void {
|
|
66
|
+
this.byCoord.set(bytesToB64url(coord), encodedCert);
|
|
67
|
+
}
|
|
68
|
+
}
|