本文为 dfuse 与 EOS Studio 合作内容,原文由 EOS Studio 发布
我们将在近一段时间内陆续推出多期的系列教程,深度详解一些开源的 EOS 智能合约项目。我们将仔细挑选那些内容优质、设计精心、并可以成功构建的合约示例,其中的一些已经在 EOS 主网上广泛使用。通过本次系列教程,我们希望能为 EOSIO 上的 dApp 开发者提供更多的学习资料,并帮助他们了解更多智能合约的设计模式和应用场景。
在第一期和第二期中,我们讨论了智能合约 eosio.forum 的设计动机,以及进行提案和投票的基本流程。本篇教程中,我们将会带大家一起阅读源码,解析技术细节,理解这些智能合约的具体运行原理。
eosio.forum 合约可以大致分成四个部分:proposal (提案),vote (投票),status (状态),post (发布和回复)。所有的 actions (调用合约的方法) 和 tables (表) 都放在 forum 这个类里面。源码中定义的 action 和 table 我们都提供了指向源代码的链接,方便您在需要时快速查阅和引用。
整个 proposal 部分包含了一个名为 proposals 的 table 以及一些用来运行这个 proposals 的 actions:
ACTION propose(eosio::name proposer, eosio::name proposal_name, string title, string proposal_json, eosio::time_point_sec expires_at)
TABLE proposals { eosio::name proposal_name; // primary key eosio::name proposer; // secondary key string title; string proposal_json; eosio::time_point_sec created_at; eosio::time_point_sec expires_at; }
所有的账号都可以通过执行 propose() 来创建一个新的提案。通过一些必要的参数检查后,新的提案将会被存储在 proposals 的表里,其中所消耗的 RAM 将从提案者 proposer 的账户中扣除。每个 action 中的参数都对应着 table 中的的一列:
一旦某个提案创建了,只要在 expires_at 的时间内,任何账户(包括提案者 proposer 自己)都可以通过 vote() 的这个 action 来投票。
ACTION expire(eosio::name proposal_name)
这个 action 允许提案者 proposer 提前结束他/她的提案,并且立刻终止投票。这个操作的本质实际上是把 expires_at 字段修改成了当前的时间。
proposal_table.modify(itr, proposer, [&](auto& row) {
row.expires_at = current_time_point_sec();
});
只有初始提案者 proposer 才可以通过调用 expire() 来提前结束提案。如果在一个不存在的,或者已经结束的提案中调用此 action,系统会返回错误信息。
ACTION clnproposal(eosio::name proposal_name, uint64_t max_count)
当一个提案的冻结时间超过 3 天后(通过 FREEZE_PERIOD_IN_SECONDS 来设置这个时间),我们就可以通过这个 action 来移除该提案。
bool can_be_cleaned_up() const { return current_time_point_sec() > (expires_at + FREEZE_PERIOD_IN_SECONDS); }
操作 clnproposal() 将清除与这个提案相关的所有投票记录。由于每次删除的数量由 max_count 来限制,因此这个操作采用不断迭代的方式,通过多次调用这个 action ,直到所有的投票记录被删除。
auto index = vote_table.template get_index<"byproposal"_n>();
auto vote_key_lower_bound = compute_by_proposal_key(proposal_name, name(0x0000000000000000)); auto vote_key_upper_bound = compute_by_proposal_key(proposal_name, name(0xFFFFFFFFFFFFFFFF));
auto lower_itr = index.lower_bound(vote_key_lower_bound); auto upper_itr = index.upper_bound(vote_key_upper_bound);
uint64_t count = 0; while (count < max_count && lower_itr != upper_itr) { lower_itr = index.erase(lower_itr); count++; }
请注意,二级索引 byproposal 是用来通过 proposal_name(请查看 vote 表) 来查询和迭代所有给定投票记录的,一旦所有相关的投票记录都被移除,提案本身也将会被删除。
if (lower_itr == upper_itr && itr != proposal_table.end()) {
proposal_table.erase(itr);
}
这个操作可以有效清理提案以及它的投票记录所占用的所有 RAM 资源。任何人都可以调用 clnproposal() 这个 action,因为这个操作仅接受对已经完成且冻结期结束的提案执行。我们鼓励所有投票者、提案者以及社区的成员调用 clnproposal() 来清理过期提案,减少 RAM 资源的占用。
投票部分包含了一个名为 vote 的 table 以及 vote() 和 unvote() 两个 action。
ACTION vote(eosio::namevoter, eosio::nameproposal_name, uint8_t vote, string vote_json)
ACTION unvote(eosio::namevoter, eosio::nameproposal_name)
TABLE vote { uint64_t id; // primary key eosio::name proposal_name; // secondary key eosio::name voter; // secondary key uint8_t vote; string vote_json; eosio::time_point_sec updated_at; }
对于尚未截止的提案,任何账户都可以使用 vote() 来进行投票,这将会消耗投票者的少量 RAM 资源 (430 字节) ,用以把投票信息保存在 vote 表中。
vote 表中具体含义由 vote 的字段来表示:
在 vote 表中,主键 id 是自动生成的。二级索引 proposal_name 和 voter 是为了使用 proposal 或者 voter 字段进行搜索,而 vote_json 字段则是用来记录一个投票记录的额外信息,例如可以记录投票人投票时的一些看法等。
投票人可以通过再次调用 vote() 来修改他/她的投票,或者调用 unvote() 来把他/她的投票记录从 vote 表中删除。移除有效的投票可以拿回存储这个投票记录所占用的 RAM 资源,相应的,这个投票记录将不再被这个提案所记录。
vote() 和 unvote() 这两个 action 会首先检查其提案是否处于投票阶段,如果一个提案已经超过了投票期限,和投票相关的操作将会被系统拒绝。
bool is_expired() const { return current_time_point_sec() >= expires_at; }
因此,当一个提案的投票阶段结束后,系统可以保证所有投票记录不能被修改,大家便可以着手清点和计算投票结果了。
ACTION status(eosio::name account, string content)
TABLE status { // scope is self eosio::name account; // primary key string content; eosio::time_point_sec updated_at; }
status() 会记录与之关联的 account 账号的状态,如果参数 content 为空,这个 action 会移除之前的状态;如果不为空,将会在 status 表中新增或者修改这个 account 账号所对应的状态记录。
ACTION post(eosio::name poster, string post_uuid, string content, eosio::name reply_to_poster, string reply_to_post_uuid, bool certify, string json_metadata)
ACTION unpost(eosio::name poster, string post_uuid)
我们也可以通过 post() 和 unpost() 来发布帖子和回复,不过这两个 action 仅验证参数,并不会将数据存在数据库中。因此所有的帖子和回复内容都不会保存在 RAM 中,他们只能通过链上的交易记录来查看。因此,需要一些链下工具来为 post() 和 unpost() 这两个 action 的数据提供排序、展示、计数和统计报告等服务。例如,Novusphere 按照他们的数据格式,为用户提供了一个有用户界面的应用来展示和分类帖子。他们使用 eosio.forum 合约作为后端服务,并提供了一个基于 EOSIO 的类似 Reddit 的网页应用。
如果您觉得本教程有帮助,请别忘了点赞或关注我们的微信公众号黑曜石实验室 (Obsidianlabs),币乎号 EOSStudio,我们会持续更新更多的产品信息、技术文章和精彩内容。
深度解析 EOS 合约:eosio.forum
- 第一部分: EOSIO 公投系统
- 第二部分: 投票的流程解析
- 第三部分: 源代码的深度解读
非常感谢 dfuse 团队为本期教程的编写提供的诸多帮助!