@forestrie/receipt-verify 2.2.0 → 3.0.1
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.d.ts +8 -4
- package/dist/build-receipt-offline.d.ts.map +1 -1
- package/dist/build-receipt-offline.js +21 -22
- package/dist/checkpoint-chain.d.ts +105 -43
- package/dist/checkpoint-chain.d.ts.map +1 -1
- package/dist/checkpoint-chain.js +141 -53
- 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 +7 -5
- package/dist/freshen-receipt.d.ts.map +1 -1
- package/dist/freshen-receipt.js +16 -10
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/build-receipt-offline.ts +29 -24
- package/src/checkpoint-chain.ts +182 -72
- package/src/decode-checkpoint-consistency-proof.ts +118 -34
- package/src/freshen-receipt.ts +23 -15
- package/src/index.ts +5 -0
|
@@ -31,10 +31,14 @@ export type ParsedCheckpoint = {
|
|
|
31
31
|
/**
|
|
32
32
|
* Sealed tree size: the SIGNED `tree-size-2` from the checkpoint's
|
|
33
33
|
* PROTECTED header (ADR-0066 D1 as amended, label -65933), not the
|
|
34
|
-
* unprotected consistency proof's declared value. `null` when the
|
|
35
|
-
* checkpoint
|
|
36
|
-
*
|
|
37
|
-
* treat
|
|
34
|
+
* unprotected consistency proof's declared value. `null` ONLY when the
|
|
35
|
+
* checkpoint is genuinely unsealed — no embedded consistency proof, or a
|
|
36
|
+
* well-formed protected header with no signed tree-size-2 label — and
|
|
37
|
+
* callers must treat that the same as a checkpoint with no proof at all.
|
|
38
|
+
* A structurally malformed consistency proof, or a protected header that
|
|
39
|
+
* is not deterministically encoded (ADR-0066 D9), is a DIFFERENT
|
|
40
|
+
* condition — {@link parseCheckpoint} throws for those rather than
|
|
41
|
+
* folding them into this `null` (review finding O3).
|
|
38
42
|
*/
|
|
39
43
|
mmrSize: bigint | null;
|
|
40
44
|
delegationCert: Uint8Array | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-receipt-offline.d.ts","sourceRoot":"","sources":["../src/build-receipt-offline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,OAAO,EAIL,mBAAmB,EAEnB,KAAK,eAAe,EAErB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAO5B;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAC/B,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,oEAAoE;IACpE,YAAY,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC/B
|
|
1
|
+
{"version":3,"file":"build-receipt-offline.d.ts","sourceRoot":"","sources":["../src/build-receipt-offline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,OAAO,EAIL,mBAAmB,EAEnB,KAAK,eAAe,EAErB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAO5B;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAC/B,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,oEAAoE;IACpE,YAAY,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC/B;;;;;;;;;;;OAWG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,UAAU,GAAG,IAAI,CAAC;CACnC,CAAC;AAEF,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,eAAe,EAAE,UAAU,GAAG,gBAAgB,CAqB7E;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,WAAW,EAAE,UAAU,CAAC;IACxB,eAAe,EAAE,UAAU,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,wBAAwB,GAC9B,UAAU,CAyCZ;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,gBAAgB,EAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EAAE,GAClB,UAAU,CA4CZ;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,mEAAmE;IACnE,IAAI,EAAE,UAAU,CAAC;IACjB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,SAAS,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,WAAW,EAAE,UAAU,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAwBnC"}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { COSE_LABEL_PEAK_RECEIPTS, COSE_LABEL_VDP, decodeCborDeterministic, encodeCborDeterministic, readProtectedTreeSize2, } from "@forestrie/encoding";
|
|
18
18
|
import { calculateRoot, inclusionProof, massifIndexFromMMRIndex, openMassifNodeStore, peakIndexForLeafProof, } from "@forestrie/merklelog";
|
|
19
19
|
import { requireCoseSign1, toHeaderMap, unwrapCoseSign1Tag, } from "./parse-receipt.js";
|
|
20
|
-
import {
|
|
20
|
+
import { decodeConsistencyProofsFromUnprotected } from "./decode-checkpoint-consistency-proof.js";
|
|
21
21
|
import { SubtleHasher } from "./subtle-hasher.js";
|
|
22
22
|
/** Not in scope for the shared cose-labels module (FOR-568 §4.1). */
|
|
23
23
|
const DELEGATION_CERT_LABEL = 1000;
|
|
@@ -158,30 +158,29 @@ function cborBytes(value) {
|
|
|
158
158
|
* (ADR-0066 D1 as amended, label -65933) from the PROTECTED header — not the
|
|
159
159
|
* unprotected consistency proof's declared value, which a checkpoint without
|
|
160
160
|
* a signing key could freely restate (ADR-0066's "keyless first checkpoint"
|
|
161
|
-
* case).
|
|
162
|
-
* with no proof is not sealed at all)
|
|
163
|
-
*
|
|
164
|
-
*
|
|
161
|
+
* case). At least one consistency proof must still be present (an unsigned
|
|
162
|
+
* checkpoint with no proof is not sealed at all) — one or more, since a
|
|
163
|
+
* checkpoint may relay a chain of sealed steps under one signature
|
|
164
|
+
* (ADR-0066 D2). Their declared sizes are not otherwise used here;
|
|
165
|
+
* {@link checkpointConsistencyProof} in `checkpoint-chain.ts` is the
|
|
166
|
+
* validating decode that requires the signed size and the last relayed
|
|
167
|
+
* proof's to agree.
|
|
165
168
|
*
|
|
166
|
-
* Lenient
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
169
|
+
* Lenient only for an ABSENT proof or an absent signed tree-size-2: both
|
|
170
|
+
* yield `null`, so a caller of {@link parseCheckpoint} sees the same "not
|
|
171
|
+
* verifiable" outcome for either. A proof or protected header that IS
|
|
172
|
+
* present but malformed is a different condition and is not swallowed here
|
|
173
|
+
* — {@link decodeConsistencyProofsFromUnprotected} throws for a structurally
|
|
174
|
+
* malformed proof (including an empty consistency-proofs array), and
|
|
175
|
+
* {@link readProtectedTreeSize2} throws for a protected header that is not
|
|
176
|
+
* deterministically encoded (ADR-0066 D9); both propagate rather than
|
|
177
|
+
* folding into `null` (review finding O3: a D9 conformance failure is a
|
|
178
|
+
* sealer-side defect worth attributing, not the ordinary "no proof yet"
|
|
179
|
+
* case).
|
|
170
180
|
*/
|
|
171
181
|
function sealedSizeFromCheckpoint(coseSign1, unprotected) {
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
declared = decodeConsistencyProofFromUnprotected(unprotected);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
182
|
+
const declared = decodeConsistencyProofsFromUnprotected(unprotected);
|
|
179
183
|
if (declared === null)
|
|
180
184
|
return null;
|
|
181
|
-
|
|
182
|
-
return readProtectedTreeSize2(coseSign1[0]);
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
185
|
+
return readProtectedTreeSize2(coseSign1[0]);
|
|
187
186
|
}
|
|
@@ -1,19 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
1
|
+
import { EmptyConsistencyProofsError, type DecodedConsistencyProof } from "./decode-checkpoint-consistency-proof.js";
|
|
2
|
+
/**
|
|
3
|
+
* The draft-bryce consistency proofs embedded in a v3 checkpoint: one or
|
|
4
|
+
* more, in relay order (`consistency-proofs = [ + consistency-proof ]`,
|
|
5
|
+
* ADR-0066 D2). A checkpoint sealing a single step carries the chain of
|
|
6
|
+
* one; there is no separate single-proof shape.
|
|
7
|
+
*
|
|
8
|
+
* {@link treeSize2} — the LAST proof's — is cross-checked against the
|
|
9
|
+
* checkpoint's SIGNED tree-size-2 (ADR-0066 D1 as amended, D5.5).
|
|
10
|
+
* {@link treeSize1} — the FIRST proof's — is unsigned prover context, not
|
|
11
|
+
* cross-checked here; see {@link verifyCheckpointChain}, which compares it
|
|
12
|
+
* with the trusted origin instead. The sizes between the two are named by
|
|
13
|
+
* no signature: {@link computeCheckpointAccumulator} holds the chain
|
|
14
|
+
* together by requiring each proof to continue the one before it.
|
|
15
|
+
*/
|
|
6
16
|
export type CheckpointConsistencyProof = {
|
|
17
|
+
/** The relayed proofs, in chain order; never empty. */
|
|
18
|
+
proofs: DecodedConsistencyProof[];
|
|
19
|
+
/** `tree-size-1` of the FIRST proof: the size the chain continues from. */
|
|
7
20
|
treeSize1: bigint;
|
|
21
|
+
/** `tree-size-2` of the LAST proof: the size the chain reaches. */
|
|
8
22
|
treeSize2: bigint;
|
|
9
23
|
/** Signed `tree-size-2` (protected header label -65933); equal to
|
|
10
24
|
* {@link treeSize2} — {@link checkpointConsistencyProof} enforces this. */
|
|
11
25
|
signedTreeSize2: bigint;
|
|
12
|
-
/** One inclusion path per tree-size-1 peak, proven at tree-size-2. */
|
|
13
|
-
paths: Uint8Array[][];
|
|
14
|
-
/** New peaks not covered by the proven roots (draft `right-peaks`). */
|
|
15
|
-
rightPeaks: Uint8Array[];
|
|
16
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* A relayed consistency-proof chain does not join up: a proof's declared
|
|
29
|
+
* `tree-size-1` is not the size the fold has reached — the caller's trusted
|
|
30
|
+
* size for the first proof, the previous proof's `tree-size-2` after that —
|
|
31
|
+
* or, having applied every proof, the fold reaches a size other than the
|
|
32
|
+
* chain link's own declared `tree-size-2` (F3: a caller-assembled link —
|
|
33
|
+
* `freshenReceipt` callers build these from on-chain calldata, never
|
|
34
|
+
* through {@link checkpointConsistencyProof} — can set `proofs` and
|
|
35
|
+
* `treeSize2` independently, which `checkpointConsistencyProof` itself
|
|
36
|
+
* never allows to disagree). go-merklelog reuses its equivalent
|
|
37
|
+
* `ErrProofChainNotContiguous` for this same end-of-chain comparison
|
|
38
|
+
* (`checkpointverify.go:245-251`, folded size vs. signed size). Reported as
|
|
39
|
+
* `"size_mismatch"` by {@link verifyCheckpointChain}, the same as any other
|
|
40
|
+
* size disagreement, because the sizes it names are unsigned (ADR-0066 D2):
|
|
41
|
+
* nothing distinguishes a relay assembled in the wrong order from one
|
|
42
|
+
* assembled over a different log.
|
|
43
|
+
*/
|
|
44
|
+
export declare class ConsistencyChainNotContiguousError extends Error {
|
|
45
|
+
constructor(message: string);
|
|
46
|
+
}
|
|
47
|
+
export { EmptyConsistencyProofsError };
|
|
17
48
|
/**
|
|
18
49
|
* The checkpoint's SIGNED `tree-size-2` (protected header, ADR-0066 D1 as
|
|
19
50
|
* amended) differs from the declared `tree-size-2` of its embedded
|
|
@@ -55,12 +86,14 @@ export declare class CheckpointProtectedHeaderAlgError extends Error {
|
|
|
55
86
|
constructor(message: string);
|
|
56
87
|
}
|
|
57
88
|
/**
|
|
58
|
-
* Decode the embedded consistency
|
|
59
|
-
*
|
|
60
|
-
* the
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
89
|
+
* Decode the embedded consistency proofs (`vdp` 396 key -2, one or more in
|
|
90
|
+
* relay order) and require the LAST proof's declared `tree-size-2` to equal
|
|
91
|
+
* the checkpoint's SIGNED `tree-size-2` from the protected header (ADR-0066
|
|
92
|
+
* D1 as amended, D2, D5.5, label -65933). The earlier proofs' sizes are not
|
|
93
|
+
* signed: the fold checks them against each other
|
|
94
|
+
* ({@link computeCheckpointAccumulator}). `tree-size-1` is not signed
|
|
95
|
+
* either; see {@link verifyCheckpointChain} for its comparison against the
|
|
96
|
+
* trusted origin.
|
|
64
97
|
*
|
|
65
98
|
* Also rejects a malleable high-s ES256 signature (see
|
|
66
99
|
* {@link CheckpointHighSSignatureError}) before any fold or WebCrypto verify
|
|
@@ -68,12 +101,14 @@ export declare class CheckpointProtectedHeaderAlgError extends Error {
|
|
|
68
101
|
* subject to this check, since it does not go through the P-256 WebCrypto
|
|
69
102
|
* path this guards.
|
|
70
103
|
*
|
|
71
|
-
* @throws {Error} when the
|
|
72
|
-
*
|
|
73
|
-
* {@link
|
|
104
|
+
* @throws {Error} when the unprotected header carries no consistency proof,
|
|
105
|
+
* a proof is structurally malformed (see
|
|
106
|
+
* {@link decodeConsistencyProofsFromUnprotected}), or the protected header
|
|
74
107
|
* carries no signed tree-size-2 label
|
|
108
|
+
* @throws {EmptyConsistencyProofsError} when the consistency-proofs array is
|
|
109
|
+
* present but empty
|
|
75
110
|
* @throws {CheckpointSignedSizeMismatchError} when the signed tree-size-2
|
|
76
|
-
* differs from the declared proof's tree-size-2
|
|
111
|
+
* differs from the LAST declared proof's tree-size-2
|
|
77
112
|
* @throws {CheckpointHighSSignatureError} when the checkpoint is ES256-signed
|
|
78
113
|
* with a high-s (malleable) signature
|
|
79
114
|
* @throws {CheckpointProtectedHeaderAlgError} when the protected header
|
|
@@ -81,25 +116,45 @@ export declare class CheckpointProtectedHeaderAlgError extends Error {
|
|
|
81
116
|
*/
|
|
82
117
|
export declare function checkpointConsistencyProof(checkpointBytes: Uint8Array): CheckpointConsistencyProof;
|
|
83
118
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
119
|
+
* Fold a checkpoint's relayed consistency proofs, in order, from the
|
|
120
|
+
* CALLER-TRUSTED accumulator at `sizeFrom` to the accumulator at the last
|
|
121
|
+
* proof's `tree-size-2` — the value the checkpoint's signature covers.
|
|
122
|
+
*
|
|
123
|
+
* Each proof is applied by the size-driven {@link consistentRootsForSizes}
|
|
124
|
+
* (ADR-0066 D5), which yields `roots` (the proven prefix); the proof's own
|
|
125
|
+
* right-peaks (the target peaks no path reaches) complete that step's
|
|
126
|
+
* accumulator, and it becomes the next step's input. A checkpoint sealing
|
|
127
|
+
* one step carries the chain of one and runs the same loop once.
|
|
128
|
+
*
|
|
129
|
+
* `sizeFrom` is a parameter, not read off the proofs, because the fold must
|
|
130
|
+
* start from a size the CALLER already trusts (the previous checkpoint's
|
|
131
|
+
* verified `treeSize2`, or the caller's anchor for a first link) — reading
|
|
132
|
+
* it from the relay instead would let an unsigned or substituted proof
|
|
133
|
+
* dictate its own starting point (ADR-0066 D5.4). Every proof after the
|
|
134
|
+
* first is held to the size the previous one reached for the same reason:
|
|
135
|
+
* only the last step's size is signed, so the intermediate sizes are worth
|
|
136
|
+
* no more than their agreement with each other.
|
|
89
137
|
*
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
* `
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
138
|
+
* `proof.proofs` must hold at least one step and, once every step has been
|
|
139
|
+
* applied, the fold must have reached exactly `proof.treeSize2` (F3).
|
|
140
|
+
* `checkpointConsistencyProof` already guarantees both — the decode rejects
|
|
141
|
+
* an empty `consistency-proofs` array (`EmptyConsistencyProofsError`) and
|
|
142
|
+
* sets `treeSize2` from the last decoded proof — but a caller-assembled
|
|
143
|
+
* link (`freshenReceipt` callers building from on-chain calldata) is not
|
|
144
|
+
* decoded through it, so both are re-checked here rather than trusted from
|
|
145
|
+
* the type.
|
|
97
146
|
*
|
|
98
|
-
* @throws {
|
|
99
|
-
*
|
|
147
|
+
* @throws {EmptyConsistencyProofsError} when `proof.proofs` is empty — an
|
|
148
|
+
* already-decoded link the caller assembled themselves rather than one
|
|
149
|
+
* `checkpointConsistencyProof` produced, which never returns one
|
|
150
|
+
* @throws {ConsistencyChainNotContiguousError} when the first proof's
|
|
151
|
+
* `treeSize1` is not `sizeFrom`, a later proof's `treeSize1` is not the
|
|
152
|
+
* previous proof's `treeSize2`, or the size the fold reaches after every
|
|
153
|
+
* proof is not `proof.treeSize2`
|
|
154
|
+
* @throws {Error} when a proof supplies a right-peaks count other than
|
|
100
155
|
* {@link consistentRootsForSizes}'s `expectedRight`
|
|
101
|
-
* @throws {ConsistencyShapeError} (`@forestrie/merklelog`) when
|
|
102
|
-
*
|
|
156
|
+
* @throws {ConsistencyShapeError} (`@forestrie/merklelog`) when a proof does
|
|
157
|
+
* not have the shape its two sizes imply
|
|
103
158
|
*/
|
|
104
159
|
export declare function computeCheckpointAccumulator(proof: CheckpointConsistencyProof, accumulatorFrom: Uint8Array[], sizeFrom: bigint): Promise<Uint8Array[]>;
|
|
105
160
|
/** Detached payload the checkpoint signature covers (ADR-0046): the raw
|
|
@@ -135,8 +190,10 @@ export type CheckpointChainResult = {
|
|
|
135
190
|
* `tree-size-2` (ADR-0066 D1 as amended, D5.5), or a link's declared
|
|
136
191
|
* `tree-size-1` disagrees with the size the fold starts from — the
|
|
137
192
|
* caller's `trustedBase.size` (0 with no `trustedBase`) for the
|
|
138
|
-
* first link, the previous link's `tree-size-2` after that
|
|
139
|
-
*
|
|
193
|
+
* first link, the previous link's `tree-size-2` after that — or a
|
|
194
|
+
* relayed proof WITHIN a checkpoint disagrees with the size the
|
|
195
|
+
* proof before it reached ({@link
|
|
196
|
+
* ConsistencyChainNotContiguousError}). `detail` names both sizes.
|
|
140
197
|
*/
|
|
141
198
|
| "size_mismatch";
|
|
142
199
|
/** Index of the offending checkpoint. */
|
|
@@ -164,15 +221,20 @@ export type CheckpointChainResult = {
|
|
|
164
221
|
* - The first link's declared `tree-size-1` must equal that trusted
|
|
165
222
|
* starting size, and every subsequent link's must equal the previous
|
|
166
223
|
* link's sealed `tree-size-2`; either disagreement is `size_mismatch`.
|
|
167
|
-
* `tree-size-1` itself is never compared with a signed value (
|
|
168
|
-
*
|
|
224
|
+
* `tree-size-1` itself is never compared with a signed value (the signed
|
|
225
|
+
* origin ADR-0066 D2 first proposed was withdrawn): only this
|
|
226
|
+
* trusted-origin comparison applies. Because it is
|
|
169
227
|
* unsigned, the reason it produces carries no more meaning than the size
|
|
170
228
|
* disagreement itself (ADR-0066 D6: no pre-FOR-410 state is supported, so
|
|
171
229
|
* there is no drift condition to fall back from).
|
|
172
|
-
* -
|
|
173
|
-
*
|
|
174
|
-
* (
|
|
175
|
-
* `
|
|
230
|
+
* - A checkpoint may relay SEVERAL consistency proofs under one signature
|
|
231
|
+
* (ADR-0066 D2; draft `consistency-proofs = [ + consistency-proof ]`).
|
|
232
|
+
* Its SIGNED `tree-size-2` (ADR-0066 D1 as amended) must equal the LAST
|
|
233
|
+
* proof's declared `tree-size-2` ({@link checkpointConsistencyProof}),
|
|
234
|
+
* and each relayed proof must continue the one before it
|
|
235
|
+
* ({@link computeCheckpointAccumulator}); either disagreement is
|
|
236
|
+
* `size_mismatch`. A checkpoint sealing one step is the relay of one and
|
|
237
|
+
* takes the same path.
|
|
176
238
|
* - Each link's signature is checked over its computed accumulator via
|
|
177
239
|
* the injected verifier (the caller owns trust resolution — genesis
|
|
178
240
|
* roots, caller-known keys, or the label-1000 delegation path). A link
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint-chain.d.ts","sourceRoot":"","sources":["../src/checkpoint-chain.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"checkpoint-chain.d.ts","sourceRoot":"","sources":["../src/checkpoint-chain.ts"],"names":[],"mappings":"AA2DA,OAAO,EAEL,2BAA2B,EAC3B,KAAK,uBAAuB,EAC7B,MAAM,0CAA0C,CAAC;AAElD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,uDAAuD;IACvD,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAClC,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB;+EAC2E;IAC3E,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,kCAAmC,SAAQ,KAAK;gBAC/C,OAAO,EAAE,MAAM;CAI5B;AAED,OAAO,EAAE,2BAA2B,EAAE,CAAC;AAEvC;;;;;;GAMG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,UAAU,GAC1B,0BAA0B,CAgD5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,0BAA0B,EACjC,eAAe,EAAE,UAAU,EAAE,EAC7B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,EAAE,CAAC,CA+CvB;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;mBACe;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,2EAA2E;IAC3E,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,WAAW;IACb;;;;;OAKG;OACD,qBAAqB,GACrB,iBAAiB;IACnB;;;;;;;;;;OAUG;OACD,eAAe,CAAC;IACpB,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B,CAAC;AAEN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;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;+EAC2E;IAC3E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;CAC3D,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA6JjC"}
|
package/dist/checkpoint-chain.js
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
|
|
@@ -42,7 +46,31 @@ import { COSE_ALG_ES256, ProtectedHeaderAlgError, isLowS, readProtectedAlg, read
|
|
|
42
46
|
import { consistentRootsForSizes, mmrSizeForLeafCount, peakMMRIndexes, peaksBitmap, } from "@forestrie/merklelog";
|
|
43
47
|
import { SubtleHasher } from "./subtle-hasher.js";
|
|
44
48
|
import { parseCheckpoint } from "./build-receipt-offline.js";
|
|
45
|
-
import {
|
|
49
|
+
import { decodeConsistencyProofsFromUnprotected, EmptyConsistencyProofsError, } from "./decode-checkpoint-consistency-proof.js";
|
|
50
|
+
/**
|
|
51
|
+
* A relayed consistency-proof chain does not join up: a proof's declared
|
|
52
|
+
* `tree-size-1` is not the size the fold has reached — the caller's trusted
|
|
53
|
+
* size for the first proof, the previous proof's `tree-size-2` after that —
|
|
54
|
+
* or, having applied every proof, the fold reaches a size other than the
|
|
55
|
+
* chain link's own declared `tree-size-2` (F3: a caller-assembled link —
|
|
56
|
+
* `freshenReceipt` callers build these from on-chain calldata, never
|
|
57
|
+
* through {@link checkpointConsistencyProof} — can set `proofs` and
|
|
58
|
+
* `treeSize2` independently, which `checkpointConsistencyProof` itself
|
|
59
|
+
* never allows to disagree). go-merklelog reuses its equivalent
|
|
60
|
+
* `ErrProofChainNotContiguous` for this same end-of-chain comparison
|
|
61
|
+
* (`checkpointverify.go:245-251`, folded size vs. signed size). Reported as
|
|
62
|
+
* `"size_mismatch"` by {@link verifyCheckpointChain}, the same as any other
|
|
63
|
+
* size disagreement, because the sizes it names are unsigned (ADR-0066 D2):
|
|
64
|
+
* nothing distinguishes a relay assembled in the wrong order from one
|
|
65
|
+
* assembled over a different log.
|
|
66
|
+
*/
|
|
67
|
+
export class ConsistencyChainNotContiguousError extends Error {
|
|
68
|
+
constructor(message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "ConsistencyChainNotContiguousError";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export { EmptyConsistencyProofsError };
|
|
46
74
|
/**
|
|
47
75
|
* The checkpoint's SIGNED `tree-size-2` (protected header, ADR-0066 D1 as
|
|
48
76
|
* amended) differs from the declared `tree-size-2` of its embedded
|
|
@@ -93,12 +121,14 @@ export class CheckpointProtectedHeaderAlgError extends Error {
|
|
|
93
121
|
}
|
|
94
122
|
}
|
|
95
123
|
/**
|
|
96
|
-
* Decode the embedded consistency
|
|
97
|
-
*
|
|
98
|
-
* the
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
124
|
+
* Decode the embedded consistency proofs (`vdp` 396 key -2, one or more in
|
|
125
|
+
* relay order) and require the LAST proof's declared `tree-size-2` to equal
|
|
126
|
+
* the checkpoint's SIGNED `tree-size-2` from the protected header (ADR-0066
|
|
127
|
+
* D1 as amended, D2, D5.5, label -65933). The earlier proofs' sizes are not
|
|
128
|
+
* signed: the fold checks them against each other
|
|
129
|
+
* ({@link computeCheckpointAccumulator}). `tree-size-1` is not signed
|
|
130
|
+
* either; see {@link verifyCheckpointChain} for its comparison against the
|
|
131
|
+
* trusted origin.
|
|
102
132
|
*
|
|
103
133
|
* Also rejects a malleable high-s ES256 signature (see
|
|
104
134
|
* {@link CheckpointHighSSignatureError}) before any fold or WebCrypto verify
|
|
@@ -106,12 +136,14 @@ export class CheckpointProtectedHeaderAlgError extends Error {
|
|
|
106
136
|
* subject to this check, since it does not go through the P-256 WebCrypto
|
|
107
137
|
* path this guards.
|
|
108
138
|
*
|
|
109
|
-
* @throws {Error} when the
|
|
110
|
-
*
|
|
111
|
-
* {@link
|
|
139
|
+
* @throws {Error} when the unprotected header carries no consistency proof,
|
|
140
|
+
* a proof is structurally malformed (see
|
|
141
|
+
* {@link decodeConsistencyProofsFromUnprotected}), or the protected header
|
|
112
142
|
* carries no signed tree-size-2 label
|
|
143
|
+
* @throws {EmptyConsistencyProofsError} when the consistency-proofs array is
|
|
144
|
+
* present but empty
|
|
113
145
|
* @throws {CheckpointSignedSizeMismatchError} when the signed tree-size-2
|
|
114
|
-
* differs from the declared proof's tree-size-2
|
|
146
|
+
* differs from the LAST declared proof's tree-size-2
|
|
115
147
|
* @throws {CheckpointHighSSignatureError} when the checkpoint is ES256-signed
|
|
116
148
|
* with a high-s (malleable) signature
|
|
117
149
|
* @throws {CheckpointProtectedHeaderAlgError} when the protected header
|
|
@@ -137,56 +169,102 @@ export function checkpointConsistencyProof(checkpointBytes) {
|
|
|
137
169
|
throw new CheckpointHighSSignatureError("checkpoint ES256 signature is not low-s canonical (s > n/2); rejected " +
|
|
138
170
|
"to match the univocity contract's P-256 verifier and go-merklelog");
|
|
139
171
|
}
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
172
|
+
const proofs = decodeConsistencyProofsFromUnprotected(unprotected);
|
|
173
|
+
if (proofs === null) {
|
|
142
174
|
throw new Error("checkpoint carries no consistency proof (vdp key -2)");
|
|
143
175
|
}
|
|
176
|
+
const last = proofs[proofs.length - 1];
|
|
144
177
|
const signedTreeSize2 = readProtectedTreeSize2(coseSign1[0]);
|
|
145
178
|
if (signedTreeSize2 === null) {
|
|
146
179
|
throw new Error("checkpoint protected header carries no signed tree-size-2 (-65933)");
|
|
147
180
|
}
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
// The signature covers the size the LAST proof reaches, and only that
|
|
182
|
+
// size: a relay may hold any number of steps before it, none of them
|
|
183
|
+
// signed (ADR-0066 D2).
|
|
184
|
+
if (signedTreeSize2 !== last.treeSize2) {
|
|
185
|
+
throw new CheckpointSignedSizeMismatchError(`signed tree-size-2 (-65933) ${signedTreeSize2} != declared consistency-proof tree-size-2 ${last.treeSize2}`);
|
|
150
186
|
}
|
|
151
187
|
return {
|
|
152
|
-
|
|
153
|
-
|
|
188
|
+
proofs,
|
|
189
|
+
treeSize1: proofs[0].treeSize1,
|
|
190
|
+
treeSize2: last.treeSize2,
|
|
154
191
|
signedTreeSize2,
|
|
155
|
-
paths: declared.paths,
|
|
156
|
-
rightPeaks: declared.rightPeaks,
|
|
157
192
|
};
|
|
158
193
|
}
|
|
159
194
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* prefix) followed by the proof's supplied right-peaks (the target peaks no
|
|
164
|
-
* path reaches).
|
|
195
|
+
* Fold a checkpoint's relayed consistency proofs, in order, from the
|
|
196
|
+
* CALLER-TRUSTED accumulator at `sizeFrom` to the accumulator at the last
|
|
197
|
+
* proof's `tree-size-2` — the value the checkpoint's signature covers.
|
|
165
198
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* disagreeing means this proof does not continue from the state being
|
|
172
|
-
* folded, not a mere shape defect, so it is checked before any fold work.
|
|
199
|
+
* Each proof is applied by the size-driven {@link consistentRootsForSizes}
|
|
200
|
+
* (ADR-0066 D5), which yields `roots` (the proven prefix); the proof's own
|
|
201
|
+
* right-peaks (the target peaks no path reaches) complete that step's
|
|
202
|
+
* accumulator, and it becomes the next step's input. A checkpoint sealing
|
|
203
|
+
* one step carries the chain of one and runs the same loop once.
|
|
173
204
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
205
|
+
* `sizeFrom` is a parameter, not read off the proofs, because the fold must
|
|
206
|
+
* start from a size the CALLER already trusts (the previous checkpoint's
|
|
207
|
+
* verified `treeSize2`, or the caller's anchor for a first link) — reading
|
|
208
|
+
* it from the relay instead would let an unsigned or substituted proof
|
|
209
|
+
* dictate its own starting point (ADR-0066 D5.4). Every proof after the
|
|
210
|
+
* first is held to the size the previous one reached for the same reason:
|
|
211
|
+
* only the last step's size is signed, so the intermediate sizes are worth
|
|
212
|
+
* no more than their agreement with each other.
|
|
213
|
+
*
|
|
214
|
+
* `proof.proofs` must hold at least one step and, once every step has been
|
|
215
|
+
* applied, the fold must have reached exactly `proof.treeSize2` (F3).
|
|
216
|
+
* `checkpointConsistencyProof` already guarantees both — the decode rejects
|
|
217
|
+
* an empty `consistency-proofs` array (`EmptyConsistencyProofsError`) and
|
|
218
|
+
* sets `treeSize2` from the last decoded proof — but a caller-assembled
|
|
219
|
+
* link (`freshenReceipt` callers building from on-chain calldata) is not
|
|
220
|
+
* decoded through it, so both are re-checked here rather than trusted from
|
|
221
|
+
* the type.
|
|
222
|
+
*
|
|
223
|
+
* @throws {EmptyConsistencyProofsError} when `proof.proofs` is empty — an
|
|
224
|
+
* already-decoded link the caller assembled themselves rather than one
|
|
225
|
+
* `checkpointConsistencyProof` produced, which never returns one
|
|
226
|
+
* @throws {ConsistencyChainNotContiguousError} when the first proof's
|
|
227
|
+
* `treeSize1` is not `sizeFrom`, a later proof's `treeSize1` is not the
|
|
228
|
+
* previous proof's `treeSize2`, or the size the fold reaches after every
|
|
229
|
+
* proof is not `proof.treeSize2`
|
|
230
|
+
* @throws {Error} when a proof supplies a right-peaks count other than
|
|
176
231
|
* {@link consistentRootsForSizes}'s `expectedRight`
|
|
177
|
-
* @throws {ConsistencyShapeError} (`@forestrie/merklelog`) when
|
|
178
|
-
*
|
|
232
|
+
* @throws {ConsistencyShapeError} (`@forestrie/merklelog`) when a proof does
|
|
233
|
+
* not have the shape its two sizes imply
|
|
179
234
|
*/
|
|
180
235
|
export async function computeCheckpointAccumulator(proof, accumulatorFrom, sizeFrom) {
|
|
181
|
-
if (proof.
|
|
182
|
-
throw new
|
|
236
|
+
if (proof.proofs.length === 0) {
|
|
237
|
+
throw new EmptyConsistencyProofsError("consistency proof relays no proofs (empty proofs array); at least " +
|
|
238
|
+
"one is required to fold");
|
|
183
239
|
}
|
|
184
240
|
const hasher = new SubtleHasher();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
241
|
+
let accumulator = accumulatorFrom;
|
|
242
|
+
let size = sizeFrom;
|
|
243
|
+
for (let i = 0; i < proof.proofs.length; i++) {
|
|
244
|
+
const step = proof.proofs[i];
|
|
245
|
+
if (step.treeSize1 !== size) {
|
|
246
|
+
throw new ConsistencyChainNotContiguousError(i === 0
|
|
247
|
+
? `consistency proof base tree-size-1 ${step.treeSize1} does not match the trusted size ${size}`
|
|
248
|
+
: `consistency-proofs entry ${i} declares tree-size-1 ${step.treeSize1}; the previous proof reached ${size}`);
|
|
249
|
+
}
|
|
250
|
+
const { roots, expectedRight } = await consistentRootsForSizes(hasher, size, step.treeSize2, accumulator, step.paths);
|
|
251
|
+
if (step.rightPeaks.length !== expectedRight) {
|
|
252
|
+
throw new Error(`checkpoint supplies ${step.rightPeaks.length} right-peaks; size ${step.treeSize2} requires ${expectedRight}`);
|
|
253
|
+
}
|
|
254
|
+
accumulator = [...roots, ...step.rightPeaks];
|
|
255
|
+
size = step.treeSize2;
|
|
256
|
+
}
|
|
257
|
+
// The fold must land exactly on the size the LINK itself declares —
|
|
258
|
+
// `proof.treeSize2` — not merely on whatever size its last proof happened
|
|
259
|
+
// to reach: a caller-assembled link can set the two independently (e.g.
|
|
260
|
+
// proofs folding 1 -> 3 alongside a declared treeSize2 of 7), which would
|
|
261
|
+
// otherwise fold to 3 and be reported as size 7 to every downstream
|
|
262
|
+
// caller reading the declared field instead of the fold. Mirrors
|
|
263
|
+
// go-merklelog's own end-of-chain check (checkpointverify.go:245-251).
|
|
264
|
+
if (size !== proof.treeSize2) {
|
|
265
|
+
throw new ConsistencyChainNotContiguousError(`consistency proof folds to tree-size-2 ${size}; the chain's declared tree-size-2 is ${proof.treeSize2}`);
|
|
188
266
|
}
|
|
189
|
-
return
|
|
267
|
+
return accumulator;
|
|
190
268
|
}
|
|
191
269
|
/** Detached payload the checkpoint signature covers (ADR-0046): the raw
|
|
192
270
|
* concatenation of the accumulator peaks in contract order. */
|
|
@@ -219,15 +297,20 @@ export function accumulatorPayload(accumulator) {
|
|
|
219
297
|
* - The first link's declared `tree-size-1` must equal that trusted
|
|
220
298
|
* starting size, and every subsequent link's must equal the previous
|
|
221
299
|
* link's sealed `tree-size-2`; either disagreement is `size_mismatch`.
|
|
222
|
-
* `tree-size-1` itself is never compared with a signed value (
|
|
223
|
-
*
|
|
300
|
+
* `tree-size-1` itself is never compared with a signed value (the signed
|
|
301
|
+
* origin ADR-0066 D2 first proposed was withdrawn): only this
|
|
302
|
+
* trusted-origin comparison applies. Because it is
|
|
224
303
|
* unsigned, the reason it produces carries no more meaning than the size
|
|
225
304
|
* disagreement itself (ADR-0066 D6: no pre-FOR-410 state is supported, so
|
|
226
305
|
* there is no drift condition to fall back from).
|
|
227
|
-
* -
|
|
228
|
-
*
|
|
229
|
-
* (
|
|
230
|
-
* `
|
|
306
|
+
* - A checkpoint may relay SEVERAL consistency proofs under one signature
|
|
307
|
+
* (ADR-0066 D2; draft `consistency-proofs = [ + consistency-proof ]`).
|
|
308
|
+
* Its SIGNED `tree-size-2` (ADR-0066 D1 as amended) must equal the LAST
|
|
309
|
+
* proof's declared `tree-size-2` ({@link checkpointConsistencyProof}),
|
|
310
|
+
* and each relayed proof must continue the one before it
|
|
311
|
+
* ({@link computeCheckpointAccumulator}); either disagreement is
|
|
312
|
+
* `size_mismatch`. A checkpoint sealing one step is the relay of one and
|
|
313
|
+
* takes the same path.
|
|
231
314
|
* - Each link's signature is checked over its computed accumulator via
|
|
232
315
|
* the injected verifier (the caller owns trust resolution — genesis
|
|
233
316
|
* roots, caller-known keys, or the label-1000 delegation path). A link
|
|
@@ -343,9 +426,14 @@ export async function verifyCheckpointChain(opts) {
|
|
|
343
426
|
computed = await computeCheckpointAccumulator(proof, accumulator, expectedBase);
|
|
344
427
|
}
|
|
345
428
|
catch (err) {
|
|
429
|
+
// A relay that does not join up is a size disagreement like any
|
|
430
|
+
// other: every size it names but the last is unsigned, so the reason
|
|
431
|
+
// can say no more than that two sizes differ.
|
|
346
432
|
return {
|
|
347
433
|
ok: false,
|
|
348
|
-
reason:
|
|
434
|
+
reason: err instanceof ConsistencyChainNotContiguousError
|
|
435
|
+
? "size_mismatch"
|
|
436
|
+
: "proof_malformed",
|
|
349
437
|
at: i,
|
|
350
438
|
detail: err instanceof Error ? err.message : String(err),
|
|
351
439
|
links,
|