Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions learn/renjuntao/task2/.readme.md.swp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### task2: 设计一个简单的投票统计器

1. 设计一个简单的投票统计器用于小团队内部投票,要求能累积统计出赞成票和反对票的票数
2. 考虑检查投票者属于团队成员,假设队员不会重复投票

请提交测试执行脚本。
158 changes: 158 additions & 0 deletions learn/renjuntao/task2/Vote.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { AccountUpdate, Field, MerkleMap, Mina, Poseidon, PrivateKey, PublicKey } from 'o1js';
import Vote from './Vote';

let proofsEnabled = false;

interface Member {
key: PrivateKey;
account: Mina.TestPublicKey;
hashKey: Field;
}

describe('Vote test', () => {
let deployerAccount: Mina.TestPublicKey;
let deployerKey: PrivateKey;

let members: Member[] = [];
let notMembers: Member[] = [];

let zkAppAddress: PublicKey;
let zkAppPrivateKey: PrivateKey;
let zkApp: Vote;
let memberMap = new MerkleMap();

beforeAll(async () => {
if (proofsEnabled) await Vote.compile();
});

beforeEach(async () => {
const Local = await Mina.LocalBlockchain({ proofsEnabled });
Mina.setActiveInstance(Local);
deployerAccount = Local.testAccounts[0];
deployerKey = deployerAccount.key;

Local.testAccounts.slice(1, 6).forEach((item, index) => {
members[index] = {
account: item,
key: item.key,
hashKey: Poseidon.hash(item.toFields()),
};
memberMap.set(members[index].hashKey, Field(1));
});

Local.testAccounts.slice(6, 10).forEach((item, index) => {
notMembers[index] = {
account: item,
key: item.key,
hashKey: Poseidon.hash(item.toFields()),
};
});

zkAppPrivateKey = PrivateKey.random();
zkAppAddress = zkAppPrivateKey.toPublicKey();

zkApp = new Vote(zkAppAddress);
});

async function localDeploy() {
const txn = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount);
await zkApp.deploy();
});
await txn.prove();
await txn.sign([deployerKey, zkAppPrivateKey]).send();

const txnInit = await Mina.transaction(deployerAccount, async () => {
await zkApp.updateMemberRoot(memberMap.getRoot());
});
await txnInit.prove();
await txnInit.sign([deployerKey, zkAppPrivateKey]).send();
}

it('member vote approve', async () => {
await localDeploy();

const user = members[Math.floor(Math.random() * members.length)];
const state1 = zkApp.getVoteCounts();
const txn = await Mina.transaction(user.account, async () => {
await zkApp.vote(Field(1), memberMap.getWitness(user.hashKey));
});
await txn.prove();
await txn.sign([user.key]).send();
const state2 = zkApp.getVoteCounts();
expect([state2.approve, state2.reject])
.toEqual([state1.approve.add(Field(1)), state1.reject]);
});

it('member vote reject', async () => {
await localDeploy();

const user = members[Math.floor(Math.random() * members.length)];
const state1 = zkApp.getVoteCounts();
const txn = await Mina.transaction(user.account, async () => {
await zkApp.vote(Field(0), memberMap.getWitness(user.hashKey));
});
await txn.prove();
await txn.sign([user.key]).send();
const state2 = zkApp.getVoteCounts();
expect([state2.approve, state2.reject])
.toEqual([state1.approve, state1.reject.add(Field(1))]);
});

it('member vote complex scene', async () => {
await localDeploy();

const state1 = zkApp.getVoteCounts();

const castVote = async (user: any, voteOption: Field) => {
const txn = await Mina.transaction(user.account, async () => {
await zkApp.vote(voteOption, memberMap.getWitness(user.hashKey));
});
await txn.prove();
await txn.sign([user.key]).send();
};

await Promise.all([
await castVote(members[0], Field(0)),
await castVote(members[1], Field(1)),
await castVote(members[2], Field(1)),
await castVote(members[3], Field(1)),
await castVote(members[4], Field(0))
]);

const state2 = zkApp.getVoteCounts();

expect([state2.approve, state2.reject])
.toEqual([state1.approve.add(Field(3)), state1.reject.add(Field(2))]);
});

it('notmember vote approve', async () => {
await localDeploy();
const user = notMembers[Math.floor(Math.random() * notMembers.length)];
const state1 = zkApp.getVoteCounts();
await expect(async () => {
const txn = await Mina.transaction(user.account, async () => {
await zkApp.vote(Field(1), memberMap.getWitness(user.hashKey));
});
await txn.prove();
await txn.sign([user.key]).send();
}).rejects.toThrow('Member validation failed');
const state2 = zkApp.getVoteCounts();
expect(state2).toEqual(state1);
});

it('merkle tree root update and access', async () => {
await localDeploy();
const tree = new MerkleMap();
members.forEach((member) => {
tree.set(member.hashKey, Field(1));
});
const txnRootUpdate = await Mina.transaction(deployerAccount, async () => {
await zkApp.updateMemberRoot(tree.getRoot());
});
await txnRootUpdate.prove();
await txnRootUpdate.sign([deployerKey, zkAppPrivateKey]).send();
const currentRoot = zkApp.getMemberRoot();
expect(currentRoot).toEqual(tree.getRoot());
});
});
60 changes: 60 additions & 0 deletions learn/renjuntao/task2/Vote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Bool, Field, MerkleMapWitness, method, Poseidon, PublicKey, SmartContract, state, State } from 'o1js';

export default class Vote extends SmartContract {
@state(PublicKey) deployerPublicKey = State<PublicKey>();
// 赞成
@state(Field) yesVotes = State<Field>();
// 反对
@state(Field) noVotes = State<Field>();
// 成员证明
@state(Field) VoteMemberMapRoot = State<Field>();

init() {
super.init();
this.deployerPublicKey.set(this.sender.getAndRequireSignature());
this.yesVotes.set(Field(0));
this.noVotes.set(Field(0));
this.VoteMemberMapRoot.set(Field(0));
}

@method async vote(ticket: Field, witness: MerkleMapWitness) {
// 成员检测
const currentRoot = this.VoteMemberMapRoot.getAndRequireEquals();
const sender = this.sender.getAndRequireSignature();
const key = Poseidon.hash(sender.toFields());
const [root, keyWitness] = witness.computeRootAndKey(Field(1));
Bool.and(
currentRoot.equals(root),
keyWitness.equals(key),
).assertTrue('Member validation failed');

// 参数验证
ticket.equals(Field(0)).or(ticket.equals(Field(1))).assertTrue('isYes must be 0 or 1');

// 获取当前的“是”票和“否”票数量
const currentYesVotes = this.yesVotes.getAndRequireEquals();
const currentNoVotes = this.noVotes.getAndRequireEquals();

// 更新票数
this.yesVotes.set(currentYesVotes.add(ticket));
this.noVotes.set(currentNoVotes.add(Field(1).sub(ticket)));
}

@method async updateMemberRoot(newRoot: Field) {
const deployer = this.deployerPublicKey.getAndRequireEquals();
const sender = this.sender.getAndRequireSignature();
sender.equals(deployer).assertTrue('Only deployer can perform this action');
this.VoteMemberMapRoot.set(newRoot);
}

getVoteCounts() {
return {
approve: this.yesVotes.get(),
reject: this.noVotes.get(),
};
}

getMemberRoot() {
return this.VoteMemberMapRoot.get();
}
}
6 changes: 6 additions & 0 deletions learn/renjuntao/task2/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### task2: 设计一个简单的投票统计器

1. 设计一个简单的投票统计器用于小团队内部投票,要求能累积统计出赞成票和反对票的票数
2. 考虑检查投票者属于团队成员,假设队员不会重复投票

请提交测试执行脚本。
Binary file added learn/renjuntao/task2/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.