@optimystic/db-p2p 0.22.0 → 0.24.1
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-repo.d.ts +39 -14
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +226 -119
- package/dist/src/cluster/cluster-repo.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/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +37 -7
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +77 -19
- 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 +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +88 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +134 -28
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +25 -1
- package/dist/src/libp2p-node-base.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/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- 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/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- 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 +62 -9
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +242 -73
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +3 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +3 -0
- package/dist/src/rn.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-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/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.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 +35 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -668
- 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 -203
- package/src/cluster/cluster-repo.ts +242 -122
- package/src/cluster/cluster-size-coupling.ts +45 -45
- 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 -203
- 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 +42 -11
- 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 +79 -19
- 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 -49
- package/src/libp2p-key-network.ts +1120 -990
- package/src/libp2p-node-base.ts +1675 -1651
- 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 +3 -3
- 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/optimystic-node.ts +36 -36
- 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 +7 -3
- 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 +281 -74
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -36
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- 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 -1014
- package/src/testing/cohort-topic-mesh-harness.ts +673 -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 -417
- 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,139 +1,139 @@
|
|
|
1
|
-
import type { ActionId, ClusterRecord, CollectionChangeEvent, CommitCert } from "@optimystic/db-core";
|
|
2
|
-
import { fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
3
|
-
import { createLogger } from "../logger.js";
|
|
4
|
-
|
|
5
|
-
const log = createLogger('commit-cert');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Build the {@link CommitCert} for a consensus-committed action from its {@link ClusterRecord}.
|
|
9
|
-
*
|
|
10
|
-
* The threshold signature is the cluster's `approve` **commit** signatures — the per-member Ed25519
|
|
11
|
-
* commit votes the cohort already produced over the commit hash — concatenated in ascending
|
|
12
|
-
* signer-id order, so the blob is reproducible and `signers[i]` aligns with chunk `i` (the
|
|
13
|
-
* collected-multisig convention the cohort-topic verifier uses). This is the **authoritative**
|
|
14
|
-
* commit cert: reactivity reuses these exact bytes as a notification's signature and never re-signs.
|
|
15
|
-
*
|
|
16
|
-
* `signedPayload` is the exact byte preimage every approving member signed — the cluster commit-vote
|
|
17
|
-
* payload `utf8(commitHash + ":approve")`, identical across signers. The caller supplies it (this stays
|
|
18
|
-
* pure and does **not** recompute the commit hash, since the canonical-JSON helper that derives it is
|
|
19
|
-
* private to `ClusterMember` and duplicating it here risks drift). Reactivity sets a notification's
|
|
20
|
-
* `digest` to base64url(signedPayload) so a subscriber's threshold-verify over `digest` reproduces the
|
|
21
|
-
* exact signed image. Pure.
|
|
22
|
-
*/
|
|
23
|
-
export function buildCommitCert(record: ClusterRecord, minSigs: number, signedPayload: Uint8Array): CommitCert {
|
|
24
|
-
const approvals = Object.entries(record.commits)
|
|
25
|
-
.filter(([, sig]) => sig.type === 'approve')
|
|
26
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
27
|
-
const signers = approvals.map(([peerId]) => peerId);
|
|
28
|
-
const thresholdSig = concatBytes(approvals.map(([, sig]) => uint8ArrayFromString(sig.signature, 'base64url')));
|
|
29
|
-
return { thresholdSig, signers, minSigs, signedPayload };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The deterministic change-event/notification `actionId` for a durable invalidation, derived from the
|
|
34
|
-
* `(invalidatedActionId, disputeId)` pair that uniquely identifies the reversal. An invalidation has no
|
|
35
|
-
* transaction `actionId` of its own (it reverses one), but reactivity keys both the captured commit cert
|
|
36
|
-
* and the emitted {@link CollectionChangeEvent} by an `actionId` so the bridge's cert extractor can
|
|
37
|
-
* resolve the reused cert. Both the cluster member's cert capture and the invalidation-apply sink that
|
|
38
|
-
* emits the change event derive this same id from the same pair, closing the loop. Distinct from the
|
|
39
|
-
* per-block deterministic revert `actionId` in `dispute/invalidation.ts` (which also carries blockId+rev);
|
|
40
|
-
* this is the single transaction-level id for the whole reversal.
|
|
41
|
-
*/
|
|
42
|
-
export function invalidationActionId(invalidatedActionId: ActionId, disputeId: string): ActionId {
|
|
43
|
-
return `inv:${invalidatedActionId}:${disputeId}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
|
|
47
|
-
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
48
|
-
const out = new Uint8Array(total);
|
|
49
|
-
let offset = 0;
|
|
50
|
-
for (const part of parts) {
|
|
51
|
-
out.set(part, offset);
|
|
52
|
-
offset += part.length;
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Default retention for a captured commit cert — long enough for the post-commit change event to
|
|
58
|
-
* resolve it, short enough that the store stays bounded under sustained commit load. */
|
|
59
|
-
export const DEFAULT_COMMIT_CERT_TTL_MS = 60_000;
|
|
60
|
-
|
|
61
|
-
/** Default cap on retained certs — a backstop against unbounded growth if commits ever stop emitting. */
|
|
62
|
-
export const DEFAULT_COMMIT_CERT_MAX_ENTRIES = 4096;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Bounded, TTL'd cache of recently-committed {@link CommitCert}s keyed by `actionId`. The cluster
|
|
66
|
-
* member records a cert here just before consensus applies the commit to local storage; the
|
|
67
|
-
* change-notifier bridge's extractor reads it when the StorageRepo emits the matching
|
|
68
|
-
* {@link CollectionChangeEvent}. Both happen on the same node within one commit, so the window is
|
|
69
|
-
* tiny — but a TTL + cap keep the map from leaking if a commit ever lands without emitting a change
|
|
70
|
-
* event (e.g. an idempotent re-commit, or a path with no catch-all subscriber).
|
|
71
|
-
*/
|
|
72
|
-
export interface CommitCertStore {
|
|
73
|
-
/** Record `cert` for `actionId` (overwrites any prior entry). `now` is unix ms (injectable for tests). */
|
|
74
|
-
put(actionId: ActionId, cert: CommitCert, now?: number): void;
|
|
75
|
-
/** The cert for `actionId` if still retained, else `undefined`. */
|
|
76
|
-
get(actionId: ActionId, now?: number): CommitCert | undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface CommitCertStoreOptions {
|
|
80
|
-
/** Retention per entry (ms). Default {@link DEFAULT_COMMIT_CERT_TTL_MS}. */
|
|
81
|
-
readonly ttlMs?: number;
|
|
82
|
-
/** Maximum retained entries (oldest evicted first). Default {@link DEFAULT_COMMIT_CERT_MAX_ENTRIES}. */
|
|
83
|
-
readonly maxEntries?: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Build an in-memory {@link CommitCertStore} (insertion-ordered Map → recency for eviction). */
|
|
87
|
-
export function createCommitCertStore(options: CommitCertStoreOptions = {}): CommitCertStore {
|
|
88
|
-
const ttlMs = options.ttlMs ?? DEFAULT_COMMIT_CERT_TTL_MS;
|
|
89
|
-
const maxEntries = options.maxEntries ?? DEFAULT_COMMIT_CERT_MAX_ENTRIES;
|
|
90
|
-
const entries = new Map<ActionId, { cert: CommitCert; expiresAt: number }>();
|
|
91
|
-
|
|
92
|
-
const dropExpired = (now: number): void => {
|
|
93
|
-
for (const [actionId, entry] of entries) {
|
|
94
|
-
if (entry.expiresAt <= now) {
|
|
95
|
-
entries.delete(actionId);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
put(actionId, cert, now = Date.now()): void {
|
|
102
|
-
dropExpired(now);
|
|
103
|
-
// Re-insert so a refreshed key moves to the most-recent end (Map preserves insertion order).
|
|
104
|
-
entries.delete(actionId);
|
|
105
|
-
entries.set(actionId, { cert, expiresAt: now + ttlMs });
|
|
106
|
-
while (entries.size > maxEntries) {
|
|
107
|
-
const oldest = entries.keys().next().value;
|
|
108
|
-
if (oldest === undefined) break;
|
|
109
|
-
entries.delete(oldest);
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
get(actionId, now = Date.now()): CommitCert | undefined {
|
|
113
|
-
const entry = entries.get(actionId);
|
|
114
|
-
if (!entry) {
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
if (entry.expiresAt <= now) {
|
|
118
|
-
entries.delete(actionId);
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
return entry.cert;
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* The bridge's `extractCommitCert` over a {@link CommitCertStore}: resolve a change event's committed
|
|
128
|
-
* action to the cluster commit cert captured for it. Returns `undefined` (logged) when no cert is
|
|
129
|
-
* retained — the bridge then skips origination rather than fabricating an unsigned notification.
|
|
130
|
-
*/
|
|
131
|
-
export function makeClusterCommitCertExtractor(store: CommitCertStore): (event: CollectionChangeEvent) => CommitCert | undefined {
|
|
132
|
-
return (event) => {
|
|
133
|
-
const cert = store.get(event.actionId);
|
|
134
|
-
if (!cert) {
|
|
135
|
-
log('no commit cert retained for actionId=%s (collection=%s rev=%d); skipping origination', event.actionId, event.collectionId, event.rev);
|
|
136
|
-
}
|
|
137
|
-
return cert;
|
|
138
|
-
};
|
|
139
|
-
}
|
|
1
|
+
import type { ActionId, ClusterRecord, CollectionChangeEvent, CommitCert } from "@optimystic/db-core";
|
|
2
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
3
|
+
import { createLogger } from "../logger.js";
|
|
4
|
+
|
|
5
|
+
const log = createLogger('commit-cert');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build the {@link CommitCert} for a consensus-committed action from its {@link ClusterRecord}.
|
|
9
|
+
*
|
|
10
|
+
* The threshold signature is the cluster's `approve` **commit** signatures — the per-member Ed25519
|
|
11
|
+
* commit votes the cohort already produced over the commit hash — concatenated in ascending
|
|
12
|
+
* signer-id order, so the blob is reproducible and `signers[i]` aligns with chunk `i` (the
|
|
13
|
+
* collected-multisig convention the cohort-topic verifier uses). This is the **authoritative**
|
|
14
|
+
* commit cert: reactivity reuses these exact bytes as a notification's signature and never re-signs.
|
|
15
|
+
*
|
|
16
|
+
* `signedPayload` is the exact byte preimage every approving member signed — the cluster commit-vote
|
|
17
|
+
* payload `utf8(commitHash + ":approve")`, identical across signers. The caller supplies it (this stays
|
|
18
|
+
* pure and does **not** recompute the commit hash, since the canonical-JSON helper that derives it is
|
|
19
|
+
* private to `ClusterMember` and duplicating it here risks drift). Reactivity sets a notification's
|
|
20
|
+
* `digest` to base64url(signedPayload) so a subscriber's threshold-verify over `digest` reproduces the
|
|
21
|
+
* exact signed image. Pure.
|
|
22
|
+
*/
|
|
23
|
+
export function buildCommitCert(record: ClusterRecord, minSigs: number, signedPayload: Uint8Array): CommitCert {
|
|
24
|
+
const approvals = Object.entries(record.commits)
|
|
25
|
+
.filter(([, sig]) => sig.type === 'approve')
|
|
26
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
27
|
+
const signers = approvals.map(([peerId]) => peerId);
|
|
28
|
+
const thresholdSig = concatBytes(approvals.map(([, sig]) => uint8ArrayFromString(sig.signature, 'base64url')));
|
|
29
|
+
return { thresholdSig, signers, minSigs, signedPayload };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The deterministic change-event/notification `actionId` for a durable invalidation, derived from the
|
|
34
|
+
* `(invalidatedActionId, disputeId)` pair that uniquely identifies the reversal. An invalidation has no
|
|
35
|
+
* transaction `actionId` of its own (it reverses one), but reactivity keys both the captured commit cert
|
|
36
|
+
* and the emitted {@link CollectionChangeEvent} by an `actionId` so the bridge's cert extractor can
|
|
37
|
+
* resolve the reused cert. Both the cluster member's cert capture and the invalidation-apply sink that
|
|
38
|
+
* emits the change event derive this same id from the same pair, closing the loop. Distinct from the
|
|
39
|
+
* per-block deterministic revert `actionId` in `dispute/invalidation.ts` (which also carries blockId+rev);
|
|
40
|
+
* this is the single transaction-level id for the whole reversal.
|
|
41
|
+
*/
|
|
42
|
+
export function invalidationActionId(invalidatedActionId: ActionId, disputeId: string): ActionId {
|
|
43
|
+
return `inv:${invalidatedActionId}:${disputeId}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
|
|
47
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
48
|
+
const out = new Uint8Array(total);
|
|
49
|
+
let offset = 0;
|
|
50
|
+
for (const part of parts) {
|
|
51
|
+
out.set(part, offset);
|
|
52
|
+
offset += part.length;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Default retention for a captured commit cert — long enough for the post-commit change event to
|
|
58
|
+
* resolve it, short enough that the store stays bounded under sustained commit load. */
|
|
59
|
+
export const DEFAULT_COMMIT_CERT_TTL_MS = 60_000;
|
|
60
|
+
|
|
61
|
+
/** Default cap on retained certs — a backstop against unbounded growth if commits ever stop emitting. */
|
|
62
|
+
export const DEFAULT_COMMIT_CERT_MAX_ENTRIES = 4096;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bounded, TTL'd cache of recently-committed {@link CommitCert}s keyed by `actionId`. The cluster
|
|
66
|
+
* member records a cert here just before consensus applies the commit to local storage; the
|
|
67
|
+
* change-notifier bridge's extractor reads it when the StorageRepo emits the matching
|
|
68
|
+
* {@link CollectionChangeEvent}. Both happen on the same node within one commit, so the window is
|
|
69
|
+
* tiny — but a TTL + cap keep the map from leaking if a commit ever lands without emitting a change
|
|
70
|
+
* event (e.g. an idempotent re-commit, or a path with no catch-all subscriber).
|
|
71
|
+
*/
|
|
72
|
+
export interface CommitCertStore {
|
|
73
|
+
/** Record `cert` for `actionId` (overwrites any prior entry). `now` is unix ms (injectable for tests). */
|
|
74
|
+
put(actionId: ActionId, cert: CommitCert, now?: number): void;
|
|
75
|
+
/** The cert for `actionId` if still retained, else `undefined`. */
|
|
76
|
+
get(actionId: ActionId, now?: number): CommitCert | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CommitCertStoreOptions {
|
|
80
|
+
/** Retention per entry (ms). Default {@link DEFAULT_COMMIT_CERT_TTL_MS}. */
|
|
81
|
+
readonly ttlMs?: number;
|
|
82
|
+
/** Maximum retained entries (oldest evicted first). Default {@link DEFAULT_COMMIT_CERT_MAX_ENTRIES}. */
|
|
83
|
+
readonly maxEntries?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Build an in-memory {@link CommitCertStore} (insertion-ordered Map → recency for eviction). */
|
|
87
|
+
export function createCommitCertStore(options: CommitCertStoreOptions = {}): CommitCertStore {
|
|
88
|
+
const ttlMs = options.ttlMs ?? DEFAULT_COMMIT_CERT_TTL_MS;
|
|
89
|
+
const maxEntries = options.maxEntries ?? DEFAULT_COMMIT_CERT_MAX_ENTRIES;
|
|
90
|
+
const entries = new Map<ActionId, { cert: CommitCert; expiresAt: number }>();
|
|
91
|
+
|
|
92
|
+
const dropExpired = (now: number): void => {
|
|
93
|
+
for (const [actionId, entry] of entries) {
|
|
94
|
+
if (entry.expiresAt <= now) {
|
|
95
|
+
entries.delete(actionId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
put(actionId, cert, now = Date.now()): void {
|
|
102
|
+
dropExpired(now);
|
|
103
|
+
// Re-insert so a refreshed key moves to the most-recent end (Map preserves insertion order).
|
|
104
|
+
entries.delete(actionId);
|
|
105
|
+
entries.set(actionId, { cert, expiresAt: now + ttlMs });
|
|
106
|
+
while (entries.size > maxEntries) {
|
|
107
|
+
const oldest = entries.keys().next().value;
|
|
108
|
+
if (oldest === undefined) break;
|
|
109
|
+
entries.delete(oldest);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
get(actionId, now = Date.now()): CommitCert | undefined {
|
|
113
|
+
const entry = entries.get(actionId);
|
|
114
|
+
if (!entry) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (entry.expiresAt <= now) {
|
|
118
|
+
entries.delete(actionId);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
return entry.cert;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The bridge's `extractCommitCert` over a {@link CommitCertStore}: resolve a change event's committed
|
|
128
|
+
* action to the cluster commit cert captured for it. Returns `undefined` (logged) when no cert is
|
|
129
|
+
* retained — the bridge then skips origination rather than fabricating an unsigned notification.
|
|
130
|
+
*/
|
|
131
|
+
export function makeClusterCommitCertExtractor(store: CommitCertStore): (event: CollectionChangeEvent) => CommitCert | undefined {
|
|
132
|
+
return (event) => {
|
|
133
|
+
const cert = store.get(event.actionId);
|
|
134
|
+
if (!cert) {
|
|
135
|
+
log('no commit cert retained for actionId=%s (collection=%s rev=%d); skipping origination', event.actionId, event.collectionId, event.rev);
|
|
136
|
+
}
|
|
137
|
+
return cert;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import type { ClusterRecord } from "@optimystic/db-core";
|
|
2
|
-
|
|
3
|
-
/** Serializable coordinator transaction state (excludes timers, Pending wrapper) */
|
|
4
|
-
export interface PersistedCoordinatorState {
|
|
5
|
-
messageHash: string;
|
|
6
|
-
record: ClusterRecord;
|
|
7
|
-
lastUpdate: number;
|
|
8
|
-
/** Which phase was reached when last persisted */
|
|
9
|
-
phase: 'promising' | 'committing' | 'broadcasting';
|
|
10
|
-
/** Retry state for commit broadcast failures (excludes timer) */
|
|
11
|
-
retryState?: {
|
|
12
|
-
pendingPeers: string[];
|
|
13
|
-
attempt: number;
|
|
14
|
-
intervalMs: number;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Serializable participant transaction state (excludes timers) */
|
|
19
|
-
export interface PersistedParticipantState {
|
|
20
|
-
messageHash: string;
|
|
21
|
-
record: ClusterRecord;
|
|
22
|
-
lastUpdate: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Platform-agnostic store for persisting 2PC transaction state. */
|
|
26
|
-
export interface ITransactionStateStore {
|
|
27
|
-
// --- Coordinator state (keyed by messageHash) ---
|
|
28
|
-
saveCoordinatorState(messageHash: string, state: PersistedCoordinatorState): Promise<void>;
|
|
29
|
-
getCoordinatorState(messageHash: string): Promise<PersistedCoordinatorState | undefined>;
|
|
30
|
-
deleteCoordinatorState(messageHash: string): Promise<void>;
|
|
31
|
-
getAllCoordinatorStates(): Promise<PersistedCoordinatorState[]>;
|
|
32
|
-
|
|
33
|
-
// --- Participant state (keyed by messageHash) ---
|
|
34
|
-
saveParticipantState(messageHash: string, state: PersistedParticipantState): Promise<void>;
|
|
35
|
-
getParticipantState(messageHash: string): Promise<PersistedParticipantState | undefined>;
|
|
36
|
-
deleteParticipantState(messageHash: string): Promise<void>;
|
|
37
|
-
getAllParticipantStates(): Promise<PersistedParticipantState[]>;
|
|
38
|
-
|
|
39
|
-
// --- Executed transaction dedup guard ---
|
|
40
|
-
markExecuted(messageHash: string, timestamp: number): Promise<void>;
|
|
41
|
-
wasExecuted(messageHash: string): Promise<boolean>;
|
|
42
|
-
pruneExecuted(olderThan: number): Promise<void>;
|
|
43
|
-
}
|
|
1
|
+
import type { ClusterRecord } from "@optimystic/db-core";
|
|
2
|
+
|
|
3
|
+
/** Serializable coordinator transaction state (excludes timers, Pending wrapper) */
|
|
4
|
+
export interface PersistedCoordinatorState {
|
|
5
|
+
messageHash: string;
|
|
6
|
+
record: ClusterRecord;
|
|
7
|
+
lastUpdate: number;
|
|
8
|
+
/** Which phase was reached when last persisted */
|
|
9
|
+
phase: 'promising' | 'committing' | 'broadcasting';
|
|
10
|
+
/** Retry state for commit broadcast failures (excludes timer) */
|
|
11
|
+
retryState?: {
|
|
12
|
+
pendingPeers: string[];
|
|
13
|
+
attempt: number;
|
|
14
|
+
intervalMs: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Serializable participant transaction state (excludes timers) */
|
|
19
|
+
export interface PersistedParticipantState {
|
|
20
|
+
messageHash: string;
|
|
21
|
+
record: ClusterRecord;
|
|
22
|
+
lastUpdate: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Platform-agnostic store for persisting 2PC transaction state. */
|
|
26
|
+
export interface ITransactionStateStore {
|
|
27
|
+
// --- Coordinator state (keyed by messageHash) ---
|
|
28
|
+
saveCoordinatorState(messageHash: string, state: PersistedCoordinatorState): Promise<void>;
|
|
29
|
+
getCoordinatorState(messageHash: string): Promise<PersistedCoordinatorState | undefined>;
|
|
30
|
+
deleteCoordinatorState(messageHash: string): Promise<void>;
|
|
31
|
+
getAllCoordinatorStates(): Promise<PersistedCoordinatorState[]>;
|
|
32
|
+
|
|
33
|
+
// --- Participant state (keyed by messageHash) ---
|
|
34
|
+
saveParticipantState(messageHash: string, state: PersistedParticipantState): Promise<void>;
|
|
35
|
+
getParticipantState(messageHash: string): Promise<PersistedParticipantState | undefined>;
|
|
36
|
+
deleteParticipantState(messageHash: string): Promise<void>;
|
|
37
|
+
getAllParticipantStates(): Promise<PersistedParticipantState[]>;
|
|
38
|
+
|
|
39
|
+
// --- Executed transaction dedup guard ---
|
|
40
|
+
markExecuted(messageHash: string, timestamp: number): Promise<void>;
|
|
41
|
+
wasExecuted(messageHash: string): Promise<boolean>;
|
|
42
|
+
pruneExecuted(olderThan: number): Promise<void>;
|
|
43
|
+
}
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import type { ITransactionStateStore, PersistedCoordinatorState, PersistedParticipantState } from "./i-transaction-state-store.js";
|
|
2
|
-
|
|
3
|
-
/** In-memory ITransactionStateStore. Default when no persistent store is injected. */
|
|
4
|
-
export class MemoryTransactionStateStore implements ITransactionStateStore {
|
|
5
|
-
private readonly coordinatorStates = new Map<string, PersistedCoordinatorState>();
|
|
6
|
-
private readonly participantStates = new Map<string, PersistedParticipantState>();
|
|
7
|
-
private readonly executedMap = new Map<string, number>();
|
|
8
|
-
|
|
9
|
-
async saveCoordinatorState(messageHash: string, state: PersistedCoordinatorState): Promise<void> {
|
|
10
|
-
this.coordinatorStates.set(messageHash, state);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async getCoordinatorState(messageHash: string): Promise<PersistedCoordinatorState | undefined> {
|
|
14
|
-
return this.coordinatorStates.get(messageHash);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async deleteCoordinatorState(messageHash: string): Promise<void> {
|
|
18
|
-
this.coordinatorStates.delete(messageHash);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async getAllCoordinatorStates(): Promise<PersistedCoordinatorState[]> {
|
|
22
|
-
return Array.from(this.coordinatorStates.values());
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async saveParticipantState(messageHash: string, state: PersistedParticipantState): Promise<void> {
|
|
26
|
-
this.participantStates.set(messageHash, state);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async getParticipantState(messageHash: string): Promise<PersistedParticipantState | undefined> {
|
|
30
|
-
return this.participantStates.get(messageHash);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async deleteParticipantState(messageHash: string): Promise<void> {
|
|
34
|
-
this.participantStates.delete(messageHash);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async getAllParticipantStates(): Promise<PersistedParticipantState[]> {
|
|
38
|
-
return Array.from(this.participantStates.values());
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async markExecuted(messageHash: string, timestamp: number): Promise<void> {
|
|
42
|
-
this.executedMap.set(messageHash, timestamp);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async wasExecuted(messageHash: string): Promise<boolean> {
|
|
46
|
-
return this.executedMap.has(messageHash);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async pruneExecuted(olderThan: number): Promise<void> {
|
|
50
|
-
for (const [hash, ts] of this.executedMap) {
|
|
51
|
-
if (ts < olderThan) {
|
|
52
|
-
this.executedMap.delete(hash);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
import type { ITransactionStateStore, PersistedCoordinatorState, PersistedParticipantState } from "./i-transaction-state-store.js";
|
|
2
|
+
|
|
3
|
+
/** In-memory ITransactionStateStore. Default when no persistent store is injected. */
|
|
4
|
+
export class MemoryTransactionStateStore implements ITransactionStateStore {
|
|
5
|
+
private readonly coordinatorStates = new Map<string, PersistedCoordinatorState>();
|
|
6
|
+
private readonly participantStates = new Map<string, PersistedParticipantState>();
|
|
7
|
+
private readonly executedMap = new Map<string, number>();
|
|
8
|
+
|
|
9
|
+
async saveCoordinatorState(messageHash: string, state: PersistedCoordinatorState): Promise<void> {
|
|
10
|
+
this.coordinatorStates.set(messageHash, state);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getCoordinatorState(messageHash: string): Promise<PersistedCoordinatorState | undefined> {
|
|
14
|
+
return this.coordinatorStates.get(messageHash);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async deleteCoordinatorState(messageHash: string): Promise<void> {
|
|
18
|
+
this.coordinatorStates.delete(messageHash);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getAllCoordinatorStates(): Promise<PersistedCoordinatorState[]> {
|
|
22
|
+
return Array.from(this.coordinatorStates.values());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async saveParticipantState(messageHash: string, state: PersistedParticipantState): Promise<void> {
|
|
26
|
+
this.participantStates.set(messageHash, state);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getParticipantState(messageHash: string): Promise<PersistedParticipantState | undefined> {
|
|
30
|
+
return this.participantStates.get(messageHash);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async deleteParticipantState(messageHash: string): Promise<void> {
|
|
34
|
+
this.participantStates.delete(messageHash);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getAllParticipantStates(): Promise<PersistedParticipantState[]> {
|
|
38
|
+
return Array.from(this.participantStates.values());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async markExecuted(messageHash: string, timestamp: number): Promise<void> {
|
|
42
|
+
this.executedMap.set(messageHash, timestamp);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async wasExecuted(messageHash: string): Promise<boolean> {
|
|
46
|
+
return this.executedMap.has(messageHash);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async pruneExecuted(olderThan: number): Promise<void> {
|
|
50
|
+
for (const [hash, ts] of this.executedMap) {
|
|
51
|
+
if (ts < olderThan) {
|
|
52
|
+
this.executedMap.delete(hash);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Key ↔ peer-id binding predicate for the cluster/dispute vote paths.
|
|
3
|
-
*
|
|
4
|
-
* For a libp2p Ed25519 identity the peer-id **is** the multihash of the public key, so a public key
|
|
5
|
-
* that a record self-asserts for a peer id can be checked against the id itself — no network lookup.
|
|
6
|
-
* Both the cluster two-phase-commit path (`cluster-repo.ts`) and the dispute path (`dispute-service.ts`)
|
|
7
|
-
* read a signing key straight from the record they are handed; without this check a coordinator could
|
|
8
|
-
* attach a key it controls under an honest peer's id and have a forged vote verify. This predicate is
|
|
9
|
-
* the gate that ties a self-asserted key back to the identity it claims.
|
|
10
|
-
*
|
|
11
|
-
* Mirrors the binding logic already used by the cohort-topic substrate in `cohort-topic/peer-sig.ts`
|
|
12
|
-
* (`peerIdFromString(str).publicKey.raw`); intentionally NOT importing that module, which is
|
|
13
|
-
* specialized for its own wire encoding.
|
|
14
|
-
*
|
|
15
|
-
* NOTE: this proves only that a vote attributed to peer id `X` was signed by the key `X` names. It does
|
|
16
|
-
* NOT decide whether `X` is legitimately a cohort member — a coordinator that mints fresh keypairs and
|
|
17
|
-
* uses each key's own derived id passes this check for every one of them. Sybil/membership is a separate
|
|
18
|
-
* layer (cohort-topic membership certificates); do not assume it is solved here.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { peerIdFromString } from "@libp2p/peer-id";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* True iff `rawKey` is the Ed25519 public key that `peerIdStr` names. Total: returns `false` on a
|
|
25
|
-
* non-Ed25519 id, a missing/mismatched key, or any malformed input — never throws.
|
|
26
|
-
*/
|
|
27
|
-
export function peerIdBindsPublicKey(peerIdStr: string, rawKey: Uint8Array): boolean {
|
|
28
|
-
try {
|
|
29
|
-
const peerId = peerIdFromString(peerIdStr);
|
|
30
|
-
if (peerId.type !== 'Ed25519' || peerId.publicKey === undefined) return false;
|
|
31
|
-
const expected = peerId.publicKey.raw; // 32 bytes for Ed25519
|
|
32
|
-
if (expected.length !== rawKey.length) return false;
|
|
33
|
-
return expected.every((b, i) => b === rawKey[i]);
|
|
34
|
-
} catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Key ↔ peer-id binding predicate for the cluster/dispute vote paths.
|
|
3
|
+
*
|
|
4
|
+
* For a libp2p Ed25519 identity the peer-id **is** the multihash of the public key, so a public key
|
|
5
|
+
* that a record self-asserts for a peer id can be checked against the id itself — no network lookup.
|
|
6
|
+
* Both the cluster two-phase-commit path (`cluster-repo.ts`) and the dispute path (`dispute-service.ts`)
|
|
7
|
+
* read a signing key straight from the record they are handed; without this check a coordinator could
|
|
8
|
+
* attach a key it controls under an honest peer's id and have a forged vote verify. This predicate is
|
|
9
|
+
* the gate that ties a self-asserted key back to the identity it claims.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the binding logic already used by the cohort-topic substrate in `cohort-topic/peer-sig.ts`
|
|
12
|
+
* (`peerIdFromString(str).publicKey.raw`); intentionally NOT importing that module, which is
|
|
13
|
+
* specialized for its own wire encoding.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: this proves only that a vote attributed to peer id `X` was signed by the key `X` names. It does
|
|
16
|
+
* NOT decide whether `X` is legitimately a cohort member — a coordinator that mints fresh keypairs and
|
|
17
|
+
* uses each key's own derived id passes this check for every one of them. Sybil/membership is a separate
|
|
18
|
+
* layer (cohort-topic membership certificates); do not assume it is solved here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True iff `rawKey` is the Ed25519 public key that `peerIdStr` names. Total: returns `false` on a
|
|
25
|
+
* non-Ed25519 id, a missing/mismatched key, or any malformed input — never throws.
|
|
26
|
+
*/
|
|
27
|
+
export function peerIdBindsPublicKey(peerIdStr: string, rawKey: Uint8Array): boolean {
|
|
28
|
+
try {
|
|
29
|
+
const peerId = peerIdFromString(peerIdStr);
|
|
30
|
+
if (peerId.type !== 'Ed25519' || peerId.publicKey === undefined) return false;
|
|
31
|
+
const expected = peerId.publicKey.raw; // 32 bytes for Ed25519
|
|
32
|
+
if (expected.length !== rawKey.length) return false;
|
|
33
|
+
return expected.every((b, i) => b === rawKey[i]);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|