@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,263 @@
|
|
|
1
|
+
import { canonicalBlockHash, clusterVoteVerificationPayload, computeClusterCommitHash, computeClusterMessageHash, computeClusterPromiseHash, membershipDigestFromIds } from "@optimystic/db-core";
|
|
2
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
3
|
+
import { publicKeyFromRaw } from "@libp2p/crypto/keys";
|
|
4
|
+
import { fromString as uint8ArrayFromString } from "uint8arrays/from-string";
|
|
5
|
+
/**
|
|
6
|
+
* Project a consensus-committed {@link ClusterRecord} into a {@link BlockCommitProof}. A cheap
|
|
7
|
+
* projection — no hashing, no signature work; the record's maps are carried by reference (the
|
|
8
|
+
* storage layer JSON-snapshots on save). Returns `undefined` for a v1 / unversioned record: its
|
|
9
|
+
* hashes bind no peer set, so its signer list is unbound and it can never be certified (the caller
|
|
10
|
+
* logs the skip).
|
|
11
|
+
*/
|
|
12
|
+
export function buildBlockCommitProof(record) {
|
|
13
|
+
if (record.membershipVersion !== 2 || typeof record.membershipDigest !== 'string') {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
v: 1,
|
|
18
|
+
messageHash: record.messageHash,
|
|
19
|
+
message: record.message,
|
|
20
|
+
promises: record.promises,
|
|
21
|
+
commits: record.commits,
|
|
22
|
+
membershipVersion: 2,
|
|
23
|
+
membershipDigest: record.membershipDigest,
|
|
24
|
+
peerIds: Object.keys(record.peers).sort()
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Verify that `proof` certifies the CLAIM — `claim.blockId` at `claim.rev` under `claim.actionId` —
|
|
29
|
+
* without needing the block bytes. Pure and total on hostile input: never throws; every failure is a
|
|
30
|
+
* distinguishable {@link ProofFailure}.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors the outcome discipline of `ClusterMember.verifySignature`: a malformed or unbound signer is
|
|
33
|
+
* a verification failure that must NEVER be turned into a reputation penalty, because the identity
|
|
34
|
+
* behind it was not proven. Callers deciding attributability must key off the reason values, and
|
|
35
|
+
* treat `unknown-signer` / `non-ed25519-signer` / `malformed-signature` / `malformed-proof` as
|
|
36
|
+
* non-attributable.
|
|
37
|
+
*
|
|
38
|
+
* **Why both thresholds.** The commit-round vote is cast blind — a member signs the commit whenever
|
|
39
|
+
* the approve-promises reach super-majority, regardless of its own promise vote. The promise-round
|
|
40
|
+
* approvals are the votes that carry "I validated this message, including its content digest".
|
|
41
|
+
* Requiring only the commit approvals would count signatures that attest to nothing; requiring only
|
|
42
|
+
* the promises would accept a record the cohort never actually committed. Require both.
|
|
43
|
+
*
|
|
44
|
+
* The claim step (a `{ commit }` op whose `blockIds`/`actionId`/`rev` all match) is what stops
|
|
45
|
+
* replay: a genuine proof for rev 5 presented for rev 9, or for a different block id, dies there.
|
|
46
|
+
*
|
|
47
|
+
* ## What a passing verdict does NOT prove — two caller obligations
|
|
48
|
+
*
|
|
49
|
+
* 1. **The signers are not bound to the block.** A verdict says "the cohort in `proof.peerIds`
|
|
50
|
+
* agreed", never "that is the cohort responsible for `claim.blockId`". Any attacker who controls
|
|
51
|
+
* N keys can stand up their own N-peer cohort, sign a commit for any block id at any revision,
|
|
52
|
+
* and produce a proof that verifies here. Nothing offline can close this: a block's cohort is
|
|
53
|
+
* chosen by live placement and rotates over history, so `peerIds` cannot be checked against a
|
|
54
|
+
* fixed expected set. A caller accepting proofs from untrusted peers MUST corroborate the cohort
|
|
55
|
+
* separately (overlap with the block's currently-derived cohort, or a membership anchor — see
|
|
56
|
+
* `feat-cluster-membership-threshold-cert-anchoring`). Both repair paths do this via
|
|
57
|
+
* `cluster/certified-claims.ts`, whose layer 2 logs the overlap and surfaces the unanchored
|
|
58
|
+
* residual; the anchor itself is still open.
|
|
59
|
+
* 2. **Cost is attacker-chosen.** This performs one Ed25519 verify per approve vote and hashes the
|
|
60
|
+
* whole message; nothing here caps `peerIds`, the vote maps, or the message. A caller reading a
|
|
61
|
+
* proof off the wire must bound its size and cohort count BEFORE calling.
|
|
62
|
+
*/
|
|
63
|
+
export async function verifyBlockCommitProofClaim(proof, claim, thresholds) {
|
|
64
|
+
try {
|
|
65
|
+
if (proof === null || typeof proof !== 'object') {
|
|
66
|
+
return { ok: false, reason: 'malformed-proof' };
|
|
67
|
+
}
|
|
68
|
+
// A version this verifier does not implement is never half-verified. `membershipVersion !== 2`
|
|
69
|
+
// means the signer list is unbound (v1 / unversioned history) — never certifiable.
|
|
70
|
+
if (proof.v !== 1 || proof.membershipVersion !== 2) {
|
|
71
|
+
return { ok: false, reason: 'legacy-record' };
|
|
72
|
+
}
|
|
73
|
+
if (typeof proof.messageHash !== 'string'
|
|
74
|
+
|| typeof proof.membershipDigest !== 'string'
|
|
75
|
+
|| proof.message === null || typeof proof.message !== 'object'
|
|
76
|
+
|| !Array.isArray(proof.message.operations)
|
|
77
|
+
|| !isVoteMap(proof.promises) || !isVoteMap(proof.commits)
|
|
78
|
+
|| !Array.isArray(proof.peerIds) || !proof.peerIds.every(id => typeof id === 'string')) {
|
|
79
|
+
return { ok: false, reason: 'malformed-proof' };
|
|
80
|
+
}
|
|
81
|
+
// A duplicated id in the denominator list would let one signer count twice against the
|
|
82
|
+
// threshold. (A JSON-parsed vote map cannot carry duplicate keys, so the array is the one
|
|
83
|
+
// reachable site for duplication.)
|
|
84
|
+
const peerSet = new Set(proof.peerIds);
|
|
85
|
+
if (peerSet.size !== proof.peerIds.length) {
|
|
86
|
+
return { ok: false, reason: 'duplicate-signer' };
|
|
87
|
+
}
|
|
88
|
+
// The peer list must be the one every signature covers: recompute the membership digest from
|
|
89
|
+
// it, then the message hash over (message + digest). Tampering with peerIds dies here;
|
|
90
|
+
// tampering with the message dies on the next check.
|
|
91
|
+
if (await membershipDigestFromIds(proof.peerIds) !== proof.membershipDigest) {
|
|
92
|
+
return { ok: false, reason: 'membership-mismatch' };
|
|
93
|
+
}
|
|
94
|
+
if (await computeClusterMessageHash(proof.message, proof.membershipDigest) !== proof.messageHash) {
|
|
95
|
+
return { ok: false, reason: 'message-hash-mismatch' };
|
|
96
|
+
}
|
|
97
|
+
const promiseHash = await computeClusterPromiseHash(proof.messageHash, proof.message, proof.membershipDigest);
|
|
98
|
+
const commitHash = await computeClusterCommitHash(proof.messageHash, proof.message, proof.promises, proof.membershipDigest);
|
|
99
|
+
const promiseRound = await countApprovals(proof.promises, promiseHash, peerSet);
|
|
100
|
+
if ('reason' in promiseRound) {
|
|
101
|
+
return { ok: false, reason: promiseRound.reason };
|
|
102
|
+
}
|
|
103
|
+
const commitRound = await countApprovals(proof.commits, commitHash, peerSet);
|
|
104
|
+
if ('reason' in commitRound) {
|
|
105
|
+
return { ok: false, reason: commitRound.reason };
|
|
106
|
+
}
|
|
107
|
+
// Threshold gates. When a threshold fails AND that round skipped a signer outside `peerIds`,
|
|
108
|
+
// report `unknown-signer` rather than the bare threshold reason — the skipped signature is the
|
|
109
|
+
// record of why the count came up short.
|
|
110
|
+
const superMajority = Math.ceil(thresholds.superMajorityThreshold * proof.peerIds.length);
|
|
111
|
+
if (promiseRound.approves < superMajority) {
|
|
112
|
+
return { ok: false, reason: promiseRound.sawUnknownSigner ? 'unknown-signer' : 'promise-threshold' };
|
|
113
|
+
}
|
|
114
|
+
if (!(commitRound.approves > proof.peerIds.length * thresholds.simpleMajorityThreshold)) {
|
|
115
|
+
return { ok: false, reason: commitRound.sawUnknownSigner ? 'unknown-signer' : 'commit-threshold' };
|
|
116
|
+
}
|
|
117
|
+
const op = findClaimedCommitOp(proof.message, claim);
|
|
118
|
+
if (!op) {
|
|
119
|
+
return { ok: false, reason: 'claim-not-in-message' };
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, declaredDigest: op.blockDigests?.[claim.blockId]?.digest };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return { ok: false, reason: 'malformed-proof' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Claim verification ({@link verifyBlockCommitProofClaim}) PLUS: the digest the commit op declared
|
|
129
|
+
* for `claim.blockId` must be present (`no-digest-declared`) and equal `canonicalBlockHash(block)`
|
|
130
|
+
* (`digest-mismatch`). Total like the claim half; a block shape that `canonicalBlockHash` cannot
|
|
131
|
+
* digest reads as `digest-mismatch` — the received bytes provably are not the declared content.
|
|
132
|
+
*/
|
|
133
|
+
export async function verifyBlockCommitProofContent(proof, claim, block, thresholds) {
|
|
134
|
+
const verdict = await verifyBlockCommitProofClaim(proof, claim, thresholds);
|
|
135
|
+
if (!verdict.ok) {
|
|
136
|
+
return verdict;
|
|
137
|
+
}
|
|
138
|
+
if (typeof verdict.declaredDigest !== 'string') {
|
|
139
|
+
return { ok: false, reason: 'no-digest-declared' };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
if (await canonicalBlockHash(block) !== verdict.declaredDigest) {
|
|
143
|
+
return { ok: false, reason: 'digest-mismatch' };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { ok: false, reason: 'digest-mismatch' };
|
|
148
|
+
}
|
|
149
|
+
return verdict;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The digest `proof.message`'s commit operation declares for the claimed block, or `undefined`
|
|
153
|
+
* when the claim resolves no commit op or the op carries no digest for that block id. This is the
|
|
154
|
+
* SAME op resolution {@link verifyBlockCommitProofClaim}'s claim step uses, so the digest the
|
|
155
|
+
* storage layer's retention rule compares against (persist only when the local materialization
|
|
156
|
+
* matches) can never drift from the digest a later verifier extracts.
|
|
157
|
+
*/
|
|
158
|
+
export function proofDeclaredDigest(proof, claim) {
|
|
159
|
+
return findClaimedCommitOp(proof.message, claim)?.blockDigests?.[claim.blockId]?.digest;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Does `proof`'s message actually carry a commit operation for this exact
|
|
163
|
+
* `(blockId, rev, actionId)`? The claim step of {@link verifyBlockCommitProofClaim} with NO
|
|
164
|
+
* cryptography — the same {@link findClaimedCommitOp} resolution, so the cheap structural check and
|
|
165
|
+
* the full verification can never disagree about which op a claim names.
|
|
166
|
+
*
|
|
167
|
+
* This is the pairing guard a SERVER uses: a proof is looked up by revision, so attaching one whose
|
|
168
|
+
* message names a different revision (or block, or action) would be publishing a mis-paired
|
|
169
|
+
* artifact. It says nothing about signatures or thresholds — a receiver must still verify. Total on
|
|
170
|
+
* hostile input like its verifying sibling: never throws, `false` on any malformed shape.
|
|
171
|
+
*/
|
|
172
|
+
export function proofClaimsCommit(proof, claim) {
|
|
173
|
+
try {
|
|
174
|
+
return findClaimedCommitOp(proof.message, claim) !== undefined;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** A plain-object (non-array) map of votes — the shape `promises` / `commits` must have. */
|
|
181
|
+
function isVoteMap(value) {
|
|
182
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Count distinct `approve` votes over `hash` from signers inside `peerSet`, verifying each signature
|
|
186
|
+
* against the Ed25519 key its peer id names. Per-entry rules (the spec's step 5/6):
|
|
187
|
+
*
|
|
188
|
+
* - a non-`approve` vote is ignored silently (rejects/conflicts are part of the signed record but
|
|
189
|
+
* never count toward approval);
|
|
190
|
+
* - a signer outside `peerSet` is skipped and noted (`sawUnknownSigner`) — the caller downgrades a
|
|
191
|
+
* failed threshold to `unknown-signer`;
|
|
192
|
+
* - a signer inside `peerSet` whose id is not a valid Ed25519 peer id → immediate
|
|
193
|
+
* `non-ed25519-signer` (its key cannot be recovered, so the proof as a whole is unverifiable);
|
|
194
|
+
* - a signature that is not a string, fails base64url decode, or fails cryptographic verify →
|
|
195
|
+
* immediate `malformed-signature`;
|
|
196
|
+
* - each signer id is counted at most once.
|
|
197
|
+
*/
|
|
198
|
+
async function countApprovals(votes, hash, peerSet) {
|
|
199
|
+
let approves = 0;
|
|
200
|
+
let sawUnknownSigner = false;
|
|
201
|
+
const counted = new Set();
|
|
202
|
+
for (const [signerId, vote] of Object.entries(votes)) {
|
|
203
|
+
if (vote === null || typeof vote !== 'object' || vote.type !== 'approve') {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!peerSet.has(signerId)) {
|
|
207
|
+
sawUnknownSigner = true;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (counted.has(signerId)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
let rawKey;
|
|
214
|
+
try {
|
|
215
|
+
const peerId = peerIdFromString(signerId);
|
|
216
|
+
if (peerId.type !== 'Ed25519' || peerId.publicKey === undefined) {
|
|
217
|
+
return { reason: 'non-ed25519-signer' };
|
|
218
|
+
}
|
|
219
|
+
rawKey = peerId.publicKey.raw;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// An id that does not even parse as a peer id certainly names no Ed25519 key.
|
|
223
|
+
return { reason: 'non-ed25519-signer' };
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if (typeof vote.signature !== 'string') {
|
|
227
|
+
return { reason: 'malformed-signature' };
|
|
228
|
+
}
|
|
229
|
+
const sigBytes = uint8ArrayFromString(vote.signature, 'base64url');
|
|
230
|
+
const payload = clusterVoteVerificationPayload(hash, vote);
|
|
231
|
+
if (!await publicKeyFromRaw(rawKey).verify(payload, sigBytes)) {
|
|
232
|
+
return { reason: 'malformed-signature' };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return { reason: 'malformed-signature' };
|
|
237
|
+
}
|
|
238
|
+
counted.add(signerId);
|
|
239
|
+
approves++;
|
|
240
|
+
}
|
|
241
|
+
return { approves, sawUnknownSigner };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The `{ commit }` operation the claim points at: `blockIds` contains the claimed block and the
|
|
245
|
+
* op's `actionId` / `rev` equal the claim's. Absent → `claim-not-in-message` (the replay stop).
|
|
246
|
+
*/
|
|
247
|
+
function findClaimedCommitOp(message, claim) {
|
|
248
|
+
for (const operation of message.operations) {
|
|
249
|
+
if (operation === null || typeof operation !== 'object' || !('commit' in operation)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const commit = operation.commit;
|
|
253
|
+
if (commit !== null && typeof commit === 'object'
|
|
254
|
+
&& Array.isArray(commit.blockIds)
|
|
255
|
+
&& commit.blockIds.includes(claim.blockId)
|
|
256
|
+
&& commit.actionId === claim.actionId
|
|
257
|
+
&& commit.rev === claim.rev) {
|
|
258
|
+
return commit;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=commit-proof.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit-proof.js","sourceRoot":"","sources":["../../../src/cluster/commit-proof.ts"],"names":[],"mappings":"AACA,OAAO,EACN,kBAAkB,EAAE,8BAA8B,EAAE,wBAAwB,EAC5E,yBAAyB,EAAE,yBAAyB,EAAE,uBAAuB,EAC7E,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AA+D7E;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAqB;IAC1D,IAAI,MAAM,CAAC,iBAAiB,KAAK,CAAC,IAAI,OAAO,MAAM,CAAC,gBAAgB,KAAK,QAAQ,EAAE,CAAC;QACnF,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO;QACN,CAAC,EAAE,CAAC;QACJ,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,iBAAiB,EAAE,CAAC;QACpB,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;QACzC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE;KACzC,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAChD,KAAuB,EAAE,KAAiB,EAAE,UAA2B;IAEvE,IAAI,CAAC;QACJ,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACjD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;QACjD,CAAC;QACD,+FAA+F;QAC/F,mFAAmF;QACnF,IAAI,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,iBAAiB,KAAK,CAAC,EAAE,CAAC;YACpD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;QAC/C,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ;eACrC,OAAO,KAAK,CAAC,gBAAgB,KAAK,QAAQ;eAC1C,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;eAC3D,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC;eACxC,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC;eACvD,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,EAAE,CAAC;YACzF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;QACjD,CAAC;QAED,uFAAuF;QACvF,0FAA0F;QAC1F,mCAAmC;QACnC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACvC,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC3C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QAClD,CAAC;QAED,6FAA6F;QAC7F,uFAAuF;QACvF,qDAAqD;QACrD,IAAI,MAAM,uBAAuB,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC,gBAAgB,EAAE,CAAC;YAC7E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;QACrD,CAAC;QACD,IAAI,MAAM,yBAAyB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,gBAAgB,CAAC,KAAK,KAAK,CAAC,WAAW,EAAE,CAAC;YAClG,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;QACvD,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,yBAAyB,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAC9G,MAAM,UAAU,GAAG,MAAM,wBAAwB,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAE5H,MAAM,YAAY,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;QAChF,IAAI,QAAQ,IAAI,YAAY,EAAE,CAAC;YAC9B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,MAAM,EAAE,CAAC;QACnD,CAAC;QACD,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;QAC7E,IAAI,QAAQ,IAAI,WAAW,EAAE,CAAC;YAC7B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC;QAClD,CAAC;QAED,6FAA6F;QAC7F,+FAA+F;QAC/F,yCAAyC;QACzC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,sBAAsB,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC1F,IAAI,YAAY,CAAC,QAAQ,GAAG,aAAa,EAAE,CAAC;YAC3C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,mBAAmB,EAAE,CAAC;QACtG,CAAC;QACD,IAAI,CAAC,CAAC,WAAW,CAAC,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,UAAU,CAAC,uBAAuB,CAAC,EAAE,CAAC;YACzF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAAC;QACpG,CAAC;QAED,MAAM,EAAE,GAAG,mBAAmB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACrD,IAAI,CAAC,EAAE,EAAE,CAAC;YACT,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;QACtD,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACjD,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAClD,KAAuB,EAAE,KAAiB,EAAE,KAAa,EAAE,UAA2B;IAEtF,MAAM,OAAO,GAAG,MAAM,2BAA2B,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;IAC5E,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,OAAO,CAAC;IAChB,CAAC;IACD,IAAI,OAAO,OAAO,CAAC,cAAc,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IACpD,CAAC;IACD,IAAI,CAAC;QACJ,IAAI,MAAM,kBAAkB,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC;YAChE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;QACjD,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAuB,EAAE,KAAiB;IAC7E,OAAO,mBAAmB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;AACzF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAuB,EAAE,KAAiB;IAC3E,IAAI,CAAC;QACJ,OAAO,mBAAmB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,SAAS,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC;AAED,4FAA4F;AAC5F,SAAS,SAAS,CAAC,KAAc;IAChC,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,cAAc,CAC5B,KAAgC,EAChC,IAAY,EACZ,OAA4B;IAE5B,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACtD,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC1E,SAAS;QACV,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,gBAAgB,GAAG,IAAI,CAAC;YACxB,SAAS;QACV,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3B,SAAS;QACV,CAAC;QACD,IAAI,MAAkB,CAAC;QACvB,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAC1C,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;gBACjE,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;YACzC,CAAC;YACD,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACR,8EAA8E;YAC9E,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;QACzC,CAAC;QACD,IAAI,CAAC;YACJ,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;gBACxC,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;YAC1C,CAAC;YACD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;YACnE,MAAM,OAAO,GAAG,8BAA8B,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC3D,IAAI,CAAC,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;gBAC/D,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;YAC1C,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;QAC1C,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtB,QAAQ,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;AACvC,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,OAAoB,EAAE,KAAiB;IACnE,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QAC5C,IAAI,SAAS,KAAK,IAAI,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,IAAI,SAAS,CAAC,EAAE,CAAC;YACrF,SAAS;QACV,CAAC;QACD,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC;QAChC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;eAC7C,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;eAC9B,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;eACvC,MAAM,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ;eAClC,MAAM,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC;QACf,CAAC;IACF,CAAC;IACD,OAAO,SAAS,CAAC;AAClB,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IBlock } from "@optimystic/db-core";
|
|
2
|
+
import type { BlockCommitProof } from "./commit-proof.js";
|
|
2
3
|
/**
|
|
3
4
|
* Quorum-corroboration helpers shared by the two block-restoration paths:
|
|
4
5
|
* - read-repair (`CoordinatorRepo.queryClusterForLatest`)
|
|
@@ -9,9 +10,13 @@ import type { IBlock } from "@optimystic/db-core";
|
|
|
9
10
|
* "max wins" with "highest value corroborated by a quorum of distinct peers".
|
|
10
11
|
*
|
|
11
12
|
* NOTE: the quorum here is corroboration-of-a-claim, NOT Sybil-resistant cohort
|
|
12
|
-
* membership. A peer minting fresh keypairs can still cast a vote
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* membership. A peer minting fresh keypairs can still cast a vote. Selection now
|
|
14
|
+
* additionally weighs *certified* claims — those whose cohort commit proof a
|
|
15
|
+
* caller has already verified (`cluster/certified-claims.ts`) and marked via the
|
|
16
|
+
* injected `certified` flag — so a lone honest holder with a valid proof is
|
|
17
|
+
* sufficient where uncertified claims still need distinct-peer corroboration.
|
|
18
|
+
* Verification itself never happens here: both selectors stay pure and
|
|
19
|
+
* synchronous; verdicts arrive as booleans.
|
|
15
20
|
*/
|
|
16
21
|
/** A single peer's self-reported (rev, actionId) for a block. */
|
|
17
22
|
export interface RevClaim {
|
|
@@ -19,13 +24,34 @@ export interface RevClaim {
|
|
|
19
24
|
peerId: string;
|
|
20
25
|
rev: number;
|
|
21
26
|
actionId: string;
|
|
27
|
+
/**
|
|
28
|
+
* The cohort commit proof the claiming peer attached, when it had one. Selection never reads
|
|
29
|
+
* this field: presence proves nothing on its own — the peer chose what to attach. A caller
|
|
30
|
+
* that verifies it (`certifyClaim` / `certifyContent` in `cluster/certified-claims.ts`, built
|
|
31
|
+
* on `verifyBlockCommitProofClaim`) records the verdict in {@link certified}, which is what
|
|
32
|
+
* {@link selectQuorumRev} weighs.
|
|
33
|
+
*/
|
|
34
|
+
proof?: BlockCommitProof;
|
|
35
|
+
/**
|
|
36
|
+
* Injected verdict: the caller verified this claim's cohort commit proof and it certifies this
|
|
37
|
+
* exact `(rev, actionId)`. A certified claim carries the cohort's signature set as its
|
|
38
|
+
* corroboration, so {@link selectQuorumRev} can select it without a second peer vouching.
|
|
39
|
+
* Never set this from the mere presence of {@link proof}.
|
|
40
|
+
*/
|
|
41
|
+
certified?: boolean;
|
|
22
42
|
}
|
|
23
43
|
/** The (rev, actionId) pair a quorum agreed on, plus the peers that corroborated it. */
|
|
24
44
|
export interface QuorumRev {
|
|
25
45
|
rev: number;
|
|
26
46
|
actionId: string;
|
|
27
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Distinct peer-ids that voted for this exact (rev, actionId). When {@link certified} is set,
|
|
49
|
+
* these are the certified claimants at that pair instead — possibly a single peer, whose
|
|
50
|
+
* corroboration is the proof's signature set rather than other voters.
|
|
51
|
+
*/
|
|
28
52
|
supporters: string[];
|
|
53
|
+
/** Set when the certified path selected this pair, so callers can log which rule won. */
|
|
54
|
+
certified?: true;
|
|
29
55
|
}
|
|
30
56
|
/**
|
|
31
57
|
* Votes a claim needs when the cohort is big enough to supply them: a claim must be
|
|
@@ -91,15 +117,55 @@ export declare function corroboratorCapacity(cohortPeerCount: number, repairCorr
|
|
|
91
117
|
*
|
|
92
118
|
* Returns `undefined` when nothing is corroborated — an uncorroborated claim
|
|
93
119
|
* must never drive restoration.
|
|
120
|
+
*
|
|
121
|
+
* **Certified claims** (`certified === true`, injected by a caller that verified the claim's
|
|
122
|
+
* cohort commit proof — see `cluster/certified-claims.ts`) short-circuit the distinct-peer rule,
|
|
123
|
+
* because the proof's signature set IS the corroboration:
|
|
124
|
+
*
|
|
125
|
+
* - No certified claims → today's corroboration result, unchanged.
|
|
126
|
+
* - A corroborated pair at a HIGHER rev than every certified claim wins — corroboration stays a
|
|
127
|
+
* legitimate weaker path, so a legacy uncertified tail written after the last proven rev
|
|
128
|
+
* remains readable.
|
|
129
|
+
* - Otherwise the highest certified rev wins: this covers a certified rev beaten in raw rev only
|
|
130
|
+
* by an *uncorroborated* claim (which failed quorum and is no evidence), and the equal-rev
|
|
131
|
+
* tie, where the proof outweighs votes.
|
|
132
|
+
* - Except: two distinct `actionId`s certified at that top rev is equivocation — the cohort
|
|
133
|
+
* provably signed two different actions into one revision — and the whole selection declines
|
|
134
|
+
* (`undefined`). Callers distinguish this decline from a plain no-quorum via
|
|
135
|
+
* {@link certifiedEquivocation}.
|
|
94
136
|
*/
|
|
95
137
|
export declare function selectQuorumRev(claims: RevClaim[], simpleMajorityThreshold: number, corroboratorCapacity?: number): QuorumRev | undefined;
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
138
|
+
/**
|
|
139
|
+
* The conflicting certified set at the top certified rev, when there is one: two-plus distinct
|
|
140
|
+
* `actionId`s each carrying a verified cohort commit proof for the SAME revision. It deserves a
|
|
141
|
+
* distinct log line from a plain no-quorum — the cohort (or whoever holds its keys) provably
|
|
142
|
+
* signed both sides. Selection stays pure, so callers do the logging with what this reports.
|
|
143
|
+
*
|
|
144
|
+
* `undefined` when no certified claim exists or the top certified rev names a single action —
|
|
145
|
+
* conflicts at LOWER certified revs are history already superseded, not equivocation worth
|
|
146
|
+
* declining over.
|
|
147
|
+
*
|
|
148
|
+
* **Ask this only on a decline.** A non-`undefined` result does NOT imply {@link selectQuorumRev}
|
|
149
|
+
* returned `undefined`: a corroborated pair STRICTLY above the top certified rev still wins, and
|
|
150
|
+
* the equivocation below it is reported here while selection succeeded. Call it when selection
|
|
151
|
+
* declined, to say WHY it declined.
|
|
152
|
+
*/
|
|
153
|
+
export declare function certifiedEquivocation(claims: RevClaim[]): {
|
|
154
|
+
rev: number;
|
|
155
|
+
actionIds: string[];
|
|
156
|
+
} | undefined;
|
|
157
|
+
/** One block candidate paired with its serving peer and canonical hash
|
|
158
|
+
* (`canonicalBlockHash` from `@optimystic/db-core`). */
|
|
99
159
|
export interface BlockHashCandidate {
|
|
100
160
|
peerId: string;
|
|
101
161
|
hash: string;
|
|
102
162
|
block: IBlock;
|
|
163
|
+
/**
|
|
164
|
+
* Injected verdict: the caller verified a cohort commit proof binding this candidate's CONTENT
|
|
165
|
+
* (`certifyContent` in `cluster/certified-claims.ts` — the declared digest matched these
|
|
166
|
+
* bytes), so the cohort's signatures stand in for other peers serving the same hash.
|
|
167
|
+
*/
|
|
168
|
+
certified?: boolean;
|
|
103
169
|
}
|
|
104
170
|
/**
|
|
105
171
|
* Among candidates already known to corroborate the target `(rev, actionId)`,
|
|
@@ -113,16 +179,37 @@ export interface BlockHashCandidate {
|
|
|
113
179
|
* Omit it to keep the floor at two.
|
|
114
180
|
*
|
|
115
181
|
* **What relaxing this does and does not cost.** Block ids are random 256-bit strings, not
|
|
116
|
-
* content-addressed, so
|
|
182
|
+
* content-addressed, so `canonicalBlockHash` (db-core) is a cross-peer *agreement* hash and never a
|
|
117
183
|
* check against the requested id — nothing here re-derives an id from received bytes. At capacity
|
|
118
184
|
* one the sole peer's content is therefore taken on its word. That extends no trust the cohort had
|
|
119
185
|
* not already extended: the same peer's `(rev, actionId)` claim is equally uncorroborable at that
|
|
120
186
|
* size, and a two-member cohort has no honest majority to appeal to. Pass a capacity a shrunken
|
|
121
187
|
* view of the network cannot talk down (see {@link corroboratorCapacity}), so only a cohort that is
|
|
122
188
|
* *genuinely* that small reaches this branch.
|
|
189
|
+
*
|
|
190
|
+
* **Certified candidates** (`certified === true` — a caller verified a cohort commit proof whose
|
|
191
|
+
* declared digest matches these exact bytes) short-circuit the hash quorum: exactly one distinct
|
|
192
|
+
* certified hash → that block wins outright, however many peers served it. Two-plus distinct
|
|
193
|
+
* certified hashes is certified content equivocation → decline (`undefined`), mirroring the
|
|
194
|
+
* existing unique-hash-tie decline; callers name that decline via
|
|
195
|
+
* {@link certifiedContentEquivocation}. No certified candidate → the hash quorum below, unchanged.
|
|
123
196
|
*/
|
|
124
197
|
export declare function selectQuorumBlock(candidates: BlockHashCandidate[], simpleMajorityThreshold: number, corroboratorCapacity?: number): {
|
|
125
198
|
block: IBlock;
|
|
126
199
|
hash: string;
|
|
127
200
|
} | undefined;
|
|
201
|
+
/**
|
|
202
|
+
* The conflicting certified hashes when there are two or more — the content-side sibling of
|
|
203
|
+
* {@link certifiedEquivocation}, and the reason {@link selectQuorumBlock} declines outright.
|
|
204
|
+
* Candidates reaching that selector all carry the SAME `(rev, actionId)`, so two certified hashes
|
|
205
|
+
* mean the cohort's keys signed two different digests into one revision: a provable compromise an
|
|
206
|
+
* operator must be able to tell apart from the routine "not enough carriers agreed" decline. Both
|
|
207
|
+
* declines return `undefined`, so without this they log identically.
|
|
208
|
+
*
|
|
209
|
+
* `undefined` when fewer than two distinct certified hashes exist — including the ordinary
|
|
210
|
+
* no-certified-candidate case, where a decline really is a plain content-quorum shortfall.
|
|
211
|
+
*/
|
|
212
|
+
export declare function certifiedContentEquivocation(candidates: BlockHashCandidate[]): {
|
|
213
|
+
hashes: string[];
|
|
214
|
+
} | undefined;
|
|
128
215
|
//# sourceMappingURL=quorum-restore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quorum-restore.d.ts","sourceRoot":"","sources":["../../../src/cluster/quorum-restore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"quorum-restore.d.ts","sourceRoot":"","sources":["../../../src/cluster/quorum-restore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;;;;;;;GAiBG;AAEH,iEAAiE;AACjE,MAAM,WAAW,QAAQ;IACxB,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wFAAwF;AACxF,MAAM,WAAW,SAAS;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,yFAAyF;IACzF,SAAS,CAAC,EAAE,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CACzB,cAAc,EAAE,MAAM,EACtB,uBAAuB,EAAE,MAAM,EAC/B,oBAAoB,GAAE,MAAiC,GACrD,MAAM,CAGR;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,EAAE,8BAA8B,EAAE,MAAM,GAAG,MAAM,CAE5G;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,QAAQ,EAAE,EAClB,uBAAuB,EAAE,MAAM,EAC/B,oBAAoB,CAAC,EAAE,MAAM,GAC3B,SAAS,GAAG,SAAS,CA+CvB;AAsBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAI1G;AAED;wDACwD;AACxD,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,iBAAiB,CAChC,UAAU,EAAE,kBAAkB,EAAE,EAChC,uBAAuB,EAAE,MAAM,EAC/B,oBAAoB,CAAC,EAAE,MAAM,GAC3B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CA+B7C;AAWD;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,kBAAkB,EAAE,GAAG;IAAE,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAG/G"}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { sha256 } from "multiformats/hashes/sha2";
|
|
2
|
-
import { toString as uint8ArrayToString } from 'uint8arrays';
|
|
3
1
|
/**
|
|
4
2
|
* Votes a claim needs when the cohort is big enough to supply them: a claim must be
|
|
5
3
|
* seconded by a second, independent peer. See {@link quorumSize} for the cap that
|
|
@@ -69,6 +67,22 @@ export function corroboratorCapacity(cohortPeerCount, repairCorroborationCluster
|
|
|
69
67
|
*
|
|
70
68
|
* Returns `undefined` when nothing is corroborated — an uncorroborated claim
|
|
71
69
|
* must never drive restoration.
|
|
70
|
+
*
|
|
71
|
+
* **Certified claims** (`certified === true`, injected by a caller that verified the claim's
|
|
72
|
+
* cohort commit proof — see `cluster/certified-claims.ts`) short-circuit the distinct-peer rule,
|
|
73
|
+
* because the proof's signature set IS the corroboration:
|
|
74
|
+
*
|
|
75
|
+
* - No certified claims → today's corroboration result, unchanged.
|
|
76
|
+
* - A corroborated pair at a HIGHER rev than every certified claim wins — corroboration stays a
|
|
77
|
+
* legitimate weaker path, so a legacy uncertified tail written after the last proven rev
|
|
78
|
+
* remains readable.
|
|
79
|
+
* - Otherwise the highest certified rev wins: this covers a certified rev beaten in raw rev only
|
|
80
|
+
* by an *uncorroborated* claim (which failed quorum and is no evidence), and the equal-rev
|
|
81
|
+
* tie, where the proof outweighs votes.
|
|
82
|
+
* - Except: two distinct `actionId`s certified at that top rev is equivocation — the cohort
|
|
83
|
+
* provably signed two different actions into one revision — and the whole selection declines
|
|
84
|
+
* (`undefined`). Callers distinguish this decline from a plain no-quorum via
|
|
85
|
+
* {@link certifiedEquivocation}.
|
|
72
86
|
*/
|
|
73
87
|
export function selectQuorumRev(claims, simpleMajorityThreshold, corroboratorCapacity) {
|
|
74
88
|
if (claims.length === 0)
|
|
@@ -99,21 +113,67 @@ export function selectQuorumRev(claims, simpleMajorityThreshold, corroboratorCap
|
|
|
99
113
|
best = g;
|
|
100
114
|
}
|
|
101
115
|
}
|
|
102
|
-
|
|
116
|
+
const corroborated = best
|
|
103
117
|
? { rev: best.rev, actionId: best.actionId, supporters: [...best.supporters] }
|
|
104
118
|
: undefined;
|
|
119
|
+
const certified = certifiedGroups(claims);
|
|
120
|
+
if (!certified)
|
|
121
|
+
return corroborated;
|
|
122
|
+
// A corroborated pair strictly above every certified rev wins — a legacy uncertified tail
|
|
123
|
+
// must stay readable. A merely UNcorroborated higher rev never reaches here (it is not in
|
|
124
|
+
// `corroborated`), so it cannot outrank a proof.
|
|
125
|
+
if (corroborated && corroborated.rev > certified.rev)
|
|
126
|
+
return corroborated;
|
|
127
|
+
// Two actions provably signed into the same top revision: decline the whole selection rather
|
|
128
|
+
// than pick a side. Callers log via certifiedEquivocation.
|
|
129
|
+
if (certified.byAction.size !== 1)
|
|
130
|
+
return undefined;
|
|
131
|
+
const [entry] = certified.byAction;
|
|
132
|
+
const [actionId, supporters] = entry; // size === 1 checked above
|
|
133
|
+
return { rev: certified.rev, actionId, supporters: [...supporters], certified: true };
|
|
105
134
|
}
|
|
106
|
-
/**
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
/** Certified claims at the top certified rev, keyed by actionId → distinct certified claimants. */
|
|
136
|
+
function certifiedGroups(claims) {
|
|
137
|
+
let top;
|
|
138
|
+
for (const c of claims) {
|
|
139
|
+
if (c.certified === true && (top === undefined || c.rev > top))
|
|
140
|
+
top = c.rev;
|
|
141
|
+
}
|
|
142
|
+
if (top === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
const byAction = new Map();
|
|
145
|
+
for (const c of claims) {
|
|
146
|
+
if (c.certified !== true || c.rev !== top)
|
|
147
|
+
continue;
|
|
148
|
+
let s = byAction.get(c.actionId);
|
|
149
|
+
if (!s) {
|
|
150
|
+
s = new Set();
|
|
151
|
+
byAction.set(c.actionId, s);
|
|
152
|
+
}
|
|
153
|
+
s.add(c.peerId);
|
|
154
|
+
}
|
|
155
|
+
return { rev: top, byAction };
|
|
111
156
|
}
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
157
|
+
/**
|
|
158
|
+
* The conflicting certified set at the top certified rev, when there is one: two-plus distinct
|
|
159
|
+
* `actionId`s each carrying a verified cohort commit proof for the SAME revision. It deserves a
|
|
160
|
+
* distinct log line from a plain no-quorum — the cohort (or whoever holds its keys) provably
|
|
161
|
+
* signed both sides. Selection stays pure, so callers do the logging with what this reports.
|
|
162
|
+
*
|
|
163
|
+
* `undefined` when no certified claim exists or the top certified rev names a single action —
|
|
164
|
+
* conflicts at LOWER certified revs are history already superseded, not equivocation worth
|
|
165
|
+
* declining over.
|
|
166
|
+
*
|
|
167
|
+
* **Ask this only on a decline.** A non-`undefined` result does NOT imply {@link selectQuorumRev}
|
|
168
|
+
* returned `undefined`: a corroborated pair STRICTLY above the top certified rev still wins, and
|
|
169
|
+
* the equivocation below it is reported here while selection succeeded. Call it when selection
|
|
170
|
+
* declined, to say WHY it declined.
|
|
171
|
+
*/
|
|
172
|
+
export function certifiedEquivocation(claims) {
|
|
173
|
+
const groups = certifiedGroups(claims);
|
|
174
|
+
if (!groups || groups.byAction.size < 2)
|
|
175
|
+
return undefined;
|
|
176
|
+
return { rev: groups.rev, actionIds: [...groups.byAction.keys()] };
|
|
117
177
|
}
|
|
118
178
|
/**
|
|
119
179
|
* Among candidates already known to corroborate the target `(rev, actionId)`,
|
|
@@ -127,17 +187,32 @@ export async function canonicalBlockHash(block) {
|
|
|
127
187
|
* Omit it to keep the floor at two.
|
|
128
188
|
*
|
|
129
189
|
* **What relaxing this does and does not cost.** Block ids are random 256-bit strings, not
|
|
130
|
-
* content-addressed, so
|
|
190
|
+
* content-addressed, so `canonicalBlockHash` (db-core) is a cross-peer *agreement* hash and never a
|
|
131
191
|
* check against the requested id — nothing here re-derives an id from received bytes. At capacity
|
|
132
192
|
* one the sole peer's content is therefore taken on its word. That extends no trust the cohort had
|
|
133
193
|
* not already extended: the same peer's `(rev, actionId)` claim is equally uncorroborable at that
|
|
134
194
|
* size, and a two-member cohort has no honest majority to appeal to. Pass a capacity a shrunken
|
|
135
195
|
* view of the network cannot talk down (see {@link corroboratorCapacity}), so only a cohort that is
|
|
136
196
|
* *genuinely* that small reaches this branch.
|
|
197
|
+
*
|
|
198
|
+
* **Certified candidates** (`certified === true` — a caller verified a cohort commit proof whose
|
|
199
|
+
* declared digest matches these exact bytes) short-circuit the hash quorum: exactly one distinct
|
|
200
|
+
* certified hash → that block wins outright, however many peers served it. Two-plus distinct
|
|
201
|
+
* certified hashes is certified content equivocation → decline (`undefined`), mirroring the
|
|
202
|
+
* existing unique-hash-tie decline; callers name that decline via
|
|
203
|
+
* {@link certifiedContentEquivocation}. No certified candidate → the hash quorum below, unchanged.
|
|
137
204
|
*/
|
|
138
205
|
export function selectQuorumBlock(candidates, simpleMajorityThreshold, corroboratorCapacity) {
|
|
139
206
|
if (candidates.length === 0)
|
|
140
207
|
return undefined;
|
|
208
|
+
const certifiedByHash = certifiedHashes(candidates);
|
|
209
|
+
if (certifiedByHash.size === 1) {
|
|
210
|
+
const [entry] = certifiedByHash;
|
|
211
|
+
const [hash, block] = entry;
|
|
212
|
+
return { block, hash };
|
|
213
|
+
}
|
|
214
|
+
if (certifiedByHash.size > 1)
|
|
215
|
+
return undefined; // certified content equivocation — decline
|
|
141
216
|
// One vote per distinct peer per hash group, matching selectQuorumRev — a peer appearing twice
|
|
142
217
|
// must not be able to second itself into a content quorum.
|
|
143
218
|
const groups = new Map();
|
|
@@ -159,4 +234,28 @@ export function selectQuorumBlock(candidates, simpleMajorityThreshold, corrobora
|
|
|
159
234
|
const [hash, group] = meeting[0];
|
|
160
235
|
return { block: group.block, hash };
|
|
161
236
|
}
|
|
237
|
+
/** Distinct hashes carried by certified candidates → the first block instance serving each. */
|
|
238
|
+
function certifiedHashes(candidates) {
|
|
239
|
+
const byHash = new Map();
|
|
240
|
+
for (const c of candidates) {
|
|
241
|
+
if (c.certified === true && !byHash.has(c.hash))
|
|
242
|
+
byHash.set(c.hash, c.block);
|
|
243
|
+
}
|
|
244
|
+
return byHash;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The conflicting certified hashes when there are two or more — the content-side sibling of
|
|
248
|
+
* {@link certifiedEquivocation}, and the reason {@link selectQuorumBlock} declines outright.
|
|
249
|
+
* Candidates reaching that selector all carry the SAME `(rev, actionId)`, so two certified hashes
|
|
250
|
+
* mean the cohort's keys signed two different digests into one revision: a provable compromise an
|
|
251
|
+
* operator must be able to tell apart from the routine "not enough carriers agreed" decline. Both
|
|
252
|
+
* declines return `undefined`, so without this they log identically.
|
|
253
|
+
*
|
|
254
|
+
* `undefined` when fewer than two distinct certified hashes exist — including the ordinary
|
|
255
|
+
* no-certified-candidate case, where a decline really is a plain content-quorum shortfall.
|
|
256
|
+
*/
|
|
257
|
+
export function certifiedContentEquivocation(candidates) {
|
|
258
|
+
const byHash = certifiedHashes(candidates);
|
|
259
|
+
return byHash.size < 2 ? undefined : { hashes: [...byHash.keys()] };
|
|
260
|
+
}
|
|
162
261
|
//# sourceMappingURL=quorum-restore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quorum-restore.js","sourceRoot":"","sources":["../../../src/cluster/quorum-restore.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"quorum-restore.js","sourceRoot":"","sources":["../../../src/cluster/quorum-restore.ts"],"names":[],"mappings":"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"}
|