o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
286 lines • 10.9 kB
JavaScript
import { Field, Bool, Scalar, Group } from '../wrapped.js';
import { provable, provableTuple, HashInput } from './provable-derivers.js';
import { DynamicProof, Proof } from '../../proof-system/proof.js';
// external API
export { Struct, };
// internal API
export { provableTuple, cloneCircuitValue, circuitValueEquals, HashInput, StructNoJson, };
/**
* `Struct` lets you declare composite types for use in o1js circuits.
*
* These composite types can be passed in as arguments to smart contract methods, used for on-chain state variables
* or as event / action types.
*
* @example
* Here's an example of creating a "Voter" struct, which holds a public key and a collection of votes on 3 different proposals:
* ```ts
* let Vote = { hasVoted: Bool, inFavor: Bool };
*
* class Voter extends Struct({
* publicKey: PublicKey,
* votes: [Vote, Vote, Vote]
* }) {}
*
* // use Voter as SmartContract input:
* class VoterContract extends SmartContract {
* \@method register(voter: Voter) {
* // ...
* }
* }
* ```
* In this example, there are no instance methods on the class. This makes `Voter` type-compatible with an anonymous object of the form
* `{ publicKey: PublicKey, votes: Vote[] }`.
* This mean you don't have to create instances by using `new Voter(...)`, you can operate with plain objects:
* ```ts
* voterContract.register({ publicKey, votes });
* ```
*
* On the other hand, you can also add your own methods:
* ```ts
* class Voter extends Struct({
* publicKey: PublicKey,
* votes: [Vote, Vote, Vote]
* }) {
* vote(index: number, inFavor: Bool) {
* let vote = this.votes[i];
* vote.hasVoted = Bool(true);
* vote.inFavor = inFavor;
* }
* }
* ```
*
* In this case, you'll need the constructor to create instances of `Voter`. It always takes as input the plain object:
* ```ts
* let emptyVote = { hasVoted: Bool(false), inFavor: Bool(false) };
* let voter = new Voter({ publicKey, votes: Array(3).fill(emptyVote) });
* voter.vote(1, Bool(true));
* ```
*
* In addition to creating types composed of Field elements, you can also include auxiliary data which does not become part of the proof.
* This, for example, allows you to reuse the same type outside o1js methods, where you might want to store additional metadata.
*
* To declare non-proof values of type `string`, `number`, etc, you can use the built-in objects `String`, `Number`, etc.
* Here's how we could add the voter's name (a string) as auxiliary data:
* ```ts
* class Voter extends Struct({
* publicKey: PublicKey,
* votes: [Vote, Vote, Vote],
* fullName: String
* }) {}
* ```
*
* Again, it's important to note that this doesn't enable you to prove anything about the `fullName` string.
* From the circuit point of view, it simply doesn't exist!
*
* **Note**: Ensure you do not use or extend `Struct` as a type directly. Instead, always call it as a function to construct a type. `Struct` is not a valid provable type itself, types created with `Struct(...)` are.
*
* @param type Object specifying the layout of the `Struct`
* @returns Class which you can extend
*/
function Struct(type) {
class Struct_ {
constructor(value) {
Object.assign(this, value);
}
/**
* This method is for internal use, you will probably not need it.
* @returns the size of this struct in field elements
*/
static sizeInFields() {
return this.type.sizeInFields();
}
/**
* This method is for internal use, you will probably not need it.
* @param value
* @returns the raw list of field elements that represent this struct inside the proof
*/
static toFields(value) {
return this.type.toFields(value);
}
/**
* This method is for internal use, you will probably not need it.
* @param value
* @returns the raw non-field element data contained in the struct
*/
static toAuxiliary(value) {
return this.type.toAuxiliary(value);
}
/**
* This method is for internal use, you will probably not need it.
* @param value
* @returns a representation of this struct as field elements, which can be hashed efficiently
*/
static toInput(value) {
return this.type.toInput(value);
}
/**
* Convert this struct to a JSON object, consisting only of numbers, strings, booleans, arrays and plain objects.
* @param value
* @returns a JSON representation of this struct
*/
static toJSON(value) {
return this.type.toJSON(value);
}
/**
* Convert from a JSON object to an instance of this struct.
* @param json
* @returns a JSON representation of this struct
*/
static fromJSON(json) {
let value = this.type.fromJSON(json);
let struct = Object.create(this.prototype);
return Object.assign(struct, value);
}
/**
* Create an instance of this struct filled with default values
* @returns an empty instance of this struct
*/
static empty() {
let value = this.type.empty();
let struct = Object.create(this.prototype);
return Object.assign(struct, value);
}
/**
* This method is for internal use, you will probably not need it.
* Method to make assertions which should be always made whenever a struct of this type is created in a proof.
* @param value
*/
static check(value) {
return this.type.check(value);
}
/**
* `Provable<T>.toCanonical()`
*/
static toCanonical(value) {
let canonical = this.type.toCanonical?.(value) ?? value;
let struct = Object.create(this.prototype);
return Object.assign(struct, canonical);
}
/**
* `Provable<T>.toValue()`
*/
static toValue(x) {
return this.type.toValue(x);
}
/**
* `Provable<T>.fromValue()`
*/
static fromValue(v) {
let value = this.type.fromValue(v);
let struct = Object.create(this.prototype);
return Object.assign(struct, value);
}
/**
* This method is for internal use, you will probably not need it.
* Recover a struct from its raw field elements and auxiliary data.
* @param fields the raw fields elements
* @param aux the raw non-field element data
*/
static fromFields(fields, aux) {
let value = this.type.fromFields(fields, aux);
let struct = Object.create(this.prototype);
return Object.assign(struct, value);
}
}
Struct_.type = provable(type);
Struct_._isStruct = true;
return Struct_;
}
function StructNoJson(type) {
return Struct(type);
}
let primitives = new Set([Field, Bool, Scalar, Group]);
function isPrimitive(obj) {
for (let P of primitives) {
if (obj instanceof P)
return true;
}
return false;
}
function cloneCircuitValue(obj) {
// primitive JS types and functions aren't cloned
if (typeof obj !== 'object' || obj === null)
return obj;
// classes that define clone() are cloned using that method
if (obj.constructor !== undefined && 'clone' in obj.constructor) {
return obj.constructor.clone(obj);
}
if ('clone' in obj && typeof obj.clone === 'function') {
return obj.clone(obj);
}
// built-in JS datatypes with custom cloning strategies
if (Array.isArray(obj))
return obj.map(cloneCircuitValue);
if (obj instanceof Set)
return new Set([...obj].map(cloneCircuitValue));
if (obj instanceof Map)
return new Map([...obj].map(([k, v]) => [k, cloneCircuitValue(v)]));
if (ArrayBuffer.isView(obj))
return new obj.constructor(obj);
// o1js primitives and proofs aren't cloned
if (isPrimitive(obj)) {
return obj;
}
if (obj instanceof Proof || obj instanceof DynamicProof) {
return obj;
}
// cloning strategy that works for plain objects AND classes whose constructor only assigns properties
let propertyDescriptors = {};
for (let [key, value] of Object.entries(obj)) {
propertyDescriptors[key] = {
value: cloneCircuitValue(value),
writable: true,
enumerable: true,
configurable: true,
};
}
return Object.create(Object.getPrototypeOf(obj), propertyDescriptors);
}
function circuitValueEquals(a, b) {
// primitive JS types and functions are checked for exact equality
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null)
return a === b;
// built-in JS datatypes with custom equality checks
if (Array.isArray(a)) {
return (Array.isArray(b) && a.length === b.length && a.every((a_, i) => circuitValueEquals(a_, b[i])));
}
if (a instanceof Set) {
return b instanceof Set && a.size === b.size && [...a].every((a_) => b.has(a_));
}
if (a instanceof Map) {
return (b instanceof Map &&
a.size === b.size &&
[...a].every(([k, v]) => circuitValueEquals(v, b.get(k))));
}
if (ArrayBuffer.isView(a) && !(a instanceof DataView)) {
// typed array
return (ArrayBuffer.isView(b) &&
!(b instanceof DataView) &&
circuitValueEquals([...a], [...b]));
}
// the two checks below cover o1js primitives and CircuitValues
// if we have an .equals method, try to use it
if ('equals' in a && typeof a.equals === 'function') {
let isEqual = a.equals(b).toBoolean();
if (typeof isEqual === 'boolean')
return isEqual;
if (isEqual instanceof Bool)
return isEqual.toBoolean();
}
// if we have a .toFields method, try to use it
if ('toFields' in a &&
typeof a.toFields === 'function' &&
'toFields' in b &&
typeof b.toFields === 'function') {
let aFields = a.toFields();
let bFields = b.toFields();
return aFields.every((a, i) => a.equals(bFields[i]).toBoolean());
}
// equality test that works for plain objects AND classes whose constructor only assigns properties
let aEntries = Object.entries(a).filter(([, v]) => v !== undefined);
let bEntries = Object.entries(b).filter(([, v]) => v !== undefined);
if (aEntries.length !== bEntries.length)
return false;
return aEntries.every(([key, value]) => key in b && circuitValueEquals(b[key], value));
}
//# sourceMappingURL=struct.js.map