o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
536 lines • 22 kB
JavaScript
import { Field } from './wrapped.js';
import { Provable } from './provable.js';
import { Struct } from './types/struct.js';
import { assert } from './gadgets/common.js';
import { provableFromClass } from './types/provable-derivers.js';
import { Poseidon, packToFields } from './crypto/poseidon.js';
import { Unconstrained } from './types/unconstrained.js';
import { ProvableType } from './types/provable-intf.js';
import { Option } from './option.js';
export { MerkleListBase, MerkleList, MerkleListIterator, WithHash, emptyHash, genericHash, merkleListHash, withHashes, };
// common base types for both MerkleList and MerkleListIterator
const emptyHash = Field(0);
function WithHash(type) {
return Struct({ previousHash: Field, element: type });
}
function toConstant(type, node) {
return {
previousHash: node.previousHash.toConstant(),
element: Provable.toConstant(type, node.element),
};
}
function MerkleListBase() {
return class extends Struct({ hash: Field, data: Unconstrained }) {
static empty() {
return { hash: emptyHash, data: Unconstrained.from([]) };
}
};
}
// merkle list
/**
* Dynamic-length list which is represented as a single hash
*
* Supported operations are {@link push()} and {@link pop()} and some variants thereof.
*
*
* A Merkle list is generic over its element types, so before using it you must create a subclass for your element type:
*
* ```ts
* class MyList extends MerkleList.create(MyType) {}
*
* // now use it
* let list = MyList.empty();
*
* list.push(new MyType(...));
*
* let element = list.pop();
* ```
*
* Internal detail: `push()` adds elements to the _start_ of the internal array and `pop()` removes them from the start.
* This is so that the hash which represents the list is consistent with {@link MerkleListIterator},
* and so a `MerkleList` can be used as input to `MerkleListIterator.startIterating(list)`
* (which will then iterate starting from the last pushed element).
*/
class MerkleList {
constructor({ hash, data }) {
this.hash = hash;
this.data = data;
}
isEmpty() {
return this.hash.equals(this.Constructor.emptyHash);
}
/**
* Push a new element to the list.
*/
push(element) {
let previousHash = this.hash;
this.hash = this.nextHash(previousHash, element);
this.data.updateAsProver((data) => [
toConstant(this.innerProvable, { previousHash, element }),
...data,
]);
}
/**
* Push a new element to the list, if the `condition` is true.
*/
pushIf(condition, element) {
let previousHash = this.hash;
this.hash = Provable.if(condition, this.nextHash(previousHash, element), previousHash);
this.data.updateAsProver((data) => condition.toBoolean()
? [toConstant(this.innerProvable, { previousHash, element }), ...data]
: data);
}
popWitness() {
return Provable.witness(WithHash(this.innerProvable), () => {
let [value, ...data] = this.data.get();
let head = value ?? {
previousHash: this.Constructor.emptyHash,
element: this.innerProvable.empty(),
};
this.data.set(data);
return head;
});
}
/**
* Remove the last element from the list and return it.
*
* This proves that the list is non-empty, and fails otherwise.
*/
popExn() {
let { previousHash, element } = this.popWitness();
let currentHash = this.nextHash(previousHash, element);
this.hash.assertEquals(currentHash);
this.hash = previousHash;
return element;
}
/**
* Remove the last element from the list and return it.
*
* If the list is empty, returns a dummy element.
*/
pop() {
let { previousHash, element } = this.popWitness();
let isEmpty = this.isEmpty();
let emptyHash = this.Constructor.emptyHash;
let currentHash = this.nextHash(previousHash, element);
currentHash = Provable.if(isEmpty, emptyHash, currentHash);
this.hash.assertEquals(currentHash);
this.hash = Provable.if(isEmpty, emptyHash, previousHash);
let provable = this.innerProvable;
return Provable.if(isEmpty, provable, provable.empty(), element);
}
/**
* Remove the last element from the list and return it as an option:
* Some(element) if the list is non-empty, None if the list is empty.
*
* **Warning**: If the list is empty, the the option's .value is entirely unconstrained.
*/
popOption() {
let { previousHash, element } = this.popWitness();
let isEmpty = this.isEmpty();
let emptyHash = this.Constructor.emptyHash;
let currentHash = this.nextHash(previousHash, element);
currentHash = Provable.if(isEmpty, emptyHash, currentHash);
this.hash.assertEquals(currentHash);
this.hash = Provable.if(isEmpty, emptyHash, previousHash);
let provable = this.innerProvable;
const OptionT = Option(provable);
return new OptionT({ isSome: isEmpty.not(), value: element });
}
/**
* Return the last element, but only remove it if `condition` is true.
*
* If the list is empty, returns a dummy element.
*/
popIf(condition) {
let originalHash = this.hash;
let element = this.pop();
// if the condition is false, we restore the original state
this.data.updateAsProver((data) => {
let node = { previousHash: this.hash, element };
return condition.toBoolean() ? data : [toConstant(this.innerProvable, node), ...data];
});
this.hash = Provable.if(condition, this.hash, originalHash);
return element;
}
/**
* Low-level, minimal version of `pop()` which lets the _caller_ decide whether there is an element to pop.
*
* I.e. this proves:
* - If the input condition is true, this returns the last element and removes it from the list.
* - If the input condition is false, the list is unchanged and the return value is garbage.
*
* Note that if the caller passes `true` but the list is empty, this will fail.
* If the caller passes `false` but the list is non-empty, this succeeds and just doesn't pop off an element.
*/
popIfUnsafe(shouldPop) {
let { previousHash, element } = Provable.witness(WithHash(this.innerProvable), () => {
let dummy = {
previousHash: this.hash,
element: this.innerProvable.empty(),
};
if (!shouldPop.toBoolean())
return dummy;
let [value, ...data] = this.data.get();
this.data.set(data);
return value ?? dummy;
});
let nextHash = this.nextHash(previousHash, element);
let currentHash = Provable.if(shouldPop, nextHash, this.hash);
this.hash.assertEquals(currentHash);
this.hash = Provable.if(shouldPop, previousHash, this.hash);
return element;
}
clone() {
let data = Unconstrained.witness(() => [...this.data.get()]);
return new this.Constructor({ hash: this.hash, data });
}
/**
* Iterate through the list in a fixed number of steps any apply a given callback on each element.
*
* Proves that the iteration traverses the entire list.
* Once past the last element, dummy elements will be passed to the callback.
*
* Note: There are no guarantees about the contents of dummy elements, so the callback is expected
* to handle the `isDummy` flag separately.
*/
forEach(length, callback) {
let iter = this.startIterating();
for (let i = 0; i < length; i++) {
let { element, isDummy } = iter.Unsafe.next();
callback(element, isDummy, i);
}
iter.assertAtEnd(`Expected MerkleList to have at most ${length} elements, but it has more.`);
}
startIterating() {
let merkleArray = MerkleListIterator.createFromList(this.Constructor);
return merkleArray.startIterating(this);
}
startIteratingFromLast() {
let merkleArray = MerkleListIterator.createFromList(this.Constructor);
return merkleArray.startIteratingFromLast(this);
}
toArrayUnconstrained() {
return Unconstrained.witness(() => [...this.data.get()].reverse().map((x) => x.element));
}
lengthUnconstrained() {
return Unconstrained.witness(() => this.data.get().length);
}
/**
* Create a Merkle list type
*
* Optionally, you can tell `create()` how to do the hash that pushes a new list element, by passing a `nextHash` function.
*
* @example
* ```ts
* class MyList extends MerkleList.create(Field, (hash, x) =>
* Poseidon.hashWithPrefix('custom', [hash, x])
* ) {}
* ```
*/
static create(type, nextHash = merkleListHash(ProvableType.get(type)), emptyHash_ = emptyHash) {
let provable = ProvableType.get(type);
class MerkleListTBase extends MerkleList {
static empty() {
return new this({ hash: emptyHash_, data: Unconstrained.from([]) });
}
static from(array) {
array = [...array].reverse();
let { hash, data } = withHashes(array, nextHash, emptyHash_);
let unconstrained = Unconstrained.witness(() => data.map((x) => toConstant(provable, x)));
return new this({ data: unconstrained, hash });
}
static fromReverse(array) {
let { hash, data } = withHashes(array, nextHash, emptyHash_);
let unconstrained = Unconstrained.witness(() => data.map((x) => toConstant(provable, x)));
return new this({ data: unconstrained, hash });
}
static get provable() {
assert(this._provable !== undefined, 'MerkleList not initialized');
return this._provable;
}
static set provable(_provable) {
this._provable = _provable;
}
}
MerkleListTBase._innerProvable = provable;
MerkleListTBase._provable = provableFromClass(MerkleListTBase, {
hash: Field,
data: Unconstrained,
});
MerkleListTBase._nextHash = nextHash;
MerkleListTBase._emptyHash = emptyHash_;
// override `instanceof` for subclasses
return class MerkleListT extends MerkleListTBase {
static [Symbol.hasInstance](x) {
return x instanceof MerkleListTBase;
}
};
}
get Constructor() {
return this.constructor;
}
nextHash(hash, value) {
assert(this.Constructor._nextHash !== undefined, 'MerkleList not initialized');
return this.Constructor._nextHash(hash, value);
}
static get emptyHash() {
assert(this._emptyHash !== undefined, 'MerkleList not initialized');
return this._emptyHash;
}
get innerProvable() {
assert(this.Constructor._innerProvable !== undefined, 'MerkleList not initialized');
return this.Constructor._innerProvable;
}
}
/**
* MerkleListIterator helps iterating through a Merkle list.
* This works similar to calling `list.pop()` or `list.push()` repeatedly, but maintaining the entire list instead of removing elements.
*
* The core methods that support iteration are {@link next()} and {@link previous()}.
*
* ```ts
* let iterator = MerkleListIterator.startIterating(list);
*
* let firstElement = iterator.next();
* ```
*
* We maintain two commitments:
* - One to the entire array, to be able to prove that we end iteration at the correct point.
* - One to the array from the current index until the end, to efficiently step forward.
*/
class MerkleListIterator {
constructor(value) {
Object.assign(this, value);
}
assertAtStart() {
return this.currentHash.assertEquals(this.Constructor.emptyHash);
}
isAtEnd() {
return this.currentHash.equals(this.hash);
}
jumpToEnd() {
this.currentIndex.setTo(Unconstrained.witness(() => 0));
this.currentHash = this.hash;
}
jumpToEndIf(condition) {
Provable.asProver(() => {
if (condition.toBoolean()) {
this.currentIndex.set(0);
}
});
this.currentHash = Provable.if(condition, this.hash, this.currentHash);
}
assertAtEnd(message) {
return this.currentHash.assertEquals(this.hash, message ?? 'Merkle list iterator is not at the end');
}
isAtStart() {
return this.currentHash.equals(this.Constructor.emptyHash);
}
jumpToStart() {
this.currentIndex.setTo(Unconstrained.witness(() => this.data.get().length));
this.currentHash = this.Constructor.emptyHash;
}
jumpToStartIf(condition) {
Provable.asProver(() => {
if (condition.toBoolean()) {
this.currentIndex.set(this.data.get().length);
}
});
this.currentHash = Provable.if(condition, this.Constructor.emptyHash, this.currentHash);
}
_index(direction, i) {
i ??= this.currentIndex.get();
if (direction === 'next') {
return Math.min(Math.max(i, -1), this.data.get().length - 1);
}
else {
return Math.max(Math.min(i, this.data.get().length), 0);
}
}
_updateIndex(direction) {
this.currentIndex.updateAsProver(() => {
let i = this._index(direction);
return this._index(direction, direction === 'next' ? i - 1 : i + 1);
});
}
previous() {
// `previous()` corresponds to `pop()` in MerkleList
// it returns a dummy element if we're at the start of the array
let { previousHash, element } = Provable.witness(WithHash(this.innerProvable), () => this.data.get()[this._index('previous')] ?? {
previousHash: this.Constructor.emptyHash,
element: this.innerProvable.empty(),
});
let isDummy = this.isAtStart();
let emptyHash = this.Constructor.emptyHash;
let correctHash = this.nextHash(previousHash, element);
let requiredHash = Provable.if(isDummy, emptyHash, correctHash);
this.currentHash.assertEquals(requiredHash);
this._updateIndex('previous');
this.currentHash = Provable.if(isDummy, emptyHash, previousHash);
return Provable.if(isDummy, this.innerProvable, this.innerProvable.empty(), element);
}
next() {
// instead of starting from index `0`, we start at index `length - 1` and go in reverse
// this is like MerkleList.push() but we witness the next element instead of taking it as input,
// and we return a dummy element if we're at the end of the array
let element = Provable.witness(this.innerProvable, () => this.data.get()[this._index('next')]?.element ?? this.innerProvable.empty());
let isDummy = this.isAtEnd();
let currentHash = this.nextHash(this.currentHash, element);
this.currentHash = Provable.if(isDummy, this.hash, currentHash);
this._updateIndex('next');
return Provable.if(isDummy, this.innerProvable, this.innerProvable.empty(), element);
}
/**
* Low-level APIs for advanced uses
*/
get Unsafe() {
let self = this;
return {
/**
* Version of {@link previous} which doesn't guarantee anything about
* the returned element in case the iterator is at the start.
*
* Instead, the `isDummy` flag is also returned so that this case can
* be handled in a custom way.
*/
previous() {
let { previousHash, element } = Provable.witness(WithHash(self.innerProvable), () => self.data.get()[self._index('previous')] ?? {
previousHash: self.Constructor.emptyHash,
element: self.innerProvable.empty(),
});
let isDummy = self.isAtStart();
let emptyHash = self.Constructor.emptyHash;
let correctHash = self.nextHash(previousHash, element);
let requiredHash = Provable.if(isDummy, emptyHash, correctHash);
self.currentHash.assertEquals(requiredHash);
self._updateIndex('previous');
self.currentHash = Provable.if(isDummy, emptyHash, previousHash);
return { element, isDummy };
},
/**
* Version of {@link next} which doesn't guarantee anything about
* the returned element in case the iterator is at the end.
*
* Instead, the `isDummy` flag is also returned so that this case can
* be handled in a custom way.
*/
next() {
let element = Provable.witness(self.innerProvable, () => {
return self.data.get()[self._index('next')]?.element ?? self.innerProvable.empty();
});
let isDummy = self.isAtEnd();
let currentHash = self.nextHash(self.currentHash, element);
self.currentHash = Provable.if(isDummy, self.hash, currentHash);
self._updateIndex('next');
return { element, isDummy };
},
};
}
clone() {
let data = Unconstrained.witness(() => [...this.data.get()]);
let currentIndex = Unconstrained.witness(() => this.currentIndex.get());
return new this.Constructor({
data,
hash: this.hash,
currentHash: this.currentHash,
currentIndex,
});
}
/**
* Create a Merkle array type
*/
static create(type, nextHash = merkleListHash(ProvableType.get(type)), emptyHash_ = emptyHash) {
var _a;
let provable = ProvableType.get(type);
return _a = class Iterator extends MerkleListIterator {
static from(array) {
let { hash, data } = withHashes(array, nextHash, emptyHash_);
let unconstrained = Unconstrained.witness(() => data.map((x) => toConstant(provable, x)));
return this.startIterating({ data: unconstrained, hash });
}
static fromLast(array) {
array = [...array].reverse();
let { hash, data } = withHashes(array, nextHash, emptyHash_);
let unconstrained = Unconstrained.witness(() => data.map((x) => toConstant(provable, x)));
return this.startIteratingFromLast({ data: unconstrained, hash });
}
static startIterating({ data, hash }) {
return new this({
data,
hash,
currentHash: emptyHash_,
// note: for an empty list or any list which is "at the end", the currentIndex is -1
currentIndex: Unconstrained.witness(() => data.get().length - 1),
});
}
static startIteratingFromLast({ data, hash }) {
return new this({
data,
hash,
currentHash: hash,
currentIndex: Unconstrained.from(0),
});
}
static empty() {
return this.from([]);
}
static get provable() {
assert(this._provable !== undefined, 'MerkleListIterator not initialized');
return this._provable;
}
},
_a._innerProvable = ProvableType.get(provable),
_a._provable = provableFromClass(_a, {
hash: Field,
data: Unconstrained,
currentHash: Field,
currentIndex: Unconstrained,
}),
_a._nextHash = nextHash,
_a._emptyHash = emptyHash_,
_a;
}
static createFromList(merkleList) {
return this.create(merkleList.prototype.innerProvable, merkleList._nextHash, merkleList.emptyHash);
}
get Constructor() {
return this.constructor;
}
nextHash(hash, value) {
assert(this.Constructor._nextHash !== undefined, 'MerkleListIterator not initialized');
return this.Constructor._nextHash(hash, value);
}
static get emptyHash() {
assert(this._emptyHash !== undefined, 'MerkleList not initialized');
return this._emptyHash;
}
get innerProvable() {
assert(this.Constructor._innerProvable !== undefined, 'MerkleListIterator not initialized');
return this.Constructor._innerProvable;
}
}
// hash helpers
function genericHash(provable, prefix, value) {
let input = provable.toInput(value);
let packed = packToFields(input);
return Poseidon.hashWithPrefix(prefix, packed);
}
function merkleListHash(provable, prefix = '') {
return function nextHash(hash, value) {
let input = provable.toInput(value);
let packed = packToFields(input);
return Poseidon.hashWithPrefix(prefix, [hash, ...packed]);
};
}
function withHashes(data, nextHash, emptyHash) {
let n = data.length;
let arrayWithHashes = Array(n);
let currentHash = emptyHash;
for (let i = n - 1; i >= 0; i--) {
arrayWithHashes[i] = { previousHash: currentHash, element: data[i] };
currentHash = nextHash(currentHash, data[i]);
}
return { data: arrayWithHashes, hash: currentHash };
}
//# sourceMappingURL=merkle-list.js.map