aelf系统概论
aelf区块链
aelf的结构示意(Figma链接):

左边是共识数据,假设有5个区块生产节点(Block Producer);
中间是aelf链上的结构,包含区块同步、共识调度和交易池等;
右边是区块生产的流程。
aelf系统在整体上可以看做一个状态机,其初始状态随着创世区块(Genesis Block)的生产而被定义。
查看https://explorer.aelf.io/block/1,进入Transactions选项卡,可以看到共计16个交易,其中1个交易为Initialize,完成了Genesis合约的初始化,其余的都是DeploySystemSmartContract,即部署和初始化系统合约的交易。 aelf每一个节点的创世区块的哈希值,足以代表该节点实际上能够同步aelf哪个网络上的区块(主网主链、主网侧链、测试网主链、测试网侧链等都是不同的aelf网络)。
随着后续交易被Miner定时地打包进新的区块中,aelf系统的状态不断地进行更新。对于每一个aelf节点,状态的更新都在kv数据库中完成,我们称之为节点的StateDb。外界可以通过访问aelf链上智能合约的view方法,获取所连接的aelf节点的StateDb中的状态,如账户余额、选举票数、分红数量等等。
访问智能合约的view方法的具体方式为:
构造一个MethodName为该view方法的交易;
将交易广播给一个aelf节点;
读取该交易的返回结果。
交易(Transaction),是改变StateDb的唯一手段。
关于交易生命周期的文档:aelf交易生命周期。
区块(Block),其主要意义之一是作为交易的载体。区块与区块之间以hash的方式前后相连:后置区块的区块头(Block Header)中,必须包含其前置区块的hash,记为previousBlockHash;高度为1的创世区块除外。除此之外,区块头中还包含一些能够标识当前StateDb状态的值,为了节省空间,都以hash的方式保存。
关于区块数据结构的文档:aelf区块数据结构和ChainId。
区块和交易都保存在区别于StateDb的数据库中,我们称之为节点的ChainDb。StateDb和ChainDb是独立的两个kv数据库,当前aelf提供了支持Redis协议的实现,在实际使用中,都使用ssdb(支持Redis协议的、使用硬盘持久化数据的kv数据库)。
不过ssdb在数据量过大的时候,会出现无法重启的问题,这时候要么删库,恢复最新备份的镜像,重新同步;要么给机器加内存才能重启。
任何区块链系统都会使用共识机制激励挖矿行为,aelf采用了DPoS共识,并选择将这种激励行为的全部逻辑实现为智能合约。具体而言,成为aelf的Miner的前提是获得足够的选票,从而能够在每一届结束的时候,得到区块奖励。
aelf主网上的持币用户必须抵押10万个ELF,才能成为Miner的候选人(Candidate) -> 这一部分逻辑包含在主链的Election合约中。
用户可以对aelf主网主链上候选人进行投票,1个ELF代表1票 -> 这一部分逻辑也包含在主链的Election合约中。
只有票数靠前的前17名(2022年)候选人,才可以在下次换届(目前主网每周四换届)的时候成为Miner;在DPoS语境下,我们更愿意称之为区块生产者(Block Producer)。
以上过程都包含在选举界面中:https://explorer.aelf.io/vote/election。
账户地址和哈希值
分别对应aelf代码实现中的Address和Hash。
账户地址和哈希值本质上都是二进制数组,见下表:
Value长度
32字节
32字节
长度的常量定义在AElfConstants.cs中。
如何构造
FromPublicKey(byte[] bytes)通过账户公钥的二进制数组生成AddressFromBytes(byte[] bytes)通过长度为32位的二进制数组生成AddressFromBase58(string inputStr)通过Base58格式的字符串生成Address
LoadFromByteArray(byte[] bytes)通过长度为32位的二进制数组生成HashLoadFromBase64(string base64)通过Base64格式的字符串生成HashLoadFromHex(string hex)通过Hex格式的字符串生成Hash
Hash类型的实例,除了成员函数之外,还可以通过HashHelper中的方法进行构造。
AddressHelper中包含一个验证地址格式的方法,不过本质上就是看地址是否符合Base58的格式。
如何打印
ToBase58()一般而言,外界看到的账户地址都是Base58格式的
ToHex()一般而言,外界看到的哈希值是Hex格式的字符串ToInt64()在一些情况下,会把哈希值转化为长整型
在个别情况下(特别是使用sdk时),Hash的会表现为Base64格式的字符串,而非Hex。
不过地址的字符串格式的表现永远为Base58。(这里说的是原地址,不是加入了aelf定义的前后缀的地址)
aelf的地址格式
在evm生态的区块链中,由于这些生态的区块链(以太坊、BSC、Polygon等)使用同样的账户体系,加入A只给B提供自己的收款地址,如0x6315BC5e9429f89c75abf77F9B4fC469a185818f,B无法判断应该在哪个网络上对A进行转账。
aelf由于一主链多侧链的结构,主链和侧链上必然也使用的是同样的账户体系。在aelf官方的生态体系中,都会使用这样的格式表现用户的地址:ELF_2RehEQSpXeZ5DUzkjTyhAkr9csu7fWgE5DAuB2RaKQCpdhB8zC_{ChainId}
前缀
ELF_表示该地址是aelf某个网络中的地址(可能是主网,也有可能是测试网);后缀
_{ChainId}就是进一步告知该地址的链信息,比如A借此告知B希望在哪一条侧链上进行收款。
关于ChainId的相关文档,见aelf区块数据结构和ChainId。
我们可以称地址2RehEQSpXeZ5DUzkjTyhAkr9csu7fWgE5DAuB2RaKQCpdhB8zC为short address,称地址ELF_2RehEQSpXeZ5DUzkjTyhAkr9csu7fWgE5DAuB2RaKQCpdhB8zC_AELF为full address。
虽然该前后缀的标准只有aelf官方的工具中进行了支持,不过希望aelf社区的其他DApp也能够支持该标准。
aelf的哈希算法
aelf使用SHA256来对二进制数组进行哈希运算。
ByteExtensions.cs:
ELF代币
与以太坊等其他公链不同,aelf区块链本身并不维护aelf账户的余额,也就是说ChainDb中不会包含“某个地址拥有多少个ELF”这样的信息。
取而代之的是,我们构建了一个MultiToken合约,该合约有如下几个重要的方法(理解这些方法的时候可以参考ERC20标准):
Create
创建一个新的Token
Issue
发行Token
Transfer
转移Token
TransferFrom
将A的Token转移给B
Approve
授权第三方使用自己的Token
UnApprove
取消第三方使用自己的Token的授权
Burn
销毁Token
MultiToken合约会作为系统合约之一,在aelf网络的创世区块中完成部署和初始化。
而ELF代币,正是MultiToken合约所维护的Token之一。在该合约中,我们使用symbol作为Token的唯一标识,因此在每个aelf网络中,实际上能够被认可的ELF代币有且仅有被这一个:状态被维护在作为系统合约MultiToken合约中,symbol为ELF的Token。
区块定稿(AEDPoS共识)
区块链是分布式系统的一种,借助特殊的共识机制(如PoW、PoS、DPoS等),实现了去中心化分布式系统。要理解区块链共识,就需要借助分布式系统领域中一个常常提及的概念:最终一致性。(这里不展开讲分布式系统中的概念。)
aelf使用DPoS作为区块链共识机制。与ELF Token的实现类似,DPoS共识的状态也并没有维护在ChainDb中,而是使用一系列合约来存储共识信息,也就是说,共识数据实际上是存储在StateDb里的。这些合约,以AEDPoS合约为主,还涉及Election合约、Vote合约。
维护DPoS共识数据本身。
区块生产的调度;
区块头共识信息和共识交易的生成;
区块头共识信息的验证;
共识交易的执行后验证。
节点竞选相关数据和功能。
普通账户通过抵押参选aelf节点,成为Candidate;
普通用户为Candidate投票;
统计排名靠前的Candidate,确认下一届的区块生产者(Block Producer)。
Election合约的底层合约。
维护Candidate列表;
统计选举投票票数,对一些时间点的投票结果做快照。
区块生产调度
在DPoS共识中,可以认为,区块链网络的全局会在每一个确定的时间点,产生一个短暂的“中心”,在这个时间点附近的时间段中,全网的其他节点只能接收该“中心”产生的区块(通过验证则意味着接收了该区块)。
把每个时间段如何确定短暂“中心”的过程称之为区块生产的进程(Process)。具体的每一个时间段称为时间槽(Time Slot)。
aelf主网的时间槽长度为4秒;在每个时间槽中,BP最多能够产生8个区块(第9个区块会验证失败)。
根据aelf技术白皮书,aelf的区块生产的进程以轮(Round)为单位向前推进。
在每一轮中,每个BP首先会被分配一个时间槽,其分配的次序是随机的。最后,会有一个额外时间槽(Extra Time Slot)作为收尾,该时间槽会被随机地分配给一个BP。

解读上图,假设有4个BP:
One block generation each -> 在实际实现中改为了每个时间槽生产8个区块(为了提高区块确认速度)。
上图包含前3轮:
第一轮是A、B、C、D四个BP按顺序占据时间槽,额外时间槽分配给了B。
第一轮产生了随机数后,第二轮的次序根据第一轮的随机数做了重新排布:C、B、D、A,额外时间槽分配给D。
第三轮体现了顺序的协商过程,当mod冲突的时候会做一个调整,最终这个协商过程优化为:后来的mod结果如果发生了冲突,反过来调整前面占据该时间槽的BP,这样只要这一轮还没结束,谁也不知道下一轮的真正次序到底是什么。
之后的轮次都以相同的逻辑进行推进。
调度相关逻辑的实现细节,以及换届的时候调度方式的改变:aelf区块生产调度。
接下来我们展开一下aelf主网上BP的选举过程(创世 -> 选举 -> 投票 -> 换届)。
关于选举的四个过程中都可以在该界面直观地看到:https://explorer.aelf.io/vote/election
创世
aelf主网启动时,共有五个节点,这五个节点的公钥配置在appsettings.json中:
这五个初始节点的公钥和排布顺序都参与决定创世区块的哈希值。
如果启动节点的时候使用了不同的appsettings.json文件,那就一定无法加入aelf主网,因为这个节点在验证高度为2的区块时,会发现previousBlockHash与本地的高度1区块的哈希值并不一致,从而拒绝同步。
以上的顺序也同时决定了AEDPoS共识进程的第一轮出块顺序。实际操作的时候,启动顺序为:
先启动第一个公钥的节点,该节点的高度会停留在17,也就是除了创世区块以外,它会在出完两个时间槽的区块后,停下来等待其他节点的加入(事实上这是一个通用的逻辑:当一个BP发现自己连续产生了过多的区块后,会自觉停下来等待其他的BP继续出块,因为此时很有可能发生了网络故障);
随后启动其他公钥的节点,至少再启动三个,因为同时出块的BP必须大于总BP数量的2/3时,区块才能持续、稳定地生产下去。
参选
随着初始节点的启动,aelf主网正式开始运行。
接下来要做的事情,就是将五个初始节点,逐步替换为17个由ELF持币者选举出来的BP。
在aelf主网中,在五个初始节点依然参与出块的前提下,每周为BP列表开放两个新的名额,直到BP总数达到17为止。
开放名额的操作就是通过议会提案调用AEDPoS合约的SetMaximumMinersCount方法,将BP数量上限逐步往上提升,比如这个提案将BP总数从5改为7。账户A如果试图成为BP,首先要成为候选人(Candidate):
账户A自身在账户中准备100, 000个ELF+足以支付一笔交易的手续费(0.3个ELF左右),调用Election合约的
AnnounceElection方法。委托其他账户准备100, 000个ELF+足以支付一笔交易的手续费(0.3个ELF左右),调用Election合约的
AnnounceElectionFor方法,并将pubkey参数设置给账户A的公钥。
所谓的调用XX合约的YY方法,实际操作都是构造并对aelf节点广播一个To为XX合约地址,MethodName为YY的交易。
上述的交易执行成功后,账户A便会进入候选人列表,允许其他账户对该账户的公钥进行投票。
投票
在aelf主网主链持有ELF的账户,都可以通过Election合约的Vote方法对候选人进行投票。
参数为:
candidate_pubkey即参选的候选人的公钥;
amount为票数;
end_timestamp为这笔投票的赎回时间;
amount和end_timestamp越大,这笔投票对应的权重就越大,在每次换届时可以领取到的公民福利分红(Citizen Welfare Profits)就越多。
token是用来辅助生成这笔投票的VoteId(作为唯一标识),一般情况下都为空,这种情况下会使用Election合约地址、被投票者的公钥、被投票者获得的票数作为依据,计算得到VoteId。
关于公民福利分红的部分,详情见本文的经济系统模块。
换届
在aelf主网运行的过程中,ELF持币者随时可以使用ELF进行投票。
aelf每周都会在一个固定的时间点(开始)统计当前票数,由这个时间点的BP(可能是任意一个),获取当前得票数最高的17个候选人,然后根据新产生的17个BP,生成下一轮的时间槽信息;这个下一轮,实质上就是下一届的第一轮。
这个下一轮信息的合法性会被其他aelf节点进行校验,最终,在每一个实际成为BP的节点上,共识的调度器会解析共识数据,进而通知节点的区块生产相关服务在未来属于该公钥的时间槽中对区块进行打包。

以5个BP的aelf网络为例,记某一届的BP为BP1-BP5;
第999轮中间,到了该换届的时间点,但是不回立刻换届:因为BP5所在的位置不是Extra Time Slot;
BP3在Extra Time Slot中生产区块时,得知下一轮应该产生新一届的信息了,便从Election合约中获取到最新的得票top5的节点:BP3-BP7,即BP1和BP2落选了;
BP3随即生成BP列表为BP3-BP7的新一轮信息,作为第1000轮,换届完成。
在999轮末尾,BP3做的事情实际上是将NextRound行为改为NextTerm行为,具体的相关逻辑见AEDPoS合约 。
共识奖励
不同于比特币、以太坊等区块链网络,aelf只有在每次换届的时候才进行区块奖励的发放。
aelf主网的换届周期为1周,在每次换届的时候,都会统计刚刚结束的一届,所有BP的出块数量(共识合约中有现成的数据),按每个区块0.125个ELF(从2020年年底起,四年减半),将所有的奖励打入Treasury分红池。随后这些奖励会持续地分配到下一级和下下级的分红池中。
其中,BP能够获取的作为出块奖励的份额为总奖励的25%。每个BP都以上一轮的实际出块数量作为权重,根据权重分配这一部分奖励。但是当某一个BP的实际出块数对比平均出块数量过少的话,会对进一步减少其权重。
具体的算法可见aelf经济与治理白皮书。
不可逆转区块
在任何一个区块链中,最新产生的区块都有被逆转的可能。在以太坊中,会统计ommers并给与一定奖励;当某个高度的区块被推翻/逆转的概率已经很低的时候,会将该区块标记为Safe Block。
可以参考一下https://etherscan.io/里的LAST FINALIZED BLOCK和LAST SAFE BLOCK。
aelf的共识机制会通过双重确认的方式,推举一个过往的区块作为最近不可逆转区块(Last Irreversable Block)。在后文中,我们记最近不可逆转区块为LIB。该区块的信息会记录在ChainDb中的Chain结构中。
即Chain数据结构中的两个属性:last_irreversible_block_hash和last_irreversible_block_height。
如果在一个时间槽中,这个时间槽对应的BP生产了至少一个区块,我们称这个时间槽被填充了。
BP在出块过程中,通过观察最近两轮的时间槽的填充状况,如果时间槽填充状况理想,就将当前高度暗示为LIB;
所谓的双重确认,是指BP会随时统计其他BP所暗示的LIB,在暗示的数量足够的前提下,取前一轮的第1/3的位置的BP所暗示的高度,推举为LIB。
LIB的确认,就意味着aelf区块链的区块的定稿。
系统合约
在aelf中,系统合约是维持aelf区块链网络基本功能、为其他DApp提供基础服务的一组合约,通常部署在创世区块中,并可以在议会提案通过的前提下,对系统合约进行升级。
零合约 / Genesis合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/genesis.html
主要的相关proto文件:
basic_contract_zero_impl.proto 就是个壳子,把下面两个proto文件作为base撺到一起。
basic_contract_zero.proto 主要是Controller地址的读写,有两个Controller,一个管理合约部署和升级,一个管理合约代码检查。
acs0.proto 包含合约部署和升级的所有方法。
零合约,又称Genesis合约,是aelf网络中负责部署和升级其他任何合约的合约,包括升级零合约本身。关于零合约的细节描述,见文档:Genesis合约 。
经济系统相关合约
经济系统相关的系统合约有:
MultiToken合约
Profit合约
TokenHolder合约
Economic合约
TokenConverter合约
Treasury合约
MultiToken合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/multi-token.html
主要的相关proto文件:
token_contract.proto
transaction_fee.proto 包含关于手续费收取的一些数据结构定义。
MultiToken合约的功能主要分为两大类:
多币种的创建和维护(发行、转账、授权、销毁等等);
aelf系统手续费的收取。
第一类与ERC20和ERC1155都很类似。
第二类则是MultiToken合约与aelf交易执行插件配合,会拦截每一个实现了acs1、acs8标准的合约的交易,额外生成一些交易,来完成对用户手续费的收取;我们称额外生成的这些交易(一笔或多笔)为插件(Plugin)交易。
从执行顺序上,插件交易分为前置插件(Pre-plugin)交易和后置插件(Post-plugin)交易,分别是指该交易会在原交易执行前就执行、在原交易执行后再执行。
插件交易可以配置是否阻断原交易的执行:实现接口IPreExecutionPlugin的IsStopExecuting方法即可。阻断意味着,如果插件交易执行失败,那么原交易也会执行失败,要么原交易会无法通过预验证(大概率)从而不会进入交易池,要么原交易虽然被打包,但是不会造成StateDb的任何改动。
上述逻辑详见文档ACS1 Method Fee收取过程。
Profit合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/profit.html
主要的相关proto文件:
profit_contract.proto
Profit合约的使命是维护分红方案(Profit Scheme)。
分红方案有如下几个特征:
任何账户都可以通过Profit合约创建分红方案。
分红方案的受益者(beneficiary)分为两种,一种是普通账户,表现为一个账户地址;另一种是其他分红方案。也就是说,分红方案可以拥有下一级的分红方案(Treasury就是这么实现的)。
普通账户需要手动领取分红,而次一级的分红方案会在上一级分红方案进行分红的时候直接收到分红。
分红方案维护受益者的股份(shares)。在每次进行分红的时候,根据每个受益者的股份进行分配。(当然, 这里的受益者包含了普通账户和次一级分红方案。)
分红方案的分红时机是不固定的,取决于该方案的管理者(manager)什么时候调用分红合约的
DistributeProfits方法。
TokenHolder合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/token-holder.html
主要的相关proto文件:
token_holder_contract.proto
TokenHolder合约实质上是对Profit合约的一个应用。它让用户间接使用Profit合约管理分红方案的同时,接管了对分红方案中股份的维护。
通过TokenHolder合约创建的分红方案,受益人股份的增加只有一种途径:调用TokenHolder合约的RegisterForProfits方法。
这个方法的实质是抵押分红方案创建者指定的某一种Token(当然仅限于创建于系统MultiToken合约种的Token),从而获得该分红方案的股份。
其余方案基本和Profit合约一致。
Economic合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/economic.html
主要的相关proto文件:
economic_contract.proto
Economic合约的职责只有一个:初始化aelf的经济系统。
当外界试图使用aelf的源代码启动一个自己的区块链网络时,绕不开的一步就是重写Economic合约,定制自己的经济系统。
这个合约只有两个接口,InitialEconomicSystem用于完成经济系统初始化的工作,IssueNativeToken用于完成ELF代币初始发行的工作(实际上初始就完成了TotalSupply的发行)。
具体的初始化过程,见Economic合约。
aelf主网上的初始化交易是这个。
TokenConverter合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/token-convert.html
主要的相关proto文件:
token_converter_contract.proto
TokenConverter合约实现了Bancor协议。与AMM协议类似,不过:
代币对的连接称为Connector;
作为交易基准的BaseToken只能有一个,我们设置为ELF;
用户通过Buy方法购买除了ELF以外的其他Token;
用户通过Sell方法卖掉除了ELF以外的其他Token,从而获得ELF。
aelf的八种资源币都创建了Connector,可以在这里进行交易https://explorer.aelf.io/resource:。
Treasury合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/treasury.html
主要的相关proto文件:
treasury_contract.proto
Treasury合约也是对Profit合约的一个应用,它维护了一个类似下图的分红方案。

第一级的分红方案为Treasury,分红来源有三个:
每一届的出块数量奖励(初始为0.125个ELF/Block,每四年减半);
通过acs1收取的用户的手续费的90%(其余的10%会被销毁);
主动捐赠(任何人都可以通过Treasury合约的
Donate方法进行捐赠)。
第二级的分红方案有三个:
Miner Rewards:总体而言是给BP的。
Backup Subsidy:平分给所有参选的节点(BP和Candidate),但是能获取该奖励的节点上限为(当前BP数量上限*5)。在aelf主网有17个BP的情况下,得票数为前85的节点会分享该收益。
Citizen Welfare:根据投票者投票的权重,分享该部分分红。
Miner Rewards下有第三级分红方案:
Basic Rewards:占Miner Rewards的50%的股份,会根据BP上一届的实际出块数量,分配给BP。
Welcome Rewards:占Miner Rewards的25%的股份,当本届有新的BP(也就是此前从来没有产生过区块的节点)上任,这一部分奖励会平均分配给新BP;否则,打入Basic Rewards分红方案。
Flexible Rewards:占Miner Rewards的25%的股份,当本届有新的BP(也就是此前从来没有产生过区块的节点)上任,这一部分奖励会打入Citizen Welfare分红方案(作为激励ELF持币者把票投给新BP的手段);否则,打入Basic Rewards分红方案。
Treasury合约实现了对上述分红方案的创建和维护。维护入口为AEDPoS合约和Election合约,它们会调用Treasury合约,来实际上影响每一个分红方案的分配细节。
治理相关合约
治理系统相关的系统合约有:
Vote合约
Election合约
Association合约
Parliament合约
Referendum合约
Configuration合约
Vote合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/vote.html
主要的相关proto文件:
vote_contract.proto
Vote合约用于维护投票项目。
任何账户都可以在Vote合约中创建一个投票项目(Voting Item),我们称创建投票项目的账户为Sponsor。Sponsor可以指定投票项目只接受哪一种Token进行投票(当然这个Token是被创建在系统MultiToken合约中的)。
Sponsor可以随时增加和删除投票项目的选项。除此之外,Sponsor可以自行对当前的投票结果进行快照(Snapshot)。
创建和维护投票项目就像出一道问券调查。参与投票的账户就像填写问卷的考生,每一次只能选择一个选项,但是可以进行多次填写。只要每次都使用了指定的Token,就会影响问卷最终的结果。
Election合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/election.html
主要的相关proto文件:
election_contract.proto
Election合约实际上是对Vote合约和Profit合约的应用,并且,由于选举系统深度参与aelf的共识,该合约的一些功能也配合着AEDPoS和Treasury合约。
对Vote合约的使用
aelf节点选举,实际上就是在Vote合约中创建了一个投票项目:
这个投票项目的Id是:
fba313a9a70cd80f65ea5a34aa44e55c7de2a3df6ec7639f84eaea352272297e;Sponsor是Election合约;
选项为Candidate列表;
指定的投票代币为ELF。
随后ELF持币者对节点进行投票,投票数据实际上都维护在Vote合约中。Election仅是读取结果。
对Profit合约的使用
伴随Candidate的诞生(有人参选),Election合约会主动维护Profit合约中创建的Backup Subsidy分红方案的股份:添加一个beneficiary,其股份为1。
伴随用户投票,Election合约会主动维护Profit合约中创建的Citizen Welfare分红方案的股份:添加一个beneficiary,股份为根据投票参数算出来的投票权重(算法在aelf经济与治理白皮书中有详细描述)。
与AEDPoS合约的配合
AEDPoS合约对Election合约:
如前文提到的,当议会通过共识合约的
SetMaximumMinersCount方法调整BP数量上限的时候,会同步调用Election合约的UpdateMinersCount方法,对BP数量做一下调整。NextTerm共识行为,即实质上触发aelf换届的共识交易,会对Election合约做一些读写操作:
读:使用
GetVictories方法读取当前得票数较高的候选人;写:调用
UpdateMinersCount方法更新BP数量上限(可能更新);调用UpdateCandidateInformation方法更新BP的出块信息;调用TakeSnapshot方法对本次选举结果生成快照。
NextRound共识行为,即实质上触发aelf共识进程进入下一轮的共识交易时:
会根据当前时间计算一下是否应当增长BP数量上限了(每自然年会主动增长2个BP数量),如果需要增长,则调用
UpdateMinersCount方法进行更新。会回顾一下最近的共识信息,是否有BP因为长期不出块等作恶行为应该被替换,如果有,则会调用
GetMinerReplacementInformation方法寻找能够替换作恶BP的候选人,并将该候选人加入下一轮的BP列表中。
Election合约对AEDPoS合约:
Election合约的
ReplaceCandidatePubkey方法可以实现对候选人的公钥的替换(候选人的Admin有权进行该操作),在实现替换时,需要将该信息同步给AEDPoS合约(通过AEDPoS的RecordCandidateReplacement方法)。AEDPoS合约会将当前Round信息中的响应公钥直接进行替换,这样很快新的公钥就被允许生产区块了。Election合约对选举结果进行快照的时候,需要调用AEDPoS合约的
GetPreviousTermMinerPubkeyList方法获取上一届出块BP的公钥,从而进一步维护各自的出块信息(这个出块信息应当算作aelf的BP的声望系统的一部分)。
与Treasury合约的配合
Treasury合约对Election合约:
Treasury中有一个关于分红领取的feature:当BP的公钥不方便领取分红的话(因为BP常常使用冷钱包),可以另外设置一个账户来领取分红,即
SetProfitsReceiver方法。但是Backup Subsidy这一分红方案并不是由Treasury合约维护的,而是由Election合约维护的,因此在设置分红领取者的时候,会调用Election合约的同名方法SetProfitsReceiver,来通知Election合约维护Backup Subsidy的股份。除此之外,在Treasury的SetProfitsReceiver方法中,也需要调用Election合约的GetCandidateAdmin方法来验证Sender的权限(只有BP的Admin才可以进行相关设置)。当Treasury合约需要根据投票数量和锁仓时长计算投票权重的时候,需要调用Election合约的
GetCalculateVoteWeight方法进行计算。
Election合约对Treasury合约:
当BP由于作恶被踢出,被其他候选人替换上任的时候,(有可能)需要通知Treasury合约修改收益领取人。
Association合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/association.html
主要的相关proto文件:
association_contract.proto
acs3.proto
aelf主网上,多个账户可以通过Association合约共同成立组织/协会实体,进而能够使用Association合约中生成的虚拟地址(Virtual Address)实现组织上的提案和权限控制等手段。
虚拟地址机制是aelf区块链的一个独特的地址,也是在aelf中实现合约账户功能的主要依赖之一。
要说明白虚拟地址机制,就需要从aelf合约开发SDK的Context.Origin和Context.Sender字段谈起(完全不高深,只不过牵扯到对交易发送者的验证的问题)。
简而言之,在执行合约的过程中,合约执行环境需要知道这一笔交易是由谁发送的。
比如在evm的solidity合约中,使用msg.sender代表与当前合约代码做交互的账户地址,可以是EOA,也可以是CA。在move合约中,使用&signer代表这笔交易的签名者的地址。
在aelf合约中,使用Context.Origin代表交易的签名者的地址,使用Context.Sender代表与合约交互的账户地址,可以是用户持有私钥的地址(类似于EOA),也可以是一个虚拟地址(类似于CA,但是更加自由)。
aelf的合约执行环境保证了虚拟地址的生成和使用是十分受限的:只有特定的合约,才能使用特定的参数,生成和使用一个特定的虚拟地址。这里的使用,指的就是通过该虚拟地址发送一个交易,这个交易会在原交易执行完毕后执行,我们称这个交易为内联(Inline)交易。在Inline交易执行的过程中,Context.Sender所取到的值就是虚拟地址。
要注意的一点是,Inline交易虽然在代码上是顺序调用的,但是其实际执行的时机是在原交易执行之后,按照Inline交易的书写顺序进行逐个调用。
Inline交易类似于Post-plugin交易,只不过执行的时机要比Post-plugin交易早。
因此,在信任某一个合约的逻辑的前提下,可以使用提前推算出的虚拟地址,来完成合约权限控制的功能:判断Context.Sender是所预期的虚拟地址,如果为真,则证明确实在上一层的交易中,已经完成了触发该方法的条件,从而能够继续执行该Inline交易。那么,Association合约的本质就是:
多个aelf账户一起建立一个association;
这个association可以通过Association合约代为生成多个虚拟地址;
那么,被创建的association就可以利用虚拟地址的上述特性实现权限控制,比如判断提案的通过等。
事实上,对于虚拟地址在提案等权限控制相关的应用,都定义在acs3中。Parliament合约和Referendum合约也都使用了一样的原理。关于提案机制的实现细节,详见aelf虚拟地址和合约权限控制 。
Parliament合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/parliament.html
主要的相关proto文件:
parliament_contract.proto
acs3.proto
简要地说,Parliament合约就是一个association的特例,其成员即当前在任的BP。我们称这个组织为议会。
议会的成员是有可能每一届都产生变动的。当BP作恶被候选人替换的情况发生,议会的成员也会发生变动。
不过议会成员并不由Parliament合约亲自维护。需要得知当前的成员列表的话,调用AEDPoS合约的GetCurrentMinerList方法就可以了。当Parliament合约想要知道某个BP是不是当前时间槽的BP,会调用AEDPoS合约的IsCurrentMiner方法进行判断(个别情况有此必要)。
Referendum合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/referendum.html
主要相关的proto文件:
referendum_contract.proto
acs3.proto
Referendum合约可以创建专门用于公投的组织。该组织的提案有一个特点:提案能否释放,取决于某一个特定代币在该提案上抵押的数量。Referendum合约的组织是与特定代币密切相关的。
Configuration合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/configuration.html
主要的相关proto文件:
configuration_contract.proto
Configuration合约是aelf作为一个去中心化分布式系统的全局的配置中心。
Configuration的SetConfiguration交易被打包进一个区块,就意味着一个配置项被写入(并不意味着会立即生效)。
共识合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/consensus.html
主要的相关proto文件:
aedpos_contract.proto
acs4.proto
在概述里,只谈一下acs4。
ACS4作为ACS之一,是实现任意一种共识合约时,需要实现的一些接口。
这里主要讨论共识标准接口的可行性和aelf中为共识合约的实现所提供的接口及其他支持。
抽象共识的思路
站在实现区块链共识的角度,我们主要关心三件事:
谁可以产生区块,如PoW共识允许每一个人参与算力竞争,PoS和DPoS共识则要对此做出一定限制;
如果可以产生区块,那么应该在什么时候产生,或者说当前的时间能不能开始尝试广播区块;
作为一个区块链全节点,应该怎么验证这个区块的合法性。
针对这三点,我们很容易想到三类接口:
输入公钥,判断这个公钥有没有资格产生区块,PoW共识直接返回true就可以了,DPoS可能会对大部分人返回false。
输入公钥,返回这个公钥下一次能够产生区块的时间戳,或者返回这个公钥当前能不能产生区块。
全节点得到区块头(block header)之后,输入从中提取到的共识数据,验证这个区块的1和2相关信息,即,PoW共识需要验证nonce的合法性,DPoS共识需要验证新区块的生产者身份合法性、生产者是否尊重自己的时间槽,得到验证结果。
除此以外,为了得到接口3中的输入,即区块头中的共识数据,至少还需要为区块生产者提供一个生成区块头共识数据的方法。
aelf合约中的实践
所有的共识合约标准上的接口,都是只读的,因为单纯获取这些数据无需改动aelf节点的StateDb。
ACS4中,合并了接口1和接口2,得到一个接口:
很显然,NextBlockMiningLeftMilliseconds的值取决于ExpectedMiningTime(预期出块时间)与当前时间(调用这个接口的时间)的差值。
LimitMillisecondsOfMiningBlock是系统分配给这个块用于打包的时间,决定了这个区块能够打包多少用户发送的交易(给打包用户发送的交易留了多长时间)。
Hint字段用来传递一些单独的状态,取决于选用的共识协议,在PoW中这个字段可能显得不必要,而AEDPoS定义了一些区块类型,如Normal Block,Extra Block,只更新少量共识信息的Tiny Block。这些,都需要反应在Hint中——共识合约不仅需要告知区块生产者多久以后可以着手产生区块,也需要告知他应该产生什么类型的区块,而不同区块会更新不同的共识信息。除此之外,引入Hint字段也能为实现其他共识协议提供了更多的可能。
这个接口分别会在链刚刚启动时、在本地Best分支上同步完一个新的区块(无论这个区块是不是自己产生的)时、区块生产者在执行自己的区块前的验证中忽然发现当前时间已经超出了自己的时间槽(即调用下文的ValidateConsensusBeforeExecution接口)后被调用。
调用之后,Consensus Service会将NextBlockMiningLeftMilliseconds传入共识的调度器中,时间一到,就会去触发生产区块的逻辑。注意,调度器中的时间是可以随时被覆盖的。事实上,每一次同步一个新的区块,调度器都会被重新初始化。
事关接口3,ACS4拆分出两个接口:
也就是说,aelf中,每一个区块执行前后,会分别调用以上两个接口的实现对区块头进行验证。验证的逻辑位于实现ACS4的合约中,验证的数据就不得不基于StateDb。因为你不能凭空去验证区块头里的共识信息对不对,共识合约里需要存储最近的共识信息,才能给最近的共识信息提供验证的支撑。
既然验证的数据基于StateDb,而ACS4的接口都是只读的方法,那就需要单独生成一笔交易去更新StateDb(区块链的特性,更新WorldState或者StateDb必须通过执行交易来完成)。
这样,ACS4除了要提供生成区块头信息的方法之外,还需要能够返回一个(或者一组)能够更新StateDb中的共识信息的交易。这个交易会作为系统交易被生成,然后在打包过程中,最先添加进区块中。系统交易的执行会先于普通交易,这样普通交易在执行时会获取到最近更新的系统提供的信息,如hash-commit-reveal策略下的随机数。在aelf中,每个区块会至少包含一个共识系统交易。
ACS4的最后两个与更新共识信息相关的接口如下:
在aelf的kernel模块代码中,GetInformationToUpdateConsensus会在生成区块头的过程中被调用,该接口返回的数据会作为区块头的Extra Data之一,用于收到区块的人验证共识信息。GenerateConsensusTransactions接口会在生成区块头之后,进一步生成系统交易的过程中被调用。补充一句,ValidateConsensusAfterExecution接口本质上只是为了验证区块头中的共识信息和执行完共识系统交易后StateDb中的共识信息的一致性,防止区块生产者“出尔反尔”。
跨链合约
合约API:https://docs.aelf.io/en/latest/reference/smart-contract-api/cross-chain.html
关于跨链机制和跨链合约的设计,见aelf跨链机制和侧链
交易执行
合约代码打包和合约代码检查
aelf没有实现自己的智能合约执行虚拟机。或者也可以说,aelf使用了一个.net core运行时的子集来作为智能合约的执行环境。
我们通过提供白名单和黑名单的方式定义这个子集。这个子集保证了aelf的智能合约无法访问机器资源、无法产生随机的执行结果(这样的话状态会无法在全局达到一致性)等等。
除此之外,为了保证合约代码执行过程中无限循环一定会停止,我们提供了一个工具完成如下操作:
静态分析C#写的代码;
在每一个有效的OpCode后注入一个计数器(
ExecutionObserverProxy);将完成注入的代码打包成为dll,后缀为dll.patch。
只有通过以上工具生成的后缀为dll.patch的文件,才有可能通过部署合约过程中,aelf节点执行的代码检查。代码检查,本质上也是检查待部署的代码是否满足:
待部署的代码引用的lib是否符合白名单和黑名单的校验规则;
待部署的代码是否注入了OpCode计数器。
实际上的代码检查远不止这些,详见aelf合约代码打包和合约代码检查。
合约部署和升级
参考上一节和aelf主网合约部署流程 ,aelf合约在部署或升级前,需要保证它能够通过代码检查。总体而言,在当前,aelf的智能合约在需要部署和升级时,需要:
发起部署/升级的提案,等待Parliament批准;
Parliament批准后,基于以前的提案,发起一个新的CodeCheck提案,BP的节点会在本地各自执行代码检查;
代码检查通过后,BP会自动批准CodeCheck提案;
CodeCheck提案通过后,释放该提案,合约就可以完成部署/升级了。
交易执行插件和交易费收取
在上文的MultiToken合约概述中,对交易执行插件略有提及。
实现IPreExecutionPlugin接口并注册,即可以添加一个Pre-plugin交易执行插件。
实现IPostExecutionPlugin接口并注册,即可以添加一个Post-plugin交易执行插件。
两个接口中关键方法分别为GetPreTransactionsAsync和GetPostTransactionsAsync,其实就是在原交易执行过程中,分别插入如干个前置交易和若干个后置交易,来辅助完成一些aelf系统定义的其他工作。
这两个方法的形参是一样的:
IReadOnlyList<ServiceDescriptor> descriptors:这个列表的元素是原交易所属合约实现的一系列proto文件的描述。ITransactionContext:本次交易执行的上下文,有发送者地址、前置区块哈希、当前区块高度、交易执行调用深度、合约OpCode计数器、执行过程中修改过的State的集合等等。
以上两个形参就是定义交易执行插件过程中所能访问到的资源。
aelf主网主链中,系统合约通过acs1定义手续费的收取,而实际上的收取过程,就是使用Pre-plugin手段实现的:在实现了acs1的合约的交易执行之前,先通过Pre-plugin机制生成一个MultiToken合约的ChargeTransactionFees交易并执行;如果该交易执行失败了,就无须执行原交易了(因为这个Pre-plugin的实现中,进一步实现了IsStopExecuting接口)。
当然,交易费的收取流程远远不止一个前置的ChargeTransactionFees交易这么简单,还涉及计算公式定义和校验,详情见文档:ACS1 Method Fee收取过程。
交易的并行执行
BP打包交易生成区块、以及其他节点在执行包含在某一个区块中的交易的过程中,都会查询相关的合约是否实现acs2标准。
如果实现了acs2标准,就意味着这个交易有单独分组从而与其他交易并行执行的可能性。acs2提供了一个方法GetResourceInfo,入参为一个交易,返回值为该交易进行并行分组的参考信息ResourceInfo:
接口ITransactionGrouper的实现会根据上述的数据结构,检测一组交易会使用到的资源,将没有资源冲突的交易分配到不同的并行组中。这里的资源,指的就是StatePath,StatePath能够表示某一个具体的State在StateDb中存储的key。
同构跨链
同构跨链是aelf区块链系统内置的一个跨链机制,最终实现的是aelf“一链一场景”的目标。
aelf跨链机制和侧链 从解释跨链转账的原理入手阐述了跨链机制。
这里则从四个模块入手。
一共需要分四个模块介绍aelf的同构跨链:
跨链通信:负责主侧链之间通信和跨链数据交互
跨链缓存:跨链数据缓存层, 以准备进行跨链索引数据使用
跨链应用:生成及验证跨链索引数据的模块,索引功能的核心模块
跨链合约:CrossChain合约是一个系统合约,用于记录其他链的数据信息,及记录跨链索引数据信息
以上模块共同实现了如下功能:
创建侧链
侧链索引费用
停止侧链索引
父链索引子链
子链索引父链
父链验证子链交易验证
子链验证父链交易验证
兄弟链之间交易验证
主要使用的数据结构有:
SideChainBlockData是子链跨链数据,存储在父链中。子链ChainId、子链高度、子链区块哈希、子链交易状态Merkle树根。(区别于交易Merkle树,这棵树的叶子节点是带交易执行结果的,成功还是失败了,主要就是用于跨链验证。)
ParentChainBlockData是父链跨链数据,存储在子链中。父链ChainId、父链高度、父链的所有子链的交易状态Merkle树根组成的Merkle树根(为兄弟链互相索引提供了数据)、父链交易状态Merkle树根、某侧链(请求该父链跨链数据的侧链)的区块高度及其对应的MerklePath、父链区块头信息中的跨链相关信息。
CrossChainBlockData是请求索引跨链信息的方法ProposeCrossChainIndexing的参数。
跨链通信
通信模块使用GRPC作为底层通信框架。
侧链启动时会主动向主链发出握手请求,主链接收后也会向侧链的server发起握手请求。
若握手成功,则在本地保留相应client,即主链节点可能同时拥有多个client,每个client对应一条侧链的节点。
每次区块执行会触发client跨链请求,每次请求server端会返回多次数据,每个数据对应一个高度对应的跨链数据,直到没有数据可以返回或者client端停止接收。server端只返回低于LIB高度的区块跨链数据。
跨链缓存
从通信接收到的跨链数据会进入缓存模块, 每条链都有一个对应的缓存结构:
维护一个目前的目标高度,添加一个跨链数据到缓存时,只有等于目标高度的的跨链数据才能添加到缓存。
每个缓存有个数量上限1024,达到上限后不会接收新的跨链数据。
跨链模块会从缓存中根据高度提取跨链数据,提取跨链数据时可指定缓存中数量的下限,若缓存中数量不足则不返回跨俩数据。
当有lib事件,会根据lib对应的状态指定一个高度,清理缓存中该高度以下的高度数据。
跨链应用
提议索引数据
从缓存层获取跨链数据,针对每条链的一次跨链数据数量上限为256,合约状态决定了哪些链的索引数据可以被提出,所以一次可提出一至多条链的索引数据。
用获取的跨链数据生成系统交易,并打包到区块中。
交易执行成功说明提议跨链数据完成,等待通过。
评审索引数据
上述跨链数据成功提议到跨链合约会释放出事件,(各个节点)应用层捕获到事件验证跨链数据合法性。
验证过程是将被提议的跨链数据和自己缓存层的跨链数据进行对比,若数据一致,则通过验证。
若跨链数据通过验证,BP节点对这次“提议”进行Approve (该过程由BP通过治理模块完成)。
释放索引数据
生成系统交易时发现有可以释放的索引数据,会生成交易数据并打包进区块,释放对应的索引数据,若成功释放则本次索引完成。
如果索引的是侧链,那么会同时生成cross chain block extra data。生成过程是用被索引的侧链交易hash root生成新的merkle tree, 并使用该merkle tree root作为cross chain block extra data。
对被释放索引数据所对应的链可以提出新的索引数据。
如果某条链的索引数据被提出后,在一定时间范围内(120s)没有被释放,则该索引数据作废,等待新的索引数据被提出。
跨链模块相关的区块验证
区块执行前验证:该区块是否应该包含数据cross chain extra data。验证方法是判断合约中是否有可以释放的索引数据, 如果没有且区块头中包含cross chain extra data,则区块验证失败
区块执行后验证:该区块包含的cross chain extra data是否正确。验证方法是判断该块内跨链合约是否成功释放索引数据,如果成功释放,则验证释放的索引数据和区块头cross chain extra data是否一致
经济系统
经济系统初始化
aelf主网的经济系统是通过Economic合约初始化的。而这个系统合约本身也只有初始化经济系统合约一个作用。
Treasury分红池
Treasury是aelf主网主链的分红池,分红池的细节见上文Treasury合约的概述。
该分红池进行分红的时机是换届的时刻。
分红逻辑的入口是Treasury合约的Release方法,这个方法是通过Inline方式调用的,其原交易为AEDPoS合约的NextTerm方法。也就是在执行换届的过程中,对上一届产生的分红进行释放。
资源币的购买和使用
在aelf上,资源token交易采用Bancor模型,没有询价的过程,买卖价格都由Bancor模型决定,可以快速交易,在用户不多、交易量少的情况下,仍能保证资源Token与ELF的即时兑换。除了资源token以外,用户也可以根据自己的需求,利用Bancor模型建立自己的token与ELF的自由兑换关系。
Bancor模型的核心公式是:
cw (connector_weight) = connector_balance / (resource_token_supply x price)
其中,cw为常量,connector balance为其resource token初始锚定ELF的总量。
该公式的思想是在任意时刻,一种token在市场上以ELF来衡量的总价格与其connector balance的比值是不变的。从公式可以看出,当resource token supply减少时,即购买resource token时,与其对应的connector balance一定会增加(买resource token会消耗ELF),所以price只有增加才能保证cw不变。 (跟AMM的恒定乘积有一点像。)
首先介绍一下连接器Connector。
Connector是Bancor模型里建立其他代币(aelf资源币)和基准代币(ELF)的兑换关系的工具。symbol、virtual_balance和weight用于建立Bancor模型。
比如建立代币ALICE和ELF的兑换关系,需要建立两个Connector:
ALICE Connector
symbol:ALICE
weight:AliceWeight
virtual_balance:AliceBalance
ELF Connector
symbol:ALICE Native
weight:NativeTokenWeight
virtual_balance:NativeTokenVirtualBalance
令deposit为某一时刻,已被购买的所有代币ALICE的抵押金。
那么此时,用户购买a个ALICE代币所需要付出的ELF数量为:
cost = NativeTokenBalance x ((AliceBalance / (AliceBalance - a))^(AliceWeight / NativeTokenWeight) - 1)
卖出a个ALICE代币可获得ELF数量:
gain = AliceBalance x (1 - (NativeTokenBalance /( NativeTokenBalance + a))^(NativeTokenWeight / AliceWeight))
更多详情见aelf经济与治理白皮书2.4节和TokenConverter合约 。
侧链经济系统
在TokenConverter合约初始化过程中,一共创建了8种资源币,其中每4种为一组,分为两组,分别用在两种模型中:
CPU、RAM、DISK、NET,给侧链付租金(按侧链的运行时间进行付费)。
READ、WRITE、STORAGE、TRAFFIC,用于开发者付费模型(通过Post-plugin交易插件在交易执行后从合约收取手续费)。
在aelf主网的经济系统中,除了上述模型,还有两种:
通过Pre-plugin交易插件在交易执行前,从用户收取手续费。
BP在进行跨链索引的时候,每执行一次索引,从指定的账户中扣除ELF作为索引费。
对比一下这四种模型。要注意的是,这四种模型本质上只是规定了能使用的代币和实现方式,实际中如何使用,是只选择一个还是选择多个同时实现,都取决于DApp开发者(或侧链所有者)和Parliament的协商结果,是灵活的。
用户付费
ELF或其他代币
acs1 + Pre-plugin
系统合约都实现了acs1;其他合约也可以选择实现acs1。
系统合约不以盈利为目的,因此acs1主要作用是防止DDoS攻击,其收益会进入Treasury分红池。
开发者付费
READ、WRITE、STORAGE、TRAFFIC这四种资源币
acs8 + Post-plugin
比较合适的场景是在共享侧链中,不同DApp的合约支付各自的使用费用。
只要合约实现了acs8,就可以让自己付费。
侧链租金
CPU、RAM、DISK、NET这四种资源币
调用侧链MultiToken合约的UpdateRentedResources和UpdateRental方法
使用该模型的话,意味着该侧链为独享侧链,BP不管侧链具体运行什么DApp。但是如果这个侧链想要跟主链互相索引,要加钱(跨链索引费)。
Rented Resource的解释在表格下。Rental就是这四类资源每个单位的租金。最终要付的租金是:侧链运行时间 * 租赁的资源单位数 * 每个单位的租金
跨链索引费
ELF
每条侧链创建的时候都会在主链的CrossChain合约中,为侧链生成一个用于交索引费的账户,侧链的维护者需要持续为该账户充值ELF
只要试图让侧链和主链之间能够互相索引,就需要使用该模型。但是在侧链创建的时候,可以把索引费设置为0,这时候就意味着该侧链为公共侧链。
在主链的CrossChain合约中使用AdjustIndexingFeePrice为侧链调整索引费(通过这个方法进行调整的话,不能调整为0)。
Rented Resource表示BP为了运行该侧链,向侧链所有者出租了哪些资源,比如4 Core CPU、8G 内存、500G硬盘、1000M带宽,那么UpdateRentedResources的参数就应该为:
治理系统
虚拟地址
aelf治理系统始终围绕着虚拟地址(Virtual Address)展开。
关于虚拟地址的基本概念,在Association合约概述部分已有介绍。
简单地说,这个机制允许某个合约基于某个特定参数发送Inline交易,在该Inline交易执行过程中,Context.Sender表现为一个不存在私钥的地址,我们称之为虚拟地址。如果合约在发送该Inline交易之前,进行了一系列与权限或达成条件相关的判断,就意味着应用虚拟地址机制实现了合约执行条件控制。
以Parliament合约中创建的议会组织为例。
该组织的初始化是在Parliament合约的初始化方法中完成的,创建该组织的参数其实代表的是提案被该组织通过的参数设置:
MinimalApprovalThreshold
6667
提案通过需要至少66.67%的成员投同意票(Approve)
MinimalVoteThreshold
7500
提案通过需要至少75%的成员进行过表决
MaximalAbstentionThreshold
2000
提案通过允许至多20%的成员投弃权票(Abstain)
MaximalRejectionThreshold
2000
提案通过允许至多20%的成员投反对票(Reject)
以这个为参数创建的组织,会拥有一个属于该组织的虚拟地址(存储在Parliament合约的State.DefaultOrganizationAddress里)。
基于这个组织的虚拟地址:
实现一个该组织同意后才能使用该虚拟地址发送Inline交易的逻辑(Parliament合约的
Release方法):
放置一个该组织同意后才能执行后续逻辑的判断:
这样,如果试图执行成功上面的Assert语句之后的代码,就需要先对Parliament的默认组织发起提案,在提案通过后,使用Parliament合约的
Release方法,就可以基于虚拟地址parliamentDefaultOrganizationAddress成功发送Inline交易,从而得以执行后续的代码。
在aelf治理上,Association和Referendum合约的逻辑与Parliament合约相似:
为组织(提案参数)分配虚拟地址 -> 发起针对该虚拟地址的提案 -> 对提案表决,相应合约发现条件满足后,发出Inline交易 -> 在Inline交易中达成对需要判断权限的代码的调用。
ACS3
针对通过提案进行治理的方案,aelf提取了一个合约实现的标准,acs3,它包含以下几个主要接口:
CreateProposal:创建提案;
Approve:同意提案;
Reject:拒绝提案;
Abstain:弃权;
Release:释放提案。
Association合约、Parliament合约和Referendum合约都(借助虚拟地址的设计)实现了acs3。
投票竞选
在上文Election合约概述中,提到了aelf主网的节点选举其实就是在Vote合约中创建了一个投票项目,每个候选人的公钥作为该投票项目的选项,用户每抵押1个ELF,就相当于投了1票。
当aelf中需要某个代币的持币人从若干个选项中进行投票的话,都可以考虑使用Vote合约创建和维护投票项目。
Last updated