@forestrie/merklelog 0.3.0 → 0.4.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/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/mmr/algorithms.d.ts +89 -3
- package/dist/mmr/algorithms.d.ts.map +1 -1
- package/dist/mmr/algorithms.js +171 -18
- package/dist/mmr/errors.d.ts +69 -0
- package/dist/mmr/errors.d.ts.map +1 -0
- package/dist/mmr/errors.js +95 -0
- package/dist/mmr/proof.d.ts +18 -0
- package/dist/mmr/proof.d.ts.map +1 -1
- package/dist/mmr/proof.js +18 -2
- package/package.json +1 -1
- package/src/index.ts +12 -0
- package/src/mmr/algorithms.ts +203 -20
- package/src/mmr/errors.ts +93 -0
- package/src/mmr/proof.ts +19 -2
package/dist/index.d.ts
CHANGED
|
@@ -18,10 +18,11 @@ export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
|
|
|
18
18
|
export type { MassifLeafIndex, LocatedLeaf } from "./massifs/leafindex.js";
|
|
19
19
|
export { mmrIndex, massifFirstLeaf, leafMinusSpurSum } from "./mmr/index.js";
|
|
20
20
|
export { heightIndex, height, mmrIndexFromLeafIndex, leafIndex, mmrPosition, mmrSizeFromHeightIndex, leafCount, leafCountFromHeightIndex, } from "./mmr/math.js";
|
|
21
|
-
export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, consistentRoots, indexConsistencyProof, type ConsistencyProof, } from "./mmr/algorithms.js";
|
|
21
|
+
export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, consistentRoots, consistentRootsForSizes, indexConsistencyProof, type ConsistencyProof, } from "./mmr/algorithms.js";
|
|
22
|
+
export { ConsistencyShapeError, IncompleteTreeSize, ConsistencyPeakCountMismatch, ConsistencyPathLengthMismatch, ConsistencyRootMismatch, SizeMustIncrease, } from "./mmr/errors.js";
|
|
22
23
|
export { createSyncHasher } from "./mmr/algorithms-sync.js";
|
|
23
24
|
export type { Proof, Peak } from "./mmr/types.js";
|
|
24
25
|
export type { Hasher } from "./mmr/types.js";
|
|
25
|
-
export { inclusionProof, inclusionProofPath, inclusionProofLocalExtend, peakMMRIndexes, peaksBitmap, peakIndexForLeafProof, indexHeight, firstMMRSize, massifIndexFromMMRIndex, peakStackMap, } from "./mmr/proof.js";
|
|
26
|
+
export { inclusionProof, inclusionProofPath, inclusionProofLocalExtend, peakMMRIndexes, peaksBitmap, peakIndexForLeafProof, mmrSizeForLeafCount, indexHeight, firstMMRSize, massifIndexFromMMRIndex, peakStackMap, } from "./mmr/proof.js";
|
|
26
27
|
export type { NodeGetter, ConsistencyProofLocal } from "./mmr/proof.js";
|
|
27
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EACL,SAAS,EACT,KAAK,EACL,KAAK,EACL,OAAO,EACP,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,6BAA6B,EAC7B,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG3E,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EACL,WAAW,EACX,MAAM,EACN,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,QAAQ,EACR,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EACL,SAAS,EACT,KAAK,EACL,KAAK,EACL,OAAO,EACP,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,6BAA6B,EAC7B,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG3E,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EACL,WAAW,EACX,MAAM,EACN,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,QAAQ,EACR,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,EAC5B,6BAA6B,EAC7B,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,uBAAuB,EACvB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,10 @@ export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
|
|
|
20
20
|
// Export mmr module
|
|
21
21
|
export { mmrIndex, massifFirstLeaf, leafMinusSpurSum } from "./mmr/index.js";
|
|
22
22
|
export { heightIndex, height, mmrIndexFromLeafIndex, leafIndex, mmrPosition, mmrSizeFromHeightIndex, leafCount, leafCountFromHeightIndex, } from "./mmr/math.js";
|
|
23
|
-
export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, consistentRoots, indexConsistencyProof, } from "./mmr/algorithms.js";
|
|
23
|
+
export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, consistentRoots, consistentRootsForSizes, indexConsistencyProof, } from "./mmr/algorithms.js";
|
|
24
|
+
// Typed proof-shape rejections raised by consistentRootsForSizes (names
|
|
25
|
+
// mirror the Solidity IUnivocityErrors members) — plan-2609-10 s4.3, FOR-568.
|
|
26
|
+
export { ConsistencyShapeError, IncompleteTreeSize, ConsistencyPeakCountMismatch, ConsistencyPathLengthMismatch, ConsistencyRootMismatch, SizeMustIncrease, } from "./mmr/errors.js";
|
|
24
27
|
export { createSyncHasher } from "./mmr/algorithms-sync.js";
|
|
25
28
|
// MMR proof building (pure; store-agnostic) — plan-2607-15 §4 (go: mmr/proof.go, peaks.go)
|
|
26
|
-
export { inclusionProof, inclusionProofPath, inclusionProofLocalExtend, peakMMRIndexes, peaksBitmap, peakIndexForLeafProof, indexHeight, firstMMRSize, massifIndexFromMMRIndex, peakStackMap, } from "./mmr/proof.js";
|
|
29
|
+
export { inclusionProof, inclusionProofPath, inclusionProofLocalExtend, peakMMRIndexes, peaksBitmap, peakIndexForLeafProof, mmrSizeForLeafCount, indexHeight, firstMMRSize, massifIndexFromMMRIndex, peakStackMap, } from "./mmr/proof.js";
|
package/dist/mmr/algorithms.d.ts
CHANGED
|
@@ -79,28 +79,114 @@ export interface ConsistencyProof {
|
|
|
79
79
|
* duplicate roots collapse (many old peaks share one new peak). Requires
|
|
80
80
|
* one path per MMR(A) peak (draft: `len(peaks(ifrom)) == len(accumulatorfrom)`).
|
|
81
81
|
*
|
|
82
|
+
* This fold does not enforce the proof shape the two sizes imply: it reads
|
|
83
|
+
* whatever path lengths it is given and collapses whatever roots coincide.
|
|
84
|
+
* Use {@link consistentRootsForSizes} for verification — it checks each path
|
|
85
|
+
* against the length MMR(A) -> MMR(B) fixes and requires the paths under one
|
|
86
|
+
* target peak to agree.
|
|
87
|
+
*
|
|
82
88
|
* @param ifrom - last node index of the complete MMR(A) (`mmrSizeA - 1`)
|
|
83
89
|
* @param accumulatorFrom - MMR(A) peak values, descending height order
|
|
84
90
|
* @param paths - inclusion path per peak, proven in MMR(B)
|
|
85
91
|
*/
|
|
86
92
|
export declare function consistentRoots(hasher: Hasher, ifrom: bigint, accumulatorFrom: Uint8Array[], paths: Uint8Array[][]): Promise<Uint8Array[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Produce the peaks of MMR(sizeTo) that `paths` prove from the peaks of
|
|
95
|
+
* MMR(sizeFrom), requiring the paths to have exactly the shape the two sizes
|
|
96
|
+
* imply (draft-bryce-cose-receipts-mmr-profile, "Verifying the Receipt of
|
|
97
|
+
* consistency", with the draft's SHOULD on path lengths enforced in the same
|
|
98
|
+
* pass). Line-for-line port of the reference `consistent_roots_for_sizes`
|
|
99
|
+
* (algorithms.py) and of Solidity `consistentRootsForSizes`
|
|
100
|
+
* (univocity `src/algorithms/consistentRoots.sol`).
|
|
101
|
+
*
|
|
102
|
+
* Sizes are node counts (the MMR ending at index `i` has `i + 1` nodes).
|
|
103
|
+
* `sizeFrom` MUST be the size of the state the verifier already trusts; taken
|
|
104
|
+
* from the proof instead, the check is void. Only `sizeTo` is required to be
|
|
105
|
+
* a complete MMR size: every trusted size was itself a checked target.
|
|
106
|
+
*
|
|
107
|
+
* For a complete MMR the set bits of `peaksBitmap(size)` are the peak heights,
|
|
108
|
+
* high to low, which is accumulator order. Let `split` be the highest bit on
|
|
109
|
+
* which the two bitmaps differ; as `sizeTo > sizeFrom` the target has it and
|
|
110
|
+
* the origin does not. An origin peak above `split` is also a peak of the
|
|
111
|
+
* target: its path is empty and it is returned unchanged. Every origin peak
|
|
112
|
+
* below `split` is committed by the target peak of height `split`: its path
|
|
113
|
+
* has length `split - h`, and every such path must prove the same root. The
|
|
114
|
+
* target's remaining peaks lie below every origin peak, so no path reaches
|
|
115
|
+
* them; the prover supplies them separately and their count is returned.
|
|
116
|
+
*
|
|
117
|
+
* Because the shape is fixed by the sizes alone, no peak index list and no
|
|
118
|
+
* per-hop bookkeeping beyond the hash itself is needed: the bitmaps are
|
|
119
|
+
* iterated directly.
|
|
120
|
+
*
|
|
121
|
+
* @param hasher - cryptographic hasher instance
|
|
122
|
+
* @param sizeFrom - node count of the trusted origin state (0 for an empty log)
|
|
123
|
+
* @param sizeTo - node count of the target state; must exceed `sizeFrom` and
|
|
124
|
+
* must be a complete MMR size
|
|
125
|
+
* @param accumulatorFrom - peaks of MMR(sizeFrom), descending height order
|
|
126
|
+
* @param paths - one path per origin peak, in the same order
|
|
127
|
+
* @returns `roots`, the peaks of MMR(sizeTo) proven from the origin peaks in
|
|
128
|
+
* descending height order (the unchanged peaks, then the one proven root if
|
|
129
|
+
* any), and `expectedRight`, the number of MMR(sizeTo) peaks the prover must
|
|
130
|
+
* supply as right peaks. `roots` followed by those right peaks is the
|
|
131
|
+
* accumulator of MMR(sizeTo).
|
|
132
|
+
* @throws {SizeMustIncrease} if `sizeTo <= sizeFrom`
|
|
133
|
+
* @throws {IncompleteTreeSize} if `sizeTo` is not a complete MMR size
|
|
134
|
+
* @throws {ConsistencyPeakCountMismatch} if `accumulatorFrom` or `paths` does
|
|
135
|
+
* not have one entry per origin peak
|
|
136
|
+
* @throws {ConsistencyPathLengthMismatch} if a path length differs from the
|
|
137
|
+
* length the two sizes imply
|
|
138
|
+
* @throws {ConsistencyRootMismatch} if two paths under one target peak produce
|
|
139
|
+
* different roots
|
|
140
|
+
*/
|
|
141
|
+
export declare function consistentRootsForSizes(hasher: Hasher, sizeFrom: bigint, sizeTo: bigint, accumulatorFrom: Uint8Array[], paths: Uint8Array[][]): Promise<{
|
|
142
|
+
roots: Uint8Array[];
|
|
143
|
+
expectedRight: number;
|
|
144
|
+
}>;
|
|
87
145
|
/**
|
|
88
146
|
* Verify MMR(A) is a committed prefix of MMR(B)
|
|
89
147
|
* (draft-bryce "Verifying the Receipt of consistency";
|
|
90
148
|
* go-merklelog `VerifyConsistency`).
|
|
91
149
|
*
|
|
150
|
+
* BOTH sizes are CALLER-SUPPLIED trusted state (ADR-0066 D5.4), never read
|
|
151
|
+
* off the proof: `sizeFrom` is the size of the state `peaksFrom` is the
|
|
152
|
+
* accumulator of, and `sizeTo` the size `peaksTo` is claimed to be the
|
|
153
|
+
* accumulator of. The proof contributes only `paths`. Taking either size
|
|
154
|
+
* from the proof makes the check void: the fold constrains a declared
|
|
155
|
+
* `sizeFrom` by the origin peak COUNT alone, which a whole family of sizes
|
|
156
|
+
* share, so one set of paths and peaks verified at many size pairs — with
|
|
157
|
+
* the draft's own MMR(11) -> MMR(39) material, 24 distinct pairs, of which
|
|
158
|
+
* one is the true relation. `indexConsistencyProof` still returns the
|
|
159
|
+
* {@link ConsistencyProof} wire type with its declared sizes; a caller that
|
|
160
|
+
* relays that type passes `proof.paths` here and supplies the sizes it
|
|
161
|
+
* trusts, comparing the declared sizes with them separately if it wants to
|
|
162
|
+
* report a disagreement.
|
|
163
|
+
*
|
|
164
|
+
* Routed through {@link consistentRootsForSizes}, so the proof must have the
|
|
165
|
+
* shape `sizeFrom -> sizeTo` implies: one path per MMR(A) peak, empty above
|
|
166
|
+
* the split and of length `split - h` below it, with every path below the
|
|
167
|
+
* split proving the same root. The proven roots must then be the leading
|
|
168
|
+
* entries of `peaksTo`, and `peaksTo` must hold exactly those plus the
|
|
169
|
+
* `expectedRight` right peaks no path reaches — so a truncated or padded
|
|
170
|
+
* target accumulator is rejected on its length alone.
|
|
171
|
+
*
|
|
92
172
|
* Replaces the plan-0027 always-true stub (FOR-368 Phase 1,
|
|
93
173
|
* plan-2607-29): the previous signature took two inclusion proofs and
|
|
94
174
|
* returned true unconditionally; no caller existed.
|
|
95
175
|
*
|
|
96
|
-
* @param
|
|
176
|
+
* @param sizeFrom - TRUSTED node count of MMR(A)
|
|
177
|
+
* @param sizeTo - TRUSTED node count of MMR(B)
|
|
97
178
|
* @param peaksFrom - TRUSTED MMR(A) accumulator (e.g. a signed checkpoint
|
|
98
179
|
* payload), descending height order
|
|
180
|
+
* @param paths - one inclusion path per MMR(A) peak, in the same order
|
|
99
181
|
* @param peaksTo - MMR(B) accumulator to prove against (e.g. an anchored
|
|
100
182
|
* on-chain state), descending height order
|
|
101
|
-
* @returns ok, with the MMR(B) accumulator on success
|
|
183
|
+
* @returns ok, with the MMR(B) accumulator on success. A value that does not
|
|
184
|
+
* match `peaksTo` returns `{ok: false, accumulator: []}`; a proof that does
|
|
185
|
+
* not have the shape the sizes imply throws one of the typed errors in
|
|
186
|
+
* `./errors.js` (all `ConsistencyShapeError` subclasses), which callers
|
|
187
|
+
* report as a malformed proof rather than a failed comparison.
|
|
102
188
|
*/
|
|
103
|
-
export declare function verifyConsistency(hasher: Hasher,
|
|
189
|
+
export declare function verifyConsistency(hasher: Hasher, sizeFrom: bigint, sizeTo: bigint, peaksFrom: Uint8Array[], paths: Uint8Array[][], peaksTo: Uint8Array[]): Promise<{
|
|
104
190
|
ok: boolean;
|
|
105
191
|
accumulator: Uint8Array[];
|
|
106
192
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"algorithms.d.ts","sourceRoot":"","sources":["../../src/mmr/algorithms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,
|
|
1
|
+
{"version":3,"file":"algorithms.d.ts","sourceRoot":"","sources":["../../src/mmr/algorithms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AA4BpB;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,UAAU,EAAE,GAClB,OAAO,CAAC,UAAU,CAAC,CAwBrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,CAAC,CAkCrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,UAAU,EAAE,EAC7B,KAAK,EAAE,UAAU,EAAE,EAAE,GACpB,OAAO,CAAC,UAAU,EAAE,CAAC,CA0BvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,UAAU,EAAE,EAC7B,KAAK,EAAE,UAAU,EAAE,EAAE,GACpB,OAAO,CAAC;IAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,CA+EzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,UAAU,EAAE,EACvB,KAAK,EAAE,UAAU,EAAE,EAAE,EACrB,OAAO,EAAE,UAAU,EAAE,GACpB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC,CAqBrD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,gBAAgB,CAMlB"}
|
package/dist/mmr/algorithms.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* And the associated IETF draft specification:
|
|
11
11
|
* https://raw.githubusercontent.com/robinbryce/draft-bryce-cose-receipts-mmr-profile/refs/heads/main/draft-bryce-cose-receipts-mmr-profile.md
|
|
12
12
|
*/
|
|
13
|
-
import { inclusionProof, peakMMRIndexes } from "./proof.js";
|
|
13
|
+
import { inclusionProof, peakMMRIndexes, bitLength, popcount, peaksBitmap, mmrSizeForLeafCount, } from "./proof.js";
|
|
14
|
+
import { ConsistencyPathLengthMismatch, ConsistencyPeakCountMismatch, ConsistencyRootMismatch, IncompleteTreeSize, SizeMustIncrease, } from "./errors.js";
|
|
14
15
|
import { Uint64 } from "../uint64/index.js";
|
|
15
16
|
import { heightIndex } from "./math.js";
|
|
16
17
|
import { arraysEqual } from "../utils/arrays.js";
|
|
@@ -140,6 +141,12 @@ export async function verifyInclusion(hasher, leafHash, proof, root) {
|
|
|
140
141
|
* duplicate roots collapse (many old peaks share one new peak). Requires
|
|
141
142
|
* one path per MMR(A) peak (draft: `len(peaks(ifrom)) == len(accumulatorfrom)`).
|
|
142
143
|
*
|
|
144
|
+
* This fold does not enforce the proof shape the two sizes imply: it reads
|
|
145
|
+
* whatever path lengths it is given and collapses whatever roots coincide.
|
|
146
|
+
* Use {@link consistentRootsForSizes} for verification — it checks each path
|
|
147
|
+
* against the length MMR(A) -> MMR(B) fixes and requires the paths under one
|
|
148
|
+
* target peak to agree.
|
|
149
|
+
*
|
|
143
150
|
* @param ifrom - last node index of the complete MMR(A) (`mmrSizeA - 1`)
|
|
144
151
|
* @param accumulatorFrom - MMR(A) peak values, descending height order
|
|
145
152
|
* @param paths - inclusion path per peak, proven in MMR(B)
|
|
@@ -162,40 +169,186 @@ export async function consistentRoots(hasher, ifrom, accumulatorFrom, paths) {
|
|
|
162
169
|
}
|
|
163
170
|
return roots;
|
|
164
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Produce the peaks of MMR(sizeTo) that `paths` prove from the peaks of
|
|
174
|
+
* MMR(sizeFrom), requiring the paths to have exactly the shape the two sizes
|
|
175
|
+
* imply (draft-bryce-cose-receipts-mmr-profile, "Verifying the Receipt of
|
|
176
|
+
* consistency", with the draft's SHOULD on path lengths enforced in the same
|
|
177
|
+
* pass). Line-for-line port of the reference `consistent_roots_for_sizes`
|
|
178
|
+
* (algorithms.py) and of Solidity `consistentRootsForSizes`
|
|
179
|
+
* (univocity `src/algorithms/consistentRoots.sol`).
|
|
180
|
+
*
|
|
181
|
+
* Sizes are node counts (the MMR ending at index `i` has `i + 1` nodes).
|
|
182
|
+
* `sizeFrom` MUST be the size of the state the verifier already trusts; taken
|
|
183
|
+
* from the proof instead, the check is void. Only `sizeTo` is required to be
|
|
184
|
+
* a complete MMR size: every trusted size was itself a checked target.
|
|
185
|
+
*
|
|
186
|
+
* For a complete MMR the set bits of `peaksBitmap(size)` are the peak heights,
|
|
187
|
+
* high to low, which is accumulator order. Let `split` be the highest bit on
|
|
188
|
+
* which the two bitmaps differ; as `sizeTo > sizeFrom` the target has it and
|
|
189
|
+
* the origin does not. An origin peak above `split` is also a peak of the
|
|
190
|
+
* target: its path is empty and it is returned unchanged. Every origin peak
|
|
191
|
+
* below `split` is committed by the target peak of height `split`: its path
|
|
192
|
+
* has length `split - h`, and every such path must prove the same root. The
|
|
193
|
+
* target's remaining peaks lie below every origin peak, so no path reaches
|
|
194
|
+
* them; the prover supplies them separately and their count is returned.
|
|
195
|
+
*
|
|
196
|
+
* Because the shape is fixed by the sizes alone, no peak index list and no
|
|
197
|
+
* per-hop bookkeeping beyond the hash itself is needed: the bitmaps are
|
|
198
|
+
* iterated directly.
|
|
199
|
+
*
|
|
200
|
+
* @param hasher - cryptographic hasher instance
|
|
201
|
+
* @param sizeFrom - node count of the trusted origin state (0 for an empty log)
|
|
202
|
+
* @param sizeTo - node count of the target state; must exceed `sizeFrom` and
|
|
203
|
+
* must be a complete MMR size
|
|
204
|
+
* @param accumulatorFrom - peaks of MMR(sizeFrom), descending height order
|
|
205
|
+
* @param paths - one path per origin peak, in the same order
|
|
206
|
+
* @returns `roots`, the peaks of MMR(sizeTo) proven from the origin peaks in
|
|
207
|
+
* descending height order (the unchanged peaks, then the one proven root if
|
|
208
|
+
* any), and `expectedRight`, the number of MMR(sizeTo) peaks the prover must
|
|
209
|
+
* supply as right peaks. `roots` followed by those right peaks is the
|
|
210
|
+
* accumulator of MMR(sizeTo).
|
|
211
|
+
* @throws {SizeMustIncrease} if `sizeTo <= sizeFrom`
|
|
212
|
+
* @throws {IncompleteTreeSize} if `sizeTo` is not a complete MMR size
|
|
213
|
+
* @throws {ConsistencyPeakCountMismatch} if `accumulatorFrom` or `paths` does
|
|
214
|
+
* not have one entry per origin peak
|
|
215
|
+
* @throws {ConsistencyPathLengthMismatch} if a path length differs from the
|
|
216
|
+
* length the two sizes imply
|
|
217
|
+
* @throws {ConsistencyRootMismatch} if two paths under one target peak produce
|
|
218
|
+
* different roots
|
|
219
|
+
*/
|
|
220
|
+
export async function consistentRootsForSizes(hasher, sizeFrom, sizeTo, accumulatorFrom, paths) {
|
|
221
|
+
if (sizeTo <= sizeFrom) {
|
|
222
|
+
throw new SizeMustIncrease(sizeFrom, sizeTo);
|
|
223
|
+
}
|
|
224
|
+
const to = peaksBitmap(sizeTo);
|
|
225
|
+
// peaksBitmap rounds an incomplete size down to the largest MMR below it,
|
|
226
|
+
// so `to` describes MMR(sizeTo) only if sizeTo is complete. Without this a
|
|
227
|
+
// target such as 6 anchors an accumulator that is no MMR's, and a verifier
|
|
228
|
+
// later reads its entries at the wrong heights.
|
|
229
|
+
if (mmrSizeForLeafCount(to) !== sizeTo) {
|
|
230
|
+
throw new IncompleteTreeSize(sizeTo);
|
|
231
|
+
}
|
|
232
|
+
const from = peaksBitmap(sizeFrom);
|
|
233
|
+
const n = popcount(from);
|
|
234
|
+
if (accumulatorFrom.length !== n) {
|
|
235
|
+
throw new ConsistencyPeakCountMismatch(n, accumulatorFrom.length);
|
|
236
|
+
}
|
|
237
|
+
if (paths.length !== n) {
|
|
238
|
+
throw new ConsistencyPeakCountMismatch(n, paths.length);
|
|
239
|
+
}
|
|
240
|
+
const nto = popcount(to);
|
|
241
|
+
if (n === 0) {
|
|
242
|
+
return { roots: [], expectedRight: nto };
|
|
243
|
+
}
|
|
244
|
+
const split = bitLength(from ^ to) - 1;
|
|
245
|
+
const roots = [];
|
|
246
|
+
// Nodes preceding the current origin peak's subtree; a peak of height h
|
|
247
|
+
// sits at offset + 2^(h+1) - 2 and its subtree has 2^(h+1) - 1 nodes.
|
|
248
|
+
let offset = 0n;
|
|
249
|
+
let i = 0;
|
|
250
|
+
// Origin peaks above the split are also peaks of the target. The path is
|
|
251
|
+
// not read; requiring it to be empty rejects unused material (a shape
|
|
252
|
+
// check: the result does not depend on it).
|
|
253
|
+
for (let h = bitLength(from) - 1; h > split; h--) {
|
|
254
|
+
if (((from >> BigInt(h)) & 1n) === 0n)
|
|
255
|
+
continue;
|
|
256
|
+
if (paths[i].length !== 0) {
|
|
257
|
+
throw new ConsistencyPathLengthMismatch(i, 0, paths[i].length);
|
|
258
|
+
}
|
|
259
|
+
roots.push(accumulatorFrom[i]);
|
|
260
|
+
offset += (1n << BigInt(h + 1)) - 1n;
|
|
261
|
+
i += 1;
|
|
262
|
+
}
|
|
263
|
+
// Origin peaks below the split are all committed by the target peak of
|
|
264
|
+
// height `split` (bit `split` itself is clear in `from`), so each path must
|
|
265
|
+
// have length split - h and every path must prove the same root. The first
|
|
266
|
+
// `above` peaks were returned unchanged, so i === above at the first peak
|
|
267
|
+
// below the split.
|
|
268
|
+
const above = roots.length;
|
|
269
|
+
let root;
|
|
270
|
+
for (let h = split - 1; h >= 0; h--) {
|
|
271
|
+
if (((from >> BigInt(h)) & 1n) === 0n)
|
|
272
|
+
continue;
|
|
273
|
+
const expected = split - h;
|
|
274
|
+
if (paths[i].length !== expected) {
|
|
275
|
+
throw new ConsistencyPathLengthMismatch(i, expected, paths[i].length);
|
|
276
|
+
}
|
|
277
|
+
const subtree = (1n << BigInt(h + 1)) - 1n;
|
|
278
|
+
const peakMMRIndex = offset + subtree - 1n;
|
|
279
|
+
const proven = await calculateRoot(hasher, accumulatorFrom[i], { path: paths[i], mmrIndex: peakMMRIndex }, peakMMRIndex);
|
|
280
|
+
if (i === above) {
|
|
281
|
+
root = proven;
|
|
282
|
+
}
|
|
283
|
+
else if (!arraysEqual(proven, root)) {
|
|
284
|
+
throw new ConsistencyRootMismatch(i);
|
|
285
|
+
}
|
|
286
|
+
offset += subtree;
|
|
287
|
+
i += 1;
|
|
288
|
+
}
|
|
289
|
+
if (n > above) {
|
|
290
|
+
roots.push(root);
|
|
291
|
+
}
|
|
292
|
+
return { roots, expectedRight: nto - roots.length };
|
|
293
|
+
}
|
|
165
294
|
/**
|
|
166
295
|
* Verify MMR(A) is a committed prefix of MMR(B)
|
|
167
296
|
* (draft-bryce "Verifying the Receipt of consistency";
|
|
168
297
|
* go-merklelog `VerifyConsistency`).
|
|
169
298
|
*
|
|
299
|
+
* BOTH sizes are CALLER-SUPPLIED trusted state (ADR-0066 D5.4), never read
|
|
300
|
+
* off the proof: `sizeFrom` is the size of the state `peaksFrom` is the
|
|
301
|
+
* accumulator of, and `sizeTo` the size `peaksTo` is claimed to be the
|
|
302
|
+
* accumulator of. The proof contributes only `paths`. Taking either size
|
|
303
|
+
* from the proof makes the check void: the fold constrains a declared
|
|
304
|
+
* `sizeFrom` by the origin peak COUNT alone, which a whole family of sizes
|
|
305
|
+
* share, so one set of paths and peaks verified at many size pairs — with
|
|
306
|
+
* the draft's own MMR(11) -> MMR(39) material, 24 distinct pairs, of which
|
|
307
|
+
* one is the true relation. `indexConsistencyProof` still returns the
|
|
308
|
+
* {@link ConsistencyProof} wire type with its declared sizes; a caller that
|
|
309
|
+
* relays that type passes `proof.paths` here and supplies the sizes it
|
|
310
|
+
* trusts, comparing the declared sizes with them separately if it wants to
|
|
311
|
+
* report a disagreement.
|
|
312
|
+
*
|
|
313
|
+
* Routed through {@link consistentRootsForSizes}, so the proof must have the
|
|
314
|
+
* shape `sizeFrom -> sizeTo` implies: one path per MMR(A) peak, empty above
|
|
315
|
+
* the split and of length `split - h` below it, with every path below the
|
|
316
|
+
* split proving the same root. The proven roots must then be the leading
|
|
317
|
+
* entries of `peaksTo`, and `peaksTo` must hold exactly those plus the
|
|
318
|
+
* `expectedRight` right peaks no path reaches — so a truncated or padded
|
|
319
|
+
* target accumulator is rejected on its length alone.
|
|
320
|
+
*
|
|
170
321
|
* Replaces the plan-0027 always-true stub (FOR-368 Phase 1,
|
|
171
322
|
* plan-2607-29): the previous signature took two inclusion proofs and
|
|
172
323
|
* returned true unconditionally; no caller existed.
|
|
173
324
|
*
|
|
174
|
-
* @param
|
|
325
|
+
* @param sizeFrom - TRUSTED node count of MMR(A)
|
|
326
|
+
* @param sizeTo - TRUSTED node count of MMR(B)
|
|
175
327
|
* @param peaksFrom - TRUSTED MMR(A) accumulator (e.g. a signed checkpoint
|
|
176
328
|
* payload), descending height order
|
|
329
|
+
* @param paths - one inclusion path per MMR(A) peak, in the same order
|
|
177
330
|
* @param peaksTo - MMR(B) accumulator to prove against (e.g. an anchored
|
|
178
331
|
* on-chain state), descending height order
|
|
179
|
-
* @returns ok, with the MMR(B) accumulator on success
|
|
332
|
+
* @returns ok, with the MMR(B) accumulator on success. A value that does not
|
|
333
|
+
* match `peaksTo` returns `{ok: false, accumulator: []}`; a proof that does
|
|
334
|
+
* not have the shape the sizes imply throws one of the typed errors in
|
|
335
|
+
* `./errors.js` (all `ConsistencyShapeError` subclasses), which callers
|
|
336
|
+
* report as a malformed proof rather than a failed comparison.
|
|
180
337
|
*/
|
|
181
|
-
export async function verifyConsistency(hasher,
|
|
182
|
-
const
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (ito >= peaksTo.length || !arraysEqual(peaksTo[ito], root)) {
|
|
338
|
+
export async function verifyConsistency(hasher, sizeFrom, sizeTo, peaksFrom, paths, peaksTo) {
|
|
339
|
+
const { roots, expectedRight } = await consistentRootsForSizes(hasher, sizeFrom, sizeTo, peaksFrom, paths);
|
|
340
|
+
// roots is the leading run of the MMR(B) accumulator and expectedRight
|
|
341
|
+
// counts the peaks below every MMR(A) peak, which no path reaches. Together
|
|
342
|
+
// they fix the length of MMR(B)'s accumulator, so the supplied peaksTo must
|
|
343
|
+
// have that length and must start with the proven roots.
|
|
344
|
+
if (peaksTo.length !== roots.length + expectedRight) {
|
|
345
|
+
return { ok: false, accumulator: [] };
|
|
346
|
+
}
|
|
347
|
+
for (let i = 0; i < roots.length; i++) {
|
|
348
|
+
if (!arraysEqual(peaksTo[i], roots[i])) {
|
|
193
349
|
return { ok: false, accumulator: [] };
|
|
194
350
|
}
|
|
195
351
|
}
|
|
196
|
-
// The full MMR(B) accumulator is the proven prefix plus any right-peaks;
|
|
197
|
-
// returning peaksTo is safe because consistentRoots enforced one proof
|
|
198
|
-
// per MMR(A) peak (see go-merklelog VerifyConsistency).
|
|
199
352
|
return { ok: true, accumulator: peaksTo };
|
|
200
353
|
}
|
|
201
354
|
/**
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors for the size-driven consistency fold
|
|
3
|
+
* (`consistentRootsForSizes`).
|
|
4
|
+
*
|
|
5
|
+
* The names mirror the Solidity `IUnivocityErrors` members raised by the
|
|
6
|
+
* on-chain implementation (`univocity/src/algorithms/consistentRoots.sol`),
|
|
7
|
+
* so a shape rejection reads the same on both sides. Every one of these
|
|
8
|
+
* reports a condition the two sizes fix in advance — the proof does not
|
|
9
|
+
* have the shape MMR(sizeFrom) -> MMR(sizeTo) implies — as distinct from a
|
|
10
|
+
* value mismatch against a trusted accumulator, which callers surface as a
|
|
11
|
+
* plain verification failure.
|
|
12
|
+
*
|
|
13
|
+
* All derive from {@link ConsistencyShapeError} so a caller can map the whole
|
|
14
|
+
* family to "malformed proof" with a single `instanceof`.
|
|
15
|
+
*/
|
|
16
|
+
/** Base class for every consistency proof-shape rejection. */
|
|
17
|
+
export declare class ConsistencyShapeError extends Error {
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The target size is not a complete MMR size.
|
|
22
|
+
*
|
|
23
|
+
* `peaksBitmap` rounds an incomplete size down to the largest complete MMR
|
|
24
|
+
* below it, so for an incomplete `sizeTo` the bitmap describes a different
|
|
25
|
+
* tree than the one named. Accepting it would anchor an accumulator that is
|
|
26
|
+
* no MMR's, and a later verifier would read its entries at the wrong heights.
|
|
27
|
+
*/
|
|
28
|
+
export declare class IncompleteTreeSize extends ConsistencyShapeError {
|
|
29
|
+
readonly size: bigint;
|
|
30
|
+
constructor(size: bigint);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The accumulator or the proof list does not have one entry per origin peak.
|
|
34
|
+
* The origin peak count is fixed by `sizeFrom` alone.
|
|
35
|
+
*/
|
|
36
|
+
export declare class ConsistencyPeakCountMismatch extends ConsistencyShapeError {
|
|
37
|
+
readonly expected: number;
|
|
38
|
+
readonly actual: number;
|
|
39
|
+
constructor(expected: number, actual: number);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A path does not have the length the two sizes imply: 0 for an origin peak
|
|
43
|
+
* above the split, `split - h` for an origin peak of height `h` below it.
|
|
44
|
+
*/
|
|
45
|
+
export declare class ConsistencyPathLengthMismatch extends ConsistencyShapeError {
|
|
46
|
+
readonly index: number;
|
|
47
|
+
readonly expected: number;
|
|
48
|
+
readonly actual: number;
|
|
49
|
+
constructor(index: number, expected: number, actual: number);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Two origin peaks below the split proved different roots. Every origin peak
|
|
53
|
+
* below the split is committed by the single target peak of height `split`,
|
|
54
|
+
* so all of their paths must produce the same value.
|
|
55
|
+
*/
|
|
56
|
+
export declare class ConsistencyRootMismatch extends ConsistencyShapeError {
|
|
57
|
+
readonly index: number;
|
|
58
|
+
constructor(index: number);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The target size does not exceed the origin size. Consistency is defined for
|
|
62
|
+
* a growing log; equal or decreasing sizes have no proof shape to check.
|
|
63
|
+
*/
|
|
64
|
+
export declare class SizeMustIncrease extends ConsistencyShapeError {
|
|
65
|
+
readonly sizeFrom: bigint;
|
|
66
|
+
readonly sizeTo: bigint;
|
|
67
|
+
constructor(sizeFrom: bigint, sizeTo: bigint);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/mmr/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,8DAA8D;AAC9D,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,qBAAqB;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAIlC;AAED;;;GAGG;AACH,qBAAa,4BAA6B,SAAQ,qBAAqB;IAEnE,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B;AAED;;;GAGG;AACH,qBAAa,6BAA8B,SAAQ,qBAAqB;IAEpE,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAFd,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,qBAAqB;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;CAInC;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,qBAAqB;IAEvD,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors for the size-driven consistency fold
|
|
3
|
+
* (`consistentRootsForSizes`).
|
|
4
|
+
*
|
|
5
|
+
* The names mirror the Solidity `IUnivocityErrors` members raised by the
|
|
6
|
+
* on-chain implementation (`univocity/src/algorithms/consistentRoots.sol`),
|
|
7
|
+
* so a shape rejection reads the same on both sides. Every one of these
|
|
8
|
+
* reports a condition the two sizes fix in advance — the proof does not
|
|
9
|
+
* have the shape MMR(sizeFrom) -> MMR(sizeTo) implies — as distinct from a
|
|
10
|
+
* value mismatch against a trusted accumulator, which callers surface as a
|
|
11
|
+
* plain verification failure.
|
|
12
|
+
*
|
|
13
|
+
* All derive from {@link ConsistencyShapeError} so a caller can map the whole
|
|
14
|
+
* family to "malformed proof" with a single `instanceof`.
|
|
15
|
+
*/
|
|
16
|
+
/** Base class for every consistency proof-shape rejection. */
|
|
17
|
+
export class ConsistencyShapeError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ConsistencyShapeError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The target size is not a complete MMR size.
|
|
25
|
+
*
|
|
26
|
+
* `peaksBitmap` rounds an incomplete size down to the largest complete MMR
|
|
27
|
+
* below it, so for an incomplete `sizeTo` the bitmap describes a different
|
|
28
|
+
* tree than the one named. Accepting it would anchor an accumulator that is
|
|
29
|
+
* no MMR's, and a later verifier would read its entries at the wrong heights.
|
|
30
|
+
*/
|
|
31
|
+
export class IncompleteTreeSize extends ConsistencyShapeError {
|
|
32
|
+
size;
|
|
33
|
+
constructor(size) {
|
|
34
|
+
super(`size ${size} is not a complete MMR size`);
|
|
35
|
+
this.size = size;
|
|
36
|
+
this.name = "IncompleteTreeSize";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The accumulator or the proof list does not have one entry per origin peak.
|
|
41
|
+
* The origin peak count is fixed by `sizeFrom` alone.
|
|
42
|
+
*/
|
|
43
|
+
export class ConsistencyPeakCountMismatch extends ConsistencyShapeError {
|
|
44
|
+
expected;
|
|
45
|
+
actual;
|
|
46
|
+
constructor(expected, actual) {
|
|
47
|
+
super(`expected ${expected} entries for the origin peaks, got ${actual}`);
|
|
48
|
+
this.expected = expected;
|
|
49
|
+
this.actual = actual;
|
|
50
|
+
this.name = "ConsistencyPeakCountMismatch";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A path does not have the length the two sizes imply: 0 for an origin peak
|
|
55
|
+
* above the split, `split - h` for an origin peak of height `h` below it.
|
|
56
|
+
*/
|
|
57
|
+
export class ConsistencyPathLengthMismatch extends ConsistencyShapeError {
|
|
58
|
+
index;
|
|
59
|
+
expected;
|
|
60
|
+
actual;
|
|
61
|
+
constructor(index, expected, actual) {
|
|
62
|
+
super(`path ${index}: expected length ${expected}, got ${actual}`);
|
|
63
|
+
this.index = index;
|
|
64
|
+
this.expected = expected;
|
|
65
|
+
this.actual = actual;
|
|
66
|
+
this.name = "ConsistencyPathLengthMismatch";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Two origin peaks below the split proved different roots. Every origin peak
|
|
71
|
+
* below the split is committed by the single target peak of height `split`,
|
|
72
|
+
* so all of their paths must produce the same value.
|
|
73
|
+
*/
|
|
74
|
+
export class ConsistencyRootMismatch extends ConsistencyShapeError {
|
|
75
|
+
index;
|
|
76
|
+
constructor(index) {
|
|
77
|
+
super(`path ${index} proves a different root from the paths before it`);
|
|
78
|
+
this.index = index;
|
|
79
|
+
this.name = "ConsistencyRootMismatch";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The target size does not exceed the origin size. Consistency is defined for
|
|
84
|
+
* a growing log; equal or decreasing sizes have no proof shape to check.
|
|
85
|
+
*/
|
|
86
|
+
export class SizeMustIncrease extends ConsistencyShapeError {
|
|
87
|
+
sizeFrom;
|
|
88
|
+
sizeTo;
|
|
89
|
+
constructor(sizeFrom, sizeTo) {
|
|
90
|
+
super(`sizeTo (${sizeTo}) must exceed sizeFrom (${sizeFrom})`);
|
|
91
|
+
this.sizeFrom = sizeFrom;
|
|
92
|
+
this.sizeTo = sizeTo;
|
|
93
|
+
this.name = "SizeMustIncrease";
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/mmr/proof.d.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** Reads the 32-byte node at MMR index `i` from some backing store. */
|
|
13
13
|
export type NodeGetter = (i: bigint) => Uint8Array;
|
|
14
|
+
/** Number of bits needed to represent `num` (0 for 0). */
|
|
15
|
+
export declare function bitLength(num: bigint): number;
|
|
14
16
|
/**
|
|
15
17
|
* Zero-based height index of the node at MMR index `i` (leaves are 0).
|
|
16
18
|
* (go-merklelog `mmr/indexheight.go` IndexHeight)
|
|
@@ -68,6 +70,22 @@ export declare function inclusionProofLocalExtend(get: NodeGetter, mmrSizeA: big
|
|
|
68
70
|
* subtree (peak). (go-merklelog `mmr/peaks.go` PeaksBitmap)
|
|
69
71
|
*/
|
|
70
72
|
export declare function peaksBitmap(mmrSize: bigint): bigint;
|
|
73
|
+
/**
|
|
74
|
+
* Node count of the complete MMR with `leaves` leaves (reference
|
|
75
|
+
* `mmr_size_for_leaf_count`, Solidity `mmrSizeForLeafCount`).
|
|
76
|
+
*
|
|
77
|
+
* Every leaf adds itself plus one interior node per binary carry, and each
|
|
78
|
+
* peak is a carry that has not happened, so the count is
|
|
79
|
+
* `2 * leaves - popcount(leaves)`.
|
|
80
|
+
*
|
|
81
|
+
* Because {@link peaksBitmap} rounds an incomplete size down to the largest
|
|
82
|
+
* complete MMR below it, `mmrSizeForLeafCount(peaksBitmap(size)) === size`
|
|
83
|
+
* holds exactly for complete sizes; that identity is the completeness test
|
|
84
|
+
* used by `consistentRootsForSizes`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mmrSizeForLeafCount(leaves: bigint): bigint;
|
|
87
|
+
/** Population count of a non-negative BigInt. */
|
|
88
|
+
export declare function popcount(x: bigint): number;
|
|
71
89
|
/**
|
|
72
90
|
* Accumulator slot (left-to-right) committed by an inclusion proof of length
|
|
73
91
|
* `proofLen` in a tree of size `mmrSize`. (server resolve-receipt.ts:735,
|
package/dist/mmr/proof.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../../src/mmr/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,uEAAuE;AACvE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../../src/mmr/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,uEAAuE;AACvE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,UAAU,CAAC;AAEnD,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG7C;AAqCD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,UAAU,EACf,YAAY,EAAE,MAAM,EACpB,CAAC,EAAE,MAAM,GACR,UAAU,EAAE,CAyBd;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAmB5E;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,CAAC,EAAE,MAAM,GACR,qBAAqB,CA4CvB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ1C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CAIR;AAQD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAczD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,CAAC,EAAE,MAAM,GACR,MAAM,CAKR;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAWrB"}
|
package/dist/mmr/proof.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* browser-safe (ADR-0048). Function names mirror the go-merklelog originals.
|
|
11
11
|
*/
|
|
12
12
|
/** Number of bits needed to represent `num` (0 for 0). */
|
|
13
|
-
function bitLength(num) {
|
|
13
|
+
export function bitLength(num) {
|
|
14
14
|
if (num <= 0n)
|
|
15
15
|
return 0;
|
|
16
16
|
return num.toString(2).length;
|
|
@@ -193,8 +193,24 @@ export function peaksBitmap(mmrSize) {
|
|
|
193
193
|
}
|
|
194
194
|
return peakMap;
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Node count of the complete MMR with `leaves` leaves (reference
|
|
198
|
+
* `mmr_size_for_leaf_count`, Solidity `mmrSizeForLeafCount`).
|
|
199
|
+
*
|
|
200
|
+
* Every leaf adds itself plus one interior node per binary carry, and each
|
|
201
|
+
* peak is a carry that has not happened, so the count is
|
|
202
|
+
* `2 * leaves - popcount(leaves)`.
|
|
203
|
+
*
|
|
204
|
+
* Because {@link peaksBitmap} rounds an incomplete size down to the largest
|
|
205
|
+
* complete MMR below it, `mmrSizeForLeafCount(peaksBitmap(size)) === size`
|
|
206
|
+
* holds exactly for complete sizes; that identity is the completeness test
|
|
207
|
+
* used by `consistentRootsForSizes`.
|
|
208
|
+
*/
|
|
209
|
+
export function mmrSizeForLeafCount(leaves) {
|
|
210
|
+
return 2n * leaves - BigInt(popcount(leaves));
|
|
211
|
+
}
|
|
196
212
|
/** Population count of a non-negative BigInt. */
|
|
197
|
-
function popcount(x) {
|
|
213
|
+
export function popcount(x) {
|
|
198
214
|
let count = 0;
|
|
199
215
|
let v = x;
|
|
200
216
|
while (v > 0n) {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -71,9 +71,20 @@ export {
|
|
|
71
71
|
verifyInclusion,
|
|
72
72
|
verifyConsistency,
|
|
73
73
|
consistentRoots,
|
|
74
|
+
consistentRootsForSizes,
|
|
74
75
|
indexConsistencyProof,
|
|
75
76
|
type ConsistencyProof,
|
|
76
77
|
} from "./mmr/algorithms.js";
|
|
78
|
+
// Typed proof-shape rejections raised by consistentRootsForSizes (names
|
|
79
|
+
// mirror the Solidity IUnivocityErrors members) — plan-2609-10 s4.3, FOR-568.
|
|
80
|
+
export {
|
|
81
|
+
ConsistencyShapeError,
|
|
82
|
+
IncompleteTreeSize,
|
|
83
|
+
ConsistencyPeakCountMismatch,
|
|
84
|
+
ConsistencyPathLengthMismatch,
|
|
85
|
+
ConsistencyRootMismatch,
|
|
86
|
+
SizeMustIncrease,
|
|
87
|
+
} from "./mmr/errors.js";
|
|
77
88
|
export { createSyncHasher } from "./mmr/algorithms-sync.js";
|
|
78
89
|
export type { Proof, Peak } from "./mmr/types.js";
|
|
79
90
|
export type { Hasher } from "./mmr/types.js";
|
|
@@ -86,6 +97,7 @@ export {
|
|
|
86
97
|
peakMMRIndexes,
|
|
87
98
|
peaksBitmap,
|
|
88
99
|
peakIndexForLeafProof,
|
|
100
|
+
mmrSizeForLeafCount,
|
|
89
101
|
indexHeight,
|
|
90
102
|
firstMMRSize,
|
|
91
103
|
massifIndexFromMMRIndex,
|
package/src/mmr/algorithms.ts
CHANGED
|
@@ -12,7 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { Proof, Hasher } from "./types.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
inclusionProof,
|
|
17
|
+
peakMMRIndexes,
|
|
18
|
+
bitLength,
|
|
19
|
+
popcount,
|
|
20
|
+
peaksBitmap,
|
|
21
|
+
mmrSizeForLeafCount,
|
|
22
|
+
type NodeGetter,
|
|
23
|
+
} from "./proof.js";
|
|
24
|
+
import {
|
|
25
|
+
ConsistencyPathLengthMismatch,
|
|
26
|
+
ConsistencyPeakCountMismatch,
|
|
27
|
+
ConsistencyRootMismatch,
|
|
28
|
+
IncompleteTreeSize,
|
|
29
|
+
SizeMustIncrease,
|
|
30
|
+
} from "./errors.js";
|
|
16
31
|
import { Uint64 } from "../uint64/index.js";
|
|
17
32
|
import { heightIndex } from "./math.js";
|
|
18
33
|
import { arraysEqual } from "../utils/arrays.js";
|
|
@@ -185,6 +200,12 @@ export interface ConsistencyProof {
|
|
|
185
200
|
* duplicate roots collapse (many old peaks share one new peak). Requires
|
|
186
201
|
* one path per MMR(A) peak (draft: `len(peaks(ifrom)) == len(accumulatorfrom)`).
|
|
187
202
|
*
|
|
203
|
+
* This fold does not enforce the proof shape the two sizes imply: it reads
|
|
204
|
+
* whatever path lengths it is given and collapses whatever roots coincide.
|
|
205
|
+
* Use {@link consistentRootsForSizes} for verification — it checks each path
|
|
206
|
+
* against the length MMR(A) -> MMR(B) fixes and requires the paths under one
|
|
207
|
+
* target peak to agree.
|
|
208
|
+
*
|
|
188
209
|
* @param ifrom - last node index of the complete MMR(A) (`mmrSizeA - 1`)
|
|
189
210
|
* @param accumulatorFrom - MMR(A) peak values, descending height order
|
|
190
211
|
* @param paths - inclusion path per peak, proven in MMR(B)
|
|
@@ -222,50 +243,212 @@ export async function consistentRoots(
|
|
|
222
243
|
return roots;
|
|
223
244
|
}
|
|
224
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Produce the peaks of MMR(sizeTo) that `paths` prove from the peaks of
|
|
248
|
+
* MMR(sizeFrom), requiring the paths to have exactly the shape the two sizes
|
|
249
|
+
* imply (draft-bryce-cose-receipts-mmr-profile, "Verifying the Receipt of
|
|
250
|
+
* consistency", with the draft's SHOULD on path lengths enforced in the same
|
|
251
|
+
* pass). Line-for-line port of the reference `consistent_roots_for_sizes`
|
|
252
|
+
* (algorithms.py) and of Solidity `consistentRootsForSizes`
|
|
253
|
+
* (univocity `src/algorithms/consistentRoots.sol`).
|
|
254
|
+
*
|
|
255
|
+
* Sizes are node counts (the MMR ending at index `i` has `i + 1` nodes).
|
|
256
|
+
* `sizeFrom` MUST be the size of the state the verifier already trusts; taken
|
|
257
|
+
* from the proof instead, the check is void. Only `sizeTo` is required to be
|
|
258
|
+
* a complete MMR size: every trusted size was itself a checked target.
|
|
259
|
+
*
|
|
260
|
+
* For a complete MMR the set bits of `peaksBitmap(size)` are the peak heights,
|
|
261
|
+
* high to low, which is accumulator order. Let `split` be the highest bit on
|
|
262
|
+
* which the two bitmaps differ; as `sizeTo > sizeFrom` the target has it and
|
|
263
|
+
* the origin does not. An origin peak above `split` is also a peak of the
|
|
264
|
+
* target: its path is empty and it is returned unchanged. Every origin peak
|
|
265
|
+
* below `split` is committed by the target peak of height `split`: its path
|
|
266
|
+
* has length `split - h`, and every such path must prove the same root. The
|
|
267
|
+
* target's remaining peaks lie below every origin peak, so no path reaches
|
|
268
|
+
* them; the prover supplies them separately and their count is returned.
|
|
269
|
+
*
|
|
270
|
+
* Because the shape is fixed by the sizes alone, no peak index list and no
|
|
271
|
+
* per-hop bookkeeping beyond the hash itself is needed: the bitmaps are
|
|
272
|
+
* iterated directly.
|
|
273
|
+
*
|
|
274
|
+
* @param hasher - cryptographic hasher instance
|
|
275
|
+
* @param sizeFrom - node count of the trusted origin state (0 for an empty log)
|
|
276
|
+
* @param sizeTo - node count of the target state; must exceed `sizeFrom` and
|
|
277
|
+
* must be a complete MMR size
|
|
278
|
+
* @param accumulatorFrom - peaks of MMR(sizeFrom), descending height order
|
|
279
|
+
* @param paths - one path per origin peak, in the same order
|
|
280
|
+
* @returns `roots`, the peaks of MMR(sizeTo) proven from the origin peaks in
|
|
281
|
+
* descending height order (the unchanged peaks, then the one proven root if
|
|
282
|
+
* any), and `expectedRight`, the number of MMR(sizeTo) peaks the prover must
|
|
283
|
+
* supply as right peaks. `roots` followed by those right peaks is the
|
|
284
|
+
* accumulator of MMR(sizeTo).
|
|
285
|
+
* @throws {SizeMustIncrease} if `sizeTo <= sizeFrom`
|
|
286
|
+
* @throws {IncompleteTreeSize} if `sizeTo` is not a complete MMR size
|
|
287
|
+
* @throws {ConsistencyPeakCountMismatch} if `accumulatorFrom` or `paths` does
|
|
288
|
+
* not have one entry per origin peak
|
|
289
|
+
* @throws {ConsistencyPathLengthMismatch} if a path length differs from the
|
|
290
|
+
* length the two sizes imply
|
|
291
|
+
* @throws {ConsistencyRootMismatch} if two paths under one target peak produce
|
|
292
|
+
* different roots
|
|
293
|
+
*/
|
|
294
|
+
export async function consistentRootsForSizes(
|
|
295
|
+
hasher: Hasher,
|
|
296
|
+
sizeFrom: bigint,
|
|
297
|
+
sizeTo: bigint,
|
|
298
|
+
accumulatorFrom: Uint8Array[],
|
|
299
|
+
paths: Uint8Array[][],
|
|
300
|
+
): Promise<{ roots: Uint8Array[]; expectedRight: number }> {
|
|
301
|
+
if (sizeTo <= sizeFrom) {
|
|
302
|
+
throw new SizeMustIncrease(sizeFrom, sizeTo);
|
|
303
|
+
}
|
|
304
|
+
const to = peaksBitmap(sizeTo);
|
|
305
|
+
// peaksBitmap rounds an incomplete size down to the largest MMR below it,
|
|
306
|
+
// so `to` describes MMR(sizeTo) only if sizeTo is complete. Without this a
|
|
307
|
+
// target such as 6 anchors an accumulator that is no MMR's, and a verifier
|
|
308
|
+
// later reads its entries at the wrong heights.
|
|
309
|
+
if (mmrSizeForLeafCount(to) !== sizeTo) {
|
|
310
|
+
throw new IncompleteTreeSize(sizeTo);
|
|
311
|
+
}
|
|
312
|
+
const from = peaksBitmap(sizeFrom);
|
|
313
|
+
const n = popcount(from);
|
|
314
|
+
if (accumulatorFrom.length !== n) {
|
|
315
|
+
throw new ConsistencyPeakCountMismatch(n, accumulatorFrom.length);
|
|
316
|
+
}
|
|
317
|
+
if (paths.length !== n) {
|
|
318
|
+
throw new ConsistencyPeakCountMismatch(n, paths.length);
|
|
319
|
+
}
|
|
320
|
+
const nto = popcount(to);
|
|
321
|
+
if (n === 0) {
|
|
322
|
+
return { roots: [], expectedRight: nto };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const split = bitLength(from ^ to) - 1;
|
|
326
|
+
const roots: Uint8Array[] = [];
|
|
327
|
+
// Nodes preceding the current origin peak's subtree; a peak of height h
|
|
328
|
+
// sits at offset + 2^(h+1) - 2 and its subtree has 2^(h+1) - 1 nodes.
|
|
329
|
+
let offset = 0n;
|
|
330
|
+
let i = 0;
|
|
331
|
+
|
|
332
|
+
// Origin peaks above the split are also peaks of the target. The path is
|
|
333
|
+
// not read; requiring it to be empty rejects unused material (a shape
|
|
334
|
+
// check: the result does not depend on it).
|
|
335
|
+
for (let h = bitLength(from) - 1; h > split; h--) {
|
|
336
|
+
if (((from >> BigInt(h)) & 1n) === 0n) continue;
|
|
337
|
+
if (paths[i].length !== 0) {
|
|
338
|
+
throw new ConsistencyPathLengthMismatch(i, 0, paths[i].length);
|
|
339
|
+
}
|
|
340
|
+
roots.push(accumulatorFrom[i]);
|
|
341
|
+
offset += (1n << BigInt(h + 1)) - 1n;
|
|
342
|
+
i += 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Origin peaks below the split are all committed by the target peak of
|
|
346
|
+
// height `split` (bit `split` itself is clear in `from`), so each path must
|
|
347
|
+
// have length split - h and every path must prove the same root. The first
|
|
348
|
+
// `above` peaks were returned unchanged, so i === above at the first peak
|
|
349
|
+
// below the split.
|
|
350
|
+
const above = roots.length;
|
|
351
|
+
let root: Uint8Array | undefined;
|
|
352
|
+
for (let h = split - 1; h >= 0; h--) {
|
|
353
|
+
if (((from >> BigInt(h)) & 1n) === 0n) continue;
|
|
354
|
+
const expected = split - h;
|
|
355
|
+
if (paths[i].length !== expected) {
|
|
356
|
+
throw new ConsistencyPathLengthMismatch(i, expected, paths[i].length);
|
|
357
|
+
}
|
|
358
|
+
const subtree = (1n << BigInt(h + 1)) - 1n;
|
|
359
|
+
const peakMMRIndex = offset + subtree - 1n;
|
|
360
|
+
const proven = await calculateRoot(
|
|
361
|
+
hasher,
|
|
362
|
+
accumulatorFrom[i],
|
|
363
|
+
{ path: paths[i], mmrIndex: peakMMRIndex },
|
|
364
|
+
peakMMRIndex,
|
|
365
|
+
);
|
|
366
|
+
if (i === above) {
|
|
367
|
+
root = proven;
|
|
368
|
+
} else if (!arraysEqual(proven, root as Uint8Array)) {
|
|
369
|
+
throw new ConsistencyRootMismatch(i);
|
|
370
|
+
}
|
|
371
|
+
offset += subtree;
|
|
372
|
+
i += 1;
|
|
373
|
+
}
|
|
374
|
+
if (n > above) {
|
|
375
|
+
roots.push(root as Uint8Array);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { roots, expectedRight: nto - roots.length };
|
|
379
|
+
}
|
|
380
|
+
|
|
225
381
|
/**
|
|
226
382
|
* Verify MMR(A) is a committed prefix of MMR(B)
|
|
227
383
|
* (draft-bryce "Verifying the Receipt of consistency";
|
|
228
384
|
* go-merklelog `VerifyConsistency`).
|
|
229
385
|
*
|
|
386
|
+
* BOTH sizes are CALLER-SUPPLIED trusted state (ADR-0066 D5.4), never read
|
|
387
|
+
* off the proof: `sizeFrom` is the size of the state `peaksFrom` is the
|
|
388
|
+
* accumulator of, and `sizeTo` the size `peaksTo` is claimed to be the
|
|
389
|
+
* accumulator of. The proof contributes only `paths`. Taking either size
|
|
390
|
+
* from the proof makes the check void: the fold constrains a declared
|
|
391
|
+
* `sizeFrom` by the origin peak COUNT alone, which a whole family of sizes
|
|
392
|
+
* share, so one set of paths and peaks verified at many size pairs — with
|
|
393
|
+
* the draft's own MMR(11) -> MMR(39) material, 24 distinct pairs, of which
|
|
394
|
+
* one is the true relation. `indexConsistencyProof` still returns the
|
|
395
|
+
* {@link ConsistencyProof} wire type with its declared sizes; a caller that
|
|
396
|
+
* relays that type passes `proof.paths` here and supplies the sizes it
|
|
397
|
+
* trusts, comparing the declared sizes with them separately if it wants to
|
|
398
|
+
* report a disagreement.
|
|
399
|
+
*
|
|
400
|
+
* Routed through {@link consistentRootsForSizes}, so the proof must have the
|
|
401
|
+
* shape `sizeFrom -> sizeTo` implies: one path per MMR(A) peak, empty above
|
|
402
|
+
* the split and of length `split - h` below it, with every path below the
|
|
403
|
+
* split proving the same root. The proven roots must then be the leading
|
|
404
|
+
* entries of `peaksTo`, and `peaksTo` must hold exactly those plus the
|
|
405
|
+
* `expectedRight` right peaks no path reaches — so a truncated or padded
|
|
406
|
+
* target accumulator is rejected on its length alone.
|
|
407
|
+
*
|
|
230
408
|
* Replaces the plan-0027 always-true stub (FOR-368 Phase 1,
|
|
231
409
|
* plan-2607-29): the previous signature took two inclusion proofs and
|
|
232
410
|
* returned true unconditionally; no caller existed.
|
|
233
411
|
*
|
|
234
|
-
* @param
|
|
412
|
+
* @param sizeFrom - TRUSTED node count of MMR(A)
|
|
413
|
+
* @param sizeTo - TRUSTED node count of MMR(B)
|
|
235
414
|
* @param peaksFrom - TRUSTED MMR(A) accumulator (e.g. a signed checkpoint
|
|
236
415
|
* payload), descending height order
|
|
416
|
+
* @param paths - one inclusion path per MMR(A) peak, in the same order
|
|
237
417
|
* @param peaksTo - MMR(B) accumulator to prove against (e.g. an anchored
|
|
238
418
|
* on-chain state), descending height order
|
|
239
|
-
* @returns ok, with the MMR(B) accumulator on success
|
|
419
|
+
* @returns ok, with the MMR(B) accumulator on success. A value that does not
|
|
420
|
+
* match `peaksTo` returns `{ok: false, accumulator: []}`; a proof that does
|
|
421
|
+
* not have the shape the sizes imply throws one of the typed errors in
|
|
422
|
+
* `./errors.js` (all `ConsistencyShapeError` subclasses), which callers
|
|
423
|
+
* report as a malformed proof rather than a failed comparison.
|
|
240
424
|
*/
|
|
241
425
|
export async function verifyConsistency(
|
|
242
426
|
hasher: Hasher,
|
|
243
|
-
|
|
427
|
+
sizeFrom: bigint,
|
|
428
|
+
sizeTo: bigint,
|
|
244
429
|
peaksFrom: Uint8Array[],
|
|
430
|
+
paths: Uint8Array[][],
|
|
245
431
|
peaksTo: Uint8Array[],
|
|
246
432
|
): Promise<{ ok: boolean; accumulator: Uint8Array[] }> {
|
|
247
|
-
const
|
|
433
|
+
const { roots, expectedRight } = await consistentRootsForSizes(
|
|
248
434
|
hasher,
|
|
249
|
-
|
|
435
|
+
sizeFrom,
|
|
436
|
+
sizeTo,
|
|
250
437
|
peaksFrom,
|
|
251
|
-
|
|
438
|
+
paths,
|
|
252
439
|
);
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (ito >= peaksTo.length || !arraysEqual(peaksTo[ito], root)) {
|
|
440
|
+
// roots is the leading run of the MMR(B) accumulator and expectedRight
|
|
441
|
+
// counts the peaks below every MMR(A) peak, which no path reaches. Together
|
|
442
|
+
// they fix the length of MMR(B)'s accumulator, so the supplied peaksTo must
|
|
443
|
+
// have that length and must start with the proven roots.
|
|
444
|
+
if (peaksTo.length !== roots.length + expectedRight) {
|
|
445
|
+
return { ok: false, accumulator: [] };
|
|
446
|
+
}
|
|
447
|
+
for (let i = 0; i < roots.length; i++) {
|
|
448
|
+
if (!arraysEqual(peaksTo[i], roots[i])) {
|
|
263
449
|
return { ok: false, accumulator: [] };
|
|
264
450
|
}
|
|
265
451
|
}
|
|
266
|
-
// The full MMR(B) accumulator is the proven prefix plus any right-peaks;
|
|
267
|
-
// returning peaksTo is safe because consistentRoots enforced one proof
|
|
268
|
-
// per MMR(A) peak (see go-merklelog VerifyConsistency).
|
|
269
452
|
return { ok: true, accumulator: peaksTo };
|
|
270
453
|
}
|
|
271
454
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors for the size-driven consistency fold
|
|
3
|
+
* (`consistentRootsForSizes`).
|
|
4
|
+
*
|
|
5
|
+
* The names mirror the Solidity `IUnivocityErrors` members raised by the
|
|
6
|
+
* on-chain implementation (`univocity/src/algorithms/consistentRoots.sol`),
|
|
7
|
+
* so a shape rejection reads the same on both sides. Every one of these
|
|
8
|
+
* reports a condition the two sizes fix in advance — the proof does not
|
|
9
|
+
* have the shape MMR(sizeFrom) -> MMR(sizeTo) implies — as distinct from a
|
|
10
|
+
* value mismatch against a trusted accumulator, which callers surface as a
|
|
11
|
+
* plain verification failure.
|
|
12
|
+
*
|
|
13
|
+
* All derive from {@link ConsistencyShapeError} so a caller can map the whole
|
|
14
|
+
* family to "malformed proof" with a single `instanceof`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Base class for every consistency proof-shape rejection. */
|
|
18
|
+
export class ConsistencyShapeError extends Error {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "ConsistencyShapeError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The target size is not a complete MMR size.
|
|
27
|
+
*
|
|
28
|
+
* `peaksBitmap` rounds an incomplete size down to the largest complete MMR
|
|
29
|
+
* below it, so for an incomplete `sizeTo` the bitmap describes a different
|
|
30
|
+
* tree than the one named. Accepting it would anchor an accumulator that is
|
|
31
|
+
* no MMR's, and a later verifier would read its entries at the wrong heights.
|
|
32
|
+
*/
|
|
33
|
+
export class IncompleteTreeSize extends ConsistencyShapeError {
|
|
34
|
+
constructor(readonly size: bigint) {
|
|
35
|
+
super(`size ${size} is not a complete MMR size`);
|
|
36
|
+
this.name = "IncompleteTreeSize";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The accumulator or the proof list does not have one entry per origin peak.
|
|
42
|
+
* The origin peak count is fixed by `sizeFrom` alone.
|
|
43
|
+
*/
|
|
44
|
+
export class ConsistencyPeakCountMismatch extends ConsistencyShapeError {
|
|
45
|
+
constructor(
|
|
46
|
+
readonly expected: number,
|
|
47
|
+
readonly actual: number,
|
|
48
|
+
) {
|
|
49
|
+
super(`expected ${expected} entries for the origin peaks, got ${actual}`);
|
|
50
|
+
this.name = "ConsistencyPeakCountMismatch";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A path does not have the length the two sizes imply: 0 for an origin peak
|
|
56
|
+
* above the split, `split - h` for an origin peak of height `h` below it.
|
|
57
|
+
*/
|
|
58
|
+
export class ConsistencyPathLengthMismatch extends ConsistencyShapeError {
|
|
59
|
+
constructor(
|
|
60
|
+
readonly index: number,
|
|
61
|
+
readonly expected: number,
|
|
62
|
+
readonly actual: number,
|
|
63
|
+
) {
|
|
64
|
+
super(`path ${index}: expected length ${expected}, got ${actual}`);
|
|
65
|
+
this.name = "ConsistencyPathLengthMismatch";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Two origin peaks below the split proved different roots. Every origin peak
|
|
71
|
+
* below the split is committed by the single target peak of height `split`,
|
|
72
|
+
* so all of their paths must produce the same value.
|
|
73
|
+
*/
|
|
74
|
+
export class ConsistencyRootMismatch extends ConsistencyShapeError {
|
|
75
|
+
constructor(readonly index: number) {
|
|
76
|
+
super(`path ${index} proves a different root from the paths before it`);
|
|
77
|
+
this.name = "ConsistencyRootMismatch";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The target size does not exceed the origin size. Consistency is defined for
|
|
83
|
+
* a growing log; equal or decreasing sizes have no proof shape to check.
|
|
84
|
+
*/
|
|
85
|
+
export class SizeMustIncrease extends ConsistencyShapeError {
|
|
86
|
+
constructor(
|
|
87
|
+
readonly sizeFrom: bigint,
|
|
88
|
+
readonly sizeTo: bigint,
|
|
89
|
+
) {
|
|
90
|
+
super(`sizeTo (${sizeTo}) must exceed sizeFrom (${sizeFrom})`);
|
|
91
|
+
this.name = "SizeMustIncrease";
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/mmr/proof.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
export type NodeGetter = (i: bigint) => Uint8Array;
|
|
15
15
|
|
|
16
16
|
/** Number of bits needed to represent `num` (0 for 0). */
|
|
17
|
-
function bitLength(num: bigint): number {
|
|
17
|
+
export function bitLength(num: bigint): number {
|
|
18
18
|
if (num <= 0n) return 0;
|
|
19
19
|
return num.toString(2).length;
|
|
20
20
|
}
|
|
@@ -230,8 +230,25 @@ export function peaksBitmap(mmrSize: bigint): bigint {
|
|
|
230
230
|
return peakMap;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Node count of the complete MMR with `leaves` leaves (reference
|
|
235
|
+
* `mmr_size_for_leaf_count`, Solidity `mmrSizeForLeafCount`).
|
|
236
|
+
*
|
|
237
|
+
* Every leaf adds itself plus one interior node per binary carry, and each
|
|
238
|
+
* peak is a carry that has not happened, so the count is
|
|
239
|
+
* `2 * leaves - popcount(leaves)`.
|
|
240
|
+
*
|
|
241
|
+
* Because {@link peaksBitmap} rounds an incomplete size down to the largest
|
|
242
|
+
* complete MMR below it, `mmrSizeForLeafCount(peaksBitmap(size)) === size`
|
|
243
|
+
* holds exactly for complete sizes; that identity is the completeness test
|
|
244
|
+
* used by `consistentRootsForSizes`.
|
|
245
|
+
*/
|
|
246
|
+
export function mmrSizeForLeafCount(leaves: bigint): bigint {
|
|
247
|
+
return 2n * leaves - BigInt(popcount(leaves));
|
|
248
|
+
}
|
|
249
|
+
|
|
233
250
|
/** Population count of a non-negative BigInt. */
|
|
234
|
-
function popcount(x: bigint): number {
|
|
251
|
+
export function popcount(x: bigint): number {
|
|
235
252
|
let count = 0;
|
|
236
253
|
let v = x;
|
|
237
254
|
while (v > 0n) {
|