@forestrie/receipt-verify 2.2.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.
@@ -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 { decodeConsistencyProofFromUnprotected } from "./decode-checkpoint-consistency-proof.js";
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,20 +158,24 @@ 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). A consistency proof must still be present (an unsigned checkpoint
162
- * with no proof is not sealed at all); its declared sizes are not otherwise
163
- * used here — {@link checkpointConsistencyProof} in `checkpoint-chain.ts`
164
- * is the validating decode that requires the two to agree.
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 by design: an absent proof, a malformed proof, or an absent or
167
- * malformed signed tree-size-2 all yield `null` rather than a throw, so a
168
- * caller of {@link parseCheckpoint} sees exactly the same "not verifiable"
169
- * outcome either way.
169
+ * Lenient by design: an absent proof, an empty or malformed
170
+ * consistency-proofs array, or an absent or malformed signed tree-size-2
171
+ * all yield `null` rather than a throw, so a caller of
172
+ * {@link parseCheckpoint} sees exactly the same "not verifiable" outcome
173
+ * either way.
170
174
  */
171
175
  function sealedSizeFromCheckpoint(coseSign1, unprotected) {
172
176
  let declared;
173
177
  try {
174
- declared = decodeConsistencyProofFromUnprotected(unprotected);
178
+ declared = decodeConsistencyProofsFromUnprotected(unprotected);
175
179
  }
176
180
  catch {
177
181
  return null;
@@ -1,19 +1,50 @@
1
- /** Draft-bryce consistency proof embedded in a v3 checkpoint. `treeSize2` is
2
- * cross-checked against the checkpoint's SIGNED tree-size-2 (ADR-0066 D1 as
3
- * amended); `treeSize1` is unsigned prover context, not cross-checked here —
4
- * see {@link verifyCheckpointChain}, which compares it with the trusted
5
- * origin instead. */
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 proof (`vdp` 396 key -2) and require its
59
- * declared `tree-size-2` to equal the checkpoint's SIGNED `tree-size-2` from
60
- * the protected header (ADR-0066 D1 as amended, D5.5, label -65933).
61
- * `tree-size-1` is not signed and is not checked here (D2 is withdrawn); see
62
- * {@link verifyCheckpointChain} for its comparison against the trusted
63
- * origin.
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 protected header carries no consistency proof,
72
- * the proof is structurally malformed (see
73
- * {@link decodeConsistencyProofFromUnprotected}), or the protected header
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
- * One fold step: from the CALLER-TRUSTED accumulator at `sizeFrom`, produce
85
- * the `proof.treeSize2` accumulator via the size-driven
86
- * {@link consistentRootsForSizes} (ADR-0066 D5) — `roots` (the proven
87
- * prefix) followed by the proof's supplied right-peaks (the target peaks no
88
- * path reaches).
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
- * `sizeFrom` is a parameter, not read off `proof`, because the fold must run
91
- * against a size the CALLER already trusts (the previous link's verified
92
- * `treeSize2`, or the caller's anchor for a first link) — reading it from
93
- * the proof instead would let an unsigned or substituted proof dictate its
94
- * own starting point. `proof.treeSize1` must equal it regardless: the two
95
- * disagreeing means this proof does not continue from the state being
96
- * folded, not a mere shape defect, so it is checked before any fold work.
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 {Error} when `proof.treeSize1 !== sizeFrom`, or when the proof
99
- * supplies a right-peaks count other than
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 the proof
102
- * does not have the shape MMR(sizeFrom) -> MMR(proof.treeSize2) implies
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
- * `detail` names both sizes.
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 (D2 is
168
- * withdrawn): only this trusted-origin comparison applies. Because it is
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
- * - Each checkpoint's SIGNED `tree-size-2` (ADR-0066 D1 as amended) must
173
- * equal its declared consistency-proof `tree-size-2`
174
- * ({@link checkpointConsistencyProof}); a disagreement is
175
- * `size_mismatch`.
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":"AAyDA;;;;qBAIqB;AACrB,MAAM,MAAM,0BAA0B,GAAG;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;+EAC2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;IACtB,uEAAuE;IACvE,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B,CAAC;AAEF;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,UAAU,GAC1B,0BAA0B,CA6C5B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,0BAA0B,EACjC,eAAe,EAAE,UAAU,EAAE,EAC7B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,EAAE,CAAC,CAoBvB;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;;;;;;;;OAQG;OACD,eAAe,CAAC;IACpB,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B,CAAC;AAEN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;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,CAuJjC"}
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"}
@@ -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 (D2 chain semantics is
24
- * withdrawn) — a signed size-1 comparison would reject every re-based
25
- * publish and every multi-link catch-up. A checkpoint without the signed
26
- * size-2 label, or whose signed size-2 disagrees with its declared proof,
27
- * is rejected before any fold is attempted.
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 { decodeConsistencyProofFromUnprotected } from "./decode-checkpoint-consistency-proof.js";
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 proof (`vdp` 396 key -2) and require its
97
- * declared `tree-size-2` to equal the checkpoint's SIGNED `tree-size-2` from
98
- * the protected header (ADR-0066 D1 as amended, D5.5, label -65933).
99
- * `tree-size-1` is not signed and is not checked here (D2 is withdrawn); see
100
- * {@link verifyCheckpointChain} for its comparison against the trusted
101
- * origin.
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 protected header carries no consistency proof,
110
- * the proof is structurally malformed (see
111
- * {@link decodeConsistencyProofFromUnprotected}), or the protected header
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 declared = decodeConsistencyProofFromUnprotected(unprotected);
141
- if (declared === null) {
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
- if (signedTreeSize2 !== declared.treeSize2) {
149
- throw new CheckpointSignedSizeMismatchError(`signed tree-size-2 (-65933) ${signedTreeSize2} != declared consistency-proof tree-size-2 ${declared.treeSize2}`);
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
- treeSize1: declared.treeSize1,
153
- treeSize2: declared.treeSize2,
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
- * One fold step: from the CALLER-TRUSTED accumulator at `sizeFrom`, produce
161
- * the `proof.treeSize2` accumulator via the size-driven
162
- * {@link consistentRootsForSizes} (ADR-0066 D5) — `roots` (the proven
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
- * `sizeFrom` is a parameter, not read off `proof`, because the fold must run
167
- * against a size the CALLER already trusts (the previous link's verified
168
- * `treeSize2`, or the caller's anchor for a first link) — reading it from
169
- * the proof instead would let an unsigned or substituted proof dictate its
170
- * own starting point. `proof.treeSize1` must equal it regardless: the two
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
- * @throws {Error} when `proof.treeSize1 !== sizeFrom`, or when the proof
175
- * supplies a right-peaks count other than
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 the proof
178
- * does not have the shape MMR(sizeFrom) -> MMR(proof.treeSize2) implies
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.treeSize1 !== sizeFrom) {
182
- throw new Error(`consistency proof base tree-size-1 ${proof.treeSize1} does not match the trusted size ${sizeFrom}`);
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
- const { roots, expectedRight } = await consistentRootsForSizes(hasher, sizeFrom, proof.treeSize2, accumulatorFrom, proof.paths);
186
- if (proof.rightPeaks.length !== expectedRight) {
187
- throw new Error(`checkpoint supplies ${proof.rightPeaks.length} right-peaks; size ${proof.treeSize2} requires ${expectedRight}`);
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 [...roots, ...proof.rightPeaks];
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 (D2 is
223
- * withdrawn): only this trusted-origin comparison applies. Because it is
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
- * - Each checkpoint's SIGNED `tree-size-2` (ADR-0066 D1 as amended) must
228
- * equal its declared consistency-proof `tree-size-2`
229
- * ({@link checkpointConsistencyProof}); a disagreement is
230
- * `size_mismatch`.
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: "proof_malformed",
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,
@@ -1,7 +1,20 @@
1
1
  /**
2
- * Decode the draft-bryce consistency proof `[tree-size-1, tree-size-2, paths,
3
- * right-peaks]` carried under a checkpoint's verifiable-proofs UNPROTECTED
4
- * header (draft-bryce label 396, key -2 = `VDP_CONSISTENCY_PROOF_KEY`).
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,10 +22,11 @@
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; `tree-size-1` is unsigned
13
- * prover context and is not cross-checked against a signed value).
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
- /** The declared (unprotected, unsigned) consistency proof of a checkpoint. */
29
+ /** One declared (unprotected, unsigned) consistency proof of a checkpoint. */
16
30
  export type DecodedConsistencyProof = {
17
31
  treeSize1: bigint;
18
32
  treeSize2: bigint;
@@ -22,17 +36,35 @@ export type DecodedConsistencyProof = {
22
36
  rightPeaks: Uint8Array[];
23
37
  };
24
38
  /**
25
- * Decode the embedded consistency proof from a checkpoint's UNPROTECTED
26
- * header map. Returns `null` when the checkpoint carries no verifiable-proofs
27
- * header (396) or no consistency-proof bstr there (key -2) — an ABSENT
28
- * proof, not a malformed one.
39
+ * The verifiable-proofs header carries the consistency-proof key with an
40
+ * EMPTY array. Distinct from an absent proof (no -2 key at all, which
41
+ * decodes to `null`): the key is present and claims to relay a chain, but
42
+ * the chain has no links, so there is nothing to fold and no last proof for
43
+ * the signed `tree-size-2` to equal. `consistency-proofs = [ + ... ]`
44
+ * requires at least one.
45
+ */
46
+ export declare class EmptyConsistencyProofsError extends Error {
47
+ constructor(message: string);
48
+ }
49
+ /**
50
+ * Decode the embedded consistency proofs from a checkpoint's UNPROTECTED
51
+ * header map, in the order they are relayed. Returns `null` when the
52
+ * checkpoint carries no verifiable-proofs header (396) or no
53
+ * consistency-proof entry there (key -2) — an ABSENT proof, not a malformed
54
+ * one. A returned array always holds at least one proof.
55
+ *
56
+ * This decode establishes each proof's shape only. Nothing here relates one
57
+ * proof to the next, or to a signed size: the chain has to be checked
58
+ * against state the CALLER trusts, which is `computeCheckpointAccumulator`
59
+ * and `checkpointConsistencyProof` (ADR-0066 D5.4).
29
60
  *
30
- * @throws When a consistency-proof bstr IS present but its contents are not
31
- * the shape `[tree-size-1, tree-size-2, paths, right-peaks]`, either size
32
- * is not an unsigned integer, the proof does not grow the tree
61
+ * @throws {EmptyConsistencyProofsError} when the -2 entry is an empty array
62
+ * @throws When a consistency proof IS present but its contents are not the
63
+ * shape `[tree-size-1, tree-size-2, paths, right-peaks]`, either size is
64
+ * not an unsigned integer, a proof does not grow the tree
33
65
  * (`tree-size-2 <= tree-size-1`), a path element or a right-peak is not a
34
66
  * 32-byte string — or when header 396 is present but is not map-valued,
35
- * or its `-2` entry is present but not a byte string.
67
+ * or its `-2` entry is neither a byte string nor an array of byte strings.
36
68
  */
37
- export declare function decodeConsistencyProofFromUnprotected(unprotected: Map<number, unknown>): DecodedConsistencyProof | null;
69
+ export declare function decodeConsistencyProofsFromUnprotected(unprotected: Map<number, unknown>): DecodedConsistencyProof[] | null;
38
70
  //# sourceMappingURL=decode-checkpoint-consistency-proof.d.ts.map