UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

769 lines (658 loc) 25.6 kB
import { Poseidon as PoseidonBigint } from '../../bindings/crypto/poseidon.js'; import { Bool, Field } from './wrapped.js'; import { Option } from './option.js'; import { Struct } from './types/struct.js'; import { From, InferValue } from '../../bindings/lib/provable-generic.js'; import { assert } from './gadgets/common.js'; import { Unconstrained } from './types/unconstrained.js'; import { Provable } from './provable.js'; import { Poseidon } from './crypto/poseidon.js'; import { conditionalSwap } from './merkle-tree.js'; import { provableFromClass } from './types/provable-derivers.js'; // external API export { IndexedMerkleMap, IndexedMerkleMapBase }; // internal API export { Leaf }; /** * Class factory for an Indexed Merkle Map with a given height. * * ```ts * class MerkleMap extends IndexedMerkleMap(33) {} * * let map = new MerkleMap(); * * map.insert(2n, 14n); * map.insert(1n, 13n); * * let x = map.get(2n); // 14 * ``` * * Indexed Merkle maps can be used directly in provable code: * * ```ts * ZkProgram({ * methods: { * test: { * privateInputs: [MerkleMap, Field], * * method(map: MerkleMap, key: Field) { * // get the value associated with `key` * let value = map.getOption(key).orElse(0n); * * // increment the value by 1 * map.set(key, value.add(1)); * } * } * } * }) * ``` * * Initially, every `IndexedMerkleMap` is populated by a single key-value pair: `(0, 0)`. The value for key `0` can be updated like any other. * When keys and values are hash outputs, `(0, 0)` can serve as a convenient way to represent a dummy update to the tree, since 0 is not * efficiently computable as a hash image, and this update doesn't affect the Merkle root. */ function IndexedMerkleMap(height: number): typeof IndexedMerkleMapBase { assert(height > 0, 'height must be positive'); assert( height < 53, 'height must be less than 53, so that we can use 64-bit floats to represent indices.' ); return class IndexedMerkleMap extends IndexedMerkleMapBase { get height() { return height; } static provable = provableFromClass(IndexedMerkleMap, provableBase); }; } const provableBase = { root: Field, length: Field, data: Unconstrained.withEmpty({ nodes: [] as (bigint | undefined)[][], sortedLeaves: [] as StoredLeaf[], }), }; class IndexedMerkleMapBase { // data defining the provable interface of a tree root: Field; length: Field; // length of the leaves array // static data defining constraints get height(): number { throw Error('Height must be defined in a subclass'); } // the raw data stored in the tree readonly data: Unconstrained<{ // for every level, an array of hashes readonly nodes: (bigint | undefined)[][]; // leaves sorted by key, with a linked list encoded by nextKey // we always have // sortedLeaves[0].key = 0 // sortedLeaves[n-1].nextKey = Field.ORDER - 1 // for i=0,...n-2, sortedLeaves[i].nextKey = sortedLeaves[i+1].key readonly sortedLeaves: StoredLeaf[]; }>; // we'd like to do `abstract static provable` here but that's not supported static provable: Provable<IndexedMerkleMapBase, InferValue<typeof provableBase>> = undefined as any; /** * Creates a new, empty Indexed Merkle Map. */ constructor() { let height = this.height; let nodes: (bigint | undefined)[][] = Array(height); for (let level = 0; level < height; level++) { nodes[level] = []; } let firstLeaf = IndexedMerkleMapBase._firstLeaf; let firstNode = Leaf.hashNode(firstLeaf).toBigInt(); let root = Nodes.setLeaf(nodes, 0, firstNode); this.root = Field(root); this.length = Field(1); this.data = Unconstrained.from({ nodes, sortedLeaves: [firstLeaf] }); } static _firstLeaf = { key: 0n, value: 0n, // the 0 key encodes the minimum and maximum at the same time // so, if a second node is inserted, it will get `nextKey = 0`, and thus point to the first node nextKey: 0n, index: 0, }; /** * Clone the entire Merkle map. * * This method is provable. */ clone() { let cloned = new (this.constructor as typeof IndexedMerkleMapBase)(); cloned.root = this.root; cloned.length = this.length; cloned.data.updateAsProver(() => { let { nodes, sortedLeaves } = this.data.get(); return { nodes: nodes.map((row) => [...row]), sortedLeaves: [...sortedLeaves], }; }); return cloned; } /** * Overwrite the entire Merkle map with another one. * * This method is provable. */ overwrite(other: IndexedMerkleMapBase) { this.overwriteIf(true, other); } /** * Overwrite the entire Merkle map with another one, if the condition is true. * * This method is provable. */ overwriteIf(condition: Bool | boolean, other: IndexedMerkleMapBase) { condition = Bool(condition); this.root = Provable.if(condition, other.root, this.root); this.length = Provable.if(condition, other.length, this.length); this.data.updateAsProver(() => Bool(condition).toBoolean() ? other.clone().data.get() : this.data.get() ); } /** * Insert a new leaf `(key, value)`. * * Proves that `key` doesn't exist yet. */ insert(key: Field | bigint, value: Field | bigint) { key = Field(key); value = Field(value); // check that we can insert a new leaf, by asserting the length fits in the tree let index = this.length; let indexBits = index.toBits(this.height - 1); // prove that the key doesn't exist yet by presenting a valid low node let low = Provable.witness(Leaf, () => this._findLeaf(key).low); let lowPath = this._proveInclusion(low, 'Invalid low node (root)'); // if the key does exist, we have lowNode.nextKey == key, and this line fails assertStrictlyBetween(low.key, key, low.nextKey, 'Key already exists in the tree'); // at this point, we know that we have a valid insertion; so we can mutate internal data // update low node let newLow = { ...low, nextKey: key }; this.root = this._proveUpdate(newLow, lowPath); this._setLeafUnconstrained(true, newLow); // create new leaf to append let leaf = Leaf.nextAfter(newLow, index, { key, value, nextKey: low.nextKey, }); // prove empty slot in the tree, and insert our leaf let path = this._proveEmpty(indexBits); this.root = this._proveUpdate(leaf, path); this.length = this.length.add(1); this._setLeafUnconstrained(false, leaf); } /** * Update an existing leaf `(key, value)`. * * Proves that the `key` exists. * * Returns the previous value. */ update(key: Field | bigint, value: Field | bigint): Field { key = Field(key); value = Field(value); // prove that the key exists by presenting a leaf that contains it let self = Provable.witness(Leaf, () => this._findLeaf(key).self); let path = this._proveInclusion(self, 'Key does not exist in the tree'); self.key.assertEquals(key, 'Invalid leaf (key)'); // at this point, we know that we have a valid update; so we can mutate internal data // update leaf let newSelf = { ...self, value }; this.root = this._proveUpdate(newSelf, path); this._setLeafUnconstrained(true, newSelf); return self.value; } /** * 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> { key = Field(key); value = Field(value); // prove whether the key exists or not, by showing a valid low node let { low, self } = Provable.witness(LeafPair, () => this._findLeaf(key)); let lowPath = this._proveInclusion(low, 'Invalid low node (root)'); assertBetween(low.key, key, low.nextKey, 'Invalid low node (key)'); // the key exists iff lowNode.nextKey == key let keyExists = low.nextKey.equals(key); // the leaf's index depends on whether it exists let index = Provable.witness(Field, () => self.index.get()); index = Provable.if(keyExists, index, this.length); let indexBits = index.toBits(this.height - 1); // at this point, we know that we have a valid update or insertion; so we can mutate internal data // update low node, or leave it as is let newLow = { ...low, nextKey: key }; this.root = this._proveUpdate(newLow, lowPath); this._setLeafUnconstrained(true, newLow); // prove inclusion of this leaf if it exists let path = this._proveInclusionOrEmpty(keyExists, indexBits, self, 'Invalid leaf (root)'); assert(keyExists.implies(self.key.equals(key)), 'Invalid leaf (key)'); // update leaf, or append a new one let newLeaf = Leaf.nextAfter(newLow, index, { key, value, nextKey: Provable.if(keyExists, self.nextKey, low.nextKey), }); this.root = this._proveUpdate(newLeaf, path); this.length = Provable.if(keyExists, this.length, this.length.add(1)); this._setLeafUnconstrained(keyExists, newLeaf); // return the previous value return new OptionField({ isSome: keyExists, value: self.value }); } /** * 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) { return this.set( Provable.if(Bool(condition), Field(key), Field(0n)), Provable.if(Bool(condition), Field(value), Field(0n)) ); } /** * Get a value from a key. * * Proves that the key already exists in the map yet and fails otherwise. */ get(key: Field | bigint): Field { key = Field(key); // prove that the key exists by presenting a leaf that contains it let self = Provable.witness(Leaf, () => this._findLeaf(key).self); this._proveInclusion(self, 'Key does not exist in the tree'); self.key.assertEquals(key, 'Invalid leaf (key)'); return self.value; } /** * 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> { key = Field(key); // prove whether the key exists or not, by showing a valid low node let { low, self } = Provable.witness(LeafPair, () => this._findLeaf(key)); this._proveInclusion(low, 'Invalid low node (root)'); assertBetween(low.key, key, low.nextKey, 'Invalid low node (key)'); // the key exists iff lowNode.nextKey == key let keyExists = low.nextKey.equals(key); // prove inclusion of this leaf if it exists this._proveInclusionIf(keyExists, self, 'Invalid leaf (root)'); assert(keyExists.implies(self.key.equals(key)), 'Invalid leaf (key)'); return new OptionField({ isSome: keyExists, value: self.value }); } // methods to check for inclusion for a key without being concerned about the value /** * Prove that the given key exists in the map. */ assertIncluded(key: Field | bigint, message?: string) { key = Field(key); // prove that the key exists by presenting a leaf that contains it let self = Provable.witness(Leaf, () => this._findLeaf(key).self); this._proveInclusion(self, message ?? 'Key does not exist in the tree'); self.key.assertEquals(key, 'Invalid leaf (key)'); } /** * Prove that the given key does not exist in the map. */ assertNotIncluded(key: Field | bigint, message?: string) { key = Field(key); // prove that the key does not exist yet, by showing a valid low node let low = Provable.witness(Leaf, () => this._findLeaf(key).low); this._proveInclusion(low, 'Invalid low node (root)'); assertStrictlyBetween(low.key, key, low.nextKey, message ?? 'Key already exists in the tree'); } /** * Check whether the given key exists in the map. */ isIncluded(key: Field | bigint): Bool { key = Field(key); // prove that the key does not exist yet, by showing a valid low node let low = Provable.witness(Leaf, () => this._findLeaf(key).low); this._proveInclusion(low, 'Invalid low node (root)'); assertBetween(low.key, key, low.nextKey, 'Invalid low node (key)'); return low.nextKey.equals(key); } // helper methods /** * Helper method to prove inclusion of a leaf in the tree. */ _proveInclusion(leaf: Leaf, message?: string) { let node = Leaf.hashNode(leaf); // here, we don't care at which index the leaf is included, so we pass it in as unconstrained let { root, path } = this._computeRoot(node, leaf.index); root.assertEquals(this.root, message ?? 'Leaf is not included in the tree'); return path; } /** * Helper method to conditionally prove inclusion of a leaf in the tree. */ _proveInclusionIf(condition: Bool, leaf: Leaf, message?: string) { let node = Leaf.hashNode(leaf); // here, we don't care at which index the leaf is included, so we pass it in as unconstrained let { root } = this._computeRoot(node, leaf.index); assert( condition.implies(root.equals(this.root)), message ?? 'Leaf is not included in the tree' ); } /** * Helper method to prove inclusion of an empty leaf in the tree. * * This validates the path against the current root, so that we can use it to insert a new leaf. */ _proveEmpty(index: Bool[]) { let node = Field(0n); let { root, path } = this._computeRoot(node, index); root.assertEquals(this.root, 'Leaf is not empty'); return path; } /** * Helper method to conditionally prove inclusion of a leaf in the tree. * * If the condition is false, we prove that the tree contains an empty leaf instead. */ _proveInclusionOrEmpty(condition: Bool, index: Bool[], leaf: BaseLeaf, message?: string) { let node = Provable.if(condition, Leaf.hashNode(leaf), Field(0n)); let { root, path } = this._computeRoot(node, index); root.assertEquals(this.root, message ?? 'Leaf is not included in the tree'); return path; } /** * Helper method to update the root against a previously validated path. * * Returns the new root. */ _proveUpdate(leaf: BaseLeaf, path: { index: Bool[]; witness: Field[] }) { let node = Leaf.hashNode(leaf); let { root } = this._computeRoot(node, path.index, path.witness); return root; } /** * Helper method to compute the root given a leaf node and its index. * * The index can be given as a `Field` or as an array of bits. */ _computeRoot(node: Field, index: Unconstrained<number> | Bool[], witness?: Field[]) { // if the index was passed in as unconstrained, we witness its bits here let indexBits = index instanceof Unconstrained ? Provable.witness(Provable.Array(Bool, this.height - 1), () => Field(index.get()).toBits(this.height - 1) ) : index; // if the witness was not passed in, we create it here let witness_ = witness ?? Provable.witnessFields(this.height - 1, () => { let witness: bigint[] = []; let index = Number(Field.fromBits(indexBits)); let { nodes } = this.data.get(); for (let level = 0; level < this.height - 1; level++) { let i = index % 2 === 0 ? index + 1 : index - 1; let sibling = Nodes.getNode(nodes, level, i, false); witness.push(sibling); index >>= 1; } return witness; }); assert(indexBits.length === this.height - 1, 'Invalid index size'); assert(witness_.length === this.height - 1, 'Invalid witness size'); for (let level = 0; level < this.height - 1; level++) { let isRight = indexBits[level]; let sibling = witness_[level]; let [right, left] = conditionalSwap(isRight, node, sibling); node = Poseidon.hash([left, right]); } // now, `node` is the root of the tree return { root: node, path: { witness: witness_, index: indexBits } }; } /** * Given a key, returns both the low node and the leaf that contains the key. * * If the key does not exist, a dummy value is returned for the leaf. * * Can only be called outside provable code. */ _findLeaf(key_: Field | bigint): InferValue<typeof LeafPair> { let key = typeof key_ === 'bigint' ? key_ : key_.toBigInt(); assert(key >= 0n, 'key must be positive'); let leaves = this.data.get().sortedLeaves; // this case is typically invalid, but we want to handle it gracefully here // and reject it using comparison constraints if (key === 0n) return { low: Leaf.fromStored(leaves[leaves.length - 1], leaves.length - 1), self: Leaf.fromStored(leaves[0], 0), }; let { lowIndex, foundValue } = bisectUnique(key, (i) => leaves[i].key, leaves.length); let iLow = foundValue ? lowIndex - 1 : lowIndex; let low = Leaf.fromStored(leaves[iLow], iLow); let iSelf = foundValue ? lowIndex : 0; let selfBase = foundValue ? leaves[lowIndex] : Leaf.toStored(Leaf.empty()); let self = Leaf.fromStored(selfBase, iSelf); return { low, self }; } /** * Update or append a leaf in our internal data structures */ _setLeafUnconstrained(leafExists: Bool | boolean, leaf: Leaf) { Provable.asProver(() => { let { nodes, sortedLeaves } = this.data.get(); // update internal hash nodes let i = leaf.index.get(); Nodes.setLeaf(nodes, i, Leaf.hashNode(leaf).toBigInt()); // update sorted list let leafValue = Leaf.toStored(leaf); let iSorted = leaf.sortedIndex.get(); if (Bool(leafExists).toBoolean()) { // for key=0, the sorted index overflows the length because we compute it as low.sortedIndex + 1 // in that case, it should wrap back to 0 sortedLeaves[iSorted % sortedLeaves.length] = leafValue; } else { sortedLeaves.splice(iSorted, 0, leafValue); } }); } } // helpers for updating nodes type Nodes = (bigint | undefined)[][]; namespace Nodes { /** * Sets the leaf node at the given index, updates all parent nodes and returns the new root. */ export function setLeaf(nodes: Nodes, index: number, leaf: bigint) { nodes[0][index] = leaf; let height = nodes.length; for (let level = 0; level < height - 1; level++) { let isLeft = index % 2 === 0; index = Math.floor(index / 2); let left = getNode(nodes, level, index * 2, isLeft); let right = getNode(nodes, level, index * 2 + 1, !isLeft); nodes[level + 1][index] = PoseidonBigint.hash([left, right]); } return getNode(nodes, height - 1, 0, true); } export function getNode( nodes: Nodes, level: number, index: number, // whether the node is required to be non-empty nonEmpty: boolean ) { let node = nodes[level]?.[index]; if (node === undefined) { if (nonEmpty) throw Error(`node at level=${level}, index=${index} was expected to be known, but isn't.`); node = empty(level); } return node; } // cache of empty nodes (=: zero leaves and nodes with only empty nodes below them) const emptyNodes = [0n]; export function empty(level: number) { for (let i = emptyNodes.length; i <= level; i++) { let zero = emptyNodes[i - 1]; emptyNodes[i] = PoseidonBigint.hash([zero, zero]); } return emptyNodes[level]; } } // leaf class BaseLeaf extends Struct({ key: Field, value: Field, nextKey: Field, }) {} class Leaf extends Struct({ value: Field, key: Field, nextKey: Field, // auxiliary data that tells us where the leaf is stored index: Unconstrained.withEmpty(0), sortedIndex: Unconstrained.withEmpty(0), }) { /** * Compute a leaf node: the hash of a leaf that becomes part of the Merkle tree. */ static hashNode(leaf: From<typeof BaseLeaf>) { // note: we don't have to include the index in the leaf hash, // because computing the root already commits to the index return Poseidon.hashPacked(BaseLeaf, BaseLeaf.fromValue(leaf)); } /** * Create a new leaf, given its low node and index. */ static nextAfter(low: Leaf, index: Field, leaf: BaseLeaf): Leaf { return { key: leaf.key, value: leaf.value, nextKey: leaf.nextKey, index: Unconstrained.witness(() => Number(index)), sortedIndex: Unconstrained.witness(() => low.sortedIndex.get() + 1), }; } // convert to/from internally stored format static toStored(leaf: Leaf): StoredLeaf { return { key: leaf.key.toBigInt(), value: leaf.value.toBigInt(), nextKey: leaf.nextKey.toBigInt(), index: leaf.index.get(), }; } static fromStored(leaf: StoredLeaf, sortedIndex: number) { return { ...leaf, sortedIndex }; } } type StoredLeaf = { readonly value: bigint; readonly key: bigint; readonly nextKey: bigint; readonly index: number; }; class LeafPair extends Struct({ low: Leaf, self: Leaf }) {} class OptionField extends Option(Field) {} // helper /** * Bisect indices in an array of unique values that is sorted in ascending order. * * `getValue()` returns the value at the given index. * * We return * - `lowIndex := max { i in [0, length) | getValue(i) <= target }` * - `foundValue` := whether `getValue(lowIndex) == target` */ function bisectUnique( target: bigint, getValue: (index: number) => bigint, length: number ): { lowIndex: number; foundValue: boolean; } { let [iLow, iHigh] = [0, length - 1]; // handle out of bounds if (getValue(iLow) > target) return { lowIndex: -1, foundValue: false }; if (getValue(iHigh) < target) return { lowIndex: iHigh, foundValue: false }; // invariant: 0 <= iLow <= lowIndex <= iHigh < length // since we are decreasing (iHigh - iLow) in every iteration, we'll terminate while (iHigh !== iLow) { // we have iLow < iMid <= iHigh // in both branches, the range gets strictly smaller let iMid = Math.ceil((iLow + iHigh) / 2); if (getValue(iMid) <= target) { // iMid is in the candidate set, and strictly larger than iLow // preserves iLow <= lowIndex iLow = iMid; } else { // iMid is no longer in the candidate set, so we can exclude it right away // preserves lowIndex <= iHigh iHigh = iMid - 1; } } return { lowIndex: iLow, foundValue: getValue(iLow) === target }; } // custom comparison methods where 0 can act as the min and max value simultaneously /** * Assert that `x in (low, high)`, i.e. low < x < high, with the following exceptions: * * - high=0 is treated as the maximum value, so `x in (low, 0)` always succeeds if only low < x; except for x = 0. * - x=0 is also treated as the maximum value, so `0 in (low, high)` always fails, because x >= high. */ function assertStrictlyBetween(low: Field, x: Field, high: Field, message?: string) { // exclude x=0 x.assertNotEquals(0n, message ?? '0 is not in any strict range'); // normal assertion for low < x low.assertLessThan(x, message); // for x < high, use a safe comparison that also works if high=0 let highIsZero = high.equals(0n); let xSafe = Provable.witness(Field, () => (highIsZero.toBoolean() ? 0n : x)); let highSafe = Provable.witness(Field, () => (highIsZero.toBoolean() ? 1n : high)); xSafe.assertLessThan(highSafe, message); assert(xSafe.equals(x).or(highIsZero), message); assert(highSafe.equals(high).or(highIsZero), message); } /** * Assert that `x in (low, high]`, i.e. low < x <= high, with the following exceptions: * * - high=0 is treated as the maximum value, so `x in (low, 0]` always succeeds if only low < x. * - x=0 is also treated as the maximum value, so `0 in (low, high]` fails except if high=0. * - note: `0 in (n, 0]` succeeds for any n! */ function assertBetween(low: Field, x: Field, high: Field, message?: string) { // for low < x, we need to handle the x=0 case separately let xIsZero = x.equals(0n); let lowSafe = Provable.witness(Field, () => (xIsZero.toBoolean() ? 0n : low)); let xSafe1 = Provable.witness(Field, () => (xIsZero.toBoolean() ? 1n : x)); lowSafe.assertLessThan(xSafe1, message); assert(lowSafe.equals(low).or(xIsZero), message); assert(xSafe1.equals(x).or(xIsZero), message); // for x <= high, we need to handle the high=0 case separately let highIsZero = high.equals(0n); let xSafe0 = Provable.witness(Field, () => (highIsZero.toBoolean() ? 0n : x)); xSafe0.assertLessThanOrEqual(high, message); assert(xSafe0.equals(x).or(highIsZero), message); }