基于区块链技术框架的Substrate智能合约平台解析
扫描二维码
随时随地手机看文章
运行节点
1. 接入测试网
请参考加入 ChainX 测试网的相关说明:
完成相关配置后,应保证节点同步到最新,钱包相应配置完成。
2. 运行本地节点
请参考 ChainX Dev 模式的相关说明:
完成相应配置后,请保证已经出块超过150个区块,因为150个块后才会对 Alice 发放第一层次奖励。
若需要反复测试,可以对超过150个块后的区块数据进行备份,若需重新启动,则只需要删除老数据目录,使用备份数据目录替换即可。
ChainX 上的 Substrate Contracts 智能合约平台
Substrate 作为第一个区块链领域的技术框架,让开发者能够专注于链的运行时逻辑,而不用再花费大量的时间精力构建区块链底层的基础设施。此外, Substrate 默认提供了很多功能模块,比如 Staking, Consensus, 方便框架使用者根据自己的需求进行自由组合和定制。合约模块就是其中的一个功能模块,不管是任何一条基于 Substrate 技术的独立链,还是未来的平行链, 只要集成了合约模块,就可以成为一个智能合约平台。
本次 ChainX 智能合约平台的主要实现方式就是集成 Substrate 的合约模块,并进行适配。ChainX 的合约功能与 Substrate 默认的合约模块主要区别如下:
1. 取消合约存储收费的设计。 合约存储收费简单来说就是当合约部署到链上以后, 根据该合约在链上所占存储的大小和该存储的占用时间收取一定的费用,当合约账户因为余额不够无法支持存储费用时,合约就会被删除,甚至可能无法恢复。 即使合约被删除后可以恢复,目前的合约恢复可操作性也是极低,可能会对目前的合约开发造成极大的困扰。因此,我们目前决定暂时取消合约存储收费,只收取合约调用的 Gas 费用, 也就是与目前以太坊的收费设计一致。当合约存储收费的模型成熟后,可以重新启用这个设计。
2. 使用适配后的 chainx-org/ink 编写合约。 chainx-org/ink fork 自 paritytech/ink, 主要改动在于引入了 DefaultXrmlTypes 替代 DefaultRrmlTypes来适配 ChainX 的 runtime:
· 关联类型 Balance 从 u128 改为 u64.
· 适配关联类型 Call 以支持 PCX 转账和将跨链资产划转过去的 XRC20 Token 转回 ChainX 跨链资产, 合约开发者可以基于 PCX 和 ChainX 的跨链资产设计自己的 Dapp。
除了以上两点改动,在合约编写上与 paritytech/ink 保持一致,目前合约写法仍然采用 ink1.0 的写法,后续我们将会升级使用 ink2.0.
ink 合约编写相关资源:
· substrate-contracts-workshop: ink 官方教程
· polkaworld-org/workshop: polkaworld 合约相关技术讲座
与 Gas 相关的参数配置
Schedule {
version: 0, // 配置版本
put_code_per_byte_cost: 200, // 设置wasm代码时,每字节需要的Gas
grow_mem_cost: 1, // 单页中内存增值需要的Gas
regular_op_cost: 1, // 普通操作符Op需要的Gas
return_data_per_byte_cost: 1, // 返回值每字节消耗的Gas,因此对于合约见互相调用应仔细设计这个值
event_data_per_byte_cost: 20, // 合约中 event 每字节消耗的Gas
event_per_topic_cost: 1, // 合约中 event 每个topic消耗的Gas
event_base_cost: 1, // 合约中每个event要消耗的基础Gas,例如每打一个event就要先减少这个值的Gas
call_base_cost: 60000, // 调用合约或合约调用合约消耗的基础Gas,例如每调用一个合约函数需要减少这个值的Gas
instanTIate_base_cost: 200000, // 合约初始化(实例化)消耗的基础Gas
sandbox_data_read_cost: 1, // 合约中读取一个字节消耗的Gas
sandbox_data_write_cost: 1, // 合约中写入一个字节消耗的Gas
max_event_topics: 4, // 一次调用中最多可以打的event个数
max_stack_height: 64 * 1024, // 合约中栈的最大值
max_memory_pages: 16, // 合约执行中最大的页数
max_table_size: 16 * 1024, // 合约中最大数据结构表数
enable_println: false, // 是否允许合约中出现 print
max_subject_len: 32, // 最大的PRNG subject数
}
当前 ChainX 中默认的 Gas Price 为5
其中请留意put_code_per_byte_cost,call_base_cost,instanTIate_base_cost。因此在 ChainX 中:
· 设置 WASM 合约代码的代价比较大,因此建议合约开发者仔细设计及模块化自己的合约代码,不鼓励过多的设置合约代码
· 实例化一个合约的代价稍大,因此建议合约开发者尽量重用已实例化过的合约实例
· 调用一个合约的代价一般,因此建议合约开发者精简自己的合约,集中在一个合约中处理逻辑。
· Event 最多只能打4个,因此需要合约开发者小心设计 Event。
部署及调用合约
对于部署及调用合约,请参考 ChainX 的钱包相关部分:
· 合约功能部分
· 合约开发独立部署组件开发调试合约
开发调试合约
我们强烈建议:
1. 合约开发者在 ChainX Dev 节点下开发调试合约,因为很多错误信息只会在节点日志中出现。
2. 在 Dev 模式下可以在合约中使用env.println,而在主网和测试网中一定要将合约中的env.println删除。
3. 合约无论是执行还是方法的返回值都可以通过 RPC 在不打包的情况下调用,建议合约开发者可以先使用 rpc 调用模拟合约执行情况
一些异常错误
日志中与合约相关的日志均含有[runTIme|xrml_xcontracts]字段,因此若只关心合约及合约执行结果,只需要对日志 grep:
· tail -f log/chainx.log | egrep “xcontracts|apply_extrinsic”
若设置,部署,调用合约不成功,可以参考如下问题列表:
1. 设置合约代码相关
1. 合约部署时 gas limit 提供的不够
[runTIme|chainx_runtime::xexecutive|183L] [apply_extrinsic] failed: there is not enough gas for storing the code
因为 ChainX 中put_code_per_byte_cost设置得比较大,因此合约部署者应注意部署合约时提供的 gas limit 是否足够。一般情况下应大于len(wasm) * put_code_per_byte_cost
2. wasm 合约时,在组件间调用时因 wasm 过大,导致 wasm 被裁剪,部署不完整 wasm
[runtime|chainx_runtime::xexecutive|183L] [apply_extrinsic] failed: Can‘t decode wasm code
此时请检查组件调用是否会裁剪 wasm
3. wasm 合约已经存在于链上,对于相同的合约,不应该重复设置代码
[runtime|chainx_runtime::xexecutive|183L] [apply_extrinsic] failed: the code is already stored on chain
开发者自己编写工具时,请一定在上传代码前通过 sdk 的相关接口检查合约是否已经在链上。
4. 合约中含有ext.print,在自己测试能部署,在测试网与主网上不能部署
[runtime|chainx_runtime::xexecutive|183L] [apply_extrinsic] failed: module imports `ext_println` but debug features disabled
请注意在测试网和主网中,一定要将ext.print从合约中删除,或者使用条件编译控制
2. 部署合约代码相关
1. 合约实例已经存在,请勿重复实例化。
[runtime|chainx_runtime::xexecutive|183L] [apply_extrinsic] failed: Alive contract or tombstone already exists
在 ChainX 合约(Substrate Contracts)模型中,生成合约地址的方式为:
hash(code_hash + hash(input_data) + deployer)
因此相同的部署者对于相同的一份合约使用相同的实例化参数,最后的合约结果都是一样的。若需要用相同的 wasm 代码,相同的参数实例化另一个实例,请换一个账户
2. 参数传递错误,或者在实例化过程中崩溃,无法实例化
[runtime|chainx_runtime::xexecutive|183L] [apply_extrinsic] failed: during execution|Failed to invoke an exported function for some reason|wrong selector, decode params fail or inner error
请检查参数和合约代码,如:
· 实例化接收的参数是 u128,但是传递过去的是 u64
· 合约中的存储未初始化就进行加减操作/溢出/有 panic 异常。..
3. 部署达到了 gas limit
错误见下文
3. 调用合约代码相关
1. 合约调用或内部崩溃
[runtime|chainx_runtime::xexecutive|183L] [apply_extrinsic] failed: during execution|Failed to invoke an exported function for some reason|wrong selector, decode params fail or inner error
· 调用合约传递的 selector 不匹配,或参数编码错误
selector 请一定按照编译合约生成的的abi.json或者old_abi.json去调用,若使用不存在的 selector 则会调用不成功。编码错误同理
· 调用的方法中出现异常,如使用未初始化存储
struct Incrementer {
value: storage::Value《u32》,
}
impl Deploy for Incrementer {
fn deploy(&mut self) {
// not init value
}
}
impl Incrementer {
/// Flips the current state of our smart contract.
pub(external) fn inc(&mut self, by: u32) {
// 在日志中能看得到这一句日志
env.println(&format!(“Incrementer::inc by = {:?}”, by));
self.value += by; // 调用未初始化的存储
}
}
因此需要调试是因为参数 /selector 错误还是合约内部出错,请在合约内部打日志即可判定
· 溢出或 panic
具体请参考代码
2. 调用过程中达到了 gas limit
[runtime|chainx_runtime::xexecutive|183L] [apply_extrinsic] failed: during execution|Failed to invoke an exported function for some reason|reach gas limit
请通过 rpc 接口chainx_contractCall在非打包过程中去尝试得到适合的gas limit,建议覆盖合约执行中最远的执行分支。
通过 grep 日志:
[runtime|xrml_xcontracts::gas] [refund_unused_gas]|account:88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee (5SjUJPJJ.。.)|gas_payment:2500000|refund:0|real cost:2500000|gas_spent:500000
可以看到这次调用中的 gas 耗费,其中
注意该部分消耗指的是在合约中的 Gas 执行的消耗,真实支付的 PCX 还要再加上调用合约方法的外部手续费。
· gas_payment为gas_limit*gas_price的值,是调用者暂存的 PCX
· refund是执行完成后返回的 PCX
· real cost是真实消耗的PCX
· gas_spent是消耗的 gas
ChainX 智能合约中的跨链资产
当前 ChainX 智能合约中只支持比特币
ChainX 智能合约中多币种的实现方式
由于 Substrate Contracts 智能合约平台模型与以太坊智能合约模型相似,对于多币种的处理是“本币+代币”模型。
针对这种模型,ChainX 的智能合约上的跨链资产采用合约代币模型,因此在 ChainX 上 PCX 将会类似以太坊一样对智能合约提供 gas 及本币流通的价值,对于 ChainX 上的跨链资产以合约代币的形式引入。
使用代币模型而不是将多币种直接引入合约基于如下考虑:
1. 以太坊代币合约已经比较成熟,因此对于合约开发者在代币模型下可以比较容易的将以太坊合约迁移到 ChainX 智能合约平台上。
2. 若在合约中引入多币种的概念,将会极大修改 Substrate Contracts 的模型,容易引入非预期问题。
当前 ChainX 实现智能合约中的比特币采用 XRC20 模型,根据合约开发者的反馈,将来也可采用其他标准。目前已经考虑的代币模型有:
XRC20 (原以太坊 ERC20)
XRC721 (原以太坊 ERC721)
XRC777 (原以太坊 ERC777)
ChainX 资产中的多币种与合约中代币的转换
ChainX 上的比特币 X-BTC
当前 X-BTC 对应于智能合约平台中采用的代币模型为 XRC20。用户可以自由发起交易让 ChainX 的 X-BTC 与合约平台中的 XRC20 互相转换。
其中 ChainX 修改了 ERC20 合约标准(XRC20),添加了issue,destroy两个接口。并首先将一个合约部署到链上并实例化,同时在链上唯一指定了这个合约实例,因此除指定的实例以外的代币实例均不被 ChainX 的 Runtime 承认
其中:
· issue 只能被 Runtime 中的交易convert_to_xrc20调用,不可通过直接调用合约方法调用。通过交易convert_to_xrc20调用后,将会把该用户的资产转移到合约实例下,并在合约中对该发送交易的账户自动发放相应的金额。
· destroy 能被用户调用,用于将合约中的资产转移到 ChainX 资产模块中。其中首先销毁了合约中对应的代币,然后合约中会自动调用convert_to_assets将资产从合约中返回给用户,而convert_to_assets不可通过外部交易调用。
ChainX 上的 XRC20
XRC20 项目
部署于 ChainX 上的 XRC20 项目为:
https://github.com/chainx-org/xrc20
针对 XRC20,ChainX 已经提供了2个对应的 RPC 可进行操作:
· chainx_contractXRCTokenInfo
· chainx_contractXRC20Call
ChainX 钱包导入 XRC20 abi
当前钱包还未集成 XRC20 的 abi,因此需要开发者手动添加。
1. 获取链上指定的 XRC20-BTC 合约实例地址:
调用 rpc chainx_contractXRCTokenInfo 可在返回值中看到链上已经存在的 XRC20-BTC 合约地址公钥
2. 参考[合约功能部分|实例化合约]( wallet#2. 实例化合约(部署合约))部分的第3节,通过“添加已存在的合约”的方式,将该 ERC20-BTC 合约地址的公钥填入,然后在 XRC20 项目中的target/abi.json的 abi 文件上传即可添加该合约实例。
主网与测试网-道
对于主网与测试网-道,该针对 X-BTC 的合约实例已经部署且被设置完成。合约开发者或用户只需要在合约平台上导入该项目中的abi 文件target/abi.json即可调用合约。其中该XRC20的地址可以通过rpc chainx_contractXRCTokenInfo 获取到。
ChainX Dev 节点
对于 ChainX Dev 节点而言,该合约没有被内置。因此若需要调试和智能合约上的比特币相关操作,需要:
· 在 ChainX Dev 中设置 XRC20-BTC 合约
· 发放测试使用的假 X-BTC
在xrc20项目中已经提供了一个脚本执行这两件事情,详情请参考 README。
该脚本默认使用 Alice 的私钥(见 ChainX Dev 模式的开头部分)执行。请注意执行该脚本至少需要等待已经出了150个区块后( Alice 才会具备资产执行交易)。
若 PCX 不足,该脚本也提供了从 Alice 验证者领取奖池的操作。