@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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
2
2
|
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
|
|
3
3
|
import type {
|
|
4
|
-
ActionId, BlockId, IBlock, Log,
|
|
4
|
+
ActionId, ActionRev, BlockId, IBlock, Log,
|
|
5
5
|
DisputeResolutionProof, ArbitrationVoteProof, RevertedBlock,
|
|
6
6
|
} from '@optimystic/db-core';
|
|
7
7
|
import { applyTransform, hashString } from '@optimystic/db-core';
|
|
8
8
|
import type { IBlockStorage } from '../storage/i-block-storage.js';
|
|
9
|
+
import { acquireBlockWriteLatches } from '../storage/block-latch.js';
|
|
9
10
|
import type { DisputeResolution, ArbitrationVote } from './types.js';
|
|
10
11
|
import { createLogger } from '../logger.js';
|
|
11
12
|
|
|
@@ -478,16 +479,6 @@ export type InvalidationContext = {
|
|
|
478
479
|
readonly log: Log<unknown>;
|
|
479
480
|
/** Resolves a block's storage so the compensating revision can be written. */
|
|
480
481
|
readonly createBlockStorage: (blockId: BlockId) => IBlockStorage;
|
|
481
|
-
/**
|
|
482
|
-
* Runs `fn` while holding the same per-block commit latch `StorageRepo.commit` and
|
|
483
|
-
* `saveReplicatedBlock` hold, so the compensating `saveReplica`/`saveDeletion` read-modify-write of
|
|
484
|
-
* `meta.latest` is mutually exclusive with a concurrent local commit on that block. Without it, an
|
|
485
|
-
* invalidation advancing `latest` outside that latch is invisible to commit's staleness guard, so a
|
|
486
|
-
* commit that read a stale `latest` can clobber it back down — a lost-update / non-monotonic
|
|
487
|
-
* regression. Optional: when omitted (unit tests / non-`StorageRepo` hosts) the write runs unlatched,
|
|
488
|
-
* preserving today's behavior. The host supplies `StorageRepo`'s `withBlockCommitLatch`.
|
|
489
|
-
*/
|
|
490
|
-
readonly withBlockCommitLatch?: <T>(blockId: BlockId, fn: () => Promise<T>) => Promise<T>;
|
|
491
482
|
};
|
|
492
483
|
|
|
493
484
|
export type ApplyInvalidationParams = {
|
|
@@ -499,8 +490,19 @@ export type ApplyInvalidationParams = {
|
|
|
499
490
|
readonly proof: DisputeResolutionProof;
|
|
500
491
|
/**
|
|
501
492
|
* Consensus-assigned revision slot for the compensating revision (collection-global). When
|
|
502
|
-
* omitted, computed as one past the highest current tip across the reverted blocks —
|
|
503
|
-
*
|
|
493
|
+
* omitted, computed as one past the highest current tip across the reverted blocks — read under
|
|
494
|
+
* the blocks' write latches, so an omitted slot is strictly past every tip and its compensating
|
|
495
|
+
* writes can never be refused by the monotonic guard.
|
|
496
|
+
*
|
|
497
|
+
* An explicit slot at or below any affected block's tip is refused wholesale (`applied: false`,
|
|
498
|
+
* reason `stale-revision`) rather than partially written.
|
|
499
|
+
*
|
|
500
|
+
* NOTE: nothing on the wire supplies this today. `InvalidateRequest`
|
|
501
|
+
* (`db-core/src/network/struct.ts`) carries `invalidatedActionId` / `invalidatedRev` / `blockIds` /
|
|
502
|
+
* `collectionId` / `resolution` and NO compensating-revision field, so the network-facing apply
|
|
503
|
+
* path leaves this undefined and every member computes its own slot locally. The `stale-revision`
|
|
504
|
+
* path is therefore reachable only from tests and from a future caller that does pass an agreed
|
|
505
|
+
* slot; adding that wire field is out of scope here.
|
|
504
506
|
*/
|
|
505
507
|
readonly rev?: number;
|
|
506
508
|
/**
|
|
@@ -526,11 +528,40 @@ export type ApplyInvalidationParams = {
|
|
|
526
528
|
|
|
527
529
|
export type ApplyInvalidationResult = {
|
|
528
530
|
readonly applied: boolean;
|
|
529
|
-
|
|
531
|
+
/**
|
|
532
|
+
* - `already-applied` — a re-receipt; the log already holds this `(invalidatedActionId, disputeId)`.
|
|
533
|
+
* `reverted` carries the EXISTING entry's blocks, so a caller can reuse them.
|
|
534
|
+
* - `invalid-certificate` — the proof is not a valid challenger-wins certificate for the target.
|
|
535
|
+
* - `stale-revision` — at least one affected block already sits at or past the compensating
|
|
536
|
+
* revision slot, so the compensating write would be refused by the monotonic guard. Nothing was
|
|
537
|
+
* written and nothing was appended (all-or-nothing); the invalidation is safely re-deliverable
|
|
538
|
+
* once the caller supplies a slot past every affected block's tip. Reachable only when the
|
|
539
|
+
* caller supplies an explicit {@link ApplyInvalidationParams.rev} — a slot this function
|
|
540
|
+
* computes itself is strictly past every tip by construction.
|
|
541
|
+
*/
|
|
542
|
+
readonly reason?: 'already-applied' | 'invalid-certificate' | 'stale-revision';
|
|
530
543
|
readonly rev?: number;
|
|
531
544
|
readonly reverted: ReadonlyArray<RevertedBlock>;
|
|
532
545
|
};
|
|
533
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Boundary invariant for the compensating write: the effective `ActionRev` a forward write returns
|
|
549
|
+
* must be the one we intended. The precheck above already establishes that, so a mismatch means a
|
|
550
|
+
* write path refused for a reason the precheck does not model — an internal contradiction (a write
|
|
551
|
+
* refused while its latch was held and its tip was checked), not a condition to degrade around. It
|
|
552
|
+
* throws rather than returning `applied: false`: `ClusterRepo.applyConsensusInvalidation` tolerates a
|
|
553
|
+
* sink throw, logs it, and rolls back its dedup marker so a re-broadcast retries.
|
|
554
|
+
*/
|
|
555
|
+
function assertWriteLanded(blockId: BlockId, intended: ActionRev, effective: ActionRev): void {
|
|
556
|
+
if (effective.rev !== intended.rev || effective.actionId !== intended.actionId) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
`Invalidation compensating write did not land for block ${blockId}: intended ` +
|
|
559
|
+
`{ rev: ${intended.rev}, actionId: ${intended.actionId} }, effective ` +
|
|
560
|
+
`{ rev: ${effective.rev}, actionId: ${effective.actionId} }`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
534
565
|
/**
|
|
535
566
|
* Deterministically applies a single-collection invalidation: the durable reversal primitive every
|
|
536
567
|
* cluster member runs identically (it carries the `reverted` targets and proof, exactly as the
|
|
@@ -540,15 +571,29 @@ export type ApplyInvalidationResult = {
|
|
|
540
571
|
* 1. **Dedup** — if the log already holds an invalidation for `(invalidatedActionId, disputeId)`,
|
|
541
572
|
* this is a re-receipt (rebroadcast / sync / retry): no-op, append nothing.
|
|
542
573
|
* 2. **Certificate** — reject (append nothing) unless `proof` is a valid challenger-wins certificate.
|
|
543
|
-
* 3. **Reverted revisions** —
|
|
544
|
-
* new monotonic revision (a forward compensating
|
|
574
|
+
* 3. **Reverted revisions** — holding EVERY affected block's write latch, recompute each block's
|
|
575
|
+
* as-if-`T_inv`-absent content and write a new monotonic revision (a forward compensating
|
|
576
|
+
* transform; prior revisions are retained). All-or-nothing: if any block already sits at or past
|
|
577
|
+
* the compensating slot, nothing is written and nothing is appended.
|
|
545
578
|
* 4. **Log entry** — append the {@link InvalidationEntry} carrying the proof and `reverted` targets,
|
|
546
579
|
* making `committed-invalidated` durable and recoverable on sync.
|
|
580
|
+
*
|
|
581
|
+
* The `reverted` entries an applied result carries therefore describe writes that ACTUALLY landed,
|
|
582
|
+
* at the revision and with the content hash they name — the property `cascade.ts` relies on when it
|
|
583
|
+
* decides a read-dependent's fate by comparing observed content against `restoredContentHash`.
|
|
547
584
|
*/
|
|
548
585
|
export async function applyInvalidation(ctx: InvalidationContext, params: ApplyInvalidationParams): Promise<ApplyInvalidationResult> {
|
|
549
586
|
const { invalidatedActionId, invalidatedRev, blockIds, proof } = params;
|
|
550
587
|
|
|
551
588
|
// 1. Idempotent re-receipt — keyed on (invalidatedActionId, disputeId).
|
|
589
|
+
// NOTE: this read is outside the block latches taken in step 3, so two OVERLAPPING applies of the
|
|
590
|
+
// same invalidation would both pass here and both write a compensating revision (the second at a
|
|
591
|
+
// slot one past the first), appending two entries for one invalidation. Not reachable today: the
|
|
592
|
+
// network path serializes through consensus and dedups in memory first
|
|
593
|
+
// (`ClusterRepo.applyConsensusInvalidation`), and the cascade applies its children sequentially.
|
|
594
|
+
// If a caller ever applies the same invalidation concurrently, this dedup has to move inside the
|
|
595
|
+
// critical section — which means keying it on something the block latches actually cover, since
|
|
596
|
+
// `ctx.log` is not latched by them.
|
|
552
597
|
const existing = await ctx.log.findInvalidation(invalidatedActionId);
|
|
553
598
|
if (existing && existing.resolution.disputeId === proof.disputeId) {
|
|
554
599
|
log('apply-skip-duplicate actionId=%s disputeId=%s', invalidatedActionId, proof.disputeId);
|
|
@@ -567,48 +612,113 @@ export async function applyInvalidation(ctx: InvalidationContext, params: ApplyI
|
|
|
567
612
|
return { applied: false, reason: 'invalid-certificate', reverted: [] };
|
|
568
613
|
}
|
|
569
614
|
|
|
570
|
-
// 3.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
615
|
+
// 3. Compensating revisions — ONE critical section spanning every affected block.
|
|
616
|
+
//
|
|
617
|
+
// Latch discipline: hold ALL affected blocks' write latches across compute → revision slot →
|
|
618
|
+
// precheck → writes. `acquireBlockWriteLatches` is the package's single multi-latch entry point
|
|
619
|
+
// (it dedups and acquires in the one global sorted order), so this cannot deadlock against the
|
|
620
|
+
// other multi-latch holder, `StorageRepo.commit`. (The previous code took each block's latch
|
|
621
|
+
// around only its own write, which left the tips it computed from — and the revision slot derived
|
|
622
|
+
// from them — read outside any latch: a concurrent commit could land a newer revision in the gap,
|
|
623
|
+
// the monotonic guard in saveReplica/saveDeletion would correctly refuse the compensating write,
|
|
624
|
+
// and the log entry appended in step 4 would then assert a restore that never happened.)
|
|
625
|
+
//
|
|
626
|
+
// Collapsing duplicate ids is why `uniqueBlockIds` drives the writes and `reverted` too: one
|
|
627
|
+
// latched block yields one compensating write and one entry. The certificate target above
|
|
628
|
+
// deliberately still uses the caller's `blockIds` verbatim — the arbitrators signed over that list.
|
|
629
|
+
const uniqueBlockIds = Array.from(new Set(blockIds));
|
|
630
|
+
const { latches, release } = await acquireBlockWriteLatches(uniqueBlockIds);
|
|
631
|
+
let reverted: RevertedBlock[];
|
|
632
|
+
let rev: number;
|
|
633
|
+
try {
|
|
634
|
+
// --- Start of critical section ---
|
|
635
|
+
|
|
636
|
+
// Compute compensating content from tips read UNDER the latches. `computeRevertedBlock` reads
|
|
637
|
+
// only through getLatest/listRevisions/getBlock/getTransaction — none of which acquire a latch
|
|
638
|
+
// or touch the network (`BlockStorage.getBlock` throws `RevisionNotCoveredError` rather than
|
|
639
|
+
// restoring; the healing re-read lives one layer up in `StorageRepo.get`), so it is safe to
|
|
640
|
+
// call while holding them. Keep it that way: routing it through the healing path, which
|
|
641
|
+
// restores under the block latch, would self-deadlock here.
|
|
642
|
+
//
|
|
643
|
+
// NOTE: this compute now runs inside the critical section, so every commit and pend on these
|
|
644
|
+
// blocks queues behind it, and its cost grows with the number of revisions between T_inv and
|
|
645
|
+
// the tip (`listRevisions` + a transform apply each). Fine at invalidation's rate — a reversal
|
|
646
|
+
// is rare and its blocks are few. If invalidations ever become frequent, or reversals of very
|
|
647
|
+
// old transactions show up delaying commits, compute optimistically outside the latches and
|
|
648
|
+
// re-verify the tips inside (recomputing only the blocks that moved).
|
|
649
|
+
const computations = await Promise.all(
|
|
650
|
+
uniqueBlockIds.map(async (blockId) => {
|
|
651
|
+
const storage = ctx.createBlockStorage(blockId);
|
|
652
|
+
return {
|
|
653
|
+
blockId,
|
|
654
|
+
storage,
|
|
655
|
+
latch: latches.get(blockId)!,
|
|
656
|
+
latest: await storage.getLatest(),
|
|
657
|
+
computation: await computeRevertedBlock(storage, invalidatedRev),
|
|
658
|
+
};
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
const maxFromRev = computations.reduce((max, c) => Math.max(max, c.computation.fromRev), invalidatedRev);
|
|
662
|
+
rev = params.rev ?? maxFromRev + 1;
|
|
663
|
+
|
|
664
|
+
// All-or-nothing precheck, mirroring the monotonic guard in `BlockStorage.saveForwardRevision`
|
|
665
|
+
// (`meta.latest.rev >= rev` ⇒ refuse). With the tips read under these same latches, a computed
|
|
666
|
+
// slot is strictly greater than every tip by construction, so this can only trip when the CALLER
|
|
667
|
+
// supplied an explicit `rev` at or below a current tip. Writing nothing is the right outcome:
|
|
668
|
+
// skipping the block or retrying at a higher slot would make one member's durable entry differ
|
|
669
|
+
// from another's for the same invalidation, and step 1 dedups on (invalidatedActionId, disputeId)
|
|
670
|
+
// so an invalidation that appended nothing is simply re-deliverable.
|
|
671
|
+
const stale = computations.find(c => c.latest !== undefined && c.latest.rev >= rev);
|
|
672
|
+
if (stale) {
|
|
673
|
+
log('apply-reject-stale-revision actionId=%s disputeId=%s rev=%d blockId=%s held=%d',
|
|
674
|
+
invalidatedActionId, proof.disputeId, rev, stale.blockId, stale.latest!.rev);
|
|
675
|
+
return { applied: false, reason: 'stale-revision', reverted: [] };
|
|
601
676
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
677
|
+
|
|
678
|
+
reverted = [];
|
|
679
|
+
for (const { blockId, storage, latch, computation } of computations) {
|
|
680
|
+
// Deterministic compensating-revision actionId — identical on every member, so all converge on
|
|
681
|
+
// the same (rev, actionId) for both the restore and the tombstone path.
|
|
682
|
+
const revertActionId = await hashString(`inv:${invalidatedActionId}:${proof.disputeId}:${blockId}:${rev}`);
|
|
683
|
+
const intended: ActionRev = { rev, actionId: revertActionId };
|
|
684
|
+
let effective: ActionRev;
|
|
685
|
+
if (computation.kind === 'delete') {
|
|
686
|
+
// Block-creation reversal: physically remove the created block by writing a forward tombstone
|
|
687
|
+
// revision. The `restoredContentHash` is the DELETED_BLOCK_RESTORE sentinel — a deleted block
|
|
688
|
+
// has no content hash, and the sentinel tells dependents "observed content is gone → invalidate".
|
|
689
|
+
effective = await storage.saveDeletion(intended, latch);
|
|
690
|
+
assertWriteLanded(blockId, intended, effective);
|
|
691
|
+
log('apply-delete-restore blockId=%s invalidatedRev=%d rev=%d', blockId, invalidatedRev, rev);
|
|
692
|
+
reverted.push({ blockId, fromRev: computation.fromRev, restoredContentHash: DELETED_BLOCK_RESTORE });
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (computation.laterActions > 0) {
|
|
696
|
+
// Surviving later actions were replayed verbatim; true read-dependents are out of scope here.
|
|
697
|
+
log('apply-replayed-later-actions blockId=%s count=%d', blockId, computation.laterActions);
|
|
698
|
+
}
|
|
699
|
+
effective = await storage.saveReplica(computation.block, intended, undefined, latch);
|
|
700
|
+
assertWriteLanded(blockId, intended, effective);
|
|
701
|
+
reverted.push({ blockId, fromRev: computation.fromRev, restoredContentHash: computation.restoredContentHash });
|
|
605
702
|
}
|
|
606
|
-
|
|
607
|
-
|
|
703
|
+
|
|
704
|
+
// --- End of critical section ---
|
|
705
|
+
} finally {
|
|
706
|
+
// Every acquired latch is released even if a compute or a write threw.
|
|
707
|
+
release();
|
|
608
708
|
}
|
|
609
709
|
|
|
610
710
|
// 4. Durable, append-only invalidation entry (the source of truth for committed-invalidated).
|
|
711
|
+
// Appended OUTSIDE the block latches on purpose: `ctx.log` writes through a `BlockStore` that may
|
|
712
|
+
// itself be repo-backed, and `Latches` has no re-entrancy — appending under a block latch could
|
|
713
|
+
// self-deadlock.
|
|
611
714
|
// `cascadeRoot` is set when this is a cascade step (a reverted read-dependent), undefined for a root.
|
|
715
|
+
//
|
|
716
|
+
// NOTE: the gap between the writes landing and this append is a crash window — a crash inside it
|
|
717
|
+
// leaves compensating revisions with no entry naming them. Deliberate, and the recoverable
|
|
718
|
+
// direction of the invariant this function establishes: an entry never over-claims, it can only
|
|
719
|
+
// under-claim, and re-delivery re-applies (step 1 dedups on the entry, which is not there). The
|
|
720
|
+
// asymmetry only stops being acceptable if a consumer starts treating "revision present, entry
|
|
721
|
+
// absent" as authoritative rather than as a state to re-sync.
|
|
612
722
|
await ctx.log.addInvalidation(invalidatedActionId, invalidatedRev, proof, reverted, rev, params.cascadeRoot, params.timestamp);
|
|
613
723
|
log('apply-complete actionId=%s disputeId=%s rev=%d blocks=%d', invalidatedActionId, proof.disputeId, rev, reverted.length);
|
|
614
724
|
|
package/src/dispute/service.ts
CHANGED
|
@@ -1,125 +1,126 @@
|
|
|
1
|
-
import { pipe } from 'it-pipe';
|
|
2
|
-
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed';
|
|
3
|
-
import type { Startable, Logger, Stream, Connection, StreamHandler } from '@libp2p/interface';
|
|
4
|
-
import type { Uint8ArrayList } from 'uint8arraylist';
|
|
5
|
-
import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
6
|
-
import type { DisputeMessage } from './types.js';
|
|
7
|
-
import type { DisputeService } from './dispute-service.js';
|
|
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
|
-
|
|
38
|
-
private readonly
|
|
39
|
-
private readonly
|
|
40
|
-
private readonly
|
|
41
|
-
private readonly
|
|
42
|
-
private readonly
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// request per
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
1
|
+
import { pipe } from 'it-pipe';
|
|
2
|
+
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed';
|
|
3
|
+
import type { Startable, Logger, Stream, Connection, StreamHandler } from '@libp2p/interface';
|
|
4
|
+
import type { Uint8ArrayList } from 'uint8arraylist';
|
|
5
|
+
import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
6
|
+
import type { DisputeMessage } from './types.js';
|
|
7
|
+
import type { DisputeService } from './dispute-service.js';
|
|
8
|
+
import { registerProtocolHandler } from '../network/register-protocol-handler.js';
|
|
9
|
+
|
|
10
|
+
interface BaseComponents {
|
|
11
|
+
logger: { forComponent: (name: string) => Logger };
|
|
12
|
+
registrar: {
|
|
13
|
+
handle: (protocol: string, handler: StreamHandler, options: any) => Promise<void>;
|
|
14
|
+
unhandle: (protocol: string) => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DisputeProtocolServiceComponents extends BaseComponents {
|
|
19
|
+
disputeService: DisputeService;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DisputeProtocolServiceInit {
|
|
23
|
+
protocol?: string;
|
|
24
|
+
protocolPrefix?: string;
|
|
25
|
+
maxInboundStreams?: number;
|
|
26
|
+
maxOutboundStreams?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function disputeProtocolService(init: DisputeProtocolServiceInit = {}): (components: DisputeProtocolServiceComponents) => DisputeProtocolService {
|
|
30
|
+
return (components: DisputeProtocolServiceComponents) => new DisputeProtocolService(components, init);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Libp2p service that handles dispute protocol messages.
|
|
35
|
+
* Follows the same pattern as ClusterService.
|
|
36
|
+
*/
|
|
37
|
+
export class DisputeProtocolService implements Startable {
|
|
38
|
+
private readonly protocol: string;
|
|
39
|
+
private readonly maxInboundStreams: number;
|
|
40
|
+
private readonly maxOutboundStreams: number;
|
|
41
|
+
private readonly log: Logger;
|
|
42
|
+
private readonly disputeService: DisputeService;
|
|
43
|
+
private readonly components: DisputeProtocolServiceComponents;
|
|
44
|
+
private running: boolean;
|
|
45
|
+
|
|
46
|
+
constructor(components: DisputeProtocolServiceComponents, init: DisputeProtocolServiceInit = {}) {
|
|
47
|
+
this.components = components;
|
|
48
|
+
this.protocol = init.protocol ?? (init.protocolPrefix ?? '/db-p2p') + '/dispute/1.0.0';
|
|
49
|
+
this.maxInboundStreams = init.maxInboundStreams ?? 16;
|
|
50
|
+
this.maxOutboundStreams = init.maxOutboundStreams ?? 32;
|
|
51
|
+
this.log = components.logger.forComponent('db-p2p:dispute');
|
|
52
|
+
this.disputeService = components.disputeService;
|
|
53
|
+
this.running = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
readonly [Symbol.toStringTag] = '@libp2p/dispute';
|
|
57
|
+
|
|
58
|
+
async start(): Promise<void> {
|
|
59
|
+
if (this.running) return;
|
|
60
|
+
|
|
61
|
+
await registerProtocolHandler(this.components.registrar, this.protocol, this.handleIncomingStream.bind(this), {
|
|
62
|
+
maxInboundStreams: this.maxInboundStreams,
|
|
63
|
+
maxOutboundStreams: this.maxOutboundStreams,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.running = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async stop(): Promise<void> {
|
|
70
|
+
if (!this.running) return;
|
|
71
|
+
await this.components.registrar.unhandle(this.protocol);
|
|
72
|
+
this.running = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private handleIncomingStream(stream: Stream, connection: Connection): void {
|
|
76
|
+
const peerId = connection.remotePeer;
|
|
77
|
+
|
|
78
|
+
const processStream = async function* (this: DisputeProtocolService, source: AsyncIterable<Uint8ArrayList>) {
|
|
79
|
+
for await (const msg of source) {
|
|
80
|
+
const decoded = new TextDecoder().decode(msg.subarray());
|
|
81
|
+
const message = JSON.parse(decoded) as DisputeMessage;
|
|
82
|
+
|
|
83
|
+
let response: any;
|
|
84
|
+
switch (message.type) {
|
|
85
|
+
case 'challenge': {
|
|
86
|
+
const vote = await this.disputeService.handleChallenge(message.challenge);
|
|
87
|
+
response = { type: 'vote', vote };
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'resolution': {
|
|
91
|
+
this.disputeService.handleResolution(message.resolution);
|
|
92
|
+
response = { type: 'ack' };
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
default:
|
|
96
|
+
throw new Error(`Unknown dispute message type: ${(message as any).type}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
yield new TextEncoder().encode(JSON.stringify(response));
|
|
100
|
+
// One request per stream: every real DisputeClient sends exactly one
|
|
101
|
+
// request per dial (see ProtocolClient.processMessage), so complete the
|
|
102
|
+
// generator after the first response. A second frame a peer queued is then
|
|
103
|
+
// never read or parsed. Mirrors sync/block-transfer.
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
void (async () => {
|
|
109
|
+
try {
|
|
110
|
+
const responses = pipe(
|
|
111
|
+
stream,
|
|
112
|
+
(source) => lpDecode(source, { maxDataLength: MAX_CONTROL_MESSAGE_BYTES }),
|
|
113
|
+
processStream.bind(this),
|
|
114
|
+
(source) => lpEncode(source)
|
|
115
|
+
);
|
|
116
|
+
for await (const chunk of responses) {
|
|
117
|
+
stream.send(chunk);
|
|
118
|
+
}
|
|
119
|
+
await stream.close();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
this.log.error('error handling dispute protocol message from %p - %e', peerId, err);
|
|
122
|
+
stream.abort(err instanceof Error ? err : new Error(String(err)));
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from "./cluster/client.js";
|
|
2
|
+
export * from "./cluster/client-signature-verifier.js";
|
|
2
3
|
export * from "./cluster/cluster-repo.js";
|
|
3
4
|
export * from "./cluster/cluster-policy.js";
|
|
4
5
|
export * from "./cluster/commit-cert.js";
|
|
6
|
+
export * from "./cluster/commit-proof.js";
|
|
5
7
|
export * from "./cluster/service.js";
|
|
6
8
|
export * from "./cluster/rebalance-monitor.js";
|
|
7
9
|
export * from "./cluster/spread-on-churn.js";
|
|
@@ -12,18 +14,23 @@ export * from "./protocol-client.js";
|
|
|
12
14
|
export * from "./repo/client.js";
|
|
13
15
|
export * from "./repo/cluster-coordinator.js";
|
|
14
16
|
export * from "./repo/coordinator-repo.js";
|
|
17
|
+
export * from "./repo/served-repo-proxy.js";
|
|
15
18
|
export * from "./repo/service.js";
|
|
16
19
|
export * from "./storage/block-storage.js";
|
|
20
|
+
export * from "./storage/block-latch.js";
|
|
21
|
+
export * from "./storage/store-identity.js";
|
|
17
22
|
export * from "./storage/raw-store-driver.js";
|
|
18
23
|
export * from "./storage/kv-raw-storage.js";
|
|
19
24
|
export * from "./storage/shared-cache-pool.js";
|
|
20
25
|
export * from "./storage/cached-store-driver.js";
|
|
21
26
|
export * from "./storage/cached-raw-storage.js";
|
|
27
|
+
export * from "./storage/with-read-cache.js";
|
|
22
28
|
export * from "./storage/memory-store-driver.js";
|
|
23
29
|
export * from "./storage/memory-storage.js";
|
|
24
30
|
export * from "./storage/i-block-storage.js";
|
|
25
31
|
export * from "./storage/i-raw-storage.js";
|
|
26
32
|
export * from "./storage/struct.js";
|
|
33
|
+
export * from "./storage/block-archive.js";
|
|
27
34
|
export * from "./storage/storage-repo.js";
|
|
28
35
|
export * from "./storage/restoration-coordinator.js";
|
|
29
36
|
export * from "./storage/ring-selector.js";
|