@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.
Files changed (194) hide show
  1. package/dist/src/cluster/client.d.ts +10 -0
  2. package/dist/src/cluster/client.d.ts.map +1 -1
  3. package/dist/src/cluster/client.js +30 -1
  4. package/dist/src/cluster/client.js.map +1 -1
  5. package/dist/src/cluster/cluster-repo.d.ts +39 -14
  6. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-repo.js +226 -119
  8. package/dist/src/cluster/cluster-repo.js.map +1 -1
  9. package/dist/src/cluster/service.d.ts +32 -1
  10. package/dist/src/cluster/service.d.ts.map +1 -1
  11. package/dist/src/cluster/service.js +43 -2
  12. package/dist/src/cluster/service.js.map +1 -1
  13. package/dist/src/cohort-topic/host.js +34 -11
  14. package/dist/src/cohort-topic/host.js.map +1 -1
  15. package/dist/src/cohort-topic/stream-util.d.ts +37 -7
  16. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  17. package/dist/src/cohort-topic/stream-util.js +77 -19
  18. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  19. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  20. package/dist/src/dispute/dispute-service.js +9 -3
  21. package/dist/src/dispute/dispute-service.js.map +1 -1
  22. package/dist/src/index.d.ts +3 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +3 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/libp2p-key-network.d.ts +88 -2
  27. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  28. package/dist/src/libp2p-key-network.js +134 -28
  29. package/dist/src/libp2p-key-network.js.map +1 -1
  30. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  31. package/dist/src/libp2p-node-base.js +25 -1
  32. package/dist/src/libp2p-node-base.js.map +1 -1
  33. package/dist/src/logger.d.ts +17 -1
  34. package/dist/src/logger.d.ts.map +1 -1
  35. package/dist/src/logger.js +19 -2
  36. package/dist/src/logger.js.map +1 -1
  37. package/dist/src/matchmaking/query-transport.js +3 -3
  38. package/dist/src/matchmaking/query-transport.js.map +1 -1
  39. package/dist/src/owned-block-seed.d.ts +6 -3
  40. package/dist/src/owned-block-seed.d.ts.map +1 -1
  41. package/dist/src/owned-block-seed.js +16 -3
  42. package/dist/src/owned-block-seed.js.map +1 -1
  43. package/dist/src/peer-address-book.d.ts +72 -0
  44. package/dist/src/peer-address-book.d.ts.map +1 -0
  45. package/dist/src/peer-address-book.js +123 -0
  46. package/dist/src/peer-address-book.js.map +1 -0
  47. package/dist/src/reactivity/notify-transport.d.ts +4 -4
  48. package/dist/src/reactivity/notify-transport.js +6 -6
  49. package/dist/src/reactivity/notify-transport.js.map +1 -1
  50. package/dist/src/reactivity/push-state-gossip.js +2 -2
  51. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  52. package/dist/src/reactivity/recover-transport.d.ts +6 -2
  53. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  54. package/dist/src/reactivity/recover-transport.js +7 -3
  55. package/dist/src/reactivity/recover-transport.js.map +1 -1
  56. package/dist/src/repo/client.d.ts.map +1 -1
  57. package/dist/src/repo/client.js +11 -2
  58. package/dist/src/repo/client.js.map +1 -1
  59. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  60. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  61. package/dist/src/repo/cluster-coordinator.js +95 -3
  62. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  63. package/dist/src/repo/coordinator-repo.d.ts +62 -9
  64. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  65. package/dist/src/repo/coordinator-repo.js +242 -73
  66. package/dist/src/repo/coordinator-repo.js.map +1 -1
  67. package/dist/src/rn.d.ts +3 -0
  68. package/dist/src/rn.d.ts.map +1 -1
  69. package/dist/src/rn.js +3 -0
  70. package/dist/src/rn.js.map +1 -1
  71. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  72. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  73. package/dist/src/storage/cached-raw-storage.js +152 -0
  74. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  75. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  76. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  77. package/dist/src/storage/cached-store-driver.js +775 -0
  78. package/dist/src/storage/cached-store-driver.js.map +1 -0
  79. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  80. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  81. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  82. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  83. package/dist/src/storage/shared-cache-pool.js +354 -0
  84. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  85. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
  86. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
  87. package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
  88. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
  89. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  90. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  91. package/dist/src/testing/raw-storage-conformance.js +35 -2
  92. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  93. package/package.json +3 -3
  94. package/readme.md +668 -668
  95. package/src/cluster/block-transfer.ts +424 -424
  96. package/src/cluster/client.ts +119 -88
  97. package/src/cluster/cluster-error.ts +64 -64
  98. package/src/cluster/cluster-policy.ts +203 -203
  99. package/src/cluster/cluster-repo.ts +242 -122
  100. package/src/cluster/cluster-size-coupling.ts +45 -45
  101. package/src/cluster/commit-cert.ts +139 -139
  102. package/src/cluster/i-transaction-state-store.ts +43 -43
  103. package/src/cluster/memory-transaction-state-store.ts +56 -56
  104. package/src/cluster/peer-key-binding.ts +37 -37
  105. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  106. package/src/cluster/quorum-restore.ts +223 -223
  107. package/src/cluster/reconcile-block.ts +203 -203
  108. package/src/cluster/service.ts +293 -241
  109. package/src/cluster/supermajority-coupling.ts +37 -37
  110. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  111. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  112. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  113. package/src/cohort-topic/change-bridge.ts +109 -109
  114. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  115. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  116. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  117. package/src/cohort-topic/host.ts +42 -11
  118. package/src/cohort-topic/index.ts +13 -13
  119. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  120. package/src/cohort-topic/membership-source.ts +68 -68
  121. package/src/cohort-topic/peer-codec.ts +31 -31
  122. package/src/cohort-topic/peer-sig.ts +86 -86
  123. package/src/cohort-topic/protocols.ts +71 -71
  124. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  125. package/src/cohort-topic/size-estimator.ts +16 -16
  126. package/src/cohort-topic/stream-util.ts +79 -19
  127. package/src/cohort-topic/threshold-crypto.ts +239 -239
  128. package/src/cohort-topic/topic-router.ts +77 -77
  129. package/src/dispute/arbitrator-selection.ts +138 -138
  130. package/src/dispute/cascade.ts +524 -524
  131. package/src/dispute/dispute-service.ts +11 -5
  132. package/src/dispute/invalidation.ts +625 -625
  133. package/src/inbound-authorization.ts +190 -190
  134. package/src/index.ts +52 -49
  135. package/src/libp2p-key-network.ts +1120 -990
  136. package/src/libp2p-node-base.ts +1675 -1651
  137. package/src/libp2p-node-rn.ts +30 -30
  138. package/src/libp2p-node.ts +36 -36
  139. package/src/logger.ts +19 -2
  140. package/src/matchmaking/aggregate-counts.ts +104 -104
  141. package/src/matchmaking/index.ts +20 -20
  142. package/src/matchmaking/module.ts +363 -363
  143. package/src/matchmaking/protocols.ts +51 -51
  144. package/src/matchmaking/provider-manager.ts +95 -95
  145. package/src/matchmaking/query-handler.ts +88 -88
  146. package/src/matchmaking/query-transport.ts +3 -3
  147. package/src/matchmaking/seeker-manager.ts +64 -64
  148. package/src/matchmaking/seeker-walk-client.ts +293 -293
  149. package/src/matchmaking/traffic-validation.ts +195 -195
  150. package/src/optimystic-node.ts +36 -36
  151. package/src/owned-block-seed.ts +53 -40
  152. package/src/peer-address-book.ts +149 -0
  153. package/src/protocol-limits.ts +33 -33
  154. package/src/reactivity/forwarder-host.ts +438 -438
  155. package/src/reactivity/index.ts +19 -19
  156. package/src/reactivity/notify-transport.ts +144 -144
  157. package/src/reactivity/origination-manager.ts +192 -192
  158. package/src/reactivity/protocols.ts +61 -61
  159. package/src/reactivity/push-state-gossip.ts +291 -291
  160. package/src/reactivity/recover-transport.ts +7 -3
  161. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  162. package/src/reactivity/subscriber-registry.ts +96 -96
  163. package/src/reactivity/subscription-manager.ts +450 -450
  164. package/src/reactivity/topic-bytes.ts +37 -37
  165. package/src/repo/client.ts +12 -2
  166. package/src/repo/cluster-coordinator.ts +99 -3
  167. package/src/repo/coordinator-repo.ts +281 -74
  168. package/src/repo/types.ts +7 -7
  169. package/src/rn.ts +39 -36
  170. package/src/rpc-deadline.ts +45 -45
  171. package/src/storage/arachnode-partition.ts +74 -74
  172. package/src/storage/cached-raw-storage.ts +180 -0
  173. package/src/storage/cached-store-driver.ts +859 -0
  174. package/src/storage/i-kv-store.ts +8 -8
  175. package/src/storage/i-raw-storage.ts +12 -5
  176. package/src/storage/kv-raw-storage.ts +135 -135
  177. package/src/storage/memory-kv-store.ts +28 -28
  178. package/src/storage/memory-storage.ts +25 -25
  179. package/src/storage/memory-store-driver.ts +157 -157
  180. package/src/storage/raw-store-codec.ts +42 -42
  181. package/src/storage/raw-store-driver.ts +80 -80
  182. package/src/storage/ring-selector.ts +317 -317
  183. package/src/storage/ring-shift-coordinator.ts +271 -271
  184. package/src/storage/shared-cache-pool.ts +452 -0
  185. package/src/storage/storage-repo.ts +1014 -1014
  186. package/src/testing/cohort-topic-mesh-harness.ts +673 -663
  187. package/src/testing/index.ts +8 -8
  188. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  189. package/src/testing/raw-storage-conformance.ts +453 -417
  190. package/src/testing/reactivity-mesh-harness.ts +922 -922
  191. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  192. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  193. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  194. 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
+ }