创建文章

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

使用智能合约实现有许可投票应用

简介

概览

在线投票应用非常有用,但大多数都存在缺乏透明度等问题,对于使他们成为一个理想的候选区块链造成一定影响。特别是当你用它们来对客户进行问卷调查,或者在某个特定的话题上征求公众意见,这种情况尤其明显。因此,投票的利害关系更加重要,如何解决这种应对关系,这对区块链应用构建者来说是一个问题。区块链应用有一些基本的匿名性,与此同时,一些挑战需要解决。最突出的问题是双重投票。如何防止一个人创建多个账户并且投票不止一次?我们需要的是一个有许可的投票应用。允许投票的应用应该只允许一个人投票一次。这个实例将介绍一个如何使用 Algorand 来构建该应用的方法。

该应用将涉及许多Algorand技术,包括无状态智能合约、原子转移、标准资产和资产交易。本文将从设计概述开始,然后详细介绍如何在Algorand上构建应用。

设计概述

为了在Algorand实现一个有许可的投票应用,需要一个中央机构来为用户提供投票权。在这个例子中,这是通过Algorand标准资产来进行的。中央当局创建一个投票代币,然后给予已经注册的选民一个投票代币。然后选民通过投票智能合约在一个整数范围内注册,选择进入合约。然后,选民通过一个包含两笔交易的原子交易组进行投票。第一个是调用一个智能合约去给候选人a或候选人b投票,第二个是转移投票代币回到中央机构。投票只允许在投票范围内进行。

要在Algorand上创建这种类型的应用,需要以下四个步骤。

  1. 创建资产- 中央当局需要使用Algorand ASA创建一个投票代币。选民需要使用其地址对该资产进行注册(opt-in),资产ID需要存储在有状态智能合约中,以验证用户何时投票,并确认他们正在使用投票代币。中央政府需要向选民发送一个投票代币;
  2. 创建投票智能合约 - 中央当局需要在Algorand区块链上创建投票智能合约,并通过轮数迭代进行注册和投票。创建者地址被传递给创建方法。这仅用于允许创建者删除有投票权的智能合约;
  3. 登记投票 - 选民需要对合约进行 opt-in 以在投票智能合约中注册。注册投票发生在合约创立过程中设定的一组区块之间;
  4. 投票 - 选民通过原子交易将两个交易组合并提交给区块链进行投票。第一个交易是调用智能合约,为候选人a或候选人b投票。第二个交易是从选民到中央当局的资产转移,以使用他们的投票权标记。

EditorImages/2020/08/18 17:34/1.png

EditorImages/2020/08/18 17:35/2.png

这个架构中的每个步骤将在下面的小节中解释。此解决方案仅使用goal来使应用调用有状态应用。SDKs也提供了同样的功能,可以用来代替goal。

1. 创建资产

中央机构首先需要创建一个投票代币。这可以通过SDKs,或者goal 命令行工具,或使用像algodesk.io这样的资产创建IDE来实现。

EditorImages/2020/08/18 17:44/3.png

下面是一个使用goal创建投票代币的示例。

$ goal asset create --creator {ACCOUNT} --total 1000 --unitname votetkn --decimals 0 -d ~/node/data

在这个示例中,创建了1000个投票代币。

$ goal asset send -a 0 -f {VOTER_ACCOUNT} -t {VOTER_ACCOUNT}  --creator {CENTRAL_ACCOUNT} --assetid {ASSETID} -d ~/node/data

在创建投票代币之后,从区块链返回的 assetID 必须被传递到上面的调用中。然后,中央当局应向在中央当局注册的选民发送选票代币。中央机关是如何处理注册的,这篇文章没有解释。

$ goal asset send -a 1 -f {CENTRAL_ACCOUNT} -t {VOTER_ACCOUNT}  --creator {CENTRAL_ACCOUNT} --assetid {ASSETID} -d ~/node

投票代币的assetID应该硬编码到投票智能合约中。也可以将其作为参数传入。assetID在步骤4中有更详细的讨论。要了解有关使用Assets的更多信息,请参阅开发人员文档

2. 投票智能合约创建

Goal命令行工具提供了一组用于操作应用或与应用相关的命令。goal app create命令用于创建应用。这是一个针对区块链的特定应用交易,类似于Algorand Assets的工作方式,它将返回一个应用ID。将几个参数传递给创建方法。这些参数主要围绕应用使用了多少存储空间。在有状态智能合约中,您将存储指定为全局或本地存储。全局存储表示应用本身可用的空间量,本地存储表示每个地址账户本地的存储空间。

EditorImages/2020/08/18 17:48/4.png

这个投票应用中使用了七个全局变量(一个 byte 数组和六个 int)和一个本地存储变量(byte)。全局byte 数组用于构建创建者地址,六个全局整数表示注册和投票的整数范围。本地byte 数组用于存储特定账户(即候选人a或候选人b)的投票。

$ goal app create --creator {CENTRAL_ACCOUNT}   --approval-prog ./p_vote.teal --global-byteslices 1 --global-ints 6 --local-byteslices 1 --local-ints 0 --app-arg "int:1" --app-arg "int:20" --app-arg "int:20" --app-arg "int:100" --clear-prog ./p_vote_opt_out.teal

在这个示例中,还有几个应用参数要传递给create方法。这些是有状态智能合约应用参数,代表注册和投票的轮数范围。这个投票应用使用这些范围而不是时间戳。如果需要,可以通过时间戳对应用进行修改。审批和清除程序也被传递给创建方法。有关参数传递和有状态智能合约创建的更多信息,请参见开发人员文档。

有状态智能合约的TEAL代码执行以下操作。

  • 检查是否没有设置应用ID,表示这是一个创建应用方法的调用
  • 将创建者地址存储到全局状态
  • 存储注册和投票轮数范围到全局状态

// Approval Program 审批程序
#pragma version 2

// check if the app is being created 检查是否app被创建
// if so save creator 如果是,保存创建者
int 0
txn ApplicationID
==
bz not_creation

byte "Creator"
txn Sender
app_global_put

// 4 args must be used on creation 创建变量必须使用4个参数
txn NumAppArgs 
int 4
==
bz failed

// set round ranges 设定轮数范围
byte "RegBegin"
txna ApplicationArgs 0
btoi
app_global_put

byte "RegEnd"
txna ApplicationArgs 1
btoi
app_global_put

byte "VoteBegin"
txna ApplicationArgs 2
btoi
app_global_put

byte "VoteEnd"
txna ApplicationArgs 3
btoi
app_global_put

int 1
return

not_creation:

3. 选民地址对投票智能合约进行 Opt-In

如果合约使用本地存储,用户必须选择有状态智能合约。此应用将选民的选择存储在本地存储中,并要求用户 Opt-In。

EditorImages/2020/08/18 17:53/5.png

可以使用goal或sdk进行 Opt-In。

$ goal app optin  --app-id {APPID} --from {ACCOUNT} --app-arg "str:register" -d ~/node/data

这个投票应用使用一个应用参数,其值为“register” ,以执行Opt-In操作。智能合约执行以下操作。

  • 检查智能合约的第一个参数是否为“register”
  • 确认该轮目前处于开始注册和结束注册轮之间
  • 验证该账户是否已Opt-In

// register 注册

txna ApplicationArgs 0
byte "register"
==
bnz register

.
.
.

register:
global Round
byte "RegBegin"
app_global_get
>=

global Round
byte "RegEnd"
app_global_get
<=
&&

int OptIn
txn OnCompletion
==
&&

bz failed
int 1
return

有关 Opt-In 有状态智能合约的更多信息,请参见开发人员文档

4. 用户投票

一旦注册了智能合约,用户就可以投票了。这需要调用原子交易,一个交易组有两笔交易。第一笔交易是调用有状态智能合约投票,第二笔交易是从投票者到中央机构的资产转移(投票代币)。

EditorImages/2020/08/18 17:55/6.png

本例中的原子交易是使用goal命令行工具完成的。

$ goal app call --app-id {APPID} --app-arg "str:vote" --app-arg "str:candidatea" --from {ACCOUNT}  --out=unsignedtransaction1.tx
$ goal asset send --from={ACCOUNT} --to={CENTRAL_ACCOUNT} --creator {CENTRAL_ACCOUNT} --assetid {VOTE_TOKEN_ID} --fee=1000 --amount=1 --out=unsignedtransaction2.tx

$ cat unsignedtransaction1.tx unsignedtransaction2.tx > combinedtransactions.tx
$ goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx 
$ goal clerk sign -i groupedtransactions.tx -o signout.tx
$ goal clerk rawsend -f signout.tx

智能合约通过以下操作处理此请求。

  • 验证包含字符串“vote”的第一个应用参数
  • 验证投票调用是在投票抡次的开始和结束之间
  • 检查投票人是否选择了智能合约
  • 检查投票者的账户,以确认他们至少有一个投票代币(硬编码,应该更改)
  • 验证组中是否有两个交易
  • 检查第二个交易是否为资产转移,转移的代币是投票代币
  • 检查第二个交易的接收方是否是应用的创建者
  • 检查账户是否已经投票,如果已经投票,则只返回true,不更改全局状态
  • 验证用户是否投票给候选人a或b
  • 从全局状态读取候选人的当前总数并增加值
  • 将候选选项存储到用户的本地状态

// vote 投票
txna ApplicationArgs 0
byte "vote" 
==
bnz vote
.
.
.
vote:

// verify in voting rounds 在投票轮中进行验证

global Round
byte "VoteBegin"
app_global_get
>=

global Round
byte "VoteEnd"
app_global_get
<=
&&
bz failed

// Check that the account has opted in 检查账户是否选择
// account offset (0 == sender, 
// 1 == txn.accounts[0], 2 == txn.accounts[1], etc..) 检查账户余额

int 0 
txn ApplicationID
app_opted_in
bz failed

// check if they have the vote token 检查是否有意向资产的投票代币
// assuming assetid 2. This should  2. 应该被更改为合适的资产ID
// be changed to appropriate asset id
// sender
int 0

// hard-coded assetid 硬编码
int 2
// returns frozen asset balance 返回冻结资产余额
// pop frozen 冻结
asset_holding_get AssetBalance
pop

// does voter have at least 1 vote token 判断投票者至少有一个投票代币
int 1
>=
bz failed

// two transactions 两个交易
global GroupSize
int 2
==
bz failed

// second tx is an asset xfer 第二个交易是资产转移
gtxn 1 TypeEnum
int 4
==
bz failed

// creator receiving the vote token 创建者接收投票代币
byte "Creator"
app_global_get
gtxn 1 AssetReceiver
==
bz failed

// verify the proper token spent 验证投票代币消费
gtxn 1 XferAsset 
// hard coded and should be changed  硬编码并应该被修改
int 2
==
bz failed

// spent 1 vote token 消费一个投票代币
gtxn 1 AssetAmount
int 1
==
bz failed

//check local to see if they have voted 检查本地,确认是否进行投票
int 0 // sender 发送者
txn ApplicationID
byte "voted"
app_local_get_ex 

// if voted skip incrementing count 如果投票,跳过
bnz voted
pop

// can only vote for candidate a
// or candidate b 仅能为候选人a或b投票
txna ApplicationArgs 1
byte "candidatea" 
==
txna ApplicationArgs 1
byte "candidateb" 
==
||
bz failed

// read existing vote candidate
// in global state and increment vote 从全局状态或增量投票中读取存在的候选人
int 0
txna ApplicationArgs 1
app_global_get_ex
bnz increment_existing
pop
int 0
increment_existing:
int 1
+
store 1
txna ApplicationArgs 1
load 1
app_global_put

// store the voters choice in local state 在本地状态中存储投票者选择
int 0 //sender
byte "voted"
txna ApplicationArgs 1
app_local_put

int 1
return

voted:
pop
int 1
return

有关原子交易有状态智能合约资产的更多信息,请参见开发人员文档。

总结

这个有许可投票应用说明了如何使用Algorand的几个特性来实现一个功能性的智能合约。应用的完整源代码可以在Github上找到。