o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
216 lines (180 loc) • 6.26 kB
text/typescript
/*
* Warning: The reducer API in o1js is currently not safe to use in production applications. The `reduce()`
* method breaks if more than the hard-coded number (default: 32) of actions are pending. Work is actively
* in progress to mitigate this limitation.
*/
import {
Field,
SmartContract,
state,
State,
method,
Permissions,
Bool,
PublicKey,
Reducer,
provablePure,
AccountUpdate,
Provable,
} from 'o1js';
import { Member } from './member.js';
import { ParticipantPreconditions } from './preconditions.js';
let participantPreconditions = ParticipantPreconditions.default;
Provable;
type MembershipParams = {
participantPreconditions: ParticipantPreconditions;
contractAddress: PublicKey;
doProofs: boolean;
};
/**
* Returns a new contract instance that based on a set of preconditions.
* @param params {@link MembershipParams}
*/
export async function Membership(
params: MembershipParams
): Promise<Membership_> {
participantPreconditions = params.participantPreconditions;
let contract = new Membership_(params.contractAddress);
params.doProofs = true;
if (params.doProofs) {
await Membership_.compile();
}
return contract;
}
/**
* The Membership contract keeps track of a set of members.
* The contract can either be of type Voter or Candidate.
*/
export class Membership_ extends SmartContract {
/**
* Root of the merkle tree that stores all committed members.
*/
committedMembers = State<Field>();
/**
* Accumulator of all emitted members.
*/
accumulatedMembers = State<Field>();
reducer = Reducer({ actionType: Member });
events = {
newMemberState: provablePure({
accumulatedMembersRoot: Field,
committedMembersRoot: Field,
}),
};
async deploy() {
await super.deploy();
this.account.permissions.set({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
editActionState: Permissions.proofOrSignature(),
setPermissions: Permissions.proofOrSignature(),
setVerificationKey: Permissions.VerificationKey.proofOrSignature(),
incrementNonce: Permissions.proofOrSignature(),
});
}
/**
* Method used to add a new member.
* Dispatches a new member sequence event.
* @param member
*/
.returns(Bool)
async addEntry(member: Member) {
// Emit event that indicates adding this item
// Preconditions: Restrict who can vote or who can be a candidate
// since we need to keep this contract "generic", we always assert within a range
// even tho voters cant have a maximum balance, only candidates
// but for a voter we simply use UInt64.MAXINT() as the maximum
let accountUpdate = AccountUpdate.create(member.publicKey);
accountUpdate.account.balance.requireEquals(
accountUpdate.account.balance.get()
);
let balance = accountUpdate.account.balance.get();
balance.assertGreaterThanOrEqual(
participantPreconditions.minMina,
'Balance not high enough!'
);
balance.assertLessThanOrEqual(
participantPreconditions.maxMina,
'Balance too high!'
);
let accumulatedMembers = this.accumulatedMembers.get();
this.accumulatedMembers.requireEquals(accumulatedMembers);
// checking if the member already exists within the accumulator
let exists = this.reducer.reduce(
this.reducer.getActions({
fromActionState: accumulatedMembers,
}),
Bool,
(state: Bool, action: Member) => {
return Provable.equal(Member, action, member).or(state);
},
// initial state
Bool(false)
);
/*
we cant really branch the control flow - we will always have to emit an event no matter what,
so we emit an empty event if the member already exists
it the member doesn't exist, emit the "real" member
*/
let toEmit = Provable.if(exists, Member.empty(), member);
this.reducer.dispatch(toEmit);
return exists;
}
/**
* Method used to check whether a member exists within the committed storage.
* @param accountId
* @returns true if member exists
*/
.returns(Bool)
async isMember(member: Member) {
// Verify membership (voter or candidate) with the accountId via merkle tree committed to by the sequence events and returns a boolean
// Preconditions: Item exists in committed storage
let committedMembers = this.committedMembers.get();
this.committedMembers.requireEquals(committedMembers);
return member.witness
.calculateRoot(member.getHash())
.equals(committedMembers);
}
/**
* Method used to commit to the accumulated list of members.
*/
async publish() {
// Commit to the items accumulated so far. This is a periodic update
let accumulatedMembers = this.accumulatedMembers.get();
this.accumulatedMembers.requireEquals(accumulatedMembers);
let committedMembers = this.committedMembers.get();
this.committedMembers.requireEquals(committedMembers);
let pendingActions = this.reducer.getActions({
fromActionState: accumulatedMembers,
});
let newCommittedMembers = this.reducer.reduce(
pendingActions,
Field,
(state: Field, action: Member) => {
// because we inserted empty members, we need to check if a member is empty or "real"
let isRealMember = Provable.if(
action.publicKey.equals(PublicKey.empty()),
Bool(false),
Bool(true)
);
// if the member is real and not empty, we calculate and return the new merkle root
// otherwise, we simply return the unmodified state - this is our way of branching
return Provable.if(
isRealMember,
action.witness.calculateRoot(action.getHash()),
state
);
},
// initial state
committedMembers,
{ maxUpdatesWithActions: 2 }
);
let newAccumulatedMembers = pendingActions.hash;
this.committedMembers.set(newCommittedMembers);
this.accumulatedMembers.set(newAccumulatedMembers);
this.emitEvent('newMemberState', {
committedMembersRoot: newCommittedMembers,
accumulatedMembersRoot: newAccumulatedMembers,
});
}
}