Skip to content

Commit 77cbcb3

Browse files
authored
Merge pull request #2114 from o1-labs/2025-04-promote-indexed-merkle-map
Promote indexed merkle map to standard API
2 parents 32323d3 + 32a3ef3 commit 77cbcb3

File tree

5 files changed

+40
-22
lines changed

5 files changed

+40
-22
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ This project adheres to
3232
methods of a ZkProgram by executing them.
3333
- Now only a single method is analyzed at a time.
3434

35+
### Changed
36+
- `IndexedMerkleMap` has been promoted from the `Experimental` namespace and is now part of the public API. https://github.com/o1-labs/o1js/pull/2114
37+
-
38+
### Fixed
39+
- The `IndexedMerkleMap` root now includes the tree length in its root(commitment), addressing a vulnerability where a malicious user could insert a larger leaf and render the tree unreconstructable by others.
40+
3541
## [2.6.0](https://github.com/o1-labs/o1js/compare/4e23a60...3eef10d) - 2025-05-30
3642

3743
### Added

src/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ export { Types } from './bindings/mina-transaction/v1/types.js';
4444
export { DynamicArray } from './lib/provable/dynamic-array.js';
4545

4646
export { MerkleList, MerkleListIterator } from './lib/provable/merkle-list.js';
47-
import { IndexedMerkleMap, IndexedMerkleMapBase } from './lib/provable/merkle-tree-indexed.js';
47+
import {
48+
IndexedMerkleMap as IndexedMerkleMap_,
49+
IndexedMerkleMapBase,
50+
} from './lib/provable/merkle-tree-indexed.js';
51+
export let IndexedMerkleMap = IndexedMerkleMap_;
52+
export type IndexedMerkleMap = IndexedMerkleMapBase;
4853
export { Option } from './lib/provable/option.js';
4954

5055
export * as Mina from './lib/mina/v1/mina.js';
@@ -130,7 +135,6 @@ import { Field } from './lib/provable/wrapped.js';
130135

131136
const Experimental_ = {
132137
memoizeWitness,
133-
IndexedMerkleMap,
134138
V2: V2_,
135139
};
136140

@@ -167,10 +171,6 @@ namespace Experimental {
167171
export let ProvableBigInt = ProvableBigInt_;
168172
export let createProvableBigInt = createProvableBigInt_;
169173

170-
// indexed merkle map
171-
export let IndexedMerkleMap = Experimental_.IndexedMerkleMap;
172-
export type IndexedMerkleMap = IndexedMerkleMapBase;
173-
174174
// offchain state
175175
export let OffchainState = OffchainState_.OffchainState;
176176

src/lib/mina/v1/actions/batch-reducer.unit-test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Bool,
77
Experimental,
88
Field,
9+
IndexedMerkleMap,
910
method,
1011
Poseidon,
1112
Provable,
@@ -17,7 +18,7 @@ import {
1718
assert,
1819
} from '../../../../index.js';
1920
import { TestInstruction, expectBalance, testLocal, transaction } from '../test/test-contract.js';
20-
const { IndexedMerkleMap, BatchReducer } = Experimental;
21+
const { BatchReducer } = Experimental;
2122

2223
const MINA = 1_000_000_000n;
2324
const AMOUNT = 10n * MINA;

src/lib/provable/merkle-tree-indexed.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function IndexedMerkleMap(height: number): typeof IndexedMerkleMapBase {
7171
}
7272

7373
const provableBase = {
74-
root: Field,
74+
_internalRoot: Field,
7575
length: Field,
7676
data: Unconstrained.withEmpty({
7777
nodes: [] as (bigint | undefined)[][],
@@ -81,7 +81,7 @@ const provableBase = {
8181

8282
class IndexedMerkleMapBase {
8383
// data defining the provable interface of a tree
84-
root: Field;
84+
_internalRoot: Field;
8585
length: Field; // length of the leaves array
8686

8787
// static data defining constraints
@@ -102,6 +102,14 @@ class IndexedMerkleMapBase {
102102
readonly sortedLeaves: StoredLeaf[];
103103
}>;
104104

105+
/**
106+
* Public getter for the root(commitment) that combines the internal root of the indexed merkle tree and length.
107+
* This provides protection against a attack vector mentioned in https://github.com/o1-labs/o1js/pull/2114#issuecomment-2948339955
108+
*/
109+
get root(): Field {
110+
return Poseidon.hash([this._internalRoot, this.length]);
111+
}
112+
105113
// we'd like to do `abstract static provable` here but that's not supported
106114
static provable: Provable<IndexedMerkleMapBase, InferValue<typeof provableBase>> =
107115
undefined as any;
@@ -120,7 +128,7 @@ class IndexedMerkleMapBase {
120128
let firstLeaf = IndexedMerkleMapBase._firstLeaf;
121129
let firstNode = Leaf.hashNode(firstLeaf).toBigInt();
122130
let root = Nodes.setLeaf(nodes, 0, firstNode);
123-
this.root = Field(root);
131+
this._internalRoot = Field(root);
124132
this.length = Field(1);
125133

126134
this.data = Unconstrained.from({ nodes, sortedLeaves: [firstLeaf] });
@@ -142,7 +150,7 @@ class IndexedMerkleMapBase {
142150
*/
143151
clone() {
144152
let cloned = new (this.constructor as typeof IndexedMerkleMapBase)();
145-
cloned.root = this.root;
153+
cloned._internalRoot = this._internalRoot;
146154
cloned.length = this.length;
147155
cloned.data.updateAsProver(() => {
148156
let { nodes, sortedLeaves } = this.data.get();
@@ -171,7 +179,7 @@ class IndexedMerkleMapBase {
171179
overwriteIf(condition: Bool | boolean, other: IndexedMerkleMapBase) {
172180
condition = Bool(condition);
173181

174-
this.root = Provable.if(condition, other.root, this.root);
182+
this._internalRoot = Provable.if(condition, other._internalRoot, this._internalRoot);
175183
this.length = Provable.if(condition, other.length, this.length);
176184
this.data.updateAsProver(() =>
177185
Bool(condition).toBoolean() ? other.clone().data.get() : this.data.get()
@@ -201,7 +209,7 @@ class IndexedMerkleMapBase {
201209

202210
// update low node
203211
let newLow = { ...low, nextKey: key };
204-
this.root = this._proveUpdate(newLow, lowPath);
212+
this._internalRoot = this._proveUpdate(newLow, lowPath);
205213
this._setLeafUnconstrained(true, newLow);
206214

207215
// create new leaf to append
@@ -213,7 +221,7 @@ class IndexedMerkleMapBase {
213221

214222
// prove empty slot in the tree, and insert our leaf
215223
let path = this._proveEmpty(indexBits);
216-
this.root = this._proveUpdate(leaf, path);
224+
this._internalRoot = this._proveUpdate(leaf, path);
217225
this.length = this.length.add(1);
218226
this._setLeafUnconstrained(false, leaf);
219227
}
@@ -238,7 +246,7 @@ class IndexedMerkleMapBase {
238246

239247
// update leaf
240248
let newSelf = { ...self, value };
241-
this.root = this._proveUpdate(newSelf, path);
249+
this._internalRoot = this._proveUpdate(newSelf, path);
242250
this._setLeafUnconstrained(true, newSelf);
243251

244252
return self.value;
@@ -275,7 +283,7 @@ class IndexedMerkleMapBase {
275283

276284
// update low node, or leave it as is
277285
let newLow = { ...low, nextKey: key };
278-
this.root = this._proveUpdate(newLow, lowPath);
286+
this._internalRoot = this._proveUpdate(newLow, lowPath);
279287
this._setLeafUnconstrained(true, newLow);
280288

281289
// prove inclusion of this leaf if it exists
@@ -288,7 +296,7 @@ class IndexedMerkleMapBase {
288296
value,
289297
nextKey: Provable.if(keyExists, self.nextKey, low.nextKey),
290298
});
291-
this.root = this._proveUpdate(newLeaf, path);
299+
this._internalRoot = this._proveUpdate(newLeaf, path);
292300
this.length = Provable.if(keyExists, this.length, this.length.add(1));
293301
this._setLeafUnconstrained(keyExists, newLeaf);
294302

@@ -403,7 +411,7 @@ class IndexedMerkleMapBase {
403411
let node = Leaf.hashNode(leaf);
404412
// here, we don't care at which index the leaf is included, so we pass it in as unconstrained
405413
let { root, path } = this._computeRoot(node, leaf.index);
406-
root.assertEquals(this.root, message ?? 'Leaf is not included in the tree');
414+
root.assertEquals(this._internalRoot, message ?? 'Leaf is not included in the tree');
407415

408416
return path;
409417
}
@@ -416,7 +424,7 @@ class IndexedMerkleMapBase {
416424
// here, we don't care at which index the leaf is included, so we pass it in as unconstrained
417425
let { root } = this._computeRoot(node, leaf.index);
418426
assert(
419-
condition.implies(root.equals(this.root)),
427+
condition.implies(root.equals(this._internalRoot)),
420428
message ?? 'Leaf is not included in the tree'
421429
);
422430
}
@@ -429,7 +437,7 @@ class IndexedMerkleMapBase {
429437
_proveEmpty(index: Bool[]) {
430438
let node = Field(0n);
431439
let { root, path } = this._computeRoot(node, index);
432-
root.assertEquals(this.root, 'Leaf is not empty');
440+
root.assertEquals(this._internalRoot, 'Leaf is not empty');
433441

434442
return path;
435443
}
@@ -442,7 +450,7 @@ class IndexedMerkleMapBase {
442450
_proveInclusionOrEmpty(condition: Bool, index: Bool[], leaf: BaseLeaf, message?: string) {
443451
let node = Provable.if(condition, Leaf.hashNode(leaf), Field(0n));
444452
let { root, path } = this._computeRoot(node, index);
445-
root.assertEquals(this.root, message ?? 'Leaf is not included in the tree');
453+
root.assertEquals(this._internalRoot, message ?? 'Leaf is not included in the tree');
446454

447455
return path;
448456
}

src/lib/provable/test/merkle-tree.unit-test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Provable } from '../provable.js';
99
import { constraintSystem } from '../../testing/constraint-system.js';
1010
import { field } from '../../testing/equivalent.js';
1111
import { throwError } from './test-utils.js';
12+
import { Poseidon } from '../crypto/poseidon.js';
1213

1314
const height = 31;
1415
const IndexedMap30 = IndexedMerkleMap(height);
@@ -99,7 +100,9 @@ console.log(
99100
expect(map.length.toBigInt()).toEqual(1n);
100101
let initialTree = new MerkleTree(3);
101102
initialTree.setLeaf(0n, Leaf.hashNode(IndexedMerkleMap(3)._firstLeaf));
102-
expect(map.root).toEqual(initialTree.getRoot());
103+
// The merkle root is combined with length for safety
104+
let expectedRoot = Poseidon.hash([initialTree.getRoot(), Field(1)]);
105+
expect(map.root).toEqual(expectedRoot);
103106

104107
// the initial value at key 0 is 0
105108
expect(map.getOption(0n).assertSome().toBigInt()).toEqual(0n);

0 commit comments

Comments
 (0)