@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,319 @@
|
|
|
1
|
+
import type { ActionId, BlockId, IBlock } from "@optimystic/db-core";
|
|
2
|
+
import {
|
|
3
|
+
verifyBlockCommitProofClaim, verifyBlockCommitProofContent,
|
|
4
|
+
type BlockCommitProof, type ProofClaim, type ProofFailure, type ProofThresholds
|
|
5
|
+
} from "./commit-proof.js";
|
|
6
|
+
import { createLogger } from "../logger.js";
|
|
7
|
+
|
|
8
|
+
const log = createLogger('certified-claims');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Certification of repair claims by cohort commit proof — the shared layer both block-restoration
|
|
12
|
+
* paths (read-repair in `CoordinatorRepo`, reconcile in `reconcile-block`) run a peer-attached
|
|
13
|
+
* {@link BlockCommitProof} through before marking a `RevClaim` / `BlockHashCandidate` as
|
|
14
|
+
* `certified` for the selection helpers in `quorum-restore.ts`.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors the layered posture of `verifyInvalidationCertificate` (`dispute/invalidation.ts`):
|
|
17
|
+
*
|
|
18
|
+
* - **Layer 1 (always)**: offline cryptographic verification — thresholds of Ed25519 votes from
|
|
19
|
+
* the proof's own signer list (`verifyBlockCommitProofClaim` / `...Content`), preceded by the
|
|
20
|
+
* {@link MAX_PROOF_SIGNERS} cap this module owes the verifier (its caller obligation #2:
|
|
21
|
+
* verification cost is attacker-chosen, so bound the cohort BEFORE any hashing).
|
|
22
|
+
* - **Layer 2 (optional, observational)**: {@link ProofAnchoring.recomputeBlockCohort} re-derives
|
|
23
|
+
* the block's cohort from the caller's own topology view and the overlap with the proof's
|
|
24
|
+
* signers is LOGGED — never gated on. Historic cohort rotation makes zero overlap legitimate
|
|
25
|
+
* for old data; gating would re-create the very lone-holder-unreadable defect the certified
|
|
26
|
+
* path exists to fix. Like invalidation's `recomputeArbitratorSet`, no production caller wires
|
|
27
|
+
* this yet (see the note at the `clusterMember` construction in `libp2p-node-base.ts`).
|
|
28
|
+
* - **Degradation**: with no recompute capability, or an infeasible recompute, an accepted proof
|
|
29
|
+
* is certified anyway but never silently — logged, and surfaced via
|
|
30
|
+
* {@link ProofAnchoring.onUnanchored} so callers can count/report the residual, exactly as
|
|
31
|
+
* invalidation's `acceptUnanchored` does.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Verdict from a {@link RecomputeBlockCohort}:
|
|
36
|
+
* - `{ feasible: false }` — the caller could not re-derive the block's current cohort (no routing
|
|
37
|
+
* view, lookup failed); anchoring degrades to accept-and-surface.
|
|
38
|
+
* - `{ feasible: true, cohortPeerIds }` — the currently-derived cohort, compared (log-only)
|
|
39
|
+
* against the proof's signer list.
|
|
40
|
+
*/
|
|
41
|
+
export type CohortRecomputeVerdict =
|
|
42
|
+
| { readonly feasible: false }
|
|
43
|
+
| { readonly feasible: true; readonly cohortPeerIds: string[] };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Injected layer-2 capability: re-derive the block's responsible cohort from the caller's own
|
|
47
|
+
* topology view. A real implementation would come from `IKeyNetwork.findCluster` — the same source
|
|
48
|
+
* `deriveExpectedCluster` uses in `libp2p-node-base.ts`. NOT wired in production yet; the overlap
|
|
49
|
+
* it enables is observational (logged), never a gate.
|
|
50
|
+
*/
|
|
51
|
+
export type RecomputeBlockCohort = (blockId: BlockId) => Promise<CohortRecomputeVerdict>;
|
|
52
|
+
|
|
53
|
+
/** Reported when a proof is accepted on layer-1 cryptography alone — see {@link ProofAnchoring.onUnanchored}. */
|
|
54
|
+
export type UnanchoredProofAcceptance = {
|
|
55
|
+
readonly blockId: BlockId;
|
|
56
|
+
readonly rev: number;
|
|
57
|
+
readonly actionId: ActionId;
|
|
58
|
+
/** Size of the proof's signer list (`proof.peerIds.length`). */
|
|
59
|
+
readonly signerCount: number;
|
|
60
|
+
/** Why the anchoring layer did not run to a comparison. */
|
|
61
|
+
readonly reason: 'no-recompute-capability' | 'recompute-infeasible';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Optional anchoring capabilities a caller threads into {@link certifyClaim} / {@link certifyContent}. */
|
|
65
|
+
export type ProofAnchoring = {
|
|
66
|
+
/** Layer-2 recompute; omitted → layer-1-only certification with the documented surfacing. */
|
|
67
|
+
readonly recomputeBlockCohort?: RecomputeBlockCohort;
|
|
68
|
+
/**
|
|
69
|
+
* Invoked when a proof is accepted without a cohort comparison (capability absent or
|
|
70
|
+
* infeasible). Lets a caller surface "certified a claim it could not anchor to topology"
|
|
71
|
+
* alongside the internal log. Exceptions thrown from it are swallowed (logged), never allowed
|
|
72
|
+
* to un-certify an already-verified proof.
|
|
73
|
+
*/
|
|
74
|
+
readonly onUnanchored?: (info: UnanchoredProofAcceptance) => void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Everything {@link ProofFailure} names, plus the pre-verification cap decline this module adds. */
|
|
78
|
+
export type CertifyFailure = ProofFailure | 'oversized-cohort';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hard bound on `proof.peerIds.length`, enforced BEFORE any hashing or signature work. Nothing on
|
|
82
|
+
* the repair wire bounds a proof's cohort count — only byte caps apply (1 MiB control / 8 MiB sync
|
|
83
|
+
* response), which would still admit ~80k signature verifications — so this module enforces the
|
|
84
|
+
* bound `commit-proof.ts` declares as its caller obligation #2. 256 is far above any plausible
|
|
85
|
+
* cohort (deployments run ~10) while keeping the worst-case verification cost trivial.
|
|
86
|
+
*
|
|
87
|
+
* NOTE: this cap bounds the SIGNATURE count (at most `MAX_PROOF_SIGNERS` Ed25519 verifies per
|
|
88
|
+
* round, since `countApprovals` skips signers outside `peerIds` before any crypto). It does NOT
|
|
89
|
+
* bound the proof's serialized SIZE: `computeClusterMessageHash` / `...PromiseHash` /
|
|
90
|
+
* `...CommitHash` canonically serialize `message`, `promises` and `commits`, whose entry counts a
|
|
91
|
+
* peer chooses freely inside the transport byte cap. That cost is linear and today bounded by the
|
|
92
|
+
* 1 MiB control-message cap; if repair verification ever shows up in a profile, or a deployment
|
|
93
|
+
* raises those transport caps, add a serialized-size bound here alongside the signer count.
|
|
94
|
+
*/
|
|
95
|
+
export const MAX_PROOF_SIGNERS = 256;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Every {@link CertifyFailure}, classified: may it be held against the peer that SERVED the proof?
|
|
99
|
+
*
|
|
100
|
+
* Exhaustive by type — `Record<CertifyFailure, boolean>` means adding a {@link ProofFailure}
|
|
101
|
+
* variant in `commit-proof.ts` fails the build here until it is classified, rather than silently
|
|
102
|
+
* defaulting to `true` (penalize), which is the wrong direction for an unknown reason.
|
|
103
|
+
*
|
|
104
|
+
* `false` — never a penalty, because the identity behind the artifact was not proven (mirrors
|
|
105
|
+
* `ClusterMember.verifySignature`'s outcome discipline, restated in `commit-proof.ts`) or because
|
|
106
|
+
* the outcome is not misbehavior at all.
|
|
107
|
+
*
|
|
108
|
+
* `true` — the peer's own artifact provably lies, or provably does not cover what it was served
|
|
109
|
+
* for.
|
|
110
|
+
*/
|
|
111
|
+
const ATTRIBUTABLE_PROOF_FAILURES: Readonly<Record<CertifyFailure, boolean>> = {
|
|
112
|
+
// Identity not proven — an unparseable or unbound signer, or structural garbage that could have
|
|
113
|
+
// been authored by anyone in the chain.
|
|
114
|
+
'unknown-signer': false,
|
|
115
|
+
'non-ed25519-signer': false,
|
|
116
|
+
'malformed-signature': false,
|
|
117
|
+
'malformed-proof': false,
|
|
118
|
+
// A v1 / unversioned record is history, not misbehavior: it binds no peer set and never could.
|
|
119
|
+
'legacy-record': false,
|
|
120
|
+
// A genuine mega-cohort is conceivable and the cap declines it unexamined — no evidence either way.
|
|
121
|
+
'oversized-cohort': false,
|
|
122
|
+
// Not a failure of the proof at all: the cohort declared no digest for this block, so there was
|
|
123
|
+
// nothing to compare the served bytes against. A verdict ("content uncertified, rev certified"),
|
|
124
|
+
// never misbehavior.
|
|
125
|
+
'no-digest-declared': false,
|
|
126
|
+
// The artifact contradicts itself or the claim it was served for. `buildBlockCommitProof` derives
|
|
127
|
+
// `peerIds` from `Object.keys(record.peers)`, so honest construction cannot produce a duplicate —
|
|
128
|
+
// serving one implies authorship, as does a digest or hash that does not recompute.
|
|
129
|
+
'membership-mismatch': true,
|
|
130
|
+
'message-hash-mismatch': true,
|
|
131
|
+
'duplicate-signer': true,
|
|
132
|
+
'promise-threshold': true,
|
|
133
|
+
'commit-threshold': true,
|
|
134
|
+
// The replay case: a genuine proof presented for a claim it does not cover.
|
|
135
|
+
'claim-not-in-message': true,
|
|
136
|
+
// The peer served bytes that provably are not the committed content.
|
|
137
|
+
'digest-mismatch': true
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Failure reasons that must NEVER become a reputation penalty for the serving peer — the `false`
|
|
142
|
+
* half of {@link ATTRIBUTABLE_PROOF_FAILURES}, derived so the set and
|
|
143
|
+
* {@link isAttributableProofFailure} cannot drift apart. Prefer the predicate; this set is exported
|
|
144
|
+
* for callers that want to name the whole population (logs, tests).
|
|
145
|
+
*/
|
|
146
|
+
export const NON_ATTRIBUTABLE_PROOF_FAILURES: ReadonlySet<CertifyFailure> = new Set(
|
|
147
|
+
(Object.keys(ATTRIBUTABLE_PROOF_FAILURES) as CertifyFailure[])
|
|
148
|
+
.filter(failure => !ATTRIBUTABLE_PROOF_FAILURES[failure])
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* May this failure be held against the peer that served the proof? One predicate so the two repair
|
|
153
|
+
* paths cannot drift on the classification — see {@link ATTRIBUTABLE_PROOF_FAILURES} for the
|
|
154
|
+
* per-reason rationale.
|
|
155
|
+
*/
|
|
156
|
+
export function isAttributableProofFailure(failure: CertifyFailure): boolean {
|
|
157
|
+
return ATTRIBUTABLE_PROOF_FAILURES[failure] === true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The {@link ProofThresholds} both repair paths verify against, from the cohort's configured
|
|
162
|
+
* `superMajorityThreshold`.
|
|
163
|
+
*
|
|
164
|
+
* `simpleMajorityThreshold` is hardcoded 0.5 — NOT the deployment's configured value (0.51 by
|
|
165
|
+
* default): cohort members enforce `count > total / 2` (`ClusterMember.hasMajority`), and
|
|
166
|
+
* `ProofThresholds` requires a verifier to mirror what the members actually enforced. Verifying
|
|
167
|
+
* against the configured value would reject proofs real cohorts produce.
|
|
168
|
+
*
|
|
169
|
+
* One function so the read path (`CoordinatorRepo.queryClusterForLatest`) and the commit-path
|
|
170
|
+
* reconcile (`cluster/reconcile-block.ts`) cannot drift on the one number that has to match
|
|
171
|
+
* `hasMajority`.
|
|
172
|
+
*/
|
|
173
|
+
export function proofThresholds(superMajorityThreshold: number): ProofThresholds {
|
|
174
|
+
return { superMajorityThreshold, simpleMajorityThreshold: 0.5 };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Certification verdict for a claim — a discriminated union, mirroring `ProofVerdict`
|
|
179
|
+
* (`commit-proof.ts`) so "certified" and "why not" cannot disagree: a failure reason is REQUIRED
|
|
180
|
+
* when uncertified and unrepresentable when certified.
|
|
181
|
+
*/
|
|
182
|
+
export type ClaimCertification =
|
|
183
|
+
/** The proof certifies `claim` — cap passed, thresholds of valid cohort signatures met. */
|
|
184
|
+
| { certified: true }
|
|
185
|
+
/** Classify the reason via {@link isAttributableProofFailure} before penalizing anyone. */
|
|
186
|
+
| { certified: false; failure: CertifyFailure };
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Certification verdict for a claim AND the block bytes served for it. Three reachable outcomes,
|
|
190
|
+
* spelled as three arms so an impossible mix (content certified while the rev is not, a failure
|
|
191
|
+
* alongside full success) cannot be constructed:
|
|
192
|
+
*/
|
|
193
|
+
export type ContentCertification =
|
|
194
|
+
/** Both halves passed: the proof certifies `(blockId, rev, actionId)` AND these exact bytes. */
|
|
195
|
+
| { revCertified: true; contentCertified: true }
|
|
196
|
+
/**
|
|
197
|
+
* The claim half passed, the content half did not: `digest-mismatch` (attributable — drop AND
|
|
198
|
+
* penalize the served bytes) or `no-digest-declared` (drop only; the cohort declared nothing to
|
|
199
|
+
* compare against). The `(rev, actionId)` is certified either way.
|
|
200
|
+
*/
|
|
201
|
+
| { revCertified: true; contentCertified: false; failure: 'digest-mismatch' | 'no-digest-declared' }
|
|
202
|
+
/** The claim half failed, so nothing is certified. Classify via {@link isAttributableProofFailure}. */
|
|
203
|
+
| { revCertified: false; contentCertified: false; failure: CertifyFailure };
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Certify a repair claim by its attached cohort commit proof: cap the cohort, verify offline
|
|
207
|
+
* ({@link verifyBlockCommitProofClaim}), then run the anchoring layer on success. Total on hostile
|
|
208
|
+
* input like the verifier it wraps — never throws.
|
|
209
|
+
*/
|
|
210
|
+
export async function certifyClaim(
|
|
211
|
+
proof: BlockCommitProof, claim: ProofClaim, thresholds: ProofThresholds, anchoring: ProofAnchoring = {}
|
|
212
|
+
): Promise<ClaimCertification> {
|
|
213
|
+
if (exceedsSignerCap(proof)) {
|
|
214
|
+
return { certified: false, failure: 'oversized-cohort' };
|
|
215
|
+
}
|
|
216
|
+
const verdict = await verifyBlockCommitProofClaim(proof, claim, thresholds);
|
|
217
|
+
if (!verdict.ok) {
|
|
218
|
+
return { certified: false, failure: verdict.reason };
|
|
219
|
+
}
|
|
220
|
+
await anchorAcceptedProof(proof, claim, anchoring);
|
|
221
|
+
return { certified: true };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Certify a claim AND the served block bytes ({@link verifyBlockCommitProofContent}). The two
|
|
226
|
+
* halves are reported separately because the content checks (`digest-mismatch` /
|
|
227
|
+
* `no-digest-declared`) only run after the claim half has already PASSED — a proof that certifies
|
|
228
|
+
* the revision while the bytes disagree still certifies the revision; the caller drops (and, for
|
|
229
|
+
* `digest-mismatch`, penalizes) the bytes. Anchoring runs whenever the claim half passed: that is
|
|
230
|
+
* the moment a proof was accepted as evidence.
|
|
231
|
+
*/
|
|
232
|
+
export async function certifyContent(
|
|
233
|
+
proof: BlockCommitProof, claim: ProofClaim, block: IBlock, thresholds: ProofThresholds,
|
|
234
|
+
anchoring: ProofAnchoring = {}
|
|
235
|
+
): Promise<ContentCertification> {
|
|
236
|
+
if (exceedsSignerCap(proof)) {
|
|
237
|
+
return { revCertified: false, contentCertified: false, failure: 'oversized-cohort' };
|
|
238
|
+
}
|
|
239
|
+
const verdict = await verifyBlockCommitProofContent(proof, claim, block, thresholds);
|
|
240
|
+
if (verdict.ok) {
|
|
241
|
+
await anchorAcceptedProof(proof, claim, anchoring);
|
|
242
|
+
return { revCertified: true, contentCertified: true };
|
|
243
|
+
}
|
|
244
|
+
if (verdict.reason === 'digest-mismatch' || verdict.reason === 'no-digest-declared') {
|
|
245
|
+
await anchorAcceptedProof(proof, claim, anchoring);
|
|
246
|
+
return { revCertified: true, contentCertified: false, failure: verdict.reason };
|
|
247
|
+
}
|
|
248
|
+
return { revCertified: false, contentCertified: false, failure: verdict.reason };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The cap check, structurally safe on hostile shapes: a `peerIds` that is not an array — or a
|
|
253
|
+
* property access that throws, which is why the whole read is guarded — falls through to the
|
|
254
|
+
* verifier, which reports `malformed-proof` without doing signature work either. This runs before
|
|
255
|
+
* {@link certifyClaim}'s and {@link certifyContent}'s only other call, so it is the one place their
|
|
256
|
+
* "never throws" contract could be broken.
|
|
257
|
+
*/
|
|
258
|
+
function exceedsSignerCap(proof: BlockCommitProof): boolean {
|
|
259
|
+
try {
|
|
260
|
+
return proof !== null && typeof proof === 'object'
|
|
261
|
+
&& Array.isArray(proof.peerIds) && proof.peerIds.length > MAX_PROOF_SIGNERS;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Layer 2 + degradation for a proof the offline layer accepted. Purely observational — nothing
|
|
269
|
+
* here can revoke the certification: overlap is logged (never gated — historic cohort rotation
|
|
270
|
+
* makes zero overlap legitimate for old data, and gating would re-create the lone-holder-unreadable
|
|
271
|
+
* defect), infeasibility degrades to accept-and-surface, and a throwing capability is logged and
|
|
272
|
+
* treated as infeasible.
|
|
273
|
+
*/
|
|
274
|
+
async function anchorAcceptedProof(proof: BlockCommitProof, claim: ProofClaim, anchoring: ProofAnchoring): Promise<void> {
|
|
275
|
+
const signerCount = proof.peerIds.length;
|
|
276
|
+
if (!anchoring.recomputeBlockCohort) {
|
|
277
|
+
acceptUnanchored(claim, signerCount, 'no-recompute-capability', anchoring);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
let verdict: CohortRecomputeVerdict;
|
|
281
|
+
try {
|
|
282
|
+
verdict = await anchoring.recomputeBlockCohort(claim.blockId);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
log('anchor-recompute-error block=%s error=%o', claim.blockId, err);
|
|
285
|
+
acceptUnanchored(claim, signerCount, 'recompute-infeasible', anchoring);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// A verdict that is null or otherwise off-contract reads as infeasible rather than throwing —
|
|
289
|
+
// `recomputeBlockCohort` is caller-supplied, so its return shape is as untrusted as its behavior.
|
|
290
|
+
if (!verdict?.feasible) {
|
|
291
|
+
acceptUnanchored(claim, signerCount, 'recompute-infeasible', anchoring);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const cohort = new Set(verdict.cohortPeerIds);
|
|
296
|
+
const overlap = proof.peerIds.filter(id => cohort.has(id)).length;
|
|
297
|
+
log('anchor-overlap block=%s rev=%d action=%s overlap=%d signers=%d cohort=%d',
|
|
298
|
+
claim.blockId, claim.rev, claim.actionId, overlap, signerCount, cohort.size);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
// A non-iterable `cohortPeerIds`. The comparison is observational, so losing it costs a log
|
|
301
|
+
// line, never the certification.
|
|
302
|
+
log('anchor-overlap-error block=%s error=%o', claim.blockId, err);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Accept-and-surface a proof certified on layer 1 alone — never silent, never throwing. */
|
|
307
|
+
function acceptUnanchored(
|
|
308
|
+
claim: ProofClaim, signerCount: number, reason: UnanchoredProofAcceptance['reason'], anchoring: ProofAnchoring
|
|
309
|
+
): void {
|
|
310
|
+
log('accept-unanchored block=%s rev=%d action=%s signers=%d reason=%s',
|
|
311
|
+
claim.blockId, claim.rev, claim.actionId, signerCount, reason);
|
|
312
|
+
try {
|
|
313
|
+
anchoring.onUnanchored?.({
|
|
314
|
+
blockId: claim.blockId, rev: claim.rev, actionId: claim.actionId, signerCount, reason
|
|
315
|
+
});
|
|
316
|
+
} catch (err) {
|
|
317
|
+
log('accept-unanchored-callback-error block=%s error=%o', claim.blockId, err);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The p2p-backed {@link ClientSignatureVerifier}: the closure a cluster member's
|
|
3
|
+
* {@link TransactionValidator} uses to decide whether a client transaction was really signed by the
|
|
4
|
+
* identity its stamp claims.
|
|
5
|
+
*
|
|
6
|
+
* Lives here — one exported factory rather than a closure inlined at each wiring site — because two
|
|
7
|
+
* consumers must agree byte-for-byte or the tests stop proving anything about production: the
|
|
8
|
+
* Quereus plugin's `createQuereusValidator` (the real deployment path once a composition root
|
|
9
|
+
* supplies `NodeOptions.validator`) and `db-p2p`'s own signature specs, whose whole claim is that
|
|
10
|
+
* they drive "the exact closure production binds".
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ClientSignatureVerifier } from '@optimystic/db-core';
|
|
14
|
+
import { b64urlToBytes } from '@optimystic/db-core';
|
|
15
|
+
import { verifyPeerSig } from '../cohort-topic/peer-sig.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Derive the signer's Ed25519 public key from the peer-id string embedded in `stamp.peerId` and
|
|
19
|
+
* verify the base64url signature over the canonical payload.
|
|
20
|
+
*
|
|
21
|
+
* TOTAL on adversarial input, which the {@link ClientSignatureVerifier} port requires: a throw here
|
|
22
|
+
* escapes `ClusterMember.validatePendOperations` instead of becoming a signed reject vote.
|
|
23
|
+
* `verifyPeerSig` already returns `false` rather than throwing on a non-Ed25519 or malformed
|
|
24
|
+
* peer-id; the try/catch adds the same guarantee for the base64url decode.
|
|
25
|
+
*/
|
|
26
|
+
export function createPeerClientSignatureVerifier(): ClientSignatureVerifier {
|
|
27
|
+
return (peerId: string, payload: Uint8Array, signature: string): boolean => {
|
|
28
|
+
try {
|
|
29
|
+
return verifyPeerSig(peerId, payload, b64urlToBytes(signature));
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|