@optimystic/db-core 0.21.0 → 0.24.0
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/README.md +336 -336
- package/dist/src/btree/btree.d.ts +2 -1
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +1 -1
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +1 -1
- package/dist/src/chain/chain.js.map +1 -1
- package/dist/src/cluster/structs.d.ts +39 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +24 -0
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +20 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +31 -4
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +2 -1
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/diary/struct.js +1 -1
- package/dist/src/collections/diary/struct.js.map +1 -1
- package/dist/src/collections/tree/collection-trunk.js +1 -1
- package/dist/src/collections/tree/collection-trunk.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +1 -1
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +2 -1
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +5 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +7 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/log/log.d.ts +1 -1
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +2 -2
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/network/i-peer-network.d.ts +16 -0
- package/dist/src/network/i-peer-network.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +39 -2
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +18 -0
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +95 -8
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +133 -12
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +2 -1
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +1 -1
- package/dist/src/transaction/transaction.js +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +57 -18
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/cache-source.js +1 -1
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +7 -6
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +2 -1
- package/dist/src/transform/tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/btree/btree.ts +2 -1
- package/src/chain/chain.ts +2 -1
- package/src/cluster/membership.ts +85 -85
- package/src/cluster/structs.ts +43 -4
- package/src/cohort-topic/addressing.ts +120 -120
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
- package/src/cohort-topic/antidos/index.ts +5 -5
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
- package/src/cohort-topic/antidos/replay-guard.ts +146 -146
- package/src/cohort-topic/antidos/topic-budget.ts +160 -160
- package/src/cohort-topic/antiflood/index.ts +2 -2
- package/src/cohort-topic/antiflood/invariants.ts +108 -108
- package/src/cohort-topic/antiflood/jitter.ts +117 -117
- package/src/cohort-topic/coldstart.ts +237 -237
- package/src/cohort-topic/dmax.ts +88 -88
- package/src/cohort-topic/gossip/bus.ts +254 -254
- package/src/cohort-topic/gossip/index.ts +3 -3
- package/src/cohort-topic/gossip/records.ts +45 -45
- package/src/cohort-topic/gossip/view.ts +91 -91
- package/src/cohort-topic/index.ts +20 -20
- package/src/cohort-topic/load/barometer.ts +134 -134
- package/src/cohort-topic/load/index.ts +1 -1
- package/src/cohort-topic/member-engine.ts +430 -430
- package/src/cohort-topic/membership/index.ts +3 -3
- package/src/cohort-topic/membership/publisher.ts +163 -163
- package/src/cohort-topic/membership/source.ts +41 -41
- package/src/cohort-topic/membership/verifier.ts +461 -461
- package/src/cohort-topic/ports.ts +157 -157
- package/src/cohort-topic/promotion.ts +405 -405
- package/src/cohort-topic/registration/bytes.ts +37 -37
- package/src/cohort-topic/registration/handoff.ts +154 -154
- package/src/cohort-topic/registration/index.ts +6 -6
- package/src/cohort-topic/registration/renewal.ts +495 -495
- package/src/cohort-topic/registration/sharding.ts +61 -61
- package/src/cohort-topic/registration/store.ts +81 -81
- package/src/cohort-topic/registration/types.ts +91 -91
- package/src/cohort-topic/ring-hash.ts +50 -50
- package/src/cohort-topic/service.ts +416 -416
- package/src/cohort-topic/sig/index.ts +2 -2
- package/src/cohort-topic/sig/payloads.ts +59 -59
- package/src/cohort-topic/sig/threshold.ts +64 -64
- package/src/cohort-topic/tiers.ts +74 -74
- package/src/cohort-topic/traffic.ts +233 -233
- package/src/cohort-topic/walk.ts +326 -326
- package/src/cohort-topic/willingness.ts +237 -237
- package/src/cohort-topic/wire/codec.ts +216 -216
- package/src/cohort-topic/wire/index.ts +18 -18
- package/src/cohort-topic/wire/payloads.ts +126 -126
- package/src/cohort-topic/wire/primitives.ts +188 -188
- package/src/cohort-topic/wire/types.ts +475 -475
- package/src/cohort-topic/wire/validate.ts +512 -512
- package/src/collection/collection-type-registry.ts +37 -37
- package/src/collection/collection.ts +32 -4
- package/src/collections/diary/diary.ts +68 -67
- package/src/collections/diary/struct.ts +1 -1
- package/src/collections/tree/collection-trunk.ts +1 -1
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/struct.ts +3 -1
- package/src/collections/tree/tree.ts +320 -312
- package/src/log/log.ts +2 -2
- package/src/matchmaking/capability-filter.ts +45 -45
- package/src/matchmaking/config.ts +98 -98
- package/src/matchmaking/index.ts +21 -21
- package/src/matchmaking/multi-cohort-seeker.ts +234 -234
- package/src/matchmaking/provider.ts +123 -123
- package/src/matchmaking/query-eval.ts +105 -105
- package/src/matchmaking/seeker-walk.ts +127 -127
- package/src/matchmaking/seeker.ts +86 -86
- package/src/matchmaking/topic-anchor.ts +90 -90
- package/src/matchmaking/voting-quorum.ts +394 -394
- package/src/matchmaking/wire.ts +603 -603
- package/src/network/i-peer-network.ts +17 -0
- package/src/network/stale-failure.ts +43 -43
- package/src/network/struct.ts +41 -2
- package/src/network/types.ts +37 -37
- package/src/reactivity/backfill.ts +220 -220
- package/src/reactivity/backpressure.ts +191 -191
- package/src/reactivity/checkpoint.ts +308 -308
- package/src/reactivity/config.ts +172 -172
- package/src/reactivity/dedupe.ts +132 -132
- package/src/reactivity/forwarder.ts +87 -87
- package/src/reactivity/index.ts +34 -34
- package/src/reactivity/notification.ts +123 -123
- package/src/reactivity/policy.ts +79 -79
- package/src/reactivity/push-state.ts +310 -310
- package/src/reactivity/recover.ts +153 -153
- package/src/reactivity/replay-buffer.ts +141 -141
- package/src/reactivity/resume.ts +549 -549
- package/src/reactivity/rotation.ts +415 -415
- package/src/reactivity/subscriber.ts +132 -132
- package/src/reactivity/subscription.ts +66 -66
- package/src/reactivity/topic-anchor.ts +71 -71
- package/src/reactivity/verify.ts +73 -73
- package/src/reactivity/wire-validate.ts +13 -13
- package/src/reactivity/wire.ts +224 -224
- package/src/testing/async-wait.ts +65 -65
- package/src/testing/index.ts +2 -2
- package/src/testing/test-transactor.ts +638 -489
- package/src/transaction/coordinator.ts +2 -1
- package/src/transaction/errors.ts +91 -91
- package/src/transaction/operations-hash.ts +196 -196
- package/src/transaction/read-dependency-collector.ts +78 -78
- package/src/transaction/transaction.ts +1 -1
- package/src/transactor/change-notifier.ts +80 -80
- package/src/transactor/index.ts +5 -5
- package/src/transactor/network-transactor.ts +58 -19
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/cache-source.ts +1 -1
- package/src/transform/helpers.ts +159 -158
- package/src/transform/tracker.ts +2 -1
- package/src/utility/backoff.ts +95 -95
- package/src/utility/batch-coordinator.ts +191 -191
- package/dist/src/transaction/context.d.ts +0 -60
- package/dist/src/transaction/context.d.ts.map +0 -1
- package/dist/src/transaction/context.js +0 -91
- package/dist/src/transaction/context.js.map +0 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./source.js";
|
|
2
|
-
export * from "./verifier.js";
|
|
3
|
-
export * from "./publisher.js";
|
|
1
|
+
export * from "./source.js";
|
|
2
|
+
export * from "./verifier.js";
|
|
3
|
+
export * from "./publisher.js";
|
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cohort-topic substrate — cohort-side `MembershipCertV1` publication.
|
|
3
|
-
*
|
|
4
|
-
* Per `docs/cohort-topic.md` §Membership snapshots: the cohort publishes a threshold-signed
|
|
5
|
-
* `MembershipCertV1`
|
|
6
|
-
*
|
|
7
|
-
* - at stabilization, and on any change to the cohort identity — the epoch `H(sorted all members)`,
|
|
8
|
-
* so *any* member change (head or tail) forces a fresh publish ({@link MembershipCertPublisher.onStabilized});
|
|
9
|
-
* - refreshed every `T_membership_refresh` (default 5 min) ({@link MembershipCertPublisher.tick}).
|
|
10
|
-
*
|
|
11
|
-
* The publisher is db-core logic over the {@link CohortSigner} (threshold sign) and the
|
|
12
|
-
* {@link IMembershipPublishSink} (serve/advertise). It never imports FRET. Members are sorted
|
|
13
|
-
* ascending for the cert (matching the sharding order); the snapshot's `cohortEpoch` is the full
|
|
14
|
-
* cohort identity, and a change to it across a publish forces a fresh publish.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { IMembershipPublishSink } from "../ports.js";
|
|
18
|
-
import { bytesToB64url, encodeCohortMessage } from "../wire/codec.js";
|
|
19
|
-
import type { MembershipCertV1 } from "../wire/types.js";
|
|
20
|
-
import { compareBytes } from "../registration/bytes.js";
|
|
21
|
-
import { type CohortSigner } from "../sig/threshold.js";
|
|
22
|
-
import { membershipCertSigningPayload, type MembershipCertSignable } from "../sig/payloads.js";
|
|
23
|
-
|
|
24
|
-
/** Default membership-cert refresh interval (`T_membership_refresh`). */
|
|
25
|
-
export const DEFAULT_T_MEMBERSHIP_REFRESH_MS = 5 * 60_000;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The canonical {@link MembershipCertSignable} for a stabilized snapshot: members sorted ascending (the
|
|
29
|
-
* cert / sharding order), byte fields base64url-encoded, over exactly the threshold-signed fields. The
|
|
30
|
-
* publisher signs `membershipCertSigningPayload` of this; a rotation-attestation **producer** (db-p2p,
|
|
31
|
-
* `cohort-topic-trust-anchor-rotation-production`) signs the *successor* cert's image through the SAME
|
|
32
|
-
* helper, so signer and verifier never canonicalize the cert independently (signature-image agreement —
|
|
33
|
-
* a mismatch makes the db-core chain check fail).
|
|
34
|
-
*/
|
|
35
|
-
export function membershipCertSignable(snapshot: CohortSnapshot): MembershipCertSignable {
|
|
36
|
-
const sorted = [...snapshot.members].sort(compareBytes);
|
|
37
|
-
return {
|
|
38
|
-
cohortCoord: bytesToB64url(snapshot.coord),
|
|
39
|
-
cohortEpoch: bytesToB64url(snapshot.cohortEpoch),
|
|
40
|
-
members: sorted.map(bytesToB64url),
|
|
41
|
-
stabilizedAt: snapshot.stabilizedAt,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** A stabilized cohort snapshot the publisher attests. */
|
|
46
|
-
export interface CohortSnapshot {
|
|
47
|
-
/** Cohort coord (32 bytes). */
|
|
48
|
-
coord: Uint8Array;
|
|
49
|
-
/** Cohort epoch for this snapshot. */
|
|
50
|
-
cohortEpoch: Uint8Array;
|
|
51
|
-
/** Cohort members (any order; the cert sorts them ascending). */
|
|
52
|
-
members: readonly Uint8Array[];
|
|
53
|
-
/** Unix ms this membership stabilized. */
|
|
54
|
-
stabilizedAt: number;
|
|
55
|
-
/** Optional FRET stabilization proof. */
|
|
56
|
-
fretAttestation?: Uint8Array;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* A predecessor-cohort rotation attestation to attach to a fresh cert (an epoch rotation). The
|
|
61
|
-
* predecessor cohort threshold-signs the **successor** cert's `membershipCertSigningPayload`; that
|
|
62
|
-
* `(prevEpoch, rotationSig, rotationSigners)` triple lets a participant holding a trusted predecessor
|
|
63
|
-
* verify the rotation is legitimate. Producing this triple is the db-p2p ticket
|
|
64
|
-
* `cohort-topic-trust-anchor-rotation-production`; the publisher only attaches a provided one.
|
|
65
|
-
*/
|
|
66
|
-
export interface RotationAttestation {
|
|
67
|
-
/** Predecessor cohort epoch this cert rotates from (32 bytes). */
|
|
68
|
-
prevEpoch: Uint8Array;
|
|
69
|
-
/** Predecessor cohort's threshold signature over the successor cert's signing payload. */
|
|
70
|
-
rotationSig: Uint8Array;
|
|
71
|
-
/** Predecessor cohort signers that produced {@link rotationSig}; `>= minSigs`. */
|
|
72
|
-
rotationSigners: readonly Uint8Array[];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Cohort-side membership-cert publisher. */
|
|
76
|
-
export interface MembershipCertPublisher {
|
|
77
|
-
/**
|
|
78
|
-
* Drive publication on a stabilization event. Publishes (and returns the cert) when the cohort identity
|
|
79
|
-
* (`cohortEpoch = H(sorted members)`) changed since the last publish — i.e. on **any** member change,
|
|
80
|
-
* head or tail — or on the first call; otherwise returns `undefined`. Pass `rotation` to attach a
|
|
81
|
-
* predecessor-cohort rotation attestation (an epoch rotation); omit it for the default non-rotation
|
|
82
|
-
* publish (no rotation fields are emitted).
|
|
83
|
-
*/
|
|
84
|
-
onStabilized(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1 | undefined>;
|
|
85
|
-
/**
|
|
86
|
-
* Periodic tick. Re-publishes (and returns the cert) when `T_membership_refresh` has elapsed since
|
|
87
|
-
* the last publish; otherwise returns `undefined`. Pass `rotation` to attach a rotation attestation.
|
|
88
|
-
*/
|
|
89
|
-
tick(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1 | undefined>;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface MembershipCertPublisherDeps {
|
|
93
|
-
signer: CohortSigner;
|
|
94
|
-
sink: IMembershipPublishSink;
|
|
95
|
-
refreshMs?: number;
|
|
96
|
-
/**
|
|
97
|
-
* Accepted for call-site compatibility but no longer read by the publisher: the republish gate now keys
|
|
98
|
-
* on the full `cohortEpoch`, not the first `k − x` members, and the threshold-sign quorum lives in the
|
|
99
|
-
* {@link CohortSigner}. Kept so existing callers (host + db-p2p tests) compile unchanged.
|
|
100
|
-
*/
|
|
101
|
-
minSigs?: number;
|
|
102
|
-
maxMessageBytes?: number;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
class SigningMembershipCertPublisher implements MembershipCertPublisher {
|
|
106
|
-
private readonly refreshMs: number;
|
|
107
|
-
/** base64url of the last published snapshot's `cohortEpoch` — the full cohort-identity republish key. */
|
|
108
|
-
private lastEpoch: string | undefined;
|
|
109
|
-
private lastPublishedAt: number | undefined;
|
|
110
|
-
|
|
111
|
-
constructor(private readonly deps: MembershipCertPublisherDeps) {
|
|
112
|
-
this.refreshMs = deps.refreshMs ?? DEFAULT_T_MEMBERSHIP_REFRESH_MS;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async onStabilized(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1 | undefined> {
|
|
116
|
-
// NOTE: republish (and the caller's rotation attestation) now fire on ANY epoch change — any member
|
|
117
|
-
// change, head or tail — not just first-(k − x) churn. Gated on an actual epoch change, never per
|
|
118
|
-
// tick, so cost is bounded by real churn. If a high-churn cohort ever shows excess `/sign` or publish
|
|
119
|
-
// load, reconsider a short debounce / batching here.
|
|
120
|
-
const epoch = bytesToB64url(snapshot.cohortEpoch);
|
|
121
|
-
if (this.lastEpoch !== undefined && this.lastEpoch === epoch) {
|
|
122
|
-
return undefined; // cohort identity unchanged — no republish needed
|
|
123
|
-
}
|
|
124
|
-
return this.publish(snapshot, now, rotation);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async tick(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1 | undefined> {
|
|
128
|
-
if (this.lastPublishedAt !== undefined && now - this.lastPublishedAt < this.refreshMs) {
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
return this.publish(snapshot, now, rotation);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private async publish(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1> {
|
|
135
|
-
const signable = membershipCertSignable(snapshot);
|
|
136
|
-
const { thresholdSig, signers } = await this.deps.signer.thresholdSign(membershipCertSigningPayload(signable));
|
|
137
|
-
const cert: MembershipCertV1 = {
|
|
138
|
-
v: 1,
|
|
139
|
-
...signable,
|
|
140
|
-
thresholdSig: bytesToB64url(thresholdSig),
|
|
141
|
-
signers: signers.map(bytesToB64url),
|
|
142
|
-
};
|
|
143
|
-
if (snapshot.fretAttestation !== undefined) {
|
|
144
|
-
cert.fretAttestation = bytesToB64url(snapshot.fretAttestation);
|
|
145
|
-
}
|
|
146
|
-
// A rotation attestation (when given) signs *over* the cert's signing payload, so it is attached to the
|
|
147
|
-
// envelope after signing and is not part of `membershipCertSigningPayload`. Omitted → no rotation fields.
|
|
148
|
-
if (rotation !== undefined) {
|
|
149
|
-
cert.prevEpoch = bytesToB64url(rotation.prevEpoch);
|
|
150
|
-
cert.rotationSig = bytesToB64url(rotation.rotationSig);
|
|
151
|
-
cert.rotationSigners = rotation.rotationSigners.map(bytesToB64url);
|
|
152
|
-
}
|
|
153
|
-
this.deps.sink.publish(encodeCohortMessage(cert, this.deps.maxMessageBytes));
|
|
154
|
-
this.lastEpoch = bytesToB64url(snapshot.cohortEpoch);
|
|
155
|
-
this.lastPublishedAt = now;
|
|
156
|
-
return cert;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Build a cohort-side {@link MembershipCertPublisher}. */
|
|
161
|
-
export function createMembershipCertPublisher(deps: MembershipCertPublisherDeps): MembershipCertPublisher {
|
|
162
|
-
return new SigningMembershipCertPublisher(deps);
|
|
163
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — cohort-side `MembershipCertV1` publication.
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/cohort-topic.md` §Membership snapshots: the cohort publishes a threshold-signed
|
|
5
|
+
* `MembershipCertV1`
|
|
6
|
+
*
|
|
7
|
+
* - at stabilization, and on any change to the cohort identity — the epoch `H(sorted all members)`,
|
|
8
|
+
* so *any* member change (head or tail) forces a fresh publish ({@link MembershipCertPublisher.onStabilized});
|
|
9
|
+
* - refreshed every `T_membership_refresh` (default 5 min) ({@link MembershipCertPublisher.tick}).
|
|
10
|
+
*
|
|
11
|
+
* The publisher is db-core logic over the {@link CohortSigner} (threshold sign) and the
|
|
12
|
+
* {@link IMembershipPublishSink} (serve/advertise). It never imports FRET. Members are sorted
|
|
13
|
+
* ascending for the cert (matching the sharding order); the snapshot's `cohortEpoch` is the full
|
|
14
|
+
* cohort identity, and a change to it across a publish forces a fresh publish.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { IMembershipPublishSink } from "../ports.js";
|
|
18
|
+
import { bytesToB64url, encodeCohortMessage } from "../wire/codec.js";
|
|
19
|
+
import type { MembershipCertV1 } from "../wire/types.js";
|
|
20
|
+
import { compareBytes } from "../registration/bytes.js";
|
|
21
|
+
import { type CohortSigner } from "../sig/threshold.js";
|
|
22
|
+
import { membershipCertSigningPayload, type MembershipCertSignable } from "../sig/payloads.js";
|
|
23
|
+
|
|
24
|
+
/** Default membership-cert refresh interval (`T_membership_refresh`). */
|
|
25
|
+
export const DEFAULT_T_MEMBERSHIP_REFRESH_MS = 5 * 60_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The canonical {@link MembershipCertSignable} for a stabilized snapshot: members sorted ascending (the
|
|
29
|
+
* cert / sharding order), byte fields base64url-encoded, over exactly the threshold-signed fields. The
|
|
30
|
+
* publisher signs `membershipCertSigningPayload` of this; a rotation-attestation **producer** (db-p2p,
|
|
31
|
+
* `cohort-topic-trust-anchor-rotation-production`) signs the *successor* cert's image through the SAME
|
|
32
|
+
* helper, so signer and verifier never canonicalize the cert independently (signature-image agreement —
|
|
33
|
+
* a mismatch makes the db-core chain check fail).
|
|
34
|
+
*/
|
|
35
|
+
export function membershipCertSignable(snapshot: CohortSnapshot): MembershipCertSignable {
|
|
36
|
+
const sorted = [...snapshot.members].sort(compareBytes);
|
|
37
|
+
return {
|
|
38
|
+
cohortCoord: bytesToB64url(snapshot.coord),
|
|
39
|
+
cohortEpoch: bytesToB64url(snapshot.cohortEpoch),
|
|
40
|
+
members: sorted.map(bytesToB64url),
|
|
41
|
+
stabilizedAt: snapshot.stabilizedAt,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A stabilized cohort snapshot the publisher attests. */
|
|
46
|
+
export interface CohortSnapshot {
|
|
47
|
+
/** Cohort coord (32 bytes). */
|
|
48
|
+
coord: Uint8Array;
|
|
49
|
+
/** Cohort epoch for this snapshot. */
|
|
50
|
+
cohortEpoch: Uint8Array;
|
|
51
|
+
/** Cohort members (any order; the cert sorts them ascending). */
|
|
52
|
+
members: readonly Uint8Array[];
|
|
53
|
+
/** Unix ms this membership stabilized. */
|
|
54
|
+
stabilizedAt: number;
|
|
55
|
+
/** Optional FRET stabilization proof. */
|
|
56
|
+
fretAttestation?: Uint8Array;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A predecessor-cohort rotation attestation to attach to a fresh cert (an epoch rotation). The
|
|
61
|
+
* predecessor cohort threshold-signs the **successor** cert's `membershipCertSigningPayload`; that
|
|
62
|
+
* `(prevEpoch, rotationSig, rotationSigners)` triple lets a participant holding a trusted predecessor
|
|
63
|
+
* verify the rotation is legitimate. Producing this triple is the db-p2p ticket
|
|
64
|
+
* `cohort-topic-trust-anchor-rotation-production`; the publisher only attaches a provided one.
|
|
65
|
+
*/
|
|
66
|
+
export interface RotationAttestation {
|
|
67
|
+
/** Predecessor cohort epoch this cert rotates from (32 bytes). */
|
|
68
|
+
prevEpoch: Uint8Array;
|
|
69
|
+
/** Predecessor cohort's threshold signature over the successor cert's signing payload. */
|
|
70
|
+
rotationSig: Uint8Array;
|
|
71
|
+
/** Predecessor cohort signers that produced {@link rotationSig}; `>= minSigs`. */
|
|
72
|
+
rotationSigners: readonly Uint8Array[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Cohort-side membership-cert publisher. */
|
|
76
|
+
export interface MembershipCertPublisher {
|
|
77
|
+
/**
|
|
78
|
+
* Drive publication on a stabilization event. Publishes (and returns the cert) when the cohort identity
|
|
79
|
+
* (`cohortEpoch = H(sorted members)`) changed since the last publish — i.e. on **any** member change,
|
|
80
|
+
* head or tail — or on the first call; otherwise returns `undefined`. Pass `rotation` to attach a
|
|
81
|
+
* predecessor-cohort rotation attestation (an epoch rotation); omit it for the default non-rotation
|
|
82
|
+
* publish (no rotation fields are emitted).
|
|
83
|
+
*/
|
|
84
|
+
onStabilized(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1 | undefined>;
|
|
85
|
+
/**
|
|
86
|
+
* Periodic tick. Re-publishes (and returns the cert) when `T_membership_refresh` has elapsed since
|
|
87
|
+
* the last publish; otherwise returns `undefined`. Pass `rotation` to attach a rotation attestation.
|
|
88
|
+
*/
|
|
89
|
+
tick(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1 | undefined>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MembershipCertPublisherDeps {
|
|
93
|
+
signer: CohortSigner;
|
|
94
|
+
sink: IMembershipPublishSink;
|
|
95
|
+
refreshMs?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Accepted for call-site compatibility but no longer read by the publisher: the republish gate now keys
|
|
98
|
+
* on the full `cohortEpoch`, not the first `k − x` members, and the threshold-sign quorum lives in the
|
|
99
|
+
* {@link CohortSigner}. Kept so existing callers (host + db-p2p tests) compile unchanged.
|
|
100
|
+
*/
|
|
101
|
+
minSigs?: number;
|
|
102
|
+
maxMessageBytes?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class SigningMembershipCertPublisher implements MembershipCertPublisher {
|
|
106
|
+
private readonly refreshMs: number;
|
|
107
|
+
/** base64url of the last published snapshot's `cohortEpoch` — the full cohort-identity republish key. */
|
|
108
|
+
private lastEpoch: string | undefined;
|
|
109
|
+
private lastPublishedAt: number | undefined;
|
|
110
|
+
|
|
111
|
+
constructor(private readonly deps: MembershipCertPublisherDeps) {
|
|
112
|
+
this.refreshMs = deps.refreshMs ?? DEFAULT_T_MEMBERSHIP_REFRESH_MS;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async onStabilized(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1 | undefined> {
|
|
116
|
+
// NOTE: republish (and the caller's rotation attestation) now fire on ANY epoch change — any member
|
|
117
|
+
// change, head or tail — not just first-(k − x) churn. Gated on an actual epoch change, never per
|
|
118
|
+
// tick, so cost is bounded by real churn. If a high-churn cohort ever shows excess `/sign` or publish
|
|
119
|
+
// load, reconsider a short debounce / batching here.
|
|
120
|
+
const epoch = bytesToB64url(snapshot.cohortEpoch);
|
|
121
|
+
if (this.lastEpoch !== undefined && this.lastEpoch === epoch) {
|
|
122
|
+
return undefined; // cohort identity unchanged — no republish needed
|
|
123
|
+
}
|
|
124
|
+
return this.publish(snapshot, now, rotation);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async tick(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1 | undefined> {
|
|
128
|
+
if (this.lastPublishedAt !== undefined && now - this.lastPublishedAt < this.refreshMs) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return this.publish(snapshot, now, rotation);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async publish(snapshot: CohortSnapshot, now: number, rotation?: RotationAttestation): Promise<MembershipCertV1> {
|
|
135
|
+
const signable = membershipCertSignable(snapshot);
|
|
136
|
+
const { thresholdSig, signers } = await this.deps.signer.thresholdSign(membershipCertSigningPayload(signable));
|
|
137
|
+
const cert: MembershipCertV1 = {
|
|
138
|
+
v: 1,
|
|
139
|
+
...signable,
|
|
140
|
+
thresholdSig: bytesToB64url(thresholdSig),
|
|
141
|
+
signers: signers.map(bytesToB64url),
|
|
142
|
+
};
|
|
143
|
+
if (snapshot.fretAttestation !== undefined) {
|
|
144
|
+
cert.fretAttestation = bytesToB64url(snapshot.fretAttestation);
|
|
145
|
+
}
|
|
146
|
+
// A rotation attestation (when given) signs *over* the cert's signing payload, so it is attached to the
|
|
147
|
+
// envelope after signing and is not part of `membershipCertSigningPayload`. Omitted → no rotation fields.
|
|
148
|
+
if (rotation !== undefined) {
|
|
149
|
+
cert.prevEpoch = bytesToB64url(rotation.prevEpoch);
|
|
150
|
+
cert.rotationSig = bytesToB64url(rotation.rotationSig);
|
|
151
|
+
cert.rotationSigners = rotation.rotationSigners.map(bytesToB64url);
|
|
152
|
+
}
|
|
153
|
+
this.deps.sink.publish(encodeCohortMessage(cert, this.deps.maxMessageBytes));
|
|
154
|
+
this.lastEpoch = bytesToB64url(snapshot.cohortEpoch);
|
|
155
|
+
this.lastPublishedAt = now;
|
|
156
|
+
return cert;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Build a cohort-side {@link MembershipCertPublisher}. */
|
|
161
|
+
export function createMembershipCertPublisher(deps: MembershipCertPublisherDeps): MembershipCertPublisher {
|
|
162
|
+
return new SigningMembershipCertPublisher(deps);
|
|
163
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cohort-topic substrate — membership-source dispatch.
|
|
3
|
-
*
|
|
4
|
-
* Resolved open question (per `docs/cohort-topic.md` §Membership source): how *committed*
|
|
5
|
-
* membership is obtained differs by tier.
|
|
6
|
-
*
|
|
7
|
-
* - **T0/T1** cohorts serve committed work (transaction commits, chain serving); their membership is
|
|
8
|
-
* anchored in the transaction log's commit certificate. The verifier reads it from there (it never
|
|
9
|
-
* writes the log).
|
|
10
|
-
* - **T2/T3** cohorts (matchmaking, push forwarding) derive membership from current FRET state and
|
|
11
|
-
* verify against FRET's signed `MembershipCertV1` advertisements.
|
|
12
|
-
*
|
|
13
|
-
* Both back-ends present the same {@link IMembershipSource} surface; this router picks between them by
|
|
14
|
-
* tier. db-p2p supplies the two concrete sources (tx-log reader and FRET cert source).
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { IMembershipSource } from "../ports.js";
|
|
18
|
-
|
|
19
|
-
/** Picks the authoritative membership source for a cohort by its tier. */
|
|
20
|
-
export interface IMembershipSourceRouter {
|
|
21
|
-
/** Source for a cohort at `tier`: T0/T1 → committed tx-log, T2/T3 → FRET certs. */
|
|
22
|
-
for(tier: number): IMembershipSource;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Tiers whose committed membership is anchored in the transaction log. */
|
|
26
|
-
function isCommittedTier(tier: number): boolean {
|
|
27
|
-
return tier === 0 || tier === 1;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Build a router over the two membership back-ends.
|
|
32
|
-
* @param deps.committed source backed by the transaction-log commit certificate (T0/T1).
|
|
33
|
-
* @param deps.fret source backed by FRET's signed `MembershipCertV1` advertisements (T2/T3).
|
|
34
|
-
*/
|
|
35
|
-
export function createMembershipSourceRouter(deps: { committed: IMembershipSource; fret: IMembershipSource }): IMembershipSourceRouter {
|
|
36
|
-
return {
|
|
37
|
-
for(tier: number): IMembershipSource {
|
|
38
|
-
return isCommittedTier(tier) ? deps.committed : deps.fret;
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — membership-source dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Resolved open question (per `docs/cohort-topic.md` §Membership source): how *committed*
|
|
5
|
+
* membership is obtained differs by tier.
|
|
6
|
+
*
|
|
7
|
+
* - **T0/T1** cohorts serve committed work (transaction commits, chain serving); their membership is
|
|
8
|
+
* anchored in the transaction log's commit certificate. The verifier reads it from there (it never
|
|
9
|
+
* writes the log).
|
|
10
|
+
* - **T2/T3** cohorts (matchmaking, push forwarding) derive membership from current FRET state and
|
|
11
|
+
* verify against FRET's signed `MembershipCertV1` advertisements.
|
|
12
|
+
*
|
|
13
|
+
* Both back-ends present the same {@link IMembershipSource} surface; this router picks between them by
|
|
14
|
+
* tier. db-p2p supplies the two concrete sources (tx-log reader and FRET cert source).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { IMembershipSource } from "../ports.js";
|
|
18
|
+
|
|
19
|
+
/** Picks the authoritative membership source for a cohort by its tier. */
|
|
20
|
+
export interface IMembershipSourceRouter {
|
|
21
|
+
/** Source for a cohort at `tier`: T0/T1 → committed tx-log, T2/T3 → FRET certs. */
|
|
22
|
+
for(tier: number): IMembershipSource;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Tiers whose committed membership is anchored in the transaction log. */
|
|
26
|
+
function isCommittedTier(tier: number): boolean {
|
|
27
|
+
return tier === 0 || tier === 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a router over the two membership back-ends.
|
|
32
|
+
* @param deps.committed source backed by the transaction-log commit certificate (T0/T1).
|
|
33
|
+
* @param deps.fret source backed by FRET's signed `MembershipCertV1` advertisements (T2/T3).
|
|
34
|
+
*/
|
|
35
|
+
export function createMembershipSourceRouter(deps: { committed: IMembershipSource; fret: IMembershipSource }): IMembershipSourceRouter {
|
|
36
|
+
return {
|
|
37
|
+
for(tier: number): IMembershipSource {
|
|
38
|
+
return isCommittedTier(tier) ? deps.committed : deps.fret;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|