@optimystic/db-core 0.22.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/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 +17 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +24 -2
- package/dist/src/collection/collection.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/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 +121 -8
- package/dist/src/testing/test-transactor.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 +48 -13
- 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/package.json +1 -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 +25 -2
- package/src/collections/diary/diary.ts +68 -68
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/tree.ts +320 -312
- 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 -502
- 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 +49 -14
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/helpers.ts +159 -159
- 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,253 +1,253 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cohort-topic substrate — bootstrap-evidence envelope (anti-DoS, crypto-free).
|
|
3
|
-
*
|
|
4
|
-
* The on-the-wire structure a participant attaches to a cold-start `bootstrap: true` register, and the
|
|
5
|
-
* byte-level canonicalization both sides agree on. This module owns only the **format** — the versioned
|
|
6
|
-
* envelope, the canonical anti-replay bound image, and the proof-of-work puzzle's preimage/difficulty.
|
|
7
|
-
* It embeds **no cryptography**: the actual PoW hashing, reputation-signature checks, and parent-topic
|
|
8
|
-
* verification live in db-p2p (which binds the node's `RingHash` and peer-key crypto). The sibling
|
|
9
|
-
* discipline of `wire/payloads.ts` / `sig/payloads.ts` — explicitly-ordered arrays, deterministic UTF-8
|
|
10
|
-
* JSON, base64url without padding — so a participant who *mints* evidence and a cohort that *verifies*
|
|
11
|
-
* it never re-canonicalize bytes independently.
|
|
12
|
-
*
|
|
13
|
-
* The envelope rides in the dedicated, signature-covered {@link RegisterV1.bootstrapEvidence} field
|
|
14
|
-
* (not `appPayload`). A verifier reads only the kind its tier accepts; an absent kind, a malformed
|
|
15
|
-
* envelope, or a wrong/future version all surface as "this kind not offered" (the parse is **total** —
|
|
16
|
-
* like `verifyPeerSig`, any decode error yields `undefined`, never a throw), so a verifier fails its
|
|
17
|
-
* check (→ `unwilling_cohort`) rather than crashing on attacker-supplied input.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { b64urlToBytes, bytesToB64url } from "../wire/codec.js";
|
|
21
|
-
import type { RegisterV1 } from "../wire/types.js";
|
|
22
|
-
|
|
23
|
-
const utf8 = new TextEncoder();
|
|
24
|
-
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
-
|
|
26
|
-
/** Proof-of-work evidence (T2/T3 path). `nonce` is base64url, bound via {@link powPreimage}. */
|
|
27
|
-
export interface PowEvidenceV1 {
|
|
28
|
-
/** Base64url nonce; `hash(powPreimage(reg, nonce))` must satisfy {@link meetsDifficulty}. */
|
|
29
|
-
nonce: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Signed parent-topic reference (all tiers). Both base64url; `sig` is over {@link parentRefSigningImage}. */
|
|
33
|
-
export interface ParentRefEvidenceV1 {
|
|
34
|
-
/** The committed parent topic id, base64url. */
|
|
35
|
-
parentTopicId: string;
|
|
36
|
-
/** Participant peer-key signature over {@link parentRefSigningImage} (the bound tuple extended with `parentTopicId`), base64url. */
|
|
37
|
-
sig: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Reputation endorsement (T2/T3 path). `referee` is the endorsing peer-id-string bytes (base64url) and
|
|
42
|
-
* `sig` is that referee's peer-key signature over the bound image. The referee MAY equal the
|
|
43
|
-
* participant (a reputable participant self-vouches).
|
|
44
|
-
*/
|
|
45
|
-
export interface ReputationEvidenceV1 {
|
|
46
|
-
/** Endorsing peer-id-string bytes, base64url. */
|
|
47
|
-
referee: string;
|
|
48
|
-
/** Referee's peer-key signature over the bound image, base64url. */
|
|
49
|
-
sig: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** V1 bootstrap-evidence envelope, base64url-encoded into {@link RegisterV1.bootstrapEvidence}. */
|
|
53
|
-
export interface BootstrapEvidenceEnvelopeV1 {
|
|
54
|
-
v: 1;
|
|
55
|
-
/** Proof-of-work (T2/T3 path). Absent → no PoW offered. */
|
|
56
|
-
pow?: PowEvidenceV1;
|
|
57
|
-
/** Signed parent-topic reference (all tiers). Absent → none offered. */
|
|
58
|
-
parentRef?: ParentRefEvidenceV1;
|
|
59
|
-
/** Reputation endorsement (T2/T3 path). Absent → none offered. */
|
|
60
|
-
reputation?: ReputationEvidenceV1;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The four register fields every bootstrap-evidence kind is bound to (the anti-replay tuple). The full
|
|
65
|
-
* {@link RegisterV1} satisfies this, so a verifier passes its decoded register directly; the
|
|
66
|
-
* participant-side builder passes just these fields (it has no full register yet).
|
|
67
|
-
*/
|
|
68
|
-
export type BootstrapBoundFields = Pick<RegisterV1, "topicId" | "tier" | "participantCoord" | "timestamp">;
|
|
69
|
-
|
|
70
|
-
/** Default PoW difficulty: leading zero bits required. ~2^bits hashes to mint, one hash to verify. */
|
|
71
|
-
export const DEFAULT_POW_DIFFICULTY_BITS = 20;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The canonical bytes every bootstrap-evidence kind is bound to: `(topicId, tier, participantCoord,
|
|
75
|
-
* timestamp)`. `topicId`/`participantCoord` are bound as their base64url wire strings verbatim (matching
|
|
76
|
-
* `sig/payloads.ts`) so signer and verifier never re-canonicalize bytes independently. Binding all four
|
|
77
|
-
* means evidence minted for one (topic, tier, peer, time) cannot be replayed for another; binding
|
|
78
|
-
* `timestamp` additionally bounds a captured proof's reuse window, since the register replay guard drops
|
|
79
|
-
* a `timestamp` older than its acceptance window.
|
|
80
|
-
*/
|
|
81
|
-
export function bootstrapBoundImage(reg: BootstrapBoundFields): Uint8Array {
|
|
82
|
-
return utf8.encode(JSON.stringify([
|
|
83
|
-
"BootstrapEvidenceV1",
|
|
84
|
-
reg.topicId,
|
|
85
|
-
reg.tier,
|
|
86
|
-
reg.participantCoord,
|
|
87
|
-
reg.timestamp,
|
|
88
|
-
]));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The canonical bytes a **signed parent-topic reference** binds: the {@link bootstrapBoundImage} tuple
|
|
93
|
-
* `(topicId, tier, participantCoord, timestamp)` **extended with the referenced `parentTopicId`**, under a
|
|
94
|
-
* distinct discriminator tag (`"BootstrapParentRefV1"`). Extending the image with `parentTopicId` means a
|
|
95
|
-
* reference minted for one `(topic, tier, peer, time, parent)` cannot be lifted onto another register —
|
|
96
|
-
* including onto a register naming a *different* parent; the distinct tag keeps it from colliding with a
|
|
97
|
-
* {@link bootstrapBoundImage} reputation/PoW signature (domain separation). `topicId`/`participantCoord`/
|
|
98
|
-
* `parentTopicId` are bound as their base64url wire strings verbatim, like {@link bootstrapBoundImage}, so
|
|
99
|
-
* the participant who *signs* the reference and the cohort that *verifies* it never re-canonicalize bytes.
|
|
100
|
-
*/
|
|
101
|
-
export function parentRefSigningImage(reg: BootstrapBoundFields, parentTopicId: string): Uint8Array {
|
|
102
|
-
return utf8.encode(JSON.stringify([
|
|
103
|
-
"BootstrapParentRefV1",
|
|
104
|
-
reg.topicId,
|
|
105
|
-
reg.tier,
|
|
106
|
-
reg.participantCoord,
|
|
107
|
-
reg.timestamp,
|
|
108
|
-
parentTopicId,
|
|
109
|
-
]));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The proof-of-work hash preimage: {@link bootstrapBoundImage} concatenated with `nonce`. db-p2p hashes
|
|
114
|
-
* this (via the node's `RingHash`) and checks the digest against {@link meetsDifficulty}. Bound to the
|
|
115
|
-
* register tuple ⇒ no cross-topic / cross-peer replay; cheap to verify (one hash + bit check), tunably
|
|
116
|
-
* costly to produce.
|
|
117
|
-
*/
|
|
118
|
-
export function powPreimage(reg: BootstrapBoundFields, nonce: Uint8Array): Uint8Array {
|
|
119
|
-
const image = bootstrapBoundImage(reg);
|
|
120
|
-
const out = new Uint8Array(image.length + nonce.length);
|
|
121
|
-
out.set(image, 0);
|
|
122
|
-
out.set(nonce, image.length);
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* True iff the first `bits` most-significant bits of `hash` are zero (the PoW difficulty target). Bits
|
|
128
|
-
* are read MSB-first per byte (mirroring `addressing.ts`'s `prefixBits`). `bits = 0` is trivially met
|
|
129
|
-
* (lets a config disable PoW cost for tests); a non-finite `bits` is a guard-failure (never met); a
|
|
130
|
-
* negative `bits` clamps to 0; `bits` larger than `hash.length * 8` requires every bit of `hash` to be
|
|
131
|
-
* zero and so is effectively unsatisfiable for a random hash.
|
|
132
|
-
*/
|
|
133
|
-
export function meetsDifficulty(hash: Uint8Array, bits: number): boolean {
|
|
134
|
-
if (!Number.isFinite(bits)) {
|
|
135
|
-
return false; // NaN / ±Infinity → defensive guard, never satisfiable
|
|
136
|
-
}
|
|
137
|
-
let remaining = Math.floor(bits);
|
|
138
|
-
if (remaining <= 0) {
|
|
139
|
-
return true; // 0 (or clamped-negative) leading zero bits is trivially met
|
|
140
|
-
}
|
|
141
|
-
for (let i = 0; i < hash.length && remaining > 0; i++) {
|
|
142
|
-
const byte = hash[i]!;
|
|
143
|
-
if (remaining >= 8) {
|
|
144
|
-
if (byte !== 0) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
remaining -= 8;
|
|
148
|
-
} else {
|
|
149
|
-
// Check only the top `remaining` MSBs of this partial final byte (MSB-first).
|
|
150
|
-
const mask = (0xff << (8 - remaining)) & 0xff;
|
|
151
|
-
if ((byte & mask) !== 0) {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
remaining = 0;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// Exhausted the hash before satisfying `bits` (oversize target) → unsatisfiable.
|
|
158
|
-
return remaining === 0;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Serialize an envelope to the base64url JSON string carried in {@link RegisterV1.bootstrapEvidence}. */
|
|
162
|
-
export function serializeBootstrapEvidenceEnvelope(env: BootstrapEvidenceEnvelopeV1): string {
|
|
163
|
-
// Rebuild in a fixed field order so serialization is a pure function of the logical content
|
|
164
|
-
// (serialize∘parse is stable regardless of how a caller ordered the source object's keys).
|
|
165
|
-
const out: BootstrapEvidenceEnvelopeV1 = { v: 1 };
|
|
166
|
-
if (env.pow !== undefined) {
|
|
167
|
-
out.pow = { nonce: env.pow.nonce };
|
|
168
|
-
}
|
|
169
|
-
if (env.parentRef !== undefined) {
|
|
170
|
-
out.parentRef = { parentTopicId: env.parentRef.parentTopicId, sig: env.parentRef.sig };
|
|
171
|
-
}
|
|
172
|
-
if (env.reputation !== undefined) {
|
|
173
|
-
out.reputation = { referee: env.reputation.referee, sig: env.reputation.sig };
|
|
174
|
-
}
|
|
175
|
-
return bytesToB64url(utf8.encode(JSON.stringify(out)));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Decode {@link RegisterV1.bootstrapEvidence} (base64url → JSON), structurally validate it, and return
|
|
180
|
-
* the {@link BootstrapEvidenceEnvelopeV1}. **Total**: returns `undefined` on an absent/empty field, a
|
|
181
|
-
* non-base64url or non-JSON body, a wrong/future `v`, or a structurally-invalid kind — never throws. A
|
|
182
|
-
* verifier treats `undefined` as "this kind not offered" and fails its check (fails closed).
|
|
183
|
-
*/
|
|
184
|
-
export function parseBootstrapEvidenceEnvelope(reg: Pick<RegisterV1, "bootstrapEvidence">): BootstrapEvidenceEnvelopeV1 | undefined {
|
|
185
|
-
const raw = reg.bootstrapEvidence;
|
|
186
|
-
if (raw === undefined || raw === "") {
|
|
187
|
-
return undefined; // not offered
|
|
188
|
-
}
|
|
189
|
-
let parsed: unknown;
|
|
190
|
-
try {
|
|
191
|
-
parsed = JSON.parse(utf8Decoder.decode(b64urlToBytes(raw)));
|
|
192
|
-
} catch {
|
|
193
|
-
return undefined; // not base64url / not UTF-8 / not JSON → fail closed
|
|
194
|
-
}
|
|
195
|
-
return narrowEnvelope(parsed);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Structurally narrow an already-parsed value to a v1 envelope, or `undefined`. */
|
|
199
|
-
function narrowEnvelope(value: unknown): BootstrapEvidenceEnvelopeV1 | undefined {
|
|
200
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
201
|
-
return undefined;
|
|
202
|
-
}
|
|
203
|
-
const obj = value as Record<string, unknown>;
|
|
204
|
-
if (obj["v"] !== 1) {
|
|
205
|
-
return undefined; // wrong / future version → fail closed under the v1 reader
|
|
206
|
-
}
|
|
207
|
-
const out: BootstrapEvidenceEnvelopeV1 = { v: 1 };
|
|
208
|
-
|
|
209
|
-
if (obj["pow"] !== undefined) {
|
|
210
|
-
const nonce = b64urlSubfield(obj["pow"], "nonce");
|
|
211
|
-
if (nonce === undefined) {
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
out.pow = { nonce };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (obj["parentRef"] !== undefined) {
|
|
218
|
-
const parentTopicId = b64urlSubfield(obj["parentRef"], "parentTopicId");
|
|
219
|
-
const sig = b64urlSubfield(obj["parentRef"], "sig");
|
|
220
|
-
if (parentTopicId === undefined || sig === undefined) {
|
|
221
|
-
return undefined;
|
|
222
|
-
}
|
|
223
|
-
out.parentRef = { parentTopicId, sig };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (obj["reputation"] !== undefined) {
|
|
227
|
-
const referee = b64urlSubfield(obj["reputation"], "referee");
|
|
228
|
-
const sig = b64urlSubfield(obj["reputation"], "sig");
|
|
229
|
-
if (referee === undefined || sig === undefined) {
|
|
230
|
-
return undefined;
|
|
231
|
-
}
|
|
232
|
-
out.reputation = { referee, sig };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return out;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** A required base64url sub-field: a non-empty string that decodes cleanly, else `undefined`. */
|
|
239
|
-
function b64urlSubfield(container: unknown, key: string): string | undefined {
|
|
240
|
-
if (typeof container !== "object" || container === null || Array.isArray(container)) {
|
|
241
|
-
return undefined;
|
|
242
|
-
}
|
|
243
|
-
const value = (container as Record<string, unknown>)[key];
|
|
244
|
-
if (typeof value !== "string" || value === "") {
|
|
245
|
-
return undefined; // missing, non-string, or empty → treat as absent
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
b64urlToBytes(value); // structural: must decode as base64url
|
|
249
|
-
} catch {
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
return value;
|
|
253
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — bootstrap-evidence envelope (anti-DoS, crypto-free).
|
|
3
|
+
*
|
|
4
|
+
* The on-the-wire structure a participant attaches to a cold-start `bootstrap: true` register, and the
|
|
5
|
+
* byte-level canonicalization both sides agree on. This module owns only the **format** — the versioned
|
|
6
|
+
* envelope, the canonical anti-replay bound image, and the proof-of-work puzzle's preimage/difficulty.
|
|
7
|
+
* It embeds **no cryptography**: the actual PoW hashing, reputation-signature checks, and parent-topic
|
|
8
|
+
* verification live in db-p2p (which binds the node's `RingHash` and peer-key crypto). The sibling
|
|
9
|
+
* discipline of `wire/payloads.ts` / `sig/payloads.ts` — explicitly-ordered arrays, deterministic UTF-8
|
|
10
|
+
* JSON, base64url without padding — so a participant who *mints* evidence and a cohort that *verifies*
|
|
11
|
+
* it never re-canonicalize bytes independently.
|
|
12
|
+
*
|
|
13
|
+
* The envelope rides in the dedicated, signature-covered {@link RegisterV1.bootstrapEvidence} field
|
|
14
|
+
* (not `appPayload`). A verifier reads only the kind its tier accepts; an absent kind, a malformed
|
|
15
|
+
* envelope, or a wrong/future version all surface as "this kind not offered" (the parse is **total** —
|
|
16
|
+
* like `verifyPeerSig`, any decode error yields `undefined`, never a throw), so a verifier fails its
|
|
17
|
+
* check (→ `unwilling_cohort`) rather than crashing on attacker-supplied input.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { b64urlToBytes, bytesToB64url } from "../wire/codec.js";
|
|
21
|
+
import type { RegisterV1 } from "../wire/types.js";
|
|
22
|
+
|
|
23
|
+
const utf8 = new TextEncoder();
|
|
24
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
+
|
|
26
|
+
/** Proof-of-work evidence (T2/T3 path). `nonce` is base64url, bound via {@link powPreimage}. */
|
|
27
|
+
export interface PowEvidenceV1 {
|
|
28
|
+
/** Base64url nonce; `hash(powPreimage(reg, nonce))` must satisfy {@link meetsDifficulty}. */
|
|
29
|
+
nonce: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Signed parent-topic reference (all tiers). Both base64url; `sig` is over {@link parentRefSigningImage}. */
|
|
33
|
+
export interface ParentRefEvidenceV1 {
|
|
34
|
+
/** The committed parent topic id, base64url. */
|
|
35
|
+
parentTopicId: string;
|
|
36
|
+
/** Participant peer-key signature over {@link parentRefSigningImage} (the bound tuple extended with `parentTopicId`), base64url. */
|
|
37
|
+
sig: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reputation endorsement (T2/T3 path). `referee` is the endorsing peer-id-string bytes (base64url) and
|
|
42
|
+
* `sig` is that referee's peer-key signature over the bound image. The referee MAY equal the
|
|
43
|
+
* participant (a reputable participant self-vouches).
|
|
44
|
+
*/
|
|
45
|
+
export interface ReputationEvidenceV1 {
|
|
46
|
+
/** Endorsing peer-id-string bytes, base64url. */
|
|
47
|
+
referee: string;
|
|
48
|
+
/** Referee's peer-key signature over the bound image, base64url. */
|
|
49
|
+
sig: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** V1 bootstrap-evidence envelope, base64url-encoded into {@link RegisterV1.bootstrapEvidence}. */
|
|
53
|
+
export interface BootstrapEvidenceEnvelopeV1 {
|
|
54
|
+
v: 1;
|
|
55
|
+
/** Proof-of-work (T2/T3 path). Absent → no PoW offered. */
|
|
56
|
+
pow?: PowEvidenceV1;
|
|
57
|
+
/** Signed parent-topic reference (all tiers). Absent → none offered. */
|
|
58
|
+
parentRef?: ParentRefEvidenceV1;
|
|
59
|
+
/** Reputation endorsement (T2/T3 path). Absent → none offered. */
|
|
60
|
+
reputation?: ReputationEvidenceV1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The four register fields every bootstrap-evidence kind is bound to (the anti-replay tuple). The full
|
|
65
|
+
* {@link RegisterV1} satisfies this, so a verifier passes its decoded register directly; the
|
|
66
|
+
* participant-side builder passes just these fields (it has no full register yet).
|
|
67
|
+
*/
|
|
68
|
+
export type BootstrapBoundFields = Pick<RegisterV1, "topicId" | "tier" | "participantCoord" | "timestamp">;
|
|
69
|
+
|
|
70
|
+
/** Default PoW difficulty: leading zero bits required. ~2^bits hashes to mint, one hash to verify. */
|
|
71
|
+
export const DEFAULT_POW_DIFFICULTY_BITS = 20;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The canonical bytes every bootstrap-evidence kind is bound to: `(topicId, tier, participantCoord,
|
|
75
|
+
* timestamp)`. `topicId`/`participantCoord` are bound as their base64url wire strings verbatim (matching
|
|
76
|
+
* `sig/payloads.ts`) so signer and verifier never re-canonicalize bytes independently. Binding all four
|
|
77
|
+
* means evidence minted for one (topic, tier, peer, time) cannot be replayed for another; binding
|
|
78
|
+
* `timestamp` additionally bounds a captured proof's reuse window, since the register replay guard drops
|
|
79
|
+
* a `timestamp` older than its acceptance window.
|
|
80
|
+
*/
|
|
81
|
+
export function bootstrapBoundImage(reg: BootstrapBoundFields): Uint8Array {
|
|
82
|
+
return utf8.encode(JSON.stringify([
|
|
83
|
+
"BootstrapEvidenceV1",
|
|
84
|
+
reg.topicId,
|
|
85
|
+
reg.tier,
|
|
86
|
+
reg.participantCoord,
|
|
87
|
+
reg.timestamp,
|
|
88
|
+
]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The canonical bytes a **signed parent-topic reference** binds: the {@link bootstrapBoundImage} tuple
|
|
93
|
+
* `(topicId, tier, participantCoord, timestamp)` **extended with the referenced `parentTopicId`**, under a
|
|
94
|
+
* distinct discriminator tag (`"BootstrapParentRefV1"`). Extending the image with `parentTopicId` means a
|
|
95
|
+
* reference minted for one `(topic, tier, peer, time, parent)` cannot be lifted onto another register —
|
|
96
|
+
* including onto a register naming a *different* parent; the distinct tag keeps it from colliding with a
|
|
97
|
+
* {@link bootstrapBoundImage} reputation/PoW signature (domain separation). `topicId`/`participantCoord`/
|
|
98
|
+
* `parentTopicId` are bound as their base64url wire strings verbatim, like {@link bootstrapBoundImage}, so
|
|
99
|
+
* the participant who *signs* the reference and the cohort that *verifies* it never re-canonicalize bytes.
|
|
100
|
+
*/
|
|
101
|
+
export function parentRefSigningImage(reg: BootstrapBoundFields, parentTopicId: string): Uint8Array {
|
|
102
|
+
return utf8.encode(JSON.stringify([
|
|
103
|
+
"BootstrapParentRefV1",
|
|
104
|
+
reg.topicId,
|
|
105
|
+
reg.tier,
|
|
106
|
+
reg.participantCoord,
|
|
107
|
+
reg.timestamp,
|
|
108
|
+
parentTopicId,
|
|
109
|
+
]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The proof-of-work hash preimage: {@link bootstrapBoundImage} concatenated with `nonce`. db-p2p hashes
|
|
114
|
+
* this (via the node's `RingHash`) and checks the digest against {@link meetsDifficulty}. Bound to the
|
|
115
|
+
* register tuple ⇒ no cross-topic / cross-peer replay; cheap to verify (one hash + bit check), tunably
|
|
116
|
+
* costly to produce.
|
|
117
|
+
*/
|
|
118
|
+
export function powPreimage(reg: BootstrapBoundFields, nonce: Uint8Array): Uint8Array {
|
|
119
|
+
const image = bootstrapBoundImage(reg);
|
|
120
|
+
const out = new Uint8Array(image.length + nonce.length);
|
|
121
|
+
out.set(image, 0);
|
|
122
|
+
out.set(nonce, image.length);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* True iff the first `bits` most-significant bits of `hash` are zero (the PoW difficulty target). Bits
|
|
128
|
+
* are read MSB-first per byte (mirroring `addressing.ts`'s `prefixBits`). `bits = 0` is trivially met
|
|
129
|
+
* (lets a config disable PoW cost for tests); a non-finite `bits` is a guard-failure (never met); a
|
|
130
|
+
* negative `bits` clamps to 0; `bits` larger than `hash.length * 8` requires every bit of `hash` to be
|
|
131
|
+
* zero and so is effectively unsatisfiable for a random hash.
|
|
132
|
+
*/
|
|
133
|
+
export function meetsDifficulty(hash: Uint8Array, bits: number): boolean {
|
|
134
|
+
if (!Number.isFinite(bits)) {
|
|
135
|
+
return false; // NaN / ±Infinity → defensive guard, never satisfiable
|
|
136
|
+
}
|
|
137
|
+
let remaining = Math.floor(bits);
|
|
138
|
+
if (remaining <= 0) {
|
|
139
|
+
return true; // 0 (or clamped-negative) leading zero bits is trivially met
|
|
140
|
+
}
|
|
141
|
+
for (let i = 0; i < hash.length && remaining > 0; i++) {
|
|
142
|
+
const byte = hash[i]!;
|
|
143
|
+
if (remaining >= 8) {
|
|
144
|
+
if (byte !== 0) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
remaining -= 8;
|
|
148
|
+
} else {
|
|
149
|
+
// Check only the top `remaining` MSBs of this partial final byte (MSB-first).
|
|
150
|
+
const mask = (0xff << (8 - remaining)) & 0xff;
|
|
151
|
+
if ((byte & mask) !== 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
remaining = 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Exhausted the hash before satisfying `bits` (oversize target) → unsatisfiable.
|
|
158
|
+
return remaining === 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Serialize an envelope to the base64url JSON string carried in {@link RegisterV1.bootstrapEvidence}. */
|
|
162
|
+
export function serializeBootstrapEvidenceEnvelope(env: BootstrapEvidenceEnvelopeV1): string {
|
|
163
|
+
// Rebuild in a fixed field order so serialization is a pure function of the logical content
|
|
164
|
+
// (serialize∘parse is stable regardless of how a caller ordered the source object's keys).
|
|
165
|
+
const out: BootstrapEvidenceEnvelopeV1 = { v: 1 };
|
|
166
|
+
if (env.pow !== undefined) {
|
|
167
|
+
out.pow = { nonce: env.pow.nonce };
|
|
168
|
+
}
|
|
169
|
+
if (env.parentRef !== undefined) {
|
|
170
|
+
out.parentRef = { parentTopicId: env.parentRef.parentTopicId, sig: env.parentRef.sig };
|
|
171
|
+
}
|
|
172
|
+
if (env.reputation !== undefined) {
|
|
173
|
+
out.reputation = { referee: env.reputation.referee, sig: env.reputation.sig };
|
|
174
|
+
}
|
|
175
|
+
return bytesToB64url(utf8.encode(JSON.stringify(out)));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Decode {@link RegisterV1.bootstrapEvidence} (base64url → JSON), structurally validate it, and return
|
|
180
|
+
* the {@link BootstrapEvidenceEnvelopeV1}. **Total**: returns `undefined` on an absent/empty field, a
|
|
181
|
+
* non-base64url or non-JSON body, a wrong/future `v`, or a structurally-invalid kind — never throws. A
|
|
182
|
+
* verifier treats `undefined` as "this kind not offered" and fails its check (fails closed).
|
|
183
|
+
*/
|
|
184
|
+
export function parseBootstrapEvidenceEnvelope(reg: Pick<RegisterV1, "bootstrapEvidence">): BootstrapEvidenceEnvelopeV1 | undefined {
|
|
185
|
+
const raw = reg.bootstrapEvidence;
|
|
186
|
+
if (raw === undefined || raw === "") {
|
|
187
|
+
return undefined; // not offered
|
|
188
|
+
}
|
|
189
|
+
let parsed: unknown;
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(utf8Decoder.decode(b64urlToBytes(raw)));
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined; // not base64url / not UTF-8 / not JSON → fail closed
|
|
194
|
+
}
|
|
195
|
+
return narrowEnvelope(parsed);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Structurally narrow an already-parsed value to a v1 envelope, or `undefined`. */
|
|
199
|
+
function narrowEnvelope(value: unknown): BootstrapEvidenceEnvelopeV1 | undefined {
|
|
200
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const obj = value as Record<string, unknown>;
|
|
204
|
+
if (obj["v"] !== 1) {
|
|
205
|
+
return undefined; // wrong / future version → fail closed under the v1 reader
|
|
206
|
+
}
|
|
207
|
+
const out: BootstrapEvidenceEnvelopeV1 = { v: 1 };
|
|
208
|
+
|
|
209
|
+
if (obj["pow"] !== undefined) {
|
|
210
|
+
const nonce = b64urlSubfield(obj["pow"], "nonce");
|
|
211
|
+
if (nonce === undefined) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
out.pow = { nonce };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (obj["parentRef"] !== undefined) {
|
|
218
|
+
const parentTopicId = b64urlSubfield(obj["parentRef"], "parentTopicId");
|
|
219
|
+
const sig = b64urlSubfield(obj["parentRef"], "sig");
|
|
220
|
+
if (parentTopicId === undefined || sig === undefined) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
out.parentRef = { parentTopicId, sig };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (obj["reputation"] !== undefined) {
|
|
227
|
+
const referee = b64urlSubfield(obj["reputation"], "referee");
|
|
228
|
+
const sig = b64urlSubfield(obj["reputation"], "sig");
|
|
229
|
+
if (referee === undefined || sig === undefined) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
out.reputation = { referee, sig };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A required base64url sub-field: a non-empty string that decodes cleanly, else `undefined`. */
|
|
239
|
+
function b64urlSubfield(container: unknown, key: string): string | undefined {
|
|
240
|
+
if (typeof container !== "object" || container === null || Array.isArray(container)) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
const value = (container as Record<string, unknown>)[key];
|
|
244
|
+
if (typeof value !== "string" || value === "") {
|
|
245
|
+
return undefined; // missing, non-string, or empty → treat as absent
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
b64urlToBytes(value); // structural: must decode as base64url
|
|
249
|
+
} catch {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|