@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
package/src/cluster/structs.ts
CHANGED
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
import type { RepoMessage } from "../network/repo-protocol.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* One member's vote on a cluster transaction, in either the promise or the commit map.
|
|
5
|
+
*
|
|
6
|
+
* A discriminated union rather than one shape with optional fields, so each vote kind carries
|
|
7
|
+
* exactly its own payload: a `conflict` without its `conflictWith` (or a stray `rejectReason` on an
|
|
8
|
+
* `approve`) does not typecheck. Every variant's extra field is folded into the signed payload
|
|
9
|
+
* ({@link clusterVoteSigningPayload}), so none of them can be altered in transit.
|
|
10
|
+
*/
|
|
11
|
+
export type Signature =
|
|
12
|
+
| { type: 'approve'; signature: string }
|
|
13
|
+
| { type: 'reject'; signature: string; rejectReason?: string }
|
|
14
|
+
/**
|
|
15
|
+
* This member refuses the transaction *for now*: it holds a conflicting transaction that won
|
|
16
|
+
* the deterministic race (`resolveRace`). Retryable — NOT a validity judgement, and never
|
|
17
|
+
* counted toward the permanent-rejection threshold. `conflictWith` is the winning
|
|
18
|
+
* transaction's messageHash: structured, signed, and readable without parsing prose.
|
|
19
|
+
*/
|
|
20
|
+
| { type: 'conflict'; signature: string; conflictWith: string };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The exact bytes a vote signature covers: `<hash>:<type>[:<extra>]`, where `extra` is the variant's
|
|
24
|
+
* own payload — a reject's `rejectReason`, a conflict's `conflictWith`, nothing for an approve.
|
|
25
|
+
* Folding the extra in is what makes it integrity-protected in transit rather than free-floating
|
|
26
|
+
* prose.
|
|
27
|
+
*
|
|
28
|
+
* Producers and verifiers must both build the preimage here. It lives beside {@link Signature}
|
|
29
|
+
* rather than in either consumer because a second copy that forgets a variant does not fail loudly:
|
|
30
|
+
* it reports an honest vote as an invalid signature. (The dispute path once carried such a copy.)
|
|
31
|
+
*
|
|
32
|
+
* "Cluster" in the name distinguishes these consensus votes from the dispute subsystem's
|
|
33
|
+
* arbitration votes, which have their own unrelated preimage (`dispute/invalidation.ts`).
|
|
34
|
+
*/
|
|
35
|
+
export function clusterVoteSigningPayload(hash: string, type: Signature['type'], extra?: string): Uint8Array {
|
|
36
|
+
return new TextEncoder().encode(hash + ':' + type + (extra ? ':' + extra : ''));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Verifier-side {@link clusterVoteSigningPayload}: reads each variant's signed extra off the vote itself. */
|
|
40
|
+
export function clusterVoteVerificationPayload(hash: string, signature: Signature): Uint8Array {
|
|
41
|
+
switch (signature.type) {
|
|
42
|
+
case 'reject': return clusterVoteSigningPayload(hash, 'reject', signature.rejectReason);
|
|
43
|
+
case 'conflict': return clusterVoteSigningPayload(hash, 'conflict', signature.conflictWith);
|
|
44
|
+
default: return clusterVoteSigningPayload(hash, signature.type);
|
|
45
|
+
}
|
|
7
46
|
}
|
|
8
47
|
|
|
9
48
|
export type ClusterPeers = {
|
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cohort-topic substrate — tier addressing (`coord_d`).
|
|
3
|
-
*
|
|
4
|
-
* Transcribed from `docs/cohort-topic.md` §Tier addressing:
|
|
5
|
-
*
|
|
6
|
-
* ```
|
|
7
|
-
* coord_0(_, topicId) = H(0x00 ‖ topicId)
|
|
8
|
-
* coord_d(P, topicId) = H(d ‖ prefix(H(P), d·log₂F) ‖ topicId) for d ≥ 1
|
|
9
|
-
* ```
|
|
10
|
-
*
|
|
11
|
-
* `P` is ring-hashed before the prefix so the shard input is uniformly distributed across
|
|
12
|
-
* participants (the raw peer-id string bytes share a near-constant `12D3Koo…` prefix that would
|
|
13
|
-
* collapse all tier-`d` shards to one coordinate). The wire field `participantCoord` keeps the
|
|
14
|
-
* unmodified peer id so the Ed25519 key remains recoverable; the ring-hash is applied only inside
|
|
15
|
-
* the addressing math.
|
|
16
|
-
*
|
|
17
|
-
* `H` is the injected {@link IRingHash} (db-core's own SHA-256 truncated to the ring width — **not**
|
|
18
|
-
* a FRET import). `prefix(P, n)` is the `n` most-significant bits of peer id `P`, left-padded if
|
|
19
|
-
* shorter. `F` is the fan-out (default 16, `log₂F = 4`); tier `d` has exactly `F^d` coordinates.
|
|
20
|
-
*
|
|
21
|
-
* The db-p2p binding is responsible for ensuring the coord byte layout (ring width) matches FRET's
|
|
22
|
-
* `RING_BITS` so the routing keys produced here line up with FRET's ring on the wire.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import type { IRingHash, RingCoord } from "./ports.js";
|
|
26
|
-
|
|
27
|
-
/** Tier-addressing surface — derives the ring coordinate for tier `d` of a topic. */
|
|
28
|
-
export interface TierAddressing {
|
|
29
|
-
/** Fan-out per tier (default 16). */
|
|
30
|
-
readonly F: number;
|
|
31
|
-
/** Tier-0 root coordinate: `H(0x00 ‖ topicId)`. Peer-independent. */
|
|
32
|
-
coord0(topicId: Uint8Array): RingCoord;
|
|
33
|
-
/** Tier-`d` coordinate for `d ≥ 1`: `H(d ‖ prefix(H(P), d·log₂F) ‖ topicId)` where `H(P)` is the ring-hash of `peerId`. */
|
|
34
|
-
coordD(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord;
|
|
35
|
-
/** Dispatches `d === 0` to {@link coord0}, otherwise to {@link coordD}. */
|
|
36
|
-
coord(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Default fan-out per tier (`log₂16 = 4`). */
|
|
40
|
-
export const DEFAULT_FANOUT = 16;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* `prefix(P, n)` — the `n` most-significant bits of `P`, packed MSB-first into `⌈n/8⌉` bytes with
|
|
44
|
-
* the trailing partial byte's unused low bits zeroed. If `P` carries fewer than `n` bits it is
|
|
45
|
-
* **left-padded**: the high `(n − bits(P))` positions are zero and `P`'s bits occupy the low end, so
|
|
46
|
-
* the result is always exactly `n` bits wide. `n === 0` yields an empty array (the tier-0 case).
|
|
47
|
-
*
|
|
48
|
-
* Exported for tests; not part of the public substrate surface.
|
|
49
|
-
*/
|
|
50
|
-
export function prefixBits(peerId: Uint8Array, n: number): Uint8Array {
|
|
51
|
-
if (!Number.isInteger(n) || n < 0) {
|
|
52
|
-
throw new RangeError(`prefix bit count must be a non-negative integer, got ${n}`);
|
|
53
|
-
}
|
|
54
|
-
const outBytes = Math.ceil(n / 8);
|
|
55
|
-
const out = new Uint8Array(outBytes);
|
|
56
|
-
const available = peerId.length * 8;
|
|
57
|
-
const pad = Math.max(0, n - available);
|
|
58
|
-
for (let j = 0; j < n; j++) {
|
|
59
|
-
// Result bit j (MSB-first). High `pad` positions are zero (left-pad); the rest map to P.
|
|
60
|
-
if (j < pad) continue;
|
|
61
|
-
const srcBit = j - pad;
|
|
62
|
-
const srcByte = peerId[srcBit >>> 3]!;
|
|
63
|
-
const bit = (srcByte >>> (7 - (srcBit & 7))) & 1;
|
|
64
|
-
if (bit) out[j >>> 3]! |= 1 << (7 - (j & 7));
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Implements {@link TierAddressing} over an injected {@link IRingHash}. */
|
|
70
|
-
export class HashTierAddressing implements TierAddressing {
|
|
71
|
-
public readonly F: number;
|
|
72
|
-
private readonly log2F: number;
|
|
73
|
-
|
|
74
|
-
constructor(private readonly hash: IRingHash, F: number = DEFAULT_FANOUT) {
|
|
75
|
-
if (!Number.isInteger(F) || F < 2) {
|
|
76
|
-
throw new RangeError(`fan-out F must be an integer ≥ 2, got ${F}`);
|
|
77
|
-
}
|
|
78
|
-
const log2F = Math.log2(F);
|
|
79
|
-
if (!Number.isInteger(log2F)) {
|
|
80
|
-
throw new RangeError(`fan-out F must be a power of two, got ${F}`);
|
|
81
|
-
}
|
|
82
|
-
this.F = F;
|
|
83
|
-
this.log2F = log2F;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
coord0(topicId: Uint8Array): RingCoord {
|
|
87
|
-
// H(0x00 ‖ topicId)
|
|
88
|
-
const input = new Uint8Array(1 + topicId.length);
|
|
89
|
-
input[0] = 0x00;
|
|
90
|
-
input.set(topicId, 1);
|
|
91
|
-
return this.hash.H(input);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
coordD(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord {
|
|
95
|
-
if (!Number.isInteger(d) || d < 1) {
|
|
96
|
-
throw new RangeError(`coordD requires an integer tier d ≥ 1, got ${d}`);
|
|
97
|
-
}
|
|
98
|
-
if (d > 255) {
|
|
99
|
-
throw new RangeError(`tier d must fit in one byte (≤ 255), got ${d}`);
|
|
100
|
-
}
|
|
101
|
-
// H(d ‖ prefix(H(P), d·log₂F) ‖ topicId) — ring-hash P first so the shard input is uniform
|
|
102
|
-
// NOTE: re-hashes peerId on every coordD call; a walk over a tier ladder recomputes H(self) per
|
|
103
|
-
// tier. Negligible today (walk steps are network-bound); if coord becomes hot, cache H(peerId).
|
|
104
|
-
const prefix = prefixBits(this.hash.H(peerId), d * this.log2F);
|
|
105
|
-
const input = new Uint8Array(1 + prefix.length + topicId.length);
|
|
106
|
-
input[0] = d;
|
|
107
|
-
input.set(prefix, 1);
|
|
108
|
-
input.set(topicId, 1 + prefix.length);
|
|
109
|
-
return this.hash.H(input);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
coord(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord {
|
|
113
|
-
return d === 0 ? this.coord0(topicId) : this.coordD(d, peerId, topicId);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Convenience factory mirroring the db-p2p adapter construction style. */
|
|
118
|
-
export function createTierAddressing(hash: IRingHash, F: number = DEFAULT_FANOUT): TierAddressing {
|
|
119
|
-
return new HashTierAddressing(hash, F);
|
|
120
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — tier addressing (`coord_d`).
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Tier addressing:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* coord_0(_, topicId) = H(0x00 ‖ topicId)
|
|
8
|
+
* coord_d(P, topicId) = H(d ‖ prefix(H(P), d·log₂F) ‖ topicId) for d ≥ 1
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* `P` is ring-hashed before the prefix so the shard input is uniformly distributed across
|
|
12
|
+
* participants (the raw peer-id string bytes share a near-constant `12D3Koo…` prefix that would
|
|
13
|
+
* collapse all tier-`d` shards to one coordinate). The wire field `participantCoord` keeps the
|
|
14
|
+
* unmodified peer id so the Ed25519 key remains recoverable; the ring-hash is applied only inside
|
|
15
|
+
* the addressing math.
|
|
16
|
+
*
|
|
17
|
+
* `H` is the injected {@link IRingHash} (db-core's own SHA-256 truncated to the ring width — **not**
|
|
18
|
+
* a FRET import). `prefix(P, n)` is the `n` most-significant bits of peer id `P`, left-padded if
|
|
19
|
+
* shorter. `F` is the fan-out (default 16, `log₂F = 4`); tier `d` has exactly `F^d` coordinates.
|
|
20
|
+
*
|
|
21
|
+
* The db-p2p binding is responsible for ensuring the coord byte layout (ring width) matches FRET's
|
|
22
|
+
* `RING_BITS` so the routing keys produced here line up with FRET's ring on the wire.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { IRingHash, RingCoord } from "./ports.js";
|
|
26
|
+
|
|
27
|
+
/** Tier-addressing surface — derives the ring coordinate for tier `d` of a topic. */
|
|
28
|
+
export interface TierAddressing {
|
|
29
|
+
/** Fan-out per tier (default 16). */
|
|
30
|
+
readonly F: number;
|
|
31
|
+
/** Tier-0 root coordinate: `H(0x00 ‖ topicId)`. Peer-independent. */
|
|
32
|
+
coord0(topicId: Uint8Array): RingCoord;
|
|
33
|
+
/** Tier-`d` coordinate for `d ≥ 1`: `H(d ‖ prefix(H(P), d·log₂F) ‖ topicId)` where `H(P)` is the ring-hash of `peerId`. */
|
|
34
|
+
coordD(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord;
|
|
35
|
+
/** Dispatches `d === 0` to {@link coord0}, otherwise to {@link coordD}. */
|
|
36
|
+
coord(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Default fan-out per tier (`log₂16 = 4`). */
|
|
40
|
+
export const DEFAULT_FANOUT = 16;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `prefix(P, n)` — the `n` most-significant bits of `P`, packed MSB-first into `⌈n/8⌉` bytes with
|
|
44
|
+
* the trailing partial byte's unused low bits zeroed. If `P` carries fewer than `n` bits it is
|
|
45
|
+
* **left-padded**: the high `(n − bits(P))` positions are zero and `P`'s bits occupy the low end, so
|
|
46
|
+
* the result is always exactly `n` bits wide. `n === 0` yields an empty array (the tier-0 case).
|
|
47
|
+
*
|
|
48
|
+
* Exported for tests; not part of the public substrate surface.
|
|
49
|
+
*/
|
|
50
|
+
export function prefixBits(peerId: Uint8Array, n: number): Uint8Array {
|
|
51
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
52
|
+
throw new RangeError(`prefix bit count must be a non-negative integer, got ${n}`);
|
|
53
|
+
}
|
|
54
|
+
const outBytes = Math.ceil(n / 8);
|
|
55
|
+
const out = new Uint8Array(outBytes);
|
|
56
|
+
const available = peerId.length * 8;
|
|
57
|
+
const pad = Math.max(0, n - available);
|
|
58
|
+
for (let j = 0; j < n; j++) {
|
|
59
|
+
// Result bit j (MSB-first). High `pad` positions are zero (left-pad); the rest map to P.
|
|
60
|
+
if (j < pad) continue;
|
|
61
|
+
const srcBit = j - pad;
|
|
62
|
+
const srcByte = peerId[srcBit >>> 3]!;
|
|
63
|
+
const bit = (srcByte >>> (7 - (srcBit & 7))) & 1;
|
|
64
|
+
if (bit) out[j >>> 3]! |= 1 << (7 - (j & 7));
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Implements {@link TierAddressing} over an injected {@link IRingHash}. */
|
|
70
|
+
export class HashTierAddressing implements TierAddressing {
|
|
71
|
+
public readonly F: number;
|
|
72
|
+
private readonly log2F: number;
|
|
73
|
+
|
|
74
|
+
constructor(private readonly hash: IRingHash, F: number = DEFAULT_FANOUT) {
|
|
75
|
+
if (!Number.isInteger(F) || F < 2) {
|
|
76
|
+
throw new RangeError(`fan-out F must be an integer ≥ 2, got ${F}`);
|
|
77
|
+
}
|
|
78
|
+
const log2F = Math.log2(F);
|
|
79
|
+
if (!Number.isInteger(log2F)) {
|
|
80
|
+
throw new RangeError(`fan-out F must be a power of two, got ${F}`);
|
|
81
|
+
}
|
|
82
|
+
this.F = F;
|
|
83
|
+
this.log2F = log2F;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
coord0(topicId: Uint8Array): RingCoord {
|
|
87
|
+
// H(0x00 ‖ topicId)
|
|
88
|
+
const input = new Uint8Array(1 + topicId.length);
|
|
89
|
+
input[0] = 0x00;
|
|
90
|
+
input.set(topicId, 1);
|
|
91
|
+
return this.hash.H(input);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
coordD(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord {
|
|
95
|
+
if (!Number.isInteger(d) || d < 1) {
|
|
96
|
+
throw new RangeError(`coordD requires an integer tier d ≥ 1, got ${d}`);
|
|
97
|
+
}
|
|
98
|
+
if (d > 255) {
|
|
99
|
+
throw new RangeError(`tier d must fit in one byte (≤ 255), got ${d}`);
|
|
100
|
+
}
|
|
101
|
+
// H(d ‖ prefix(H(P), d·log₂F) ‖ topicId) — ring-hash P first so the shard input is uniform
|
|
102
|
+
// NOTE: re-hashes peerId on every coordD call; a walk over a tier ladder recomputes H(self) per
|
|
103
|
+
// tier. Negligible today (walk steps are network-bound); if coord becomes hot, cache H(peerId).
|
|
104
|
+
const prefix = prefixBits(this.hash.H(peerId), d * this.log2F);
|
|
105
|
+
const input = new Uint8Array(1 + prefix.length + topicId.length);
|
|
106
|
+
input[0] = d;
|
|
107
|
+
input.set(prefix, 1);
|
|
108
|
+
input.set(topicId, 1 + prefix.length);
|
|
109
|
+
return this.hash.H(input);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
coord(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord {
|
|
113
|
+
return d === 0 ? this.coord0(topicId) : this.coordD(d, peerId, topicId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Convenience factory mirroring the db-p2p adapter construction style. */
|
|
118
|
+
export function createTierAddressing(hash: IRingHash, F: number = DEFAULT_FANOUT): TierAddressing {
|
|
119
|
+
return new HashTierAddressing(hash, F);
|
|
120
|
+
}
|