@optimystic/db-p2p 0.14.1 → 0.16.3
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 +441 -89
- 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 +511 -81
- 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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key ↔ peer-id binding predicate for the cluster/dispute vote paths.
|
|
3
|
+
*
|
|
4
|
+
* For a libp2p Ed25519 identity the peer-id **is** the multihash of the public key, so a public key
|
|
5
|
+
* that a record self-asserts for a peer id can be checked against the id itself — no network lookup.
|
|
6
|
+
* Both the cluster two-phase-commit path (`cluster-repo.ts`) and the dispute path (`dispute-service.ts`)
|
|
7
|
+
* read a signing key straight from the record they are handed; without this check a coordinator could
|
|
8
|
+
* attach a key it controls under an honest peer's id and have a forged vote verify. This predicate is
|
|
9
|
+
* the gate that ties a self-asserted key back to the identity it claims.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the binding logic already used by the cohort-topic substrate in `cohort-topic/peer-sig.ts`
|
|
12
|
+
* (`peerIdFromString(str).publicKey.raw`); intentionally NOT importing that module, which is
|
|
13
|
+
* specialized for its own wire encoding.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: this proves only that a vote attributed to peer id `X` was signed by the key `X` names. It does
|
|
16
|
+
* NOT decide whether `X` is legitimately a cohort member — a coordinator that mints fresh keypairs and
|
|
17
|
+
* uses each key's own derived id passes this check for every one of them. Sybil/membership is a separate
|
|
18
|
+
* layer (cohort-topic membership certificates); do not assume it is solved here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True iff `rawKey` is the Ed25519 public key that `peerIdStr` names. Total: returns `false` on a
|
|
25
|
+
* non-Ed25519 id, a missing/mismatched key, or any malformed input — never throws.
|
|
26
|
+
*/
|
|
27
|
+
export function peerIdBindsPublicKey(peerIdStr: string, rawKey: Uint8Array): boolean {
|
|
28
|
+
try {
|
|
29
|
+
const peerId = peerIdFromString(peerIdStr);
|
|
30
|
+
if (peerId.type !== 'Ed25519' || peerId.publicKey === undefined) return false;
|
|
31
|
+
const expected = peerId.publicKey.raw; // 32 bytes for Ed25519
|
|
32
|
+
if (expected.length !== rawKey.length) return false;
|
|
33
|
+
return expected.every((b, i) => b === rawKey[i]);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
Binary file
|
|
@@ -15,6 +15,13 @@ export interface RebalanceEvent {
|
|
|
15
15
|
lost: string[]
|
|
16
16
|
/** Peers that are now closer for the lost blocks: blockId → peerId[] */
|
|
17
17
|
newOwners: Map<string, string[]>
|
|
18
|
+
/**
|
|
19
|
+
* Replication floor `N` for this event — the cohort size FRET assembled at check time
|
|
20
|
+
* ({@link RebalanceMonitor.getCohortSize}). The reaction gates release of a `lost` block on
|
|
21
|
+
* confirming it replicated to this many new owners, so a lost block is never released below the
|
|
22
|
+
* floor. See `docs/arachnode-ring-handoff.md` § Part 2.
|
|
23
|
+
*/
|
|
24
|
+
floor: number
|
|
18
25
|
/** Timestamp of the topology change that triggered this */
|
|
19
26
|
triggeredAt: number
|
|
20
27
|
}
|
|
@@ -33,13 +40,27 @@ export interface RebalanceMonitorDeps {
|
|
|
33
40
|
fret: FretService
|
|
34
41
|
partitionDetector: PartitionDetector
|
|
35
42
|
fretAdapter: ArachnodeFretAdapter
|
|
43
|
+
/**
|
|
44
|
+
* The owned-block tracked set. When provided (e.g. the shared `ownedBlocks` set wired in
|
|
45
|
+
* `libp2p-node-base`), the monitor references this exact `Set` instead of constructing its own,
|
|
46
|
+
* so it stays in lock-step with the `SpreadOnChurnMonitor` that shares it. Omit for standalone
|
|
47
|
+
* construction (unit tests) — a fresh private `Set` preserves all existing behavior. Note: only
|
|
48
|
+
* `trackedBlocks` is shared; `responsibilitySnapshot` stays per-monitor (it is rebalance's own
|
|
49
|
+
* was-responsible memory, not owned-block tracking).
|
|
50
|
+
*/
|
|
51
|
+
trackedBlocks?: Set<string>
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
type RebalanceHandler = (event: RebalanceEvent) => void
|
|
39
55
|
|
|
40
56
|
export class RebalanceMonitor implements Startable {
|
|
41
57
|
private running = false
|
|
42
|
-
private readonly trackedBlocks
|
|
58
|
+
private readonly trackedBlocks: Set<string>
|
|
59
|
+
// Per-monitor was-responsible memory (NOT shared, unlike trackedBlocks). When the shared
|
|
60
|
+
// trackedBlocks set is mutated externally — spread's no-local-data self-prune, or the node's
|
|
61
|
+
// responsibility-loss eviction going through untrackBlock — a snapshot entry for a since-removed
|
|
62
|
+
// block may linger here. That is acceptable: performRebalanceCheck only iterates trackedBlocks, so
|
|
63
|
+
// a lingering entry is inert; if the block is later re-fed, its responsibility is simply re-derived.
|
|
43
64
|
private readonly responsibilitySnapshot = new Map<string, boolean>()
|
|
44
65
|
private readonly handlers: RebalanceHandler[] = []
|
|
45
66
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -58,6 +79,10 @@ export class RebalanceMonitor implements Startable {
|
|
|
58
79
|
private readonly deps: RebalanceMonitorDeps,
|
|
59
80
|
config: RebalanceMonitorConfig = {}
|
|
60
81
|
) {
|
|
82
|
+
// Share the injected owned-block set when present (so spread + rebalance never drift);
|
|
83
|
+
// otherwise own a private set (standalone construction / unit tests). Only trackedBlocks is
|
|
84
|
+
// shared — responsibilitySnapshot stays per-monitor.
|
|
85
|
+
this.trackedBlocks = deps.trackedBlocks ?? new Set<string>()
|
|
61
86
|
this.debounceMs = config.debounceMs ?? 5000
|
|
62
87
|
this.minRebalanceIntervalMs = config.minRebalanceIntervalMs ?? 60000
|
|
63
88
|
this.suppressDuringPartition = config.suppressDuringPartition ?? true
|
|
@@ -192,10 +217,16 @@ export class RebalanceMonitor implements Startable {
|
|
|
192
217
|
|
|
193
218
|
log('rebalance check: gained=%d lost=%d', gained.length, lost.length)
|
|
194
219
|
|
|
195
|
-
return { gained, lost, newOwners, triggeredAt }
|
|
220
|
+
return { gained, lost, newOwners, floor: this.getCohortSize(), triggeredAt }
|
|
196
221
|
}
|
|
197
222
|
|
|
198
|
-
|
|
223
|
+
/**
|
|
224
|
+
* The replication floor `N` — the cohort size FRET assembles for a block. Public so the ring-shift
|
|
225
|
+
* handoff and the rebalance reaction can gate release on confirming replication to this many
|
|
226
|
+
* holders (`docs/arachnode-ring-handoff.md` § Replication floor). Derives from FRET's network-size
|
|
227
|
+
* estimate: `clamp(ceil(sqrt(n_est)), 1, 3)`, defaulting to 3 when no confident estimate exists.
|
|
228
|
+
*/
|
|
229
|
+
getCohortSize(): number {
|
|
199
230
|
const diag: any = (this.deps.fret as any).getDiagnostics?.()
|
|
200
231
|
const estimate = diag?.estimate ?? diag?.n
|
|
201
232
|
if (typeof estimate === 'number' && Number.isFinite(estimate) && estimate > 0) {
|
package/src/cluster/service.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId } fro
|
|
|
5
5
|
import type { ICluster, ClusterRecord } from '@optimystic/db-core';
|
|
6
6
|
import { encodePeers, type RedirectPayload } from '../repo/redirect.js';
|
|
7
7
|
import { toClusterErrorEnvelope } from './cluster-error.js';
|
|
8
|
+
import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
8
9
|
import type { Uint8ArrayList } from 'uint8arraylist';
|
|
9
10
|
|
|
10
11
|
interface BaseComponents {
|
|
@@ -204,6 +205,11 @@ export class ClusterService implements Startable {
|
|
|
204
205
|
|
|
205
206
|
// Encode and yield the response
|
|
206
207
|
yield new TextEncoder().encode(JSON.stringify(response));
|
|
208
|
+
// One request per stream: every real ClusterClient sends exactly one
|
|
209
|
+
// request per dial (see ProtocolClient.processMessage), so complete the
|
|
210
|
+
// generator after the first response. A second frame a peer queued is
|
|
211
|
+
// then never read or parsed. Mirrors sync/block-transfer.
|
|
212
|
+
return;
|
|
207
213
|
}
|
|
208
214
|
};
|
|
209
215
|
|
|
@@ -211,7 +217,7 @@ export class ClusterService implements Startable {
|
|
|
211
217
|
try {
|
|
212
218
|
const responses = pipe(
|
|
213
219
|
stream,
|
|
214
|
-
(source) => lpDecode(source),
|
|
220
|
+
(source) => lpDecode(source, { maxDataLength: MAX_CONTROL_MESSAGE_BYTES }),
|
|
215
221
|
processStream.bind(this),
|
|
216
222
|
(source) => lpEncode(source)
|
|
217
223
|
);
|
|
@@ -48,6 +48,13 @@ export interface SpreadOnChurnDeps {
|
|
|
48
48
|
peerNetwork: IPeerNetwork
|
|
49
49
|
clusterSize: number
|
|
50
50
|
protocolPrefix?: string
|
|
51
|
+
/**
|
|
52
|
+
* The owned-block tracked set. When provided (e.g. the shared `ownedBlocks` set wired in
|
|
53
|
+
* `libp2p-node-base`), the monitor references this exact `Set` instead of constructing its own,
|
|
54
|
+
* so it stays in lock-step with the `RebalanceMonitor` that shares it. Omit for standalone
|
|
55
|
+
* construction (unit tests) — a fresh private `Set` preserves all existing behavior.
|
|
56
|
+
*/
|
|
57
|
+
trackedBlocks?: Set<string>
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
export interface SpreadEvent {
|
|
@@ -83,7 +90,7 @@ const DEFAULT_CONFIG: SpreadOnChurnConfig = {
|
|
|
83
90
|
|
|
84
91
|
export class SpreadOnChurnMonitor implements Startable {
|
|
85
92
|
private running = false
|
|
86
|
-
private readonly trackedBlocks
|
|
93
|
+
private readonly trackedBlocks: Set<string>
|
|
87
94
|
private readonly handlers: SpreadHandler[] = []
|
|
88
95
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
89
96
|
private departureTimestamps: number[] = []
|
|
@@ -96,6 +103,9 @@ export class SpreadOnChurnMonitor implements Startable {
|
|
|
96
103
|
private readonly deps: SpreadOnChurnDeps,
|
|
97
104
|
config: Partial<SpreadOnChurnConfig> = {}
|
|
98
105
|
) {
|
|
106
|
+
// Share the injected owned-block set when present (so spread + rebalance never drift);
|
|
107
|
+
// otherwise own a private set (standalone construction / unit tests).
|
|
108
|
+
this.trackedBlocks = deps.trackedBlocks ?? new Set<string>()
|
|
99
109
|
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
100
110
|
this.onConnectionClose = () => this.handleDeparture()
|
|
101
111
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
|
|
2
|
+
|
|
3
|
+
/** A component that exposes the super-majority threshold it actually runs on. */
|
|
4
|
+
export interface HasEffectiveSuperMajorityThreshold {
|
|
5
|
+
readonly effectiveSuperMajorityThreshold: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fail-fast coupling check for a live node's consensus wiring.
|
|
10
|
+
*
|
|
11
|
+
* The cluster **member** (what accepts a super-majority of promises as sufficient for a commit) and the
|
|
12
|
+
* **coordinator** (what declares a transaction committed once it has that super-majority) MUST run the
|
|
13
|
+
* SAME `superMajorityThreshold`. If they disagree, the coordinator can declare a transaction final on a
|
|
14
|
+
* super-majority the member never accepts as final — a silent phase-disagreement that only surfaces
|
|
15
|
+
* mid-consensus, far too late.
|
|
16
|
+
*
|
|
17
|
+
* On a live node both are fed from a single resolved `consensusConfig`, so this check normally passes. It
|
|
18
|
+
* exists to catch *future* drift — divergent fallback defaults, a mis-threaded `clusterPolicy`, a
|
|
19
|
+
* per-deployment config that reaches only one side — by throwing at construction with both values and
|
|
20
|
+
* their provenance, rather than letting the node come up mismatched.
|
|
21
|
+
*
|
|
22
|
+
* @throws Error naming both resolved thresholds and where they come from, if they are not equal.
|
|
23
|
+
*/
|
|
24
|
+
export function assertSuperMajorityCoupling(
|
|
25
|
+
member: HasEffectiveSuperMajorityThreshold,
|
|
26
|
+
coordinator: HasEffectiveSuperMajorityThreshold
|
|
27
|
+
): void {
|
|
28
|
+
const memberThreshold = member.effectiveSuperMajorityThreshold;
|
|
29
|
+
const coordinatorThreshold = coordinator.effectiveSuperMajorityThreshold;
|
|
30
|
+
if (memberThreshold !== coordinatorThreshold) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Super-majority threshold mismatch at node startup: cluster member resolved ${memberThreshold} but coordinator resolved ${coordinatorThreshold}. ` +
|
|
33
|
+
`Both derive from consensusConfig.superMajorityThreshold (options.clusterPolicy?.superMajorityThreshold ?? DEFAULT_SUPER_MAJORITY_THRESHOLD=${DEFAULT_SUPER_MAJORITY_THRESHOLD}); ` +
|
|
34
|
+
`they must be equal or the coordinator would declare transactions committed on a super-majority the member rejects as final.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
toGossipRecord,
|
|
17
17
|
willingnessBitsHex,
|
|
18
18
|
selfWillingnessBits,
|
|
19
|
+
type ChildLinkRefV1,
|
|
19
20
|
type CohortGossipV1,
|
|
20
21
|
type CohortTopicSummary,
|
|
21
22
|
type GossipRecordRefV1,
|
|
@@ -35,6 +36,20 @@ import {
|
|
|
35
36
|
*/
|
|
36
37
|
export const DEFAULT_GOSSIP_INTERVAL_MS = 5_000;
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Default `T_willingness_heartbeat` (ms): how often a genuinely-**idle** but **willing** engine re-broadcasts
|
|
41
|
+
* a willingness-only heartbeat so a cold cohort can bootstrap (siblings hear it, instantiate, and reciprocate
|
|
42
|
+
* their own willingness) without waiting on a first registration that can never be admitted while the view is
|
|
43
|
+
* empty. See `docs/cohort-topic.md` §Cold-start instantiation / §Configuration.
|
|
44
|
+
*
|
|
45
|
+
* On the order of the ping interval (~30 s, `ttl/3` Core) — a few gossip rounds at the 5 s cadence. A
|
|
46
|
+
* record-carrying (non-idle) round already ships willingness every round and resets this clock, so the
|
|
47
|
+
* throttle governs only engines with nothing else to say. The very first idle round after an engine is
|
|
48
|
+
* created emits immediately (no wait), so bootstrap converges in ~2 rounds; the throttle only paces the
|
|
49
|
+
* steady-state re-broadcast of an idle willing cohort.
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_WILLINGNESS_HEARTBEAT_MS = 30_000;
|
|
52
|
+
|
|
38
53
|
/**
|
|
39
54
|
* Per-`CoordEngine` queue of registration-record deltas accumulated between gossip rounds. The renewal
|
|
40
55
|
* cohort side calls {@link PendingDeltas.touch} on every served ping/re-attach and
|
|
@@ -46,18 +61,46 @@ export interface PendingDeltas {
|
|
|
46
61
|
touch(rec: RegistrationRecord): void;
|
|
47
62
|
/** Queue an eviction ref and drop any pending record for the same key (a stale record can't also re-advertise). */
|
|
48
63
|
evicted(rec: RegistrationRecord): void;
|
|
64
|
+
/**
|
|
65
|
+
* Queue a child-cohort **link** for replication (keyed by `(topicId, childCohortCoord)`; last write wins on
|
|
66
|
+
* `effectiveAt`). Enqueued by the parent engine only when the local child registry actually changed, so a
|
|
67
|
+
* stale/no-op record is not re-gossiped. A link and a later unlink for the same child in one round collapse
|
|
68
|
+
* to whichever carries the newer `effectiveAt`.
|
|
69
|
+
*/
|
|
70
|
+
childLink(topicId: Uint8Array, childCohortCoord: Uint8Array, effectiveAt: number): void;
|
|
71
|
+
/** Queue a child-cohort **unlink** (a released/demoted child) for replication; same key + last-writer-wins as {@link childLink}. */
|
|
72
|
+
childUnlink(topicId: Uint8Array, childCohortCoord: Uint8Array, effectiveAt: number): void;
|
|
49
73
|
/** True iff nothing is queued. */
|
|
50
74
|
isEmpty(): boolean;
|
|
51
75
|
/** Drain the queue into wire-shaped deltas, clearing it. */
|
|
52
|
-
drain(): { records: GossipRecordV1[]; evicted: GossipRecordRefV1[] };
|
|
76
|
+
drain(): { records: GossipRecordV1[]; evicted: GossipRecordRefV1[]; childLinks: ChildLinkRefV1[]; childUnlinks: ChildLinkRefV1[] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A queued child link/unlink: the wire ref plus whether it is a link (`true`) or an unlink (`false`). */
|
|
80
|
+
interface PendingChildDelta {
|
|
81
|
+
ref: ChildLinkRefV1;
|
|
82
|
+
linked: boolean;
|
|
53
83
|
}
|
|
54
84
|
|
|
55
85
|
/** Build an empty {@link PendingDeltas} queue. */
|
|
56
86
|
export function createPendingDeltas(): PendingDeltas {
|
|
57
87
|
const records = new Map<string, RegistrationRecord>();
|
|
58
88
|
const evicted = new Map<string, GossipRecordRefV1>();
|
|
89
|
+
const childDeltas = new Map<string, PendingChildDelta>();
|
|
59
90
|
const keyOf = (topicId: Uint8Array, participantId: Uint8Array): string =>
|
|
60
91
|
`${bytesToB64url(topicId)}|${bytesToB64url(participantId)}`;
|
|
92
|
+
// A child delta and its later opposite (link→unlink) share one key so the round drains only the newest.
|
|
93
|
+
const childKeyOf = (topicId: Uint8Array, childCohortCoord: Uint8Array): string =>
|
|
94
|
+
`${bytesToB64url(topicId)}|${bytesToB64url(childCohortCoord)}`;
|
|
95
|
+
const queueChild = (topicId: Uint8Array, childCohortCoord: Uint8Array, effectiveAt: number, linked: boolean): void => {
|
|
96
|
+
const key = childKeyOf(topicId, childCohortCoord);
|
|
97
|
+
const held = childDeltas.get(key);
|
|
98
|
+
// Last-writer-wins on effectiveAt: never let an older link/unlink shadow a newer one queued the same round.
|
|
99
|
+
if (held !== undefined && effectiveAt < held.ref.effectiveAt) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
childDeltas.set(key, { ref: { topicId: bytesToB64url(topicId), childCohortCoord: bytesToB64url(childCohortCoord), effectiveAt }, linked });
|
|
103
|
+
};
|
|
61
104
|
return {
|
|
62
105
|
touch(rec: RegistrationRecord): void {
|
|
63
106
|
const key = keyOf(rec.topicId, rec.participantId);
|
|
@@ -70,19 +113,35 @@ export function createPendingDeltas(): PendingDeltas {
|
|
|
70
113
|
},
|
|
71
114
|
evicted(rec: RegistrationRecord): void {
|
|
72
115
|
const key = keyOf(rec.topicId, rec.participantId);
|
|
73
|
-
evicted
|
|
116
|
+
// Stamp the evicted record's lastPing so the receiver can gate the delete on freshness (a stale
|
|
117
|
+
// eviction must not delete a record the participant has since re-registered — see bus.mergeRecords).
|
|
118
|
+
evicted.set(key, { topicId: bytesToB64url(rec.topicId), participantId: bytesToB64url(rec.participantId), lastPing: rec.lastPing });
|
|
74
119
|
records.delete(key);
|
|
75
120
|
},
|
|
121
|
+
childLink(topicId: Uint8Array, childCohortCoord: Uint8Array, effectiveAt: number): void {
|
|
122
|
+
queueChild(topicId, childCohortCoord, effectiveAt, true);
|
|
123
|
+
},
|
|
124
|
+
childUnlink(topicId: Uint8Array, childCohortCoord: Uint8Array, effectiveAt: number): void {
|
|
125
|
+
queueChild(topicId, childCohortCoord, effectiveAt, false);
|
|
126
|
+
},
|
|
76
127
|
isEmpty(): boolean {
|
|
77
|
-
return records.size === 0 && evicted.size === 0;
|
|
128
|
+
return records.size === 0 && evicted.size === 0 && childDeltas.size === 0;
|
|
78
129
|
},
|
|
79
|
-
drain(): { records: GossipRecordV1[]; evicted: GossipRecordRefV1[] } {
|
|
130
|
+
drain(): { records: GossipRecordV1[]; evicted: GossipRecordRefV1[]; childLinks: ChildLinkRefV1[]; childUnlinks: ChildLinkRefV1[] } {
|
|
131
|
+
const childLinks: ChildLinkRefV1[] = [];
|
|
132
|
+
const childUnlinks: ChildLinkRefV1[] = [];
|
|
133
|
+
for (const delta of childDeltas.values()) {
|
|
134
|
+
(delta.linked ? childLinks : childUnlinks).push(delta.ref);
|
|
135
|
+
}
|
|
80
136
|
const out = {
|
|
81
137
|
records: [...records.values()].map(toGossipRecord),
|
|
82
138
|
evicted: [...evicted.values()],
|
|
139
|
+
childLinks,
|
|
140
|
+
childUnlinks,
|
|
83
141
|
};
|
|
84
142
|
records.clear();
|
|
85
143
|
evicted.clear();
|
|
144
|
+
childDeltas.clear();
|
|
86
145
|
return out;
|
|
87
146
|
},
|
|
88
147
|
};
|
|
@@ -96,6 +155,16 @@ export interface GossipFrameInputs {
|
|
|
96
155
|
readonly coord: string;
|
|
97
156
|
/** This cohort's epoch, base64url. */
|
|
98
157
|
readonly cohortEpoch: string;
|
|
158
|
+
/** This cohort's tree tier `d` — carried on the frame so a cold sibling instantiates at the right tier. */
|
|
159
|
+
readonly treeTier: number;
|
|
160
|
+
/**
|
|
161
|
+
* True to emit a **willingness-only heartbeat** even when this engine is idle (no resident topics, no
|
|
162
|
+
* pending deltas) — provided the node is actually willing for some tier. Lets an idle-but-willing engine
|
|
163
|
+
* still tell siblings it will serve, so a cold cohort can bootstrap (§Cold-start instantiation). The caller
|
|
164
|
+
* (the gossip round) sets this from the per-engine heartbeat clock; a non-idle round ignores it (it already
|
|
165
|
+
* ships willingness).
|
|
166
|
+
*/
|
|
167
|
+
readonly heartbeat: boolean;
|
|
99
168
|
/** This node's tier profile (the `∧` half of the willingness vector). */
|
|
100
169
|
readonly profile: NodeProfile;
|
|
101
170
|
/** This member's load barometer (load buckets + the load-shed half of willingness). */
|
|
@@ -108,18 +177,29 @@ export interface GossipFrameInputs {
|
|
|
108
177
|
readonly records: GossipRecordV1[];
|
|
109
178
|
/** Drained eviction refs for this round. */
|
|
110
179
|
readonly evicted: GossipRecordRefV1[];
|
|
180
|
+
/** Drained child-cohort link refs for this round (cross-member child-set convergence). */
|
|
181
|
+
readonly childLinks: ChildLinkRefV1[];
|
|
182
|
+
/** Drained child-cohort unlink refs for this round (a released/demoted child). */
|
|
183
|
+
readonly childUnlinks: ChildLinkRefV1[];
|
|
111
184
|
/** Round timestamp, unix ms. */
|
|
112
185
|
readonly timestamp: number;
|
|
113
186
|
}
|
|
114
187
|
|
|
115
188
|
/**
|
|
116
|
-
* Assemble one round's {@link CohortGossipV1}, or `undefined` when this engine
|
|
117
|
-
*
|
|
118
|
-
* empty engines cost no gossip)
|
|
189
|
+
* Assemble one round's {@link CohortGossipV1}, or `undefined` when this engine has nothing to say. An engine
|
|
190
|
+
* is **idle** when it holds no resident topics and no pending deltas. An idle engine normally builds no frame
|
|
191
|
+
* (idle empty engines cost no gossip) — **except** on a willingness heartbeat ({@link GossipFrameInputs.heartbeat}),
|
|
192
|
+
* where an idle engine that is willing for at least one tier (`selfWillingnessBits !== 0`) still emits a
|
|
193
|
+
* willingness/load-only frame (empty `topicSummaries`, no `records`/`evicted`) so siblings can hear it and a
|
|
194
|
+
* cold cohort can bootstrap (§Cold-start instantiation). An idle-and-unwilling engine stays silent even on a
|
|
195
|
+
* heartbeat (nothing to bootstrap). `willingnessBits` is `profile ∧ load` ({@link selfWillingnessBits}); the
|
|
119
196
|
* `signature` slot is left empty for the host's peer-key signer to fill before broadcast.
|
|
120
197
|
*/
|
|
121
198
|
export function buildCohortGossip(i: GossipFrameInputs): CohortGossipV1 | undefined {
|
|
122
|
-
|
|
199
|
+
const willingness = selfWillingnessBits(i.profile, i.barometer);
|
|
200
|
+
const idle = i.topicSummaries.length === 0 && i.records.length === 0 && i.evicted.length === 0
|
|
201
|
+
&& i.childLinks.length === 0 && i.childUnlinks.length === 0;
|
|
202
|
+
if (idle && !(i.heartbeat && willingness !== 0)) {
|
|
123
203
|
return undefined;
|
|
124
204
|
}
|
|
125
205
|
const g: CohortGossipV1 = {
|
|
@@ -127,7 +207,8 @@ export function buildCohortGossip(i: GossipFrameInputs): CohortGossipV1 | undefi
|
|
|
127
207
|
fromMember: i.fromMember,
|
|
128
208
|
coord: i.coord,
|
|
129
209
|
cohortEpoch: i.cohortEpoch,
|
|
130
|
-
|
|
210
|
+
treeTier: i.treeTier,
|
|
211
|
+
willingnessBits: willingnessBitsHex(willingness),
|
|
131
212
|
loadBuckets: i.barometer.loadBuckets(),
|
|
132
213
|
windowSeconds: i.windowSeconds,
|
|
133
214
|
topicSummaries: i.topicSummaries,
|
|
@@ -140,5 +221,11 @@ export function buildCohortGossip(i: GossipFrameInputs): CohortGossipV1 | undefi
|
|
|
140
221
|
if (i.evicted.length > 0) {
|
|
141
222
|
g.evicted = i.evicted;
|
|
142
223
|
}
|
|
224
|
+
if (i.childLinks.length > 0) {
|
|
225
|
+
g.childLinks = i.childLinks;
|
|
226
|
+
}
|
|
227
|
+
if (i.childUnlinks.length > 0) {
|
|
228
|
+
g.childUnlinks = i.childUnlinks;
|
|
229
|
+
}
|
|
143
230
|
return g;
|
|
144
231
|
}
|
|
@@ -64,6 +64,16 @@ export class FretCohortGossipTransport implements ICohortGossipTransport {
|
|
|
64
64
|
return () => this.handlers.delete(handler);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Number of live inbound subscriptions (one per subscribed gossip bus). Test/diagnostic introspection:
|
|
69
|
+
* each {@link CoordEngine}'s bus subscribes via {@link onMessage} and drops its subscription on `close()`,
|
|
70
|
+
* so this tracks the number of live coord engines (+ the node-level participant bus) — used to observe that
|
|
71
|
+
* the coord-engine registry's LRU eviction actually tears evicted engines down (no leaked subscriptions).
|
|
72
|
+
*/
|
|
73
|
+
get subscriberCount(): number {
|
|
74
|
+
return this.handlers.size;
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
/** Feed an inbound gossip frame (called by the host's `/cohort-gossip` protocol handler). */
|
|
68
78
|
deliver(fromPeerId: string, msg: Uint8Array): void {
|
|
69
79
|
const from: PeerRef = { id: peerIdToBytes(fromPeerId) };
|