The Indexed Merkle Map API is currently an experimental feature.
Indexed Merkle Map
Similar to a Merkle Tree, a Merkle Map allows referencing off-chain data by storing a single hash, also known as the root.
A Merkle Map is a wrapper around a Merkle Tree. Both data structures are analogous, but instead of using an index to set a leaf in a tree, a Merkle Map uses a key in a map.
Design
The Indexed Merkle Map is an improved version of the MerkleMap, offering enhanced efficiency and usability:
- Reduced Constraints: Uses 4-8x fewer constraints than
MerkleMap
. - Support for Dummy Updates: Handles dummy updates for keys like
0
and-1
. - Provable Code Integration: Unlike
MerkleTree
andMerkleMap
, the high-level API ofIndexedMerkleMap
is usable within provable code. - Comprehensive Methods: Comes with a rich set of methods that enable a wide range of operations.
Utilizing Indexed Merkle Map
Prerequisites
The IndexedMerkleMap
API is accessible within the Experimental
namespace. To use the API, import Experimental
from o1js version 1.5.0 or higher.
import { Experimental } from 'o1js';
const { IndexedMerkleMap } = Experimental;
Instantiating an Indexed Merkle Map
Given a height, you can instantiate an Indexed Merkle Map by extending the base class. The height determines the capacity of the map; the maximum number of leaf nodes it can contain.
const height = 31;
class IndexedMerkleMap31 extends IndexedMerkleMap(height) {}
In this example, IndexedMerkleMap31
is a Merkle map capable of holding up to 2(31−1) leaves; approximately 1 billion entries.
Utilizing IndexedMerkleMap in a smart contract
Developers can integrate the IndexedMerkleMap
into their zkApps by passing a Merkle map as a method parameter and invoking its methods as needed.
This is possible because the IndexedMerkleMap
can be used within provable code.
class MyContract extends SmartContract {
@state(Field) mapRoot = State<Field>();
init() {
super.init();
this.mapRoot.set(new IndexedMerkleMap31().root);
}
@method async insertNewLeaf(
newKey: Field,
newValue: Field,
indexedMerkleMap: IndexedMerkleMap31
) {
// Validate the integrity of the Merkle Map input
const currentRoot = this.mapRoot.getAndRequireEquals();
currentRoot.assertEquals(
indexedMerkleMap.root,
'Off-chain Indexed Merkle Map is out of sync!'
);
// Insert a new key-value pair
indexedMerkleMap = indexedMerkleMap.clone();
indexedMerkleMap.insert(newKey, newValue);
// Update the on-chain map root
const newMapRoot = indexedMerkleMap.root;
this.mapRoot.set(newMapRoot);
}
@method async updateExistingLeaf(
key: Field,
newValue: Field,
indexedMerkleMap: IndexedMerkleMap31
) {
// Validate integrity of the Merkle Map input
const currentRoot = this.mapRoot.getAndRequireEquals();
currentRoot.assertEquals(
indexedMerkleMap.root,
'Off-chain Indexed Merkle Map is out of sync!'
);
indexedMerkleMap = indexedMerkleMap.clone();
/**
* Proves that the key exists
* Updates an existing leaf
* Returns the previous value
*/
indexedMerkleMap.update(key, newValue);
// Update the on-chain map root
const newMapRoot = indexedMerkleMap.root;
this.mapRoot.set(newMapRoot);
}
}
⚠️ Direct modification of a method input can lead to errors. To avoid this, perform changes on a cloned Merkle Map rather than altering the map passed as a method input.
Interacting with a smart contract utilizing an indexed Merkle Map
To interact with a zkapp that utilizes an IndexedMerkleMap
, instantiate a map instance and pass it as an argument to the contract’s method within a Mina transaction.
In addition, ensure you synchronize your off-chain map with on-chain updates to maintain data integrity and prepare for subsequent interactions.
// Instantiate a new Indexed Merkle Map
indexedMerkleMap = new IndexedMerkleMap31();
const insertTx = await Mina.transaction(userPubKey, async () => {
await contract.insertNewLeaf(Field(1), Field(1234), indexedMerkleMap);
});
await insertTx.prove();
await insertTx.sign([userKey]).send();
// Synchornize the off-chain Indexed Merkle Map to match the on-chain state root
indexedMerkleMap.insert(Field(1), Field(1234));
console.log(
indexedMerkleMap.root.toBigInt() === contract.mapRoot.get().toBigInt()
);
console.log(indexedMerkleMap.data.get().sortedLeaves);
const updateTx = await Mina.transaction(userPubKey, async () => {
await contract.updateExistingLeaf(Field(1), Field(5678), indexedMerkleMap);
});
await updateTx.prove();
await updateTx.sign([userKey]).send();
// Synchronize the off-chain Indexed Merkle Map to match the on-chain state root
indexedMerkleMap.update(Field(1), Field(5678));
console.log(
indexedMerkleMap.root.toBigInt() === contract.mapRoot.get().toBigInt()
);
console.log(indexedMerkleMap.data.get().sortedLeaves);
Indexed Merkle Map - API reference
The Indexed Merkle Map API provides a comprehensive set of methods that enable developers to perform a wide range of operations within provable code.
/**
* Clone the entire Merkle map.
*
* This method is provable.
*/
clone(): IndexedMerkleMapBase;
/**
* Overwrite the entire Merkle map with another one.
*
* This method is provable.
*/
overwrite(other: IndexedMerkleMapBase): void;
/**
* Overwrite the entire Merkle map with another one, if the condition is true.
*
* This method is provable.
*/
overwriteIf(condition: Bool | boolean, other: IndexedMerkleMapBase): void;
/**
* Insert a new leaf `(key, value)`.
*
* Proves that `key` doesn't exist yet.
*/
insert(key: Field | bigint, value: Field | bigint): void;
/**
* Update an existing leaf `(key, value)`.
*
* Proves that the `key` exists.
*
* Returns the previous value.
*/
update(key: Field | bigint, value: Field | bigint): Field;
/**
* Perform _either_ an insertion or update, depending on whether the key exists.
*
* Note: This method is handling both the `insert()` and `update()` case at the same time, so you
* can use it if you don't know whether the key exists or not.
*
* However, this comes at an efficiency cost, so prefer to use `insert()` or `update()` if you know whether the key exists.
*
* Returns the previous value, as an option (which is `None` if the key didn't exist before).
*/
set(key: Field | bigint, value: Field | bigint): Option<Field, bigint>;
/**
* Perform an insertion or update, if the enabling condition is true.
*
* If the condition is false, we instead set the 0 key to the value 0.
* This is the initial value and for typical uses of `IndexedMerkleMap`, it is guaranteed to be a no-op because the 0 key is never used.
*
* **Warning**: Only use this method if you are sure that the 0 key is not used in your application.
* Otherwise, you might accidentally overwrite a valid key-value pair.
*/
setIf(condition: Bool | boolean, key: Field | bigint, value: Field | bigint): Option<import("./field.js").Field, bigint>;
/**
* Get a value from a key.
*
* Proves that the key already exists in the map yet and fails otherwise.
*/
get(key: Field | bigint): Field;
/**
* Get a value from a key.
*
* Returns an option which is `None` if the key doesn't exist. (In that case, the option's value is unconstrained.)
*
* Note that this is more flexible than `get()` and allows you to handle the case where the key doesn't exist.
* However, it uses about twice as many constraints for that reason.
*/
getOption(key: Field | bigint): Option<Field, bigint>;
/**
* Prove that the given key exists in the map.
*/
assertIncluded(key: Field | bigint, message?: string): void;
/**
* Prove that the given key does not exist in the map.
*/
assertNotIncluded(key: Field | bigint, message?: string): void;
/**
* Check whether the given key exists in the map.
*/
isIncluded(key: Field | bigint): Bool;
Additional Resources
For more details and examples, please refer to the following GitHub resources: