@forestrie/receipt-verify 0.6.2 → 0.7.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/dist/checkpoint-chain.d.ts +60 -0
- package/dist/checkpoint-chain.d.ts.map +1 -0
- package/dist/checkpoint-chain.js +207 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +3 -3
- package/src/checkpoint-chain.ts +274 -0
- package/src/index.ts +9 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Draft-bryce consistency proof embedded in a v3 checkpoint. */
|
|
2
|
+
export type CheckpointConsistencyProof = {
|
|
3
|
+
treeSize1: bigint;
|
|
4
|
+
treeSize2: bigint;
|
|
5
|
+
/** One inclusion path per tree-size-1 peak, proven at tree-size-2. */
|
|
6
|
+
paths: Uint8Array[][];
|
|
7
|
+
/** New peaks not covered by the proven roots (draft `right-peaks`). */
|
|
8
|
+
rightPeaks: Uint8Array[];
|
|
9
|
+
};
|
|
10
|
+
/** Decode the embedded consistency proof (`vdp` 396 key -2). */
|
|
11
|
+
export declare function checkpointConsistencyProof(checkpointBytes: Uint8Array): CheckpointConsistencyProof;
|
|
12
|
+
/**
|
|
13
|
+
* One fold step: from the trusted accumulator at `proof.treeSize1`,
|
|
14
|
+
* produce the `treeSize2` accumulator (draft: `consistent_roots` output
|
|
15
|
+
* plus the supplied right-peaks). Structural sanity: the result must have
|
|
16
|
+
* exactly the peak count of a size-`treeSize2` MMR.
|
|
17
|
+
*/
|
|
18
|
+
export declare function computeCheckpointAccumulator(proof: CheckpointConsistencyProof, accumulatorFrom: Uint8Array[]): Promise<Uint8Array[]>;
|
|
19
|
+
/** Detached payload the checkpoint signature covers (ADR-0046): the raw
|
|
20
|
+
* concatenation of the accumulator peaks in contract order. */
|
|
21
|
+
export declare function accumulatorPayload(accumulator: Uint8Array[]): Uint8Array;
|
|
22
|
+
export type CheckpointChainLink = {
|
|
23
|
+
treeSize1: bigint;
|
|
24
|
+
treeSize2: bigint;
|
|
25
|
+
accumulator: Uint8Array[];
|
|
26
|
+
signatureOk: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type CheckpointChainResult = {
|
|
29
|
+
ok: true;
|
|
30
|
+
links: CheckpointChainLink[];
|
|
31
|
+
accumulator: Uint8Array[];
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: "empty_chain" | "legacy_chain_break" | "signature" | "proof_malformed";
|
|
35
|
+
/** Index of the offending checkpoint. */
|
|
36
|
+
at: number;
|
|
37
|
+
detail: string;
|
|
38
|
+
links: CheckpointChainLink[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Verify a retained checkpoint chain (ascending massif order) and fold out
|
|
42
|
+
* the final authenticated accumulator.
|
|
43
|
+
*
|
|
44
|
+
* - The first link must be boundary-based from 0 (a whole-log chain), or
|
|
45
|
+
* the caller supplies `accumulatorFrom` matching its base (a suffix
|
|
46
|
+
* chain rooted in an already-trusted accumulator).
|
|
47
|
+
* - Every subsequent link's base must equal the previous link's sealed
|
|
48
|
+
* size — a mismatch is the legacy (pre-FOR-410) drift signature and is
|
|
49
|
+
* permanent for that log (`legacy_chain_break`).
|
|
50
|
+
* - Each link's signature is checked over its computed accumulator via
|
|
51
|
+
* the injected verifier (the caller owns trust resolution — genesis
|
|
52
|
+
* roots, caller-known keys, or the label-1000 delegation path).
|
|
53
|
+
*/
|
|
54
|
+
export declare function verifyCheckpointChain(opts: {
|
|
55
|
+
checkpoints: Uint8Array[];
|
|
56
|
+
verifySignature: (checkpointBytes: Uint8Array, detachedPayload: Uint8Array) => Promise<boolean>;
|
|
57
|
+
/** Trusted base accumulator for a suffix chain (absent: base must be 0). */
|
|
58
|
+
accumulatorFrom?: Uint8Array[];
|
|
59
|
+
}): Promise<CheckpointChainResult>;
|
|
60
|
+
//# sourceMappingURL=checkpoint-chain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint-chain.d.ts","sourceRoot":"","sources":["../src/checkpoint-chain.ts"],"names":[],"mappings":"AA6BA,iEAAiE;AACjE,MAAM,MAAM,0BAA0B,GAAG;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;IACtB,uEAAuE;IACvE,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B,CAAC;AAeF,gEAAgE;AAChE,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,UAAU,GAC1B,0BAA0B,CAiC5B;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,0BAA0B,EACjC,eAAe,EAAE,UAAU,EAAE,GAC5B,OAAO,CAAC,UAAU,EAAE,CAAC,CAmBvB;AAED;+DAC+D;AAC/D,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,UAAU,CAQxE;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,GACrE;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EACF,aAAa,GACb,oBAAoB,GACpB,WAAW,GACX,iBAAiB,CAAC;IACtB,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B,CAAC;AAEN;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,eAAe,EAAE,CACf,eAAe,EAAE,UAAU,EAC3B,eAAe,EAAE,UAAU,KACxB,OAAO,CAAC,OAAO,CAAC,CAAC;IACtB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,UAAU,EAAE,CAAC;CAChC,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAiGjC"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retained-checkpoint chain verification (FOR-368 Phase 3, plan-2607-29).
|
|
3
|
+
*
|
|
4
|
+
* Post-FOR-410 (ADR-0056) every checkpoint's embedded consistency proof
|
|
5
|
+
* spans its massif's ENTRY BOUNDARY to its seal, so the store's retained
|
|
6
|
+
* `.sth` objects form a contiguous chain `0 → S₁ → S₂ → …`. Folding the
|
|
7
|
+
* chain per the draft ("Chained proofs" / `consistent_roots`) reconstructs
|
|
8
|
+
* each link's tree-size-2 accumulator — which is exactly the detached
|
|
9
|
+
* payload its signature covers (ADR-0046: concat of the accumulator in
|
|
10
|
+
* descending height order). The fold therefore yields, with NO tile access
|
|
11
|
+
* and NO RPC: an authenticated accumulator at every retained seal, and the
|
|
12
|
+
* final state to check a receipt's recomputed peak against.
|
|
13
|
+
*
|
|
14
|
+
* This rung depends only on the public log store — the complement of the
|
|
15
|
+
* `CheckpointPublished` event scan (public chain data only); see the
|
|
16
|
+
* recorded both-paths decision in plan-2607-29.
|
|
17
|
+
*
|
|
18
|
+
* Legacy (pre-FOR-410) chains surface as a contiguity break
|
|
19
|
+
* (`legacy_chain_break`): a permanent per-log condition — fall back to the
|
|
20
|
+
* event scan, tile extension, or a holder cache.
|
|
21
|
+
*/
|
|
22
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
23
|
+
import { consistentRoots, peakMMRIndexes } from "@forestrie/merklelog";
|
|
24
|
+
import { SubtleHasher } from "./subtle-hasher.js";
|
|
25
|
+
import { parseCheckpoint } from "./build-receipt-offline.js";
|
|
26
|
+
const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
|
|
27
|
+
const VDP_CONSISTENCY_PROOF_KEY = -2;
|
|
28
|
+
function asBigint(v, what) {
|
|
29
|
+
if (typeof v === "bigint")
|
|
30
|
+
return v;
|
|
31
|
+
if (typeof v === "number" && Number.isSafeInteger(v))
|
|
32
|
+
return BigInt(v);
|
|
33
|
+
throw new Error(`${what}: expected an unsigned integer`);
|
|
34
|
+
}
|
|
35
|
+
function asBytesArray(v, what) {
|
|
36
|
+
if (!Array.isArray(v) || v.some((e) => !(e instanceof Uint8Array))) {
|
|
37
|
+
throw new Error(`${what}: expected an array of byte strings`);
|
|
38
|
+
}
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
/** Decode the embedded consistency proof (`vdp` 396 key -2). */
|
|
42
|
+
export function checkpointConsistencyProof(checkpointBytes) {
|
|
43
|
+
const { unprotected } = parseCheckpoint(checkpointBytes);
|
|
44
|
+
const vdpRaw = unprotected.get(VDS_COSE_RECEIPT_PROOFS_TAG);
|
|
45
|
+
if (!(vdpRaw instanceof Map)) {
|
|
46
|
+
throw new Error("checkpoint carries no verifiable-proofs header (396)");
|
|
47
|
+
}
|
|
48
|
+
const proofBstr = vdpRaw.get(VDP_CONSISTENCY_PROOF_KEY);
|
|
49
|
+
if (!(proofBstr instanceof Uint8Array)) {
|
|
50
|
+
throw new Error("checkpoint carries no consistency proof (vdp key -2)");
|
|
51
|
+
}
|
|
52
|
+
const proof = decodeCborDeterministic(proofBstr);
|
|
53
|
+
if (!Array.isArray(proof) || proof.length < 4) {
|
|
54
|
+
throw new Error("consistency proof must be [tree-size-1, tree-size-2, paths, right-peaks]");
|
|
55
|
+
}
|
|
56
|
+
const pathsRaw = proof[2];
|
|
57
|
+
if (!Array.isArray(pathsRaw) ||
|
|
58
|
+
pathsRaw.some((p) => !Array.isArray(p) || p.some((n) => !(n instanceof Uint8Array)))) {
|
|
59
|
+
throw new Error("consistency paths must be arrays of byte strings");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
treeSize1: asBigint(proof[0], "tree-size-1"),
|
|
63
|
+
treeSize2: asBigint(proof[1], "tree-size-2"),
|
|
64
|
+
paths: pathsRaw,
|
|
65
|
+
rightPeaks: asBytesArray(proof[3], "right-peaks"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One fold step: from the trusted accumulator at `proof.treeSize1`,
|
|
70
|
+
* produce the `treeSize2` accumulator (draft: `consistent_roots` output
|
|
71
|
+
* plus the supplied right-peaks). Structural sanity: the result must have
|
|
72
|
+
* exactly the peak count of a size-`treeSize2` MMR.
|
|
73
|
+
*/
|
|
74
|
+
export async function computeCheckpointAccumulator(proof, accumulatorFrom) {
|
|
75
|
+
const hasher = new SubtleHasher();
|
|
76
|
+
const proven = proof.treeSize1 === 0n
|
|
77
|
+
? []
|
|
78
|
+
: await consistentRoots(hasher, proof.treeSize1 - 1n, accumulatorFrom, proof.paths);
|
|
79
|
+
const accumulator = [...proven, ...proof.rightPeaks];
|
|
80
|
+
const expected = peakMMRIndexes(proof.treeSize2 - 1n).length;
|
|
81
|
+
if (accumulator.length !== expected) {
|
|
82
|
+
throw new Error(`computed accumulator has ${accumulator.length} peaks; size ${proof.treeSize2} requires ${expected}`);
|
|
83
|
+
}
|
|
84
|
+
return accumulator;
|
|
85
|
+
}
|
|
86
|
+
/** Detached payload the checkpoint signature covers (ADR-0046): the raw
|
|
87
|
+
* concatenation of the accumulator peaks in contract order. */
|
|
88
|
+
export function accumulatorPayload(accumulator) {
|
|
89
|
+
const out = new Uint8Array(accumulator.reduce((s, p) => s + p.length, 0));
|
|
90
|
+
let offset = 0;
|
|
91
|
+
for (const p of accumulator) {
|
|
92
|
+
out.set(p, offset);
|
|
93
|
+
offset += p.length;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Verify a retained checkpoint chain (ascending massif order) and fold out
|
|
99
|
+
* the final authenticated accumulator.
|
|
100
|
+
*
|
|
101
|
+
* - The first link must be boundary-based from 0 (a whole-log chain), or
|
|
102
|
+
* the caller supplies `accumulatorFrom` matching its base (a suffix
|
|
103
|
+
* chain rooted in an already-trusted accumulator).
|
|
104
|
+
* - Every subsequent link's base must equal the previous link's sealed
|
|
105
|
+
* size — a mismatch is the legacy (pre-FOR-410) drift signature and is
|
|
106
|
+
* permanent for that log (`legacy_chain_break`).
|
|
107
|
+
* - Each link's signature is checked over its computed accumulator via
|
|
108
|
+
* the injected verifier (the caller owns trust resolution — genesis
|
|
109
|
+
* roots, caller-known keys, or the label-1000 delegation path).
|
|
110
|
+
*/
|
|
111
|
+
export async function verifyCheckpointChain(opts) {
|
|
112
|
+
const links = [];
|
|
113
|
+
if (opts.checkpoints.length === 0) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
reason: "empty_chain",
|
|
117
|
+
at: 0,
|
|
118
|
+
detail: "no checkpoints supplied",
|
|
119
|
+
links,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
let accumulator = opts.accumulatorFrom ?? [];
|
|
123
|
+
let expectedBase = null;
|
|
124
|
+
for (let i = 0; i < opts.checkpoints.length; i++) {
|
|
125
|
+
const bytes = opts.checkpoints[i];
|
|
126
|
+
let proof;
|
|
127
|
+
try {
|
|
128
|
+
proof = checkpointConsistencyProof(bytes);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
reason: "proof_malformed",
|
|
134
|
+
at: i,
|
|
135
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
136
|
+
links,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (expectedBase === null) {
|
|
140
|
+
// First link: base 0 for a whole-log chain, else the caller's
|
|
141
|
+
// trusted accumulator must be FOR this base (peak-count check).
|
|
142
|
+
if (proof.treeSize1 !== 0n) {
|
|
143
|
+
const wanted = peakMMRIndexes(proof.treeSize1 - 1n).length;
|
|
144
|
+
if (opts.accumulatorFrom === undefined) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
reason: "legacy_chain_break",
|
|
148
|
+
at: i,
|
|
149
|
+
detail: `first checkpoint base ${proof.treeSize1} != 0 and no trusted base accumulator was supplied`,
|
|
150
|
+
links,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (accumulator.length !== wanted) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
reason: "proof_malformed",
|
|
157
|
+
at: i,
|
|
158
|
+
detail: `trusted base accumulator has ${accumulator.length} peaks; base size ${proof.treeSize1} requires ${wanted}`,
|
|
159
|
+
links,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (proof.treeSize1 !== expectedBase) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
reason: "legacy_chain_break",
|
|
168
|
+
at: i,
|
|
169
|
+
detail: `checkpoint ${i} base ${proof.treeSize1} != previous sealed size ${expectedBase} — ` +
|
|
170
|
+
"pre-FOR-410 drifted chain (permanent for this log); fall back to the event scan, tile extension, or a holder cache",
|
|
171
|
+
links,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
let computed;
|
|
175
|
+
try {
|
|
176
|
+
computed = await computeCheckpointAccumulator(proof, accumulator);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
reason: "proof_malformed",
|
|
182
|
+
at: i,
|
|
183
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
184
|
+
links,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const signatureOk = await opts.verifySignature(bytes, accumulatorPayload(computed));
|
|
188
|
+
links.push({
|
|
189
|
+
treeSize1: proof.treeSize1,
|
|
190
|
+
treeSize2: proof.treeSize2,
|
|
191
|
+
accumulator: computed,
|
|
192
|
+
signatureOk,
|
|
193
|
+
});
|
|
194
|
+
if (!signatureOk) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
reason: "signature",
|
|
198
|
+
at: i,
|
|
199
|
+
detail: `checkpoint ${i} signature does not cover the computed size-${proof.treeSize2} accumulator`,
|
|
200
|
+
links,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
accumulator = computed;
|
|
204
|
+
expectedBase = proof.treeSize2;
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, links, accumulator };
|
|
207
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,4 +38,5 @@ export { findGrantLeafInMassif } from "./find-grant-leaf.js";
|
|
|
38
38
|
export type { LocatedLeaf } from "./find-grant-leaf.js";
|
|
39
39
|
/** Re-exported so callers of findGrantLeafInMassif can catch it (FOR-344). */
|
|
40
40
|
export { MissingIndexError } from "@forestrie/merklelog";
|
|
41
|
+
export { accumulatorPayload, checkpointConsistencyProof, computeCheckpointAccumulator, verifyCheckpointChain, type CheckpointChainLink, type CheckpointChainResult, type CheckpointConsistencyProof, } from "./checkpoint-chain.js";
|
|
41
42
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE;;;;GAIG;AACH,OAAO,EACL,iCAAiC,EACjC,4BAA4B,GAC7B,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,sCAAsC,EACtC,iCAAiC,GAClC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,yEAAyE;AACzE,OAAO,EAAE,qCAAqC,EAAE,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,0EAA0E;AAC1E,OAAO,EACL,cAAc,EACd,iDAAiD,GAClD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,6EAA6E;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,oEAAoE;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,8EAA8E;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE;;;;GAIG;AACH,OAAO,EACL,iCAAiC,EACjC,4BAA4B,GAC7B,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,sCAAsC,EACtC,iCAAiC,GAClC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,yEAAyE;AACzE,OAAO,EAAE,qCAAqC,EAAE,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,0EAA0E;AAC1E,OAAO,EACL,cAAc,EACd,iDAAiD,GAClD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,6EAA6E;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,oEAAoE;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,8EAA8E;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -28,3 +28,4 @@ export { grantCommitmentHashFromGrant } from "./grant-commitment.js";
|
|
|
28
28
|
export { findGrantLeafInMassif } from "./find-grant-leaf.js";
|
|
29
29
|
/** Re-exported so callers of findGrantLeafInMassif can catch it (FOR-344). */
|
|
30
30
|
export { MissingIndexError } from "@forestrie/merklelog";
|
|
31
|
+
export { accumulatorPayload, checkpointConsistencyProof, computeCheckpointAccumulator, verifyCheckpointChain, } from "./checkpoint-chain.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forestrie/receipt-verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Offline SCITT grant receipt verification (ADR-0045)",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@noble/hashes": "^1.7.1",
|
|
31
|
-
"@forestrie/encoding": "0.
|
|
32
|
-
"@forestrie/merklelog": "0.
|
|
31
|
+
"@forestrie/encoding": "0.5.0",
|
|
32
|
+
"@forestrie/merklelog": "0.2.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"esbuild": "^0.24.0",
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retained-checkpoint chain verification (FOR-368 Phase 3, plan-2607-29).
|
|
3
|
+
*
|
|
4
|
+
* Post-FOR-410 (ADR-0056) every checkpoint's embedded consistency proof
|
|
5
|
+
* spans its massif's ENTRY BOUNDARY to its seal, so the store's retained
|
|
6
|
+
* `.sth` objects form a contiguous chain `0 → S₁ → S₂ → …`. Folding the
|
|
7
|
+
* chain per the draft ("Chained proofs" / `consistent_roots`) reconstructs
|
|
8
|
+
* each link's tree-size-2 accumulator — which is exactly the detached
|
|
9
|
+
* payload its signature covers (ADR-0046: concat of the accumulator in
|
|
10
|
+
* descending height order). The fold therefore yields, with NO tile access
|
|
11
|
+
* and NO RPC: an authenticated accumulator at every retained seal, and the
|
|
12
|
+
* final state to check a receipt's recomputed peak against.
|
|
13
|
+
*
|
|
14
|
+
* This rung depends only on the public log store — the complement of the
|
|
15
|
+
* `CheckpointPublished` event scan (public chain data only); see the
|
|
16
|
+
* recorded both-paths decision in plan-2607-29.
|
|
17
|
+
*
|
|
18
|
+
* Legacy (pre-FOR-410) chains surface as a contiguity break
|
|
19
|
+
* (`legacy_chain_break`): a permanent per-log condition — fall back to the
|
|
20
|
+
* event scan, tile extension, or a holder cache.
|
|
21
|
+
*/
|
|
22
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
23
|
+
import { consistentRoots, peakMMRIndexes } from "@forestrie/merklelog";
|
|
24
|
+
import { SubtleHasher } from "./subtle-hasher.js";
|
|
25
|
+
import { parseCheckpoint } from "./build-receipt-offline.js";
|
|
26
|
+
|
|
27
|
+
const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
|
|
28
|
+
const VDP_CONSISTENCY_PROOF_KEY = -2;
|
|
29
|
+
|
|
30
|
+
/** Draft-bryce consistency proof embedded in a v3 checkpoint. */
|
|
31
|
+
export type CheckpointConsistencyProof = {
|
|
32
|
+
treeSize1: bigint;
|
|
33
|
+
treeSize2: bigint;
|
|
34
|
+
/** One inclusion path per tree-size-1 peak, proven at tree-size-2. */
|
|
35
|
+
paths: Uint8Array[][];
|
|
36
|
+
/** New peaks not covered by the proven roots (draft `right-peaks`). */
|
|
37
|
+
rightPeaks: Uint8Array[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function asBigint(v: unknown, what: string): bigint {
|
|
41
|
+
if (typeof v === "bigint") return v;
|
|
42
|
+
if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v);
|
|
43
|
+
throw new Error(`${what}: expected an unsigned integer`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function asBytesArray(v: unknown, what: string): Uint8Array[] {
|
|
47
|
+
if (!Array.isArray(v) || v.some((e) => !(e instanceof Uint8Array))) {
|
|
48
|
+
throw new Error(`${what}: expected an array of byte strings`);
|
|
49
|
+
}
|
|
50
|
+
return v as Uint8Array[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Decode the embedded consistency proof (`vdp` 396 key -2). */
|
|
54
|
+
export function checkpointConsistencyProof(
|
|
55
|
+
checkpointBytes: Uint8Array,
|
|
56
|
+
): CheckpointConsistencyProof {
|
|
57
|
+
const { unprotected } = parseCheckpoint(checkpointBytes);
|
|
58
|
+
const vdpRaw = unprotected.get(VDS_COSE_RECEIPT_PROOFS_TAG);
|
|
59
|
+
if (!(vdpRaw instanceof Map)) {
|
|
60
|
+
throw new Error("checkpoint carries no verifiable-proofs header (396)");
|
|
61
|
+
}
|
|
62
|
+
const proofBstr = (vdpRaw as Map<number, unknown>).get(
|
|
63
|
+
VDP_CONSISTENCY_PROOF_KEY,
|
|
64
|
+
);
|
|
65
|
+
if (!(proofBstr instanceof Uint8Array)) {
|
|
66
|
+
throw new Error("checkpoint carries no consistency proof (vdp key -2)");
|
|
67
|
+
}
|
|
68
|
+
const proof = decodeCborDeterministic(proofBstr);
|
|
69
|
+
if (!Array.isArray(proof) || proof.length < 4) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"consistency proof must be [tree-size-1, tree-size-2, paths, right-peaks]",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const pathsRaw = proof[2];
|
|
75
|
+
if (
|
|
76
|
+
!Array.isArray(pathsRaw) ||
|
|
77
|
+
pathsRaw.some(
|
|
78
|
+
(p) => !Array.isArray(p) || p.some((n) => !(n instanceof Uint8Array)),
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
throw new Error("consistency paths must be arrays of byte strings");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
treeSize1: asBigint(proof[0], "tree-size-1"),
|
|
85
|
+
treeSize2: asBigint(proof[1], "tree-size-2"),
|
|
86
|
+
paths: pathsRaw as Uint8Array[][],
|
|
87
|
+
rightPeaks: asBytesArray(proof[3], "right-peaks"),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One fold step: from the trusted accumulator at `proof.treeSize1`,
|
|
93
|
+
* produce the `treeSize2` accumulator (draft: `consistent_roots` output
|
|
94
|
+
* plus the supplied right-peaks). Structural sanity: the result must have
|
|
95
|
+
* exactly the peak count of a size-`treeSize2` MMR.
|
|
96
|
+
*/
|
|
97
|
+
export async function computeCheckpointAccumulator(
|
|
98
|
+
proof: CheckpointConsistencyProof,
|
|
99
|
+
accumulatorFrom: Uint8Array[],
|
|
100
|
+
): Promise<Uint8Array[]> {
|
|
101
|
+
const hasher = new SubtleHasher();
|
|
102
|
+
const proven =
|
|
103
|
+
proof.treeSize1 === 0n
|
|
104
|
+
? []
|
|
105
|
+
: await consistentRoots(
|
|
106
|
+
hasher,
|
|
107
|
+
proof.treeSize1 - 1n,
|
|
108
|
+
accumulatorFrom,
|
|
109
|
+
proof.paths,
|
|
110
|
+
);
|
|
111
|
+
const accumulator = [...proven, ...proof.rightPeaks];
|
|
112
|
+
const expected = peakMMRIndexes(proof.treeSize2 - 1n).length;
|
|
113
|
+
if (accumulator.length !== expected) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`computed accumulator has ${accumulator.length} peaks; size ${proof.treeSize2} requires ${expected}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return accumulator;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Detached payload the checkpoint signature covers (ADR-0046): the raw
|
|
122
|
+
* concatenation of the accumulator peaks in contract order. */
|
|
123
|
+
export function accumulatorPayload(accumulator: Uint8Array[]): Uint8Array {
|
|
124
|
+
const out = new Uint8Array(accumulator.reduce((s, p) => s + p.length, 0));
|
|
125
|
+
let offset = 0;
|
|
126
|
+
for (const p of accumulator) {
|
|
127
|
+
out.set(p, offset);
|
|
128
|
+
offset += p.length;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type CheckpointChainLink = {
|
|
134
|
+
treeSize1: bigint;
|
|
135
|
+
treeSize2: bigint;
|
|
136
|
+
accumulator: Uint8Array[];
|
|
137
|
+
signatureOk: boolean;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type CheckpointChainResult =
|
|
141
|
+
| { ok: true; links: CheckpointChainLink[]; accumulator: Uint8Array[] }
|
|
142
|
+
| {
|
|
143
|
+
ok: false;
|
|
144
|
+
reason:
|
|
145
|
+
| "empty_chain"
|
|
146
|
+
| "legacy_chain_break"
|
|
147
|
+
| "signature"
|
|
148
|
+
| "proof_malformed";
|
|
149
|
+
/** Index of the offending checkpoint. */
|
|
150
|
+
at: number;
|
|
151
|
+
detail: string;
|
|
152
|
+
links: CheckpointChainLink[];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Verify a retained checkpoint chain (ascending massif order) and fold out
|
|
157
|
+
* the final authenticated accumulator.
|
|
158
|
+
*
|
|
159
|
+
* - The first link must be boundary-based from 0 (a whole-log chain), or
|
|
160
|
+
* the caller supplies `accumulatorFrom` matching its base (a suffix
|
|
161
|
+
* chain rooted in an already-trusted accumulator).
|
|
162
|
+
* - Every subsequent link's base must equal the previous link's sealed
|
|
163
|
+
* size — a mismatch is the legacy (pre-FOR-410) drift signature and is
|
|
164
|
+
* permanent for that log (`legacy_chain_break`).
|
|
165
|
+
* - Each link's signature is checked over its computed accumulator via
|
|
166
|
+
* the injected verifier (the caller owns trust resolution — genesis
|
|
167
|
+
* roots, caller-known keys, or the label-1000 delegation path).
|
|
168
|
+
*/
|
|
169
|
+
export async function verifyCheckpointChain(opts: {
|
|
170
|
+
checkpoints: Uint8Array[];
|
|
171
|
+
verifySignature: (
|
|
172
|
+
checkpointBytes: Uint8Array,
|
|
173
|
+
detachedPayload: Uint8Array,
|
|
174
|
+
) => Promise<boolean>;
|
|
175
|
+
/** Trusted base accumulator for a suffix chain (absent: base must be 0). */
|
|
176
|
+
accumulatorFrom?: Uint8Array[];
|
|
177
|
+
}): Promise<CheckpointChainResult> {
|
|
178
|
+
const links: CheckpointChainLink[] = [];
|
|
179
|
+
if (opts.checkpoints.length === 0) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: "empty_chain",
|
|
183
|
+
at: 0,
|
|
184
|
+
detail: "no checkpoints supplied",
|
|
185
|
+
links,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
let accumulator = opts.accumulatorFrom ?? [];
|
|
189
|
+
let expectedBase: bigint | null = null;
|
|
190
|
+
for (let i = 0; i < opts.checkpoints.length; i++) {
|
|
191
|
+
const bytes = opts.checkpoints[i]!;
|
|
192
|
+
let proof: CheckpointConsistencyProof;
|
|
193
|
+
try {
|
|
194
|
+
proof = checkpointConsistencyProof(bytes);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
reason: "proof_malformed",
|
|
199
|
+
at: i,
|
|
200
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
201
|
+
links,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (expectedBase === null) {
|
|
205
|
+
// First link: base 0 for a whole-log chain, else the caller's
|
|
206
|
+
// trusted accumulator must be FOR this base (peak-count check).
|
|
207
|
+
if (proof.treeSize1 !== 0n) {
|
|
208
|
+
const wanted = peakMMRIndexes(proof.treeSize1 - 1n).length;
|
|
209
|
+
if (opts.accumulatorFrom === undefined) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
reason: "legacy_chain_break",
|
|
213
|
+
at: i,
|
|
214
|
+
detail: `first checkpoint base ${proof.treeSize1} != 0 and no trusted base accumulator was supplied`,
|
|
215
|
+
links,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (accumulator.length !== wanted) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
reason: "proof_malformed",
|
|
222
|
+
at: i,
|
|
223
|
+
detail: `trusted base accumulator has ${accumulator.length} peaks; base size ${proof.treeSize1} requires ${wanted}`,
|
|
224
|
+
links,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} else if (proof.treeSize1 !== expectedBase) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
reason: "legacy_chain_break",
|
|
232
|
+
at: i,
|
|
233
|
+
detail:
|
|
234
|
+
`checkpoint ${i} base ${proof.treeSize1} != previous sealed size ${expectedBase} — ` +
|
|
235
|
+
"pre-FOR-410 drifted chain (permanent for this log); fall back to the event scan, tile extension, or a holder cache",
|
|
236
|
+
links,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
let computed: Uint8Array[];
|
|
240
|
+
try {
|
|
241
|
+
computed = await computeCheckpointAccumulator(proof, accumulator);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
reason: "proof_malformed",
|
|
246
|
+
at: i,
|
|
247
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
248
|
+
links,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const signatureOk = await opts.verifySignature(
|
|
252
|
+
bytes,
|
|
253
|
+
accumulatorPayload(computed),
|
|
254
|
+
);
|
|
255
|
+
links.push({
|
|
256
|
+
treeSize1: proof.treeSize1,
|
|
257
|
+
treeSize2: proof.treeSize2,
|
|
258
|
+
accumulator: computed,
|
|
259
|
+
signatureOk,
|
|
260
|
+
});
|
|
261
|
+
if (!signatureOk) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
reason: "signature",
|
|
265
|
+
at: i,
|
|
266
|
+
detail: `checkpoint ${i} signature does not cover the computed size-${proof.treeSize2} accumulator`,
|
|
267
|
+
links,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
accumulator = computed;
|
|
271
|
+
expectedBase = proof.treeSize2;
|
|
272
|
+
}
|
|
273
|
+
return { ok: true, links, accumulator };
|
|
274
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -62,3 +62,12 @@ export { findGrantLeafInMassif } from "./find-grant-leaf.js";
|
|
|
62
62
|
export type { LocatedLeaf } from "./find-grant-leaf.js";
|
|
63
63
|
/** Re-exported so callers of findGrantLeafInMassif can catch it (FOR-344). */
|
|
64
64
|
export { MissingIndexError } from "@forestrie/merklelog";
|
|
65
|
+
export {
|
|
66
|
+
accumulatorPayload,
|
|
67
|
+
checkpointConsistencyProof,
|
|
68
|
+
computeCheckpointAccumulator,
|
|
69
|
+
verifyCheckpointChain,
|
|
70
|
+
type CheckpointChainLink,
|
|
71
|
+
type CheckpointChainResult,
|
|
72
|
+
type CheckpointConsistencyProof,
|
|
73
|
+
} from "./checkpoint-chain.js";
|