@optimystic/db-p2p 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/dist/src/cluster/client.d.ts +10 -0
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +30 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +13 -2
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +51 -4
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +42 -17
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +229 -122
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/cluster-size-coupling.d.ts +28 -0
- package/dist/src/cluster/cluster-size-coupling.d.ts.map +1 -0
- package/dist/src/cluster/cluster-size-coupling.js +35 -0
- package/dist/src/cluster/cluster-size-coupling.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +6 -0
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +1 -1
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +15 -3
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +32 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +43 -2
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +22 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +56 -10
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +9 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +134 -7
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +174 -37
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +3 -2
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +859 -778
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts +17 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -2
- package/dist/src/logger.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +2 -0
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +4 -0
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/optimystic-node.d.ts +35 -0
- package/dist/src/optimystic-node.d.ts.map +1 -0
- package/dist/src/optimystic-node.js +2 -0
- package/dist/src/optimystic-node.js.map +1 -0
- package/dist/src/owned-block-seed.d.ts +6 -3
- package/dist/src/owned-block-seed.d.ts.map +1 -1
- package/dist/src/owned-block-seed.js +16 -3
- package/dist/src/owned-block-seed.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +72 -0
- package/dist/src/peer-address-book.d.ts.map +1 -0
- package/dist/src/peer-address-book.js +123 -0
- package/dist/src/peer-address-book.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +11 -2
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +30 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +95 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +78 -14
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +266 -81
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +57 -5
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +83 -0
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/cached-raw-storage.js +152 -0
- package/dist/src/storage/cached-raw-storage.js.map +1 -0
- package/dist/src/storage/cached-store-driver.d.ts +186 -0
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
- package/dist/src/storage/cached-store-driver.js +775 -0
- package/dist/src/storage/cached-store-driver.js.map +1 -0
- package/dist/src/storage/i-block-storage.d.ts +20 -1
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +12 -5
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +234 -0
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
- package/dist/src/storage/shared-cache-pool.js +354 -0
- package/dist/src/storage/shared-cache-pool.js.map +1 -0
- package/dist/src/storage/storage-repo.d.ts +56 -3
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +124 -18
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +52 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -653
- package/src/cluster/block-transfer.ts +424 -424
- package/src/cluster/client.ts +119 -88
- package/src/cluster/cluster-error.ts +64 -64
- package/src/cluster/cluster-policy.ts +203 -152
- package/src/cluster/cluster-repo.ts +245 -125
- package/src/cluster/cluster-size-coupling.ts +45 -0
- package/src/cluster/commit-cert.ts +139 -139
- package/src/cluster/i-transaction-state-store.ts +43 -43
- package/src/cluster/memory-transaction-state-store.ts +56 -56
- package/src/cluster/peer-key-binding.ts +37 -37
- package/src/cluster/persistent-transaction-state-store.ts +92 -92
- package/src/cluster/quorum-restore.ts +223 -223
- package/src/cluster/reconcile-block.ts +203 -191
- package/src/cluster/service.ts +293 -241
- package/src/cluster/supermajority-coupling.ts +37 -37
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
- package/src/cohort-topic/change-bridge.ts +109 -109
- package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
- package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
- package/src/cohort-topic/fret-trust-anchor.ts +153 -153
- package/src/cohort-topic/host.ts +2901 -2901
- package/src/cohort-topic/index.ts +13 -13
- package/src/cohort-topic/membership-publish-sink.ts +20 -20
- package/src/cohort-topic/membership-source.ts +68 -68
- package/src/cohort-topic/peer-codec.ts +31 -31
- package/src/cohort-topic/peer-sig.ts +86 -86
- package/src/cohort-topic/protocols.ts +71 -71
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
- package/src/cohort-topic/size-estimator.ts +16 -16
- package/src/cohort-topic/stream-util.ts +135 -87
- package/src/cohort-topic/threshold-crypto.ts +239 -239
- package/src/cohort-topic/topic-router.ts +77 -77
- package/src/dispute/arbitrator-selection.ts +138 -138
- package/src/dispute/cascade.ts +524 -524
- package/src/dispute/dispute-service.ts +11 -5
- package/src/dispute/invalidation.ts +625 -625
- package/src/inbound-authorization.ts +190 -190
- package/src/index.ts +52 -47
- package/src/libp2p-key-network.ts +1120 -958
- package/src/libp2p-node-base.ts +1675 -1591
- package/src/libp2p-node-rn.ts +30 -30
- package/src/libp2p-node.ts +36 -36
- package/src/logger.ts +19 -2
- package/src/matchmaking/aggregate-counts.ts +104 -104
- package/src/matchmaking/index.ts +20 -20
- package/src/matchmaking/module.ts +363 -363
- package/src/matchmaking/protocols.ts +51 -51
- package/src/matchmaking/provider-manager.ts +95 -95
- package/src/matchmaking/query-handler.ts +88 -88
- package/src/matchmaking/query-transport.ts +492 -492
- package/src/matchmaking/seeker-manager.ts +64 -64
- package/src/matchmaking/seeker-walk-client.ts +293 -293
- package/src/matchmaking/traffic-validation.ts +195 -195
- package/src/network/network-manager-service.ts +5 -0
- package/src/optimystic-node.ts +36 -0
- package/src/owned-block-seed.ts +53 -40
- package/src/peer-address-book.ts +149 -0
- package/src/protocol-limits.ts +33 -33
- package/src/reactivity/forwarder-host.ts +438 -438
- package/src/reactivity/index.ts +19 -19
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/origination-manager.ts +192 -192
- package/src/reactivity/protocols.ts +61 -61
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +408 -408
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
- package/src/reactivity/subscriber-registry.ts +96 -96
- package/src/reactivity/subscription-manager.ts +450 -450
- package/src/reactivity/topic-bytes.ts +37 -37
- package/src/repo/client.ts +12 -2
- package/src/repo/cluster-coordinator.ts +99 -3
- package/src/repo/coordinator-repo.ts +305 -82
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -34
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/block-storage.ts +59 -6
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- package/src/storage/i-block-storage.ts +20 -1
- package/src/storage/i-kv-store.ts +8 -8
- package/src/storage/i-raw-storage.ts +12 -5
- package/src/storage/kv-raw-storage.ts +135 -135
- package/src/storage/memory-kv-store.ts +28 -28
- package/src/storage/memory-storage.ts +25 -25
- package/src/storage/memory-store-driver.ts +157 -157
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +80 -80
- package/src/storage/ring-selector.ts +317 -317
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/shared-cache-pool.ts +452 -0
- package/src/storage/storage-repo.ts +1014 -903
- package/src/testing/cohort-topic-mesh-harness.ts +663 -663
- package/src/testing/index.ts +8 -8
- package/src/testing/matchmaking-mesh-harness.ts +475 -475
- package/src/testing/raw-storage-conformance.ts +453 -397
- package/src/testing/reactivity-mesh-harness.ts +922 -922
- package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
- package/dist/src/storage/restoration-coordinator-v2.js +0 -172
- package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verifiable dispersed arbitrator sampling.
|
|
3
|
-
*
|
|
4
|
-
* The old selection walked the ring positions *immediately adjacent* to the disputed block (sort every
|
|
5
|
-
* peer by XOR distance to `hash(blockId)`, skip the original cluster, take the next K). That recruits
|
|
6
|
-
* from exactly the neighborhood an attacker already had to own to capture the block's cluster — so
|
|
7
|
-
* "independent" arbitration drew from the *least* independent population (see `docs/correctness.md` §7.1
|
|
8
|
-
* Sybil, Theorems 8 & 10).
|
|
9
|
-
*
|
|
10
|
-
* Instead we derive `count` pseudo-random ring coordinates from `hash(blockId ‖ round ‖ epoch ‖ i)` and
|
|
11
|
-
* pick the peer nearest each coordinate. SHA-256 output is uniform over the ring, so the coordinates land
|
|
12
|
-
* spread across the whole keyspace and the sampled arbitrators are drawn from the whole population, not
|
|
13
|
-
* the block's neighborhood. To capture them an attacker needs IDs near many independent random points —
|
|
14
|
-
* a fraction of the *entire* network, not one locale.
|
|
15
|
-
*
|
|
16
|
-
* Two properties both hold:
|
|
17
|
-
* - **Deterministic & independently verifiable** — every honest node, given the same
|
|
18
|
-
* `(blockId, round, epoch)` and the same agreed membership, computes the identical set. This is what
|
|
19
|
-
* lets the dispute verify path re-derive the eligible set instead of trusting a declared one.
|
|
20
|
-
* - **Unpredictable / not pre-positionable** — the coordinates for round r are pinned only once
|
|
21
|
-
* `(blockId, round, epoch)` are all fixed. `round` advances in real time during the dispute; `epoch`
|
|
22
|
-
* is the agreed membership epoch, which rotates with membership and cannot be freely advanced by the
|
|
23
|
-
* attacker. So the attacker cannot know far enough ahead which coordinates to migrate IDs toward.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Resolve the peer-id strings nearest a ring coordinate, in ascending distance order.
|
|
28
|
-
* Production: FRET `assembleCohort(coord, wants)`. Tests: sort a fixed `KnownPeer[]` by XOR distance.
|
|
29
|
-
* May return fewer than `wants` — that signals the whole eligible membership fit in the slice.
|
|
30
|
-
*/
|
|
31
|
-
export type NearestResolver = (coord: Uint8Array, wants: number) => string[] | Promise<string[]>;
|
|
32
|
-
|
|
33
|
-
/** FRET-compatible ring hash of arbitrary bytes → coordinate (SHA-256; see db-core `RingHash.H` / FRET `hashKey`). */
|
|
34
|
-
export type RingHashFn = (bytes: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
35
|
-
|
|
36
|
-
export interface ArbitratorSamplingParams {
|
|
37
|
-
/** Disputed block id bytes (messageHash fallback), as bound into the dispute. */
|
|
38
|
-
readonly blockId: Uint8Array;
|
|
39
|
-
/** Escalation round, 0-based. Round 0 is the first arbitration. */
|
|
40
|
-
readonly round: number;
|
|
41
|
-
/**
|
|
42
|
-
* Agreed membership epoch bytes. Pins the draw to an epoch the attacker cannot freely advance.
|
|
43
|
-
* Interim source (until `design-cluster-membership-agreement` lands): hash of the agreed responsible
|
|
44
|
-
* set the admission gate already converges on (`cluster-membership-admission-gate`).
|
|
45
|
-
*/
|
|
46
|
-
readonly epoch: Uint8Array;
|
|
47
|
-
/** Number of fresh, distinct arbitrators to draw this round. */
|
|
48
|
-
readonly count: number;
|
|
49
|
-
/** Peer-id strings to exclude: original cluster + self + arbitrators already drawn in prior rounds. */
|
|
50
|
-
readonly exclude: ReadonlySet<string>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Little-endian u32 encoding of `n` — the canonical wire encoding for `round` and the coordinate index. */
|
|
54
|
-
function u32le(n: number): Uint8Array {
|
|
55
|
-
const out = new Uint8Array(4);
|
|
56
|
-
new DataView(out.buffer).setUint32(0, n >>> 0, true);
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Concatenate byte spans left-to-right. */
|
|
61
|
-
function concatBytes(parts: Uint8Array[]): Uint8Array {
|
|
62
|
-
let total = 0;
|
|
63
|
-
for (const p of parts) total += p.length;
|
|
64
|
-
const out = new Uint8Array(total);
|
|
65
|
-
let off = 0;
|
|
66
|
-
for (const p of parts) { out.set(p, off); off += p.length; }
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The exact preimage the i-th coordinate of a round hashes: `blockId ‖ u32le(round) ‖ epoch ‖ u32le(i)`.
|
|
72
|
-
* Folding `round`, `epoch`, and `i` in gives: (a) each round samples a distinct population (round changes
|
|
73
|
-
* every coordinate); (b) each of the `count` coordinates is an independent uniform draw (dispersion);
|
|
74
|
-
* (c) a replacement for an offline/duplicate pick is the *next* peer in the same coordinate's ordering,
|
|
75
|
-
* never a fresh challenger-chosen peer. The canonical little-endian encoding is asserted by a golden
|
|
76
|
-
* vector so two implementations hash identical bytes.
|
|
77
|
-
*/
|
|
78
|
-
export function coordinatePreimage(blockId: Uint8Array, round: number, epoch: Uint8Array, i: number): Uint8Array {
|
|
79
|
-
return concatBytes([blockId, u32le(round), epoch, u32le(i)]);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Deterministic dispersed arbitrator draw. Returns up to `count` distinct peer-id strings; fewer only
|
|
84
|
-
* when the network is too small to yield that many (small-network fallback) — never duplicates, never
|
|
85
|
-
* loops. In the degenerate all-peers-in-cluster case, returns `[]`.
|
|
86
|
-
*
|
|
87
|
-
* For each coordinate `i` the nearest unseen peer is chosen. When a coordinate's nearest slice is
|
|
88
|
-
* entirely `seen` (excluded, or already picked for an earlier coordinate) the slice is widened
|
|
89
|
-
* (`wants` grows) and retried; if widening exposes the whole eligible membership with nobody fresh, the
|
|
90
|
-
* entire membership is exhausted and the (short) picks are returned. Replacement of an offline/duplicate
|
|
91
|
-
* pick is thus the deterministic next peer in the same coordinate's ordering, identical on every honest
|
|
92
|
-
* node, so disputing parties cannot steer it.
|
|
93
|
-
*/
|
|
94
|
-
export async function sampleArbitrators(
|
|
95
|
-
params: ArbitratorSamplingParams,
|
|
96
|
-
nearest: NearestResolver,
|
|
97
|
-
hash: RingHashFn,
|
|
98
|
-
): Promise<string[]> {
|
|
99
|
-
const { blockId, round, epoch, count, exclude } = params;
|
|
100
|
-
const picks: string[] = [];
|
|
101
|
-
if (count <= 0) return picks;
|
|
102
|
-
|
|
103
|
-
const seen = new Set<string>(exclude);
|
|
104
|
-
|
|
105
|
-
for (let i = 0; picks.length < count; i++) {
|
|
106
|
-
const coord = await hash(coordinatePreimage(blockId, round, epoch, i));
|
|
107
|
-
|
|
108
|
-
// Walk this coordinate's ascending-distance ordering for the first peer we have not yet seen,
|
|
109
|
-
// widening the slice until we find one or have proven the whole eligible membership is exhausted.
|
|
110
|
-
// NOTE: `wants` starts at `seen.size + 1` (conservative — guarantees exhaustion is provable in one
|
|
111
|
-
// widen). Starting at 1 and widening only on a seen-collision is also correct and asks the resolver
|
|
112
|
-
// for far fewer peers per coordinate; if `assembleCohort` ever shows up as hot here, start smaller.
|
|
113
|
-
let wants = seen.size + 1;
|
|
114
|
-
let prevLen = -1;
|
|
115
|
-
let picked: string | undefined;
|
|
116
|
-
let membershipExhausted = false;
|
|
117
|
-
for (;;) {
|
|
118
|
-
const cands = await nearest(coord, wants);
|
|
119
|
-
const fresh = cands.find(c => !seen.has(c));
|
|
120
|
-
if (fresh !== undefined) { picked = fresh; break; }
|
|
121
|
-
// No fresh peer in this slice. If the resolver returned fewer than we asked (or the slice
|
|
122
|
-
// stopped growing), we have seen the whole eligible membership from this coordinate — and
|
|
123
|
-
// since every one of them is already `seen`, no future coordinate can yield anything new.
|
|
124
|
-
if (cands.length < wants || cands.length <= prevLen) { membershipExhausted = true; break; }
|
|
125
|
-
prevLen = cands.length;
|
|
126
|
-
wants *= 2;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (picked !== undefined) {
|
|
130
|
-
picks.push(picked);
|
|
131
|
-
seen.add(picked);
|
|
132
|
-
} else if (membershipExhausted) {
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return picks;
|
|
138
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Verifiable dispersed arbitrator sampling.
|
|
3
|
+
*
|
|
4
|
+
* The old selection walked the ring positions *immediately adjacent* to the disputed block (sort every
|
|
5
|
+
* peer by XOR distance to `hash(blockId)`, skip the original cluster, take the next K). That recruits
|
|
6
|
+
* from exactly the neighborhood an attacker already had to own to capture the block's cluster — so
|
|
7
|
+
* "independent" arbitration drew from the *least* independent population (see `docs/correctness.md` §7.1
|
|
8
|
+
* Sybil, Theorems 8 & 10).
|
|
9
|
+
*
|
|
10
|
+
* Instead we derive `count` pseudo-random ring coordinates from `hash(blockId ‖ round ‖ epoch ‖ i)` and
|
|
11
|
+
* pick the peer nearest each coordinate. SHA-256 output is uniform over the ring, so the coordinates land
|
|
12
|
+
* spread across the whole keyspace and the sampled arbitrators are drawn from the whole population, not
|
|
13
|
+
* the block's neighborhood. To capture them an attacker needs IDs near many independent random points —
|
|
14
|
+
* a fraction of the *entire* network, not one locale.
|
|
15
|
+
*
|
|
16
|
+
* Two properties both hold:
|
|
17
|
+
* - **Deterministic & independently verifiable** — every honest node, given the same
|
|
18
|
+
* `(blockId, round, epoch)` and the same agreed membership, computes the identical set. This is what
|
|
19
|
+
* lets the dispute verify path re-derive the eligible set instead of trusting a declared one.
|
|
20
|
+
* - **Unpredictable / not pre-positionable** — the coordinates for round r are pinned only once
|
|
21
|
+
* `(blockId, round, epoch)` are all fixed. `round` advances in real time during the dispute; `epoch`
|
|
22
|
+
* is the agreed membership epoch, which rotates with membership and cannot be freely advanced by the
|
|
23
|
+
* attacker. So the attacker cannot know far enough ahead which coordinates to migrate IDs toward.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the peer-id strings nearest a ring coordinate, in ascending distance order.
|
|
28
|
+
* Production: FRET `assembleCohort(coord, wants)`. Tests: sort a fixed `KnownPeer[]` by XOR distance.
|
|
29
|
+
* May return fewer than `wants` — that signals the whole eligible membership fit in the slice.
|
|
30
|
+
*/
|
|
31
|
+
export type NearestResolver = (coord: Uint8Array, wants: number) => string[] | Promise<string[]>;
|
|
32
|
+
|
|
33
|
+
/** FRET-compatible ring hash of arbitrary bytes → coordinate (SHA-256; see db-core `RingHash.H` / FRET `hashKey`). */
|
|
34
|
+
export type RingHashFn = (bytes: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
35
|
+
|
|
36
|
+
export interface ArbitratorSamplingParams {
|
|
37
|
+
/** Disputed block id bytes (messageHash fallback), as bound into the dispute. */
|
|
38
|
+
readonly blockId: Uint8Array;
|
|
39
|
+
/** Escalation round, 0-based. Round 0 is the first arbitration. */
|
|
40
|
+
readonly round: number;
|
|
41
|
+
/**
|
|
42
|
+
* Agreed membership epoch bytes. Pins the draw to an epoch the attacker cannot freely advance.
|
|
43
|
+
* Interim source (until `design-cluster-membership-agreement` lands): hash of the agreed responsible
|
|
44
|
+
* set the admission gate already converges on (`cluster-membership-admission-gate`).
|
|
45
|
+
*/
|
|
46
|
+
readonly epoch: Uint8Array;
|
|
47
|
+
/** Number of fresh, distinct arbitrators to draw this round. */
|
|
48
|
+
readonly count: number;
|
|
49
|
+
/** Peer-id strings to exclude: original cluster + self + arbitrators already drawn in prior rounds. */
|
|
50
|
+
readonly exclude: ReadonlySet<string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Little-endian u32 encoding of `n` — the canonical wire encoding for `round` and the coordinate index. */
|
|
54
|
+
function u32le(n: number): Uint8Array {
|
|
55
|
+
const out = new Uint8Array(4);
|
|
56
|
+
new DataView(out.buffer).setUint32(0, n >>> 0, true);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Concatenate byte spans left-to-right. */
|
|
61
|
+
function concatBytes(parts: Uint8Array[]): Uint8Array {
|
|
62
|
+
let total = 0;
|
|
63
|
+
for (const p of parts) total += p.length;
|
|
64
|
+
const out = new Uint8Array(total);
|
|
65
|
+
let off = 0;
|
|
66
|
+
for (const p of parts) { out.set(p, off); off += p.length; }
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The exact preimage the i-th coordinate of a round hashes: `blockId ‖ u32le(round) ‖ epoch ‖ u32le(i)`.
|
|
72
|
+
* Folding `round`, `epoch`, and `i` in gives: (a) each round samples a distinct population (round changes
|
|
73
|
+
* every coordinate); (b) each of the `count` coordinates is an independent uniform draw (dispersion);
|
|
74
|
+
* (c) a replacement for an offline/duplicate pick is the *next* peer in the same coordinate's ordering,
|
|
75
|
+
* never a fresh challenger-chosen peer. The canonical little-endian encoding is asserted by a golden
|
|
76
|
+
* vector so two implementations hash identical bytes.
|
|
77
|
+
*/
|
|
78
|
+
export function coordinatePreimage(blockId: Uint8Array, round: number, epoch: Uint8Array, i: number): Uint8Array {
|
|
79
|
+
return concatBytes([blockId, u32le(round), epoch, u32le(i)]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Deterministic dispersed arbitrator draw. Returns up to `count` distinct peer-id strings; fewer only
|
|
84
|
+
* when the network is too small to yield that many (small-network fallback) — never duplicates, never
|
|
85
|
+
* loops. In the degenerate all-peers-in-cluster case, returns `[]`.
|
|
86
|
+
*
|
|
87
|
+
* For each coordinate `i` the nearest unseen peer is chosen. When a coordinate's nearest slice is
|
|
88
|
+
* entirely `seen` (excluded, or already picked for an earlier coordinate) the slice is widened
|
|
89
|
+
* (`wants` grows) and retried; if widening exposes the whole eligible membership with nobody fresh, the
|
|
90
|
+
* entire membership is exhausted and the (short) picks are returned. Replacement of an offline/duplicate
|
|
91
|
+
* pick is thus the deterministic next peer in the same coordinate's ordering, identical on every honest
|
|
92
|
+
* node, so disputing parties cannot steer it.
|
|
93
|
+
*/
|
|
94
|
+
export async function sampleArbitrators(
|
|
95
|
+
params: ArbitratorSamplingParams,
|
|
96
|
+
nearest: NearestResolver,
|
|
97
|
+
hash: RingHashFn,
|
|
98
|
+
): Promise<string[]> {
|
|
99
|
+
const { blockId, round, epoch, count, exclude } = params;
|
|
100
|
+
const picks: string[] = [];
|
|
101
|
+
if (count <= 0) return picks;
|
|
102
|
+
|
|
103
|
+
const seen = new Set<string>(exclude);
|
|
104
|
+
|
|
105
|
+
for (let i = 0; picks.length < count; i++) {
|
|
106
|
+
const coord = await hash(coordinatePreimage(blockId, round, epoch, i));
|
|
107
|
+
|
|
108
|
+
// Walk this coordinate's ascending-distance ordering for the first peer we have not yet seen,
|
|
109
|
+
// widening the slice until we find one or have proven the whole eligible membership is exhausted.
|
|
110
|
+
// NOTE: `wants` starts at `seen.size + 1` (conservative — guarantees exhaustion is provable in one
|
|
111
|
+
// widen). Starting at 1 and widening only on a seen-collision is also correct and asks the resolver
|
|
112
|
+
// for far fewer peers per coordinate; if `assembleCohort` ever shows up as hot here, start smaller.
|
|
113
|
+
let wants = seen.size + 1;
|
|
114
|
+
let prevLen = -1;
|
|
115
|
+
let picked: string | undefined;
|
|
116
|
+
let membershipExhausted = false;
|
|
117
|
+
for (;;) {
|
|
118
|
+
const cands = await nearest(coord, wants);
|
|
119
|
+
const fresh = cands.find(c => !seen.has(c));
|
|
120
|
+
if (fresh !== undefined) { picked = fresh; break; }
|
|
121
|
+
// No fresh peer in this slice. If the resolver returned fewer than we asked (or the slice
|
|
122
|
+
// stopped growing), we have seen the whole eligible membership from this coordinate — and
|
|
123
|
+
// since every one of them is already `seen`, no future coordinate can yield anything new.
|
|
124
|
+
if (cands.length < wants || cands.length <= prevLen) { membershipExhausted = true; break; }
|
|
125
|
+
prevLen = cands.length;
|
|
126
|
+
wants *= 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (picked !== undefined) {
|
|
130
|
+
picks.push(picked);
|
|
131
|
+
seen.add(picked);
|
|
132
|
+
} else if (membershipExhausted) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return picks;
|
|
138
|
+
}
|