跳到主要内容

最佳实践

介绍#

本指南向用户介绍智能合约开发中需要关注的一些关键点,主要偏向于在实际开发中的实践. 用户可以通过本指南快速了解针对一笔交易如何合理的设置手续费, 如何避免被因交易失败 的同时损失手续费以及如何编码更加规范的智能合约.

费用合理设置#

当需要在 PlatON 的主网上部署合约时, 需要设置一个合理的费用限制. 费用限制是指 PlatON 中智能合约部署/执行的能源消耗成本的上限. 该限制主要通过 Gas 完成, GasPlatON 网络世界的燃料值, 它决定了 PlatON 网络生态系统的正常运行. 通 常使用 Gas 来衡量执行某些动作需要多少"工作量", 这些工作量就是为了执行该动作所 需要支付给 PlatON 网络的费用额度. 简单理解, Gas 是网络旷工的佣金, 并通过 LAT 的方式支付, 在网络上的任何交易, 合约执行, 数据存储, 都需要使用到 Gas.

PlatON 与以太坊区块链系统类似, 使用 LAT 进行支付和维护网络, 一枚 LAT 分为: mLAT/uLAT/gVON/mVON/kVON/VON, 其中VON是最小单位.

Gas 主要由两个部分组成: GasLimit(限制)和GasPrice(单价). 其中 GasLimit 是用户 愿意为执行某个操作或确认交易支付的最大 Gas 消耗量(最少21,000). GasPrice是 VON 的数量, 用于愿意为每个 Gas 所支付的单价.

用户发送一笔交易时, 会设定 GasLimitGasPrice, 二者的乘积(GasLimit * GasPrice)是用户的交易成本, 同时该成本会作为佣金奖励给旷工.

交易设置的 GasPrice 越高, 则交易的执行优先级更高, 交易成本也会更大. 每笔交易在 完成后, 剩余未使用的Gas都会退回到发送者的地址账户中. 有一点要特别注意, 如果因为 GasLimit 设置过低导致交易执行失败, 此时的 Gas 不会被回退到用户地址, 用户依然 需要为这次失败的交易支付能量成本. 因此, 无论交易是否执行成功, 交易发送者都需要向旷工支付一定的计算费用.

LAT 单位转换

单位VON值VON
VON11
kVON1e3 VON1,000
mVON1e6 VON1,000,000
gVON1e9 VON1,000,000,000
microLAT1e12 VON1,000,000,000,000
milliLAT1e15 VON1,000,000,000,000,000
LAT1e18 VON1,000,000,000,000,000,000
kLAT1e21 VON1,000,000,000,000,000,000,000
mLAT1e24 VON1,000,000,000,000,000,000,000,000
gLAT1e27 VON1,000,000,000,000,000,000,000,000,000

避免超时#

PlatON 网络上发送交易, 没有超时的概念, 但是最终会根据所设置的 Gas 限制值停 止, 如果限制值低于合约部署所需要的消耗, 则交易发送失败, 同时会扣除对应的手续费. 手续费的设定不可能无限大, 因为在网络中, 区块本身有一个最大的 Gas 上限, 当交易的 GasLimit 超过该值时, 交易将无法被接收.

如果是针对已发布的合约执行 call 调用(call调用指合约逻辑内无状态变更操作), 存在 5s 超时的限制, 如果在 5s 内合约逻辑没有执行完成, 虚拟机会超时强制退出, 导致查询失败.

为避免部署合约交易失败, 请尝试将大型合约分成较小的块, 并根据需要相互引用. 为了避 免无限循环, 请注意常见的陷阱和递归调用.

非法操作处罚#

如果智能合约不是通过标准有效的编译器编译合约或者随意的更改指令码, 都会导致操作码 无效. 此类合约不仅无法部署和执行成功, 而且还会产生 GasLimit*GasPrice 的全额惩 罚, 当次交易的手续费会全部被扣除, 这是一个力度很大的惩罚, 如果操作者没注意该点, 不断重试, 那么付出的成本会更高, 代价更重.

一般产生无效操作码有以下情况:

  1. 对正常已编译出的合约手动更改了指令码
  2. 合约编译器版本与网络锁支持的合约版本不一致
  3. 使用浮点数运算

PlatON 网络中操作合约时, 请务必先确认当前网络所支持的智能合约版本, 然后选择对应版本对的编译器.

常规操作是使用 PlatON 社区提供的最新的Truffle/PlatON-CDT来编译/部署/执行合约, 同时在切换到主网操作前, 务必在开发测试网进行有效的验证.

C/C++语言限制#

C/C++不支持的特性

  • 浮点数(float/double)
  • typeid/dynamic_cast(-fno-rtti)
  • try-catch(-fno-exeception)
  • C++17之后的特性

libc不支持的头文件

  • signal.h
  • math.h
  • locale.h
  • errno.h
  • uchar.h
  • time.h

libc++不支持的头文件

  • rand
  • atomics
  • thread
  • random

编程规范#

命名规范#

  • 函数命名, 变量命名, 文件命名要有描述性

  • 采用区块链行业内的术语

  • 尽可能少的使用缩写, 如果一定使用, 则推荐使用公共缩写和习惯缩写等

  • 文件名要全部小写, 可以包含下划线(_)或连字符(-)

  • WASM智能合约, 文件名与合约名保持一致

  • 类型名称的每个首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum

  • 变量(包括函数参数)和数据成员一律小写, 单词之间用下划线连接. 类的成员变量以下划 线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_memeber_

  • 声明为 constexprconst 的常量, 或在合约执行过程中其值始终保持不变的, 命 名时以 "k" 开头, 大小写混合. 例如:

    const int kDaysInAWeek = 7;
  • 常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunctio(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable()

  • 枚举的命名应该和常量或宏一致: kEnumNameENUM_NAME

  • 如果你的命名实体与已有的C/C++实体相似, 可参考现有命名策略

智能合约文件组织#

文件组织规则:

  • 一般超过1000行的程序代码就比较难以阅读, 尽量避免出现一个文件内代码行数过长的情 况. 每个合约文件应只包含一个单一的合约类或合约接口.

文件组织顺序:

  • 文件注释: 所有合约源文件在开头有一个注释, 其中列出文件的版权声明, 文件名, 功能 描述及创建, 修改记录.
  • 类或接口注释: 在类/接口定义之前应该进行注释, 包括类/接口的描述, 最新修改者, 版 本号, 参考链接等.
  • 类成员: 首先是公共级别的, 随后是保护级别的, 最后是私有级别.
  • 成员函数:合约内的函数应该按功能分组, 而不应该按作用域或访问权限进行分组.

特性使用建议#

结构体 vs. 类#

仅当只有数据成员时使用 struct, 其他一概使用 class.

在 C++ 中 structclass 关键字几乎含义一样. 我们为这两个关键字添加我们自 己的语义理解, 以便为定义的数据类型选择合适的关键字.

struct 用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员 之外, 没有别的函数功能. 并且存取功能是通过直接访问位域, 而非函数调用. 除了构造函 数, 析构函数, Initialize(), Reset(), Validate() 等类似的用于设定数据成员的 函数外, 不能提供其它功能的函数.

如果需要更多的函数功能, class 更适合. 如果拿不准, 就用 class.

继承#

使用组合常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.

所有继承必须是 public 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式.

不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 "is-a" 的情况下使用继承: 如果 Bar 的确 "是一种" Foo, Bar 才能继承 Foo.

多重继承#

多重继承尤其成问题,因为它通常会带来更高的性能开销 (实际上, 从单继承到多重继承的 性能下降通常可能大于从普通派发到虚拟调度的性能下降), 并且由于存在导致 "diamond" 的风 险”的继承模式,容易产生歧义,混淆和彻底的错误。

强烈建议不要使用多重继承.

move#

C++11引入的 std::move, 能有效的把资源转移给其他对象. 在我们的实践中, 使用 std::move 能有效减少 Gas 的消耗, 特别是使用容器的时候. 在返回值时, 应当返回 右值引用并使用 std::move 将左值引用转换为右值引用, 以减少 Gas 消耗. 例如:

std::vector<std::string>&& get_vec() {    std::vector<std::string> v;    // ignore    return std::move(v); // very important}
auto#

auto 关键字能够通过初始化器自动推导其类型, 配合容器,迭代器使用能简化代码. 例如:

std::map<std::string, std::string> my_map;for (auto it = my_map.begin(); it != my_map.end(); it++) {    // ignore}
引用参数#

建议所有函数参数都使用引用参数. 引用参数可以减少不必要的复制, 减少不必要的内存分 配,对于我们的WASM虚拟机来说, 内存分配是一个昂贵的操作.

容器#

C++标准库提供了一些常用的容器(map, vector, list等等), 在使用时应当仔细阅读对应的接口文档. 特别需要注意的是 map 的 operator[] 操作符, 根据接口文档说明, operator[] 当 key 不存在时, 会执行插入动作. 对应合约开发来说, 使用 StorageType 存储 map 时, 不要通过 operator[] 判断 key 是否存在, 而应该通过 find().