@forestrie/merklelog 0.0.2 → 0.1.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 +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/massifs/leafindex.d.ts +47 -0
- package/dist/massifs/leafindex.d.ts.map +1 -0
- package/dist/massifs/leafindex.js +117 -0
- package/dist/massifs/nodestore.d.ts +30 -0
- package/dist/massifs/nodestore.d.ts.map +1 -0
- package/dist/massifs/nodestore.js +70 -0
- package/dist/mmr/algorithms-sync.d.ts.map +1 -1
- package/dist/mmr/algorithms-sync.js +7 -1
- package/dist/mmr/proof.d.ts +62 -0
- package/dist/mmr/proof.d.ts.map +1 -0
- package/dist/mmr/proof.js +194 -0
- package/package.json +4 -2
- package/src/index.ts +21 -0
- package/src/massifs/leafindex.ts +144 -0
- package/src/massifs/nodestore.ts +95 -0
- package/src/mmr/algorithms-sync.ts +7 -1
- package/src/mmr/proof.ts +219 -0
package/dist/index.d.ts
CHANGED
|
@@ -12,10 +12,16 @@ export type { MassifStart, LeafComponent, LeafEnumeratorSpec, LeafEntry, } from
|
|
|
12
12
|
export { MassifStartFmt } from "./massifs/types.js";
|
|
13
13
|
export { LogFormat, Urkle, Bloom, IndexV2, leafCountForMassifHeight, leafTableBytes, nodeCountMax, nodeStoreBytes, bloomMBits, bloomBitsetBytes, bloomRegionBytes, indexDataBytesV2, peakStackEnd, massifLogEntries, urkleLeafTableStartFieldIndex, urkleLeafTableStartByteOffset, createLeafEnumerator, leafComponentByteOffset, leafComponentSize, computeLastMMRIndex, isMassifFull, } from "./massifs/types.js";
|
|
14
14
|
export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
|
|
15
|
+
export { openMassifNodeStore } from "./massifs/nodestore.js";
|
|
16
|
+
export type { MassifNodeStore } from "./massifs/nodestore.js";
|
|
17
|
+
export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
|
|
18
|
+
export type { MassifLeafIndex } from "./massifs/leafindex.js";
|
|
15
19
|
export { mmrIndex, massifFirstLeaf, leafMinusSpurSum } from "./mmr/index.js";
|
|
16
20
|
export { heightIndex, height, mmrIndexFromLeafIndex, leafIndex, mmrPosition, mmrSizeFromHeightIndex, leafCount, leafCountFromHeightIndex, } from "./mmr/math.js";
|
|
17
21
|
export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, } from "./mmr/algorithms.js";
|
|
18
22
|
export { createSyncHasher } from "./mmr/algorithms-sync.js";
|
|
19
23
|
export type { Proof, Peak } from "./mmr/types.js";
|
|
20
24
|
export type { Hasher } from "./mmr/types.js";
|
|
25
|
+
export { inclusionProof, peakMMRIndexes, peaksBitmap, peakIndexForLeafProof, indexHeight, firstMMRSize, massifIndexFromMMRIndex, peakStackMap, } from "./mmr/proof.js";
|
|
26
|
+
export type { NodeGetter } from "./mmr/proof.js";
|
|
21
27
|
//# 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,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,GAClB,MAAM,qBAAqB,CAAC;AAC7B,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"}
|
|
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,MAAM,wBAAwB,CAAC;AAG9D,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,GAClB,MAAM,qBAAqB,CAAC;AAC7B,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,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,WAAW,EACX,YAAY,EACZ,uBAAuB,EACvB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,14 @@ export { Massif } from "./massifs/massif.js";
|
|
|
13
13
|
export { MassifStartFmt } from "./massifs/types.js";
|
|
14
14
|
export { LogFormat, Urkle, Bloom, IndexV2, leafCountForMassifHeight, leafTableBytes, nodeCountMax, nodeStoreBytes, bloomMBits, bloomBitsetBytes, bloomRegionBytes, indexDataBytesV2, peakStackEnd, massifLogEntries, urkleLeafTableStartFieldIndex, urkleLeafTableStartByteOffset, createLeafEnumerator, leafComponentByteOffset, leafComponentSize, computeLastMMRIndex, isMassifFull, } from "./massifs/types.js";
|
|
15
15
|
export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
|
|
16
|
+
// Massif blob node access for MMR proof building (plan-2607-15 §4)
|
|
17
|
+
export { openMassifNodeStore } from "./massifs/nodestore.js";
|
|
18
|
+
// Content-hash leaf lookup over the massif index region (FOR-373)
|
|
19
|
+
export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
|
|
16
20
|
// Export mmr module
|
|
17
21
|
export { mmrIndex, massifFirstLeaf, leafMinusSpurSum } from "./mmr/index.js";
|
|
18
22
|
export { heightIndex, height, mmrIndexFromLeafIndex, leafIndex, mmrPosition, mmrSizeFromHeightIndex, leafCount, leafCountFromHeightIndex, } from "./mmr/math.js";
|
|
19
23
|
export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, } from "./mmr/algorithms.js";
|
|
20
24
|
export { createSyncHasher } from "./mmr/algorithms-sync.js";
|
|
25
|
+
// MMR proof building (pure; store-agnostic) — plan-2607-15 §4 (go: mmr/proof.go, peaks.go)
|
|
26
|
+
export { inclusionProof, peakMMRIndexes, peaksBitmap, peakIndexForLeafProof, indexHeight, firstMMRSize, massifIndexFromMMRIndex, peakStackMap, } from "./mmr/proof.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-hash → mmrIndex leaf lookup over a v2 massif index region (FOR-373).
|
|
3
|
+
*
|
|
4
|
+
* The v2 massif index region carries, per leaf ordinal, a fixed-width record
|
|
5
|
+
* whose committed `valueBytes[32]` is the leaf's content hash — the same value
|
|
6
|
+
* committed by the urkle trie (go-merklelog `urkle/leafrecord.go`,
|
|
7
|
+
* `massifs/indexformat_v2.go`; plan-2607-15 §1.1). The urkle trie itself is
|
|
8
|
+
* keyed by the 8-byte leaf key (idtimestamp) for duplicate/exclusion detection,
|
|
9
|
+
* so content-hash resolution reads the leaf table's value column directly and
|
|
10
|
+
* maps the matching ordinal back to an MMR index
|
|
11
|
+
* (`urkle.LeafOrdinalToMMRIndex`).
|
|
12
|
+
*
|
|
13
|
+
* This lets `forestrie create-receipt --content-hash <hex>` locate a leaf's
|
|
14
|
+
* mmrIndex from a massif blob alone, with no external mapping. A negative
|
|
15
|
+
* lookup (`null`) is distinguishable from absence-of-index: a blob with no
|
|
16
|
+
* index region throws `MissingIndexError`, while a searched-but-not-present
|
|
17
|
+
* hash returns `null`.
|
|
18
|
+
*
|
|
19
|
+
* Browser-safe: pure buffer arithmetic, no node builtins (ADR-0048).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when the blob carries no populated index region, so a content-hash
|
|
23
|
+
* lookup cannot even be attempted. Callers distinguish this from a `null`
|
|
24
|
+
* (searched, not found) result.
|
|
25
|
+
*/
|
|
26
|
+
export declare class MissingIndexError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
/** Content-hash leaf lookup over one massif blob (FOR-373). */
|
|
30
|
+
export interface MassifLeafIndex {
|
|
31
|
+
/**
|
|
32
|
+
* MMR index of the leaf whose committed content hash equals `h`, or `null`
|
|
33
|
+
* when no populated leaf in this massif carries that hash. `h` must be 32
|
|
34
|
+
* bytes.
|
|
35
|
+
*/
|
|
36
|
+
findByContentHash(h: Uint8Array): bigint | null;
|
|
37
|
+
/** Number of populated leaves in this blob. */
|
|
38
|
+
leafCount: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Open a v2 massif blob for content-hash leaf lookups.
|
|
42
|
+
*
|
|
43
|
+
* @throws MissingIndexError if the blob is too short to contain the fixed index
|
|
44
|
+
* region for its declared massif height (no index present).
|
|
45
|
+
*/
|
|
46
|
+
export declare function openMassifLeafIndex(massifBytes: Uint8Array): MassifLeafIndex;
|
|
47
|
+
//# sourceMappingURL=leafindex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leafindex.d.ts","sourceRoot":"","sources":["../../src/massifs/leafindex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAeH;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC;IAChD,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;CACnB;AAkBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,UAAU,GAAG,eAAe,CA+D5E"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-hash → mmrIndex leaf lookup over a v2 massif index region (FOR-373).
|
|
3
|
+
*
|
|
4
|
+
* The v2 massif index region carries, per leaf ordinal, a fixed-width record
|
|
5
|
+
* whose committed `valueBytes[32]` is the leaf's content hash — the same value
|
|
6
|
+
* committed by the urkle trie (go-merklelog `urkle/leafrecord.go`,
|
|
7
|
+
* `massifs/indexformat_v2.go`; plan-2607-15 §1.1). The urkle trie itself is
|
|
8
|
+
* keyed by the 8-byte leaf key (idtimestamp) for duplicate/exclusion detection,
|
|
9
|
+
* so content-hash resolution reads the leaf table's value column directly and
|
|
10
|
+
* maps the matching ordinal back to an MMR index
|
|
11
|
+
* (`urkle.LeafOrdinalToMMRIndex`).
|
|
12
|
+
*
|
|
13
|
+
* This lets `forestrie create-receipt --content-hash <hex>` locate a leaf's
|
|
14
|
+
* mmrIndex from a massif blob alone, with no external mapping. A negative
|
|
15
|
+
* lookup (`null`) is distinguishable from absence-of-index: a blob with no
|
|
16
|
+
* index region throws `MissingIndexError`, while a searched-but-not-present
|
|
17
|
+
* hash returns `null`.
|
|
18
|
+
*
|
|
19
|
+
* Browser-safe: pure buffer arithmetic, no node builtins (ADR-0048).
|
|
20
|
+
*/
|
|
21
|
+
import { Massif } from "./massif.js";
|
|
22
|
+
import { LogFormat } from "./logformat.js";
|
|
23
|
+
import { Urkle, leafCountForMassifHeight } from "./indexformat.js";
|
|
24
|
+
import { urkleLeafTableStartByteOffset } from "./urkleindex.js";
|
|
25
|
+
import { peakStackEnd } from "./peakstackend.js";
|
|
26
|
+
import { massifLogEntries } from "./massiflogentries.js";
|
|
27
|
+
import { firstMMRSize, peaksBitmap } from "../mmr/proof.js";
|
|
28
|
+
import { mmrIndex as mmrIndexFromLeafIndex } from "../mmr/index.js";
|
|
29
|
+
const HASH_BYTES = Urkle.LeafValueBytes; // 32
|
|
30
|
+
const VALUE_OFFSET = Urkle.LeafValueOffset; // 8 (after the 8-byte key)
|
|
31
|
+
const RECORD_BYTES = Urkle.LeafRecordBytes; // 128
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when the blob carries no populated index region, so a content-hash
|
|
34
|
+
* lookup cannot even be attempted. Callers distinguish this from a `null`
|
|
35
|
+
* (searched, not found) result.
|
|
36
|
+
*/
|
|
37
|
+
export class MissingIndexError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "MissingIndexError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function equals32(a, aOff, b) {
|
|
44
|
+
for (let i = 0; i < HASH_BYTES; i++) {
|
|
45
|
+
if (a[aOff + i] !== b[i])
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Leaf index of the first leaf in the massif whose first MMR index is
|
|
52
|
+
* `firstMMRIndex`. (go-merklelog `mmr/leafcount.go` LeafIndex)
|
|
53
|
+
*/
|
|
54
|
+
function leafIndexFromMMRIndex(firstMMRIndex) {
|
|
55
|
+
if (firstMMRIndex === 0n)
|
|
56
|
+
return 0n;
|
|
57
|
+
return peaksBitmap(firstMMRSize(firstMMRIndex)) - 1n;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Open a v2 massif blob for content-hash leaf lookups.
|
|
61
|
+
*
|
|
62
|
+
* @throws MissingIndexError if the blob is too short to contain the fixed index
|
|
63
|
+
* region for its declared massif height (no index present).
|
|
64
|
+
*/
|
|
65
|
+
export function openMassifLeafIndex(massifBytes) {
|
|
66
|
+
const massif = new Massif(massifBytes);
|
|
67
|
+
const start = massif.getStart();
|
|
68
|
+
const massifHeight = start.massifHeight;
|
|
69
|
+
if (!Number.isInteger(massifHeight) ||
|
|
70
|
+
massifHeight < 1 ||
|
|
71
|
+
massifHeight > LogFormat.MaxMmrHeight) {
|
|
72
|
+
throw new Error(`massif header has invalid height ${massifHeight}`);
|
|
73
|
+
}
|
|
74
|
+
const firstIndex = start.firstIndex;
|
|
75
|
+
const leafTableStart = urkleLeafTableStartByteOffset(massifHeight);
|
|
76
|
+
const capacity = Number(leafCountForMassifHeight(massifHeight));
|
|
77
|
+
const leafTableBytes = capacity * RECORD_BYTES;
|
|
78
|
+
const stackEnd = peakStackEnd(massifHeight);
|
|
79
|
+
// The index region must fit before the fixed peak stack + log region. A blob
|
|
80
|
+
// shorter than that has no populated index to search.
|
|
81
|
+
if (massifHeight > 1 && BigInt(leafTableStart + leafTableBytes) > stackEnd) {
|
|
82
|
+
throw new Error("massif index region math is inconsistent");
|
|
83
|
+
}
|
|
84
|
+
if (BigInt(massifBytes.byteLength) < stackEnd) {
|
|
85
|
+
throw new MissingIndexError("massif blob too short for a v2 index region (no index present)");
|
|
86
|
+
}
|
|
87
|
+
// Only leaves actually appended have populated records. Derive the populated
|
|
88
|
+
// leaf count from the log node count (index records are written 1:1 with
|
|
89
|
+
// appended leaves). Never exceed the fixed capacity for the height.
|
|
90
|
+
const logNodeCount = massifLogEntries(massifBytes.byteLength, massifHeight);
|
|
91
|
+
const firstLeafIndex = leafIndexFromMMRIndex(firstIndex);
|
|
92
|
+
let populated = 0;
|
|
93
|
+
for (let ordinal = 0; ordinal < capacity; ordinal++) {
|
|
94
|
+
// MMR index of leaf (firstLeafIndex + ordinal), relative to this blob's
|
|
95
|
+
// firstIndex, must be within the log node count.
|
|
96
|
+
const leafMMRIndex = mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal));
|
|
97
|
+
if (leafMMRIndex - firstIndex >= logNodeCount)
|
|
98
|
+
break;
|
|
99
|
+
populated = ordinal + 1;
|
|
100
|
+
}
|
|
101
|
+
const findByContentHash = (h) => {
|
|
102
|
+
if (h.length !== HASH_BYTES) {
|
|
103
|
+
throw new Error(`content hash must be ${HASH_BYTES} bytes, got ${h.length}`);
|
|
104
|
+
}
|
|
105
|
+
for (let ordinal = 0; ordinal < populated; ordinal++) {
|
|
106
|
+
const recordOff = leafTableStart + ordinal * RECORD_BYTES;
|
|
107
|
+
const valueOff = recordOff + VALUE_OFFSET;
|
|
108
|
+
if (valueOff + HASH_BYTES > massifBytes.byteLength)
|
|
109
|
+
break;
|
|
110
|
+
if (equals32(massifBytes, valueOff, h)) {
|
|
111
|
+
return mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
return { findByContentHash, leafCount: populated };
|
|
117
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Massif blob node access for MMR proof building.
|
|
3
|
+
*
|
|
4
|
+
* A v2 massif `.log` blob is self-sufficient for any leaf→peak inclusion path
|
|
5
|
+
* whose leaf lives in that massif: local nodes give the in-massif siblings and
|
|
6
|
+
* the fixed ancestor peak stack gives everything older (plan-2607-15 §1.1).
|
|
7
|
+
* Node lookup is pure arithmetic — `LogStart + (i − firstIndex)·32` for local
|
|
8
|
+
* nodes, `peakStackMap` for ancestors.
|
|
9
|
+
*
|
|
10
|
+
* Ported from the inline copies in canopy-api `resolve-receipt.ts` and
|
|
11
|
+
* receipt-verify `build-receipt-offline.ts` (plan-2607-15 §4, phase 2).
|
|
12
|
+
* Browser-safe: pure buffer arithmetic, no node builtins (ADR-0048).
|
|
13
|
+
*/
|
|
14
|
+
/** Node access over a single massif blob. */
|
|
15
|
+
export interface MassifNodeStore {
|
|
16
|
+
/** 32-byte node at MMR index `i` (log region for `i >= firstIndex`, else the ancestor peak stack). */
|
|
17
|
+
get(i: bigint): Uint8Array;
|
|
18
|
+
massifHeight: number;
|
|
19
|
+
massifIndex: bigint;
|
|
20
|
+
firstIndex: bigint;
|
|
21
|
+
/** Last MMR index with log data in this blob. */
|
|
22
|
+
lastIndex: bigint;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Open a v2 massif blob for MMR node reads. Nodes below `firstIndex` resolve
|
|
26
|
+
* through the ancestor peak stack; nodes above `lastIndex` are not present in
|
|
27
|
+
* this blob and throw when requested.
|
|
28
|
+
*/
|
|
29
|
+
export declare function openMassifNodeStore(massifBytes: Uint8Array): MassifNodeStore;
|
|
30
|
+
//# sourceMappingURL=nodestore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nodestore.d.ts","sourceRoot":"","sources":["../../src/massifs/nodestore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,sGAAsG;IACtG,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;CACnB;AAYD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,UAAU,GAAG,eAAe,CA8C5E"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Massif blob node access for MMR proof building.
|
|
3
|
+
*
|
|
4
|
+
* A v2 massif `.log` blob is self-sufficient for any leaf→peak inclusion path
|
|
5
|
+
* whose leaf lives in that massif: local nodes give the in-massif siblings and
|
|
6
|
+
* the fixed ancestor peak stack gives everything older (plan-2607-15 §1.1).
|
|
7
|
+
* Node lookup is pure arithmetic — `LogStart + (i − firstIndex)·32` for local
|
|
8
|
+
* nodes, `peakStackMap` for ancestors.
|
|
9
|
+
*
|
|
10
|
+
* Ported from the inline copies in canopy-api `resolve-receipt.ts` and
|
|
11
|
+
* receipt-verify `build-receipt-offline.ts` (plan-2607-15 §4, phase 2).
|
|
12
|
+
* Browser-safe: pure buffer arithmetic, no node builtins (ADR-0048).
|
|
13
|
+
*/
|
|
14
|
+
import { Massif } from "./massif.js";
|
|
15
|
+
import { LogFormat } from "./logformat.js";
|
|
16
|
+
import { peakStackEnd } from "./peakstackend.js";
|
|
17
|
+
import { peakStackMap } from "../mmr/proof.js";
|
|
18
|
+
const VALUE_BYTES = BigInt(LogFormat.ValueBytes); // 32
|
|
19
|
+
const MAX_MMR_HEIGHT = BigInt(LogFormat.MaxMmrHeight); // 64
|
|
20
|
+
function slice32(buf, offset, label) {
|
|
21
|
+
if (offset < 0n || offset + VALUE_BYTES > BigInt(buf.byteLength)) {
|
|
22
|
+
throw new Error(`out of range read for ${label}: off=${offset.toString(10)}`);
|
|
23
|
+
}
|
|
24
|
+
const start = Number(offset);
|
|
25
|
+
return buf.slice(start, start + Number(VALUE_BYTES));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Open a v2 massif blob for MMR node reads. Nodes below `firstIndex` resolve
|
|
29
|
+
* through the ancestor peak stack; nodes above `lastIndex` are not present in
|
|
30
|
+
* this blob and throw when requested.
|
|
31
|
+
*/
|
|
32
|
+
export function openMassifNodeStore(massifBytes) {
|
|
33
|
+
const massif = new Massif(massifBytes);
|
|
34
|
+
const start = massif.getStart();
|
|
35
|
+
const massifHeight = start.massifHeight;
|
|
36
|
+
if (!Number.isInteger(massifHeight) ||
|
|
37
|
+
massifHeight < 1 ||
|
|
38
|
+
massifHeight > LogFormat.MaxMmrHeight) {
|
|
39
|
+
throw new Error(`massif header has invalid height ${massifHeight}`);
|
|
40
|
+
}
|
|
41
|
+
const massifIndex = BigInt(start.massifIndex);
|
|
42
|
+
const firstIndex = start.firstIndex;
|
|
43
|
+
const logStart = peakStackEnd(massifHeight);
|
|
44
|
+
const peakStackStart = logStart - MAX_MMR_HEIGHT * VALUE_BYTES;
|
|
45
|
+
const blobLen = BigInt(massifBytes.byteLength);
|
|
46
|
+
if (blobLen < logStart) {
|
|
47
|
+
throw new Error("massif blob too short for v2 layout");
|
|
48
|
+
}
|
|
49
|
+
const logNodeCount = (blobLen - logStart) / VALUE_BYTES;
|
|
50
|
+
const lastIndex = firstIndex + logNodeCount - 1n;
|
|
51
|
+
const stackMap = peakStackMap(massifHeight, firstIndex);
|
|
52
|
+
const get = (i) => {
|
|
53
|
+
if (i >= firstIndex) {
|
|
54
|
+
if (i > lastIndex) {
|
|
55
|
+
throw new Error(`mmr index ${i.toString(10)} is beyond this massif's log data ` +
|
|
56
|
+
`(last ${lastIndex.toString(10)}); local content does not cover ` +
|
|
57
|
+
`the requested tree size`);
|
|
58
|
+
}
|
|
59
|
+
const off = logStart + (i - firstIndex) * VALUE_BYTES;
|
|
60
|
+
return slice32(massifBytes, off, "log-data");
|
|
61
|
+
}
|
|
62
|
+
const peakIdx = stackMap.get(i);
|
|
63
|
+
if (peakIdx === undefined) {
|
|
64
|
+
throw new Error(`missing ancestor peak for mmr index ${i.toString(10)}`);
|
|
65
|
+
}
|
|
66
|
+
const off = peakStackStart + BigInt(peakIdx) * VALUE_BYTES;
|
|
67
|
+
return slice32(massifBytes, off, "peak-stack");
|
|
68
|
+
};
|
|
69
|
+
return { get, massifHeight, massifIndex, firstIndex, lastIndex };
|
|
70
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"algorithms-sync.d.ts","sourceRoot":"","sources":["../../src/mmr/algorithms-sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAazC;;;;;;GAMG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"algorithms-sync.d.ts","sourceRoot":"","sources":["../../src/mmr/algorithms-sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAazC;;;;;;GAMG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAuBxD"}
|
|
@@ -23,7 +23,13 @@ function concatChunks(chunks) {
|
|
|
23
23
|
* Only available in Node.js; throws if node:crypto is not available.
|
|
24
24
|
*/
|
|
25
25
|
export async function createSyncHasher() {
|
|
26
|
-
|
|
26
|
+
// Keep the specifier opaque so bundlers do not resolve node:crypto at
|
|
27
|
+
// build time: this factory is documented runtime-guarded Node-only, and
|
|
28
|
+
// browser consumers of the package index (e.g. @forestrie/receipt-verify's
|
|
29
|
+
// ADR-0048 browser-safety guard) must not get node builtins in their
|
|
30
|
+
// module graph merely by importing it.
|
|
31
|
+
const nodeCryptoSpecifier = "node:crypto";
|
|
32
|
+
const { createHash } = await import(/* @vite-ignore */ nodeCryptoSpecifier);
|
|
27
33
|
const chunks = [];
|
|
28
34
|
return {
|
|
29
35
|
reset() {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MMR proof building (pure, store-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Ports the proof-side arithmetic from go-merklelog `mmr/proof.go` and
|
|
5
|
+
* `mmr/peaks.go`. These were previously inline-duplicated in
|
|
6
|
+
* canopy-api `resolve-receipt.ts` and receipt-verify `build-receipt-offline.ts`;
|
|
7
|
+
* this is the single hoisted home (plan-2607-15 §4, phase 2).
|
|
8
|
+
*
|
|
9
|
+
* All arithmetic is BigInt and free of node builtins so the package stays
|
|
10
|
+
* browser-safe (ADR-0048). Function names mirror the go-merklelog originals.
|
|
11
|
+
*/
|
|
12
|
+
/** Reads the 32-byte node at MMR index `i` from some backing store. */
|
|
13
|
+
export type NodeGetter = (i: bigint) => Uint8Array;
|
|
14
|
+
/**
|
|
15
|
+
* Zero-based height index of the node at MMR index `i` (leaves are 0).
|
|
16
|
+
* (go-merklelog `mmr/indexheight.go` IndexHeight)
|
|
17
|
+
*/
|
|
18
|
+
export declare function indexHeight(i: bigint): number;
|
|
19
|
+
/**
|
|
20
|
+
* Witness path (siblings, ascending) for the node at MMR index `i` in the tree
|
|
21
|
+
* ending at `mmrLastIndex`. Terminates at the covering peak.
|
|
22
|
+
* (go-merklelog `mmr/proof.go` InclusionProof)
|
|
23
|
+
*
|
|
24
|
+
* @throws if `i > mmrLastIndex`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function inclusionProof(get: NodeGetter, mmrLastIndex: bigint, i: bigint): Uint8Array[];
|
|
27
|
+
/**
|
|
28
|
+
* Peaks bitmap for an MMR size: the numeric value equals the leaf count of the
|
|
29
|
+
* largest valid MMR with size <= `mmrSize`; each set bit marks a perfect
|
|
30
|
+
* subtree (peak). (go-merklelog `mmr/peaks.go` PeaksBitmap)
|
|
31
|
+
*/
|
|
32
|
+
export declare function peaksBitmap(mmrSize: bigint): bigint;
|
|
33
|
+
/**
|
|
34
|
+
* Accumulator slot (left-to-right) committed by an inclusion proof of length
|
|
35
|
+
* `proofLen` in a tree of size `mmrSize`. (server resolve-receipt.ts:735,
|
|
36
|
+
* equivalent to go `mmr.PeakIndex(LeafCount(mmrSize), proofLen)`)
|
|
37
|
+
*/
|
|
38
|
+
export declare function peakIndexForLeafProof(mmrSize: bigint, proofLen: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* MMR indices of the peaks of the tree ending at `mmrIndex` (inclusive),
|
|
41
|
+
* ascending. Empty when `mmrIndex + 1` is not a valid MMR size (siblings
|
|
42
|
+
* without a parent). (go-merklelog `mmr/peaks.go` Peaks)
|
|
43
|
+
*/
|
|
44
|
+
export declare function peakMMRIndexes(mmrIndex: bigint): bigint[];
|
|
45
|
+
/**
|
|
46
|
+
* Smallest complete MMR size that contains the node at MMR index `mmrIndex`.
|
|
47
|
+
* (go-merklelog `mmr/firstmmrsize.go` FirstMMRSize)
|
|
48
|
+
*/
|
|
49
|
+
export declare function firstMMRSize(mmrIndex: bigint): bigint;
|
|
50
|
+
/**
|
|
51
|
+
* Massif index (0-based) that holds the node at MMR index `i` for a log of the
|
|
52
|
+
* given massif height. (go-merklelog `massifs/massifindex.go` MassifIndexFromMMRIndex)
|
|
53
|
+
*/
|
|
54
|
+
export declare function massifIndexFromMMRIndex(massifHeight: number, i: bigint): bigint;
|
|
55
|
+
/**
|
|
56
|
+
* Map from ancestor-peak MMR index to peak-stack slot for the massif whose
|
|
57
|
+
* first MMR index is `firstIndex`. Nodes below `firstIndex` resolve through the
|
|
58
|
+
* fixed peak-stack region via this map.
|
|
59
|
+
* (go-merklelog `massifs/peakstack.go` PeakStackMap)
|
|
60
|
+
*/
|
|
61
|
+
export declare function peakStackMap(massifHeight: number, firstIndex: bigint): Map<bigint, number>;
|
|
62
|
+
//# sourceMappingURL=proof.d.ts.map
|
|
@@ -0,0 +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;AAoCnD;;;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;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcnD;AAaD;;;;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"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MMR proof building (pure, store-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Ports the proof-side arithmetic from go-merklelog `mmr/proof.go` and
|
|
5
|
+
* `mmr/peaks.go`. These were previously inline-duplicated in
|
|
6
|
+
* canopy-api `resolve-receipt.ts` and receipt-verify `build-receipt-offline.ts`;
|
|
7
|
+
* this is the single hoisted home (plan-2607-15 §4, phase 2).
|
|
8
|
+
*
|
|
9
|
+
* All arithmetic is BigInt and free of node builtins so the package stays
|
|
10
|
+
* browser-safe (ADR-0048). Function names mirror the go-merklelog originals.
|
|
11
|
+
*/
|
|
12
|
+
/** Number of bits needed to represent `num` (0 for 0). */
|
|
13
|
+
function bitLength(num) {
|
|
14
|
+
if (num <= 0n)
|
|
15
|
+
return 0;
|
|
16
|
+
return num.toString(2).length;
|
|
17
|
+
}
|
|
18
|
+
/** True when `num > 0` and its binary form is all ones. */
|
|
19
|
+
function allOnes(num) {
|
|
20
|
+
return num > 0n && (num & (num + 1n)) === 0n;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Jump left to the leftmost node at the same height.
|
|
24
|
+
* (go-merklelog `mmr/indexheight.go` JumpLeftPerfect)
|
|
25
|
+
*/
|
|
26
|
+
function jumpLeftPerfect(pos) {
|
|
27
|
+
const bl = bitLength(pos);
|
|
28
|
+
if (bl === 0)
|
|
29
|
+
return pos;
|
|
30
|
+
const msb = 1n << BigInt(bl - 1);
|
|
31
|
+
return pos - (msb - 1n);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Height of a 1-based position.
|
|
35
|
+
* (go-merklelog `mmr/indexheight.go` PosHeight)
|
|
36
|
+
*/
|
|
37
|
+
function posHeight(pos) {
|
|
38
|
+
let current = pos;
|
|
39
|
+
while (!allOnes(current)) {
|
|
40
|
+
current = jumpLeftPerfect(current);
|
|
41
|
+
}
|
|
42
|
+
return bitLength(current) - 1;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Zero-based height index of the node at MMR index `i` (leaves are 0).
|
|
46
|
+
* (go-merklelog `mmr/indexheight.go` IndexHeight)
|
|
47
|
+
*/
|
|
48
|
+
export function indexHeight(i) {
|
|
49
|
+
return posHeight(i + 1n);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Witness path (siblings, ascending) for the node at MMR index `i` in the tree
|
|
53
|
+
* ending at `mmrLastIndex`. Terminates at the covering peak.
|
|
54
|
+
* (go-merklelog `mmr/proof.go` InclusionProof)
|
|
55
|
+
*
|
|
56
|
+
* @throws if `i > mmrLastIndex`.
|
|
57
|
+
*/
|
|
58
|
+
export function inclusionProof(get, mmrLastIndex, i) {
|
|
59
|
+
if (i > mmrLastIndex) {
|
|
60
|
+
throw new Error("index out of range");
|
|
61
|
+
}
|
|
62
|
+
let g = BigInt(indexHeight(i));
|
|
63
|
+
const proof = [];
|
|
64
|
+
// iSibling out of range guarantees loop termination.
|
|
65
|
+
for (;;) {
|
|
66
|
+
const siblingOffset = 2n << g;
|
|
67
|
+
let iSibling;
|
|
68
|
+
if (BigInt(indexHeight(i + 1n)) > g) {
|
|
69
|
+
// right sibling
|
|
70
|
+
iSibling = i - siblingOffset + 1n;
|
|
71
|
+
i += 1n;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// left sibling
|
|
75
|
+
iSibling = i + siblingOffset - 1n;
|
|
76
|
+
i += siblingOffset;
|
|
77
|
+
}
|
|
78
|
+
if (iSibling > mmrLastIndex) {
|
|
79
|
+
return proof;
|
|
80
|
+
}
|
|
81
|
+
proof.push(get(iSibling));
|
|
82
|
+
g += 1n;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Peaks bitmap for an MMR size: the numeric value equals the leaf count of the
|
|
87
|
+
* largest valid MMR with size <= `mmrSize`; each set bit marks a perfect
|
|
88
|
+
* subtree (peak). (go-merklelog `mmr/peaks.go` PeaksBitmap)
|
|
89
|
+
*/
|
|
90
|
+
export function peaksBitmap(mmrSize) {
|
|
91
|
+
if (mmrSize === 0n)
|
|
92
|
+
return 0n;
|
|
93
|
+
let pos = mmrSize;
|
|
94
|
+
let peakSize = (1n << BigInt(bitLength(mmrSize))) - 1n;
|
|
95
|
+
let peakMap = 0n;
|
|
96
|
+
while (peakSize > 0n) {
|
|
97
|
+
peakMap <<= 1n;
|
|
98
|
+
if (pos >= peakSize) {
|
|
99
|
+
pos -= peakSize;
|
|
100
|
+
peakMap |= 1n;
|
|
101
|
+
}
|
|
102
|
+
peakSize >>= 1n;
|
|
103
|
+
}
|
|
104
|
+
return peakMap;
|
|
105
|
+
}
|
|
106
|
+
/** Population count of a non-negative BigInt. */
|
|
107
|
+
function popcount(x) {
|
|
108
|
+
let count = 0;
|
|
109
|
+
let v = x;
|
|
110
|
+
while (v > 0n) {
|
|
111
|
+
if ((v & 1n) === 1n)
|
|
112
|
+
count += 1;
|
|
113
|
+
v >>= 1n;
|
|
114
|
+
}
|
|
115
|
+
return count;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Accumulator slot (left-to-right) committed by an inclusion proof of length
|
|
119
|
+
* `proofLen` in a tree of size `mmrSize`. (server resolve-receipt.ts:735,
|
|
120
|
+
* equivalent to go `mmr.PeakIndex(LeafCount(mmrSize), proofLen)`)
|
|
121
|
+
*/
|
|
122
|
+
export function peakIndexForLeafProof(mmrSize, proofLen) {
|
|
123
|
+
const leafCount = peaksBitmap(mmrSize);
|
|
124
|
+
const peaksMask = (1n << BigInt(proofLen + 1)) - 1n;
|
|
125
|
+
return popcount(leafCount) - popcount(leafCount & peaksMask);
|
|
126
|
+
}
|
|
127
|
+
/** Largest peak size <= size of the tree ending at index `i`. (go `mmr/peaks.go` TopPeak) */
|
|
128
|
+
function topPeak(i) {
|
|
129
|
+
const bl = bitLength(i + 2n);
|
|
130
|
+
return (1n << BigInt(bl - 1)) - 2n;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* MMR indices of the peaks of the tree ending at `mmrIndex` (inclusive),
|
|
134
|
+
* ascending. Empty when `mmrIndex + 1` is not a valid MMR size (siblings
|
|
135
|
+
* without a parent). (go-merklelog `mmr/peaks.go` Peaks)
|
|
136
|
+
*/
|
|
137
|
+
export function peakMMRIndexes(mmrIndex) {
|
|
138
|
+
let mmrSize = mmrIndex + 1n;
|
|
139
|
+
if (posHeight(mmrSize + 1n) > posHeight(mmrSize)) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
let peak = 0n;
|
|
143
|
+
const out = [];
|
|
144
|
+
while (mmrSize !== 0n) {
|
|
145
|
+
const peakSize = topPeak(mmrSize - 1n) + 1n;
|
|
146
|
+
peak = peak + peakSize;
|
|
147
|
+
out.push(peak - 1n);
|
|
148
|
+
mmrSize -= peakSize;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Smallest complete MMR size that contains the node at MMR index `mmrIndex`.
|
|
154
|
+
* (go-merklelog `mmr/firstmmrsize.go` FirstMMRSize)
|
|
155
|
+
*/
|
|
156
|
+
export function firstMMRSize(mmrIndex) {
|
|
157
|
+
let i = mmrIndex;
|
|
158
|
+
let h0 = indexHeight(i);
|
|
159
|
+
let h1 = indexHeight(i + 1n);
|
|
160
|
+
while (h0 < h1) {
|
|
161
|
+
i += 1n;
|
|
162
|
+
h0 = h1;
|
|
163
|
+
h1 = indexHeight(i + 1n);
|
|
164
|
+
}
|
|
165
|
+
return i + 1n;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Massif index (0-based) that holds the node at MMR index `i` for a log of the
|
|
169
|
+
* given massif height. (go-merklelog `massifs/massifindex.go` MassifIndexFromMMRIndex)
|
|
170
|
+
*/
|
|
171
|
+
export function massifIndexFromMMRIndex(massifHeight, i) {
|
|
172
|
+
const size = firstMMRSize(i);
|
|
173
|
+
const leafIndex = peaksBitmap(size) - 1n;
|
|
174
|
+
const massifMaxLeaves = 1n << BigInt(massifHeight - 1);
|
|
175
|
+
return leafIndex / massifMaxLeaves;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Map from ancestor-peak MMR index to peak-stack slot for the massif whose
|
|
179
|
+
* first MMR index is `firstIndex`. Nodes below `firstIndex` resolve through the
|
|
180
|
+
* fixed peak-stack region via this map.
|
|
181
|
+
* (go-merklelog `massifs/peakstack.go` PeakStackMap)
|
|
182
|
+
*/
|
|
183
|
+
export function peakStackMap(massifHeight, firstIndex) {
|
|
184
|
+
const map = new Map();
|
|
185
|
+
const iPeaks = peakMMRIndexes(firstIndex);
|
|
186
|
+
for (let i = 0; i < iPeaks.length; i++) {
|
|
187
|
+
const ip = iPeaks[i];
|
|
188
|
+
if (indexHeight(ip) < massifHeight - 1) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
map.set(ip, i);
|
|
192
|
+
}
|
|
193
|
+
return map;
|
|
194
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forestrie/merklelog",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MMR (merkle mountain range) proofs and massif storage format for Forestrie transparency logs",
|
|
@@ -28,13 +28,15 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {},
|
|
30
30
|
"devDependencies": {
|
|
31
|
+
"esbuild": "^0.24.0",
|
|
31
32
|
"typescript": "^5.9.2",
|
|
32
33
|
"vitest": "^3.2.4"
|
|
33
34
|
},
|
|
34
35
|
"scripts": {
|
|
35
36
|
"build": "tsc -p tsconfig.build.json",
|
|
36
37
|
"typecheck": "tsc --noEmit",
|
|
37
|
-
"
|
|
38
|
+
"check:browser-safe": "node tools/check-browser-safe.mjs",
|
|
39
|
+
"test": "pnpm run check:browser-safe && vitest --run",
|
|
38
40
|
"test:watch": "vitest",
|
|
39
41
|
"test:debug": "vitest --inspect --no-file-parallelism"
|
|
40
42
|
}
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,14 @@ export {
|
|
|
45
45
|
|
|
46
46
|
export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
|
|
47
47
|
|
|
48
|
+
// Massif blob node access for MMR proof building (plan-2607-15 §4)
|
|
49
|
+
export { openMassifNodeStore } from "./massifs/nodestore.js";
|
|
50
|
+
export type { MassifNodeStore } from "./massifs/nodestore.js";
|
|
51
|
+
|
|
52
|
+
// Content-hash leaf lookup over the massif index region (FOR-373)
|
|
53
|
+
export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
|
|
54
|
+
export type { MassifLeafIndex } from "./massifs/leafindex.js";
|
|
55
|
+
|
|
48
56
|
// Export mmr module
|
|
49
57
|
export { mmrIndex, massifFirstLeaf, leafMinusSpurSum } from "./mmr/index.js";
|
|
50
58
|
export {
|
|
@@ -66,3 +74,16 @@ export {
|
|
|
66
74
|
export { createSyncHasher } from "./mmr/algorithms-sync.js";
|
|
67
75
|
export type { Proof, Peak } from "./mmr/types.js";
|
|
68
76
|
export type { Hasher } from "./mmr/types.js";
|
|
77
|
+
|
|
78
|
+
// MMR proof building (pure; store-agnostic) — plan-2607-15 §4 (go: mmr/proof.go, peaks.go)
|
|
79
|
+
export {
|
|
80
|
+
inclusionProof,
|
|
81
|
+
peakMMRIndexes,
|
|
82
|
+
peaksBitmap,
|
|
83
|
+
peakIndexForLeafProof,
|
|
84
|
+
indexHeight,
|
|
85
|
+
firstMMRSize,
|
|
86
|
+
massifIndexFromMMRIndex,
|
|
87
|
+
peakStackMap,
|
|
88
|
+
} from "./mmr/proof.js";
|
|
89
|
+
export type { NodeGetter } from "./mmr/proof.js";
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-hash → mmrIndex leaf lookup over a v2 massif index region (FOR-373).
|
|
3
|
+
*
|
|
4
|
+
* The v2 massif index region carries, per leaf ordinal, a fixed-width record
|
|
5
|
+
* whose committed `valueBytes[32]` is the leaf's content hash — the same value
|
|
6
|
+
* committed by the urkle trie (go-merklelog `urkle/leafrecord.go`,
|
|
7
|
+
* `massifs/indexformat_v2.go`; plan-2607-15 §1.1). The urkle trie itself is
|
|
8
|
+
* keyed by the 8-byte leaf key (idtimestamp) for duplicate/exclusion detection,
|
|
9
|
+
* so content-hash resolution reads the leaf table's value column directly and
|
|
10
|
+
* maps the matching ordinal back to an MMR index
|
|
11
|
+
* (`urkle.LeafOrdinalToMMRIndex`).
|
|
12
|
+
*
|
|
13
|
+
* This lets `forestrie create-receipt --content-hash <hex>` locate a leaf's
|
|
14
|
+
* mmrIndex from a massif blob alone, with no external mapping. A negative
|
|
15
|
+
* lookup (`null`) is distinguishable from absence-of-index: a blob with no
|
|
16
|
+
* index region throws `MissingIndexError`, while a searched-but-not-present
|
|
17
|
+
* hash returns `null`.
|
|
18
|
+
*
|
|
19
|
+
* Browser-safe: pure buffer arithmetic, no node builtins (ADR-0048).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Massif } from "./massif.js";
|
|
23
|
+
import { LogFormat } from "./logformat.js";
|
|
24
|
+
import { Urkle, leafCountForMassifHeight } from "./indexformat.js";
|
|
25
|
+
import { urkleLeafTableStartByteOffset } from "./urkleindex.js";
|
|
26
|
+
import { peakStackEnd } from "./peakstackend.js";
|
|
27
|
+
import { massifLogEntries } from "./massiflogentries.js";
|
|
28
|
+
import { firstMMRSize, peaksBitmap } from "../mmr/proof.js";
|
|
29
|
+
import { mmrIndex as mmrIndexFromLeafIndex } from "../mmr/index.js";
|
|
30
|
+
|
|
31
|
+
const HASH_BYTES = Urkle.LeafValueBytes; // 32
|
|
32
|
+
const VALUE_OFFSET = Urkle.LeafValueOffset; // 8 (after the 8-byte key)
|
|
33
|
+
const RECORD_BYTES = Urkle.LeafRecordBytes; // 128
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Thrown when the blob carries no populated index region, so a content-hash
|
|
37
|
+
* lookup cannot even be attempted. Callers distinguish this from a `null`
|
|
38
|
+
* (searched, not found) result.
|
|
39
|
+
*/
|
|
40
|
+
export class MissingIndexError extends Error {
|
|
41
|
+
constructor(message: string) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "MissingIndexError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Content-hash leaf lookup over one massif blob (FOR-373). */
|
|
48
|
+
export interface MassifLeafIndex {
|
|
49
|
+
/**
|
|
50
|
+
* MMR index of the leaf whose committed content hash equals `h`, or `null`
|
|
51
|
+
* when no populated leaf in this massif carries that hash. `h` must be 32
|
|
52
|
+
* bytes.
|
|
53
|
+
*/
|
|
54
|
+
findByContentHash(h: Uint8Array): bigint | null;
|
|
55
|
+
/** Number of populated leaves in this blob. */
|
|
56
|
+
leafCount: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function equals32(a: Uint8Array, aOff: number, b: Uint8Array): boolean {
|
|
60
|
+
for (let i = 0; i < HASH_BYTES; i++) {
|
|
61
|
+
if (a[aOff + i] !== b[i]) return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Leaf index of the first leaf in the massif whose first MMR index is
|
|
68
|
+
* `firstMMRIndex`. (go-merklelog `mmr/leafcount.go` LeafIndex)
|
|
69
|
+
*/
|
|
70
|
+
function leafIndexFromMMRIndex(firstMMRIndex: bigint): bigint {
|
|
71
|
+
if (firstMMRIndex === 0n) return 0n;
|
|
72
|
+
return peaksBitmap(firstMMRSize(firstMMRIndex)) - 1n;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Open a v2 massif blob for content-hash leaf lookups.
|
|
77
|
+
*
|
|
78
|
+
* @throws MissingIndexError if the blob is too short to contain the fixed index
|
|
79
|
+
* region for its declared massif height (no index present).
|
|
80
|
+
*/
|
|
81
|
+
export function openMassifLeafIndex(massifBytes: Uint8Array): MassifLeafIndex {
|
|
82
|
+
const massif = new Massif(massifBytes);
|
|
83
|
+
const start = massif.getStart();
|
|
84
|
+
const massifHeight = start.massifHeight;
|
|
85
|
+
if (
|
|
86
|
+
!Number.isInteger(massifHeight) ||
|
|
87
|
+
massifHeight < 1 ||
|
|
88
|
+
massifHeight > LogFormat.MaxMmrHeight
|
|
89
|
+
) {
|
|
90
|
+
throw new Error(`massif header has invalid height ${massifHeight}`);
|
|
91
|
+
}
|
|
92
|
+
const firstIndex = start.firstIndex;
|
|
93
|
+
|
|
94
|
+
const leafTableStart = urkleLeafTableStartByteOffset(massifHeight);
|
|
95
|
+
const capacity = Number(leafCountForMassifHeight(massifHeight));
|
|
96
|
+
const leafTableBytes = capacity * RECORD_BYTES;
|
|
97
|
+
const stackEnd = peakStackEnd(massifHeight);
|
|
98
|
+
|
|
99
|
+
// The index region must fit before the fixed peak stack + log region. A blob
|
|
100
|
+
// shorter than that has no populated index to search.
|
|
101
|
+
if (massifHeight > 1 && BigInt(leafTableStart + leafTableBytes) > stackEnd) {
|
|
102
|
+
throw new Error("massif index region math is inconsistent");
|
|
103
|
+
}
|
|
104
|
+
if (BigInt(massifBytes.byteLength) < stackEnd) {
|
|
105
|
+
throw new MissingIndexError(
|
|
106
|
+
"massif blob too short for a v2 index region (no index present)",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Only leaves actually appended have populated records. Derive the populated
|
|
111
|
+
// leaf count from the log node count (index records are written 1:1 with
|
|
112
|
+
// appended leaves). Never exceed the fixed capacity for the height.
|
|
113
|
+
const logNodeCount = massifLogEntries(massifBytes.byteLength, massifHeight);
|
|
114
|
+
const firstLeafIndex = leafIndexFromMMRIndex(firstIndex);
|
|
115
|
+
let populated = 0;
|
|
116
|
+
for (let ordinal = 0; ordinal < capacity; ordinal++) {
|
|
117
|
+
// MMR index of leaf (firstLeafIndex + ordinal), relative to this blob's
|
|
118
|
+
// firstIndex, must be within the log node count.
|
|
119
|
+
const leafMMRIndex = mmrIndexFromLeafIndex(
|
|
120
|
+
firstLeafIndex + BigInt(ordinal),
|
|
121
|
+
);
|
|
122
|
+
if (leafMMRIndex - firstIndex >= logNodeCount) break;
|
|
123
|
+
populated = ordinal + 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const findByContentHash = (h: Uint8Array): bigint | null => {
|
|
127
|
+
if (h.length !== HASH_BYTES) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`content hash must be ${HASH_BYTES} bytes, got ${h.length}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
for (let ordinal = 0; ordinal < populated; ordinal++) {
|
|
133
|
+
const recordOff = leafTableStart + ordinal * RECORD_BYTES;
|
|
134
|
+
const valueOff = recordOff + VALUE_OFFSET;
|
|
135
|
+
if (valueOff + HASH_BYTES > massifBytes.byteLength) break;
|
|
136
|
+
if (equals32(massifBytes, valueOff, h)) {
|
|
137
|
+
return mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return { findByContentHash, leafCount: populated };
|
|
144
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Massif blob node access for MMR proof building.
|
|
3
|
+
*
|
|
4
|
+
* A v2 massif `.log` blob is self-sufficient for any leaf→peak inclusion path
|
|
5
|
+
* whose leaf lives in that massif: local nodes give the in-massif siblings and
|
|
6
|
+
* the fixed ancestor peak stack gives everything older (plan-2607-15 §1.1).
|
|
7
|
+
* Node lookup is pure arithmetic — `LogStart + (i − firstIndex)·32` for local
|
|
8
|
+
* nodes, `peakStackMap` for ancestors.
|
|
9
|
+
*
|
|
10
|
+
* Ported from the inline copies in canopy-api `resolve-receipt.ts` and
|
|
11
|
+
* receipt-verify `build-receipt-offline.ts` (plan-2607-15 §4, phase 2).
|
|
12
|
+
* Browser-safe: pure buffer arithmetic, no node builtins (ADR-0048).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Massif } from "./massif.js";
|
|
16
|
+
import { LogFormat } from "./logformat.js";
|
|
17
|
+
import { peakStackEnd } from "./peakstackend.js";
|
|
18
|
+
import { peakStackMap } from "../mmr/proof.js";
|
|
19
|
+
|
|
20
|
+
const VALUE_BYTES = BigInt(LogFormat.ValueBytes); // 32
|
|
21
|
+
const MAX_MMR_HEIGHT = BigInt(LogFormat.MaxMmrHeight); // 64
|
|
22
|
+
|
|
23
|
+
/** Node access over a single massif blob. */
|
|
24
|
+
export interface MassifNodeStore {
|
|
25
|
+
/** 32-byte node at MMR index `i` (log region for `i >= firstIndex`, else the ancestor peak stack). */
|
|
26
|
+
get(i: bigint): Uint8Array;
|
|
27
|
+
massifHeight: number;
|
|
28
|
+
massifIndex: bigint;
|
|
29
|
+
firstIndex: bigint;
|
|
30
|
+
/** Last MMR index with log data in this blob. */
|
|
31
|
+
lastIndex: bigint;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function slice32(buf: Uint8Array, offset: bigint, label: string): Uint8Array {
|
|
35
|
+
if (offset < 0n || offset + VALUE_BYTES > BigInt(buf.byteLength)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`out of range read for ${label}: off=${offset.toString(10)}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const start = Number(offset);
|
|
41
|
+
return buf.slice(start, start + Number(VALUE_BYTES));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Open a v2 massif blob for MMR node reads. Nodes below `firstIndex` resolve
|
|
46
|
+
* through the ancestor peak stack; nodes above `lastIndex` are not present in
|
|
47
|
+
* this blob and throw when requested.
|
|
48
|
+
*/
|
|
49
|
+
export function openMassifNodeStore(massifBytes: Uint8Array): MassifNodeStore {
|
|
50
|
+
const massif = new Massif(massifBytes);
|
|
51
|
+
const start = massif.getStart();
|
|
52
|
+
const massifHeight = start.massifHeight;
|
|
53
|
+
if (
|
|
54
|
+
!Number.isInteger(massifHeight) ||
|
|
55
|
+
massifHeight < 1 ||
|
|
56
|
+
massifHeight > LogFormat.MaxMmrHeight
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(`massif header has invalid height ${massifHeight}`);
|
|
59
|
+
}
|
|
60
|
+
const massifIndex = BigInt(start.massifIndex);
|
|
61
|
+
const firstIndex = start.firstIndex;
|
|
62
|
+
|
|
63
|
+
const logStart = peakStackEnd(massifHeight);
|
|
64
|
+
const peakStackStart = logStart - MAX_MMR_HEIGHT * VALUE_BYTES;
|
|
65
|
+
const blobLen = BigInt(massifBytes.byteLength);
|
|
66
|
+
if (blobLen < logStart) {
|
|
67
|
+
throw new Error("massif blob too short for v2 layout");
|
|
68
|
+
}
|
|
69
|
+
const logNodeCount = (blobLen - logStart) / VALUE_BYTES;
|
|
70
|
+
const lastIndex = firstIndex + logNodeCount - 1n;
|
|
71
|
+
|
|
72
|
+
const stackMap = peakStackMap(massifHeight, firstIndex);
|
|
73
|
+
|
|
74
|
+
const get = (i: bigint): Uint8Array => {
|
|
75
|
+
if (i >= firstIndex) {
|
|
76
|
+
if (i > lastIndex) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`mmr index ${i.toString(10)} is beyond this massif's log data ` +
|
|
79
|
+
`(last ${lastIndex.toString(10)}); local content does not cover ` +
|
|
80
|
+
`the requested tree size`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const off = logStart + (i - firstIndex) * VALUE_BYTES;
|
|
84
|
+
return slice32(massifBytes, off, "log-data");
|
|
85
|
+
}
|
|
86
|
+
const peakIdx = stackMap.get(i);
|
|
87
|
+
if (peakIdx === undefined) {
|
|
88
|
+
throw new Error(`missing ancestor peak for mmr index ${i.toString(10)}`);
|
|
89
|
+
}
|
|
90
|
+
const off = peakStackStart + BigInt(peakIdx) * VALUE_BYTES;
|
|
91
|
+
return slice32(massifBytes, off, "peak-stack");
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return { get, massifHeight, massifIndex, firstIndex, lastIndex };
|
|
95
|
+
}
|
|
@@ -27,7 +27,13 @@ function concatChunks(chunks: Uint8Array[]): Uint8Array {
|
|
|
27
27
|
* Only available in Node.js; throws if node:crypto is not available.
|
|
28
28
|
*/
|
|
29
29
|
export async function createSyncHasher(): Promise<Hasher> {
|
|
30
|
-
|
|
30
|
+
// Keep the specifier opaque so bundlers do not resolve node:crypto at
|
|
31
|
+
// build time: this factory is documented runtime-guarded Node-only, and
|
|
32
|
+
// browser consumers of the package index (e.g. @forestrie/receipt-verify's
|
|
33
|
+
// ADR-0048 browser-safety guard) must not get node builtins in their
|
|
34
|
+
// module graph merely by importing it.
|
|
35
|
+
const nodeCryptoSpecifier = "node:crypto";
|
|
36
|
+
const { createHash } = await import(/* @vite-ignore */ nodeCryptoSpecifier);
|
|
31
37
|
const chunks: Uint8Array[] = [];
|
|
32
38
|
return {
|
|
33
39
|
reset(): void {
|
package/src/mmr/proof.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MMR proof building (pure, store-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Ports the proof-side arithmetic from go-merklelog `mmr/proof.go` and
|
|
5
|
+
* `mmr/peaks.go`. These were previously inline-duplicated in
|
|
6
|
+
* canopy-api `resolve-receipt.ts` and receipt-verify `build-receipt-offline.ts`;
|
|
7
|
+
* this is the single hoisted home (plan-2607-15 §4, phase 2).
|
|
8
|
+
*
|
|
9
|
+
* All arithmetic is BigInt and free of node builtins so the package stays
|
|
10
|
+
* browser-safe (ADR-0048). Function names mirror the go-merklelog originals.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Reads the 32-byte node at MMR index `i` from some backing store. */
|
|
14
|
+
export type NodeGetter = (i: bigint) => Uint8Array;
|
|
15
|
+
|
|
16
|
+
/** Number of bits needed to represent `num` (0 for 0). */
|
|
17
|
+
function bitLength(num: bigint): number {
|
|
18
|
+
if (num <= 0n) return 0;
|
|
19
|
+
return num.toString(2).length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True when `num > 0` and its binary form is all ones. */
|
|
23
|
+
function allOnes(num: bigint): boolean {
|
|
24
|
+
return num > 0n && (num & (num + 1n)) === 0n;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Jump left to the leftmost node at the same height.
|
|
29
|
+
* (go-merklelog `mmr/indexheight.go` JumpLeftPerfect)
|
|
30
|
+
*/
|
|
31
|
+
function jumpLeftPerfect(pos: bigint): bigint {
|
|
32
|
+
const bl = bitLength(pos);
|
|
33
|
+
if (bl === 0) return pos;
|
|
34
|
+
const msb = 1n << BigInt(bl - 1);
|
|
35
|
+
return pos - (msb - 1n);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Height of a 1-based position.
|
|
40
|
+
* (go-merklelog `mmr/indexheight.go` PosHeight)
|
|
41
|
+
*/
|
|
42
|
+
function posHeight(pos: bigint): number {
|
|
43
|
+
let current = pos;
|
|
44
|
+
while (!allOnes(current)) {
|
|
45
|
+
current = jumpLeftPerfect(current);
|
|
46
|
+
}
|
|
47
|
+
return bitLength(current) - 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Zero-based height index of the node at MMR index `i` (leaves are 0).
|
|
52
|
+
* (go-merklelog `mmr/indexheight.go` IndexHeight)
|
|
53
|
+
*/
|
|
54
|
+
export function indexHeight(i: bigint): number {
|
|
55
|
+
return posHeight(i + 1n);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Witness path (siblings, ascending) for the node at MMR index `i` in the tree
|
|
60
|
+
* ending at `mmrLastIndex`. Terminates at the covering peak.
|
|
61
|
+
* (go-merklelog `mmr/proof.go` InclusionProof)
|
|
62
|
+
*
|
|
63
|
+
* @throws if `i > mmrLastIndex`.
|
|
64
|
+
*/
|
|
65
|
+
export function inclusionProof(
|
|
66
|
+
get: NodeGetter,
|
|
67
|
+
mmrLastIndex: bigint,
|
|
68
|
+
i: bigint,
|
|
69
|
+
): Uint8Array[] {
|
|
70
|
+
if (i > mmrLastIndex) {
|
|
71
|
+
throw new Error("index out of range");
|
|
72
|
+
}
|
|
73
|
+
let g = BigInt(indexHeight(i));
|
|
74
|
+
const proof: Uint8Array[] = [];
|
|
75
|
+
// iSibling out of range guarantees loop termination.
|
|
76
|
+
for (;;) {
|
|
77
|
+
const siblingOffset = 2n << g;
|
|
78
|
+
let iSibling: bigint;
|
|
79
|
+
if (BigInt(indexHeight(i + 1n)) > g) {
|
|
80
|
+
// right sibling
|
|
81
|
+
iSibling = i - siblingOffset + 1n;
|
|
82
|
+
i += 1n;
|
|
83
|
+
} else {
|
|
84
|
+
// left sibling
|
|
85
|
+
iSibling = i + siblingOffset - 1n;
|
|
86
|
+
i += siblingOffset;
|
|
87
|
+
}
|
|
88
|
+
if (iSibling > mmrLastIndex) {
|
|
89
|
+
return proof;
|
|
90
|
+
}
|
|
91
|
+
proof.push(get(iSibling));
|
|
92
|
+
g += 1n;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Peaks bitmap for an MMR size: the numeric value equals the leaf count of the
|
|
98
|
+
* largest valid MMR with size <= `mmrSize`; each set bit marks a perfect
|
|
99
|
+
* subtree (peak). (go-merklelog `mmr/peaks.go` PeaksBitmap)
|
|
100
|
+
*/
|
|
101
|
+
export function peaksBitmap(mmrSize: bigint): bigint {
|
|
102
|
+
if (mmrSize === 0n) return 0n;
|
|
103
|
+
let pos = mmrSize;
|
|
104
|
+
let peakSize = (1n << BigInt(bitLength(mmrSize))) - 1n;
|
|
105
|
+
let peakMap = 0n;
|
|
106
|
+
while (peakSize > 0n) {
|
|
107
|
+
peakMap <<= 1n;
|
|
108
|
+
if (pos >= peakSize) {
|
|
109
|
+
pos -= peakSize;
|
|
110
|
+
peakMap |= 1n;
|
|
111
|
+
}
|
|
112
|
+
peakSize >>= 1n;
|
|
113
|
+
}
|
|
114
|
+
return peakMap;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Population count of a non-negative BigInt. */
|
|
118
|
+
function popcount(x: bigint): number {
|
|
119
|
+
let count = 0;
|
|
120
|
+
let v = x;
|
|
121
|
+
while (v > 0n) {
|
|
122
|
+
if ((v & 1n) === 1n) count += 1;
|
|
123
|
+
v >>= 1n;
|
|
124
|
+
}
|
|
125
|
+
return count;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Accumulator slot (left-to-right) committed by an inclusion proof of length
|
|
130
|
+
* `proofLen` in a tree of size `mmrSize`. (server resolve-receipt.ts:735,
|
|
131
|
+
* equivalent to go `mmr.PeakIndex(LeafCount(mmrSize), proofLen)`)
|
|
132
|
+
*/
|
|
133
|
+
export function peakIndexForLeafProof(
|
|
134
|
+
mmrSize: bigint,
|
|
135
|
+
proofLen: number,
|
|
136
|
+
): number {
|
|
137
|
+
const leafCount = peaksBitmap(mmrSize);
|
|
138
|
+
const peaksMask = (1n << BigInt(proofLen + 1)) - 1n;
|
|
139
|
+
return popcount(leafCount) - popcount(leafCount & peaksMask);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Largest peak size <= size of the tree ending at index `i`. (go `mmr/peaks.go` TopPeak) */
|
|
143
|
+
function topPeak(i: bigint): bigint {
|
|
144
|
+
const bl = bitLength(i + 2n);
|
|
145
|
+
return (1n << BigInt(bl - 1)) - 2n;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* MMR indices of the peaks of the tree ending at `mmrIndex` (inclusive),
|
|
150
|
+
* ascending. Empty when `mmrIndex + 1` is not a valid MMR size (siblings
|
|
151
|
+
* without a parent). (go-merklelog `mmr/peaks.go` Peaks)
|
|
152
|
+
*/
|
|
153
|
+
export function peakMMRIndexes(mmrIndex: bigint): bigint[] {
|
|
154
|
+
let mmrSize = mmrIndex + 1n;
|
|
155
|
+
if (posHeight(mmrSize + 1n) > posHeight(mmrSize)) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
let peak = 0n;
|
|
159
|
+
const out: bigint[] = [];
|
|
160
|
+
while (mmrSize !== 0n) {
|
|
161
|
+
const peakSize = topPeak(mmrSize - 1n) + 1n;
|
|
162
|
+
peak = peak + peakSize;
|
|
163
|
+
out.push(peak - 1n);
|
|
164
|
+
mmrSize -= peakSize;
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Smallest complete MMR size that contains the node at MMR index `mmrIndex`.
|
|
171
|
+
* (go-merklelog `mmr/firstmmrsize.go` FirstMMRSize)
|
|
172
|
+
*/
|
|
173
|
+
export function firstMMRSize(mmrIndex: bigint): bigint {
|
|
174
|
+
let i = mmrIndex;
|
|
175
|
+
let h0 = indexHeight(i);
|
|
176
|
+
let h1 = indexHeight(i + 1n);
|
|
177
|
+
while (h0 < h1) {
|
|
178
|
+
i += 1n;
|
|
179
|
+
h0 = h1;
|
|
180
|
+
h1 = indexHeight(i + 1n);
|
|
181
|
+
}
|
|
182
|
+
return i + 1n;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Massif index (0-based) that holds the node at MMR index `i` for a log of the
|
|
187
|
+
* given massif height. (go-merklelog `massifs/massifindex.go` MassifIndexFromMMRIndex)
|
|
188
|
+
*/
|
|
189
|
+
export function massifIndexFromMMRIndex(
|
|
190
|
+
massifHeight: number,
|
|
191
|
+
i: bigint,
|
|
192
|
+
): bigint {
|
|
193
|
+
const size = firstMMRSize(i);
|
|
194
|
+
const leafIndex = peaksBitmap(size) - 1n;
|
|
195
|
+
const massifMaxLeaves = 1n << BigInt(massifHeight - 1);
|
|
196
|
+
return leafIndex / massifMaxLeaves;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Map from ancestor-peak MMR index to peak-stack slot for the massif whose
|
|
201
|
+
* first MMR index is `firstIndex`. Nodes below `firstIndex` resolve through the
|
|
202
|
+
* fixed peak-stack region via this map.
|
|
203
|
+
* (go-merklelog `massifs/peakstack.go` PeakStackMap)
|
|
204
|
+
*/
|
|
205
|
+
export function peakStackMap(
|
|
206
|
+
massifHeight: number,
|
|
207
|
+
firstIndex: bigint,
|
|
208
|
+
): Map<bigint, number> {
|
|
209
|
+
const map = new Map<bigint, number>();
|
|
210
|
+
const iPeaks = peakMMRIndexes(firstIndex);
|
|
211
|
+
for (let i = 0; i < iPeaks.length; i++) {
|
|
212
|
+
const ip = iPeaks[i]!;
|
|
213
|
+
if (indexHeight(ip) < massifHeight - 1) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
map.set(ip, i);
|
|
217
|
+
}
|
|
218
|
+
return map;
|
|
219
|
+
}
|