merkle-heap-snarkyjs
Version:
Implementation of a Merkle Heap for SnarkyJS, a framework to develop ZK-snarks on Mina Protocol.
267 lines (213 loc) • 9.44 kB
text/typescript
/**
* This file contains all code related to the Merkle Heap implementation available for SnarkyJS.
*/
import { Field } from "snarkyjs";
import {
Circuit,
MerkleTree,
isReady
}from 'snarkyjs';
/**
* A [Merkle Heap] (https://en.wikipedia.org/wiki/Binary_heap) is a Binary Heap built on
* top of a [Merkle Tree] (https://en.wikipedia.org/wiki/Merkle_tree).
*
* A Merkle Heap allows developers to easily and securely build priority queues with verifiable data.
*
* This library is built to be used with ZKApps and to be verifiable for large amounts of data.
*
* To understand how to use the library look at our [docs] (https://bon-sai.notion.site/Merkle-Heap-Based-Priority-Queue-Implementation-Docs-b198b02da22f4eb3b4a31a235b99dafe)
*
*/
export class MerkleHeap extends MerkleTree{
// Change the name of this variable to heapCount?
private nextIndexToAdd: bigint;
private numberOfNodes: bigint;
constructor(height: number) { //Option 1 Merkle Heap extendes from Merkle Tree so we call Merkle Tree constrcutr
//Option 2: Instance Merkle Tree library in a variable , can theuy accesses to the private methods?
const merkleTreeHeight = height + 1;
super(merkleTreeHeight);
this.numberOfNodes = BigInt( (2**height) - 1 );
this.nextIndexToAdd = 0n;
}
private getFatherIndexOfChild( childIndex: bigint ) {
return childIndex > 0n ? (childIndex - 1n) / 2n : null;
}
private getChildIndexesOfFather( fatherIndex: bigint ) {
let leftIndex = (2n * fatherIndex) + 1n;
if( leftIndex >= this.nextIndexToAdd ) return {left: null, right: null};
return {
left: leftIndex,
right: leftIndex + 1n
}
}
private getSmallerChildIndexOfFather( fatherIndex: bigint ) {
let childIndexes = this.getChildIndexesOfFather( fatherIndex );
let leftChildValue = this.getMerkleTreeLeaf( childIndexes.left );
let rightChildValue = this.getMerkleTreeLeaf( childIndexes.right );
if( !leftChildValue ) return null;
return !rightChildValue || leftChildValue.lte(rightChildValue) ? childIndexes.left : childIndexes.right;
}
public getHeapRoot() {
return this.getMerkleTreeLeaf(0n);
}
private findElementIndex( valueToFind: Field ): bigint | null {
let currentElement;
for( let i = 0n; i < this.nextIndexToAdd; i++ ) {
currentElement = this.getMerkleTreeLeaf( i );
if( currentElement?.equals( valueToFind ).toBoolean() )
return i;
}
return null;
}
private downHeap( startingIndex: bigint ) {
let currentIndex = startingIndex;
let currentValue = this.getMerkleTreeLeaf( currentIndex );
if( !currentValue ) return;
let smallerChildIndex = this.getSmallerChildIndexOfFather( currentIndex );
let smallerChildValue = this.getMerkleTreeLeaf( smallerChildIndex );
while( smallerChildIndex !== null && smallerChildValue !== null && currentValue.toBigInt() > smallerChildValue.toBigInt() ) {
this.setLeaf(currentIndex, smallerChildValue);
this.setLeaf(smallerChildIndex, currentValue);
currentIndex = smallerChildIndex;
smallerChildIndex = this.getSmallerChildIndexOfFather( currentIndex );
smallerChildValue = this.getMerkleTreeLeaf( smallerChildIndex );
}
}
private upHeap( startingIndex: bigint ) {
let currentIndex = startingIndex;
let currentValue = this.getMerkleTreeLeaf(currentIndex);
if( !currentValue ) return;
let fatherIndex = this.getFatherIndexOfChild(currentIndex);
let fatherValue = this.getMerkleTreeLeaf(fatherIndex);
// TODO: Review if is necessary to use Field.gt instead of Field.toBigInt() > Field.toBigInt()
while( fatherIndex !== null && fatherValue !== null && fatherValue.toBigInt() > currentValue.toBigInt() ) {
this.setLeaf( fatherIndex, currentValue );
this.setLeaf( currentIndex, fatherValue );
currentIndex = fatherIndex;
fatherIndex = this.getFatherIndexOfChild(currentIndex);
fatherValue = this.getMerkleTreeLeaf(fatherIndex);
}
}
getMerkleTreeLeaf( index: bigint | null ) {
return index !== null && index >= 0 && index < this.nextIndexToAdd
? this.getNode(0, index)
: null;
}
/**
* Insert an element into the heap, mantaining the Heap
* Property and recalculating the hashes of the Merkle Tree.
* @param value that is going to be inserted
*/
insert( value: Field ) {
// Insert the element at the leftmost open space in the bottom of the heap.
// Compare the element with its father. If they are in the correct order, stop
// Otherwise swap the element with its father and make the comparison again.
// Until the Heap Property is correct.
let currentIndexToAdd = this.nextIndexToAdd;
if( currentIndexToAdd >= this.numberOfNodes )
throw new Error(
`Heap is full and value cannot be inserted. Upper index limit: ${this.numberOfNodes}, current index to insert: ${currentIndexToAdd}`
);
this.setLeaf(currentIndexToAdd, value);
this.nextIndexToAdd = this.nextIndexToAdd + 1n;
this.upHeap( currentIndexToAdd );
}
/**
* Delete an arbitrary element of the queue at a given index.
* @param index where the element to delete is going to be located
* @returns the value that was deleted from the queue.
*/
deleteElementAtIndex( index: bigint ): Field | null {
if( index < 0 && index >= this.nextIndexToAdd ) return null;
const elementToDelete = this.getMerkleTreeLeaf(index);
let lastElementIndex = this.nextIndexToAdd - 1n;
let currentValue = this.getMerkleTreeLeaf( lastElementIndex );
if( !currentValue ) return null;
this.setLeaf(index, currentValue);
this.setLeaf(lastElementIndex, new Field(0));
this.nextIndexToAdd = this.nextIndexToAdd - 1n;
this.downHeap( index );
return elementToDelete;
}
/**
* Delete an arbitrary element of the queue with a given value.
* @param value that is going to be searched and deleted if it is found.
* @returns the value deleted from the queue.
*/
deleteElement( value: Field ): Field | null {
let elementToDeleteIndex = this.findElementIndex( value );
return elementToDeleteIndex !== null
? this.deleteElementAtIndex( elementToDeleteIndex )
: null;
}
/**
* Delete the minimum element in the queue.
* @returns the min value deleted from the queue.
*/
deleteMin(): Field | null {
// Replace the root of the tree with the last element of the last level.
// Reduce this.nextIndexToAdd
// Set the deleted leaf to a zero value in the MerkleTree (This is absoulutely necessary??)
// Compare the element with its children. If it is in correct order, stop.
// If not, swap the element.
// Repeat this until the heap property is correct.
return this.deleteElementAtIndex(0n);
}
/**
* Delete the min element of the heap and then insert another element.
* It is more efficient than executing a deleteMin and an insert independently.
* @param insertValue
* @returns the value that was inserted after the deleteMin
*/
deleteMinThenInsert( insertValue: Field ): Field {
this.setLeaf(0n, insertValue);
this.nextIndexToAdd = this.nextIndexToAdd - 1n;
this.downHeap( 0n );
return insertValue;
}
/**
* Insert an element into the heap and then extract the root of the tree.
* It is more efficient than executing an insert and a deleteMin independently.
* @param insertValue
* @returns the min value deleted from the queue.
*/
insertThenDeleteMin( insertValue: Field ): Field {
const root = this.getHeapRoot();
if( root == null || root?.toBigInt() > insertValue.toBigInt() )
return insertValue;
this.setLeaf(0n, insertValue);
this.nextIndexToAdd = this.nextIndexToAdd - 1n;
this.downHeap( 0n );
return root;
}
/**
* Search if a value is part of the queue
* @param value to search.
* @returns true if the value is in the queue or false otherwise.
*/
inQueue( value: Field ): boolean {
return this.findElement( value ) == null ? false : true;
}
/**
* @returns the min element of the queue without deleting it.
*/
findMin(): Field | null {
return this.getHeapRoot();
}
/**
* Find an arbitrary element in the heap without deleting it.
* @param valueToFind
* @returns the element found or null if it doesn't exist.
*/
findElement( valueToFind: Field ): Field | null {
let elementFoundIndex = this.findElementIndex( valueToFind );
return this.getMerkleTreeLeaf( elementFoundIndex );
}
/**
* // TODO: We are going to implement this?
* @returns the max element of the queue without deleting it.
*/
findMax(): Field {
return new Field(0);
}
}