@forestrie/merklelog 0.1.0 → 0.2.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 CHANGED
@@ -15,10 +15,10 @@ export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
15
15
  export { openMassifNodeStore } from "./massifs/nodestore.js";
16
16
  export type { MassifNodeStore } from "./massifs/nodestore.js";
17
17
  export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
18
- export type { MassifLeafIndex } from "./massifs/leafindex.js";
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, } from "./mmr/algorithms.js";
21
+ export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, consistentRoots, indexConsistencyProof, type ConsistencyProof, } from "./mmr/algorithms.js";
22
22
  export { createSyncHasher } from "./mmr/algorithms-sync.js";
23
23
  export type { Proof, Peak } from "./mmr/types.js";
24
24
  export type { Hasher } from "./mmr/types.js";
@@ -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,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"}
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;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
@@ -20,7 +20,7 @@ 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, } from "./mmr/algorithms.js";
23
+ export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, consistentRoots, indexConsistencyProof, } from "./mmr/algorithms.js";
24
24
  export { createSyncHasher } from "./mmr/algorithms-sync.js";
25
25
  // MMR proof building (pure; store-agnostic) — plan-2607-15 §4 (go: mmr/proof.go, peaks.go)
26
26
  export { inclusionProof, peakMMRIndexes, peaksBitmap, peakIndexForLeafProof, indexHeight, firstMMRSize, massifIndexFromMMRIndex, peakStackMap, } from "./mmr/proof.js";
@@ -26,6 +26,15 @@
26
26
  export declare class MissingIndexError extends Error {
27
27
  constructor(message: string);
28
28
  }
29
+ /**
30
+ * A located leaf: its MMR index plus the 8-byte idtimestamp (big-endian) the
31
+ * urkle leaf record is keyed by. Together these are the two halves of the
32
+ * permanent entry id (`idtimestamp_be8 || mmrIndex_be8`).
33
+ */
34
+ export interface LocatedLeaf {
35
+ mmrIndex: bigint;
36
+ idtimestampBe8: Uint8Array;
37
+ }
29
38
  /** Content-hash leaf lookup over one massif blob (FOR-373). */
30
39
  export interface MassifLeafIndex {
31
40
  /**
@@ -34,6 +43,12 @@ export interface MassifLeafIndex {
34
43
  * bytes.
35
44
  */
36
45
  findByContentHash(h: Uint8Array): bigint | null;
46
+ /**
47
+ * Like {@link findByContentHash} but also recovers the leaf's idtimestamp
48
+ * (the 8-byte urkle leaf key), so a caller can reconstruct the full entry id
49
+ * offline. Returns `null` when the hash is not present. `h` must be 32 bytes.
50
+ */
51
+ findLeafByContentHash(h: Uint8Array): LocatedLeaf | null;
37
52
  /** Number of populated leaves in this blob. */
38
53
  leafCount: number;
39
54
  }
@@ -1 +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"}
1
+ {"version":3,"file":"leafindex.d.ts","sourceRoot":"","sources":["../../src/massifs/leafindex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAgBH;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,UAAU,CAAC;CAC5B;AAED,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC;IAChD;;;;OAIG;IACH,qBAAqB,CAAC,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI,CAAC;IACzD,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;CACnB;AAkBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,UAAU,GAAG,eAAe,CAgF5E"}
@@ -27,6 +27,7 @@ import { massifLogEntries } from "./massiflogentries.js";
27
27
  import { firstMMRSize, peaksBitmap } from "../mmr/proof.js";
28
28
  import { mmrIndex as mmrIndexFromLeafIndex } from "../mmr/index.js";
29
29
  const HASH_BYTES = Urkle.LeafValueBytes; // 32
30
+ const KEY_BYTES = Urkle.LeafKeyBytes; // 8 (idtimestamp, big-endian)
30
31
  const VALUE_OFFSET = Urkle.LeafValueOffset; // 8 (after the 8-byte key)
31
32
  const RECORD_BYTES = Urkle.LeafRecordBytes; // 128
32
33
  /**
@@ -98,7 +99,8 @@ export function openMassifLeafIndex(massifBytes) {
98
99
  break;
99
100
  populated = ordinal + 1;
100
101
  }
101
- const findByContentHash = (h) => {
102
+ /** Ordinal of the populated leaf whose value column equals `h`, or -1. */
103
+ const ordinalForContentHash = (h) => {
102
104
  if (h.length !== HASH_BYTES) {
103
105
  throw new Error(`content hash must be ${HASH_BYTES} bytes, got ${h.length}`);
104
106
  }
@@ -107,11 +109,28 @@ export function openMassifLeafIndex(massifBytes) {
107
109
  const valueOff = recordOff + VALUE_OFFSET;
108
110
  if (valueOff + HASH_BYTES > massifBytes.byteLength)
109
111
  break;
110
- if (equals32(massifBytes, valueOff, h)) {
111
- return mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal));
112
- }
112
+ if (equals32(massifBytes, valueOff, h))
113
+ return ordinal;
113
114
  }
114
- return null;
115
+ return -1;
116
+ };
117
+ const findByContentHash = (h) => {
118
+ const ordinal = ordinalForContentHash(h);
119
+ if (ordinal < 0)
120
+ return null;
121
+ return mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal));
122
+ };
123
+ const findLeafByContentHash = (h) => {
124
+ const ordinal = ordinalForContentHash(h);
125
+ if (ordinal < 0)
126
+ return null;
127
+ // The leaf record is `[key(8) || value(32) || …]`; the key IS the
128
+ // big-endian idtimestamp the sealer assigned (go-merklelog urkle leafrecord).
129
+ const keyOff = leafTableStart + ordinal * RECORD_BYTES;
130
+ return {
131
+ mmrIndex: mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal)),
132
+ idtimestampBe8: massifBytes.slice(keyOff, keyOff + KEY_BYTES),
133
+ };
115
134
  };
116
- return { findByContentHash, leafCount: populated };
135
+ return { findByContentHash, findLeafByContentHash, leafCount: populated };
117
136
  }
@@ -11,6 +11,7 @@
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
13
  import type { Proof, Hasher } from "./types.js";
14
+ import { type NodeGetter } from "./proof.js";
14
15
  /**
15
16
  * Bags peaks together to compute a single root hash.
16
17
  *
@@ -59,21 +60,55 @@ export declare function calculateRoot(hasher: Hasher, leafHash: Uint8Array, proo
59
60
  */
60
61
  export declare function verifyInclusion(hasher: Hasher, leafHash: Uint8Array, proof: Proof, root: Uint8Array): Promise<boolean>;
61
62
  /**
62
- * Verifies a consistency proof between two MMR states.
63
+ * Consistency proof between two MMR states (draft-bryce `consistency-proof`,
64
+ * go-merklelog `ConsistencyProof`): one inclusion path per MMR(A) peak,
65
+ * proven in MMR(B). Verified against trusted accumulators for both sizes.
66
+ */
67
+ export interface ConsistencyProof {
68
+ mmrSizeA: bigint;
69
+ mmrSizeB: bigint;
70
+ /** One sibling path per MMR(A) peak (ascending), in peak order. */
71
+ paths: Uint8Array[][];
72
+ }
73
+ /**
74
+ * Recover the MMR(B) accumulator prefix committed by the MMR(A) peaks
75
+ * (draft-bryce `consistent_roots`; go-merklelog `ConsistentRoots`).
63
76
  *
64
- * WARNING — NOT IMPLEMENTED. This is a stub: it compares each root against
65
- * itself and therefore ALWAYS returns true. It performs no real consistency
66
- * check and MUST NOT be relied upon for security. No worker path currently
67
- * calls it. See the reference `verify_consistency` (algorithms.py) for the
68
- * intended algorithm (verify proof2 extends proof1 via position-committed
69
- * interior hashing).
77
+ * Each MMR(A) peak is an interior node of MMR(B) at an immovable position;
78
+ * its inclusion path climbs to the covering MMR(B) peak. Consecutive
79
+ * duplicate roots collapse (many old peaks share one new peak). Requires
80
+ * one path per MMR(A) peak (draft: `len(peaks(ifrom)) == len(accumulatorfrom)`).
70
81
  *
71
- * @param hasher - Cryptographic hasher instance
72
- * @param proof1 - Proof for the first state
73
- * @param proof2 - Proof for the second state
74
- * @param root1 - Root hash of the first state
75
- * @param root2 - Root hash of the second state
76
- * @returns Always true (stub) — do not use for verification
82
+ * @param ifrom - last node index of the complete MMR(A) (`mmrSizeA - 1`)
83
+ * @param accumulatorFrom - MMR(A) peak values, descending height order
84
+ * @param paths - inclusion path per peak, proven in MMR(B)
85
+ */
86
+ export declare function consistentRoots(hasher: Hasher, ifrom: bigint, accumulatorFrom: Uint8Array[], paths: Uint8Array[][]): Promise<Uint8Array[]>;
87
+ /**
88
+ * Verify MMR(A) is a committed prefix of MMR(B)
89
+ * (draft-bryce "Verifying the Receipt of consistency";
90
+ * go-merklelog `VerifyConsistency`).
91
+ *
92
+ * Replaces the plan-0027 always-true stub (FOR-368 Phase 1,
93
+ * plan-2607-29): the previous signature took two inclusion proofs and
94
+ * returned true unconditionally; no caller existed.
95
+ *
96
+ * @param proof - consistency proof `mmrSizeA -> mmrSizeB`
97
+ * @param peaksFrom - TRUSTED MMR(A) accumulator (e.g. a signed checkpoint
98
+ * payload), descending height order
99
+ * @param peaksTo - MMR(B) accumulator to prove against (e.g. an anchored
100
+ * on-chain state), descending height order
101
+ * @returns ok, with the MMR(B) accumulator on success
102
+ */
103
+ export declare function verifyConsistency(hasher: Hasher, proof: ConsistencyProof, peaksFrom: Uint8Array[], peaksTo: Uint8Array[]): Promise<{
104
+ ok: boolean;
105
+ accumulator: Uint8Array[];
106
+ }>;
107
+ /**
108
+ * Generate a consistency proof `mmrIndexA -> mmrIndexB` from node data
109
+ * (go-merklelog `IndexConsistencyProof`): an inclusion path in MMR(B) for
110
+ * each MMR(A) peak. Node access is by MMR index (massif tiles or any
111
+ * replicated store).
77
112
  */
78
- export declare function verifyConsistency(hasher: Hasher, proof1: Proof, proof2: Proof, root1: Uint8Array, root2: Uint8Array): boolean;
113
+ export declare function indexConsistencyProof(get: NodeGetter, mmrIndexA: bigint, mmrIndexB: bigint): ConsistencyProof;
79
114
  //# sourceMappingURL=algorithms.d.ts.map
@@ -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;AAqBhD;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,KAAK,EACb,MAAM,EAAE,KAAK,EACb,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,UAAU,GAChB,OAAO,CAKT"}
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,EAAkC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAqB7E;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;GAYG;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;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,UAAU,EAAE,EACvB,OAAO,EAAE,UAAU,EAAE,GACpB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC,CAwBrD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,gBAAgB,CAMlB"}
@@ -10,6 +10,7 @@
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
14
  import { Uint64 } from "../uint64/index.js";
14
15
  import { heightIndex } from "./math.js";
15
16
  import { arraysEqual } from "../utils/arrays.js";
@@ -131,25 +132,82 @@ export async function verifyInclusion(hasher, leafHash, proof, root) {
131
132
  return arraysEqual(calculatedRoot, root);
132
133
  }
133
134
  /**
134
- * Verifies a consistency proof between two MMR states.
135
+ * Recover the MMR(B) accumulator prefix committed by the MMR(A) peaks
136
+ * (draft-bryce `consistent_roots`; go-merklelog `ConsistentRoots`).
135
137
  *
136
- * WARNING — NOT IMPLEMENTED. This is a stub: it compares each root against
137
- * itself and therefore ALWAYS returns true. It performs no real consistency
138
- * check and MUST NOT be relied upon for security. No worker path currently
139
- * calls it. See the reference `verify_consistency` (algorithms.py) for the
140
- * intended algorithm (verify proof2 extends proof1 via position-committed
141
- * interior hashing).
138
+ * Each MMR(A) peak is an interior node of MMR(B) at an immovable position;
139
+ * its inclusion path climbs to the covering MMR(B) peak. Consecutive
140
+ * duplicate roots collapse (many old peaks share one new peak). Requires
141
+ * one path per MMR(A) peak (draft: `len(peaks(ifrom)) == len(accumulatorfrom)`).
142
142
  *
143
- * @param hasher - Cryptographic hasher instance
144
- * @param proof1 - Proof for the first state
145
- * @param proof2 - Proof for the second state
146
- * @param root1 - Root hash of the first state
147
- * @param root2 - Root hash of the second state
148
- * @returns Always true (stub) — do not use for verification
143
+ * @param ifrom - last node index of the complete MMR(A) (`mmrSizeA - 1`)
144
+ * @param accumulatorFrom - MMR(A) peak values, descending height order
145
+ * @param paths - inclusion path per peak, proven in MMR(B)
149
146
  */
150
- export function verifyConsistency(hasher, proof1, proof2, root1, root2) {
151
- // TODO(plan-0027): Implement full consistency check (verify proof2 extends
152
- // proof1) with position-committed interior hashing. Until then this returns
153
- // true unconditionally and must not be used for any trust decision.
154
- return arraysEqual(root1, root1) && arraysEqual(root2, root2);
147
+ export async function consistentRoots(hasher, ifrom, accumulatorFrom, paths) {
148
+ const fromPeaks = peakMMRIndexes(ifrom);
149
+ if (fromPeaks.length !== paths.length) {
150
+ throw new Error(`a proof for each accumulator peak is required: ${fromPeaks.length} peaks, ${paths.length} paths`);
151
+ }
152
+ if (accumulatorFrom.length !== fromPeaks.length) {
153
+ throw new Error(`accumulator length mismatch: ${accumulatorFrom.length} values for ${fromPeaks.length} peaks`);
154
+ }
155
+ const roots = [];
156
+ for (let i = 0; i < accumulatorFrom.length; i++) {
157
+ const root = await calculateRoot(hasher, accumulatorFrom[i], { path: paths[i], mmrIndex: fromPeaks[i] }, fromPeaks[i]);
158
+ if (roots.length > 0 && arraysEqual(roots[roots.length - 1], root)) {
159
+ continue;
160
+ }
161
+ roots.push(root);
162
+ }
163
+ return roots;
164
+ }
165
+ /**
166
+ * Verify MMR(A) is a committed prefix of MMR(B)
167
+ * (draft-bryce "Verifying the Receipt of consistency";
168
+ * go-merklelog `VerifyConsistency`).
169
+ *
170
+ * Replaces the plan-0027 always-true stub (FOR-368 Phase 1,
171
+ * plan-2607-29): the previous signature took two inclusion proofs and
172
+ * returned true unconditionally; no caller existed.
173
+ *
174
+ * @param proof - consistency proof `mmrSizeA -> mmrSizeB`
175
+ * @param peaksFrom - TRUSTED MMR(A) accumulator (e.g. a signed checkpoint
176
+ * payload), descending height order
177
+ * @param peaksTo - MMR(B) accumulator to prove against (e.g. an anchored
178
+ * on-chain state), descending height order
179
+ * @returns ok, with the MMR(B) accumulator on success
180
+ */
181
+ export async function verifyConsistency(hasher, proof, peaksFrom, peaksTo) {
182
+ const proven = await consistentRoots(hasher, proof.mmrSizeA - 1n, peaksFrom, proof.paths);
183
+ // Both lists are in descending height order, so every proven root must
184
+ // appear in order within peaksTo (a linear scan; go-merklelog semantics:
185
+ // a proven root matches the current peak or exactly the next one down).
186
+ let ito = 0;
187
+ for (const root of proven) {
188
+ if (ito < peaksTo.length && arraysEqual(peaksTo[ito], root)) {
189
+ continue;
190
+ }
191
+ ito += 1;
192
+ if (ito >= peaksTo.length || !arraysEqual(peaksTo[ito], root)) {
193
+ return { ok: false, accumulator: [] };
194
+ }
195
+ }
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
+ return { ok: true, accumulator: peaksTo };
200
+ }
201
+ /**
202
+ * Generate a consistency proof `mmrIndexA -> mmrIndexB` from node data
203
+ * (go-merklelog `IndexConsistencyProof`): an inclusion path in MMR(B) for
204
+ * each MMR(A) peak. Node access is by MMR index (massif tiles or any
205
+ * replicated store).
206
+ */
207
+ export function indexConsistencyProof(get, mmrIndexA, mmrIndexB) {
208
+ const paths = [];
209
+ for (const peak of peakMMRIndexes(mmrIndexA)) {
210
+ paths.push(inclusionProof(get, mmrIndexB, peak));
211
+ }
212
+ return { mmrSizeA: mmrIndexA + 1n, mmrSizeB: mmrIndexB + 1n, paths };
155
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/merklelog",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
package/src/index.ts CHANGED
@@ -51,7 +51,7 @@ export type { MassifNodeStore } from "./massifs/nodestore.js";
51
51
 
52
52
  // Content-hash leaf lookup over the massif index region (FOR-373)
53
53
  export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
54
- export type { MassifLeafIndex } from "./massifs/leafindex.js";
54
+ export type { MassifLeafIndex, LocatedLeaf } from "./massifs/leafindex.js";
55
55
 
56
56
  // Export mmr module
57
57
  export { mmrIndex, massifFirstLeaf, leafMinusSpurSum } from "./mmr/index.js";
@@ -70,6 +70,9 @@ export {
70
70
  calculateRoot,
71
71
  verifyInclusion,
72
72
  verifyConsistency,
73
+ consistentRoots,
74
+ indexConsistencyProof,
75
+ type ConsistencyProof,
73
76
  } from "./mmr/algorithms.js";
74
77
  export { createSyncHasher } from "./mmr/algorithms-sync.js";
75
78
  export type { Proof, Peak } from "./mmr/types.js";
@@ -29,6 +29,7 @@ import { firstMMRSize, peaksBitmap } from "../mmr/proof.js";
29
29
  import { mmrIndex as mmrIndexFromLeafIndex } from "../mmr/index.js";
30
30
 
31
31
  const HASH_BYTES = Urkle.LeafValueBytes; // 32
32
+ const KEY_BYTES = Urkle.LeafKeyBytes; // 8 (idtimestamp, big-endian)
32
33
  const VALUE_OFFSET = Urkle.LeafValueOffset; // 8 (after the 8-byte key)
33
34
  const RECORD_BYTES = Urkle.LeafRecordBytes; // 128
34
35
 
@@ -44,6 +45,16 @@ export class MissingIndexError extends Error {
44
45
  }
45
46
  }
46
47
 
48
+ /**
49
+ * A located leaf: its MMR index plus the 8-byte idtimestamp (big-endian) the
50
+ * urkle leaf record is keyed by. Together these are the two halves of the
51
+ * permanent entry id (`idtimestamp_be8 || mmrIndex_be8`).
52
+ */
53
+ export interface LocatedLeaf {
54
+ mmrIndex: bigint;
55
+ idtimestampBe8: Uint8Array;
56
+ }
57
+
47
58
  /** Content-hash leaf lookup over one massif blob (FOR-373). */
48
59
  export interface MassifLeafIndex {
49
60
  /**
@@ -52,6 +63,12 @@ export interface MassifLeafIndex {
52
63
  * bytes.
53
64
  */
54
65
  findByContentHash(h: Uint8Array): bigint | null;
66
+ /**
67
+ * Like {@link findByContentHash} but also recovers the leaf's idtimestamp
68
+ * (the 8-byte urkle leaf key), so a caller can reconstruct the full entry id
69
+ * offline. Returns `null` when the hash is not present. `h` must be 32 bytes.
70
+ */
71
+ findLeafByContentHash(h: Uint8Array): LocatedLeaf | null;
55
72
  /** Number of populated leaves in this blob. */
56
73
  leafCount: number;
57
74
  }
@@ -123,7 +140,8 @@ export function openMassifLeafIndex(massifBytes: Uint8Array): MassifLeafIndex {
123
140
  populated = ordinal + 1;
124
141
  }
125
142
 
126
- const findByContentHash = (h: Uint8Array): bigint | null => {
143
+ /** Ordinal of the populated leaf whose value column equals `h`, or -1. */
144
+ const ordinalForContentHash = (h: Uint8Array): number => {
127
145
  if (h.length !== HASH_BYTES) {
128
146
  throw new Error(
129
147
  `content hash must be ${HASH_BYTES} bytes, got ${h.length}`,
@@ -133,12 +151,28 @@ export function openMassifLeafIndex(massifBytes: Uint8Array): MassifLeafIndex {
133
151
  const recordOff = leafTableStart + ordinal * RECORD_BYTES;
134
152
  const valueOff = recordOff + VALUE_OFFSET;
135
153
  if (valueOff + HASH_BYTES > massifBytes.byteLength) break;
136
- if (equals32(massifBytes, valueOff, h)) {
137
- return mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal));
138
- }
154
+ if (equals32(massifBytes, valueOff, h)) return ordinal;
139
155
  }
140
- return null;
156
+ return -1;
157
+ };
158
+
159
+ const findByContentHash = (h: Uint8Array): bigint | null => {
160
+ const ordinal = ordinalForContentHash(h);
161
+ if (ordinal < 0) return null;
162
+ return mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal));
163
+ };
164
+
165
+ const findLeafByContentHash = (h: Uint8Array): LocatedLeaf | null => {
166
+ const ordinal = ordinalForContentHash(h);
167
+ if (ordinal < 0) return null;
168
+ // The leaf record is `[key(8) || value(32) || …]`; the key IS the
169
+ // big-endian idtimestamp the sealer assigned (go-merklelog urkle leafrecord).
170
+ const keyOff = leafTableStart + ordinal * RECORD_BYTES;
171
+ return {
172
+ mmrIndex: mmrIndexFromLeafIndex(firstLeafIndex + BigInt(ordinal)),
173
+ idtimestampBe8: massifBytes.slice(keyOff, keyOff + KEY_BYTES),
174
+ };
141
175
  };
142
176
 
143
- return { findByContentHash, leafCount: populated };
177
+ return { findByContentHash, findLeafByContentHash, leafCount: populated };
144
178
  }
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { Proof, Hasher } from "./types.js";
15
+ import { inclusionProof, peakMMRIndexes, type NodeGetter } from "./proof.js";
15
16
  import { Uint64 } from "../uint64/index.js";
16
17
  import { heightIndex } from "./math.js";
17
18
  import { arraysEqual } from "../utils/arrays.js";
@@ -164,31 +165,124 @@ export async function verifyInclusion(
164
165
  }
165
166
 
166
167
  /**
167
- * Verifies a consistency proof between two MMR states.
168
+ * Consistency proof between two MMR states (draft-bryce `consistency-proof`,
169
+ * go-merklelog `ConsistencyProof`): one inclusion path per MMR(A) peak,
170
+ * proven in MMR(B). Verified against trusted accumulators for both sizes.
171
+ */
172
+ export interface ConsistencyProof {
173
+ mmrSizeA: bigint;
174
+ mmrSizeB: bigint;
175
+ /** One sibling path per MMR(A) peak (ascending), in peak order. */
176
+ paths: Uint8Array[][];
177
+ }
178
+
179
+ /**
180
+ * Recover the MMR(B) accumulator prefix committed by the MMR(A) peaks
181
+ * (draft-bryce `consistent_roots`; go-merklelog `ConsistentRoots`).
168
182
  *
169
- * WARNING — NOT IMPLEMENTED. This is a stub: it compares each root against
170
- * itself and therefore ALWAYS returns true. It performs no real consistency
171
- * check and MUST NOT be relied upon for security. No worker path currently
172
- * calls it. See the reference `verify_consistency` (algorithms.py) for the
173
- * intended algorithm (verify proof2 extends proof1 via position-committed
174
- * interior hashing).
183
+ * Each MMR(A) peak is an interior node of MMR(B) at an immovable position;
184
+ * its inclusion path climbs to the covering MMR(B) peak. Consecutive
185
+ * duplicate roots collapse (many old peaks share one new peak). Requires
186
+ * one path per MMR(A) peak (draft: `len(peaks(ifrom)) == len(accumulatorfrom)`).
175
187
  *
176
- * @param hasher - Cryptographic hasher instance
177
- * @param proof1 - Proof for the first state
178
- * @param proof2 - Proof for the second state
179
- * @param root1 - Root hash of the first state
180
- * @param root2 - Root hash of the second state
181
- * @returns Always true (stub) — do not use for verification
188
+ * @param ifrom - last node index of the complete MMR(A) (`mmrSizeA - 1`)
189
+ * @param accumulatorFrom - MMR(A) peak values, descending height order
190
+ * @param paths - inclusion path per peak, proven in MMR(B)
191
+ */
192
+ export async function consistentRoots(
193
+ hasher: Hasher,
194
+ ifrom: bigint,
195
+ accumulatorFrom: Uint8Array[],
196
+ paths: Uint8Array[][],
197
+ ): Promise<Uint8Array[]> {
198
+ const fromPeaks = peakMMRIndexes(ifrom);
199
+ if (fromPeaks.length !== paths.length) {
200
+ throw new Error(
201
+ `a proof for each accumulator peak is required: ${fromPeaks.length} peaks, ${paths.length} paths`,
202
+ );
203
+ }
204
+ if (accumulatorFrom.length !== fromPeaks.length) {
205
+ throw new Error(
206
+ `accumulator length mismatch: ${accumulatorFrom.length} values for ${fromPeaks.length} peaks`,
207
+ );
208
+ }
209
+ const roots: Uint8Array[] = [];
210
+ for (let i = 0; i < accumulatorFrom.length; i++) {
211
+ const root = await calculateRoot(
212
+ hasher,
213
+ accumulatorFrom[i],
214
+ { path: paths[i], mmrIndex: fromPeaks[i] },
215
+ fromPeaks[i],
216
+ );
217
+ if (roots.length > 0 && arraysEqual(roots[roots.length - 1], root)) {
218
+ continue;
219
+ }
220
+ roots.push(root);
221
+ }
222
+ return roots;
223
+ }
224
+
225
+ /**
226
+ * Verify MMR(A) is a committed prefix of MMR(B)
227
+ * (draft-bryce "Verifying the Receipt of consistency";
228
+ * go-merklelog `VerifyConsistency`).
229
+ *
230
+ * Replaces the plan-0027 always-true stub (FOR-368 Phase 1,
231
+ * plan-2607-29): the previous signature took two inclusion proofs and
232
+ * returned true unconditionally; no caller existed.
233
+ *
234
+ * @param proof - consistency proof `mmrSizeA -> mmrSizeB`
235
+ * @param peaksFrom - TRUSTED MMR(A) accumulator (e.g. a signed checkpoint
236
+ * payload), descending height order
237
+ * @param peaksTo - MMR(B) accumulator to prove against (e.g. an anchored
238
+ * on-chain state), descending height order
239
+ * @returns ok, with the MMR(B) accumulator on success
182
240
  */
183
- export function verifyConsistency(
241
+ export async function verifyConsistency(
184
242
  hasher: Hasher,
185
- proof1: Proof,
186
- proof2: Proof,
187
- root1: Uint8Array,
188
- root2: Uint8Array,
189
- ): boolean {
190
- // TODO(plan-0027): Implement full consistency check (verify proof2 extends
191
- // proof1) with position-committed interior hashing. Until then this returns
192
- // true unconditionally and must not be used for any trust decision.
193
- return arraysEqual(root1, root1) && arraysEqual(root2, root2);
243
+ proof: ConsistencyProof,
244
+ peaksFrom: Uint8Array[],
245
+ peaksTo: Uint8Array[],
246
+ ): Promise<{ ok: boolean; accumulator: Uint8Array[] }> {
247
+ const proven = await consistentRoots(
248
+ hasher,
249
+ proof.mmrSizeA - 1n,
250
+ peaksFrom,
251
+ proof.paths,
252
+ );
253
+ // Both lists are in descending height order, so every proven root must
254
+ // appear in order within peaksTo (a linear scan; go-merklelog semantics:
255
+ // a proven root matches the current peak or exactly the next one down).
256
+ let ito = 0;
257
+ for (const root of proven) {
258
+ if (ito < peaksTo.length && arraysEqual(peaksTo[ito], root)) {
259
+ continue;
260
+ }
261
+ ito += 1;
262
+ if (ito >= peaksTo.length || !arraysEqual(peaksTo[ito], root)) {
263
+ return { ok: false, accumulator: [] };
264
+ }
265
+ }
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
+ return { ok: true, accumulator: peaksTo };
270
+ }
271
+
272
+ /**
273
+ * Generate a consistency proof `mmrIndexA -> mmrIndexB` from node data
274
+ * (go-merklelog `IndexConsistencyProof`): an inclusion path in MMR(B) for
275
+ * each MMR(A) peak. Node access is by MMR index (massif tiles or any
276
+ * replicated store).
277
+ */
278
+ export function indexConsistencyProof(
279
+ get: NodeGetter,
280
+ mmrIndexA: bigint,
281
+ mmrIndexB: bigint,
282
+ ): ConsistencyProof {
283
+ const paths: Uint8Array[][] = [];
284
+ for (const peak of peakMMRIndexes(mmrIndexA)) {
285
+ paths.push(inclusionProof(get, mmrIndexB, peak));
286
+ }
287
+ return { mmrSizeA: mmrIndexA + 1n, mmrSizeB: mmrIndexB + 1n, paths };
194
288
  }