@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.
- package/dist/src/blocks/helpers.d.ts +5 -0
- package/dist/src/blocks/helpers.d.ts.map +1 -1
- package/dist/src/blocks/helpers.js +12 -0
- package/dist/src/blocks/helpers.js.map +1 -1
- package/dist/src/cluster/membership.d.ts +7 -0
- package/dist/src/cluster/membership.d.ts.map +1 -1
- package/dist/src/cluster/membership.js +12 -8
- package/dist/src/cluster/membership.js.map +1 -1
- package/dist/src/cluster/structs.d.ts +27 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/action.d.ts +14 -0
- package/dist/src/collection/action.d.ts.map +1 -1
- package/dist/src/collection/action.js +16 -1
- package/dist/src/collection/action.js.map +1 -1
- package/dist/src/collection/collection.d.ts +273 -4
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +427 -30
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +16 -1
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +19 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/network/i-repo.d.ts +11 -1
- package/dist/src/network/i-repo.d.ts.map +1 -1
- package/dist/src/network/stale-failure.d.ts +21 -0
- package/dist/src/network/stale-failure.d.ts.map +1 -1
- package/dist/src/network/stale-failure.js +22 -0
- package/dist/src/network/stale-failure.js.map +1 -1
- package/dist/src/network/struct.d.ts +66 -21
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +22 -0
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +44 -5
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +20 -0
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +256 -113
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/operations-hash.d.ts +1 -1
- package/dist/src/transaction/operations-hash.js +1 -1
- package/dist/src/transaction/transaction.d.ts +4 -2
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +21 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +122 -29
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +11 -5
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +16 -8
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +13 -0
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +18 -0
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/digest.d.ts +18 -0
- package/dist/src/transform/digest.d.ts.map +1 -0
- package/dist/src/transform/digest.js +65 -0
- package/dist/src/transform/digest.js.map +1 -0
- package/dist/src/transform/index.d.ts +1 -0
- package/dist/src/transform/index.d.ts.map +1 -1
- package/dist/src/transform/index.js +1 -0
- package/dist/src/transform/index.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +16 -0
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +40 -1
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/canonical-json.d.ts +11 -0
- package/dist/src/utility/canonical-json.d.ts.map +1 -0
- package/dist/src/utility/canonical-json.js +15 -0
- package/dist/src/utility/canonical-json.js.map +1 -0
- package/dist/src/utility/lru-map.d.ts +2 -0
- package/dist/src/utility/lru-map.d.ts.map +1 -1
- package/dist/src/utility/lru-map.js +4 -0
- package/dist/src/utility/lru-map.js.map +1 -1
- package/package.json +2 -1
- package/src/blocks/helpers.ts +26 -13
- package/src/cluster/membership.ts +87 -85
- package/src/cluster/structs.ts +28 -1
- package/src/collection/action.ts +17 -0
- package/src/collection/collection.ts +1129 -688
- package/src/collections/tree/tree.ts +341 -320
- package/src/index.ts +24 -23
- package/src/network/i-repo.ts +59 -46
- package/src/network/stale-failure.ts +67 -43
- package/src/network/struct.ts +332 -270
- package/src/testing/test-transactor.ts +680 -638
- package/src/transaction/coordinator.ts +1266 -1110
- package/src/transaction/operations-hash.ts +1 -1
- package/src/transaction/transaction.ts +4 -2
- package/src/transactor/network-transactor.ts +123 -32
- package/src/transactor/transactor-source.ts +17 -9
- package/src/transform/cache-source.ts +20 -0
- package/src/transform/digest.ts +72 -0
- package/src/transform/index.ts +1 -0
- package/src/transform/tracker.ts +42 -1
- package/src/utility/canonical-json.ts +16 -0
- 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
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
* `
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* `
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* `
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* `
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
}
|
package/src/cluster/structs.ts
CHANGED
|
@@ -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';
|
package/src/collection/action.ts
CHANGED
|
@@ -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
|
+
}
|