@optimystic/db-p2p 0.21.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cluster/client.d.ts +10 -0
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +30 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +13 -2
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +51 -4
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +42 -17
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +229 -122
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/cluster-size-coupling.d.ts +28 -0
- package/dist/src/cluster/cluster-size-coupling.d.ts.map +1 -0
- package/dist/src/cluster/cluster-size-coupling.js +35 -0
- package/dist/src/cluster/cluster-size-coupling.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +6 -0
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +1 -1
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +15 -3
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +32 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +43 -2
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +22 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +56 -10
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +9 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +134 -7
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +174 -37
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +3 -2
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +859 -778
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts +17 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -2
- package/dist/src/logger.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +2 -0
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +4 -0
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/optimystic-node.d.ts +35 -0
- package/dist/src/optimystic-node.d.ts.map +1 -0
- package/dist/src/optimystic-node.js +2 -0
- package/dist/src/optimystic-node.js.map +1 -0
- package/dist/src/owned-block-seed.d.ts +6 -3
- package/dist/src/owned-block-seed.d.ts.map +1 -1
- package/dist/src/owned-block-seed.js +16 -3
- package/dist/src/owned-block-seed.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +72 -0
- package/dist/src/peer-address-book.d.ts.map +1 -0
- package/dist/src/peer-address-book.js +123 -0
- package/dist/src/peer-address-book.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +11 -2
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +30 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +95 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +78 -14
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +266 -81
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +57 -5
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +83 -0
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/cached-raw-storage.js +152 -0
- package/dist/src/storage/cached-raw-storage.js.map +1 -0
- package/dist/src/storage/cached-store-driver.d.ts +186 -0
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
- package/dist/src/storage/cached-store-driver.js +775 -0
- package/dist/src/storage/cached-store-driver.js.map +1 -0
- package/dist/src/storage/i-block-storage.d.ts +20 -1
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +12 -5
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +234 -0
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
- package/dist/src/storage/shared-cache-pool.js +354 -0
- package/dist/src/storage/shared-cache-pool.js.map +1 -0
- package/dist/src/storage/storage-repo.d.ts +56 -3
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +124 -18
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +52 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -653
- package/src/cluster/block-transfer.ts +424 -424
- package/src/cluster/client.ts +119 -88
- package/src/cluster/cluster-error.ts +64 -64
- package/src/cluster/cluster-policy.ts +203 -152
- package/src/cluster/cluster-repo.ts +245 -125
- package/src/cluster/cluster-size-coupling.ts +45 -0
- package/src/cluster/commit-cert.ts +139 -139
- package/src/cluster/i-transaction-state-store.ts +43 -43
- package/src/cluster/memory-transaction-state-store.ts +56 -56
- package/src/cluster/peer-key-binding.ts +37 -37
- package/src/cluster/persistent-transaction-state-store.ts +92 -92
- package/src/cluster/quorum-restore.ts +223 -223
- package/src/cluster/reconcile-block.ts +203 -191
- package/src/cluster/service.ts +293 -241
- package/src/cluster/supermajority-coupling.ts +37 -37
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
- package/src/cohort-topic/change-bridge.ts +109 -109
- package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
- package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
- package/src/cohort-topic/fret-trust-anchor.ts +153 -153
- package/src/cohort-topic/host.ts +2901 -2901
- package/src/cohort-topic/index.ts +13 -13
- package/src/cohort-topic/membership-publish-sink.ts +20 -20
- package/src/cohort-topic/membership-source.ts +68 -68
- package/src/cohort-topic/peer-codec.ts +31 -31
- package/src/cohort-topic/peer-sig.ts +86 -86
- package/src/cohort-topic/protocols.ts +71 -71
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
- package/src/cohort-topic/size-estimator.ts +16 -16
- package/src/cohort-topic/stream-util.ts +135 -87
- package/src/cohort-topic/threshold-crypto.ts +239 -239
- package/src/cohort-topic/topic-router.ts +77 -77
- package/src/dispute/arbitrator-selection.ts +138 -138
- package/src/dispute/cascade.ts +524 -524
- package/src/dispute/dispute-service.ts +11 -5
- package/src/dispute/invalidation.ts +625 -625
- package/src/inbound-authorization.ts +190 -190
- package/src/index.ts +52 -47
- package/src/libp2p-key-network.ts +1120 -958
- package/src/libp2p-node-base.ts +1675 -1591
- package/src/libp2p-node-rn.ts +30 -30
- package/src/libp2p-node.ts +36 -36
- package/src/logger.ts +19 -2
- package/src/matchmaking/aggregate-counts.ts +104 -104
- package/src/matchmaking/index.ts +20 -20
- package/src/matchmaking/module.ts +363 -363
- package/src/matchmaking/protocols.ts +51 -51
- package/src/matchmaking/provider-manager.ts +95 -95
- package/src/matchmaking/query-handler.ts +88 -88
- package/src/matchmaking/query-transport.ts +492 -492
- package/src/matchmaking/seeker-manager.ts +64 -64
- package/src/matchmaking/seeker-walk-client.ts +293 -293
- package/src/matchmaking/traffic-validation.ts +195 -195
- package/src/network/network-manager-service.ts +5 -0
- package/src/optimystic-node.ts +36 -0
- package/src/owned-block-seed.ts +53 -40
- package/src/peer-address-book.ts +149 -0
- package/src/protocol-limits.ts +33 -33
- package/src/reactivity/forwarder-host.ts +438 -438
- package/src/reactivity/index.ts +19 -19
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/origination-manager.ts +192 -192
- package/src/reactivity/protocols.ts +61 -61
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +408 -408
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
- package/src/reactivity/subscriber-registry.ts +96 -96
- package/src/reactivity/subscription-manager.ts +450 -450
- package/src/reactivity/topic-bytes.ts +37 -37
- package/src/repo/client.ts +12 -2
- package/src/repo/cluster-coordinator.ts +99 -3
- package/src/repo/coordinator-repo.ts +305 -82
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -34
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/block-storage.ts +59 -6
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- package/src/storage/i-block-storage.ts +20 -1
- package/src/storage/i-kv-store.ts +8 -8
- package/src/storage/i-raw-storage.ts +12 -5
- package/src/storage/kv-raw-storage.ts +135 -135
- package/src/storage/memory-kv-store.ts +28 -28
- package/src/storage/memory-storage.ts +25 -25
- package/src/storage/memory-store-driver.ts +157 -157
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +80 -80
- package/src/storage/ring-selector.ts +317 -317
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/shared-cache-pool.ts +452 -0
- package/src/storage/storage-repo.ts +1014 -903
- package/src/testing/cohort-topic-mesh-harness.ts +663 -663
- package/src/testing/index.ts +8 -8
- package/src/testing/matchmaking-mesh-harness.ts +475 -475
- package/src/testing/raw-storage-conformance.ts +453 -397
- package/src/testing/reactivity-mesh-harness.ts +922 -922
- package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
- package/dist/src/storage/restoration-coordinator-v2.js +0 -172
- package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import type { IKVStore } from "../storage/i-kv-store.js";
|
|
2
|
-
import type { ITransactionStateStore, PersistedCoordinatorState, PersistedParticipantState } from "./i-transaction-state-store.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* ITransactionStateStore backed by an IKVStore for cross-platform persistence.
|
|
6
|
-
*
|
|
7
|
-
* Key namespace:
|
|
8
|
-
* coordinator/{messageHash} → JSON(PersistedCoordinatorState)
|
|
9
|
-
* participant/{messageHash} → JSON(PersistedParticipantState)
|
|
10
|
-
* executed/{messageHash} → JSON({ timestamp: number })
|
|
11
|
-
*/
|
|
12
|
-
export class PersistentTransactionStateStore implements ITransactionStateStore {
|
|
13
|
-
constructor(private readonly kv: IKVStore) {}
|
|
14
|
-
|
|
15
|
-
// --- Coordinator ---
|
|
16
|
-
|
|
17
|
-
async saveCoordinatorState(messageHash: string, state: PersistedCoordinatorState): Promise<void> {
|
|
18
|
-
await this.kv.set(`coordinator/${messageHash}`, JSON.stringify(state));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async getCoordinatorState(messageHash: string): Promise<PersistedCoordinatorState | undefined> {
|
|
22
|
-
const raw = await this.kv.get(`coordinator/${messageHash}`);
|
|
23
|
-
return raw ? JSON.parse(raw) as PersistedCoordinatorState : undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async deleteCoordinatorState(messageHash: string): Promise<void> {
|
|
27
|
-
await this.kv.delete(`coordinator/${messageHash}`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async getAllCoordinatorStates(): Promise<PersistedCoordinatorState[]> {
|
|
31
|
-
const keys = await this.kv.list('coordinator/');
|
|
32
|
-
const results: PersistedCoordinatorState[] = [];
|
|
33
|
-
for (const key of keys) {
|
|
34
|
-
const raw = await this.kv.get(key);
|
|
35
|
-
if (raw) {
|
|
36
|
-
results.push(JSON.parse(raw) as PersistedCoordinatorState);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return results;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// --- Participant ---
|
|
43
|
-
|
|
44
|
-
async saveParticipantState(messageHash: string, state: PersistedParticipantState): Promise<void> {
|
|
45
|
-
await this.kv.set(`participant/${messageHash}`, JSON.stringify(state));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async getParticipantState(messageHash: string): Promise<PersistedParticipantState | undefined> {
|
|
49
|
-
const raw = await this.kv.get(`participant/${messageHash}`);
|
|
50
|
-
return raw ? JSON.parse(raw) as PersistedParticipantState : undefined;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async deleteParticipantState(messageHash: string): Promise<void> {
|
|
54
|
-
await this.kv.delete(`participant/${messageHash}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async getAllParticipantStates(): Promise<PersistedParticipantState[]> {
|
|
58
|
-
const keys = await this.kv.list('participant/');
|
|
59
|
-
const results: PersistedParticipantState[] = [];
|
|
60
|
-
for (const key of keys) {
|
|
61
|
-
const raw = await this.kv.get(key);
|
|
62
|
-
if (raw) {
|
|
63
|
-
results.push(JSON.parse(raw) as PersistedParticipantState);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return results;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// --- Executed ---
|
|
70
|
-
|
|
71
|
-
async markExecuted(messageHash: string, timestamp: number): Promise<void> {
|
|
72
|
-
await this.kv.set(`executed/${messageHash}`, JSON.stringify({ timestamp }));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async wasExecuted(messageHash: string): Promise<boolean> {
|
|
76
|
-
const raw = await this.kv.get(`executed/${messageHash}`);
|
|
77
|
-
return raw !== undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async pruneExecuted(olderThan: number): Promise<void> {
|
|
81
|
-
const keys = await this.kv.list('executed/');
|
|
82
|
-
for (const key of keys) {
|
|
83
|
-
const raw = await this.kv.get(key);
|
|
84
|
-
if (raw) {
|
|
85
|
-
const { timestamp } = JSON.parse(raw) as { timestamp: number };
|
|
86
|
-
if (timestamp < olderThan) {
|
|
87
|
-
await this.kv.delete(key);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
1
|
+
import type { IKVStore } from "../storage/i-kv-store.js";
|
|
2
|
+
import type { ITransactionStateStore, PersistedCoordinatorState, PersistedParticipantState } from "./i-transaction-state-store.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ITransactionStateStore backed by an IKVStore for cross-platform persistence.
|
|
6
|
+
*
|
|
7
|
+
* Key namespace:
|
|
8
|
+
* coordinator/{messageHash} → JSON(PersistedCoordinatorState)
|
|
9
|
+
* participant/{messageHash} → JSON(PersistedParticipantState)
|
|
10
|
+
* executed/{messageHash} → JSON({ timestamp: number })
|
|
11
|
+
*/
|
|
12
|
+
export class PersistentTransactionStateStore implements ITransactionStateStore {
|
|
13
|
+
constructor(private readonly kv: IKVStore) {}
|
|
14
|
+
|
|
15
|
+
// --- Coordinator ---
|
|
16
|
+
|
|
17
|
+
async saveCoordinatorState(messageHash: string, state: PersistedCoordinatorState): Promise<void> {
|
|
18
|
+
await this.kv.set(`coordinator/${messageHash}`, JSON.stringify(state));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getCoordinatorState(messageHash: string): Promise<PersistedCoordinatorState | undefined> {
|
|
22
|
+
const raw = await this.kv.get(`coordinator/${messageHash}`);
|
|
23
|
+
return raw ? JSON.parse(raw) as PersistedCoordinatorState : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async deleteCoordinatorState(messageHash: string): Promise<void> {
|
|
27
|
+
await this.kv.delete(`coordinator/${messageHash}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getAllCoordinatorStates(): Promise<PersistedCoordinatorState[]> {
|
|
31
|
+
const keys = await this.kv.list('coordinator/');
|
|
32
|
+
const results: PersistedCoordinatorState[] = [];
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const raw = await this.kv.get(key);
|
|
35
|
+
if (raw) {
|
|
36
|
+
results.push(JSON.parse(raw) as PersistedCoordinatorState);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Participant ---
|
|
43
|
+
|
|
44
|
+
async saveParticipantState(messageHash: string, state: PersistedParticipantState): Promise<void> {
|
|
45
|
+
await this.kv.set(`participant/${messageHash}`, JSON.stringify(state));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getParticipantState(messageHash: string): Promise<PersistedParticipantState | undefined> {
|
|
49
|
+
const raw = await this.kv.get(`participant/${messageHash}`);
|
|
50
|
+
return raw ? JSON.parse(raw) as PersistedParticipantState : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async deleteParticipantState(messageHash: string): Promise<void> {
|
|
54
|
+
await this.kv.delete(`participant/${messageHash}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getAllParticipantStates(): Promise<PersistedParticipantState[]> {
|
|
58
|
+
const keys = await this.kv.list('participant/');
|
|
59
|
+
const results: PersistedParticipantState[] = [];
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
const raw = await this.kv.get(key);
|
|
62
|
+
if (raw) {
|
|
63
|
+
results.push(JSON.parse(raw) as PersistedParticipantState);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Executed ---
|
|
70
|
+
|
|
71
|
+
async markExecuted(messageHash: string, timestamp: number): Promise<void> {
|
|
72
|
+
await this.kv.set(`executed/${messageHash}`, JSON.stringify({ timestamp }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async wasExecuted(messageHash: string): Promise<boolean> {
|
|
76
|
+
const raw = await this.kv.get(`executed/${messageHash}`);
|
|
77
|
+
return raw !== undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async pruneExecuted(olderThan: number): Promise<void> {
|
|
81
|
+
const keys = await this.kv.list('executed/');
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
const raw = await this.kv.get(key);
|
|
84
|
+
if (raw) {
|
|
85
|
+
const { timestamp } = JSON.parse(raw) as { timestamp: number };
|
|
86
|
+
if (timestamp < olderThan) {
|
|
87
|
+
await this.kv.delete(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -1,223 +1,223 @@
|
|
|
1
|
-
import type { IBlock } from "@optimystic/db-core";
|
|
2
|
-
import { sha256 } from "multiformats/hashes/sha2";
|
|
3
|
-
import { toString as uint8ArrayToString } from 'uint8arrays';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Quorum-corroboration helpers shared by the two block-restoration paths:
|
|
7
|
-
* - read-repair (`CoordinatorRepo.queryClusterForLatest`)
|
|
8
|
-
* - reconcile (`libp2p-node-base.reconcileBlock`)
|
|
9
|
-
*
|
|
10
|
-
* Both paths previously trusted a single peer's self-reported "latest" (max rev
|
|
11
|
-
* wins), so one lying peer could steer restoration. These helpers replace
|
|
12
|
-
* "max wins" with "highest value corroborated by a quorum of distinct peers".
|
|
13
|
-
*
|
|
14
|
-
* NOTE: the quorum here is corroboration-of-a-claim, NOT Sybil-resistant cohort
|
|
15
|
-
* membership. A peer minting fresh keypairs can still cast a vote — proving a
|
|
16
|
-
* voter is a legitimate cohort member requires commit-cert + membership
|
|
17
|
-
* anchoring, deferred to backlog `debt-read-repair-commit-cert-verification`.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/** A single peer's self-reported (rev, actionId) for a block. */
|
|
21
|
-
export interface RevClaim {
|
|
22
|
-
/** Distinct voter identity (peer-id string). One vote per distinct peerId per group. */
|
|
23
|
-
peerId: string;
|
|
24
|
-
rev: number;
|
|
25
|
-
actionId: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** The (rev, actionId) pair a quorum agreed on, plus the peers that corroborated it. */
|
|
29
|
-
export interface QuorumRev {
|
|
30
|
-
rev: number;
|
|
31
|
-
actionId: string;
|
|
32
|
-
/** Distinct peer-ids that voted for this exact (rev, actionId). */
|
|
33
|
-
supporters: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Votes a claim needs when the cohort is big enough to supply them: a claim must be
|
|
38
|
-
* seconded by a second, independent peer. See {@link quorumSize} for the cap that
|
|
39
|
-
* applies when the cohort is smaller than this.
|
|
40
|
-
*/
|
|
41
|
-
const CORROBORATION_FLOOR = 2;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Number of distinct corroborating votes required to accept a claim:
|
|
45
|
-
* `floor(simpleMajorityThreshold × responderCount)`, never below
|
|
46
|
-
* {@link CORROBORATION_FLOOR} — except that demanding more corroborators than the cohort
|
|
47
|
-
* can possibly supply is a deadlock, not a safety property, so the floor is additionally
|
|
48
|
-
* capped at `corroboratorCapacity`.
|
|
49
|
-
*
|
|
50
|
-
* `corroboratorCapacity` is how many peers OTHER than the asking node could answer at
|
|
51
|
-
* all. Omit it when the caller cannot state one; the absolute floor of two then applies,
|
|
52
|
-
* which is the conservative direction (a claim no one seconded is never accepted).
|
|
53
|
-
*
|
|
54
|
-
* Note the capacity caps the FLOOR only, never the proportional term: with many
|
|
55
|
-
* responders the majority requirement still grows past two.
|
|
56
|
-
*/
|
|
57
|
-
export function quorumSize(
|
|
58
|
-
responderCount: number,
|
|
59
|
-
simpleMajorityThreshold: number,
|
|
60
|
-
corroboratorCapacity: number = Number.POSITIVE_INFINITY
|
|
61
|
-
): number {
|
|
62
|
-
const floor = Math.max(1, Math.min(CORROBORATION_FLOOR, corroboratorCapacity));
|
|
63
|
-
return Math.max(floor, Math.floor(simpleMajorityThreshold * responderCount));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The `corroboratorCapacity` to hand {@link quorumSize}: how many peers other than the asking node
|
|
68
|
-
* could answer for a block at all, given `cohortPeerCount` peers currently visible (self already
|
|
69
|
-
* excluded) and `repairCorroborationClusterSize` — the cohort size this deployment is measured
|
|
70
|
-
* against, resolved by `resolveClusterPolicy` in `cluster/cluster-policy.ts`.
|
|
71
|
-
*
|
|
72
|
-
* Deliberately the MAX of the two: the corroboration floor may be relaxed only for a cohort that is
|
|
73
|
-
* *genuinely* small, never for one that merely looks small. Cohort views are unauthenticated — the
|
|
74
|
-
* read path takes them from `IKeyNetwork.findCluster`, the commit path from a coordinator-declared
|
|
75
|
-
* peer set — so a partition, a self-shrunk record, or an attacker with routing influence could
|
|
76
|
-
* otherwise talk the requirement down to a single voter. Measuring against the resolved size keeps a
|
|
77
|
-
* shrunken view out of the relaxed branch.
|
|
78
|
-
*
|
|
79
|
-
* An unconfigured node resolves this to its `clusterSize` (default 10), so the floor of two binds and
|
|
80
|
-
* a shrunken view gains nothing. The escape hatch for a real two-node deployment is one explicit
|
|
81
|
-
* operator declaration — `clusterPolicy.assumedClusterSize: 2`, which does NOT also drop the
|
|
82
|
-
* replication factor, or an honest `clusterSize: 2`.
|
|
83
|
-
*
|
|
84
|
-
* Shared by both restoration paths so the two can never drift apart on the rule that decides how
|
|
85
|
-
* much trust a lone peer gets.
|
|
86
|
-
*/
|
|
87
|
-
export function corroboratorCapacity(cohortPeerCount: number, repairCorroborationClusterSize: number): number {
|
|
88
|
-
return Math.max(cohortPeerCount, repairCorroborationClusterSize - 1);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Select the highest revision corroborated by a quorum of distinct peers.
|
|
93
|
-
*
|
|
94
|
-
* Claims are grouped by the exact `(rev, actionId)` pair; a liar's fabricated
|
|
95
|
-
* pair lands in its own singleton group and is outvoted. The highest rev whose
|
|
96
|
-
* group has `>= quorum` distinct voters wins.
|
|
97
|
-
*
|
|
98
|
-
* **Claims must not include the asking node's own revision.** A node confirming its own
|
|
99
|
-
* answer is not evidence; its revision is the baseline being repaired, and counting it
|
|
100
|
-
* both inflates the responder count and lets a lone reader "corroborate" itself. Callers
|
|
101
|
-
* filter self out before calling (see `CoordinatorRepo.queryClusterForLatest`,
|
|
102
|
-
* `reconcileBlock`).
|
|
103
|
-
*
|
|
104
|
-
* `corroboratorCapacity` — how many peers other than the asking node could corroborate at
|
|
105
|
-
* all — lets a genuinely tiny cohort still converge: a cohort with exactly one other peer
|
|
106
|
-
* cannot produce two corroborators, so requiring two makes divergence permanent rather
|
|
107
|
-
* than making it safe. Pass a capacity that a shrunken view of the network cannot talk
|
|
108
|
-
* down (see {@link corroboratorCapacity}), or omit it to keep the floor at two.
|
|
109
|
-
*
|
|
110
|
-
* Returns `undefined` when nothing is corroborated — an uncorroborated claim
|
|
111
|
-
* must never drive restoration.
|
|
112
|
-
*/
|
|
113
|
-
export function selectQuorumRev(
|
|
114
|
-
claims: RevClaim[],
|
|
115
|
-
simpleMajorityThreshold: number,
|
|
116
|
-
corroboratorCapacity?: number
|
|
117
|
-
): QuorumRev | undefined {
|
|
118
|
-
if (claims.length === 0) return undefined;
|
|
119
|
-
|
|
120
|
-
const groups = new Map<string, { rev: number; actionId: string; supporters: Set<string> }>();
|
|
121
|
-
const responders = new Set<string>();
|
|
122
|
-
for (const c of claims) {
|
|
123
|
-
responders.add(c.peerId);
|
|
124
|
-
const key = `${c.rev}\0${c.actionId}`;
|
|
125
|
-
let g = groups.get(key);
|
|
126
|
-
if (!g) {
|
|
127
|
-
g = { rev: c.rev, actionId: c.actionId, supporters: new Set() };
|
|
128
|
-
groups.set(key, g);
|
|
129
|
-
}
|
|
130
|
-
g.supporters.add(c.peerId);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const quorum = quorumSize(responders.size, simpleMajorityThreshold, corroboratorCapacity);
|
|
134
|
-
|
|
135
|
-
// Highest rev whose (rev, actionId) group meets quorum. There is deliberately no
|
|
136
|
-
// "too few responders, but they all agree" fallback: that rule fired at ANY cohort
|
|
137
|
-
// size, so a lone responder in a large cohort — where a second corroborator does
|
|
138
|
-
// exist and simply did not answer within the per-peer timeout — was accepted on its
|
|
139
|
-
// own word. `corroboratorCapacity` expresses the same permissiveness where it is
|
|
140
|
-
// actually justified (a cohort that cannot supply a second corroborator) and nowhere
|
|
141
|
-
// else.
|
|
142
|
-
let best: { rev: number; actionId: string; supporters: Set<string> } | undefined;
|
|
143
|
-
for (const g of groups.values()) {
|
|
144
|
-
if (g.supporters.size >= quorum && (!best || g.rev > best.rev)) {
|
|
145
|
-
best = g;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return best
|
|
149
|
-
? { rev: best.rev, actionId: best.actionId, supporters: [...best.supporters] }
|
|
150
|
-
: undefined;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Deterministic JSON: sorts object keys so the hash is order-independent. Mirrors `ClusterMember.canonicalJson`. */
|
|
154
|
-
function canonicalJson(value: unknown): string {
|
|
155
|
-
return JSON.stringify(value, (_, v) =>
|
|
156
|
-
v && typeof v === 'object' && !Array.isArray(v)
|
|
157
|
-
? Object.keys(v).sort().reduce((o: Record<string, unknown>, k) => { o[k] = v[k]; return o; }, {})
|
|
158
|
-
: v
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** Canonical sha256 of a block's content, base64url. Same pattern as `ClusterMember.computeCommitHash`. */
|
|
163
|
-
export async function canonicalBlockHash(block: IBlock): Promise<string> {
|
|
164
|
-
const bytes = new TextEncoder().encode(canonicalJson(block));
|
|
165
|
-
const digest = await sha256.digest(bytes);
|
|
166
|
-
return uint8ArrayToString(digest.digest, 'base64url');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** One block candidate paired with its serving peer and canonical hash. */
|
|
170
|
-
export interface BlockHashCandidate {
|
|
171
|
-
peerId: string;
|
|
172
|
-
hash: string;
|
|
173
|
-
block: IBlock;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Among candidates already known to corroborate the target `(rev, actionId)`,
|
|
178
|
-
* pick the block content agreed by a quorum. Requires a UNIQUE hash group meeting
|
|
179
|
-
* `quorum` — a cohort split on content declines rather than picking a side.
|
|
180
|
-
* Returns the agreed block, or `undefined`.
|
|
181
|
-
*
|
|
182
|
-
* `corroboratorCapacity` caps the floor exactly as it does in {@link selectQuorumRev}, and for
|
|
183
|
-
* the same reason: a cohort holding one other peer cannot produce two block-carriers, so
|
|
184
|
-
* demanding two makes the block permanently unrestorable on this node rather than making it safe.
|
|
185
|
-
* Omit it to keep the floor at two.
|
|
186
|
-
*
|
|
187
|
-
* **What relaxing this does and does not cost.** Block ids are random 256-bit strings, not
|
|
188
|
-
* content-addressed, so {@link canonicalBlockHash} is a cross-peer *agreement* hash and never a
|
|
189
|
-
* check against the requested id — nothing here re-derives an id from received bytes. At capacity
|
|
190
|
-
* one the sole peer's content is therefore taken on its word. That extends no trust the cohort had
|
|
191
|
-
* not already extended: the same peer's `(rev, actionId)` claim is equally uncorroborable at that
|
|
192
|
-
* size, and a two-member cohort has no honest majority to appeal to. Pass a capacity a shrunken
|
|
193
|
-
* view of the network cannot talk down (see {@link corroboratorCapacity}), so only a cohort that is
|
|
194
|
-
* *genuinely* that small reaches this branch.
|
|
195
|
-
*/
|
|
196
|
-
export function selectQuorumBlock(
|
|
197
|
-
candidates: BlockHashCandidate[],
|
|
198
|
-
simpleMajorityThreshold: number,
|
|
199
|
-
corroboratorCapacity?: number
|
|
200
|
-
): { block: IBlock; hash: string } | undefined {
|
|
201
|
-
if (candidates.length === 0) return undefined;
|
|
202
|
-
|
|
203
|
-
// One vote per distinct peer per hash group, matching selectQuorumRev — a peer appearing twice
|
|
204
|
-
// must not be able to second itself into a content quorum.
|
|
205
|
-
const groups = new Map<string, { block: IBlock; supporters: Set<string> }>();
|
|
206
|
-
const voters = new Set<string>();
|
|
207
|
-
for (const c of candidates) {
|
|
208
|
-
voters.add(c.peerId);
|
|
209
|
-
let g = groups.get(c.hash);
|
|
210
|
-
if (!g) {
|
|
211
|
-
g = { block: c.block, supporters: new Set() };
|
|
212
|
-
groups.set(c.hash, g);
|
|
213
|
-
}
|
|
214
|
-
g.supporters.add(c.peerId);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const quorum = quorumSize(voters.size, simpleMajorityThreshold, corroboratorCapacity);
|
|
218
|
-
const meeting = [...groups.entries()].filter(([, g]) => g.supporters.size >= quorum);
|
|
219
|
-
// Exactly one hash may meet quorum; a tie is a genuine content disagreement → decline.
|
|
220
|
-
if (meeting.length !== 1) return undefined;
|
|
221
|
-
const [hash, group] = meeting[0]!;
|
|
222
|
-
return { block: group.block, hash };
|
|
223
|
-
}
|
|
1
|
+
import type { IBlock } from "@optimystic/db-core";
|
|
2
|
+
import { sha256 } from "multiformats/hashes/sha2";
|
|
3
|
+
import { toString as uint8ArrayToString } from 'uint8arrays';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Quorum-corroboration helpers shared by the two block-restoration paths:
|
|
7
|
+
* - read-repair (`CoordinatorRepo.queryClusterForLatest`)
|
|
8
|
+
* - reconcile (`libp2p-node-base.reconcileBlock`)
|
|
9
|
+
*
|
|
10
|
+
* Both paths previously trusted a single peer's self-reported "latest" (max rev
|
|
11
|
+
* wins), so one lying peer could steer restoration. These helpers replace
|
|
12
|
+
* "max wins" with "highest value corroborated by a quorum of distinct peers".
|
|
13
|
+
*
|
|
14
|
+
* NOTE: the quorum here is corroboration-of-a-claim, NOT Sybil-resistant cohort
|
|
15
|
+
* membership. A peer minting fresh keypairs can still cast a vote — proving a
|
|
16
|
+
* voter is a legitimate cohort member requires commit-cert + membership
|
|
17
|
+
* anchoring, deferred to backlog `debt-read-repair-commit-cert-verification`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** A single peer's self-reported (rev, actionId) for a block. */
|
|
21
|
+
export interface RevClaim {
|
|
22
|
+
/** Distinct voter identity (peer-id string). One vote per distinct peerId per group. */
|
|
23
|
+
peerId: string;
|
|
24
|
+
rev: number;
|
|
25
|
+
actionId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The (rev, actionId) pair a quorum agreed on, plus the peers that corroborated it. */
|
|
29
|
+
export interface QuorumRev {
|
|
30
|
+
rev: number;
|
|
31
|
+
actionId: string;
|
|
32
|
+
/** Distinct peer-ids that voted for this exact (rev, actionId). */
|
|
33
|
+
supporters: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Votes a claim needs when the cohort is big enough to supply them: a claim must be
|
|
38
|
+
* seconded by a second, independent peer. See {@link quorumSize} for the cap that
|
|
39
|
+
* applies when the cohort is smaller than this.
|
|
40
|
+
*/
|
|
41
|
+
export const CORROBORATION_FLOOR = 2;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Number of distinct corroborating votes required to accept a claim:
|
|
45
|
+
* `floor(simpleMajorityThreshold × responderCount)`, never below
|
|
46
|
+
* {@link CORROBORATION_FLOOR} — except that demanding more corroborators than the cohort
|
|
47
|
+
* can possibly supply is a deadlock, not a safety property, so the floor is additionally
|
|
48
|
+
* capped at `corroboratorCapacity`.
|
|
49
|
+
*
|
|
50
|
+
* `corroboratorCapacity` is how many peers OTHER than the asking node could answer at
|
|
51
|
+
* all. Omit it when the caller cannot state one; the absolute floor of two then applies,
|
|
52
|
+
* which is the conservative direction (a claim no one seconded is never accepted).
|
|
53
|
+
*
|
|
54
|
+
* Note the capacity caps the FLOOR only, never the proportional term: with many
|
|
55
|
+
* responders the majority requirement still grows past two.
|
|
56
|
+
*/
|
|
57
|
+
export function quorumSize(
|
|
58
|
+
responderCount: number,
|
|
59
|
+
simpleMajorityThreshold: number,
|
|
60
|
+
corroboratorCapacity: number = Number.POSITIVE_INFINITY
|
|
61
|
+
): number {
|
|
62
|
+
const floor = Math.max(1, Math.min(CORROBORATION_FLOOR, corroboratorCapacity));
|
|
63
|
+
return Math.max(floor, Math.floor(simpleMajorityThreshold * responderCount));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The `corroboratorCapacity` to hand {@link quorumSize}: how many peers other than the asking node
|
|
68
|
+
* could answer for a block at all, given `cohortPeerCount` peers currently visible (self already
|
|
69
|
+
* excluded) and `repairCorroborationClusterSize` — the cohort size this deployment is measured
|
|
70
|
+
* against, resolved by `resolveClusterPolicy` in `cluster/cluster-policy.ts`.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately the MAX of the two: the corroboration floor may be relaxed only for a cohort that is
|
|
73
|
+
* *genuinely* small, never for one that merely looks small. Cohort views are unauthenticated — the
|
|
74
|
+
* read path takes them from `IKeyNetwork.findCluster`, the commit path from a coordinator-declared
|
|
75
|
+
* peer set — so a partition, a self-shrunk record, or an attacker with routing influence could
|
|
76
|
+
* otherwise talk the requirement down to a single voter. Measuring against the resolved size keeps a
|
|
77
|
+
* shrunken view out of the relaxed branch.
|
|
78
|
+
*
|
|
79
|
+
* An unconfigured node resolves this to its `clusterSize` (default 10), so the floor of two binds and
|
|
80
|
+
* a shrunken view gains nothing. The escape hatch for a real two-node deployment is one explicit
|
|
81
|
+
* operator declaration — `clusterPolicy.assumedClusterSize: 2`, which does NOT also drop the
|
|
82
|
+
* replication factor, or an honest `clusterSize: 2`.
|
|
83
|
+
*
|
|
84
|
+
* Shared by both restoration paths so the two can never drift apart on the rule that decides how
|
|
85
|
+
* much trust a lone peer gets.
|
|
86
|
+
*/
|
|
87
|
+
export function corroboratorCapacity(cohortPeerCount: number, repairCorroborationClusterSize: number): number {
|
|
88
|
+
return Math.max(cohortPeerCount, repairCorroborationClusterSize - 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Select the highest revision corroborated by a quorum of distinct peers.
|
|
93
|
+
*
|
|
94
|
+
* Claims are grouped by the exact `(rev, actionId)` pair; a liar's fabricated
|
|
95
|
+
* pair lands in its own singleton group and is outvoted. The highest rev whose
|
|
96
|
+
* group has `>= quorum` distinct voters wins.
|
|
97
|
+
*
|
|
98
|
+
* **Claims must not include the asking node's own revision.** A node confirming its own
|
|
99
|
+
* answer is not evidence; its revision is the baseline being repaired, and counting it
|
|
100
|
+
* both inflates the responder count and lets a lone reader "corroborate" itself. Callers
|
|
101
|
+
* filter self out before calling (see `CoordinatorRepo.queryClusterForLatest`,
|
|
102
|
+
* `reconcileBlock`).
|
|
103
|
+
*
|
|
104
|
+
* `corroboratorCapacity` — how many peers other than the asking node could corroborate at
|
|
105
|
+
* all — lets a genuinely tiny cohort still converge: a cohort with exactly one other peer
|
|
106
|
+
* cannot produce two corroborators, so requiring two makes divergence permanent rather
|
|
107
|
+
* than making it safe. Pass a capacity that a shrunken view of the network cannot talk
|
|
108
|
+
* down (see {@link corroboratorCapacity}), or omit it to keep the floor at two.
|
|
109
|
+
*
|
|
110
|
+
* Returns `undefined` when nothing is corroborated — an uncorroborated claim
|
|
111
|
+
* must never drive restoration.
|
|
112
|
+
*/
|
|
113
|
+
export function selectQuorumRev(
|
|
114
|
+
claims: RevClaim[],
|
|
115
|
+
simpleMajorityThreshold: number,
|
|
116
|
+
corroboratorCapacity?: number
|
|
117
|
+
): QuorumRev | undefined {
|
|
118
|
+
if (claims.length === 0) return undefined;
|
|
119
|
+
|
|
120
|
+
const groups = new Map<string, { rev: number; actionId: string; supporters: Set<string> }>();
|
|
121
|
+
const responders = new Set<string>();
|
|
122
|
+
for (const c of claims) {
|
|
123
|
+
responders.add(c.peerId);
|
|
124
|
+
const key = `${c.rev}\0${c.actionId}`;
|
|
125
|
+
let g = groups.get(key);
|
|
126
|
+
if (!g) {
|
|
127
|
+
g = { rev: c.rev, actionId: c.actionId, supporters: new Set() };
|
|
128
|
+
groups.set(key, g);
|
|
129
|
+
}
|
|
130
|
+
g.supporters.add(c.peerId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const quorum = quorumSize(responders.size, simpleMajorityThreshold, corroboratorCapacity);
|
|
134
|
+
|
|
135
|
+
// Highest rev whose (rev, actionId) group meets quorum. There is deliberately no
|
|
136
|
+
// "too few responders, but they all agree" fallback: that rule fired at ANY cohort
|
|
137
|
+
// size, so a lone responder in a large cohort — where a second corroborator does
|
|
138
|
+
// exist and simply did not answer within the per-peer timeout — was accepted on its
|
|
139
|
+
// own word. `corroboratorCapacity` expresses the same permissiveness where it is
|
|
140
|
+
// actually justified (a cohort that cannot supply a second corroborator) and nowhere
|
|
141
|
+
// else.
|
|
142
|
+
let best: { rev: number; actionId: string; supporters: Set<string> } | undefined;
|
|
143
|
+
for (const g of groups.values()) {
|
|
144
|
+
if (g.supporters.size >= quorum && (!best || g.rev > best.rev)) {
|
|
145
|
+
best = g;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return best
|
|
149
|
+
? { rev: best.rev, actionId: best.actionId, supporters: [...best.supporters] }
|
|
150
|
+
: undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Deterministic JSON: sorts object keys so the hash is order-independent. Mirrors `ClusterMember.canonicalJson`. */
|
|
154
|
+
function canonicalJson(value: unknown): string {
|
|
155
|
+
return JSON.stringify(value, (_, v) =>
|
|
156
|
+
v && typeof v === 'object' && !Array.isArray(v)
|
|
157
|
+
? Object.keys(v).sort().reduce((o: Record<string, unknown>, k) => { o[k] = v[k]; return o; }, {})
|
|
158
|
+
: v
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Canonical sha256 of a block's content, base64url. Same pattern as `ClusterMember.computeCommitHash`. */
|
|
163
|
+
export async function canonicalBlockHash(block: IBlock): Promise<string> {
|
|
164
|
+
const bytes = new TextEncoder().encode(canonicalJson(block));
|
|
165
|
+
const digest = await sha256.digest(bytes);
|
|
166
|
+
return uint8ArrayToString(digest.digest, 'base64url');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** One block candidate paired with its serving peer and canonical hash. */
|
|
170
|
+
export interface BlockHashCandidate {
|
|
171
|
+
peerId: string;
|
|
172
|
+
hash: string;
|
|
173
|
+
block: IBlock;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Among candidates already known to corroborate the target `(rev, actionId)`,
|
|
178
|
+
* pick the block content agreed by a quorum. Requires a UNIQUE hash group meeting
|
|
179
|
+
* `quorum` — a cohort split on content declines rather than picking a side.
|
|
180
|
+
* Returns the agreed block, or `undefined`.
|
|
181
|
+
*
|
|
182
|
+
* `corroboratorCapacity` caps the floor exactly as it does in {@link selectQuorumRev}, and for
|
|
183
|
+
* the same reason: a cohort holding one other peer cannot produce two block-carriers, so
|
|
184
|
+
* demanding two makes the block permanently unrestorable on this node rather than making it safe.
|
|
185
|
+
* Omit it to keep the floor at two.
|
|
186
|
+
*
|
|
187
|
+
* **What relaxing this does and does not cost.** Block ids are random 256-bit strings, not
|
|
188
|
+
* content-addressed, so {@link canonicalBlockHash} is a cross-peer *agreement* hash and never a
|
|
189
|
+
* check against the requested id — nothing here re-derives an id from received bytes. At capacity
|
|
190
|
+
* one the sole peer's content is therefore taken on its word. That extends no trust the cohort had
|
|
191
|
+
* not already extended: the same peer's `(rev, actionId)` claim is equally uncorroborable at that
|
|
192
|
+
* size, and a two-member cohort has no honest majority to appeal to. Pass a capacity a shrunken
|
|
193
|
+
* view of the network cannot talk down (see {@link corroboratorCapacity}), so only a cohort that is
|
|
194
|
+
* *genuinely* that small reaches this branch.
|
|
195
|
+
*/
|
|
196
|
+
export function selectQuorumBlock(
|
|
197
|
+
candidates: BlockHashCandidate[],
|
|
198
|
+
simpleMajorityThreshold: number,
|
|
199
|
+
corroboratorCapacity?: number
|
|
200
|
+
): { block: IBlock; hash: string } | undefined {
|
|
201
|
+
if (candidates.length === 0) return undefined;
|
|
202
|
+
|
|
203
|
+
// One vote per distinct peer per hash group, matching selectQuorumRev — a peer appearing twice
|
|
204
|
+
// must not be able to second itself into a content quorum.
|
|
205
|
+
const groups = new Map<string, { block: IBlock; supporters: Set<string> }>();
|
|
206
|
+
const voters = new Set<string>();
|
|
207
|
+
for (const c of candidates) {
|
|
208
|
+
voters.add(c.peerId);
|
|
209
|
+
let g = groups.get(c.hash);
|
|
210
|
+
if (!g) {
|
|
211
|
+
g = { block: c.block, supporters: new Set() };
|
|
212
|
+
groups.set(c.hash, g);
|
|
213
|
+
}
|
|
214
|
+
g.supporters.add(c.peerId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const quorum = quorumSize(voters.size, simpleMajorityThreshold, corroboratorCapacity);
|
|
218
|
+
const meeting = [...groups.entries()].filter(([, g]) => g.supporters.size >= quorum);
|
|
219
|
+
// Exactly one hash may meet quorum; a tie is a genuine content disagreement → decline.
|
|
220
|
+
if (meeting.length !== 1) return undefined;
|
|
221
|
+
const [hash, group] = meeting[0]!;
|
|
222
|
+
return { block: group.block, hash };
|
|
223
|
+
}
|