最佳实践
#
介绍本指南向用户介绍智能合约开发中需要关注的一些关键点,主要偏向于在实际开发中的实践. 用户可以通过本指南快速了解针对一笔交易如何合理的设置手续费, 如何避免被因交易失败 的同时损失手续费以及如何编码更加规范的智能合约.
#
费用合理设置当需要在 PlatON
的主网上部署合约时, 需要设置一个合理的费用限制. 费用限制是指
PlatON
中智能合约部署/执行的能源消耗成本的上限. 该限制主要通过 Gas
完成,
Gas
是 PlatON
网络世界的燃料值, 它决定了 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
所支付的单价.
用户发送一笔交易时, 会设定 GasLimit
和 GasPrice
, 二者的乘积(GasLimit *
GasPrice)是用户的交易成本, 同时该成本会作为佣金奖励给旷工.
交易设置的 GasPrice
越高, 则交易的执行优先级更高, 交易成本也会更大. 每笔交易在
完成后, 剩余未使用的Gas都会退回到发送者的地址账户中. 有一点要特别注意, 如果因为
GasLimit
设置过低导致交易执行失败, 此时的 Gas
不会被回退到用户地址, 用户依然
需要为这次失败的交易支付能量成本. 因此, 无论交易是否执行成功, 交易发送者都需要向旷工支付一定的计算费用.
LAT 单位转换
单位 | VON值 | VON |
---|---|---|
VON | 1 | 1 |
kVON | 1e3 VON | 1,000 |
mVON | 1e6 VON | 1,000,000 |
gVON | 1e9 VON | 1,000,000,000 |
microLAT | 1e12 VON | 1,000,000,000,000 |
milliLAT | 1e15 VON | 1,000,000,000,000,000 |
LAT | 1e18 VON | 1,000,000,000,000,000,000 |
kLAT | 1e21 VON | 1,000,000,000,000,000,000,000 |
mLAT | 1e24 VON | 1,000,000,000,000,000,000,000,000 |
gLAT | 1e27 VON | 1,000,000,000,000,000,000,000,000,000 |
#
避免超时在 PlatON
网络上发送交易, 没有超时的概念, 但是最终会根据所设置的 Gas 限制值停
止, 如果限制值低于合约部署所需要的消耗, 则交易发送失败, 同时会扣除对应的手续费.
手续费的设定不可能无限大, 因为在网络中, 区块本身有一个最大的 Gas 上限, 当交易的
GasLimit
超过该值时, 交易将无法被接收.
如果是针对已发布的合约执行 call
调用(call调用指合约逻辑内无状态变更操作), 存在
5s
超时的限制, 如果在 5s
内合约逻辑没有执行完成, 虚拟机会超时强制退出, 导致查询失败.
为避免部署合约交易失败, 请尝试将大型合约分成较小的块, 并根据需要相互引用. 为了避 免无限循环, 请注意常见的陷阱和递归调用.
#
非法操作处罚如果智能合约不是通过标准有效的编译器编译合约或者随意的更改指令码, 都会导致操作码
无效. 此类合约不仅无法部署和执行成功, 而且还会产生 GasLimit*GasPrice
的全额惩
罚, 当次交易的手续费会全部被扣除, 这是一个力度很大的惩罚, 如果操作者没注意该点,
不断重试, 那么付出的成本会更高, 代价更重.
一般产生无效操作码有以下情况:
- 对正常已编译出的合约手动更改了指令码
- 合约编译器版本与网络锁支持的合约版本不一致
- 使用浮点数运算
在 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_
声明为
constexpr
或const
的常量, 或在合约执行过程中其值始终保持不变的, 命 名时以 "k" 开头, 大小写混合. 例如:const int kDaysInAWeek = 7;
常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配:
MyExcitingFunctio()
,MyExcitingMethod()
,my_exciting_member_variable()
,set_my_exciting_member_variable()
枚举的命名应该和常量或宏一致:
kEnumName
或ENUM_NAME
如果你的命名实体与已有的C/C++实体相似, 可参考现有命名策略
#
智能合约文件组织文件组织规则:
- 一般超过1000行的程序代码就比较难以阅读, 尽量避免出现一个文件内代码行数过长的情 况. 每个合约文件应只包含一个单一的合约类或合约接口.
文件组织顺序:
- 文件注释: 所有合约源文件在开头有一个注释, 其中列出文件的版权声明, 文件名, 功能 描述及创建, 修改记录.
- 类或接口注释: 在类/接口定义之前应该进行注释, 包括类/接口的描述, 最新修改者, 版 本号, 参考链接等.
- 类成员: 首先是公共级别的, 随后是保护级别的, 最后是私有级别.
- 成员函数:合约内的函数应该按功能分组, 而不应该按作用域或访问权限进行分组.
#
特性使用建议#
结构体 vs. 类仅当只有数据成员时使用 struct
, 其他一概使用 class
.
在 C++ 中 struct
和 class
关键字几乎含义一样. 我们为这两个关键字添加我们自
己的语义理解, 以便为定义的数据类型选择合适的关键字.
struct
用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员
之外, 没有别的函数功能. 并且存取功能是通过直接访问位域, 而非函数调用. 除了构造函
数, 析构函数, Initialize()
, Reset()
, Validate()
等类似的用于设定数据成员的
函数外, 不能提供其它功能的函数.
如果需要更多的函数功能, class
更适合. 如果拿不准, 就用 class
.
#
继承使用组合常常比使用继承更合理. 如果使用继承的话, 定义为 public
继承.
所有继承必须是 public
的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式.
不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 "is-a" 的情况下使用继承: 如果 Bar 的确 "是一种" Foo, Bar 才能继承 Foo.
#
多重继承多重继承尤其成问题,因为它通常会带来更高的性能开销 (实际上, 从单继承到多重继承的 性能下降通常可能大于从普通派发到虚拟调度的性能下降), 并且由于存在导致 "diamond" 的风 险”的继承模式,容易产生歧义,混淆和彻底的错误。
强烈建议不要使用多重继承.
#
moveC++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}
#
autoauto
关键字能够通过初始化器自动推导其类型, 配合容器,迭代器使用能简化代码. 例如:
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()
.