@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PendRequest, ActionBlocks, IRepo, MessageOptions, CommitResult, GetBlockResults, PendResult, BlockGets, CommitRequest, RepoMessage, IKeyNetwork, ICluster, ClusterConsensusConfig, BlockId, ActionRev, ActionContext, ClusterRecord } from "@optimystic/db-core";
|
|
2
|
-
import { LruMap, blockIdsForTransforms } from "@optimystic/db-core";
|
|
2
|
+
import { LruMap, blockIdsForTransforms, DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
|
|
3
3
|
import { ClusterCoordinator } from "./cluster-coordinator.js";
|
|
4
4
|
import type { ClusterClient } from "../cluster/client.js";
|
|
5
5
|
import type { PeerId } from "@libp2p/interface";
|
|
@@ -7,7 +7,9 @@ import { peerIdFromString } from "@libp2p/peer-id";
|
|
|
7
7
|
import type { FretService } from "p2p-fret";
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
9
|
import type { IPeerReputation } from "../reputation/types.js";
|
|
10
|
+
import { PenaltyReason } from "../reputation/types.js";
|
|
10
11
|
import type { ITransactionStateStore } from "../cluster/i-transaction-state-store.js";
|
|
12
|
+
import { selectQuorumRev, type RevClaim, type QuorumRev } from "../cluster/quorum-restore.js";
|
|
11
13
|
|
|
12
14
|
const log = createLogger('coordinator-repo');
|
|
13
15
|
|
|
@@ -69,6 +71,11 @@ export class CoordinatorRepo implements IRepo {
|
|
|
69
71
|
private readonly readRepairMode: 'off' | 'lazy' | 'paranoid';
|
|
70
72
|
private readonly readRepairWindowMs: number;
|
|
71
73
|
private readonly readRepairSampleRate: number;
|
|
74
|
+
/** Simple-majority threshold from the consensus policy; drives the read-repair corroboration quorum. */
|
|
75
|
+
private readonly simpleMajorityThreshold: number;
|
|
76
|
+
/** Resolved super-majority threshold the coordinator commits on (mirrors the value handed to ClusterCoordinator). */
|
|
77
|
+
private readonly superMajorityThreshold: number;
|
|
78
|
+
private readonly reputation?: IPeerReputation;
|
|
72
79
|
/** Test seam: overridable clock for window-based read-repair gating. */
|
|
73
80
|
now: () => number = () => Date.now();
|
|
74
81
|
/** Test seam: overridable RNG (0..1) for sample-rate gating. */
|
|
@@ -89,7 +96,7 @@ export class CoordinatorRepo implements IRepo {
|
|
|
89
96
|
this.localPeerId = localPeerId;
|
|
90
97
|
const policy: ClusterConsensusConfig & { clusterSize: number } = {
|
|
91
98
|
clusterSize: cfg?.clusterSize ?? 10,
|
|
92
|
-
superMajorityThreshold: cfg?.superMajorityThreshold ??
|
|
99
|
+
superMajorityThreshold: cfg?.superMajorityThreshold ?? DEFAULT_SUPER_MAJORITY_THRESHOLD,
|
|
93
100
|
simpleMajorityThreshold: cfg?.simpleMajorityThreshold ?? 0.51,
|
|
94
101
|
minAbsoluteClusterSize: cfg?.minAbsoluteClusterSize ?? 3,
|
|
95
102
|
allowClusterDownsize: cfg?.allowClusterDownsize ?? true,
|
|
@@ -100,13 +107,20 @@ export class CoordinatorRepo implements IRepo {
|
|
|
100
107
|
commitBroadcastRetryMaxIntervalMs: cfg?.commitBroadcastRetryMaxIntervalMs ?? 8000,
|
|
101
108
|
commitBroadcastRetryMaxAttempts: cfg?.commitBroadcastRetryMaxAttempts ?? 5,
|
|
102
109
|
commitBroadcastImmediateRetries: cfg?.commitBroadcastImmediateRetries ?? 1,
|
|
110
|
+
promiseImmediateRetries: cfg?.promiseImmediateRetries ?? 1,
|
|
103
111
|
readRepairMode: cfg?.readRepairMode ?? 'lazy',
|
|
104
112
|
readRepairWindowMs: cfg?.readRepairWindowMs ?? 10000,
|
|
105
|
-
readRepairSampleRate: cfg?.readRepairSampleRate ?? 0
|
|
113
|
+
readRepairSampleRate: cfg?.readRepairSampleRate ?? 0,
|
|
114
|
+
// Default false: an undersized cluster with no confident network-size estimate
|
|
115
|
+
// is REJECTED (fail closed). Callers only opt in for single-node/local/test meshes.
|
|
116
|
+
allowUnvalidatedSmallCluster: cfg?.allowUnvalidatedSmallCluster ?? false
|
|
106
117
|
};
|
|
107
118
|
this.readRepairMode = policy.readRepairMode!;
|
|
108
119
|
this.readRepairWindowMs = policy.readRepairWindowMs!;
|
|
109
120
|
this.readRepairSampleRate = policy.readRepairSampleRate!;
|
|
121
|
+
this.simpleMajorityThreshold = policy.simpleMajorityThreshold;
|
|
122
|
+
this.superMajorityThreshold = policy.superMajorityThreshold;
|
|
123
|
+
this.reputation = reputation;
|
|
110
124
|
const localClusterRef = localCluster && localPeerId ? {
|
|
111
125
|
update: localCluster.update.bind(localCluster),
|
|
112
126
|
peerId: localPeerId,
|
|
@@ -115,6 +129,15 @@ export class CoordinatorRepo implements IRepo {
|
|
|
115
129
|
this.coordinator = new ClusterCoordinator(keyNetwork, createClusterClient, policy, localClusterRef, fretService, reputation, stateStore);
|
|
116
130
|
}
|
|
117
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The resolved super-majority threshold this coordinator commits on. Exposed so the composition root
|
|
134
|
+
* can fail-fast if the coordinator and the cluster member would run different thresholds (see the
|
|
135
|
+
* coupling assertion in `libp2p-node-base.ts`).
|
|
136
|
+
*/
|
|
137
|
+
get effectiveSuperMajorityThreshold(): number {
|
|
138
|
+
return this.superMajorityThreshold;
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
/** Recover coordinator transactions from persistent store after a restart. */
|
|
119
142
|
async recoverTransactions(): Promise<void> {
|
|
120
143
|
await this.coordinator.recoverTransactions();
|
|
@@ -183,6 +206,13 @@ export class CoordinatorRepo implements IRepo {
|
|
|
183
206
|
// (b) Stale-by-policy — block is present but read-repair policy says verify.
|
|
184
207
|
// Skip cluster fetch if this is already a sync request (to prevent recursive queries).
|
|
185
208
|
const skipClusterFetch = (options as any)?.skipClusterFetch;
|
|
209
|
+
// NOTE: NetworkTransactor.get treats an authoritative "absent" ({ state: {} })
|
|
210
|
+
// as final and no longer retries it (ticket txn-perf-authoritative-notfound),
|
|
211
|
+
// relying on this cluster reconciliation to have already run. If a coordinator
|
|
212
|
+
// is ever configured WITHOUT clusterLatestCallback, a missing block is answered
|
|
213
|
+
// from local state alone with no transactor-level retry to compensate. That is
|
|
214
|
+
// fine today (such a coordinator has no cluster to reconcile against), but keep
|
|
215
|
+
// this coupling in mind if a partial-cluster read path is added.
|
|
186
216
|
if (this.clusterLatestCallback && !skipClusterFetch) {
|
|
187
217
|
for (const blockId of blockGets.blockIds) {
|
|
188
218
|
const localEntry = localResult[blockId];
|
|
@@ -292,11 +322,23 @@ export class CoordinatorRepo implements IRepo {
|
|
|
292
322
|
}
|
|
293
323
|
|
|
294
324
|
/**
|
|
295
|
-
* Query cluster peers
|
|
325
|
+
* Query cluster peers for their latest revision and return the highest
|
|
326
|
+
* revision corroborated by a quorum of distinct peers.
|
|
327
|
+
*
|
|
328
|
+
* Replaces the old "max rev any single peer reports" — which let one lying
|
|
329
|
+
* peer over-reporting its revision steer restoration — with quorum
|
|
330
|
+
* corroboration on the exact `(rev, actionId)` pair (see
|
|
331
|
+
* {@link selectQuorumRev}). The local node's own latest is included as a
|
|
332
|
+
* corroborating vote because `clusterLatestCallback` self-short-circuits to
|
|
333
|
+
* local storage. Returns `undefined` (keep local, do not restore) when no
|
|
334
|
+
* revision is corroborated.
|
|
335
|
+
*
|
|
336
|
+
* NOTE: the quorum is corroboration-of-a-claim, NOT Sybil-resistant cohort
|
|
337
|
+
* membership — a peer minting fresh keypairs still casts a vote. Commit-cert
|
|
338
|
+
* + membership anchoring is deferred to backlog
|
|
339
|
+
* `debt-read-repair-commit-cert-verification`.
|
|
296
340
|
*/
|
|
297
341
|
private async queryClusterForLatest(peerIds: string[], blockId: BlockId, context?: ActionContext): Promise<ActionRev | undefined> {
|
|
298
|
-
let maxLatest: ActionRev | undefined;
|
|
299
|
-
|
|
300
342
|
// Add timeout wrapper to prevent hanging on unresponsive peers
|
|
301
343
|
const withTimeout = <T>(promise: Promise<T>, timeoutMs: number): Promise<T | undefined> =>
|
|
302
344
|
Promise.race([
|
|
@@ -304,30 +346,68 @@ export class CoordinatorRepo implements IRepo {
|
|
|
304
346
|
new Promise<undefined>(resolve => setTimeout(() => resolve(undefined), timeoutMs))
|
|
305
347
|
]);
|
|
306
348
|
|
|
307
|
-
// Query peers in parallel for their latest revision (with 1s timeout per peer)
|
|
349
|
+
// Query peers in parallel for their latest revision (with 1s timeout per peer),
|
|
350
|
+
// tagging each response with the peer that made it so votes stay distinct.
|
|
308
351
|
const latestResults = await Promise.allSettled(
|
|
309
|
-
peerIds.map(peerIdStr => {
|
|
352
|
+
peerIds.map(async peerIdStr => {
|
|
310
353
|
const peerId = peerIdFromString(peerIdStr);
|
|
311
|
-
|
|
354
|
+
const value = await withTimeout(this.clusterLatestCallback!(peerId, blockId, context), 1000);
|
|
355
|
+
return { peerIdStr, value };
|
|
312
356
|
})
|
|
313
357
|
);
|
|
314
358
|
|
|
359
|
+
const claims: RevClaim[] = [];
|
|
315
360
|
for (const result of latestResults) {
|
|
316
|
-
if (result.status === 'fulfilled' && result.value) {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
maxLatest = peerLatest;
|
|
320
|
-
}
|
|
361
|
+
if (result.status === 'fulfilled' && result.value.value) {
|
|
362
|
+
const { peerIdStr, value } = result.value;
|
|
363
|
+
claims.push({ peerId: peerIdStr, rev: value.rev, actionId: value.actionId });
|
|
321
364
|
}
|
|
322
365
|
}
|
|
323
366
|
|
|
324
|
-
|
|
367
|
+
const selected = selectQuorumRev(claims, this.simpleMajorityThreshold);
|
|
368
|
+
if (!selected) {
|
|
369
|
+
log('cluster-fetch:no-quorum', { blockId, responders: claims.length });
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Best-effort: penalize peers whose claim contradicts the corroborated pair
|
|
374
|
+
// (an inflated rev the quorum outvoted, or conflicting content at the agreed
|
|
375
|
+
// rev). A lower rev is just lag, never penalized. Never let this throw.
|
|
376
|
+
this.penalizeContradictingRevClaims(claims, selected, blockId);
|
|
377
|
+
|
|
378
|
+
return { actionId: selected.actionId, rev: selected.rev };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Report peers whose reported latest contradicts the quorum-corroborated pair. Best-effort.
|
|
383
|
+
*
|
|
384
|
+
* NOTE: the `rev > selected.rev` branch is NOT provable misbehavior — an honest
|
|
385
|
+
* peer legitimately ahead of the sampled quorum (an in-flight commit it durably
|
|
386
|
+
* stored, or other honest holders dropped from the sample by the 1s per-peer
|
|
387
|
+
* timeout) reports a higher rev and gets penalized (weight 30 → immediate
|
|
388
|
+
* deprioritize at threshold 20). Distinguishing a liar from an honest leader
|
|
389
|
+
* needs the commit-cert verification tracked in backlog
|
|
390
|
+
* `debt-read-repair-penalty-provable-only` / `debt-read-repair-commit-cert-verification`.
|
|
391
|
+
*/
|
|
392
|
+
private penalizeContradictingRevClaims(claims: RevClaim[], selected: QuorumRev, blockId: BlockId): void {
|
|
393
|
+
if (!this.reputation) return;
|
|
394
|
+
try {
|
|
395
|
+
for (const c of claims) {
|
|
396
|
+
const contradicts = c.rev > selected.rev
|
|
397
|
+
|| (c.rev === selected.rev && c.actionId !== selected.actionId);
|
|
398
|
+
if (contradicts) {
|
|
399
|
+
this.reputation.reportPeer(c.peerId, PenaltyReason.InvalidRestoration, `read-repair:${blockId}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
log('cluster-fetch:penalize-error', { blockId, error: (err as Error).message });
|
|
404
|
+
}
|
|
325
405
|
}
|
|
326
406
|
|
|
327
407
|
async pend(request: PendRequest, options?: MessageOptions): Promise<PendResult> {
|
|
328
408
|
const allBlockIds = blockIdsForTransforms(request.transforms);
|
|
329
409
|
await this.verifyResponsibility(allBlockIds);
|
|
330
|
-
const coordinatingBlockIds =
|
|
410
|
+
const coordinatingBlockIds = options?.coordinatingBlockIds ?? allBlockIds;
|
|
331
411
|
|
|
332
412
|
const peerCount = await this.coordinator.getClusterSize(coordinatingBlockIds[0]!);
|
|
333
413
|
if (peerCount <= 1) {
|
package/src/repo/service.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { IRepo, RepoMessage } from '@optimystic/db-core'
|
|
|
5
5
|
import { blockIdsForTransforms } from '@optimystic/db-core'
|
|
6
6
|
import { peersEqual } from '../peer-utils.js'
|
|
7
7
|
import { encodePeers, type RedirectPayload } from './redirect.js'
|
|
8
|
+
import { MAX_BLOCK_MESSAGE_BYTES } from '../protocol-limits.js'
|
|
8
9
|
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
9
10
|
import { createLogger } from '../logger.js'
|
|
10
11
|
|
|
@@ -267,6 +268,11 @@ export class RepoService implements Startable {
|
|
|
267
268
|
|
|
268
269
|
// Encode and yield the response
|
|
269
270
|
yield new TextEncoder().encode(JSON.stringify(response))
|
|
271
|
+
// One request per stream: every real RepoClient sends exactly one request
|
|
272
|
+
// per dial (see ProtocolClient.processMessage), so complete the generator
|
|
273
|
+
// after the first response. A second frame a peer queued is then never read
|
|
274
|
+
// or parsed. Mirrors sync/block-transfer.
|
|
275
|
+
return
|
|
270
276
|
}
|
|
271
277
|
}
|
|
272
278
|
|
|
@@ -274,7 +280,7 @@ export class RepoService implements Startable {
|
|
|
274
280
|
try {
|
|
275
281
|
const responses = pipe(
|
|
276
282
|
stream,
|
|
277
|
-
(source) => lpDecode(source),
|
|
283
|
+
(source) => lpDecode(source, { maxDataLength: MAX_BLOCK_MESSAGE_BYTES }),
|
|
278
284
|
processStream.bind(this),
|
|
279
285
|
(source) => lpEncode(source)
|
|
280
286
|
)
|
package/src/reputation/types.ts
CHANGED
|
@@ -20,6 +20,8 @@ export enum PenaltyReason {
|
|
|
20
20
|
FalseApproval = 'false-approval',
|
|
21
21
|
/** Challenger lost a dispute (their rejection was wrong) */
|
|
22
22
|
DisputeLost = 'dispute-lost',
|
|
23
|
+
/** Peer reported a block latest (rev/actionId) or served content contradicted by a cluster quorum during restoration */
|
|
24
|
+
InvalidRestoration = 'invalid-restoration',
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** Default penalty weights by reason */
|
|
@@ -34,6 +36,7 @@ export const DEFAULT_PENALTY_WEIGHTS: Record<PenaltyReason, number> = {
|
|
|
34
36
|
[PenaltyReason.ConnectionFailure]: 2,
|
|
35
37
|
[PenaltyReason.FalseApproval]: 40,
|
|
36
38
|
[PenaltyReason.DisputeLost]: 30,
|
|
39
|
+
[PenaltyReason.InvalidRestoration]: 30,
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
/** Thresholds controlling graduated reputation responses */
|
package/src/rn.ts
CHANGED
|
@@ -7,12 +7,15 @@ export * from './repo/cluster-coordinator.js';
|
|
|
7
7
|
export * from './repo/coordinator-repo.js';
|
|
8
8
|
export * from './repo/service.js';
|
|
9
9
|
export * from './storage/block-storage.js';
|
|
10
|
+
export * from './storage/raw-store-driver.js';
|
|
11
|
+
export * from './storage/kv-raw-storage.js';
|
|
12
|
+
export * from './storage/memory-store-driver.js';
|
|
10
13
|
export * from './storage/memory-storage.js';
|
|
11
14
|
export * from './storage/i-block-storage.js';
|
|
12
15
|
export * from './storage/i-raw-storage.js';
|
|
13
16
|
export * from './storage/struct.js';
|
|
14
17
|
export * from './storage/storage-repo.js';
|
|
15
|
-
export * from './storage/restoration-coordinator
|
|
18
|
+
export * from './storage/restoration-coordinator.js';
|
|
16
19
|
export * from './storage/ring-selector.js';
|
|
17
20
|
export * from './storage/storage-monitor.js';
|
|
18
21
|
export * from './storage/arachnode-fret-adapter.js';
|
|
@@ -26,3 +29,6 @@ export * from './routing/responsibility.js';
|
|
|
26
29
|
export * from './routing/libp2p-known-peers.js';
|
|
27
30
|
export * from './network/network-manager-service.js';
|
|
28
31
|
export * from './network/get-network-manager.js';
|
|
32
|
+
// Browser-safe peer signing seam. The rest of ./cohort-topic pulls node-heavy host.js,
|
|
33
|
+
// so only peer-sig (@noble/curves + peer-id) is surfaced through the RN/browser entry.
|
|
34
|
+
export * from './cohort-topic/peer-sig.js';
|
|
@@ -23,6 +23,24 @@ export interface ArachnodeInfo {
|
|
|
23
23
|
|
|
24
24
|
/** Ring membership status */
|
|
25
25
|
status: 'joining' | 'active' | 'moving' | 'leaving';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The ring/partition this node is moving *from*, present only while `status === 'moving'`.
|
|
29
|
+
*
|
|
30
|
+
* During a ring shift the node advertises its **target** `ringDepth`/`partition` (Phase A) but
|
|
31
|
+
* keeps serving its old range until it has confirmed the shed range is replicated elsewhere
|
|
32
|
+
* (Phase B) and releases it (Phase C). `moveFrom` records that old range so peers can treat a
|
|
33
|
+
* `moving` node as *still responsible for its old range* (fail-toward-old-holder), and so a
|
|
34
|
+
* crash between advertise and release is recoverable. Cleared on release/rollback (→ `active`).
|
|
35
|
+
* See `docs/arachnode-ring-handoff.md` § Part 2 & Part 3.
|
|
36
|
+
*/
|
|
37
|
+
moveFrom?: {
|
|
38
|
+
ringDepth: number;
|
|
39
|
+
partition?: {
|
|
40
|
+
prefixBits: number;
|
|
41
|
+
prefixValue: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/**
|
|
@@ -33,7 +51,15 @@ export interface ArachnodeInfo {
|
|
|
33
51
|
export class ArachnodeFretAdapter {
|
|
34
52
|
private static readonly ARACHNODE_KEY = 'arachnode';
|
|
35
53
|
|
|
36
|
-
constructor(
|
|
54
|
+
constructor(
|
|
55
|
+
private readonly fret: FretService,
|
|
56
|
+
/**
|
|
57
|
+
* This node's own peer id string. When provided, {@link getMyArachnodeInfo} uses it
|
|
58
|
+
* directly instead of reaching into the concrete FretService impl's private `.node` —
|
|
59
|
+
* {@link FretService} (the public interface) exposes no self-peer accessor.
|
|
60
|
+
*/
|
|
61
|
+
private readonly selfPeerId?: string
|
|
62
|
+
) {}
|
|
37
63
|
|
|
38
64
|
/**
|
|
39
65
|
* Set this node's Arachnode ring membership.
|
|
@@ -56,7 +82,12 @@ export class ArachnodeFretAdapter {
|
|
|
56
82
|
* Get my own Arachnode info.
|
|
57
83
|
*/
|
|
58
84
|
getMyArachnodeInfo(): ArachnodeInfo | undefined {
|
|
59
|
-
|
|
85
|
+
// Prefer the injected self peer id. Fall back to the concrete Libp2pFretService's private
|
|
86
|
+
// `.node.peerId` only when no id was injected — this cast is load-bearing (it depends on the
|
|
87
|
+
// concrete impl's shape, not the public FretService interface) and silently yields undefined
|
|
88
|
+
// for any impl lacking `.node`, so injection is the intended path.
|
|
89
|
+
const myPeerId = this.selfPeerId
|
|
90
|
+
?? (this.fret as unknown as { node?: { peerId?: { toString(): string } } }).node?.peerId?.toString();
|
|
60
91
|
if (!myPeerId) return undefined;
|
|
61
92
|
return this.getArachnodeInfo(myPeerId);
|
|
62
93
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ArachnodeInfo } from './arachnode-fret-adapter.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An Arachnode ring partition: the first `prefixBits` bits of a hashed coordinate must equal
|
|
5
|
+
* `prefixValue` for a key to fall inside it. Structurally identical to `ArachnodeInfo.partition`.
|
|
6
|
+
*/
|
|
7
|
+
export interface RingPartition {
|
|
8
|
+
prefixBits: number;
|
|
9
|
+
prefixValue: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract the first `bits` bits of a hashed coordinate as an integer (MSB-first).
|
|
14
|
+
*
|
|
15
|
+
* The single source of truth for the block/peer prefix comparison Arachnode uses to decide
|
|
16
|
+
* responsibility. `RingSelector.calculatePartition` (peer side) and `RestorationCoordinator`
|
|
17
|
+
* (block side) both route through this, so a peer's advertised partition and a block's derived
|
|
18
|
+
* prefix are computed the same way — otherwise "this peer owns this block's slice" would silently
|
|
19
|
+
* stop meaning what it says.
|
|
20
|
+
*/
|
|
21
|
+
export function extractPrefix(coord: Uint8Array, bits: number): number {
|
|
22
|
+
let value = 0;
|
|
23
|
+
for (let i = 0; i < bits; i++) {
|
|
24
|
+
const byteIndex = Math.floor(i / 8);
|
|
25
|
+
const bitIndex = 7 - (i % 8);
|
|
26
|
+
const bit = (coord[byteIndex]! >> bitIndex) & 1;
|
|
27
|
+
value = (value << 1) | bit;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Does `partition` cover the key at `coord`? Ring 0 (an undefined partition) covers the whole
|
|
34
|
+
* keyspace, so it always returns true.
|
|
35
|
+
*/
|
|
36
|
+
export function partitionCovers(partition: RingPartition | undefined, coord: Uint8Array): boolean {
|
|
37
|
+
if (!partition) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return extractPrefix(coord, partition.prefixBits) === partition.prefixValue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Is the node described by `info` a currently-serving responsible holder for the key at `coord`?
|
|
45
|
+
*
|
|
46
|
+
* **Fail-toward-old-holder.** A node mid-move (`status === 'moving'`) advertises its *target*
|
|
47
|
+
* `partition`, but it keeps serving its *old* range (`moveFrom`) until it releases. So a moving
|
|
48
|
+
* node covers the key if EITHER its old range or its target range covers it — it is still counted
|
|
49
|
+
* as a serving holder for everything it served before the move until it transitions to `active` at
|
|
50
|
+
* the new ring. This is what keeps a shed key covered through Phases A–B and across a mid-handoff
|
|
51
|
+
* crash. See `docs/arachnode-ring-handoff.md` § Part 3.
|
|
52
|
+
*/
|
|
53
|
+
export function isServingHolder(info: ArachnodeInfo, coord: Uint8Array): boolean {
|
|
54
|
+
if (info.status === 'moving' && info.moveFrom) {
|
|
55
|
+
return partitionCovers(info.moveFrom.partition, coord) || partitionCovers(info.partition, coord);
|
|
56
|
+
}
|
|
57
|
+
return partitionCovers(info.partition, coord);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Does the node described by `info` qualify toward *another* mover's Phase-B replication floor for
|
|
62
|
+
* the key at `coord`?
|
|
63
|
+
*
|
|
64
|
+
* A qualifying holder must still cover the key **after its own advertised move** — i.e. its
|
|
65
|
+
* *target* `partition` (the one it currently advertises) must cover it. A concurrent mover that is
|
|
66
|
+
* shedding the same sub-range advertises a target that does NOT cover the key, so it is excluded:
|
|
67
|
+
* two adjacent movers can never both count the other, so at most one reaches "confirmed" on the
|
|
68
|
+
* shared overlap and the other rolls back. For a non-moving (`active`) node, the advertised
|
|
69
|
+
* partition IS its serving range, so this reduces to plain partition coverage. See
|
|
70
|
+
* `docs/arachnode-ring-handoff.md` § Part 3 (concurrent moves).
|
|
71
|
+
*/
|
|
72
|
+
export function qualifiesForFloor(info: ArachnodeInfo, coord: Uint8Array): boolean {
|
|
73
|
+
return partitionCovers(info.partition, coord);
|
|
74
|
+
}
|