diff --git a/learn/renjuntao/task2/.readme.md.swp b/learn/renjuntao/task2/.readme.md.swp new file mode 100644 index 000000000..4226c0fe2 --- /dev/null +++ b/learn/renjuntao/task2/.readme.md.swp @@ -0,0 +1,6 @@ +### task2: 设计一个简单的投票统计器 + +1. 设计一个简单的投票统计器用于小团队内部投票,要求能累积统计出赞成票和反对票的票数 +2. 考虑检查投票者属于团队成员,假设队员不会重复投票 + +请提交测试执行脚本。 diff --git a/learn/renjuntao/task2/Vote.test.ts b/learn/renjuntao/task2/Vote.test.ts new file mode 100644 index 000000000..ae76fdbe9 --- /dev/null +++ b/learn/renjuntao/task2/Vote.test.ts @@ -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()); + }); +}); \ No newline at end of file diff --git a/learn/renjuntao/task2/Vote.ts b/learn/renjuntao/task2/Vote.ts new file mode 100644 index 000000000..8193e894e --- /dev/null +++ b/learn/renjuntao/task2/Vote.ts @@ -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(); + // 赞成 + @state(Field) yesVotes = State(); + // 反对 + @state(Field) noVotes = State(); + // 成员证明 + @state(Field) VoteMemberMapRoot = State(); + + 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(); + } +} \ No newline at end of file diff --git a/learn/renjuntao/task2/readme.md b/learn/renjuntao/task2/readme.md new file mode 100644 index 000000000..4226c0fe2 --- /dev/null +++ b/learn/renjuntao/task2/readme.md @@ -0,0 +1,6 @@ +### task2: 设计一个简单的投票统计器 + +1. 设计一个简单的投票统计器用于小团队内部投票,要求能累积统计出赞成票和反对票的票数 +2. 考虑检查投票者属于团队成员,假设队员不会重复投票 + +请提交测试执行脚本。 diff --git a/learn/renjuntao/task2/test.png b/learn/renjuntao/task2/test.png new file mode 100644 index 000000000..f65296c78 Binary files /dev/null and b/learn/renjuntao/task2/test.png differ