@optimystic/db-p2p 0.14.1 → 0.16.2
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 +616 -609
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +4 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +52 -3
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +129 -6
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +3 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +182 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +429 -69
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/peer-key-binding.d.ts +25 -0
- package/dist/src/cluster/peer-key-binding.d.ts.map +1 -0
- package/dist/src/cluster/peer-key-binding.js +39 -0
- package/dist/src/cluster/peer-key-binding.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +72 -0
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -0
- package/dist/src/cluster/quorum-restore.js +0 -0
- package/dist/src/cluster/quorum-restore.js.map +1 -0
- package/dist/src/cluster/rebalance-monitor.d.ts +23 -1
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +17 -2
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +7 -1
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +7 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +4 -1
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cluster/supermajority-coupling.d.ts +22 -0
- package/dist/src/cluster/supermajority-coupling.d.ts.map +1 -0
- package/dist/src/cluster/supermajority-coupling.js +27 -0
- package/dist/src/cluster/supermajority-coupling.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +46 -4
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-driver.js +62 -7
- package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +7 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-transport.js +9 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts +197 -27
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +643 -87
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/dispute/arbitrator-selection.d.ts +68 -7
- package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -1
- package/dist/src/dispute/arbitrator-selection.js +105 -16
- package/dist/src/dispute/arbitrator-selection.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -0
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +1 -1
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +3 -2
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +32 -3
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +82 -9
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/index.d.ts +1 -1
- package/dist/src/dispute/index.d.ts.map +1 -1
- package/dist/src/dispute/index.js +1 -1
- package/dist/src/dispute/index.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +25 -0
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +14 -2
- package/dist/src/dispute/invalidation.js.map +1 -1
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +7 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/it-utility.d.ts +1 -1
- package/dist/src/it-utility.d.ts.map +1 -1
- package/dist/src/it-utility.js +4 -13
- package/dist/src/it-utility.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +98 -1
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +272 -17
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +12 -0
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +415 -87
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +2 -0
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +6 -0
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/matchmaking/traffic-validation.js +1 -0
- package/dist/src/matchmaking/traffic-validation.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +2 -2
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +8 -4
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/owned-block-seed.d.ts +27 -0
- package/dist/src/owned-block-seed.d.ts.map +1 -0
- package/dist/src/owned-block-seed.js +37 -0
- package/dist/src/owned-block-seed.js.map +1 -0
- package/dist/src/protocol-client.d.ts +1 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +9 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/protocol-limits.d.ts +32 -0
- package/dist/src/protocol-limits.d.ts.map +1 -0
- package/dist/src/protocol-limits.js +32 -0
- package/dist/src/protocol-limits.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +3 -0
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +41 -4
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +128 -80
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +38 -1
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +92 -13
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +7 -1
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/peer-reputation.js +1 -1
- package/dist/src/reputation/peer-reputation.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -0
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +5 -1
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +7 -1
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.d.ts +30 -1
- package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.js +15 -2
- package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
- package/dist/src/storage/arachnode-partition.d.ts +49 -0
- package/dist/src/storage/arachnode-partition.d.ts.map +1 -0
- package/dist/src/storage/arachnode-partition.js +61 -0
- package/dist/src/storage/arachnode-partition.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +40 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +150 -57
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +8 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +13 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +45 -0
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/kv-raw-storage.js +113 -0
- package/dist/src/storage/kv-raw-storage.js.map +1 -0
- package/dist/src/storage/memory-storage.d.ts +20 -58
- package/dist/src/storage/memory-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.js +22 -148
- package/dist/src/storage/memory-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +39 -0
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -0
- package/dist/src/storage/memory-store-driver.js +129 -0
- package/dist/src/storage/memory-store-driver.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +10 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -0
- package/dist/src/storage/raw-store-codec.js +36 -0
- package/dist/src/storage/raw-store-codec.js.map +1 -0
- package/dist/src/storage/raw-store-driver.d.ts +69 -0
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -0
- package/dist/src/storage/raw-store-driver.js +2 -0
- package/dist/src/storage/raw-store-driver.js.map +1 -0
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator-v2.js +1 -1
- package/dist/src/storage/restoration-coordinator-v2.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts +73 -0
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -0
- package/dist/src/storage/restoration-coordinator.js +181 -0
- package/dist/src/storage/restoration-coordinator.js.map +1 -0
- package/dist/src/storage/ring-selector.d.ts +92 -5
- package/dist/src/storage/ring-selector.d.ts.map +1 -1
- package/dist/src/storage/ring-selector.js +153 -51
- package/dist/src/storage/ring-selector.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts +128 -0
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -0
- package/dist/src/storage/ring-shift-coordinator.js +196 -0
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -0
- package/dist/src/storage/storage-monitor.d.ts +28 -0
- package/dist/src/storage/storage-monitor.d.ts.map +1 -1
- package/dist/src/storage/storage-monitor.js +42 -2
- package/dist/src/storage/storage-monitor.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +19 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +172 -22
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/client.d.ts.map +1 -1
- package/dist/src/sync/client.js +5 -1
- package/dist/src/sync/client.js.map +1 -1
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +2 -1
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +28 -4
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +51 -10
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/dist/src/testing/index.d.ts +1 -0
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +15 -7
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +24 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -0
- package/dist/src/testing/raw-storage-conformance.js +313 -0
- package/dist/src/testing/raw-storage-conformance.js.map +1 -0
- package/package.json +4 -4
- package/src/cluster/block-transfer-service.ts +4 -3
- package/src/cluster/block-transfer.ts +424 -265
- package/src/cluster/client.ts +3 -1
- package/src/cluster/cluster-repo.ts +481 -71
- package/src/cluster/peer-key-binding.ts +37 -0
- package/src/cluster/quorum-restore.ts +0 -0
- package/src/cluster/rebalance-monitor.ts +34 -3
- package/src/cluster/service.ts +7 -1
- package/src/cluster/spread-on-churn.ts +11 -1
- package/src/cluster/supermajority-coupling.ts +37 -0
- package/src/cohort-topic/cohort-gossip-driver.ts +96 -9
- package/src/cohort-topic/cohort-gossip-transport.ts +10 -0
- package/src/cohort-topic/host.ts +831 -105
- package/src/dispute/arbitrator-selection.ts +138 -28
- package/src/dispute/cascade.ts +8 -1
- package/src/dispute/client.ts +3 -2
- package/src/dispute/dispute-service.ts +94 -9
- package/src/dispute/index.ts +7 -1
- package/src/dispute/invalidation.ts +40 -2
- package/src/dispute/service.ts +7 -1
- package/src/index.ts +46 -43
- package/src/it-utility.ts +5 -13
- package/src/libp2p-key-network.ts +281 -18
- package/src/libp2p-node-base.ts +485 -79
- package/src/matchmaking/query-transport.ts +9 -0
- package/src/network/network-manager-service.ts +8 -2
- package/src/owned-block-seed.ts +40 -0
- package/src/protocol-client.ts +10 -3
- package/src/protocol-limits.ts +33 -0
- package/src/repo/client.ts +3 -0
- package/src/repo/cluster-coordinator.ts +145 -82
- package/src/repo/coordinator-repo.ts +96 -16
- package/src/repo/service.ts +7 -1
- package/src/reputation/types.ts +3 -0
- package/src/rn.ts +7 -1
- package/src/storage/arachnode-fret-adapter.ts +33 -2
- package/src/storage/arachnode-partition.ts +74 -0
- package/src/storage/block-storage.ts +169 -61
- package/src/storage/i-block-storage.ts +9 -0
- package/src/storage/i-raw-storage.ts +14 -0
- package/src/storage/kv-raw-storage.ts +135 -0
- package/src/storage/memory-storage.ts +25 -173
- package/src/storage/memory-store-driver.ts +157 -0
- package/src/storage/raw-store-codec.ts +42 -0
- package/src/storage/raw-store-driver.ts +80 -0
- package/src/storage/{restoration-coordinator-v2.ts → restoration-coordinator.ts} +43 -37
- package/src/storage/ring-selector.ts +317 -176
- package/src/storage/ring-shift-coordinator.ts +271 -0
- package/src/storage/storage-monitor.ts +70 -3
- package/src/storage/storage-repo.ts +710 -558
- package/src/sync/client.ts +5 -1
- package/src/sync/service.ts +2 -1
- package/src/testing/cohort-topic-mesh-harness.ts +61 -11
- package/src/testing/index.ts +1 -0
- package/src/testing/mesh-harness.ts +14 -6
- package/src/testing/raw-storage-conformance.ts +397 -0
|
@@ -1,28 +1,138 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}
|
package/src/dispute/cascade.ts
CHANGED
|
@@ -40,6 +40,13 @@ export type CollectionEnv = {
|
|
|
40
40
|
readonly collectionId: CollectionId;
|
|
41
41
|
readonly log: Log<unknown>;
|
|
42
42
|
readonly createBlockStorage: (blockId: BlockId) => IBlockStorage;
|
|
43
|
+
/**
|
|
44
|
+
* Optional per-block commit-latch runner threaded into each cascade child's {@link applyInvalidation}
|
|
45
|
+
* so the child's compensating write serializes against a concurrent commit on the same block — the
|
|
46
|
+
* same mutual-exclusion the root apply gets (see `InvalidationContext.withBlockCommitLatch`). Omitted
|
|
47
|
+
* → child writes run unlatched (today's behavior).
|
|
48
|
+
*/
|
|
49
|
+
readonly withBlockCommitLatch?: <T>(blockId: BlockId, fn: () => Promise<T>) => Promise<T>;
|
|
43
50
|
};
|
|
44
51
|
|
|
45
52
|
// ─── Invalidated (blockId, revision) pairs ───
|
|
@@ -347,7 +354,7 @@ export async function cascadeInvalidate(input: CascadeInput): Promise<CascadeRes
|
|
|
347
354
|
|
|
348
355
|
const env = cand.env;
|
|
349
356
|
const result = await applyInvalidation(
|
|
350
|
-
{ log: env.log, createBlockStorage: env.createBlockStorage },
|
|
357
|
+
{ log: env.log, createBlockStorage: env.createBlockStorage, withBlockCommitLatch: env.withBlockCommitLatch },
|
|
351
358
|
{
|
|
352
359
|
invalidatedActionId: cand.actionId,
|
|
353
360
|
invalidatedRev: cand.rev,
|
package/src/dispute/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PeerId, IPeerNetwork } from '@optimystic/db-core';
|
|
2
2
|
import { ProtocolClient } from '../protocol-client.js';
|
|
3
3
|
import { DEFAULT_DIAL_TIMEOUT_MS, withRpcDeadlineDefaults, type RpcDeadlineOptions } from '../rpc-deadline.js';
|
|
4
|
+
import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
4
5
|
import type { DisputeChallenge, DisputeResolution, ArbitrationVote, DisputeMessage } from './types.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -32,7 +33,7 @@ export class DisputeClient extends ProtocolClient {
|
|
|
32
33
|
const response = await this.processMessage<{ type: 'vote'; vote: ArbitrationVote }>(
|
|
33
34
|
message,
|
|
34
35
|
this.protocol,
|
|
35
|
-
{ signal, dialTimeoutMs: DEFAULT_DIAL_TIMEOUT_MS }
|
|
36
|
+
{ signal, dialTimeoutMs: DEFAULT_DIAL_TIMEOUT_MS, maxDataLength: MAX_CONTROL_MESSAGE_BYTES }
|
|
36
37
|
);
|
|
37
38
|
return response.vote;
|
|
38
39
|
}
|
|
@@ -45,7 +46,7 @@ export class DisputeClient extends ProtocolClient {
|
|
|
45
46
|
await this.processMessage<{ type: 'ack' }>(
|
|
46
47
|
message,
|
|
47
48
|
this.protocol,
|
|
48
|
-
withRpcDeadlineDefaults(options),
|
|
49
|
+
{ ...withRpcDeadlineDefaults(options), maxDataLength: MAX_CONTROL_MESSAGE_BYTES },
|
|
49
50
|
);
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ClusterRecord, ITransactionValidator, InvalidateRequest, CommitRequest, PendRequest, CollectionId } from '@optimystic/db-core';
|
|
2
|
-
import { blockIdsForTransforms } from '@optimystic/db-core';
|
|
2
|
+
import { blockIdsForTransforms, computeClusterPromiseHash, recordMembershipDigest } from '@optimystic/db-core';
|
|
3
3
|
import { buildDisputeResolutionProof, computeTargetHash, computeArbitratorSetHash, voteSigningPayload, arbitratorSetSigningPayload, VOTE_VERSION, type CertificateTarget } from './invalidation.js';
|
|
4
4
|
import type { PeerId, PrivateKey } from '@libp2p/interface';
|
|
5
5
|
import { sha256 } from 'multiformats/hashes/sha2';
|
|
6
6
|
import { base58btc } from 'multiformats/bases/base58';
|
|
7
7
|
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
8
8
|
import { publicKeyFromRaw } from '@libp2p/crypto/keys';
|
|
9
|
+
import { peerIdBindsPublicKey } from '../cluster/peer-key-binding.js';
|
|
9
10
|
import type {
|
|
10
11
|
ValidationEvidence,
|
|
11
12
|
DisputeChallenge,
|
|
@@ -39,8 +40,14 @@ export interface DisputeServiceInit {
|
|
|
39
40
|
validator?: ITransactionValidator;
|
|
40
41
|
revalidate?: RevalidateTransaction;
|
|
41
42
|
config?: Partial<DisputeConfig>;
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Select arbitrators for a dispute via verifiable dispersed sampling (see `sampleArbitrators`): draw
|
|
45
|
+
* `count` distinct peers from coordinates spread across the whole keyspace, not the block's neighborhood.
|
|
46
|
+
* `round` (0-based escalation round) and `epoch` (agreed membership epoch bytes) are folded into every
|
|
47
|
+
* coordinate so the draw is deterministic yet not pre-positionable — an honest node re-derives the
|
|
48
|
+
* identical set from the same `(blockId, round, epoch)` + agreed membership.
|
|
49
|
+
*/
|
|
50
|
+
selectArbitrators: (blockId: string, excludePeers: string[], count: number, round: number, epoch: Uint8Array) => Promise<PeerId[]>;
|
|
44
51
|
/**
|
|
45
52
|
* Originates the durable reversal when a dispute resolves `challenger-wins`. The dissent
|
|
46
53
|
* coordinator (the node that initiated the dispute, holding the original record) builds the
|
|
@@ -163,9 +170,18 @@ export class DisputeService {
|
|
|
163
170
|
const originalPeers = Object.keys(record.peers);
|
|
164
171
|
const arbitratorCount = this.config.arbitratorCount ?? originalPeers.length;
|
|
165
172
|
|
|
173
|
+
// Round 0 is the only round today (single-round arbitration); `design-dispute-synchronous-escalation`
|
|
174
|
+
// drives real-time round progression. `epoch` pins the dispersed draw to a membership the attacker
|
|
175
|
+
// cannot freely advance.
|
|
176
|
+
// NOTE: interim agreed-membership epoch — hash of the admission-gate-agreed responsible set
|
|
177
|
+
// (`Object.keys(record.peers)`). When `design-cluster-membership-agreement` lands, `epoch` becomes
|
|
178
|
+
// the agreed membership epoch rather than this locally-hashed stand-in.
|
|
179
|
+
const round = 0;
|
|
180
|
+
const epoch = await this.computeEpochBytes(originalPeers);
|
|
181
|
+
|
|
166
182
|
let arbitrators: PeerId[];
|
|
167
183
|
try {
|
|
168
|
-
arbitrators = await this.selectArbitrators(blockId, originalPeers, arbitratorCount);
|
|
184
|
+
arbitrators = await this.selectArbitrators(blockId, originalPeers, arbitratorCount, round, epoch);
|
|
169
185
|
} catch (err) {
|
|
170
186
|
log('dispute-arbitrator-selection-failed', { disputeId, error: err instanceof Error ? err.message : String(err) });
|
|
171
187
|
return undefined;
|
|
@@ -195,7 +211,7 @@ export class DisputeService {
|
|
|
195
211
|
|
|
196
212
|
// Send challenge to all arbitrators and collect votes
|
|
197
213
|
const votes = await this.collectVotes(challenge, arbitrators);
|
|
198
|
-
const resolution = this.resolveDispute(challenge, votes);
|
|
214
|
+
const resolution = await this.resolveDispute(challenge, votes);
|
|
199
215
|
|
|
200
216
|
this.resolvedChallenges.set(disputeId, challenge);
|
|
201
217
|
this.activeDisputes.delete(disputeId);
|
|
@@ -234,10 +250,12 @@ export class DisputeService {
|
|
|
234
250
|
const targetHash = await this.computeChallengeTargetHash(challenge);
|
|
235
251
|
const setHash = await computeArbitratorSetHash(challenge.arbitratorSet ?? []);
|
|
236
252
|
|
|
237
|
-
// Verify the challenge signature
|
|
253
|
+
// Verify the challenge signature — bound to the challenger's id so a relay cannot attach a key it
|
|
254
|
+
// controls under some other peer's id and pass a forged challenge as that peer.
|
|
238
255
|
const validSignature = await this.verifyDisputeSignature(
|
|
239
256
|
challenge.disputeId,
|
|
240
257
|
challenge.signature,
|
|
258
|
+
challenge.challengerPeerId,
|
|
241
259
|
challenge.originalRecord.peers[challenge.challengerPeerId]?.publicKey
|
|
242
260
|
);
|
|
243
261
|
|
|
@@ -351,7 +369,7 @@ export class DisputeService {
|
|
|
351
369
|
}
|
|
352
370
|
|
|
353
371
|
/** Determine dispute resolution from collected votes */
|
|
354
|
-
resolveDispute(challenge: DisputeChallenge, votes: ArbitrationVote[]): DisputeResolution {
|
|
372
|
+
async resolveDispute(challenge: DisputeChallenge, votes: ArbitrationVote[]): Promise<DisputeResolution> {
|
|
355
373
|
const challengerVotes = votes.filter(v => v.vote === 'agree-with-challenger').length;
|
|
356
374
|
const majorityVotes = votes.filter(v => v.vote === 'agree-with-majority').length;
|
|
357
375
|
const totalDecisive = challengerVotes + majorityVotes;
|
|
@@ -366,11 +384,21 @@ export class DisputeService {
|
|
|
366
384
|
outcome = 'inconclusive';
|
|
367
385
|
} else if (challengerVotes >= superMajorityThreshold) {
|
|
368
386
|
outcome = 'challenger-wins';
|
|
369
|
-
// Penalize majority peers who approved the transaction
|
|
387
|
+
// Penalize majority peers who approved the transaction. The promise signatures in a challenge's
|
|
388
|
+
// originalRecord are NOT otherwise verified on the dispute path, so an attacker who crafts a
|
|
389
|
+
// challenge carrying a fabricated originalRecord could attach a forged approval under an honest
|
|
390
|
+
// peer's id to get that peer a FalseApproval penalty. Gate each false-approval on the approval
|
|
391
|
+
// being binding-valid (key bound to the id AND signature verifies); skip — never penalize —
|
|
392
|
+
// any approval that is unbound or invalid.
|
|
370
393
|
const originalRecord = challenge.originalRecord;
|
|
394
|
+
const promiseHash = await this.computePromiseHash(originalRecord);
|
|
371
395
|
for (const [peerId, signature] of Object.entries(originalRecord.promises)) {
|
|
372
396
|
if (signature.type === 'approve' && peerId !== challenge.challengerPeerId) {
|
|
373
|
-
|
|
397
|
+
if (await this.verifyPromiseSignature(originalRecord, peerId, promiseHash, signature)) {
|
|
398
|
+
affectedPeers.push({ peerId, reason: 'false-approval' });
|
|
399
|
+
} else {
|
|
400
|
+
log('dispute-skip-unverified-approval', { disputeId: challenge.disputeId, peerId });
|
|
401
|
+
}
|
|
374
402
|
}
|
|
375
403
|
}
|
|
376
404
|
} else if (majorityVotes >= superMajorityThreshold) {
|
|
@@ -557,6 +585,19 @@ export class DisputeService {
|
|
|
557
585
|
};
|
|
558
586
|
}
|
|
559
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Interim agreed-membership epoch bytes for the dispersed arbitrator draw: SHA-256 of the sorted,
|
|
590
|
+
* comma-joined responsible peer-id set (the same set the admission gate agrees on). Sorted so the
|
|
591
|
+
* digest is independent of enumeration order, so every honest node computes the identical epoch — the
|
|
592
|
+
* property that lets the verify path re-derive the same arbitrator set. Replaced by the real agreed
|
|
593
|
+
* membership epoch when `design-cluster-membership-agreement` lands.
|
|
594
|
+
*/
|
|
595
|
+
private async computeEpochBytes(peers: string[]): Promise<Uint8Array> {
|
|
596
|
+
const joined = [...peers].sort().join(',');
|
|
597
|
+
const digest = await sha256.digest(new TextEncoder().encode(joined));
|
|
598
|
+
return digest.digest;
|
|
599
|
+
}
|
|
600
|
+
|
|
560
601
|
private async computeDisputeId(messageHash: string, peerId: string, timestamp: number): Promise<string> {
|
|
561
602
|
const input = `${messageHash}+${peerId}+${timestamp}`;
|
|
562
603
|
const inputBytes = new TextEncoder().encode(input);
|
|
@@ -583,6 +624,7 @@ export class DisputeService {
|
|
|
583
624
|
private async verifyDisputeSignature(
|
|
584
625
|
disputeId: string,
|
|
585
626
|
signature: string,
|
|
627
|
+
challengerPeerId: string,
|
|
586
628
|
publicKey?: string | Uint8Array
|
|
587
629
|
): Promise<boolean> {
|
|
588
630
|
if (!publicKey?.length) return false;
|
|
@@ -590,6 +632,9 @@ export class DisputeService {
|
|
|
590
632
|
const keyBytes = typeof publicKey === 'string'
|
|
591
633
|
? uint8ArrayFromString(publicKey, 'base64url')
|
|
592
634
|
: publicKey;
|
|
635
|
+
// The key must be the one the challenger's id provably names, else a forged challenge could be
|
|
636
|
+
// attributed to any peer id while signed by a key the relay controls.
|
|
637
|
+
if (!peerIdBindsPublicKey(challengerPeerId, keyBytes)) return false;
|
|
593
638
|
const pubKey = publicKeyFromRaw(keyBytes);
|
|
594
639
|
const payload = new TextEncoder().encode(disputeId);
|
|
595
640
|
const sigBytes = uint8ArrayFromString(signature, 'base64url');
|
|
@@ -599,6 +644,46 @@ export class DisputeService {
|
|
|
599
644
|
}
|
|
600
645
|
}
|
|
601
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Reconstruct the cluster promise-vote hash for `record` — identical to `ClusterMember.computePromiseHash`,
|
|
649
|
+
* and version-dispatched the same way: a v2 record folds its membership digest into the preimage, a v1 /
|
|
650
|
+
* unversioned record hashes byte-identically to before. The promise signatures in a disputed record were
|
|
651
|
+
* produced over this by the cluster path, so re-verifying an approval requires reproducing it here — a v2
|
|
652
|
+
* originalRecord whose digest we omitted would fail to verify its (honest) approvals.
|
|
653
|
+
*/
|
|
654
|
+
private async computePromiseHash(record: ClusterRecord): Promise<string> {
|
|
655
|
+
return computeClusterPromiseHash(record.messageHash, record.message, recordMembershipDigest(record));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* True iff `signature` on `record` is a binding-valid promise vote from `peerId`: the record's key for
|
|
660
|
+
* `peerId` must be the one that id provably names AND the signature must verify over the reconstructed
|
|
661
|
+
* promise-vote preimage. Total: returns `false` (never throws) on a missing/unbound/malformed key or an
|
|
662
|
+
* undecodable signature. Mirrors `ClusterMember.verifySignature`'s binding-then-verify order.
|
|
663
|
+
*/
|
|
664
|
+
private async verifyPromiseSignature(
|
|
665
|
+
record: ClusterRecord,
|
|
666
|
+
peerId: string,
|
|
667
|
+
promiseHash: string,
|
|
668
|
+
signature: { type: string; signature: string; rejectReason?: string }
|
|
669
|
+
): Promise<boolean> {
|
|
670
|
+
const publicKey = record.peers[peerId]?.publicKey;
|
|
671
|
+
if (!publicKey?.length) return false;
|
|
672
|
+
try {
|
|
673
|
+
const keyBytes = typeof publicKey === 'string'
|
|
674
|
+
? uint8ArrayFromString(publicKey, 'base64url')
|
|
675
|
+
: publicKey;
|
|
676
|
+
if (!peerIdBindsPublicKey(peerId, keyBytes)) return false;
|
|
677
|
+
const pubKey = publicKeyFromRaw(keyBytes);
|
|
678
|
+
const payloadStr = promiseHash + ':' + signature.type + (signature.rejectReason ? ':' + signature.rejectReason : '');
|
|
679
|
+
const payload = new TextEncoder().encode(payloadStr);
|
|
680
|
+
const sigBytes = uint8ArrayFromString(signature.signature, 'base64url');
|
|
681
|
+
return pubKey.verify(payload, sigBytes);
|
|
682
|
+
} catch {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
602
687
|
private findChallengeForDispute(disputeId: string): DisputeChallenge | undefined {
|
|
603
688
|
return this.activeDisputes.get(disputeId) ?? this.resolvedChallenges.get(disputeId);
|
|
604
689
|
}
|
package/src/dispute/index.ts
CHANGED
|
@@ -14,7 +14,13 @@ export {
|
|
|
14
14
|
type EngineHealthState,
|
|
15
15
|
DEFAULT_DISPUTE_CONFIG,
|
|
16
16
|
} from './types.js';
|
|
17
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
sampleArbitrators,
|
|
19
|
+
coordinatePreimage,
|
|
20
|
+
type ArbitratorSamplingParams,
|
|
21
|
+
type NearestResolver,
|
|
22
|
+
type RingHashFn,
|
|
23
|
+
} from './arbitrator-selection.js';
|
|
18
24
|
export {
|
|
19
25
|
buildDisputeResolutionProof,
|
|
20
26
|
verifyInvalidationCertificate,
|
|
@@ -82,6 +82,21 @@ export type ArbitratorSetRecomputeContext = {
|
|
|
82
82
|
readonly blockIds: ReadonlyArray<BlockId>;
|
|
83
83
|
/** The carried set being judged (peer-id strings). */
|
|
84
84
|
readonly arbitratorSet: ReadonlyArray<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Escalation round the carried set was drawn at (0-based). A recompute capability MUST feed this to
|
|
87
|
+
* `sampleArbitrators` so it re-derives the SAME dispersed draw. Today every dispute is round 0
|
|
88
|
+
* (single-round arbitration), so the verify path pins it to 0; `design-dispute-synchronous-escalation`
|
|
89
|
+
* threads the real round through the proof and updates this seam.
|
|
90
|
+
*/
|
|
91
|
+
readonly round: number;
|
|
92
|
+
/**
|
|
93
|
+
* Agreed membership epoch the carried set was drawn against — the bytes `sampleArbitrators` folds into
|
|
94
|
+
* every coordinate. A recompute capability MUST feed this so its re-derivation matches the original
|
|
95
|
+
* draw. Optional until `design-dispute-synchronous-escalation` carries/derives it on the proof; while
|
|
96
|
+
* absent a recompute cannot reconstruct the draw and should return `{ feasible: false }` (degradation),
|
|
97
|
+
* which matches today's behavior — no recompute capability is wired in production.
|
|
98
|
+
*/
|
|
99
|
+
readonly epoch?: Uint8Array;
|
|
85
100
|
};
|
|
86
101
|
|
|
87
102
|
/**
|
|
@@ -291,6 +306,11 @@ export async function verifyInvalidationCertificate(proof: DisputeResolutionProo
|
|
|
291
306
|
invalidatedActionId: target.invalidatedActionId,
|
|
292
307
|
blockIds: target.blockIds,
|
|
293
308
|
arbitratorSet: proof.arbitratorSet,
|
|
309
|
+
// NOTE: `round` pinned to 0 (single-round arbitration today) and `epoch` omitted until
|
|
310
|
+
// `design-dispute-synchronous-escalation` threads the real (round, epoch) through the proof, so a
|
|
311
|
+
// `sampleArbitrators`-based recompute re-derives the identical dispersed set. Until then no recompute
|
|
312
|
+
// capability is wired in production; the test recomputes ignore these fields.
|
|
313
|
+
round: 0,
|
|
294
314
|
});
|
|
295
315
|
if (verdict.feasible) {
|
|
296
316
|
if (!verdict.legitimate) {
|
|
@@ -458,6 +478,16 @@ export type InvalidationContext = {
|
|
|
458
478
|
readonly log: Log<unknown>;
|
|
459
479
|
/** Resolves a block's storage so the compensating revision can be written. */
|
|
460
480
|
readonly createBlockStorage: (blockId: BlockId) => IBlockStorage;
|
|
481
|
+
/**
|
|
482
|
+
* Runs `fn` while holding the same per-block commit latch `StorageRepo.commit` and
|
|
483
|
+
* `saveReplicatedBlock` hold, so the compensating `saveReplica`/`saveDeletion` read-modify-write of
|
|
484
|
+
* `meta.latest` is mutually exclusive with a concurrent local commit on that block. Without it, an
|
|
485
|
+
* invalidation advancing `latest` outside that latch is invisible to commit's staleness guard, so a
|
|
486
|
+
* commit that read a stale `latest` can clobber it back down — a lost-update / non-monotonic
|
|
487
|
+
* regression. Optional: when omitted (unit tests / non-`StorageRepo` hosts) the write runs unlatched,
|
|
488
|
+
* preserving today's behavior. The host supplies `StorageRepo`'s `withBlockCommitLatch`.
|
|
489
|
+
*/
|
|
490
|
+
readonly withBlockCommitLatch?: <T>(blockId: BlockId, fn: () => Promise<T>) => Promise<T>;
|
|
461
491
|
};
|
|
462
492
|
|
|
463
493
|
export type ApplyInvalidationParams = {
|
|
@@ -552,11 +582,19 @@ export async function applyInvalidation(ctx: InvalidationContext, params: ApplyI
|
|
|
552
582
|
// Deterministic compensating-revision actionId — identical on every member, so all converge on
|
|
553
583
|
// the same (rev, actionId) for both the restore and the tombstone path.
|
|
554
584
|
const revertActionId = await hashString(`inv:${invalidatedActionId}:${proof.disputeId}:${blockId}:${rev}`);
|
|
585
|
+
// Hold the per-block commit latch around ONLY the compensating write (matching saveReplicatedBlock's
|
|
586
|
+
// scope): the monotonic guard inside saveReplica/saveDeletion then runs under the same latch a
|
|
587
|
+
// concurrent commit holds, so the two RMW of meta.latest serialize and latest stays monotonic.
|
|
588
|
+
// Acquire per block, one at a time — invalidation never holds two block latches, so it cannot
|
|
589
|
+
// deadlock against commit's sorted multi-latch acquisition. When no runner is injected, the write
|
|
590
|
+
// runs unlatched (today's behavior).
|
|
591
|
+
const runLatched = <T>(fn: () => Promise<T>): Promise<T> =>
|
|
592
|
+
ctx.withBlockCommitLatch ? ctx.withBlockCommitLatch(blockId, fn) : fn();
|
|
555
593
|
if (computation.kind === 'delete') {
|
|
556
594
|
// Block-creation reversal: physically remove the created block by writing a forward tombstone
|
|
557
595
|
// revision. The `restoredContentHash` is the DELETED_BLOCK_RESTORE sentinel — a deleted block
|
|
558
596
|
// has no content hash, and the sentinel tells dependents "observed content is gone → invalidate".
|
|
559
|
-
await storage.saveDeletion({ rev, actionId: revertActionId });
|
|
597
|
+
await runLatched(() => storage.saveDeletion({ rev, actionId: revertActionId }));
|
|
560
598
|
log('apply-delete-restore blockId=%s invalidatedRev=%d rev=%d', blockId, invalidatedRev, rev);
|
|
561
599
|
reverted.push({ blockId, fromRev: computation.fromRev, restoredContentHash: DELETED_BLOCK_RESTORE });
|
|
562
600
|
continue;
|
|
@@ -565,7 +603,7 @@ export async function applyInvalidation(ctx: InvalidationContext, params: ApplyI
|
|
|
565
603
|
// Surviving later actions were replayed verbatim; true read-dependents are out of scope here.
|
|
566
604
|
log('apply-replayed-later-actions blockId=%s count=%d', blockId, computation.laterActions);
|
|
567
605
|
}
|
|
568
|
-
await storage.saveReplica(computation.block, { rev, actionId: revertActionId });
|
|
606
|
+
await runLatched(() => storage.saveReplica(computation.block, { rev, actionId: revertActionId }));
|
|
569
607
|
reverted.push({ blockId, fromRev: computation.fromRev, restoredContentHash: computation.restoredContentHash });
|
|
570
608
|
}
|
|
571
609
|
|
package/src/dispute/service.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { pipe } from 'it-pipe';
|
|
|
2
2
|
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed';
|
|
3
3
|
import type { Startable, Logger, Stream, Connection, StreamHandler } from '@libp2p/interface';
|
|
4
4
|
import type { Uint8ArrayList } from 'uint8arraylist';
|
|
5
|
+
import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
5
6
|
import type { DisputeMessage } from './types.js';
|
|
6
7
|
import type { DisputeService } from './dispute-service.js';
|
|
7
8
|
|
|
@@ -95,6 +96,11 @@ export class DisputeProtocolService implements Startable {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
yield new TextEncoder().encode(JSON.stringify(response));
|
|
99
|
+
// One request per stream: every real DisputeClient sends exactly one
|
|
100
|
+
// request per dial (see ProtocolClient.processMessage), so complete the
|
|
101
|
+
// generator after the first response. A second frame a peer queued is then
|
|
102
|
+
// never read or parsed. Mirrors sync/block-transfer.
|
|
103
|
+
return;
|
|
98
104
|
}
|
|
99
105
|
};
|
|
100
106
|
|
|
@@ -102,7 +108,7 @@ export class DisputeProtocolService implements Startable {
|
|
|
102
108
|
try {
|
|
103
109
|
const responses = pipe(
|
|
104
110
|
stream,
|
|
105
|
-
(source) => lpDecode(source),
|
|
111
|
+
(source) => lpDecode(source, { maxDataLength: MAX_CONTROL_MESSAGE_BYTES }),
|
|
106
112
|
processStream.bind(this),
|
|
107
113
|
(source) => lpEncode(source)
|
|
108
114
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,43 +1,46 @@
|
|
|
1
|
-
export * from "./cluster/client.js";
|
|
2
|
-
export * from "./cluster/cluster-repo.js";
|
|
3
|
-
export * from "./cluster/commit-cert.js";
|
|
4
|
-
export * from "./cluster/service.js";
|
|
5
|
-
export * from "./cluster/rebalance-monitor.js";
|
|
6
|
-
export * from "./cluster/spread-on-churn.js";
|
|
7
|
-
export * from "./cluster/block-transfer.js";
|
|
8
|
-
export * from "./cluster/block-transfer-service.js";
|
|
9
|
-
export * from "./protocol-client.js";
|
|
10
|
-
export * from "./repo/client.js";
|
|
11
|
-
export * from "./repo/cluster-coordinator.js";
|
|
12
|
-
export * from "./repo/coordinator-repo.js";
|
|
13
|
-
export * from "./repo/service.js";
|
|
14
|
-
export * from "./storage/block-storage.js";
|
|
15
|
-
export * from "./storage/
|
|
16
|
-
export * from "./storage/
|
|
17
|
-
export * from "./storage/
|
|
18
|
-
export * from "./storage/
|
|
19
|
-
export * from "./storage/storage
|
|
20
|
-
export * from "./storage/
|
|
21
|
-
export * from "./storage/
|
|
22
|
-
export * from "./storage/storage-
|
|
23
|
-
export * from "./storage/
|
|
24
|
-
export * from "./
|
|
25
|
-
export * from "./
|
|
26
|
-
export * from "./
|
|
27
|
-
export * from "./
|
|
28
|
-
export * from "./
|
|
29
|
-
export * from "./
|
|
30
|
-
export * from "./
|
|
31
|
-
export * from "./
|
|
32
|
-
export * from "./
|
|
33
|
-
export * from "./
|
|
34
|
-
export * from "./
|
|
35
|
-
export * from "./
|
|
36
|
-
export * from "./
|
|
37
|
-
export * from "./
|
|
38
|
-
export * from "./
|
|
39
|
-
export * from "./
|
|
40
|
-
export * from "./
|
|
41
|
-
export * from "./
|
|
42
|
-
export * from "./
|
|
43
|
-
export * from "./
|
|
1
|
+
export * from "./cluster/client.js";
|
|
2
|
+
export * from "./cluster/cluster-repo.js";
|
|
3
|
+
export * from "./cluster/commit-cert.js";
|
|
4
|
+
export * from "./cluster/service.js";
|
|
5
|
+
export * from "./cluster/rebalance-monitor.js";
|
|
6
|
+
export * from "./cluster/spread-on-churn.js";
|
|
7
|
+
export * from "./cluster/block-transfer.js";
|
|
8
|
+
export * from "./cluster/block-transfer-service.js";
|
|
9
|
+
export * from "./protocol-client.js";
|
|
10
|
+
export * from "./repo/client.js";
|
|
11
|
+
export * from "./repo/cluster-coordinator.js";
|
|
12
|
+
export * from "./repo/coordinator-repo.js";
|
|
13
|
+
export * from "./repo/service.js";
|
|
14
|
+
export * from "./storage/block-storage.js";
|
|
15
|
+
export * from "./storage/raw-store-driver.js";
|
|
16
|
+
export * from "./storage/kv-raw-storage.js";
|
|
17
|
+
export * from "./storage/memory-store-driver.js";
|
|
18
|
+
export * from "./storage/memory-storage.js";
|
|
19
|
+
export * from "./storage/i-block-storage.js";
|
|
20
|
+
export * from "./storage/i-raw-storage.js";
|
|
21
|
+
export * from "./storage/struct.js";
|
|
22
|
+
export * from "./storage/storage-repo.js";
|
|
23
|
+
export * from "./storage/restoration-coordinator.js";
|
|
24
|
+
export * from "./storage/ring-selector.js";
|
|
25
|
+
export * from "./storage/storage-monitor.js";
|
|
26
|
+
export * from "./storage/arachnode-fret-adapter.js";
|
|
27
|
+
export * from "./sync/protocol.js";
|
|
28
|
+
export * from "./sync/client.js";
|
|
29
|
+
export * from "./sync/service.js";
|
|
30
|
+
export * from "./it-utility.js";
|
|
31
|
+
export * from "./libp2p-key-network.js";
|
|
32
|
+
export * from "./libp2p-node.js";
|
|
33
|
+
export * from "./routing/responsibility.js";
|
|
34
|
+
export * from "./routing/libp2p-known-peers.js";
|
|
35
|
+
export * from "./network/network-manager-service.js";
|
|
36
|
+
export * from "./network/get-network-manager.js";
|
|
37
|
+
export * from "./reputation/index.js";
|
|
38
|
+
export * from "./dispute/index.js";
|
|
39
|
+
export * from "./cohort-topic/index.js";
|
|
40
|
+
export * from "./matchmaking/index.js";
|
|
41
|
+
export * from "./reactivity/index.js";
|
|
42
|
+
export * from "./cluster/i-transaction-state-store.js";
|
|
43
|
+
export * from "./cluster/memory-transaction-state-store.js";
|
|
44
|
+
export * from "./cluster/persistent-transaction-state-store.js";
|
|
45
|
+
export * from "./storage/i-kv-store.js";
|
|
46
|
+
export * from "./storage/memory-kv-store.js";
|
package/src/it-utility.ts
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
export async function first<T>(
|
|
2
|
-
createIterable: (
|
|
3
|
-
onEmpty: () => T = () => { throw new Error('No items found') }
|
|
4
|
-
timeoutMs?: number
|
|
2
|
+
createIterable: () => AsyncIterable<T>,
|
|
3
|
+
onEmpty: () => T = () => { throw new Error('No items found') }
|
|
5
4
|
): Promise<T> {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
for await (const item of createIterable(controller.signal)) {
|
|
10
|
-
return item;
|
|
11
|
-
}
|
|
12
|
-
return onEmpty();
|
|
13
|
-
} finally {
|
|
14
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
15
|
-
controller.abort();
|
|
5
|
+
for await (const item of createIterable()) {
|
|
6
|
+
return item;
|
|
16
7
|
}
|
|
8
|
+
return onEmpty();
|
|
17
9
|
}
|
|
18
10
|
|
|
19
11
|
export async function asyncIteratorToArray<T>(iterator: AsyncIterable<T>): Promise<T[]> {
|