@optimystic/db-p2p 0.21.0 → 0.24.0

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