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