@forestrie/receipt-verify 2.1.0 → 3.0.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/build-receipt-offline.js +14 -10
- package/dist/checkpoint-chain.d.ts +130 -48
- package/dist/checkpoint-chain.d.ts.map +1 -1
- package/dist/checkpoint-chain.js +212 -60
- package/dist/decode-checkpoint-consistency-proof.d.ts +47 -15
- package/dist/decode-checkpoint-consistency-proof.d.ts.map +1 -1
- package/dist/decode-checkpoint-consistency-proof.js +105 -33
- package/dist/freshen-receipt.d.ts +12 -6
- package/dist/freshen-receipt.d.ts.map +1 -1
- package/dist/freshen-receipt.js +34 -14
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/src/build-receipt-offline.ts +14 -10
- package/src/checkpoint-chain.ts +254 -79
- package/src/decode-checkpoint-consistency-proof.ts +118 -34
- package/src/freshen-receipt.ts +43 -16
- package/src/index.ts +6 -0
package/src/checkpoint-chain.ts
CHANGED
|
@@ -20,11 +20,15 @@
|
|
|
20
20
|
* "keyless first checkpoint" case). `tree-size-1` stays unsigned prover
|
|
21
21
|
* context: the publisher relays several sealed steps and may re-base a step
|
|
22
22
|
* under the head checkpoint's signature, so the declared base of a
|
|
23
|
-
* checkpoint can differ from what the sealer had (
|
|
24
|
-
* withdrawn) — a signed size-1 comparison
|
|
25
|
-
* publish and every multi-link catch-up.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* checkpoint can differ from what the sealer had (the signed origin
|
|
24
|
+
* ADR-0066 D2 first proposed was withdrawn) — a signed size-1 comparison
|
|
25
|
+
* would reject every re-based publish and every multi-link catch-up. One
|
|
26
|
+
* checkpoint may itself relay SEVERAL sealed steps (ADR-0066 D2): the
|
|
27
|
+
* draft carries them under vdp key -2 as
|
|
28
|
+
* `consistency-proofs = [ + consistency-proof ]`, folded here in order,
|
|
29
|
+
* with only the last step's size signed. A checkpoint without the signed
|
|
30
|
+
* size-2 label, or whose signed size-2 disagrees with the last proof it
|
|
31
|
+
* relays, is rejected before any fold is attempted.
|
|
28
32
|
*
|
|
29
33
|
* This rung depends only on the public log store — the complement of the
|
|
30
34
|
* `CheckpointPublished` event scan (public chain data only); see the
|
|
@@ -40,8 +44,9 @@
|
|
|
40
44
|
*/
|
|
41
45
|
import {
|
|
42
46
|
COSE_ALG_ES256,
|
|
43
|
-
|
|
47
|
+
ProtectedHeaderAlgError,
|
|
44
48
|
isLowS,
|
|
49
|
+
readProtectedAlg,
|
|
45
50
|
readProtectedTreeSize2,
|
|
46
51
|
} from "@forestrie/encoding";
|
|
47
52
|
import {
|
|
@@ -52,25 +57,64 @@ import {
|
|
|
52
57
|
} from "@forestrie/merklelog";
|
|
53
58
|
import { SubtleHasher } from "./subtle-hasher.js";
|
|
54
59
|
import { parseCheckpoint } from "./build-receipt-offline.js";
|
|
55
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
decodeConsistencyProofsFromUnprotected,
|
|
62
|
+
EmptyConsistencyProofsError,
|
|
63
|
+
type DecodedConsistencyProof,
|
|
64
|
+
} from "./decode-checkpoint-consistency-proof.js";
|
|
56
65
|
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
66
|
+
/**
|
|
67
|
+
* The draft-bryce consistency proofs embedded in a v3 checkpoint: one or
|
|
68
|
+
* more, in relay order (`consistency-proofs = [ + consistency-proof ]`,
|
|
69
|
+
* ADR-0066 D2). A checkpoint sealing a single step carries the chain of
|
|
70
|
+
* one; there is no separate single-proof shape.
|
|
71
|
+
*
|
|
72
|
+
* {@link treeSize2} — the LAST proof's — is cross-checked against the
|
|
73
|
+
* checkpoint's SIGNED tree-size-2 (ADR-0066 D1 as amended, D5.5).
|
|
74
|
+
* {@link treeSize1} — the FIRST proof's — is unsigned prover context, not
|
|
75
|
+
* cross-checked here; see {@link verifyCheckpointChain}, which compares it
|
|
76
|
+
* with the trusted origin instead. The sizes between the two are named by
|
|
77
|
+
* no signature: {@link computeCheckpointAccumulator} holds the chain
|
|
78
|
+
* together by requiring each proof to continue the one before it.
|
|
79
|
+
*/
|
|
62
80
|
export type CheckpointConsistencyProof = {
|
|
81
|
+
/** The relayed proofs, in chain order; never empty. */
|
|
82
|
+
proofs: DecodedConsistencyProof[];
|
|
83
|
+
/** `tree-size-1` of the FIRST proof: the size the chain continues from. */
|
|
63
84
|
treeSize1: bigint;
|
|
85
|
+
/** `tree-size-2` of the LAST proof: the size the chain reaches. */
|
|
64
86
|
treeSize2: bigint;
|
|
65
87
|
/** Signed `tree-size-2` (protected header label -65933); equal to
|
|
66
88
|
* {@link treeSize2} — {@link checkpointConsistencyProof} enforces this. */
|
|
67
89
|
signedTreeSize2: bigint;
|
|
68
|
-
/** One inclusion path per tree-size-1 peak, proven at tree-size-2. */
|
|
69
|
-
paths: Uint8Array[][];
|
|
70
|
-
/** New peaks not covered by the proven roots (draft `right-peaks`). */
|
|
71
|
-
rightPeaks: Uint8Array[];
|
|
72
90
|
};
|
|
73
91
|
|
|
92
|
+
/**
|
|
93
|
+
* A relayed consistency-proof chain does not join up: a proof's declared
|
|
94
|
+
* `tree-size-1` is not the size the fold has reached — the caller's trusted
|
|
95
|
+
* size for the first proof, the previous proof's `tree-size-2` after that —
|
|
96
|
+
* or, having applied every proof, the fold reaches a size other than the
|
|
97
|
+
* chain link's own declared `tree-size-2` (F3: a caller-assembled link —
|
|
98
|
+
* `freshenReceipt` callers build these from on-chain calldata, never
|
|
99
|
+
* through {@link checkpointConsistencyProof} — can set `proofs` and
|
|
100
|
+
* `treeSize2` independently, which `checkpointConsistencyProof` itself
|
|
101
|
+
* never allows to disagree). go-merklelog reuses its equivalent
|
|
102
|
+
* `ErrProofChainNotContiguous` for this same end-of-chain comparison
|
|
103
|
+
* (`checkpointverify.go:245-251`, folded size vs. signed size). Reported as
|
|
104
|
+
* `"size_mismatch"` by {@link verifyCheckpointChain}, the same as any other
|
|
105
|
+
* size disagreement, because the sizes it names are unsigned (ADR-0066 D2):
|
|
106
|
+
* nothing distinguishes a relay assembled in the wrong order from one
|
|
107
|
+
* assembled over a different log.
|
|
108
|
+
*/
|
|
109
|
+
export class ConsistencyChainNotContiguousError extends Error {
|
|
110
|
+
constructor(message: string) {
|
|
111
|
+
super(message);
|
|
112
|
+
this.name = "ConsistencyChainNotContiguousError";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { EmptyConsistencyProofsError };
|
|
117
|
+
|
|
74
118
|
/**
|
|
75
119
|
* The checkpoint's SIGNED `tree-size-2` (protected header, ADR-0066 D1 as
|
|
76
120
|
* amended) differs from the declared `tree-size-2` of its embedded
|
|
@@ -105,12 +149,33 @@ export class CheckpointHighSSignatureError extends Error {
|
|
|
105
149
|
}
|
|
106
150
|
|
|
107
151
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
152
|
+
* The checkpoint's protected header carries no integer `alg` (label 1).
|
|
153
|
+
*
|
|
154
|
+
* The univocity contract rejects such a header outright — the structural
|
|
155
|
+
* walk finds the size, and the `alg` requirement in the same call raises
|
|
156
|
+
* `ClaimNotFound(1)` or `UnexpectedMajorType`. Off-chain the header used to
|
|
157
|
+
* read as "no algorithm stated", which both turned OFF the high-s rejection
|
|
158
|
+
* below (gated on the algorithm being ES256) and still yielded a signed size
|
|
159
|
+
* to fold from — so a checkpoint the chain will never anchor verified here
|
|
160
|
+
* under weaker rules than a well-formed one (review finding S-1). Reported
|
|
161
|
+
* as `"proof_malformed"` by {@link verifyCheckpointChain}.
|
|
162
|
+
*/
|
|
163
|
+
export class CheckpointProtectedHeaderAlgError extends Error {
|
|
164
|
+
constructor(message: string) {
|
|
165
|
+
super(message);
|
|
166
|
+
this.name = "CheckpointProtectedHeaderAlgError";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Decode the embedded consistency proofs (`vdp` 396 key -2, one or more in
|
|
172
|
+
* relay order) and require the LAST proof's declared `tree-size-2` to equal
|
|
173
|
+
* the checkpoint's SIGNED `tree-size-2` from the protected header (ADR-0066
|
|
174
|
+
* D1 as amended, D2, D5.5, label -65933). The earlier proofs' sizes are not
|
|
175
|
+
* signed: the fold checks them against each other
|
|
176
|
+
* ({@link computeCheckpointAccumulator}). `tree-size-1` is not signed
|
|
177
|
+
* either; see {@link verifyCheckpointChain} for its comparison against the
|
|
178
|
+
* trusted origin.
|
|
114
179
|
*
|
|
115
180
|
* Also rejects a malleable high-s ES256 signature (see
|
|
116
181
|
* {@link CheckpointHighSSignatureError}) before any fold or WebCrypto verify
|
|
@@ -118,20 +183,37 @@ export class CheckpointHighSSignatureError extends Error {
|
|
|
118
183
|
* subject to this check, since it does not go through the P-256 WebCrypto
|
|
119
184
|
* path this guards.
|
|
120
185
|
*
|
|
121
|
-
* @throws {Error} when the
|
|
122
|
-
*
|
|
123
|
-
* {@link
|
|
186
|
+
* @throws {Error} when the unprotected header carries no consistency proof,
|
|
187
|
+
* a proof is structurally malformed (see
|
|
188
|
+
* {@link decodeConsistencyProofsFromUnprotected}), or the protected header
|
|
124
189
|
* carries no signed tree-size-2 label
|
|
190
|
+
* @throws {EmptyConsistencyProofsError} when the consistency-proofs array is
|
|
191
|
+
* present but empty
|
|
125
192
|
* @throws {CheckpointSignedSizeMismatchError} when the signed tree-size-2
|
|
126
|
-
* differs from the declared proof's tree-size-2
|
|
193
|
+
* differs from the LAST declared proof's tree-size-2
|
|
127
194
|
* @throws {CheckpointHighSSignatureError} when the checkpoint is ES256-signed
|
|
128
195
|
* with a high-s (malleable) signature
|
|
196
|
+
* @throws {CheckpointProtectedHeaderAlgError} when the protected header
|
|
197
|
+
* carries no integer `alg` (label 1)
|
|
129
198
|
*/
|
|
130
199
|
export function checkpointConsistencyProof(
|
|
131
200
|
checkpointBytes: Uint8Array,
|
|
132
201
|
): CheckpointConsistencyProof {
|
|
133
202
|
const { coseSign1, unprotected } = parseCheckpoint(checkpointBytes);
|
|
134
|
-
|
|
203
|
+
// Strict: a header with no integer alg is one the contract rejects, and
|
|
204
|
+
// reading it leniently would switch the high-s rejection below off while
|
|
205
|
+
// the signed size was still taken from it (S-1).
|
|
206
|
+
let alg: number;
|
|
207
|
+
try {
|
|
208
|
+
alg = readProtectedAlg(coseSign1[0]);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (err instanceof ProtectedHeaderAlgError) {
|
|
211
|
+
throw new CheckpointProtectedHeaderAlgError(
|
|
212
|
+
`checkpoint protected header carries no integer alg (label 1): ${err.message}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
135
217
|
const signature = coseSign1[3];
|
|
136
218
|
if (alg === COSE_ALG_ES256 && signature.length === 64 && !isLowS(signature)) {
|
|
137
219
|
throw new CheckpointHighSSignatureError(
|
|
@@ -139,75 +221,125 @@ export function checkpointConsistencyProof(
|
|
|
139
221
|
"to match the univocity contract's P-256 verifier and go-merklelog",
|
|
140
222
|
);
|
|
141
223
|
}
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
224
|
+
const proofs = decodeConsistencyProofsFromUnprotected(unprotected);
|
|
225
|
+
if (proofs === null) {
|
|
144
226
|
throw new Error("checkpoint carries no consistency proof (vdp key -2)");
|
|
145
227
|
}
|
|
228
|
+
const last = proofs[proofs.length - 1]!;
|
|
146
229
|
const signedTreeSize2 = readProtectedTreeSize2(coseSign1[0]);
|
|
147
230
|
if (signedTreeSize2 === null) {
|
|
148
231
|
throw new Error(
|
|
149
232
|
"checkpoint protected header carries no signed tree-size-2 (-65933)",
|
|
150
233
|
);
|
|
151
234
|
}
|
|
152
|
-
|
|
235
|
+
// The signature covers the size the LAST proof reaches, and only that
|
|
236
|
+
// size: a relay may hold any number of steps before it, none of them
|
|
237
|
+
// signed (ADR-0066 D2).
|
|
238
|
+
if (signedTreeSize2 !== last.treeSize2) {
|
|
153
239
|
throw new CheckpointSignedSizeMismatchError(
|
|
154
|
-
`signed tree-size-2 (-65933) ${signedTreeSize2} != declared consistency-proof tree-size-2 ${
|
|
240
|
+
`signed tree-size-2 (-65933) ${signedTreeSize2} != declared consistency-proof tree-size-2 ${last.treeSize2}`,
|
|
155
241
|
);
|
|
156
242
|
}
|
|
157
243
|
return {
|
|
158
|
-
|
|
159
|
-
|
|
244
|
+
proofs,
|
|
245
|
+
treeSize1: proofs[0]!.treeSize1,
|
|
246
|
+
treeSize2: last.treeSize2,
|
|
160
247
|
signedTreeSize2,
|
|
161
|
-
paths: declared.paths,
|
|
162
|
-
rightPeaks: declared.rightPeaks,
|
|
163
248
|
};
|
|
164
249
|
}
|
|
165
250
|
|
|
166
251
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
252
|
+
* Fold a checkpoint's relayed consistency proofs, in order, from the
|
|
253
|
+
* CALLER-TRUSTED accumulator at `sizeFrom` to the accumulator at the last
|
|
254
|
+
* proof's `tree-size-2` — the value the checkpoint's signature covers.
|
|
255
|
+
*
|
|
256
|
+
* Each proof is applied by the size-driven {@link consistentRootsForSizes}
|
|
257
|
+
* (ADR-0066 D5), which yields `roots` (the proven prefix); the proof's own
|
|
258
|
+
* right-peaks (the target peaks no path reaches) complete that step's
|
|
259
|
+
* accumulator, and it becomes the next step's input. A checkpoint sealing
|
|
260
|
+
* one step carries the chain of one and runs the same loop once.
|
|
172
261
|
*
|
|
173
|
-
* `sizeFrom` is a parameter, not read off
|
|
174
|
-
*
|
|
175
|
-
* `treeSize2`, or the caller's anchor for a first link) — reading
|
|
176
|
-
* the
|
|
177
|
-
* own starting point
|
|
178
|
-
*
|
|
179
|
-
*
|
|
262
|
+
* `sizeFrom` is a parameter, not read off the proofs, because the fold must
|
|
263
|
+
* start from a size the CALLER already trusts (the previous checkpoint's
|
|
264
|
+
* verified `treeSize2`, or the caller's anchor for a first link) — reading
|
|
265
|
+
* it from the relay instead would let an unsigned or substituted proof
|
|
266
|
+
* dictate its own starting point (ADR-0066 D5.4). Every proof after the
|
|
267
|
+
* first is held to the size the previous one reached for the same reason:
|
|
268
|
+
* only the last step's size is signed, so the intermediate sizes are worth
|
|
269
|
+
* no more than their agreement with each other.
|
|
180
270
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
271
|
+
* `proof.proofs` must hold at least one step and, once every step has been
|
|
272
|
+
* applied, the fold must have reached exactly `proof.treeSize2` (F3).
|
|
273
|
+
* `checkpointConsistencyProof` already guarantees both — the decode rejects
|
|
274
|
+
* an empty `consistency-proofs` array (`EmptyConsistencyProofsError`) and
|
|
275
|
+
* sets `treeSize2` from the last decoded proof — but a caller-assembled
|
|
276
|
+
* link (`freshenReceipt` callers building from on-chain calldata) is not
|
|
277
|
+
* decoded through it, so both are re-checked here rather than trusted from
|
|
278
|
+
* the type.
|
|
279
|
+
*
|
|
280
|
+
* @throws {EmptyConsistencyProofsError} when `proof.proofs` is empty — an
|
|
281
|
+
* already-decoded link the caller assembled themselves rather than one
|
|
282
|
+
* `checkpointConsistencyProof` produced, which never returns one
|
|
283
|
+
* @throws {ConsistencyChainNotContiguousError} when the first proof's
|
|
284
|
+
* `treeSize1` is not `sizeFrom`, a later proof's `treeSize1` is not the
|
|
285
|
+
* previous proof's `treeSize2`, or the size the fold reaches after every
|
|
286
|
+
* proof is not `proof.treeSize2`
|
|
287
|
+
* @throws {Error} when a proof supplies a right-peaks count other than
|
|
183
288
|
* {@link consistentRootsForSizes}'s `expectedRight`
|
|
184
|
-
* @throws {ConsistencyShapeError} (`@forestrie/merklelog`) when
|
|
185
|
-
*
|
|
289
|
+
* @throws {ConsistencyShapeError} (`@forestrie/merklelog`) when a proof does
|
|
290
|
+
* not have the shape its two sizes imply
|
|
186
291
|
*/
|
|
187
292
|
export async function computeCheckpointAccumulator(
|
|
188
293
|
proof: CheckpointConsistencyProof,
|
|
189
294
|
accumulatorFrom: Uint8Array[],
|
|
190
295
|
sizeFrom: bigint,
|
|
191
296
|
): Promise<Uint8Array[]> {
|
|
192
|
-
if (proof.
|
|
193
|
-
throw new
|
|
194
|
-
|
|
297
|
+
if (proof.proofs.length === 0) {
|
|
298
|
+
throw new EmptyConsistencyProofsError(
|
|
299
|
+
"consistency proof relays no proofs (empty proofs array); at least " +
|
|
300
|
+
"one is required to fold",
|
|
195
301
|
);
|
|
196
302
|
}
|
|
197
303
|
const hasher = new SubtleHasher();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
proof.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
304
|
+
let accumulator = accumulatorFrom;
|
|
305
|
+
let size = sizeFrom;
|
|
306
|
+
for (let i = 0; i < proof.proofs.length; i++) {
|
|
307
|
+
const step = proof.proofs[i]!;
|
|
308
|
+
if (step.treeSize1 !== size) {
|
|
309
|
+
throw new ConsistencyChainNotContiguousError(
|
|
310
|
+
i === 0
|
|
311
|
+
? `consistency proof base tree-size-1 ${step.treeSize1} does not match the trusted size ${size}`
|
|
312
|
+
: `consistency-proofs entry ${i} declares tree-size-1 ${step.treeSize1}; the previous proof reached ${size}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const { roots, expectedRight } = await consistentRootsForSizes(
|
|
316
|
+
hasher,
|
|
317
|
+
size,
|
|
318
|
+
step.treeSize2,
|
|
319
|
+
accumulator,
|
|
320
|
+
step.paths,
|
|
208
321
|
);
|
|
322
|
+
if (step.rightPeaks.length !== expectedRight) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`checkpoint supplies ${step.rightPeaks.length} right-peaks; size ${step.treeSize2} requires ${expectedRight}`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
accumulator = [...roots, ...step.rightPeaks];
|
|
328
|
+
size = step.treeSize2;
|
|
209
329
|
}
|
|
210
|
-
|
|
330
|
+
// The fold must land exactly on the size the LINK itself declares —
|
|
331
|
+
// `proof.treeSize2` — not merely on whatever size its last proof happened
|
|
332
|
+
// to reach: a caller-assembled link can set the two independently (e.g.
|
|
333
|
+
// proofs folding 1 -> 3 alongside a declared treeSize2 of 7), which would
|
|
334
|
+
// otherwise fold to 3 and be reported as size 7 to every downstream
|
|
335
|
+
// caller reading the declared field instead of the fold. Mirrors
|
|
336
|
+
// go-merklelog's own end-of-chain check (checkpointverify.go:245-251).
|
|
337
|
+
if (size !== proof.treeSize2) {
|
|
338
|
+
throw new ConsistencyChainNotContiguousError(
|
|
339
|
+
`consistency proof folds to tree-size-2 ${size}; the chain's declared tree-size-2 is ${proof.treeSize2}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return accumulator;
|
|
211
343
|
}
|
|
212
344
|
|
|
213
345
|
/** Detached payload the checkpoint signature covers (ADR-0046): the raw
|
|
@@ -254,8 +386,10 @@ export type CheckpointChainResult =
|
|
|
254
386
|
* `tree-size-2` (ADR-0066 D1 as amended, D5.5), or a link's declared
|
|
255
387
|
* `tree-size-1` disagrees with the size the fold starts from — the
|
|
256
388
|
* caller's `trustedBase.size` (0 with no `trustedBase`) for the
|
|
257
|
-
* first link, the previous link's `tree-size-2` after that
|
|
258
|
-
*
|
|
389
|
+
* first link, the previous link's `tree-size-2` after that — or a
|
|
390
|
+
* relayed proof WITHIN a checkpoint disagrees with the size the
|
|
391
|
+
* proof before it reached ({@link
|
|
392
|
+
* ConsistencyChainNotContiguousError}). `detail` names both sizes.
|
|
259
393
|
*/
|
|
260
394
|
| "size_mismatch";
|
|
261
395
|
/** Index of the offending checkpoint. */
|
|
@@ -273,23 +407,31 @@ export type CheckpointChainResult =
|
|
|
273
407
|
* as the fold's starting accumulator (a suffix chain rooted in an
|
|
274
408
|
* already-trusted accumulator supplies both).
|
|
275
409
|
* - A supplied `trustedBase` must describe a state an MMR can be in: its
|
|
276
|
-
* `size` a complete MMR size,
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* count no MMR has
|
|
410
|
+
* `size` a complete MMR size, its `accumulator` holding one peak per peak
|
|
411
|
+
* of that size, and every one of those peaks a 32-byte node value.
|
|
412
|
+
* `peaksBitmap` rounds an incomplete size DOWN to the largest MMR below
|
|
413
|
+
* it, so without the completeness check a size of 5 folds the 4 -> N shape
|
|
414
|
+
* while every link reports a base of 5 — a node count no MMR has; and
|
|
415
|
+
* without the byte-length check an origin peak of any length is copied
|
|
416
|
+
* through the empty-path branch into the detached payload. All three are
|
|
417
|
+
* `proof_malformed`.
|
|
281
418
|
* - The first link's declared `tree-size-1` must equal that trusted
|
|
282
419
|
* starting size, and every subsequent link's must equal the previous
|
|
283
420
|
* link's sealed `tree-size-2`; either disagreement is `size_mismatch`.
|
|
284
|
-
* `tree-size-1` itself is never compared with a signed value (
|
|
285
|
-
*
|
|
421
|
+
* `tree-size-1` itself is never compared with a signed value (the signed
|
|
422
|
+
* origin ADR-0066 D2 first proposed was withdrawn): only this
|
|
423
|
+
* trusted-origin comparison applies. Because it is
|
|
286
424
|
* unsigned, the reason it produces carries no more meaning than the size
|
|
287
425
|
* disagreement itself (ADR-0066 D6: no pre-FOR-410 state is supported, so
|
|
288
426
|
* there is no drift condition to fall back from).
|
|
289
|
-
* -
|
|
290
|
-
*
|
|
291
|
-
* (
|
|
292
|
-
* `
|
|
427
|
+
* - A checkpoint may relay SEVERAL consistency proofs under one signature
|
|
428
|
+
* (ADR-0066 D2; draft `consistency-proofs = [ + consistency-proof ]`).
|
|
429
|
+
* Its SIGNED `tree-size-2` (ADR-0066 D1 as amended) must equal the LAST
|
|
430
|
+
* proof's declared `tree-size-2` ({@link checkpointConsistencyProof}),
|
|
431
|
+
* and each relayed proof must continue the one before it
|
|
432
|
+
* ({@link computeCheckpointAccumulator}); either disagreement is
|
|
433
|
+
* `size_mismatch`. A checkpoint sealing one step is the relay of one and
|
|
434
|
+
* takes the same path.
|
|
293
435
|
* - Each link's signature is checked over its computed accumulator via
|
|
294
436
|
* the injected verifier (the caller owns trust resolution — genesis
|
|
295
437
|
* roots, caller-known keys, or the label-1000 delegation path). A link
|
|
@@ -349,6 +491,25 @@ export async function verifyCheckpointChain(opts: {
|
|
|
349
491
|
links,
|
|
350
492
|
};
|
|
351
493
|
}
|
|
494
|
+
// …and every peak must be a 32-byte node value, the check arbor's
|
|
495
|
+
// producer applies to both path elements and right-peaks (`toNode32`).
|
|
496
|
+
// The count alone does not reach it: on the empty-path branch an origin
|
|
497
|
+
// peak is copied into the result verbatim, so a 0/31/33/64-byte peak
|
|
498
|
+
// reaches `accumulatorPayload` and shortens or lengthens the detached
|
|
499
|
+
// payload, with only the signature left to reject it (review finding
|
|
500
|
+
// I2, canopy C6).
|
|
501
|
+
for (let i = 0; i < trustedBase.accumulator.length; i++) {
|
|
502
|
+
const peak = trustedBase.accumulator[i] as unknown;
|
|
503
|
+
if (!(peak instanceof Uint8Array) || peak.length !== 32) {
|
|
504
|
+
return {
|
|
505
|
+
ok: false,
|
|
506
|
+
reason: "proof_malformed",
|
|
507
|
+
at: 0,
|
|
508
|
+
detail: `trusted base accumulator peak ${i} is not a 32-byte node value (${describePeak(peak)})`,
|
|
509
|
+
links,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
352
513
|
}
|
|
353
514
|
let accumulator = trustedBase?.accumulator ?? [];
|
|
354
515
|
let expectedBase = trustedBase?.size ?? 0n;
|
|
@@ -403,9 +564,15 @@ export async function verifyCheckpointChain(opts: {
|
|
|
403
564
|
expectedBase,
|
|
404
565
|
);
|
|
405
566
|
} catch (err) {
|
|
567
|
+
// A relay that does not join up is a size disagreement like any
|
|
568
|
+
// other: every size it names but the last is unsigned, so the reason
|
|
569
|
+
// can say no more than that two sizes differ.
|
|
406
570
|
return {
|
|
407
571
|
ok: false,
|
|
408
|
-
reason:
|
|
572
|
+
reason:
|
|
573
|
+
err instanceof ConsistencyChainNotContiguousError
|
|
574
|
+
? "size_mismatch"
|
|
575
|
+
: "proof_malformed",
|
|
409
576
|
at: i,
|
|
410
577
|
detail: err instanceof Error ? err.message : String(err),
|
|
411
578
|
links,
|
|
@@ -439,3 +606,11 @@ export async function verifyCheckpointChain(opts: {
|
|
|
439
606
|
}
|
|
440
607
|
return { ok: true, links, accumulator };
|
|
441
608
|
}
|
|
609
|
+
|
|
610
|
+
/** Name what was found where a 32-byte accumulator peak was required. */
|
|
611
|
+
function describePeak(peak: unknown): string {
|
|
612
|
+
if (peak instanceof Uint8Array) return `${peak.length} bytes`;
|
|
613
|
+
if (peak === null) return "null";
|
|
614
|
+
if (Array.isArray(peak)) return "an array";
|
|
615
|
+
return typeof peak;
|
|
616
|
+
}
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Decode the draft-bryce consistency
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Decode the draft-bryce consistency proofs carried under a checkpoint's
|
|
3
|
+
* verifiable-proofs UNPROTECTED header (draft-bryce label 396, key -2 =
|
|
4
|
+
* `VDP_CONSISTENCY_PROOF_KEY`).
|
|
5
|
+
*
|
|
6
|
+
* The draft's CDDL is
|
|
7
|
+
* `consistency-proofs = [ + consistency-proof ]`, with each
|
|
8
|
+
* `consistency-proof = bstr .cbor [tree-size-1, tree-size-2, paths,
|
|
9
|
+
* right-peaks]` — one or more proofs, relayed in chain order under a single
|
|
10
|
+
* signature (ADR-0066 D2). Both shapes are accepted under the -2 key:
|
|
11
|
+
*
|
|
12
|
+
* - an ARRAY of one or more proof bstrs — the draft's wire form, and the
|
|
13
|
+
* only form that can carry a relayed chain;
|
|
14
|
+
* - a BARE proof bstr — the shape every checkpoint sealed before the array
|
|
15
|
+
* form carries, and the shape the pinned `checkpoint-receipt-kat39.json`
|
|
16
|
+
* vector's `conventions.receipt` still states. It decodes to the array of
|
|
17
|
+
* one, so nothing downstream distinguishes it.
|
|
5
18
|
*
|
|
6
19
|
* Single source of truth for this decode, shared by `parseCheckpoint`
|
|
7
20
|
* (build-receipt-offline.ts, lenient: an absent or malformed proof yields a
|
|
@@ -9,8 +22,9 @@
|
|
|
9
22
|
* (checkpoint-chain.ts, full validation: an absent proof or a malformed
|
|
10
23
|
* shape throws, and the SIGNED `tree-size-2` from the protected header —
|
|
11
24
|
* read separately via `readProtectedTreeSize2`, ADR-0066 D1 as amended —
|
|
12
|
-
* must match the `tree-size-2` decoded here;
|
|
13
|
-
* prover context and is not cross-checked against
|
|
25
|
+
* must match the `tree-size-2` of the LAST proof decoded here;
|
|
26
|
+
* `tree-size-1` is unsigned prover context and is not cross-checked against
|
|
27
|
+
* a signed value).
|
|
14
28
|
*/
|
|
15
29
|
|
|
16
30
|
import {
|
|
@@ -19,7 +33,7 @@ import {
|
|
|
19
33
|
decodeCborDeterministic,
|
|
20
34
|
} from "@forestrie/encoding";
|
|
21
35
|
|
|
22
|
-
/**
|
|
36
|
+
/** One declared (unprotected, unsigned) consistency proof of a checkpoint. */
|
|
23
37
|
export type DecodedConsistencyProof = {
|
|
24
38
|
treeSize1: bigint;
|
|
25
39
|
treeSize2: bigint;
|
|
@@ -29,6 +43,21 @@ export type DecodedConsistencyProof = {
|
|
|
29
43
|
rightPeaks: Uint8Array[];
|
|
30
44
|
};
|
|
31
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The verifiable-proofs header carries the consistency-proof key with an
|
|
48
|
+
* EMPTY array. Distinct from an absent proof (no -2 key at all, which
|
|
49
|
+
* decodes to `null`): the key is present and claims to relay a chain, but
|
|
50
|
+
* the chain has no links, so there is nothing to fold and no last proof for
|
|
51
|
+
* the signed `tree-size-2` to equal. `consistency-proofs = [ + ... ]`
|
|
52
|
+
* requires at least one.
|
|
53
|
+
*/
|
|
54
|
+
export class EmptyConsistencyProofsError extends Error {
|
|
55
|
+
constructor(message: string) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "EmptyConsistencyProofsError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
function asBigint(v: unknown, what: string): bigint {
|
|
33
62
|
// Unsigned only: a negative size flows into peakMMRIndexes /
|
|
34
63
|
// consistentRootsForSizes, which reject or spin on a non-positive
|
|
@@ -56,36 +85,18 @@ function asBytesArray(v: unknown, what: string): Uint8Array[] {
|
|
|
56
85
|
return v as Uint8Array[];
|
|
57
86
|
}
|
|
58
87
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
* header map. Returns `null` when the checkpoint carries no verifiable-proofs
|
|
62
|
-
* header (396) or no consistency-proof bstr there (key -2) — an ABSENT
|
|
63
|
-
* proof, not a malformed one.
|
|
64
|
-
*
|
|
65
|
-
* @throws When a consistency-proof bstr IS present but its contents are not
|
|
66
|
-
* the shape `[tree-size-1, tree-size-2, paths, right-peaks]`, either size
|
|
67
|
-
* is not an unsigned integer, the proof does not grow the tree
|
|
68
|
-
* (`tree-size-2 <= tree-size-1`), a path element or a right-peak is not a
|
|
69
|
-
* 32-byte string — or when header 396 is present but is not map-valued,
|
|
70
|
-
* or its `-2` entry is present but not a byte string.
|
|
71
|
-
*/
|
|
72
|
-
export function decodeConsistencyProofFromUnprotected(
|
|
73
|
-
unprotected: Map<number, unknown>,
|
|
74
|
-
): DecodedConsistencyProof | null {
|
|
75
|
-
const vdpRaw = unprotected.get(COSE_LABEL_VDP);
|
|
76
|
-
if (vdpRaw === undefined || vdpRaw === null) return null;
|
|
77
|
-
if (!(vdpRaw instanceof Map)) {
|
|
78
|
-
throw new Error("checkpoint carries no verifiable-proofs header (396)");
|
|
79
|
-
}
|
|
80
|
-
const proofBstr = vdpRaw.get(VDP_CONSISTENCY_PROOF_KEY);
|
|
81
|
-
if (proofBstr === undefined || proofBstr === null) return null;
|
|
82
|
-
if (!(proofBstr instanceof Uint8Array)) {
|
|
83
|
-
throw new Error("checkpoint carries no consistency proof (vdp key -2)");
|
|
84
|
-
}
|
|
88
|
+
/** Decode one `bstr .cbor [tree-size-1, tree-size-2, paths, right-peaks]`. */
|
|
89
|
+
function decodeOneProof(proofBstr: Uint8Array): DecodedConsistencyProof {
|
|
85
90
|
const proof = decodeCborDeterministic(proofBstr);
|
|
86
|
-
|
|
91
|
+
// Exactly 4 — the draft's CDDL names a fixed-arity array, and
|
|
92
|
+
// go-merklelog's decoder rejects any other length (F2). A 5th element
|
|
93
|
+
// (e.g. another proof tuple, mistaken for a chain of two) is as malformed
|
|
94
|
+
// as a 3rd missing.
|
|
95
|
+
if (!Array.isArray(proof) || proof.length !== 4) {
|
|
87
96
|
throw new Error(
|
|
88
|
-
|
|
97
|
+
`consistency proof must be [tree-size-1, tree-size-2, paths, right-peaks] (4 elements), got ${
|
|
98
|
+
Array.isArray(proof) ? proof.length : typeof proof
|
|
99
|
+
}`,
|
|
89
100
|
);
|
|
90
101
|
}
|
|
91
102
|
const pathsRaw = proof[2];
|
|
@@ -134,3 +145,76 @@ export function decodeConsistencyProofFromUnprotected(
|
|
|
134
145
|
rightPeaks: asBytesArray(proof[3], "right-peaks"),
|
|
135
146
|
};
|
|
136
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Decode proof `at` of a relayed chain, naming its position in the message
|
|
151
|
+
* so a chain of several says which link is malformed. A header carrying a
|
|
152
|
+
* single proof names no position: its message is the one a single-proof
|
|
153
|
+
* checkpoint has always produced.
|
|
154
|
+
*/
|
|
155
|
+
function decodeProofAt(
|
|
156
|
+
proofBstr: Uint8Array,
|
|
157
|
+
at: number | null,
|
|
158
|
+
): DecodedConsistencyProof {
|
|
159
|
+
if (at === null) return decodeOneProof(proofBstr);
|
|
160
|
+
try {
|
|
161
|
+
return decodeOneProof(proofBstr);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`consistency-proofs entry ${at}: ${err instanceof Error ? err.message : String(err)}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Decode the embedded consistency proofs from a checkpoint's UNPROTECTED
|
|
171
|
+
* header map, in the order they are relayed. Returns `null` when the
|
|
172
|
+
* checkpoint carries no verifiable-proofs header (396) or no
|
|
173
|
+
* consistency-proof entry there (key -2) — an ABSENT proof, not a malformed
|
|
174
|
+
* one. A returned array always holds at least one proof.
|
|
175
|
+
*
|
|
176
|
+
* This decode establishes each proof's shape only. Nothing here relates one
|
|
177
|
+
* proof to the next, or to a signed size: the chain has to be checked
|
|
178
|
+
* against state the CALLER trusts, which is `computeCheckpointAccumulator`
|
|
179
|
+
* and `checkpointConsistencyProof` (ADR-0066 D5.4).
|
|
180
|
+
*
|
|
181
|
+
* @throws {EmptyConsistencyProofsError} when the -2 entry is an empty array
|
|
182
|
+
* @throws When a consistency proof IS present but its contents are not the
|
|
183
|
+
* shape `[tree-size-1, tree-size-2, paths, right-peaks]`, either size is
|
|
184
|
+
* not an unsigned integer, a proof does not grow the tree
|
|
185
|
+
* (`tree-size-2 <= tree-size-1`), a path element or a right-peak is not a
|
|
186
|
+
* 32-byte string — or when header 396 is present but is not map-valued,
|
|
187
|
+
* or its `-2` entry is neither a byte string nor an array of byte strings.
|
|
188
|
+
*/
|
|
189
|
+
export function decodeConsistencyProofsFromUnprotected(
|
|
190
|
+
unprotected: Map<number, unknown>,
|
|
191
|
+
): DecodedConsistencyProof[] | null {
|
|
192
|
+
const vdpRaw = unprotected.get(COSE_LABEL_VDP);
|
|
193
|
+
if (vdpRaw === undefined || vdpRaw === null) return null;
|
|
194
|
+
if (!(vdpRaw instanceof Map)) {
|
|
195
|
+
throw new Error("checkpoint carries no verifiable-proofs header (396)");
|
|
196
|
+
}
|
|
197
|
+
const entry = vdpRaw.get(VDP_CONSISTENCY_PROOF_KEY);
|
|
198
|
+
if (entry === undefined || entry === null) return null;
|
|
199
|
+
if (entry instanceof Uint8Array) {
|
|
200
|
+
// The pre-array shape: a single proof written straight under -2. It is
|
|
201
|
+
// the array of one, and is reported as such.
|
|
202
|
+
return [decodeOneProof(entry)];
|
|
203
|
+
}
|
|
204
|
+
if (!Array.isArray(entry)) {
|
|
205
|
+
throw new Error("checkpoint carries no consistency proof (vdp key -2)");
|
|
206
|
+
}
|
|
207
|
+
if (entry.length === 0) {
|
|
208
|
+
throw new EmptyConsistencyProofsError(
|
|
209
|
+
"consistency-proofs (vdp key -2) is empty; at least one proof is required",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return entry.map((proofBstr, i) => {
|
|
213
|
+
if (!(proofBstr instanceof Uint8Array)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`consistency-proofs entry ${i} is not a byte string (vdp key -2)`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return decodeProofAt(proofBstr, entry.length === 1 ? null : i);
|
|
219
|
+
});
|
|
220
|
+
}
|