eosio.forum 智能合约开发教程第三期: 源代码的深度解析

本文为 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 中的的一列:

  • proposer 指的是创建提案的账户;
  • proposal_name 指的是 proposals 这个表的主键,这是一个提案的唯一标识;
  • title 指的是类型为字符串,长度少于 1024 的提案标题;
  • proposal_json 指的是类型为 JSON 格式的字符串,用以表示提案的具体描述,格式需要遵循 Proposal JSON Structure Guidelines 的规范;
  • expires_at 定义了投票的截止期限,这个时间点需要是 action 执行的时间到未来 6 个月之间

一旦某个提案创建了,只要在 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 的字段来表示:

  • 0 代表拒绝票
  • 1 代表同意票
  • 255 代表弃权票
  • 其他值可以用来表示其他含义

在 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 团队为本期教程的编写提供的诸多帮助!

发布了76 篇原创文章 · 获赞 2 · 访问量 9828

猜你喜欢

转载自blog.csdn.net/weixin_43891115/article/details/102908804