@optimystic/db-p2p 0.14.0 → 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
|
@@ -13,20 +13,24 @@ import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
|
13
13
|
import { clusterService } from './cluster/service.js';
|
|
14
14
|
import { blockTransferService } from './cluster/block-transfer-service.js';
|
|
15
15
|
import { repoService } from './repo/service.js';
|
|
16
|
-
import { StorageRepo } from './storage/storage-repo.js';
|
|
16
|
+
import { StorageRepo, withBlockCommitLatch } from './storage/storage-repo.js';
|
|
17
17
|
import { BlockStorage } from './storage/block-storage.js';
|
|
18
18
|
import { MemoryRawStorage } from './storage/memory-storage.js';
|
|
19
|
+
import { seedOwnedBlocksFromStorage } from './owned-block-seed.js';
|
|
19
20
|
import { clusterMember } from './cluster/cluster-repo.js';
|
|
21
|
+
import { selectQuorumRev, selectQuorumBlock, canonicalBlockHash } from './cluster/quorum-restore.js';
|
|
20
22
|
import { createCommitCertStore, makeClusterCommitCertExtractor } from './cluster/commit-cert.js';
|
|
21
23
|
import { coordinatorRepo } from './repo/coordinator-repo.js';
|
|
22
24
|
import { Libp2pKeyPeerNetwork } from './libp2p-key-network.js';
|
|
23
25
|
import { ClusterClient } from './cluster/client.js';
|
|
24
26
|
import { networkManagerService } from './network/network-manager-service.js';
|
|
25
|
-
import {
|
|
27
|
+
import { BlockTransferCoordinator } from './cluster/block-transfer.js';
|
|
28
|
+
import { fretService, Libp2pFretService } from 'p2p-fret';
|
|
26
29
|
import { syncService } from './sync/service.js';
|
|
27
30
|
import { SyncClient } from './sync/client.js';
|
|
28
|
-
import { RestorationCoordinator } from './storage/restoration-coordinator
|
|
31
|
+
import { RestorationCoordinator } from './storage/restoration-coordinator.js';
|
|
29
32
|
import { RingSelector } from './storage/ring-selector.js';
|
|
33
|
+
import { RingShiftCoordinator } from './storage/ring-shift-coordinator.js';
|
|
30
34
|
import { StorageMonitor } from './storage/storage-monitor.js';
|
|
31
35
|
import { ArachnodeFretAdapter } from './storage/arachnode-fret-adapter.js';
|
|
32
36
|
import { createCohortTopicHost } from './cohort-topic/host.js';
|
|
@@ -43,14 +47,24 @@ import { DEFAULT_REACTIVITY_PROTOCOLS, reactivityProtocolList } from './reactivi
|
|
|
43
47
|
import { registerMatchmakingQueryHandler } from './matchmaking/query-transport.js';
|
|
44
48
|
import { DEFAULT_MATCHMAKING_PROTOCOLS, matchmakingProtocolList } from './matchmaking/protocols.js';
|
|
45
49
|
import { signPeer } from './cohort-topic/peer-sig.js';
|
|
46
|
-
import { createNotificationVerifier, createCorrelationReplayGuard, createStickyCohortHintCache, reactivityNodePolicy, createTierAddressing, createRingHash, Tier, b64urlToBytes, bytesToB64url, } from '@optimystic/db-core';
|
|
50
|
+
import { createNotificationVerifier, createCorrelationReplayGuard, createStickyCohortHintCache, reactivityNodePolicy, createTierAddressing, createRingHash, DEFAULT_SUPER_MAJORITY_THRESHOLD, Tier, b64urlToBytes, bytesToB64url, } from '@optimystic/db-core';
|
|
47
51
|
import { PartitionDetector } from './cluster/partition-detector.js';
|
|
52
|
+
import { assertSuperMajorityCoupling } from './cluster/supermajority-coupling.js';
|
|
48
53
|
import { createLogger } from './logger.js';
|
|
49
54
|
import { PeerReputationService } from './reputation/peer-reputation.js';
|
|
55
|
+
import { PenaltyReason } from './reputation/types.js';
|
|
50
56
|
import { DisputeService } from './dispute/dispute-service.js';
|
|
51
57
|
import { DisputeClient } from './dispute/client.js';
|
|
58
|
+
import { sampleArbitrators } from './dispute/arbitrator-selection.js';
|
|
52
59
|
/** Logger for the reactivity node-wiring (origination/forwarder/recover/rotation composition). */
|
|
53
60
|
const reactivityWiringLog = createLogger('reactivity-node-wiring');
|
|
61
|
+
/**
|
|
62
|
+
* Logger for the best-effort in-factory service wiring. These injections run during `createLibp2p`
|
|
63
|
+
* internals against the unreliable `components.libp2p` proxy; the real node is re-injected
|
|
64
|
+
* post-construction (see the load-bearing block after `createLibp2p`), so a failure here is logged,
|
|
65
|
+
* not fatal.
|
|
66
|
+
*/
|
|
67
|
+
const wiringLog = createLogger('node-wiring');
|
|
54
68
|
function resolveStorage(provider) {
|
|
55
69
|
if (!provider) {
|
|
56
70
|
return new MemoryRawStorage();
|
|
@@ -96,6 +110,14 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
96
110
|
};
|
|
97
111
|
// Create shared storage layers with restoration callback
|
|
98
112
|
const storageRepo = new StorageRepo((blockId) => new BlockStorage(blockId, rawStorage, restoreCallback));
|
|
113
|
+
// Per-block commit-latch runner, ready to thread into the invalidation-apply sink (`onInvalidate`)
|
|
114
|
+
// passed to `clusterMember(...)` and into each cascade `CollectionEnv`, the instant either is wired
|
|
115
|
+
// here. Sharing the `StorageRepo.commit:<blockId>` latch makes a compensating saveReplica/saveDeletion
|
|
116
|
+
// RMW of `meta.latest` mutually exclusive with a concurrent commit on the same block. It is unused
|
|
117
|
+
// today only because no `onInvalidate`/cascade driver is wired in the live node (see review handoff);
|
|
118
|
+
// it is bound here so that wiring is a one-liner and cannot reach for a divergent latch key.
|
|
119
|
+
const blockCommitLatch = withBlockCommitLatch;
|
|
120
|
+
void blockCommitLatch;
|
|
99
121
|
let clusterImpl;
|
|
100
122
|
let coordinatedRepo;
|
|
101
123
|
const clusterProxy = {
|
|
@@ -150,17 +172,27 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
150
172
|
listen: listenAddrs
|
|
151
173
|
},
|
|
152
174
|
connectionManager: {
|
|
153
|
-
autoDial
|
|
154
|
-
|
|
175
|
+
// `autoDial`, `minConnections`, and `dialQueue` were stale libp2p option keys silently
|
|
176
|
+
// ignored under the former `libp2pOptions as any` (removed with this change). This libp2p
|
|
177
|
+
// version has no such keys — auto-dial is now default connection-manager behavior with no
|
|
178
|
+
// direct replacement — so they are dropped rather than re-cast. See review handoff.
|
|
155
179
|
maxConnections: 16,
|
|
156
|
-
inboundConnectionUpgradeTimeout
|
|
157
|
-
|
|
180
|
+
// Renamed from the stale `inboundConnectionUpgradeTimeout`. 10_000 equals this version's
|
|
181
|
+
// default, so surfacing (and correcting) the key is behavior-preserving; the old key was a no-op.
|
|
182
|
+
inboundUpgradeTimeout: 10_000
|
|
158
183
|
},
|
|
159
184
|
...(options.connectionGater ? { connectionGater: options.connectionGater } : {}),
|
|
160
185
|
transports,
|
|
161
186
|
connectionEncrypters: [noise()],
|
|
162
187
|
streamMuxers: [yamux()],
|
|
163
|
-
services:
|
|
188
|
+
// Narrow cast confined to the `services` field: the built-in factories (identify/dcutr/…) are
|
|
189
|
+
// typed against a SECOND copy of `@libp2p/interface` pulled in transitively (via `@libp2p/crypto`),
|
|
190
|
+
// whose `Uint8Array<ArrayBuffer>` vs `<ArrayBufferLike>` PeerId/key shapes are structurally
|
|
191
|
+
// incompatible with the top-level copy — a dependency-dedup artifact, not a real mismatch. The cast
|
|
192
|
+
// stays on this field alone so the rest of `libp2pOptions` remains fully typed as `Libp2pInit`.
|
|
193
|
+
// NOTE: this cast exists ONLY because of the duplicate @libp2p/interface install; if that dedups
|
|
194
|
+
// (or on a libp2p bump) drop `as unknown as NonNullable<Libp2pInit['services']>` and type the map directly.
|
|
195
|
+
services: ({
|
|
164
196
|
identify: identify({
|
|
165
197
|
protocolPrefix: `/optimystic/${options.networkName}`
|
|
166
198
|
}),
|
|
@@ -255,10 +287,13 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
255
287
|
clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5
|
|
256
288
|
});
|
|
257
289
|
const svc = svcFactory(components);
|
|
290
|
+
// Best-effort proxy-time injection; the real node is re-injected post-construction below.
|
|
258
291
|
try {
|
|
259
|
-
svc.setLibp2p
|
|
292
|
+
svc.setLibp2p(components.libp2p);
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
wiringLog('networkManager in-factory setLibp2p failed (proxy); real node injected post-construction: %o', err);
|
|
260
296
|
}
|
|
261
|
-
catch { }
|
|
262
297
|
return svc;
|
|
263
298
|
},
|
|
264
299
|
fret: (components) => {
|
|
@@ -271,60 +306,79 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
271
306
|
bootstraps: options.bootstrapNodes ?? []
|
|
272
307
|
});
|
|
273
308
|
const svc = svcFactory(components);
|
|
309
|
+
// Best-effort proxy-time injection; the real node is re-injected post-construction below.
|
|
274
310
|
try {
|
|
275
311
|
svc.setLibp2p(components.libp2p);
|
|
276
312
|
}
|
|
277
|
-
catch {
|
|
313
|
+
catch (err) {
|
|
314
|
+
wiringLog('fret in-factory setLibp2p failed (proxy); real node injected post-construction: %o', err);
|
|
315
|
+
}
|
|
278
316
|
return svc;
|
|
279
317
|
}
|
|
280
|
-
|
|
318
|
+
// [dispute-subsystem-dormant] The /optimystic/<network>/dispute/1.0.0 handler
|
|
319
|
+
// (disputeProtocolService / DisputeProtocolService) is intentionally NOT registered here.
|
|
320
|
+
// The subsystem is staged dormant pending arbitrator-set anchoring — without it, a peer
|
|
321
|
+
// minting throwaway keypairs can forge a synthetic super-majority and pass resolution.
|
|
322
|
+
// Gate: tickets/backlog/hardening/invalidation-live-wiring-requires-arbitrator-set-anchoring
|
|
323
|
+
// Wiring plan: tickets/backlog/feat-dispute-subsystem-live-activation
|
|
324
|
+
}),
|
|
281
325
|
// Add bootstrap nodes as needed
|
|
282
326
|
peerDiscovery: [
|
|
283
327
|
...(options.bootstrapNodes?.length ? [bootstrap({ list: options.bootstrapNodes })] : [])
|
|
284
328
|
],
|
|
285
329
|
};
|
|
286
330
|
const node = await createLibp2p(libp2pOptions);
|
|
287
|
-
// Inject libp2p
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
catch { }
|
|
331
|
+
// Inject the REAL libp2p node into the services that need it, before start(). These are
|
|
332
|
+
// load-bearing and the node has NOT started yet, so any throw fails fast and rejects node
|
|
333
|
+
// creation (nothing started leaks) — far better than the service silently falling back to the
|
|
334
|
+
// unreliable `components.libp2p` proxy and surfacing later as routing/consensus failures.
|
|
335
|
+
const wired = node.services;
|
|
336
|
+
wired.fret.setLibp2p(node);
|
|
337
|
+
wired.networkManager.setLibp2p(node);
|
|
296
338
|
// RepoService.checkRedirect resolves the network manager / self id / connection
|
|
297
339
|
// addrs through this injected node (the components.libp2p proxy is unreliable
|
|
298
340
|
// from inside a service at request time). Done before start() so the protocol
|
|
299
341
|
// handler is live with a resolvable node from its first request.
|
|
300
|
-
|
|
301
|
-
node.services?.repo?.setLibp2p?.(node);
|
|
302
|
-
}
|
|
303
|
-
catch { }
|
|
342
|
+
wired.repo.setLibp2p(node);
|
|
304
343
|
await node.start();
|
|
305
344
|
// Initialize peer reputation service
|
|
306
345
|
const reputation = new PeerReputationService();
|
|
307
346
|
// Initialize cluster coordination components
|
|
308
347
|
const networkMode = (options.bootstrapNodes?.length ?? 0) > 0 ? 'joining' : 'forming';
|
|
309
|
-
|
|
310
|
-
|
|
348
|
+
// Network-namespaced protocol prefix, threaded into the key network so coordinator/
|
|
349
|
+
// cohort selection is scoped to peers that serve THIS network's cluster/repo protocol.
|
|
350
|
+
// A peer that only belongs to another network sharing the same physical nodes/
|
|
351
|
+
// bootstraps registers a different (network-namespaced) identify protocol, so it is
|
|
352
|
+
// never selected and can't drag this network's super-majority below quorum.
|
|
311
353
|
const protocolPrefix = `/optimystic/${options.networkName}`;
|
|
354
|
+
const keyNetwork = new Libp2pKeyPeerNetwork(node, options.clusterSize, undefined, networkMode, options.persistence, reputation, protocolPrefix);
|
|
355
|
+
await keyNetwork.initFromPersistedState();
|
|
312
356
|
const createClusterClient = (peerId) => ClusterClient.create(peerId, keyNetwork, protocolPrefix);
|
|
313
|
-
// Inject reputation into NetworkManagerService
|
|
357
|
+
// Inject reputation into NetworkManagerService. Load-bearing and non-optional: the service is
|
|
358
|
+
// unconditionally present, so a throw is a real wiring bug. Unlike the pre-start injections above
|
|
359
|
+
// the node has already started here, so stop it before rethrowing rather than leaking a started
|
|
360
|
+
// node + open transports (mirrors the cohortTopic hard-fail blocks below).
|
|
314
361
|
try {
|
|
315
|
-
|
|
362
|
+
wired.networkManager.setReputation(reputation);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
await node.stop();
|
|
366
|
+
throw err;
|
|
316
367
|
}
|
|
317
|
-
catch { }
|
|
318
368
|
// Create partition detector and get FRET service
|
|
319
369
|
const partitionDetector = new PartitionDetector();
|
|
320
370
|
const fretSvc = node.services?.fret;
|
|
321
371
|
const consensusConfig = {
|
|
322
|
-
superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ??
|
|
372
|
+
superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? DEFAULT_SUPER_MAJORITY_THRESHOLD,
|
|
323
373
|
simpleMajorityThreshold: 0.51,
|
|
324
374
|
minAbsoluteClusterSize: 2,
|
|
325
375
|
allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
|
|
326
376
|
clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
|
|
327
|
-
partitionDetectionWindow: 60000
|
|
377
|
+
partitionDetectionWindow: 60000,
|
|
378
|
+
// Configured full cluster size — the member's own reference for "full size" in the membership
|
|
379
|
+
// admission gate (a below-full-size declared set under low FRET confidence is refused as a possible
|
|
380
|
+
// self-shrink). Matches the size threaded into the coordinator below.
|
|
381
|
+
clusterSize: options.clusterSize ?? 10
|
|
328
382
|
};
|
|
329
383
|
// Fetch a block archive from one cohort peer over the sync protocol, bounded by a
|
|
330
384
|
// per-peer timeout so an unreachable peer can't stall reconciliation. Mirrors the
|
|
@@ -361,9 +415,16 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
361
415
|
const targets = cohortPeerIds.filter(id => id !== node.peerId.toString());
|
|
362
416
|
if (targets.length === 0)
|
|
363
417
|
return;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
418
|
+
const fetched = await Promise.all(targets.map(async (peerIdStr) => ({ peerIdStr, archive: await fetchArchiveFromPeer(peerIdStr, blockId) })));
|
|
419
|
+
// Each cohort archive contributes one (rev, actionId) claim from its max
|
|
420
|
+
// revision (>= the rev we committed). Pick the target rev by quorum
|
|
421
|
+
// corroboration rather than raw Math.max — a lone peer inflating its rev
|
|
422
|
+
// cannot steer reconciliation. Keep the serving peer + block per candidate
|
|
423
|
+
// so we can then verify content agreement.
|
|
424
|
+
// NOTE: this quorum is corroboration-of-a-claim, NOT Sybil-resistant cohort
|
|
425
|
+
// membership — deferred to backlog `debt-read-repair-commit-cert-verification`.
|
|
426
|
+
const candidates = [];
|
|
427
|
+
for (const { peerIdStr, archive } of fetched) {
|
|
367
428
|
if (!archive)
|
|
368
429
|
continue;
|
|
369
430
|
const revs = Object.keys(archive.revisions).map(Number);
|
|
@@ -373,15 +434,57 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
373
434
|
if (maxRev < committed.rev)
|
|
374
435
|
continue;
|
|
375
436
|
const data = archive.revisions[maxRev];
|
|
376
|
-
if (!data?.
|
|
437
|
+
if (!data?.action)
|
|
377
438
|
continue;
|
|
378
|
-
|
|
379
|
-
|
|
439
|
+
candidates.push({ peerIdStr, rev: maxRev, actionId: data.action.actionId, block: data.block });
|
|
440
|
+
}
|
|
441
|
+
const revClaims = candidates.map(c => ({ peerId: c.peerIdStr, rev: c.rev, actionId: c.actionId }));
|
|
442
|
+
const selected = selectQuorumRev(revClaims, consensusConfig.simpleMajorityThreshold);
|
|
443
|
+
if (!selected)
|
|
444
|
+
return; // no rev corroborated by a quorum → leave block, churn/rebalance retries later
|
|
445
|
+
// Content agreement: among archives corroborating the chosen (rev, actionId)
|
|
446
|
+
// and actually carrying the block, the content must be byte-identical across
|
|
447
|
+
// a quorum. A cohort member serving content that hashes differently is rejected.
|
|
448
|
+
// NOTE: selectQuorumBlock recomputes its quorum over only the block-CARRYING
|
|
449
|
+
// corroborators, not the full rev-responder set. If most peers corroborate the
|
|
450
|
+
// rev but few carry block bytes (e.g. mid-prune), the content quorum can shrink
|
|
451
|
+
// to 2. Harmless with honest peers; if a colluding pair ever becomes the only
|
|
452
|
+
// block-servers for an agreed rev, that is the Sybil regime already deferred to
|
|
453
|
+
// backlog `debt-read-repair-commit-cert-verification`.
|
|
454
|
+
const corroborating = candidates.filter(c => c.rev === selected.rev && c.actionId === selected.actionId && c.block);
|
|
455
|
+
const hashCandidates = await Promise.all(corroborating.map(async (c) => ({ peerId: c.peerIdStr, hash: await canonicalBlockHash(c.block), block: c.block })));
|
|
456
|
+
const agreed = selectQuorumBlock(hashCandidates, consensusConfig.simpleMajorityThreshold);
|
|
457
|
+
if (!agreed)
|
|
458
|
+
return; // no content quorum → skip persist
|
|
459
|
+
// Best-effort: penalize cohort members that served content contradicting the
|
|
460
|
+
// agreed hash for the same committed (rev, actionId). Never let this throw.
|
|
461
|
+
try {
|
|
462
|
+
for (const c of hashCandidates) {
|
|
463
|
+
if (c.hash !== agreed.hash) {
|
|
464
|
+
reputation.reportPeer(c.peerId, PenaltyReason.InvalidRestoration, `reconcile:${blockId}`);
|
|
465
|
+
}
|
|
380
466
|
}
|
|
381
467
|
}
|
|
382
|
-
|
|
383
|
-
|
|
468
|
+
catch { /* reputation write must never block restoration */ }
|
|
469
|
+
await storageRepo.saveReplicatedBlock(blockId, agreed.block, { actionId: selected.actionId, rev: selected.rev });
|
|
470
|
+
};
|
|
471
|
+
// Member-side membership derivation for the admission gate: independently re-derive this block's
|
|
472
|
+
// responsible cluster from the SAME source the coordinator uses (IKeyNetwork.findCluster), plus FRET's
|
|
473
|
+
// network-size confidence. A member gates a coordinator-declared peer set against this view before
|
|
474
|
+
// voting, so a self-shrunk minority-partition set cannot be voted into super-majority (see cluster-repo
|
|
475
|
+
// admitMembership). No FRET ⇒ confidence 0 ⇒ the gate fails closed for any downsize.
|
|
476
|
+
const deriveExpectedCluster = async (blockId) => {
|
|
477
|
+
const peers = await keyNetwork.findCluster(new TextEncoder().encode(blockId));
|
|
478
|
+
let confidence = 0;
|
|
479
|
+
if (fretSvc) {
|
|
480
|
+
try {
|
|
481
|
+
confidence = fretSvc.getNetworkSizeEstimate().confidence;
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// Leave confidence 0 → fail closed for downsizing.
|
|
485
|
+
}
|
|
384
486
|
}
|
|
487
|
+
return { peers: peers ?? {}, confidence };
|
|
385
488
|
};
|
|
386
489
|
clusterImpl = clusterMember({
|
|
387
490
|
storageRepo,
|
|
@@ -396,7 +499,8 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
396
499
|
consensusConfig,
|
|
397
500
|
stateStore: options.transactionStateStore,
|
|
398
501
|
reconcileBlock,
|
|
399
|
-
onCommitCertificate
|
|
502
|
+
onCommitCertificate,
|
|
503
|
+
deriveExpectedCluster
|
|
400
504
|
// `recomputeArbitratorSet` (invalidation layer-2) is intentionally NOT wired here yet: a live FRET
|
|
401
505
|
// recompute needs a churn-tolerance window so it does not false-reject legitimate certificates from
|
|
402
506
|
// late-joiners (a liveness regression). Until that is tuned against live topology — and the
|
|
@@ -405,7 +509,7 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
405
509
|
// `verifyInvalidationCertificate` and `tickets/plan/cohort-topic-membership-cert-trust-anchoring.md`.
|
|
406
510
|
});
|
|
407
511
|
const coordinatorRepoFactory = coordinatorRepo(keyNetwork, createClusterClient, {
|
|
408
|
-
|
|
512
|
+
// clusterSize is now part of consensusConfig (member + coordinator share one reference).
|
|
409
513
|
...consensusConfig
|
|
410
514
|
}, fretSvc, reputation, options.transactionStateStore);
|
|
411
515
|
// Create callback for querying cluster peers for their latest block revision
|
|
@@ -448,36 +552,68 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
448
552
|
localPeerId: node.peerId,
|
|
449
553
|
clusterLatestCallback
|
|
450
554
|
});
|
|
555
|
+
// Fail-fast coupling: the cluster member (what accepts a super-majority as sufficient) and the
|
|
556
|
+
// coordinator (what declares a transaction committed on that super-majority) MUST run the same
|
|
557
|
+
// threshold, or the node would come up able to disagree with itself mid-consensus. Both are fed from
|
|
558
|
+
// the single `consensusConfig` above; this asserts on their RESOLVED values so any future drift throws
|
|
559
|
+
// HERE at construction. See `assertSuperMajorityCoupling`.
|
|
560
|
+
assertSuperMajorityCoupling(clusterImpl, coordinatedRepo);
|
|
451
561
|
// Recover persisted transaction state before accepting new requests
|
|
452
562
|
if (options.transactionStateStore) {
|
|
453
563
|
await clusterImpl.recoverTransactions();
|
|
454
564
|
await coordinatedRepo.recoverTransactions();
|
|
455
565
|
}
|
|
566
|
+
// --- Shared owned-block set for the resilience monitors ---
|
|
567
|
+
// SpreadOnChurnMonitor (sender) and RebalanceMonitor (responsibility tracker) both act on "the
|
|
568
|
+
// blocks this node physically holds". They share ONE Set so the two can never drift: a single
|
|
569
|
+
// owned-block feed populates it, and the rebalance responsibility-loss signal evicts from it
|
|
570
|
+
// (in the rebalance block below). Both monitors take this exact instance via deps.trackedBlocks.
|
|
571
|
+
const networkManager = node.services?.networkManager;
|
|
572
|
+
const ownedBlocks = new Set();
|
|
573
|
+
// Single owned-block feed: every block this node commits OR receives as a replica fires
|
|
574
|
+
// storageRepo.onAnyCollectionChange. Subscribe to storageRepo DIRECTLY (not
|
|
575
|
+
// node.blockChangeNotifier): the cohort-topic activation block below may replace
|
|
576
|
+
// blockChangeNotifier with a decorating bridge, but storageRepo keeps emitting on its own
|
|
577
|
+
// surface regardless of that opt-in. NOTE: this feed does NOT re-emit blocks already durable
|
|
578
|
+
// from a previous run; those are seeded once at startup by the storage-enumeration scan wired
|
|
579
|
+
// below (seedOwnedBlocksFromStorage), so a restarted node protects on-disk data without waiting
|
|
580
|
+
// for each block to be touched again. Registered lazily the first time a
|
|
581
|
+
// monitor that reads ownedBlocks is wired, so when BOTH monitors are disabled no subscription
|
|
582
|
+
// leaks; torn down exactly once in the stop wrapper below.
|
|
583
|
+
let offOwnedBlockFeed;
|
|
584
|
+
const ensureOwnedBlockFeed = () => {
|
|
585
|
+
if (offOwnedBlockFeed)
|
|
586
|
+
return;
|
|
587
|
+
offOwnedBlockFeed = storageRepo.onAnyCollectionChange((e) => {
|
|
588
|
+
for (const blockId of e.blockIds)
|
|
589
|
+
ownedBlocks.add(blockId);
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
// Single owned-block-feed teardown. Registered up front (before either monitor's own stop
|
|
593
|
+
// wrapper) so it runs regardless of WHICH monitor subscribed the feed - including the
|
|
594
|
+
// spread-disabled / rebalance-only case. Idempotent: offOwnedBlockFeed is undefined-guarded.
|
|
595
|
+
{
|
|
596
|
+
const previousStop = node.stop.bind(node);
|
|
597
|
+
node.stop = async () => {
|
|
598
|
+
try {
|
|
599
|
+
offOwnedBlockFeed?.();
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
await previousStop();
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
456
606
|
// --- Churn-resilient spread: drive SpreadOnChurnMonitor on a live node ---
|
|
457
607
|
// Nothing previously activated the SENDING side of the churn-resilient spread protocol on a
|
|
458
|
-
// real node. Here we init + start the monitor and
|
|
459
|
-
//
|
|
460
|
-
// receiver durably persists each push via saveReplicatedBlock).
|
|
461
|
-
const networkManager = node.services?.networkManager;
|
|
608
|
+
// real node. Here we init + start the monitor (sharing ownedBlocks) and ensure the single
|
|
609
|
+
// owned-block feed is live, so a debounced connection:close re-pushes the node's blocks to
|
|
610
|
+
// expansion-cohort peers (the receiver durably persists each push via saveReplicatedBlock).
|
|
462
611
|
let spreadMonitor;
|
|
463
|
-
let offOwnedBlockFeed;
|
|
464
612
|
if (networkManager && (options.spreadOnChurn?.enabled ?? true) !== false) {
|
|
465
613
|
try {
|
|
466
|
-
spreadMonitor = networkManager.initSpreadOnChurnMonitor(partitionDetector, storageRepo, keyNetwork, options.clusterSize ?? 10, protocolPrefix, options.spreadOnChurn);
|
|
614
|
+
spreadMonitor = networkManager.initSpreadOnChurnMonitor(partitionDetector, storageRepo, keyNetwork, options.clusterSize ?? 10, protocolPrefix, ownedBlocks, options.spreadOnChurn);
|
|
467
615
|
await spreadMonitor.start();
|
|
468
|
-
|
|
469
|
-
// storageRepo.onAnyCollectionChange. Subscribe to storageRepo DIRECTLY (not
|
|
470
|
-
// node.blockChangeNotifier): the cohort-topic activation block below may replace
|
|
471
|
-
// blockChangeNotifier with a decorating bridge, but storageRepo keeps emitting on its
|
|
472
|
-
// own surface regardless of that opt-in. NOTE: blocks already durable from a previous
|
|
473
|
-
// run are NOT re-emitted on startup, so they are not tracked until next touched -
|
|
474
|
-
// acceptable here (churn re-replication re-derives over time); an initial-scan is a
|
|
475
|
-
// follow-on enhancement.
|
|
476
|
-
const monitor = spreadMonitor;
|
|
477
|
-
offOwnedBlockFeed = storageRepo.onAnyCollectionChange((e) => {
|
|
478
|
-
for (const blockId of e.blockIds)
|
|
479
|
-
monitor.trackBlock(blockId);
|
|
480
|
-
});
|
|
616
|
+
ensureOwnedBlockFeed();
|
|
481
617
|
}
|
|
482
618
|
catch (err) {
|
|
483
619
|
// Spread is a resilience optimization, not a correctness requirement - a wiring
|
|
@@ -488,16 +624,15 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
488
624
|
}
|
|
489
625
|
// Expose for tests/diagnostics (mirrors node.keyNetwork / node.reputation).
|
|
490
626
|
node.spreadOnChurnMonitor = spreadMonitor;
|
|
491
|
-
// Disposal:
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
627
|
+
// Disposal: stop the spread monitor deterministically before the transports close. Composes
|
|
628
|
+
// with the arachnode / clusterMember / cohort-topic stop wrappers (each calls its captured
|
|
629
|
+
// previousStop last). Idempotent (SpreadOnChurnMonitor.stop early-returns when not running), so
|
|
630
|
+
// a double node.stop() does not throw. The owned-block feed teardown is the separate up-front
|
|
631
|
+
// wrapper above (shared across both monitors).
|
|
496
632
|
{
|
|
497
633
|
const previousStop = node.stop.bind(node);
|
|
498
634
|
node.stop = async () => {
|
|
499
635
|
try {
|
|
500
|
-
offOwnedBlockFeed?.();
|
|
501
636
|
if (spreadMonitor)
|
|
502
637
|
await spreadMonitor.stop();
|
|
503
638
|
}
|
|
@@ -512,14 +647,34 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
512
647
|
const log = node.logger?.forComponent?.('db-p2p:arachnode');
|
|
513
648
|
const fret = node.services?.fret;
|
|
514
649
|
if (fret) {
|
|
515
|
-
const fretAdapter = new ArachnodeFretAdapter(fret);
|
|
650
|
+
const fretAdapter = new ArachnodeFretAdapter(fret, node.peerId.toString());
|
|
651
|
+
// Blocks whose shed range has been RELEASED (Phase C of a ring shift, or a confirmed
|
|
652
|
+
// rebalance release). This is the GC-eligibility signal the future storage sweep
|
|
653
|
+
// (`st-storage-sweep-archival-and-capacity-estimate`) must consult: a block's local bytes may
|
|
654
|
+
// be reclaimed ONLY once it appears here, so an unconfirmed / still-served range is never
|
|
655
|
+
// swept. Populated strictly after replication is confirmed. See
|
|
656
|
+
// docs/arachnode-ring-handoff.md § Part 2 (Local bytes vs. tracking).
|
|
657
|
+
// NOTE: no sweep consumes this set yet; it is the coordinated eligibility handoff the sweep
|
|
658
|
+
// ticket will read. Until then it grows unbounded — bound it when the sweep lands.
|
|
659
|
+
const gcEligible = new Set();
|
|
660
|
+
node.gcEligibleBlocks = gcEligible;
|
|
661
|
+
// The ring-shift state machine (advertise→confirm→release). Wired inside the rebalance block
|
|
662
|
+
// below (it needs the BlockTransferCoordinator confirmer + the cohort-size floor); left
|
|
663
|
+
// undefined when the rebalance reaction is not wired, in which case ring shifts stay inert —
|
|
664
|
+
// a move-out is unsafe without the confirm/release path.
|
|
665
|
+
let ringShift;
|
|
516
666
|
const storageMonitor = new StorageMonitor(rawStorage, options.arachnode?.storage ?? {});
|
|
517
667
|
const ringSelector = new RingSelector(fretAdapter, storageMonitor, {
|
|
518
668
|
minCapacity: 100 * 1024 * 1024,
|
|
519
669
|
thresholds: {
|
|
520
670
|
moveOut: 0.85,
|
|
521
671
|
moveIn: 0.40
|
|
522
|
-
}
|
|
672
|
+
},
|
|
673
|
+
// Damping so the ring decision cannot thrash near a boundary
|
|
674
|
+
// (docs/arachnode-ring-handoff.md § Part 1).
|
|
675
|
+
smoothingAlpha: 0.2,
|
|
676
|
+
deadband: 0.5,
|
|
677
|
+
minDwellMs: 10 * 60 * 1000
|
|
523
678
|
});
|
|
524
679
|
// Determine and announce ring membership
|
|
525
680
|
const peerId = node.peerId.toString();
|
|
@@ -534,14 +689,142 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
534
689
|
};
|
|
535
690
|
// Replace the restore callback (this is a bit hacky, but works for now)
|
|
536
691
|
storageRepo.createBlockStorage = (blockId) => new BlockStorage(blockId, rawStorage, newRestoreCallback);
|
|
537
|
-
//
|
|
692
|
+
// --- Rebalance reaction: drive RebalanceMonitor + react via BlockTransferCoordinator ---
|
|
693
|
+
// Nothing previously activated the rebalance path on a real node: initRebalanceMonitor was
|
|
694
|
+
// never called, the monitor was never start()ed, and BlockTransferCoordinator (the
|
|
695
|
+
// pull-gained / push-lost reaction primitive) was never constructed in src. This block lives
|
|
696
|
+
// inside the arachnode `if (fret)` gate because both dependencies only exist here — the
|
|
697
|
+
// fretAdapter and the RestorationCoordinator. When arachnode is disabled or FRET is absent the
|
|
698
|
+
// rebalance path stays inert (acceptable: rebalance is a resilience optimization). A wiring
|
|
699
|
+
// failure here is non-fatal (log + continue), unlike the operator-opted-in cohortTopic block.
|
|
700
|
+
if (networkManager && (options.rebalance?.enabled ?? true) !== false) {
|
|
701
|
+
try {
|
|
702
|
+
// repo → the LOCAL storageRepo (not repoProxy/coordinatedRepo): a pulled/pushed replica
|
|
703
|
+
// must land in / be read from this node's own storage, same reasoning as the
|
|
704
|
+
// blockTransfer service handler registration. protocolPrefix (/optimystic/<networkName>)
|
|
705
|
+
// MUST match the prefix the node registers its block-transfer handler under, or every
|
|
706
|
+
// lost-block push dials the wrong protocol and fails to connect.
|
|
707
|
+
const coordinator = new BlockTransferCoordinator(storageRepo, keyNetwork, restorationCoordinatorV2, partitionDetector, protocolPrefix);
|
|
708
|
+
const rebalanceMonitor = networkManager.initRebalanceMonitor(partitionDetector, fretAdapter, ownedBlocks, options.rebalance);
|
|
709
|
+
await rebalanceMonitor.start();
|
|
710
|
+
// onRebalance fires synchronously from the monitor's debounced check; the coordinator's
|
|
711
|
+
// reaction (pull gained / push lost, each partition-guarded) is async, so hop it off the
|
|
712
|
+
// handler rather than blocking the monitor's emit loop. handleRebalanceEvent can REJECT
|
|
713
|
+
// (e.g. RestorationCoordinator.restore() throws while pulling a gained block) and a bare
|
|
714
|
+
// `void` would surface that as an unhandled rejection (process-fatal on Node >=15); the
|
|
715
|
+
// reaction is a resilience optimization, so swallow + log instead.
|
|
716
|
+
//
|
|
717
|
+
// ALONGSIDE dispatching to the coordinator, drive the shared owned-block set off this
|
|
718
|
+
// authoritative responsibility signal. A GAINED block is added immediately so it is
|
|
719
|
+
// tracked even before its next commit/replica touches the feed.
|
|
720
|
+
//
|
|
721
|
+
// A LOST block is NO LONGER released synchronously: doing so stopped spreading a block
|
|
722
|
+
// whose push to the new owners might fail, drop it below the replication floor, and let a
|
|
723
|
+
// later sweep reclaim it (docs/arachnode-ring-handoff.md § Why the current code violates
|
|
724
|
+
// it #2). Instead the release is GATED on confirmation — the coordinator returns the lost
|
|
725
|
+
// blocks it confirmed replicated to ≥ floor new owners, and ONLY those are untracked
|
|
726
|
+
// (authoritative eviction from the shared set — complements spread's lazy self-prune) and
|
|
727
|
+
// marked GC-eligible. A lost block whose push failed / was partition-skipped stays
|
|
728
|
+
// tracked and served, and is retried on the next rebalance.
|
|
729
|
+
//
|
|
730
|
+
// Best-effort iteration safety: this eviction can mutate ownedBlocks while
|
|
731
|
+
// SpreadOnChurnMonitor (or this monitor) is mid for...of over the same Set inside an
|
|
732
|
+
// async loop. Adding/deleting a Set entry during iteration does not throw in JS — entries
|
|
733
|
+
// are visited best-effort — which is acceptable for a resilience mechanism, so we
|
|
734
|
+
// document it here rather than add locking.
|
|
735
|
+
rebalanceMonitor.onRebalance((event) => {
|
|
736
|
+
for (const blockId of event.gained)
|
|
737
|
+
ownedBlocks.add(blockId);
|
|
738
|
+
coordinator.handleRebalanceEvent(event).then((result) => {
|
|
739
|
+
for (const blockId of result.released) {
|
|
740
|
+
rebalanceMonitor.untrackBlock(blockId); // also evicts from the shared ownedBlocks set
|
|
741
|
+
gcEligible.add(blockId); // confirmed replicated → safe to sweep
|
|
742
|
+
}
|
|
743
|
+
}).catch((err) => {
|
|
744
|
+
log?.('rebalance reaction failed: %o', err);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
// Ring-shift handoff (advertise→confirm→release). It needs the confirmer (this
|
|
748
|
+
// coordinator) and the cohort-size floor (this monitor), so it is wired here. The
|
|
749
|
+
// `onRelease` callback runs Phase C's local effect: stop serving/spreading the shed
|
|
750
|
+
// range and mark it GC-eligible — the same authoritative eviction the confirmed-rebalance
|
|
751
|
+
// release performs.
|
|
752
|
+
ringShift = new RingShiftCoordinator({
|
|
753
|
+
fretAdapter,
|
|
754
|
+
ringSelector,
|
|
755
|
+
fret,
|
|
756
|
+
partitionDetector,
|
|
757
|
+
confirmer: coordinator,
|
|
758
|
+
ownedBlocks,
|
|
759
|
+
selfPeerId: peerId,
|
|
760
|
+
getFloor: () => rebalanceMonitor.getCohortSize(),
|
|
761
|
+
onRelease: (blockIds) => {
|
|
762
|
+
for (const blockId of blockIds) {
|
|
763
|
+
rebalanceMonitor.untrackBlock(blockId);
|
|
764
|
+
gcEligible.add(blockId);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
// Reconcile any stale `moving` advertisement left by a crash mid-handoff (no-op unless
|
|
769
|
+
// arachnode metadata survived a restart still marked `moving`).
|
|
770
|
+
ringShift.reconcileOnStart();
|
|
771
|
+
// Feed owned blocks via the SINGLE shared feed (idempotent — already live if the spread
|
|
772
|
+
// block above wired it). Both monitors read the same ownedBlocks set this populates.
|
|
773
|
+
ensureOwnedBlockFeed();
|
|
774
|
+
// Expose for tests/diagnostics (mirrors node.spreadOnChurnMonitor).
|
|
775
|
+
node.rebalanceMonitor = rebalanceMonitor;
|
|
776
|
+
node.blockTransferCoordinator = coordinator;
|
|
777
|
+
node.ringShiftCoordinator = ringShift;
|
|
778
|
+
// Disposal: stop the monitor before transports close. Composes with the other stop
|
|
779
|
+
// wrappers (each calls its captured previousStop last). Idempotent — RebalanceMonitor.stop()
|
|
780
|
+
// early-returns when not running (NetworkManagerService.stop() also stops it). The shared
|
|
781
|
+
// owned-block feed teardown is the separate up-front wrapper (not duplicated here).
|
|
782
|
+
const previousStop = node.stop.bind(node);
|
|
783
|
+
node.stop = async () => {
|
|
784
|
+
try {
|
|
785
|
+
await rebalanceMonitor.stop();
|
|
786
|
+
}
|
|
787
|
+
finally {
|
|
788
|
+
await previousStop();
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
// Rebalance is a resilience optimization, not a correctness requirement - a wiring
|
|
794
|
+
// failure (e.g. FRET briefly unavailable) must NOT hard-fail node startup.
|
|
795
|
+
log?.('rebalance wiring init failed: %o', err);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
// Monitor capacity and adjust ring periodically. The damped `shouldTransition()` decides
|
|
799
|
+
// WHETHER/where to move (docs/arachnode-ring-handoff.md § Part 1); the RingShiftCoordinator
|
|
800
|
+
// carries the move out through the advertise→confirm→release handoff (§ Part 2) so a shift
|
|
801
|
+
// never drops a key below its replication floor. The old unilateral `setArachnodeInfo` flip —
|
|
802
|
+
// which changed advertised responsibility instantly with no data handoff — is gone.
|
|
803
|
+
//
|
|
804
|
+
// Ring shifts run ONLY when `ringShift` is wired (i.e. the rebalance reaction is enabled): a
|
|
805
|
+
// move-out is unsafe without the confirm/release path, so a node with the rebalance reaction
|
|
806
|
+
// disabled stays at its bootstrap ring rather than flipping unsafely.
|
|
538
807
|
const monitorInterval = setInterval(async () => {
|
|
808
|
+
if (!ringShift)
|
|
809
|
+
return;
|
|
539
810
|
const transition = await ringSelector.shouldTransition();
|
|
540
|
-
if (transition.shouldMove) {
|
|
811
|
+
if (transition.shouldMove && transition.direction && transition.newRingDepth !== undefined) {
|
|
541
812
|
log?.('Ring transition needed: moving %s to Ring %d', transition.direction, transition.newRingDepth);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
813
|
+
try {
|
|
814
|
+
const outcome = await ringShift.executeShift({
|
|
815
|
+
direction: transition.direction,
|
|
816
|
+
newRingDepth: transition.newRingDepth
|
|
817
|
+
});
|
|
818
|
+
log?.('Ring shift outcome: %o', outcome);
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
log?.('Ring shift failed: %o', err);
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
// Measure the minimum dwell from the SETTLED shift (completed or rolled back), not
|
|
825
|
+
// just the trigger stamped inside shouldTransition (docs/arachnode-ring-handoff.md §1.3).
|
|
826
|
+
ringSelector.recordShiftSettled();
|
|
827
|
+
}
|
|
545
828
|
}
|
|
546
829
|
}, 60_000);
|
|
547
830
|
// Cleanup on node stop
|
|
@@ -555,6 +838,44 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
555
838
|
log?.('FRET service not available, Arachnode disabled');
|
|
556
839
|
}
|
|
557
840
|
}
|
|
841
|
+
// --- Seed the shared owned-block set from already-durable storage ---
|
|
842
|
+
// Blocks durable from a previous run are otherwise untracked until next touched (see the
|
|
843
|
+
// onAnyCollectionChange comment above where ownedBlocks is declared). Placed here, AFTER both
|
|
844
|
+
// monitor-wiring blocks (spread ~line 862, rebalance ~line 974) have had their chance to call
|
|
845
|
+
// ensureOwnedBlockFeed():
|
|
846
|
+
// - Gate on offOwnedBlockFeed: only seed when a monitor actually consumes ownedBlocks; if both
|
|
847
|
+
// are disabled the set is unused and the scan (plus the background task) is wasted work.
|
|
848
|
+
// - Feed-before-scan ordering is load-bearing: because the feed is already live, a block
|
|
849
|
+
// committed/replicated DURING the scan is caught by the feed; Set.add is idempotent so the
|
|
850
|
+
// overlap is harmless. Scanning before subscribing would drop a block committed in the gap.
|
|
851
|
+
// - Fire-and-forget so a large store never blocks startup; the .catch keeps a scan rejection
|
|
852
|
+
// from becoming an unhandled rejection.
|
|
853
|
+
// - Cancellable: a stop wrapper flips seedStopping so the scan loop breaks against a
|
|
854
|
+
// stopping/closing backend rather than running the enumeration to completion.
|
|
855
|
+
// NOTE: a concurrent rebalance release can untrackBlock (delete from ownedBlocks) a confirmed-
|
|
856
|
+
// released block while this scan is still running, and the scan could then re-add that id. Benign
|
|
857
|
+
// transient: the block is still in the metadata store (no sweep reclaims metadata yet), so a
|
|
858
|
+
// re-added released block is simply re-evaluated and re-released on the next rebalance tick. Right
|
|
859
|
+
// after a restart, responsibility-loss detection lags this fast metadata scan, so the window is
|
|
860
|
+
// small. Accepted rather than synchronized.
|
|
861
|
+
if (offOwnedBlockFeed && typeof rawStorage.listBlockIds === 'function') {
|
|
862
|
+
let seedStopping = false;
|
|
863
|
+
const previousStop = node.stop.bind(node);
|
|
864
|
+
node.stop = async () => {
|
|
865
|
+
seedStopping = true;
|
|
866
|
+
await previousStop();
|
|
867
|
+
};
|
|
868
|
+
void seedOwnedBlocksFromStorage(rawStorage, ownedBlocks, () => seedStopping)
|
|
869
|
+
.catch((err) => (node.logger?.forComponent?.('db-p2p:owned-block-seed'))?.('seed failed: %o', err));
|
|
870
|
+
}
|
|
871
|
+
// [dispute-subsystem-dormant] The DisputeService object is constructed below so tests and
|
|
872
|
+
// getDisputeStatus() work, but it is unreachable from the live network path:
|
|
873
|
+
// - No inbound handler: disputeProtocolService is NOT in the services map above.
|
|
874
|
+
// - onInvalidation is deliberately unset: maybeInvalidate() is a no-op on live nodes.
|
|
875
|
+
// - revalidate is deliberately unset: handleChallenge always votes inconclusive on live nodes.
|
|
876
|
+
// Full activation requires arbitrator-set anchoring before a forged synthetic cohort can pass resolution.
|
|
877
|
+
// Gate: tickets/backlog/hardening/invalidation-live-wiring-requires-arbitrator-set-anchoring
|
|
878
|
+
// Wiring plan: tickets/backlog/feat-dispute-subsystem-live-activation
|
|
558
879
|
// Initialize dispute service if enabled
|
|
559
880
|
let disputeServiceInstance;
|
|
560
881
|
if (options.dispute?.disputeEnabled) {
|
|
@@ -567,24 +888,25 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
567
888
|
reputation,
|
|
568
889
|
validator: options.validator,
|
|
569
890
|
config: options.dispute,
|
|
570
|
-
selectArbitrators: async (blockId, excludePeers, count) => {
|
|
891
|
+
selectArbitrators: async (blockId, excludePeers, count, round, epoch) => {
|
|
571
892
|
const { hashKey: fretHashKey } = await import('p2p-fret');
|
|
572
|
-
const blockIdBytes = new TextEncoder().encode(blockId);
|
|
573
893
|
const fret = node.services?.fret;
|
|
574
894
|
if (!fret)
|
|
575
895
|
return [];
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
// Filter out original cluster peers and self, convert to PeerId
|
|
896
|
+
// Dispersed sampling: draw `count` peers from coordinates spread across the whole keyspace
|
|
897
|
+
// (hash(blockId ‖ round ‖ epoch ‖ i)) rather than the block's XOR neighborhood, so an attacker
|
|
898
|
+
// who owns the block's locale does not thereby own the arbitrators. `assembleCohort` already
|
|
899
|
+
// filters to known members; excluding the original cluster + self keeps arbitrators independent.
|
|
581
900
|
const excludeSet = new Set(excludePeers);
|
|
901
|
+
// NOTE: adding the local node's own id to `exclude` makes the draw node-relative. Cross-node
|
|
902
|
+
// determinism (the verifiable-recompute property) holds today only because the dissent
|
|
903
|
+
// coordinator running this is itself a member of the original cluster, so `self` is already in
|
|
904
|
+
// `excludePeers` — the add is a no-op and every honest node excludes the identical set. When a
|
|
905
|
+
// verify-path recompute lands, it MUST reconstruct `exclude` from the challenger's identity
|
|
906
|
+
// (`proof.challengerPeerId`) + original cluster, never the verifier's own id, or re-derivation diverges.
|
|
582
907
|
excludeSet.add(node.peerId.toString());
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
.slice(0, count)
|
|
586
|
-
.map(pid => peerIdFromString(pid));
|
|
587
|
-
return arbitratorPeerIds;
|
|
908
|
+
const picks = await sampleArbitrators({ blockId: new TextEncoder().encode(blockId), round, epoch, count, exclude: excludeSet }, (coord, wants) => fret.assembleCohort(coord, wants), fretHashKey);
|
|
909
|
+
return picks.map(pid => peerIdFromString(pid));
|
|
588
910
|
},
|
|
589
911
|
});
|
|
590
912
|
}
|
|
@@ -607,6 +929,12 @@ export async function createLibp2pNodeBase(options, defaults) {
|
|
|
607
929
|
node.keyNetwork = keyNetwork;
|
|
608
930
|
node.reputation = reputation;
|
|
609
931
|
node.disputeService = disputeServiceInstance;
|
|
932
|
+
// The node's libp2p Ed25519 identity key. Exposed on the same `(node as any).*` surface as
|
|
933
|
+
// coordinatedRepo/keyNetwork so a host can bind a client-transaction signer to it (the Quereus
|
|
934
|
+
// collection-factory's getSigner reuses this via signPeer). libp2p does not surface the private
|
|
935
|
+
// key on its public `Libp2p` interface, so this attachment is the sanctioned in-process handle.
|
|
936
|
+
// Ed25519 by construction (options.privateKey defaults to generateKeyPair('Ed25519')).
|
|
937
|
+
node.peerPrivateKey = nodePrivateKey;
|
|
610
938
|
// --- Cohort-topic origination activation (post-node: consumes the fully-assembled node + FRET) ---
|
|
611
939
|
// This is the only place that is after the node + FRET are assembled (node.start() done, fretSvc
|
|
612
940
|
// available) yet before any caller can capture `blockChangeNotifier` — the Quereus collection-factory
|