@optimystic/db-core 0.24.2 → 0.25.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 (103) hide show
  1. package/dist/src/blocks/helpers.d.ts +5 -0
  2. package/dist/src/blocks/helpers.d.ts.map +1 -1
  3. package/dist/src/blocks/helpers.js +12 -0
  4. package/dist/src/blocks/helpers.js.map +1 -1
  5. package/dist/src/cluster/membership.d.ts +7 -0
  6. package/dist/src/cluster/membership.d.ts.map +1 -1
  7. package/dist/src/cluster/membership.js +12 -8
  8. package/dist/src/cluster/membership.js.map +1 -1
  9. package/dist/src/cluster/structs.d.ts +27 -1
  10. package/dist/src/cluster/structs.d.ts.map +1 -1
  11. package/dist/src/cluster/structs.js.map +1 -1
  12. package/dist/src/collection/action.d.ts +14 -0
  13. package/dist/src/collection/action.d.ts.map +1 -1
  14. package/dist/src/collection/action.js +16 -1
  15. package/dist/src/collection/action.js.map +1 -1
  16. package/dist/src/collection/collection.d.ts +273 -4
  17. package/dist/src/collection/collection.d.ts.map +1 -1
  18. package/dist/src/collection/collection.js +427 -30
  19. package/dist/src/collection/collection.js.map +1 -1
  20. package/dist/src/collections/tree/tree.d.ts +16 -1
  21. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  22. package/dist/src/collections/tree/tree.js +19 -0
  23. package/dist/src/collections/tree/tree.js.map +1 -1
  24. package/dist/src/index.d.ts +1 -0
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/index.js +1 -0
  27. package/dist/src/index.js.map +1 -1
  28. package/dist/src/network/i-repo.d.ts +11 -1
  29. package/dist/src/network/i-repo.d.ts.map +1 -1
  30. package/dist/src/network/stale-failure.d.ts +21 -0
  31. package/dist/src/network/stale-failure.d.ts.map +1 -1
  32. package/dist/src/network/stale-failure.js +22 -0
  33. package/dist/src/network/stale-failure.js.map +1 -1
  34. package/dist/src/network/struct.d.ts +66 -21
  35. package/dist/src/network/struct.d.ts.map +1 -1
  36. package/dist/src/network/struct.js.map +1 -1
  37. package/dist/src/testing/test-transactor.d.ts +22 -0
  38. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  39. package/dist/src/testing/test-transactor.js +44 -5
  40. package/dist/src/testing/test-transactor.js.map +1 -1
  41. package/dist/src/transaction/coordinator.d.ts +20 -0
  42. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  43. package/dist/src/transaction/coordinator.js +256 -113
  44. package/dist/src/transaction/coordinator.js.map +1 -1
  45. package/dist/src/transaction/operations-hash.d.ts +1 -1
  46. package/dist/src/transaction/operations-hash.js +1 -1
  47. package/dist/src/transaction/transaction.d.ts +4 -2
  48. package/dist/src/transaction/transaction.d.ts.map +1 -1
  49. package/dist/src/transactor/network-transactor.d.ts +21 -1
  50. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  51. package/dist/src/transactor/network-transactor.js +122 -29
  52. package/dist/src/transactor/network-transactor.js.map +1 -1
  53. package/dist/src/transactor/transactor-source.d.ts +11 -5
  54. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  55. package/dist/src/transactor/transactor-source.js +16 -8
  56. package/dist/src/transactor/transactor-source.js.map +1 -1
  57. package/dist/src/transform/cache-source.d.ts +13 -0
  58. package/dist/src/transform/cache-source.d.ts.map +1 -1
  59. package/dist/src/transform/cache-source.js +18 -0
  60. package/dist/src/transform/cache-source.js.map +1 -1
  61. package/dist/src/transform/digest.d.ts +18 -0
  62. package/dist/src/transform/digest.d.ts.map +1 -0
  63. package/dist/src/transform/digest.js +65 -0
  64. package/dist/src/transform/digest.js.map +1 -0
  65. package/dist/src/transform/index.d.ts +1 -0
  66. package/dist/src/transform/index.d.ts.map +1 -1
  67. package/dist/src/transform/index.js +1 -0
  68. package/dist/src/transform/index.js.map +1 -1
  69. package/dist/src/transform/tracker.d.ts +16 -0
  70. package/dist/src/transform/tracker.d.ts.map +1 -1
  71. package/dist/src/transform/tracker.js +40 -1
  72. package/dist/src/transform/tracker.js.map +1 -1
  73. package/dist/src/utility/canonical-json.d.ts +11 -0
  74. package/dist/src/utility/canonical-json.d.ts.map +1 -0
  75. package/dist/src/utility/canonical-json.js +15 -0
  76. package/dist/src/utility/canonical-json.js.map +1 -0
  77. package/dist/src/utility/lru-map.d.ts +2 -0
  78. package/dist/src/utility/lru-map.d.ts.map +1 -1
  79. package/dist/src/utility/lru-map.js +4 -0
  80. package/dist/src/utility/lru-map.js.map +1 -1
  81. package/package.json +2 -1
  82. package/src/blocks/helpers.ts +26 -13
  83. package/src/cluster/membership.ts +87 -85
  84. package/src/cluster/structs.ts +28 -1
  85. package/src/collection/action.ts +17 -0
  86. package/src/collection/collection.ts +1129 -688
  87. package/src/collections/tree/tree.ts +341 -320
  88. package/src/index.ts +24 -23
  89. package/src/network/i-repo.ts +59 -46
  90. package/src/network/stale-failure.ts +67 -43
  91. package/src/network/struct.ts +332 -270
  92. package/src/testing/test-transactor.ts +680 -638
  93. package/src/transaction/coordinator.ts +1266 -1110
  94. package/src/transaction/operations-hash.ts +1 -1
  95. package/src/transaction/transaction.ts +4 -2
  96. package/src/transactor/network-transactor.ts +123 -32
  97. package/src/transactor/transactor-source.ts +17 -9
  98. package/src/transform/cache-source.ts +20 -0
  99. package/src/transform/digest.ts +72 -0
  100. package/src/transform/index.ts +1 -0
  101. package/src/transform/tracker.ts +42 -1
  102. package/src/utility/canonical-json.ts +16 -0
  103. package/src/utility/lru-map.ts +5 -0
@@ -1,85 +1,87 @@
1
- import type { RepoMessage } from "../network/repo-protocol.js";
2
- import type { ClusterPeers, ClusterRecord, Signature } from "./structs.js";
3
- import { sha256 } from "multiformats/hashes/sha2";
4
- import { base58btc } from "multiformats/bases/base58";
5
- import { toString as uint8ArrayToString } from "uint8arrays/to-string";
6
-
7
- /**
8
- * Membership-binding version a new coordinator emits. A record at this version folds a
9
- * {@link membershipDigest} of its peer set into `messageHash` / `promiseHash` / `commitHash`, so the
10
- * responsible peer set is covered by every signature (see `docs/correctness.md` §2). Absent or `1` marks
11
- * a legacy *unbound* record (pre-binding history) whose hashes must verify byte-identically to before
12
- * this change.
13
- */
14
- export const CURRENT_MEMBERSHIP_VERSION = 2 as const;
15
-
16
- /** Deterministic JSON: sorts object keys so the hash is independent of key insertion order. */
17
- function canonicalJson(value: unknown): string {
18
- return JSON.stringify(value, (_, v) =>
19
- v && typeof v === 'object' && !Array.isArray(v)
20
- ? Object.keys(v).sort().reduce((o: Record<string, unknown>, k) => { o[k] = v[k]; return o; }, {})
21
- : v
22
- );
23
- }
24
-
25
- /**
26
- * Canonical membership digest for a peer set: `base64url(SHA256(canonicalJson(sorted peer-id list)))`.
27
- *
28
- * Derived from the sorted peer-id list ONLY — not multiaddrs or public keys. Multiaddrs churn and a
29
- * peer's public key is already a function of its id, so folding either in would make identity unstable
30
- * without adding agreement value. The set of ids IS the membership. The digest is therefore independent
31
- * of peer-map key insertion order and of multiaddr / pubkey contents; adding or removing one id changes
32
- * it. `membershipDigest({})` (the empty set) is a fixed constant.
33
- */
34
- export async function membershipDigest(peers: ClusterPeers): Promise<string> {
35
- const ids = Object.keys(peers ?? {}).sort();
36
- const bytes = new TextEncoder().encode(canonicalJson(ids));
37
- const hash = await sha256.digest(bytes);
38
- return uint8ArrayToString(hash.digest, 'base64url');
39
- }
40
-
41
- /**
42
- * The membership digest that participates in a record's hashes, or `undefined` for a legacy (v1 /
43
- * unversioned) record. `undefined` selects the pre-binding hashing (empty-string concat is a no-op), so
44
- * a v1 record hashes byte-identically to before this change. A v2 record folds in its declared
45
- * `membershipDigest`.
46
- */
47
- export function recordMembershipDigest(record: Pick<ClusterRecord, 'membershipVersion' | 'membershipDigest'>): string | undefined {
48
- return record.membershipVersion === 2 ? record.membershipDigest : undefined;
49
- }
50
-
51
- /**
52
- * `messageHash` = `base58btc(SHA256(canonicalJson(message) + digest))`.
53
- *
54
- * `digest` `undefined` → legacy v1 preimage (`canonicalJson(message)` only); passing the membership
55
- * digest folds the peer set in (v2). Empty-string concat means the v1 image is byte-identical to the
56
- * pre-binding implementation.
57
- */
58
- export async function computeClusterMessageHash(message: RepoMessage, digest?: string): Promise<string> {
59
- const bytes = new TextEncoder().encode(canonicalJson(message) + (digest ?? ''));
60
- const hash = await sha256.digest(bytes);
61
- return base58btc.encode(hash.digest);
62
- }
63
-
64
- /**
65
- * `promiseHash` = `base64url(SHA256(messageHash + canonicalJson(message) + digest))`.
66
- *
67
- * `digest` `undefined` legacy v1 preimage; passing it folds the peer set in (v2).
68
- */
69
- export async function computeClusterPromiseHash(messageHash: string, message: RepoMessage, digest?: string): Promise<string> {
70
- const bytes = new TextEncoder().encode(messageHash + canonicalJson(message) + (digest ?? ''));
71
- const hash = await sha256.digest(bytes);
72
- return uint8ArrayToString(hash.digest, 'base64url');
73
- }
74
-
75
- /**
76
- * `commitHash` = `base64url(SHA256(messageHash + canonicalJson(message) + digest + canonicalJson(promises)))`.
77
- *
78
- * `digest` `undefined` legacy v1 preimage; passing it folds the peer set in (v2). The digest sits
79
- * between the message and the promises image, matching the v2 layout in `docs/correctness.md`.
80
- */
81
- export async function computeClusterCommitHash(messageHash: string, message: RepoMessage, promises: Record<string, Signature>, digest?: string): Promise<string> {
82
- const bytes = new TextEncoder().encode(messageHash + canonicalJson(message) + (digest ?? '') + canonicalJson(promises));
83
- const hash = await sha256.digest(bytes);
84
- return uint8ArrayToString(hash.digest, 'base64url');
85
- }
1
+ import type { RepoMessage } from "../network/repo-protocol.js";
2
+ import type { ClusterPeers, ClusterRecord, Signature } from "./structs.js";
3
+ import { sha256 } from "multiformats/hashes/sha2";
4
+ import { base58btc } from "multiformats/bases/base58";
5
+ import { toString as uint8ArrayToString } from "uint8arrays/to-string";
6
+ import { canonicalJson } from "../utility/canonical-json.js";
7
+
8
+ /**
9
+ * Membership-binding version a new coordinator emits. A record at this version folds a
10
+ * {@link membershipDigest} of its peer set into `messageHash` / `promiseHash` / `commitHash`, so the
11
+ * responsible peer set is covered by every signature (see `docs/correctness.md` §2). Absent or `1` marks
12
+ * a legacy *unbound* record (pre-binding history) whose hashes must verify byte-identically to before
13
+ * this change.
14
+ */
15
+ export const CURRENT_MEMBERSHIP_VERSION = 2 as const;
16
+
17
+ /**
18
+ * Canonical membership digest for a peer set: `base64url(SHA256(canonicalJson(sorted peer-id list)))`.
19
+ *
20
+ * Derived from the sorted peer-id list ONLY not multiaddrs or public keys. Multiaddrs churn and a
21
+ * peer's public key is already a function of its id, so folding either in would make identity unstable
22
+ * without adding agreement value. The set of ids IS the membership. The digest is therefore independent
23
+ * of peer-map key insertion order and of multiaddr / pubkey contents; adding or removing one id changes
24
+ * it. `membershipDigest({})` (the empty set) is a fixed constant.
25
+ */
26
+ export async function membershipDigest(peers: ClusterPeers): Promise<string> {
27
+ return membershipDigestFromIds(Object.keys(peers ?? {}));
28
+ }
29
+
30
+ /**
31
+ * Canonical membership digest for an explicit peer-id list the same digest {@link membershipDigest}
32
+ * derives from a peer map's keys. ONE implementation, so a verifier reading a stored `peerIds` list
33
+ * (e.g. a persisted block commit proof) and a coordinator reading live `ClusterPeers` can never
34
+ * disagree. Sorts a copy; the caller's array is not mutated.
35
+ */
36
+ export async function membershipDigestFromIds(ids: readonly string[]): Promise<string> {
37
+ const sorted = [...ids].sort();
38
+ const bytes = new TextEncoder().encode(canonicalJson(sorted));
39
+ const hash = await sha256.digest(bytes);
40
+ return uint8ArrayToString(hash.digest, 'base64url');
41
+ }
42
+
43
+ /**
44
+ * The membership digest that participates in a record's hashes, or `undefined` for a legacy (v1 /
45
+ * unversioned) record. `undefined` selects the pre-binding hashing (empty-string concat is a no-op), so
46
+ * a v1 record hashes byte-identically to before this change. A v2 record folds in its declared
47
+ * `membershipDigest`.
48
+ */
49
+ export function recordMembershipDigest(record: Pick<ClusterRecord, 'membershipVersion' | 'membershipDigest'>): string | undefined {
50
+ return record.membershipVersion === 2 ? record.membershipDigest : undefined;
51
+ }
52
+
53
+ /**
54
+ * `messageHash` = `base58btc(SHA256(canonicalJson(message) + digest))`.
55
+ *
56
+ * `digest` `undefined` → legacy v1 preimage (`canonicalJson(message)` only); passing the membership
57
+ * digest folds the peer set in (v2). Empty-string concat means the v1 image is byte-identical to the
58
+ * pre-binding implementation.
59
+ */
60
+ export async function computeClusterMessageHash(message: RepoMessage, digest?: string): Promise<string> {
61
+ const bytes = new TextEncoder().encode(canonicalJson(message) + (digest ?? ''));
62
+ const hash = await sha256.digest(bytes);
63
+ return base58btc.encode(hash.digest);
64
+ }
65
+
66
+ /**
67
+ * `promiseHash` = `base64url(SHA256(messageHash + canonicalJson(message) + digest))`.
68
+ *
69
+ * `digest` `undefined` legacy v1 preimage; passing it folds the peer set in (v2).
70
+ */
71
+ export async function computeClusterPromiseHash(messageHash: string, message: RepoMessage, digest?: string): Promise<string> {
72
+ const bytes = new TextEncoder().encode(messageHash + canonicalJson(message) + (digest ?? ''));
73
+ const hash = await sha256.digest(bytes);
74
+ return uint8ArrayToString(hash.digest, 'base64url');
75
+ }
76
+
77
+ /**
78
+ * `commitHash` = `base64url(SHA256(messageHash + canonicalJson(message) + digest + canonicalJson(promises)))`.
79
+ *
80
+ * `digest` `undefined` → legacy v1 preimage; passing it folds the peer set in (v2). The digest sits
81
+ * between the message and the promises image, matching the v2 layout in `docs/correctness.md`.
82
+ */
83
+ export async function computeClusterCommitHash(messageHash: string, message: RepoMessage, promises: Record<string, Signature>, digest?: string): Promise<string> {
84
+ const bytes = new TextEncoder().encode(messageHash + canonicalJson(message) + (digest ?? '') + canonicalJson(promises));
85
+ const hash = await sha256.digest(bytes);
86
+ return uint8ArrayToString(hash.digest, 'base64url');
87
+ }
@@ -66,8 +66,15 @@ export type ClusterRecord = {
66
66
  membershipVersion?: 1 | 2;
67
67
  /** Membership digest of {@link ClusterRecord.peers}; present iff `membershipVersion === 2`. base64url. */
68
68
  membershipDigest?: string;
69
+ /**
70
+ * The transaction's operations and the block the coordinator selected the cohort by
71
+ * ({@link RepoMessage.coordinatingBlockIds}). There is deliberately NO top-level copy of the
72
+ * coordinating block ids on the record: `messageHash` covers `message` only, so a duplicate at
73
+ * this level would be outside every hash and any relaying peer could rewrite it — and the
74
+ * membership admission gate derives its own cohort view from exactly that id
75
+ * (`ClusterMember.deriveExpectedClusterView`). One source of truth, inside the hash.
76
+ */
69
77
  message: RepoMessage;
70
- coordinatingBlockIds?: string[];
71
78
  promises: { [peerId: string]: Signature };
72
79
  commits: { [peerId: string]: Signature };
73
80
  /** Sender's recommended cluster size: min(estimated network size, configured cluster size) */
@@ -183,4 +190,24 @@ export interface ClusterConsensusConfig {
183
190
  * single-node / local dev where you knowingly run below the safe floor.
184
191
  */
185
192
  allowUnvalidatedSmallCluster?: boolean;
193
+ /**
194
+ * What a member WITH a transaction validator does with a pend that carries no `validation`
195
+ * payload. See {@link UnvalidatablePendPolicy}; default 'accept'.
196
+ */
197
+ unvalidatablePendPolicy?: UnvalidatablePendPolicy;
186
198
  }
199
+
200
+ /**
201
+ * What a receiver WITH a transaction checker does with a pend that carries no
202
+ * {@link PendRequest.validation} payload — the single-collection (`Collection.sync`) shape, which
203
+ * has no transaction to re-execute.
204
+ *
205
+ * - `'accept'` (default) preserves the historical behaviour: the pend is approved unchecked.
206
+ * - `'reject'` is the fail-closed posture for a deployment that has decided every write must be
207
+ * re-checkable; it REFUSES `Collection.sync` writes, which is the point, not a bug.
208
+ *
209
+ * Irrelevant on a receiver with no checker, which never re-checks anything. Named once here and
210
+ * referenced by every tier that carries the knob (`ClusterConsensusConfig`, db-p2p's
211
+ * `ClusterPolicyOptions` and `StorageRepoOptions`) so the three cannot drift apart.
212
+ */
213
+ export type UnvalidatablePendPolicy = 'accept' | 'reject';
@@ -29,3 +29,20 @@ export type ActionContext = {
29
29
  /** Optional uncommitted pending action ID */
30
30
  actionId?: ActionId;
31
31
  };
32
+
33
+ /** The id of the action that produced `rev` within `context`'s uncheckpointed committed list,
34
+ * or `undefined` when the list names no action at that revision.
35
+ *
36
+ * `undefined` is legitimate, not an error, and has three causes: the revision's log slot belongs
37
+ * to an entry that carries no action (a CHECKPOINT or an INVALIDATION entry takes a revision of
38
+ * its own); `rev` predates the most recent checkpoint, which is as far back as a context read off
39
+ * a log reaches (`Log.getActionContext`, `Log.getFrom`); or the context was never built from a
40
+ * log at all. A caller printing this must carry a placeholder rather than invent an id.
41
+ *
42
+ * NOTE: linear in `committed`, which grows one entry per commit between context reads; fine now —
43
+ * every caller is a `debug`-gated diagnostic, so this does not run on a normal path at all. If a
44
+ * non-diagnostic caller ever appears, index the lookup or search from the end (the entry at the
45
+ * context's own `rev` is normally the last one). */
46
+ export function actionIdAt(context: ActionContext, rev: number): ActionId | undefined {
47
+ return context.committed.find(entry => entry.rev === rev)?.actionId;
48
+ }