@optimystic/db-p2p 0.29.0 → 1.0.0-beta.2
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/cluster/cluster-repo.d.ts +75 -16
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +205 -24
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +16 -1
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +66 -7
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +5 -1
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +45 -8
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +6 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +62 -2
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/package.json +2 -2
- package/src/cluster/cluster-repo.ts +2671 -2488
- package/src/repo/cluster-coordinator.ts +1113 -1039
- package/src/repo/coordinator-repo.ts +2687 -2648
- package/src/storage/storage-repo.ts +65 -2
|
@@ -281,6 +281,15 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
281
281
|
// Sort a COPY: when `latest` is undefined, `missing` aliases the caller's
|
|
282
282
|
// `context.committed` array, and an in-place `.sort()` would reorder the shared
|
|
283
283
|
// request context under the caller's feet.
|
|
284
|
+
//
|
|
285
|
+
// NOTE: this loop SKIPS an entry whose pending it does not hold and promotes the
|
|
286
|
+
// next one anyway — so a member missing an intermediate revision that touched THIS
|
|
287
|
+
// block forks it exactly as an un-guarded commit would. `internalCommit`'s declared-
|
|
288
|
+
// base guard cannot help here: `context.committed` is a collection-level list of
|
|
289
|
+
// (actionId, rev) with no per-block base, and "no pending for that action" is the
|
|
290
|
+
// normal case for the many actions that never touched this block. Closing it needs
|
|
291
|
+
// the authored base stored WITH the pended transform — tracked by
|
|
292
|
+
// `backlog/bug-a-pended-transform-does-not-carry-its-base`.
|
|
284
293
|
try {
|
|
285
294
|
for (const { actionId, rev } of [...missing].sort((a, b) => a.rev - b.rev)) {
|
|
286
295
|
const pending = await blockStorage.getPendingTransaction(actionId);
|
|
@@ -905,7 +914,11 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
905
914
|
}
|
|
906
915
|
try {
|
|
907
916
|
// internalCommit will throw if it encounters an issue
|
|
908
|
-
|
|
917
|
+
// The writer's per-block base declaration (see BlockContentDigest.baseRev) rides on the
|
|
918
|
+
// commit op and is what lets internalCommit tell a legitimate collection-level rev gap
|
|
919
|
+
// apart from a genuinely missed update to THIS block. Untrusted wire data — the guard
|
|
920
|
+
// validates it, this call site only forwards it.
|
|
921
|
+
const collectionId = await this.internalCommit(blockId, request.actionId, request.rev, storage, latch, proof, request.blockDigests?.[blockId]?.baseRev);
|
|
909
922
|
if (collectionId !== undefined) {
|
|
910
923
|
const list = collectionBlocks.get(collectionId) ?? [];
|
|
911
924
|
list.push(blockId);
|
|
@@ -1174,7 +1187,13 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
1174
1187
|
return await this.createBlockStorage(blockId).getBlockProof(rev);
|
|
1175
1188
|
}
|
|
1176
1189
|
|
|
1177
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* @param declaredBaseRev The committed revision of the base the WRITER applied this block's
|
|
1192
|
+
* transform to, as declared in the commit op's `blockDigests[blockId].baseRev`. Untrusted wire
|
|
1193
|
+
* data, so it is typed `unknown` and validated below. Absent from the read-driven promotion in
|
|
1194
|
+
* {@link get}, which has no commit request and therefore nothing to compare against.
|
|
1195
|
+
*/
|
|
1196
|
+
private async internalCommit(blockId: BlockId, actionId: ActionId, rev: number, storage: IBlockStorage, latch: BlockWriteLatch, proof?: BlockCommitProof, declaredBaseRev?: unknown): Promise<CollectionId | undefined> {
|
|
1178
1197
|
// Note: This method is called under the block write latch — by commit() (within its locked
|
|
1179
1198
|
// critical section) and by the read-driven promotion in get() (which takes the same latch);
|
|
1180
1199
|
// `latch` is the proof of that. So, operations like getPendingTransaction, getLatest,
|
|
@@ -1194,6 +1213,50 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
1194
1213
|
|
|
1195
1214
|
// Get prior materialized block if it exists
|
|
1196
1215
|
const latest = await storage.getLatest();
|
|
1216
|
+
|
|
1217
|
+
// FORK GUARD: apply an update-only transform ONLY to the base the writer actually read.
|
|
1218
|
+
// Revisions are allocated per COLLECTION, not per block, so `rev - 1` is meaningless here — a
|
|
1219
|
+
// member legitimately holds block X at rev 1 and receives a commit of X at rev 7 when revs 2-6
|
|
1220
|
+
// touched other blocks. The only sound discriminator is the writer's own per-block declaration:
|
|
1221
|
+
// it read the block at `declaredBaseRev`, so a member holding anything else would be applying
|
|
1222
|
+
// the transform to different bytes and silently forking the block's content at this revision.
|
|
1223
|
+
//
|
|
1224
|
+
// Each clause is deliberate:
|
|
1225
|
+
// - `typeof declaredBaseRev === 'number'` — `blockDigests` is untrusted wire data with no
|
|
1226
|
+
// ingress schema (same rule as ClusterMember.validateCommitOperations). Missing, malformed,
|
|
1227
|
+
// or absent-by-design declarations ABSTAIN, preserving today's behavior for pre-upgrade
|
|
1228
|
+
// writers, undeclarable blocks, and the read-driven promotion in get().
|
|
1229
|
+
// - `!transform.insert` — an insert-carrying transform is base-independent, so there is nothing
|
|
1230
|
+
// to fork. Keyed on the member's OWN pended transform (as previewCommitDigest does), never on
|
|
1231
|
+
// the declaration, so a hostile writer cannot flip the arm by attaching a bogus baseRev.
|
|
1232
|
+
// - `latest?.rev !== declaredBaseRev` covers all three unsafe states: BEHIND the declared base
|
|
1233
|
+
// (missed updates — the fork case), AHEAD of it (this member holds a revision the writer
|
|
1234
|
+
// never saw — divergent history), and no local revision at all against a numeric declaration.
|
|
1235
|
+
//
|
|
1236
|
+
// Refusing is cheap and self-healing: refuseMissingBase throws MissingBaseRevisionError, which
|
|
1237
|
+
// commit() classifies as divergence and ClusterMember.applyConsensusOperation maps to "behind",
|
|
1238
|
+
// running reconcileDivergentCommit to pull the committed revision from a cohort peer. The
|
|
1239
|
+
// writer's retry then lands on a healed base. A hostile writer declaring a junk numeric baseRev
|
|
1240
|
+
// can force refusals and reconcile churn, but never a fork.
|
|
1241
|
+
//
|
|
1242
|
+
// NOTE: the AHEAD case is reported as "behind" divergence like every other missing-base
|
|
1243
|
+
// refusal, so a cohort where nobody holds `rev` reconciles, fails `no-rev-quorum`, and logs
|
|
1244
|
+
// that rather than a clean stale failure. Correct outcome — the writer read a base the cohort
|
|
1245
|
+
// has moved past, and its retry re-reads — but the log reads as lag when it is the opposite.
|
|
1246
|
+
// If those lines ever have to be triaged in volume, give the ahead arm its own reason string.
|
|
1247
|
+
//
|
|
1248
|
+
// NOTE: this guard only reaches what the writer declared, so two arms of the same fork still
|
|
1249
|
+
// stand — both tracked by `backlog/bug-a-pended-transform-does-not-carry-its-base`:
|
|
1250
|
+
// (1) a commit whose block declares NO digest — pre-upgrade writer, undeclarable block
|
|
1251
|
+
// (read-far-then-update eviction, see db-core transform/digest.ts), or a delete-only transform —
|
|
1252
|
+
// gap-applies exactly as before; (2) the read-driven promotion in `get`, which reaches this
|
|
1253
|
+
// method with no commit request at all (`declaredBaseRev` undefined) and so cannot check. If
|
|
1254
|
+
// forked-content reports persist, those are the residuals to look at.
|
|
1255
|
+
if (typeof declaredBaseRev === 'number' && !transform.insert && latest?.rev !== declaredBaseRev) {
|
|
1256
|
+
return await this.refuseMissingBase(blockId, actionId, rev, storage, latch,
|
|
1257
|
+
`local latest ${latest?.rev ?? 'none'} is not the declared base ${declaredBaseRev} of rev ${rev}`);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1197
1260
|
const priorBlock = await this.readCommitBase(blockId, actionId, rev, storage, latest, latch);
|
|
1198
1261
|
|
|
1199
1262
|
// Apply transform and save materialized block
|