区块链工程师认证证书:substrate 合约模块简要剖析(一)

  本文主要介绍 substrate 合约模块的实现逻辑,srml/contracts 提供了部署和执行 WASM 智能合约的功能。作为一个模块化的区块链框架,不管是未来的波卡平行链还是基于 substrate 拥有独立共识的链,比如 ChainX, 只要引入其合约模块,就具备了合约功能,可以成为一个智能合约平台。ChainX 目前就计划引入合约功能,对区块链智能合约开发者提供支持, 欢迎有兴趣的同学持续关注。

  substrate 的合约模块将会分两篇文章进行解读,本篇主要介绍基本概念,substrate 合约与以太坊合约的一些联系与区别,还会介绍一下上传合约代码 put_code 和实例化合约 instantiate 两个外部接口的实现。合约模块一共有 3 个接口,第二篇将会介绍第三个外部接口合约调用 call 的基本逻辑,并且会详细介绍下 substrate 关于合约存储收费的设计。

  以下代码分析基于 substrate 的 9 月 21 日 4117bb9ff 版本。

  基本概念

  substrate 上的合约与以太坊合约有很多联系。首先普通账户和合约账户在外部表现上没有任何区别,都是一个哈希. 合约账户可以创建新的合约,也可以调用其他合约账户和普通账户。如果是合约账户调用普通账户,就是一个普通的转账。当合约账户被删除时,关联的代码和存储也会被删除。用户调用合约时,必须指定 Gas limit, 每次调用都需要花费 Gas 手续费, 合约内部调用的指令也会消耗 Gas.

  当然也有一些区别。以太坊在合约调用中,如果出现任何问题,整个状态都会回滚。但是在 substrate 的合约中如果出现了合约嵌套调用,比如合约 A 调用了合约 B, 合约 B 调用了合约 C,B 在调用 C 的过程中发生错误,那么只有 B 这一层的状态回滚,A 调用产生的状态修改仍然保留。当以太坊出现类似情况时,整个合约调用链的状态都会回滚,也就是 A 调用的状态修改不会保留,而是会被丢弃。另外除了 Gas 费用,substrate 的合约还有一个 rent 费用, 也就是对于合约存储也进行了收费. 以太坊虽然已经有个相关的 EIP 针对存储收费的讨论 EIP 103, 但是目前还没有实施。

  合约模块一共有三个与外部交互的接口:

  

  · put_code: 上传代码, 将准备好的 WASM 合约代码存储到链上, 如果执行成功,会返回一个 code_hash, 然后可以通过这个 code_hash 创建合约。先将代码存储到链上的好处是,对于合约内部逻辑相同而只有初始化参数不一样的合约,比如很多以太坊上的很多 ERC20 合约,链上只需要存储一份代码,而不需要每次新建一个合约的时候,都要存储一份重复的代码,这显然是冗余的。

  · instantiate: 实例化合约, 通过 put_code 返回的 code_hash 并传入初始化参数创建一个合约账户,实例化过程会调用合约内部的 deploy 函数对合约进行初始化,初始化只有一次。最近 substrate 将合约模块的实例化方法从之前的 create 重命名为了 instantiate, 见:PR: 3645。

  https://github.com/paritytech/substrate/pull/3645

  · call: 调用合约。在这里需要注意的是 substrate 有个存储收费的逻辑,如果调用的时候合约账户余额不足,合约就会被删除 evict, 很多人应该遇到过这种情况。

  put_code: 上传合约代码

  1. 调用 gas::buy_gas根据gas_limit预收取手续费。这一步是预先收取交易发起人的手续费。如果最后执行完成后,如果 Gas 没用完,会将剩余的 Gas 返还给用户。buy_gas 的代码在 srml/contracts/src/gas.rs。

  收取手续费=gas_price * gas_limit

  2.将代码存储到链上,调用 wasm::code_cache::save 执行存储代码的逻辑, save 代码位于 srml/contracts/src/wasm/code_cache.rs。

  

  在 save 中, 第一步是先收取 PutCode 操作的费用, 如果手续费不够直接返回。gas_meter 中就像是一个"Gas 小管家",这个管家管理的钱就是我们上一步预先收取的费用。在整个执行过程中,如果需要支付手续费,就从 gas_meter 中扣除,如果支付失败,直接返回。

  关于手续费收取标准,也就是 gas_meter.charge(..) 接受两个参数,一个是 Token trait 的关联类型 Token::Metadata 和实现了 Token 的 trait object, Token 有一个方法 calculate_amount 返应当收取的 Gas 费。srml/contracts/src/wasm/runtime.rs 中定义了一个枚举 RuntimeToken, 它实现了 Token trait, 针对不同的操作,收取不同的费用, 比如读内存,写内存,返回数据等等。在这里用到的 PutCodeToken(u32) 并不是 RuntimeToken 的成员,而是定义了一个元组结构体并实现了 Token 的 trait.

  第二步是调用 srml/contracts/src/wasm/prepare.rs 中的 prepare_contract 函数对上传的原始代码进行校验和做一些预处理,如果全部校验通过,那么就会存储到链上。在这里会校验:

  a. 入口函数是否存在:call, deploy

  b. 是否有定义内部存储

  c. 内存使用是否超过阈值

  d. 是否有浮点数

  第三步将校验通过的代码组装成一个结构体 PrefabWasmModule, 这个结构可以直接放到 WasmExecutable 里面, 然后写入存储。这里写入了两个存储,key 都是 code_hash, 一个是原始代码 original_code, 一个是 original_code 预处理后的 prefab_module.

  3. 返回剩余的 gas.

  instantiate: 创建合约

  通过 execute_wasm 构建 wasm 的基本执行过程。外部接口 instantiate 和 call 实际上都是要走 execute_wasm,粗线条来讲,execute_wasm 第一步还是根据 gas_price * gas_limit 收取手续费, 然后构造一个顶层的执行环境 ExecutionContext 执行 wasm ,根据执行结果判断是否写入状态,返还剩余 Gas, 执行延迟动作,这里的延迟动作包括对于 runtime 模块的方法调用,抛出事件, 恢复合约等。ExecutionContext 是一个主要的结构体。

  

  之所以会将 runtime 模块的方法调用放在最后执行,是因为目前的 runtime 模块中不支持状态回滚,这也是为什么目前所有 substrate 模块的写法都是先 verify, 各种 ensure!(...), 然后 write 写入存储, 因为一旦在 write 的过程中出现问题,已经 write 的部分状态已经改变,并且不可回滚。因此, 必须将所有的判断放在前面,保证所有判断通过,最后才执行写入动作。不过这个问题 substrate 已经在着手解决了,见: Substrate Issue: 2980, 估计再过一段时间应该就会支持 runtime 调用的状态回滚了。

  execute_wasm 本质上是要执行 ExecutionContext 的方法, 代码在 srml/contracts/src/exec.rs.

  pub struct ExecutionContext<'a, T: Trait + 'a, V, L> {

  pub parent: Option<&'a ExecutionContext<'a, T, V, L>>, // 是否有上层 context, 即是不是嵌套调用

  pub self_account: T::AccountId, // 合约调用者

  pub self_trie_id: Option, // 合约存储的 key

  pub overlay: OverlayAccountDb<'a, T>, // 对于state的改动, 这里只是一个临时的存储,只有当合约执行完成后才会写到链上

  pub depth: usize, // 合约嵌套深度

  pub deferred: Vec>, // 延迟动作,因为现在 runtime 是一个先 verify 然后 write 并且不可回滚的原因,所有对于 runtime 的调用必须等合约完全成功后才能调用 runtime 里面的东西。

  pub config: &'a Config,

  pub vm: &'a V, // WasmVm::execute()

  pub loader: &'a L, // WasmLoader::load_init(), WasmLoader::load_main()

  pub timestamp: T::Moment, // 当前时间戳

  pub block_number: T::BlockNumber, // 当前块高}

  ExecutionContext 有两个 public 方法对应两个外部接口的内部实现。

  · call: 合约调用逻辑

  · instantiate: 合约创建逻辑。

  在 ExecutionContext::instantiate 中,首先判断调用深度,然后收取实例化的费用,接着计算合约地址, 地址计算公式:

  

  合约地址=blake2_256(blake2_256(code) + blake2_256(data) + origin)

  code: 合约代码, blake2_256(code)就是 put_code 返回的 code_hash.

  data: 合约初始化参数

  origin: 合约创建者账户

  然后通过 nested.overlay.create_contract(..) 创建合约, overlay 的类型是 OverlayAccountDb, 所以实际上调用的是OverlayAccountDb::create_contract, 代码在 srml/contracts/src/account_db.rs.

  pub struct OverlayAccountDb<'a, T: Trait + 'a> {

  local: RefCell>,

  underlying: &'a dyn AccountDb,}

  

  创建合约这里主要是向合约默认值注入了两项内容,一个是 code_hash, 另一个是 rent_allowance, 这个 rent_allowance 会在之后收取存储费用的时候用到, 默认是最大值。

  然后刚刚创建好的合约账户进行 transfer 的动作, 紧接着 nested.loader.load_init(..) 加载合约的构造函数 delopy 进行初始化。loader 的类型是 WasmLoader, 也就是调用 WasmLoader::load_init, 代码在 srml/contracts/src/wasm/mod.rs。

  

  load_init 和 load_main 实际上都是调用的 load_code, 它会比较 schedule 的版本,还记得我们之前在 put_code的最后是写入了两个存储,一个是原始代码,一个是原始代码预处理后的 prefab_module. 如果当前版本大于已经预处理好的版本, 那么需要重新预处理,否则直接返回已经存储的 prefab_module。load_init 最终返回 WasmExecutable 结构体 executable。

  然后将返回的 executable 放到 WasmVm 执行 execute。WasmVm 实现了 Vm trait, 这个 trait 定义了 execute 方法,代码在 srml/contracts/src/wasm/mod.rs。execute 首先会在沙盒sandbox中开辟一段新的存储用于执行 wasm 代码. execute 在最后是构建一个 sandbox::Instance, 调用了 Instance 的 invoke 方法, 这部分代码在 core/sr-sandbox/src/lib.rs,

  

  core/sr-sandbox/src/lib.rs 中的 Instance::invoke 实际调用的是 srml/sr-sandbox/src/with_std.rs 或者 srml/sr-sandbox/src/without_std.rs 的 Instance::invoke。std 下调用的是 wasmi 库, wasmi::ModuleInstance 的 invoke_export.

  执行完 deploy 初始化以后,检查合约账户余额是否足够,如果低于账户存在的最小额,返回错误。

  如果一切顺利,OverlayAccountDb 进行 commit, 注意这里还没有正式写入存储。回到最外层的 execute_wasm, 如果这里执行正确,DirectAccountDb 进行 commit,这里才是真正写到存储里面。然后又是正常的返回剩余 Gas, 和执行延后的 runtime 调用等等。

  简单回顾一下,GasMeter 负责在合约执行过程中扣手续费,所有操作都是先收费. ExecutionContext是外部接口 instantiate 和 call 的具体执行环境。OverlayAccountDb 是合约执行过程的临时存储,用来支持合约回滚。DirectAccountDb 在合约最终执行完毕后,负责真正写入存储。以上就是上传合约代码和实例化合约的大概流程,下一篇会主要介绍合约调用,合约恢复以及合约存储收费的主要内容。

文章内容系本站作者个人观点,不代表本站对其观点赞同或支持,文章的版权归该作者所有。如需转载,请注明文章来源。本文地址:http://www.cis.net.cn/kejikuaixun/43348.html
留言与评论(共有 条评论)
验证码:

最新文章

substrate 合约模块简要剖析(一)

科技快讯
本文主要介绍substrate合约模块的实现逻辑,srml/contracts提供了部署和执行WASM智能合约的功能。作为一个模块化的区块链框架,不管是未来的波卡平行链还是基于substrate拥有独立共识的链,比如ChainX,只要引入其合约模块,就具备了合约功能,可以成为一个智能合约平台。ChainX目前就计划引入合约功能,对区块链智能合约开发者提供支持,

苹果推出支付请求API-实现Ripple的Interledger

科技快讯
苹果是世界上最大的公司之一,它推出的每一种产品都会给商业市场带来巨大的影响。最新消息表明,苹果决定为ApplePay系统引入一个支付请求API,该系统在Safari平台上实现了Ripple的Interledger。?苹果实现Interledger?Ripple是市场上最主要的加密货

区块链对国际汇款的影响

科技快讯
国际汇款也是一个很大的领域。2016年,生活在世界各地的移民发送超过了5,700亿美元到他们的祖国。然而,在这一领域有一些金融技术公司在激烈的竞争中幸存下来,其中包括TransferWise、InstaReM和OFX。不过,该领域仍由前三名

数字货币的隐私性

科技快讯
如今,隐私在数字货币中是一个重要主题,这已经不是什么秘密了。无论是公司还是个人都不希望将自己的所有信息发布到公共区块链上,因为这些信息可以在不受本国政府、外国政府、家庭成员、同事或商业竞争对手的任何限制下被任意读取。目前有很多实验和研究涉及区块链的各

CREDITS靠什么征战中国市场?

科技快讯
Сredits是一个开放的区块链平台,具有自主智能合约和内部加密货币。该平台旨在通过自执行智能合约和公共数据注册表为区块链系统创建服务。该平台每秒可以执行超过1,000,000个交易,执行速度为0.01秒。因为交易费用是很小的,所以高容量的应用程序是可能的,而其他的区块链都无法匹配。没有其他平台能提供与credit

蓝狐笔记:从货币载体的视角看比特币

科技快讯
前言:什么样的商品适合成为货币?货币最终是如何形成的?货币的本质是交换媒介,拥有最多交易机会的商品就会自发成为货币。而计价单位和价值存储并不是货币的内在属性,它是货币作为交换媒介的副产品。要成为货币,最核心的不是计价单位也不是价值存储,而是其可销售性,也就是如何获得最多交易机会。从这个角度,要成为货币的载体,还有很长的

区块链共识类型:消逝时间证明、权威证明、带宽证明

科技快讯
这篇文章我们将进一步深入探讨各种共识机制,最近这些机制引起了广泛的关注,并被证明是共识问题的有价值的继承者。这些算法在理论上运行得很好,但还没有付诸实践。消逝时间证明(ProofofElapsedTime):众所周知,在芯片制造商英特尔(Intel)创立之

如何应对区块链基础设施安全风险?

科技快讯
区块链技术提供了一种颠覆性的数据存储、传播和管理机制,已然成为全球科技和经济发展新热点。2019年10月,习近平总书记在主持中共中央政治局第十八次集体学习时强调,“要把区块链作为核心技术自主创新的重要突破口”“要加强对区块链安全风险的研究和分析”“探索建立适应区块链技术机制的安全保障体系”。区块链基础设施通过建

蜜蜂计划|警惕以“区块链”之名行诈骗之实

科技快讯
“蜜蜂计划”是由市地方监督管理局组织发起的金融消费者教育保护体系建设系列活动。“蜜蜂计划”将以北京市地方金融监督管理局为主导,联合各级金融监管部门、街道社区、金融机构、新闻媒体力量,以广大为服务对象,普及金融安全知识,倡导理性投资,构建多维度、多场景的金融消费者教育保护体系,将优