@optimystic/db-p2p 0.24.2 → 0.25.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.
- package/{readme.md → README.md} +688 -668
- package/dist/src/cluster/block-transfer-service.d.ts +133 -10
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +155 -9
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +67 -7
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +127 -24
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/certified-claims.d.ts +170 -0
- package/dist/src/cluster/certified-claims.d.ts.map +1 -0
- package/dist/src/cluster/certified-claims.js +200 -0
- package/dist/src/cluster/certified-claims.js.map +1 -0
- package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
- package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
- package/dist/src/cluster/client-signature-verifier.js +33 -0
- package/dist/src/cluster/client-signature-verifier.js.map +1 -0
- package/dist/src/cluster/cluster-policy.d.ts +9 -3
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +111 -27
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +140 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +485 -29
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.d.ts +134 -0
- package/dist/src/cluster/commit-proof.d.ts.map +1 -0
- package/dist/src/cluster/commit-proof.js +263 -0
- package/dist/src/cluster/commit-proof.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +95 -8
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +113 -14
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +207 -5
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +27 -7
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +179 -38
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -4
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -18
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +8 -2
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +7 -7
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -7
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +7 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +14 -41
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -8
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +24 -7
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +5 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +13 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +33 -15
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +127 -34
- package/dist/src/dispute/invalidation.js.map +1 -1
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +2 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +14 -10
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +28 -58
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +38 -7
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +148 -63
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/network/open-protocol-stream.d.ts +56 -0
- package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
- package/dist/src/network/open-protocol-stream.js +60 -0
- package/dist/src/network/open-protocol-stream.js.map +1 -0
- package/dist/src/network/register-protocol-handler.d.ts +66 -0
- package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
- package/dist/src/network/register-protocol-handler.js +51 -0
- package/dist/src/network/register-protocol-handler.js.map +1 -0
- package/dist/src/peer-address-book.d.ts +60 -2
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +89 -1
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/pend-validation.d.ts +65 -0
- package/dist/src/pend-validation.d.ts.map +1 -0
- package/dist/src/pend-validation.js +69 -0
- package/dist/src/pend-validation.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
- package/dist/src/reactivity/notify-transport.js +2 -1
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -1
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +32 -6
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +44 -10
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +181 -25
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +650 -62
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/served-repo-proxy.d.ts +30 -0
- package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
- package/dist/src/repo/served-repo-proxy.js +50 -0
- package/dist/src/repo/served-repo-proxy.js.map +1 -0
- package/dist/src/repo/service.d.ts +16 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +18 -16
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -1
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +24 -2
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +33 -4
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-archive.d.ts +153 -0
- package/dist/src/storage/block-archive.d.ts.map +1 -0
- package/dist/src/storage/block-archive.js +194 -0
- package/dist/src/storage/block-archive.js.map +1 -0
- package/dist/src/storage/block-latch.d.ts +92 -0
- package/dist/src/storage/block-latch.d.ts.map +1 -0
- package/dist/src/storage/block-latch.js +132 -0
- package/dist/src/storage/block-latch.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +169 -12
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +415 -152
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +5 -1
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/cached-raw-storage.js +16 -1
- package/dist/src/storage/cached-raw-storage.js.map +1 -1
- package/dist/src/storage/cached-store-driver.d.ts +14 -4
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
- package/dist/src/storage/cached-store-driver.js +39 -5
- package/dist/src/storage/cached-store-driver.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +70 -19
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js +21 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +38 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +14 -8
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.js +29 -9
- package/dist/src/storage/kv-raw-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +4 -1
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
- package/dist/src/storage/memory-store-driver.js +22 -2
- package/dist/src/storage/memory-store-driver.js.map +1 -1
- package/dist/src/storage/raw-store-codec.js +2 -2
- package/dist/src/storage/raw-store-driver.d.ts +44 -3
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +60 -3
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.js +68 -4
- package/dist/src/storage/shared-cache-pool.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +204 -26
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +422 -119
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/store-identity.d.ts +21 -0
- package/dist/src/storage/store-identity.d.ts.map +1 -0
- package/dist/src/storage/store-identity.js +33 -0
- package/dist/src/storage/store-identity.js.map +1 -0
- package/dist/src/storage/struct.d.ts +15 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/storage/with-read-cache.d.ts +92 -0
- package/dist/src/storage/with-read-cache.d.ts.map +1 -0
- package/dist/src/storage/with-read-cache.js +132 -0
- package/dist/src/storage/with-read-cache.js.map +1 -0
- package/dist/src/sync/service.d.ts +13 -3
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -30
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +75 -2
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +193 -81
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +97 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +5 -6
- package/src/cluster/block-transfer-service.ts +528 -312
- package/src/cluster/block-transfer.ts +547 -424
- package/src/cluster/certified-claims.ts +319 -0
- package/src/cluster/client-signature-verifier.ts +34 -0
- package/src/cluster/cluster-policy.ts +294 -203
- package/src/cluster/cluster-repo.ts +2501 -2034
- package/src/cluster/commit-proof.ts +344 -0
- package/src/cluster/quorum-restore.ts +348 -223
- package/src/cluster/rebalance-monitor.ts +539 -254
- package/src/cluster/reconcile-block.ts +404 -203
- package/src/cluster/service.ts +23 -20
- package/src/cluster/spread-on-churn.ts +352 -346
- package/src/cohort-topic/host.ts +8 -7
- package/src/cohort-topic/stream-util.ts +14 -43
- package/src/dispute/cascade.ts +31 -15
- package/src/dispute/dispute-service.ts +706 -696
- package/src/dispute/invalidation.ts +162 -52
- package/src/dispute/service.ts +126 -125
- package/src/index.ts +7 -0
- package/src/libp2p-key-network.ts +1204 -1235
- package/src/libp2p-node-base.ts +185 -70
- package/src/network/open-protocol-stream.ts +87 -0
- package/src/network/register-protocol-handler.ts +85 -0
- package/src/peer-address-book.ts +96 -2
- package/src/pend-validation.ts +89 -0
- package/src/reactivity/notify-transport.ts +2 -1
- package/src/reactivity/push-state-gossip.ts +2 -1
- package/src/repo/cluster-coordinator.ts +65 -11
- package/src/repo/coordinator-repo.ts +729 -71
- package/src/repo/served-repo-proxy.ts +55 -0
- package/src/repo/service.ts +25 -16
- package/src/reputation/types.ts +3 -1
- package/src/rn.ts +34 -4
- package/src/storage/block-archive.ts +242 -0
- package/src/storage/block-latch.ts +144 -0
- package/src/storage/block-storage.ts +787 -498
- package/src/storage/cached-raw-storage.ts +21 -1
- package/src/storage/cached-store-driver.ts +898 -859
- package/src/storage/i-block-storage.ts +172 -116
- package/src/storage/i-raw-storage.ts +40 -0
- package/src/storage/kv-raw-storage.ts +34 -9
- package/src/storage/memory-store-driver.ts +26 -2
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +45 -3
- package/src/storage/shared-cache-pool.ts +516 -452
- package/src/storage/storage-repo.ts +1421 -1014
- package/src/storage/store-identity.ts +41 -0
- package/src/storage/struct.ts +52 -34
- package/src/storage/with-read-cache.ts +182 -0
- package/src/sync/service.ts +178 -198
- package/src/testing/mesh-harness.ts +524 -344
- package/src/testing/raw-storage-conformance.ts +114 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ActionRev, BlockId, IBlock, IRepo } from "@optimystic/db-core";
|
|
2
|
+
import type { BlockArchive } from "./struct.js";
|
|
3
|
+
import { type BlockCommitProof } from "../cluster/commit-proof.js";
|
|
4
|
+
/**
|
|
5
|
+
* The archive shape every block-repair path exchanges: ONE revision — the one being served —
|
|
6
|
+
* carrying its action, the block itself when the serving repo materialized it, and the cohort's
|
|
7
|
+
* commit proof for that revision when the serving repo retained one.
|
|
8
|
+
*
|
|
9
|
+
* One function rather than the shape re-typed at each site, because three sites had already drifted
|
|
10
|
+
* while each claimed to mirror the others: the sync service served `block: undefined` for a block
|
|
11
|
+
* with no materialized content, the mesh test harness served no archive at all for that same repo
|
|
12
|
+
* state (turning a corroborable revision claim into a phantom non-holder), and the reconcile unit
|
|
13
|
+
* spec's stand-in emitted a third shape again. `createReconcileBlock` reads
|
|
14
|
+
* `revisions[rev].action.actionId` and `revisions[rev].block`, and the difference between "absent
|
|
15
|
+
* block" and "absent archive" decides whether a peer votes in the revision quorum at all — so the
|
|
16
|
+
* shape is a contract, not a detail.
|
|
17
|
+
*
|
|
18
|
+
* `block` stays optional on purpose: a revision whose content the serving repo cannot materialize
|
|
19
|
+
* (a deleted block — see `GetBlockResult.block`) is still a revision that peer legitimately claims.
|
|
20
|
+
* It votes on `(rev, actionId)` and abstains from the content quorum, which is exactly the evidence
|
|
21
|
+
* it holds.
|
|
22
|
+
*
|
|
23
|
+
* `proof` is optional for the same kind of reason (see `ArchiveRevisions.proof`): a pre-proof
|
|
24
|
+
* revision, a diverged member, or an un-upgraded peer serves none, and every consumer must behave
|
|
25
|
+
* exactly as it did before proofs existed when it is absent.
|
|
26
|
+
*
|
|
27
|
+
* NOTE: the `range` emitted here is a COMPATIBILITY CONTRACT with `BlockStorage.vetRestoredArchive`,
|
|
28
|
+
* which refuses an archive whose declared range does not start at its lowest revision, end past its
|
|
29
|
+
* highest, or which is open-ended. `[rev, rev + 1]` around a single revision satisfies all three
|
|
30
|
+
* trivially, and this is currently the ONLY producer of an archive that crosses the restore wire —
|
|
31
|
+
* which is why no test drives a refusal end to end through `RestorationCoordinator`. If a second
|
|
32
|
+
* producer ever appears (a multi-revision or sparse archive, say), that contract stops being
|
|
33
|
+
* trivially true and needs a test pinning the two ends together.
|
|
34
|
+
*/
|
|
35
|
+
export declare function singleRevisionArchive(blockId: BlockId, source: ActionRev, block: IBlock | undefined, proof?: BlockCommitProof): BlockArchive;
|
|
36
|
+
/**
|
|
37
|
+
* A peer's answer to the latest-revision consult: the `(rev, actionId)` it claims, plus the cohort's
|
|
38
|
+
* commit proof for that revision when it retained one.
|
|
39
|
+
*
|
|
40
|
+
* Defined HERE, alongside the archive shape, rather than beside the callback that returns it
|
|
41
|
+
* (`ClusterLatestCallback` in `repo/coordinator-repo.ts`, which re-exports this name): a remote
|
|
42
|
+
* peer's answer IS a projection of the archive it served, so keeping the two in one file is what
|
|
43
|
+
* stops the projection from drifting from the shape — the same reason `singleRevisionArchive` is a
|
|
44
|
+
* function rather than a shape re-typed at each site.
|
|
45
|
+
*
|
|
46
|
+
* The proof is OPTIONAL and means nothing until a consumer verifies it. A pre-proof revision, a
|
|
47
|
+
* diverged member, and an un-upgraded peer all legitimately answer without one, and a peer is free
|
|
48
|
+
* to attach whatever it likes — verification, never presence, is what makes a proof evidence.
|
|
49
|
+
*/
|
|
50
|
+
export type CertifiedActionRev = ActionRev & {
|
|
51
|
+
proof?: BlockCommitProof;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Highest revision an archive covers, or `undefined` when it covers none.
|
|
55
|
+
*
|
|
56
|
+
* ONE implementation for every site that asks an untrusted archive this question — the two repair
|
|
57
|
+
* wires ({@link latestClaimFromArchive}) and the reconcile pass (`cluster/reconcile-block.ts`) — for
|
|
58
|
+
* the same reason {@link singleRevisionArchive} is one function: two copies is how one of them ends
|
|
59
|
+
* up without the guards below.
|
|
60
|
+
*
|
|
61
|
+
* Both guards matter on input a remote peer chose. `Object.keys` on a JSON-parsed archive yields
|
|
62
|
+
* strings, so a non-numeric key coerces to `NaN` and is skipped rather than poisoning the maximum.
|
|
63
|
+
* And the fold is deliberate rather than `Math.max(...keys)`: the spread passes one ARGUMENT per
|
|
64
|
+
* revision, which throws `RangeError: Maximum call stack size exceeded` past ~125k arguments —
|
|
65
|
+
* comfortably inside the 8 MiB `MAX_BLOCK_MESSAGE_BYTES` a sync response may carry (130k minimal
|
|
66
|
+
* revision entries serialize to ~6.3 MiB), so a peer could otherwise choose to make this throw.
|
|
67
|
+
*/
|
|
68
|
+
export declare function maxArchiveRevision(revisions: BlockArchive['revisions'] | undefined): number | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The highest-revision claim an archive carries, or `undefined` when it holds no usable revision.
|
|
71
|
+
* `undefined` is the peer having ANSWERED without data — an absent claim, never silence (see
|
|
72
|
+
* `ClusterLatestCallback`'s three-way contract).
|
|
73
|
+
*
|
|
74
|
+
* The proof is read from the SAME revision entry as the `(rev, actionId)`, so a serving peer cannot
|
|
75
|
+
* pair a genuine proof with a revision it does not certify by choosing a different layout. Nothing
|
|
76
|
+
* here verifies anything: the result is the peer's unverified assertion until a caller checks it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function latestClaimFromArchive(archive: BlockArchive): CertifiedActionRev | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* What {@link serveBlockArchive} needs of the repo it reads: `IRepo`, plus — OPTIONALLY — the
|
|
81
|
+
* revision-keyed commit-proof accessor `StorageRepo` implements.
|
|
82
|
+
*
|
|
83
|
+
* Optional rather than required so the unit-test doubles and any other plain `IRepo` that serves
|
|
84
|
+
* archives keep compiling and keep working (they simply serve no proof). A repo that CAN serve
|
|
85
|
+
* proofs and does not is not a type error here — it is the pre-proof behaviour, which stays valid.
|
|
86
|
+
*/
|
|
87
|
+
export type ArchiveServingRepo = IRepo & {
|
|
88
|
+
getBlockProof?(blockId: BlockId, rev: number): Promise<BlockCommitProof | undefined>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* An {@link ArchiveServingRepo} that definitely CAN serve proofs — the accessor required rather
|
|
92
|
+
* than optional.
|
|
93
|
+
*
|
|
94
|
+
* The optionality above exists for a serving repo that legitimately has no proofs (a test double, a
|
|
95
|
+
* plain-`IRepo` embedder). It is the wrong default for a node's OWN store: forgetting the accessor
|
|
96
|
+
* there degrades every archive it serves to proof-less, silently and without a type error. Naming
|
|
97
|
+
* the stronger shape lets a composition root demand it (`createServedRepoProxy`).
|
|
98
|
+
*/
|
|
99
|
+
export type ProofRetainingRepo = IRepo & Required<Pick<ArchiveServingRepo, 'getBlockProof'>>;
|
|
100
|
+
/**
|
|
101
|
+
* Serve `blockId` out of a local repo as a {@link singleRevisionArchive} — what a peer answers a
|
|
102
|
+
* block-repair fetch with. `undefined` when the repo holds no revision of the block at all, which
|
|
103
|
+
* callers report as "holds nothing" (`ReconcileBlockDeps.fetchArchive`'s contract folds
|
|
104
|
+
* "unreachable" into that same answer).
|
|
105
|
+
*
|
|
106
|
+
* The read skips the cluster deliberately: a peer answering a repair fetch reports what IT holds,
|
|
107
|
+
* and one that re-asked its own cohort would launder another peer's claim as its own.
|
|
108
|
+
*
|
|
109
|
+
* `rev` pins the read: `StorageRepo.get` materializes the highest committed revision of the block
|
|
110
|
+
* at or below it and reports that revision as `GetBlockResult.materialized`, which is what the
|
|
111
|
+
* archive is labelled with — its revision number, its action id, and its proof all belong to the
|
|
112
|
+
* bytes actually served. `state.latest` (the repo's NEWEST revision) is never the label: a caller
|
|
113
|
+
* asking for an older revision (`RestorationCoordinator` is the one that does) is answered with
|
|
114
|
+
* that revision as itself, or with nothing. Never with old bytes under a newer label — that pairing
|
|
115
|
+
* is what a receiver keyed by action id writes over its own good copy of the newer revision.
|
|
116
|
+
*
|
|
117
|
+
* The proof served is looked up for the revision ACTUALLY served, never for one chosen
|
|
118
|
+
* independently, so the archive can never publish a proof paired with a revision it does not
|
|
119
|
+
* certify. (A newer revision's proof over older bytes would pass `verifyBlockCommitProofClaim` and
|
|
120
|
+
* fail `verifyBlockCommitProofContent` — strictly worse than no proof.)
|
|
121
|
+
*
|
|
122
|
+
* NOTE: a single-revision archive's proof is a rounding error against the wire cap. A sync
|
|
123
|
+
* *response* is bounded by `MAX_BLOCK_MESSAGE_BYTES` (8 MiB — `SyncClient.requestBlock` sets the
|
|
124
|
+
* response decoder's `maxDataLength`; the 1 MiB `MAX_CONTROL_MESSAGE_BYTES` bounds the inbound
|
|
125
|
+
* REQUEST, not this). A proof's serialized size is dominated by two signatures plus a peer id per
|
|
126
|
+
* cohort member: a whole proof-carrying single-revision archive measured 4801 bytes at a 10-peer
|
|
127
|
+
* cohort and 8851 bytes at 20 (`test/block-archive-proof.spec.ts`, "far below the sync response
|
|
128
|
+
* cap", which prints both), i.e. ~405 bytes per additional peer. Reaching 8 MiB would take a cohort in the tens of thousands, so
|
|
129
|
+
* no plausible cluster size puts this near the cap — the block bytes the archive already carries
|
|
130
|
+
* are the term that matters. Revisit only if a proof ever grows a per-peer payload beyond its two
|
|
131
|
+
* signatures.
|
|
132
|
+
*/
|
|
133
|
+
export declare function serveBlockArchive(repo: ArchiveServingRepo, blockId: BlockId, rev?: number): Promise<BlockArchive | undefined>;
|
|
134
|
+
/**
|
|
135
|
+
* The stored proof for the revision being served, or `undefined` — including whenever anything is
|
|
136
|
+
* off about it. Serving no proof is always safe (every consumer must already handle its absence),
|
|
137
|
+
* so this fails closed on all three unhappy paths:
|
|
138
|
+
*
|
|
139
|
+
* - the repo has no proof accessor at all (a plain `IRepo`, a test double);
|
|
140
|
+
* - the lookup throws — a storage fault must not turn a servable archive into "holds nothing",
|
|
141
|
+
* because that would recreate the phantom-non-holder bug the archive shape exists to prevent;
|
|
142
|
+
* - the stored proof's own message does not name this `(blockId, rev, actionId)`. That is a local
|
|
143
|
+
* storage-integrity fault, not a peer's doing, and publishing a mis-paired proof would hand
|
|
144
|
+
* every receiver an artifact that cannot verify. Logged loudly, because it means a proof was
|
|
145
|
+
* written under a key its content contradicts.
|
|
146
|
+
*
|
|
147
|
+
* Exported for the SAME reason {@link singleRevisionArchive} is: the mesh test harness answers the
|
|
148
|
+
* latest-revision consult out of a sibling's repo directly rather than over the sync protocol, and
|
|
149
|
+
* a harness that attached proofs by a different rule than a real peer would let every mesh-tier
|
|
150
|
+
* test silently exercise a path production does not have.
|
|
151
|
+
*/
|
|
152
|
+
export declare function servableProof(repo: ArchiveServingRepo, blockId: BlockId, latest: ActionRev): Promise<BlockCommitProof | undefined>;
|
|
153
|
+
//# sourceMappingURL=block-archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-archive.d.ts","sourceRoot":"","sources":["../../../src/storage/block-archive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAKtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,KAAK,CAAC,EAAE,gBAAgB,GACtB,YAAY,CAYd;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG;IAAE,KAAK,CAAC,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAE1E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOvG;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,YAAY,GAAG,kBAAkB,GAAG,SAAS,CAU5F;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG;IACxC,aAAa,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAC;CACrF,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,CAAC,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CA6BnI;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CAClC,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAC3D,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAgBvC"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { proofClaimsCommit } from "../cluster/commit-proof.js";
|
|
2
|
+
import { createLogger } from "../logger.js";
|
|
3
|
+
const log = createLogger('block-archive');
|
|
4
|
+
/**
|
|
5
|
+
* The archive shape every block-repair path exchanges: ONE revision — the one being served —
|
|
6
|
+
* carrying its action, the block itself when the serving repo materialized it, and the cohort's
|
|
7
|
+
* commit proof for that revision when the serving repo retained one.
|
|
8
|
+
*
|
|
9
|
+
* One function rather than the shape re-typed at each site, because three sites had already drifted
|
|
10
|
+
* while each claimed to mirror the others: the sync service served `block: undefined` for a block
|
|
11
|
+
* with no materialized content, the mesh test harness served no archive at all for that same repo
|
|
12
|
+
* state (turning a corroborable revision claim into a phantom non-holder), and the reconcile unit
|
|
13
|
+
* spec's stand-in emitted a third shape again. `createReconcileBlock` reads
|
|
14
|
+
* `revisions[rev].action.actionId` and `revisions[rev].block`, and the difference between "absent
|
|
15
|
+
* block" and "absent archive" decides whether a peer votes in the revision quorum at all — so the
|
|
16
|
+
* shape is a contract, not a detail.
|
|
17
|
+
*
|
|
18
|
+
* `block` stays optional on purpose: a revision whose content the serving repo cannot materialize
|
|
19
|
+
* (a deleted block — see `GetBlockResult.block`) is still a revision that peer legitimately claims.
|
|
20
|
+
* It votes on `(rev, actionId)` and abstains from the content quorum, which is exactly the evidence
|
|
21
|
+
* it holds.
|
|
22
|
+
*
|
|
23
|
+
* `proof` is optional for the same kind of reason (see `ArchiveRevisions.proof`): a pre-proof
|
|
24
|
+
* revision, a diverged member, or an un-upgraded peer serves none, and every consumer must behave
|
|
25
|
+
* exactly as it did before proofs existed when it is absent.
|
|
26
|
+
*
|
|
27
|
+
* NOTE: the `range` emitted here is a COMPATIBILITY CONTRACT with `BlockStorage.vetRestoredArchive`,
|
|
28
|
+
* which refuses an archive whose declared range does not start at its lowest revision, end past its
|
|
29
|
+
* highest, or which is open-ended. `[rev, rev + 1]` around a single revision satisfies all three
|
|
30
|
+
* trivially, and this is currently the ONLY producer of an archive that crosses the restore wire —
|
|
31
|
+
* which is why no test drives a refusal end to end through `RestorationCoordinator`. If a second
|
|
32
|
+
* producer ever appears (a multi-revision or sparse archive, say), that contract stops being
|
|
33
|
+
* trivially true and needs a test pinning the two ends together.
|
|
34
|
+
*/
|
|
35
|
+
export function singleRevisionArchive(blockId, source, block, proof) {
|
|
36
|
+
return {
|
|
37
|
+
blockId,
|
|
38
|
+
revisions: {
|
|
39
|
+
[source.rev]: {
|
|
40
|
+
action: { actionId: source.actionId, transform: { insert: block } },
|
|
41
|
+
...(block ? { block } : {}),
|
|
42
|
+
...(proof ? { proof } : {})
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
range: [source.rev, source.rev + 1]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Highest revision an archive covers, or `undefined` when it covers none.
|
|
50
|
+
*
|
|
51
|
+
* ONE implementation for every site that asks an untrusted archive this question — the two repair
|
|
52
|
+
* wires ({@link latestClaimFromArchive}) and the reconcile pass (`cluster/reconcile-block.ts`) — for
|
|
53
|
+
* the same reason {@link singleRevisionArchive} is one function: two copies is how one of them ends
|
|
54
|
+
* up without the guards below.
|
|
55
|
+
*
|
|
56
|
+
* Both guards matter on input a remote peer chose. `Object.keys` on a JSON-parsed archive yields
|
|
57
|
+
* strings, so a non-numeric key coerces to `NaN` and is skipped rather than poisoning the maximum.
|
|
58
|
+
* And the fold is deliberate rather than `Math.max(...keys)`: the spread passes one ARGUMENT per
|
|
59
|
+
* revision, which throws `RangeError: Maximum call stack size exceeded` past ~125k arguments —
|
|
60
|
+
* comfortably inside the 8 MiB `MAX_BLOCK_MESSAGE_BYTES` a sync response may carry (130k minimal
|
|
61
|
+
* revision entries serialize to ~6.3 MiB), so a peer could otherwise choose to make this throw.
|
|
62
|
+
*/
|
|
63
|
+
export function maxArchiveRevision(revisions) {
|
|
64
|
+
let max;
|
|
65
|
+
for (const key of Object.keys(revisions ?? {})) {
|
|
66
|
+
const rev = Number(key);
|
|
67
|
+
if (Number.isFinite(rev) && (max === undefined || rev > max))
|
|
68
|
+
max = rev;
|
|
69
|
+
}
|
|
70
|
+
return max;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The highest-revision claim an archive carries, or `undefined` when it holds no usable revision.
|
|
74
|
+
* `undefined` is the peer having ANSWERED without data — an absent claim, never silence (see
|
|
75
|
+
* `ClusterLatestCallback`'s three-way contract).
|
|
76
|
+
*
|
|
77
|
+
* The proof is read from the SAME revision entry as the `(rev, actionId)`, so a serving peer cannot
|
|
78
|
+
* pair a genuine proof with a revision it does not certify by choosing a different layout. Nothing
|
|
79
|
+
* here verifies anything: the result is the peer's unverified assertion until a caller checks it.
|
|
80
|
+
*/
|
|
81
|
+
export function latestClaimFromArchive(archive) {
|
|
82
|
+
const maxRev = maxArchiveRevision(archive.revisions);
|
|
83
|
+
if (maxRev === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
const entry = archive.revisions[maxRev];
|
|
86
|
+
if (!entry?.action)
|
|
87
|
+
return undefined;
|
|
88
|
+
return {
|
|
89
|
+
actionId: entry.action.actionId,
|
|
90
|
+
rev: maxRev,
|
|
91
|
+
...(entry.proof ? { proof: entry.proof } : {})
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Serve `blockId` out of a local repo as a {@link singleRevisionArchive} — what a peer answers a
|
|
96
|
+
* block-repair fetch with. `undefined` when the repo holds no revision of the block at all, which
|
|
97
|
+
* callers report as "holds nothing" (`ReconcileBlockDeps.fetchArchive`'s contract folds
|
|
98
|
+
* "unreachable" into that same answer).
|
|
99
|
+
*
|
|
100
|
+
* The read skips the cluster deliberately: a peer answering a repair fetch reports what IT holds,
|
|
101
|
+
* and one that re-asked its own cohort would launder another peer's claim as its own.
|
|
102
|
+
*
|
|
103
|
+
* `rev` pins the read: `StorageRepo.get` materializes the highest committed revision of the block
|
|
104
|
+
* at or below it and reports that revision as `GetBlockResult.materialized`, which is what the
|
|
105
|
+
* archive is labelled with — its revision number, its action id, and its proof all belong to the
|
|
106
|
+
* bytes actually served. `state.latest` (the repo's NEWEST revision) is never the label: a caller
|
|
107
|
+
* asking for an older revision (`RestorationCoordinator` is the one that does) is answered with
|
|
108
|
+
* that revision as itself, or with nothing. Never with old bytes under a newer label — that pairing
|
|
109
|
+
* is what a receiver keyed by action id writes over its own good copy of the newer revision.
|
|
110
|
+
*
|
|
111
|
+
* The proof served is looked up for the revision ACTUALLY served, never for one chosen
|
|
112
|
+
* independently, so the archive can never publish a proof paired with a revision it does not
|
|
113
|
+
* certify. (A newer revision's proof over older bytes would pass `verifyBlockCommitProofClaim` and
|
|
114
|
+
* fail `verifyBlockCommitProofContent` — strictly worse than no proof.)
|
|
115
|
+
*
|
|
116
|
+
* NOTE: a single-revision archive's proof is a rounding error against the wire cap. A sync
|
|
117
|
+
* *response* is bounded by `MAX_BLOCK_MESSAGE_BYTES` (8 MiB — `SyncClient.requestBlock` sets the
|
|
118
|
+
* response decoder's `maxDataLength`; the 1 MiB `MAX_CONTROL_MESSAGE_BYTES` bounds the inbound
|
|
119
|
+
* REQUEST, not this). A proof's serialized size is dominated by two signatures plus a peer id per
|
|
120
|
+
* cohort member: a whole proof-carrying single-revision archive measured 4801 bytes at a 10-peer
|
|
121
|
+
* cohort and 8851 bytes at 20 (`test/block-archive-proof.spec.ts`, "far below the sync response
|
|
122
|
+
* cap", which prints both), i.e. ~405 bytes per additional peer. Reaching 8 MiB would take a cohort in the tens of thousands, so
|
|
123
|
+
* no plausible cluster size puts this near the cap — the block bytes the archive already carries
|
|
124
|
+
* are the term that matters. Revisit only if a proof ever grows a per-peer payload beyond its two
|
|
125
|
+
* signatures.
|
|
126
|
+
*/
|
|
127
|
+
export async function serveBlockArchive(repo, blockId, rev) {
|
|
128
|
+
// `ActionContext` is `{ committed, rev, actionId? }` — nothing else. `committed: []` claims no
|
|
129
|
+
// uncommitted-but-known action, and omitting `actionId` is what keeps this off `StorageRepo`'s
|
|
130
|
+
// pending-overlay path, so the read is purely "the highest COMMITTED revision at or below `rev`".
|
|
131
|
+
const context = rev !== undefined ? { rev, committed: [] } : undefined;
|
|
132
|
+
const result = await repo.get({ blockIds: [blockId], context }, { skipClusterFetch: true });
|
|
133
|
+
const entry = result[blockId];
|
|
134
|
+
const latest = entry?.state?.latest;
|
|
135
|
+
if (!latest)
|
|
136
|
+
return undefined;
|
|
137
|
+
// The revision the content in hand IS. A repo that reports `materialized` (`StorageRepo`) has
|
|
138
|
+
// served the highest committed revision at or below the pin, and that — never `state.latest`,
|
|
139
|
+
// the repo's NEWEST revision — is the archive's label. A repo that does not report it (a plain
|
|
140
|
+
// `IRepo`) can only be describing its latest.
|
|
141
|
+
const served = entry.materialized ?? latest;
|
|
142
|
+
// Fail closed rather than mislabel. A served revision ABOVE the pin is never a right answer:
|
|
143
|
+
// either the repo could not say what it materialized and its latest is newer than what was
|
|
144
|
+
// asked for (so the bytes may be pinned or may not — and the only label in hand is wrong for
|
|
145
|
+
// one of them), or the repo misreported. Labelling old bytes with a newer revision's number and
|
|
146
|
+
// action id is what the asker's `saveRestored` — keyed by action id — then writes over the good
|
|
147
|
+
// copy it already holds. Serve nothing instead: every caller already handles "holds nothing",
|
|
148
|
+
// and `restoreRevision` turns it into a loud "not found during restore attempt". A served
|
|
149
|
+
// revision AT OR BELOW the pin is the block unchanged since the pin, served exactly as before.
|
|
150
|
+
if (rev !== undefined && served.rev > rev) {
|
|
151
|
+
log('serve:skip blockId=%s served=%d requested=%d latest=%d (pinned read — refusing to mislabel content)', blockId, served.rev, rev, latest.rev);
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const proof = await servableProof(repo, blockId, served);
|
|
155
|
+
return singleRevisionArchive(blockId, served, entry.block, proof);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The stored proof for the revision being served, or `undefined` — including whenever anything is
|
|
159
|
+
* off about it. Serving no proof is always safe (every consumer must already handle its absence),
|
|
160
|
+
* so this fails closed on all three unhappy paths:
|
|
161
|
+
*
|
|
162
|
+
* - the repo has no proof accessor at all (a plain `IRepo`, a test double);
|
|
163
|
+
* - the lookup throws — a storage fault must not turn a servable archive into "holds nothing",
|
|
164
|
+
* because that would recreate the phantom-non-holder bug the archive shape exists to prevent;
|
|
165
|
+
* - the stored proof's own message does not name this `(blockId, rev, actionId)`. That is a local
|
|
166
|
+
* storage-integrity fault, not a peer's doing, and publishing a mis-paired proof would hand
|
|
167
|
+
* every receiver an artifact that cannot verify. Logged loudly, because it means a proof was
|
|
168
|
+
* written under a key its content contradicts.
|
|
169
|
+
*
|
|
170
|
+
* Exported for the SAME reason {@link singleRevisionArchive} is: the mesh test harness answers the
|
|
171
|
+
* latest-revision consult out of a sibling's repo directly rather than over the sync protocol, and
|
|
172
|
+
* a harness that attached proofs by a different rule than a real peer would let every mesh-tier
|
|
173
|
+
* test silently exercise a path production does not have.
|
|
174
|
+
*/
|
|
175
|
+
export async function servableProof(repo, blockId, latest) {
|
|
176
|
+
if (typeof repo.getBlockProof !== 'function')
|
|
177
|
+
return undefined;
|
|
178
|
+
let proof;
|
|
179
|
+
try {
|
|
180
|
+
proof = await repo.getBlockProof(blockId, latest.rev);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
log('serve:proof-lookup-failed blockId=%s rev=%d error=%s', blockId, latest.rev, error instanceof Error ? error.message : String(error));
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
if (!proof)
|
|
187
|
+
return undefined;
|
|
188
|
+
if (!proofClaimsCommit(proof, { blockId, rev: latest.rev, actionId: latest.actionId })) {
|
|
189
|
+
log('serve:proof-claim-mismatch blockId=%s rev=%d actionId=%s', blockId, latest.rev, latest.actionId);
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return proof;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=block-archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-archive.js","sourceRoot":"","sources":["../../../src/storage/block-archive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { BlockId } from "@optimystic/db-core";
|
|
2
|
+
/**
|
|
3
|
+
* The ONE per-block write lock.
|
|
4
|
+
*
|
|
5
|
+
* A block's metadata is stored as a single blob — `{ latest, ranges }` — read and written whole, so
|
|
6
|
+
* any read-modify-write of it overwrites `latest` whether it meant to or not. The invariant is
|
|
7
|
+
* therefore stated over the whole blob, not over `latest`:
|
|
8
|
+
*
|
|
9
|
+
* > A block's metadata, revision records, action transforms, pending records, and stored proofs are
|
|
10
|
+
* > only ever written while holding {@link blockWriteLatchKey}`(blockId)`.
|
|
11
|
+
*
|
|
12
|
+
* This module is the single acquirer of that key. The check, which deliberately matches the call
|
|
13
|
+
* shape (the escapes keep this very comment from matching):
|
|
14
|
+
*
|
|
15
|
+
* > grep -rnE "Latches\.acquire\(" packages/db-p2p/src
|
|
16
|
+
*
|
|
17
|
+
* That must return exactly one line — the call in `acquireBlockWriteLatch` below. A second hit
|
|
18
|
+
* anywhere means a caller has started taking the key directly and the token discipline has a hole.
|
|
19
|
+
* Every writing method on `IBlockStorage` demands a
|
|
20
|
+
* {@link BlockWriteLatch} token, which only this module can mint, so an unlatched write does not
|
|
21
|
+
* type-check rather than merely being documented as forbidden.
|
|
22
|
+
*
|
|
23
|
+
* `Latches` is a plain FIFO promise-chain mutex — no owner tracking, no re-entrancy — so a holder
|
|
24
|
+
* must never call back into something that acquires the same block's key. The token is what lets a
|
|
25
|
+
* callee prove it is already inside the latch instead of re-acquiring.
|
|
26
|
+
*/
|
|
27
|
+
export declare const blockWriteLatchKey: (blockId: BlockId) => string;
|
|
28
|
+
/**
|
|
29
|
+
* Opaque proof that the bearer is executing inside {@link blockWriteLatchKey}`(blockId)`. Only
|
|
30
|
+
* {@link acquireBlockWriteLatch} (and {@link withBlockWriteLatch} through it) can construct one:
|
|
31
|
+
* the constructor is private and the module-scoped minter is assigned from a static block, where
|
|
32
|
+
* the private constructor is callable — no cast, nothing outside this module can build a token.
|
|
33
|
+
*
|
|
34
|
+
* A token is only valid while the latch it proves is actually held: releasing expires it, so a
|
|
35
|
+
* callback that stashes its token and writes after its scope closed is rejected instead of silently
|
|
36
|
+
* writing unlatched. `live` is the check; only this module can clear it.
|
|
37
|
+
*
|
|
38
|
+
* `BlockStorage` checks `latch.blockId` against its own id on every write, so a token for one block
|
|
39
|
+
* cannot be presented for another.
|
|
40
|
+
*/
|
|
41
|
+
export declare class BlockWriteLatch {
|
|
42
|
+
#private;
|
|
43
|
+
readonly blockId: BlockId;
|
|
44
|
+
private constructor();
|
|
45
|
+
/** False once the latch this token proves has been released. */
|
|
46
|
+
get live(): boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Acquire the write latch for `blockId`. The non-scoped single-block form is the building block for
|
|
50
|
+
* {@link acquireBlockWriteLatches} and for a caller that must hold one latch across control flow a
|
|
51
|
+
* callback cannot express; everything else should prefer {@link withBlockWriteLatch}. A caller that
|
|
52
|
+
* wants N latches must go through {@link acquireBlockWriteLatches}, not loop over this. The caller
|
|
53
|
+
* MUST call `release` exactly once, in a `finally`. Releasing expires the token, so a write attempted
|
|
54
|
+
* with it afterwards is rejected rather than running outside the latch.
|
|
55
|
+
*/
|
|
56
|
+
export declare function acquireBlockWriteLatch(blockId: BlockId): Promise<{
|
|
57
|
+
latch: BlockWriteLatch;
|
|
58
|
+
release: () => void;
|
|
59
|
+
}>;
|
|
60
|
+
/** The N-latch hold {@link acquireBlockWriteLatches} returns: a token per block, and one release. */
|
|
61
|
+
export type BlockWriteLatches = {
|
|
62
|
+
/** Token per requested block id, so each write can prove it runs inside that block's latch. */
|
|
63
|
+
readonly latches: ReadonlyMap<BlockId, BlockWriteLatch>;
|
|
64
|
+
/** Releases every latch, in reverse acquisition order. Call exactly once, in a `finally`. */
|
|
65
|
+
readonly release: () => void;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Acquire the write latches for a whole set of blocks at once — the only sanctioned way to hold more
|
|
69
|
+
* than one. Two callers need it (`StorageRepo.commit` and `applyInvalidation`), and the property that
|
|
70
|
+
* keeps them from deadlocking against each other is not local to either: it is that EVERY multi-latch
|
|
71
|
+
* holder acquires in the one global order. Owning that here makes it a property of the module that
|
|
72
|
+
* owns the key rather than a rule each call site restates and a third one could get wrong.
|
|
73
|
+
*
|
|
74
|
+
* Three things this does that a hand-rolled loop keeps getting wrong:
|
|
75
|
+
* - **Dedups** the ids. `Latches` is a plain FIFO mutex with no re-entrancy, so a repeated id in the
|
|
76
|
+
* request would deadlock the acquirer against itself.
|
|
77
|
+
* - **Sorts** them, so any two multi-latch holders acquire in the same order and no cycle exists.
|
|
78
|
+
* - **Releases what it already took** if an acquisition partway through the set throws, rather than
|
|
79
|
+
* stranding those latches forever.
|
|
80
|
+
*
|
|
81
|
+
* The returned map is keyed by the ids actually held, so a caller that wants its own (e.g. request)
|
|
82
|
+
* ordering can iterate its own list and look each token up.
|
|
83
|
+
*/
|
|
84
|
+
export declare function acquireBlockWriteLatches(blockIds: Iterable<BlockId>): Promise<BlockWriteLatches>;
|
|
85
|
+
/**
|
|
86
|
+
* Run `fn` while holding the write latch for `blockId`, handing it the token to pass down to the
|
|
87
|
+
* storage writes it makes. Acquire/release is per call, so a caller holds at most one block latch at
|
|
88
|
+
* a time and cannot deadlock against a sorted, up-front {@link acquireBlockWriteLatches} hold — as
|
|
89
|
+
* long as `fn` does not itself acquire another block's latch (nothing in this package does).
|
|
90
|
+
*/
|
|
91
|
+
export declare function withBlockWriteLatch<T>(blockId: BlockId, fn: (latch: BlockWriteLatch) => Promise<T>): Promise<T>;
|
|
92
|
+
//# sourceMappingURL=block-latch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-latch.d.ts","sourceRoot":"","sources":["../../../src/storage/block-latch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAGnD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,KAAG,MAAkC,CAAC;AAKzF;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAe;;IAGP,QAAQ,CAAC,OAAO,EAAE,OAAO;IAA7C,OAAO;IAEP,gEAAgE;IAChE,IAAI,IAAI,IAAI,OAAO,CAElB;CAMD;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,CAUvH;AAED,qGAAqG;AACrG,MAAM,MAAM,iBAAiB,GAAG;IAC/B,+FAA+F;IAC/F,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IACxD,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqBtG;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOrH"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Latches } from "@optimystic/db-core";
|
|
2
|
+
/**
|
|
3
|
+
* The ONE per-block write lock.
|
|
4
|
+
*
|
|
5
|
+
* A block's metadata is stored as a single blob — `{ latest, ranges }` — read and written whole, so
|
|
6
|
+
* any read-modify-write of it overwrites `latest` whether it meant to or not. The invariant is
|
|
7
|
+
* therefore stated over the whole blob, not over `latest`:
|
|
8
|
+
*
|
|
9
|
+
* > A block's metadata, revision records, action transforms, pending records, and stored proofs are
|
|
10
|
+
* > only ever written while holding {@link blockWriteLatchKey}`(blockId)`.
|
|
11
|
+
*
|
|
12
|
+
* This module is the single acquirer of that key. The check, which deliberately matches the call
|
|
13
|
+
* shape (the escapes keep this very comment from matching):
|
|
14
|
+
*
|
|
15
|
+
* > grep -rnE "Latches\.acquire\(" packages/db-p2p/src
|
|
16
|
+
*
|
|
17
|
+
* That must return exactly one line — the call in `acquireBlockWriteLatch` below. A second hit
|
|
18
|
+
* anywhere means a caller has started taking the key directly and the token discipline has a hole.
|
|
19
|
+
* Every writing method on `IBlockStorage` demands a
|
|
20
|
+
* {@link BlockWriteLatch} token, which only this module can mint, so an unlatched write does not
|
|
21
|
+
* type-check rather than merely being documented as forbidden.
|
|
22
|
+
*
|
|
23
|
+
* `Latches` is a plain FIFO promise-chain mutex — no owner tracking, no re-entrancy — so a holder
|
|
24
|
+
* must never call back into something that acquires the same block's key. The token is what lets a
|
|
25
|
+
* callee prove it is already inside the latch instead of re-acquiring.
|
|
26
|
+
*/
|
|
27
|
+
export const blockWriteLatchKey = (blockId) => `Block.write:${blockId}`;
|
|
28
|
+
let mint;
|
|
29
|
+
let expire;
|
|
30
|
+
/**
|
|
31
|
+
* Opaque proof that the bearer is executing inside {@link blockWriteLatchKey}`(blockId)`. Only
|
|
32
|
+
* {@link acquireBlockWriteLatch} (and {@link withBlockWriteLatch} through it) can construct one:
|
|
33
|
+
* the constructor is private and the module-scoped minter is assigned from a static block, where
|
|
34
|
+
* the private constructor is callable — no cast, nothing outside this module can build a token.
|
|
35
|
+
*
|
|
36
|
+
* A token is only valid while the latch it proves is actually held: releasing expires it, so a
|
|
37
|
+
* callback that stashes its token and writes after its scope closed is rejected instead of silently
|
|
38
|
+
* writing unlatched. `live` is the check; only this module can clear it.
|
|
39
|
+
*
|
|
40
|
+
* `BlockStorage` checks `latch.blockId` against its own id on every write, so a token for one block
|
|
41
|
+
* cannot be presented for another.
|
|
42
|
+
*/
|
|
43
|
+
export class BlockWriteLatch {
|
|
44
|
+
blockId;
|
|
45
|
+
#live = true;
|
|
46
|
+
constructor(blockId) {
|
|
47
|
+
this.blockId = blockId;
|
|
48
|
+
}
|
|
49
|
+
/** False once the latch this token proves has been released. */
|
|
50
|
+
get live() {
|
|
51
|
+
return this.#live;
|
|
52
|
+
}
|
|
53
|
+
static {
|
|
54
|
+
mint = (blockId) => new BlockWriteLatch(blockId);
|
|
55
|
+
expire = (latch) => { latch.#live = false; };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Acquire the write latch for `blockId`. The non-scoped single-block form is the building block for
|
|
60
|
+
* {@link acquireBlockWriteLatches} and for a caller that must hold one latch across control flow a
|
|
61
|
+
* callback cannot express; everything else should prefer {@link withBlockWriteLatch}. A caller that
|
|
62
|
+
* wants N latches must go through {@link acquireBlockWriteLatches}, not loop over this. The caller
|
|
63
|
+
* MUST call `release` exactly once, in a `finally`. Releasing expires the token, so a write attempted
|
|
64
|
+
* with it afterwards is rejected rather than running outside the latch.
|
|
65
|
+
*/
|
|
66
|
+
export async function acquireBlockWriteLatch(blockId) {
|
|
67
|
+
const releaseLatch = await Latches.acquire(blockWriteLatchKey(blockId));
|
|
68
|
+
const latch = mint(blockId);
|
|
69
|
+
return {
|
|
70
|
+
latch,
|
|
71
|
+
release: () => {
|
|
72
|
+
expire(latch);
|
|
73
|
+
releaseLatch();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Acquire the write latches for a whole set of blocks at once — the only sanctioned way to hold more
|
|
79
|
+
* than one. Two callers need it (`StorageRepo.commit` and `applyInvalidation`), and the property that
|
|
80
|
+
* keeps them from deadlocking against each other is not local to either: it is that EVERY multi-latch
|
|
81
|
+
* holder acquires in the one global order. Owning that here makes it a property of the module that
|
|
82
|
+
* owns the key rather than a rule each call site restates and a third one could get wrong.
|
|
83
|
+
*
|
|
84
|
+
* Three things this does that a hand-rolled loop keeps getting wrong:
|
|
85
|
+
* - **Dedups** the ids. `Latches` is a plain FIFO mutex with no re-entrancy, so a repeated id in the
|
|
86
|
+
* request would deadlock the acquirer against itself.
|
|
87
|
+
* - **Sorts** them, so any two multi-latch holders acquire in the same order and no cycle exists.
|
|
88
|
+
* - **Releases what it already took** if an acquisition partway through the set throws, rather than
|
|
89
|
+
* stranding those latches forever.
|
|
90
|
+
*
|
|
91
|
+
* The returned map is keyed by the ids actually held, so a caller that wants its own (e.g. request)
|
|
92
|
+
* ordering can iterate its own list and look each token up.
|
|
93
|
+
*/
|
|
94
|
+
export async function acquireBlockWriteLatches(blockIds) {
|
|
95
|
+
const ordered = Array.from(new Set(blockIds)).sort();
|
|
96
|
+
const releases = [];
|
|
97
|
+
const release = () => {
|
|
98
|
+
for (let i = releases.length - 1; i >= 0; i--) {
|
|
99
|
+
releases[i]();
|
|
100
|
+
}
|
|
101
|
+
releases.length = 0;
|
|
102
|
+
};
|
|
103
|
+
const latches = new Map();
|
|
104
|
+
try {
|
|
105
|
+
for (const id of ordered) {
|
|
106
|
+
const acquired = await acquireBlockWriteLatch(id);
|
|
107
|
+
releases.push(acquired.release);
|
|
108
|
+
latches.set(id, acquired.latch);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
release();
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
return { latches, release };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run `fn` while holding the write latch for `blockId`, handing it the token to pass down to the
|
|
119
|
+
* storage writes it makes. Acquire/release is per call, so a caller holds at most one block latch at
|
|
120
|
+
* a time and cannot deadlock against a sorted, up-front {@link acquireBlockWriteLatches} hold — as
|
|
121
|
+
* long as `fn` does not itself acquire another block's latch (nothing in this package does).
|
|
122
|
+
*/
|
|
123
|
+
export async function withBlockWriteLatch(blockId, fn) {
|
|
124
|
+
const { latch, release } = await acquireBlockWriteLatch(blockId);
|
|
125
|
+
try {
|
|
126
|
+
return await fn(latch);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
release();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=block-latch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-latch.js","sourceRoot":"","sources":["../../../src/storage/block-latch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAgB,EAAU,EAAE,CAAC,eAAe,OAAO,EAAE,CAAC;AAEzF,IAAI,IAA4C,CAAC;AACjD,IAAI,MAAyC,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,eAAe;IAGE;IAF7B,KAAK,GAAG,IAAI,CAAC;IAEb,YAA6B,OAAgB;QAAhB,YAAO,GAAP,OAAO,CAAS;IAAI,CAAC;IAElD,gEAAgE;IAChE,IAAI,IAAI;QACP,OAAO,IAAI,CAAC,KAAK,CAAC;IACnB,CAAC;IAED;QACC,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC;CACD;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,OAAgB;IAC5D,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5B,OAAO;QACN,KAAK;QACL,OAAO,EAAE,GAAG,EAAE;YACb,MAAM,CAAC,KAAK,CAAC,CAAC;YACd,YAAY,EAAE,CAAC;QAChB,CAAC;KACD,CAAC;AACH,CAAC;AAUD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,QAA2B;IACzE,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACrD,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,GAAG,EAAE;QACpB,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/C,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAC;QAChB,CAAC;QACD,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IACrB,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IACpD,IAAI,CAAC;QACJ,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,QAAQ,GAAG,MAAM,sBAAsB,CAAC,EAAE,CAAC,CAAC;YAClD,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YAChC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;IACF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,OAAO,EAAE,CAAC;QACV,MAAM,GAAG,CAAC;IACX,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAI,OAAgB,EAAE,EAA0C;IACxG,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACjE,IAAI,CAAC;QACJ,OAAO,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;YAAS,CAAC;QACV,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC"}
|