Substrate合约模块剖析

基本概念,substrate 合约与以太坊合约的一些联系与区别,上传合约代码 put_code 和实例化合约 instantiate 两个外部接口的实现。ChainX团队的这篇文章已经阐述过了: https://www.jianshu.com/p/ad1320ef9904

本文将介绍合约模块的第三个外部接口合约调用 call 的基本逻辑,并且会详细介绍下 substrate 关于合约收费的设计。

call :调用合约

首先是调用了账户查找lookup,返回给dest的是AccountId

接着主要是调用了bare_call这个函数,和instantiate一样走得是execute_wasmctx.call实际上调用的是 ExecutionContext::call

execute_wasm.png

ExecutionContext::call中,首先判断了调用深度,然后收取调用合约的gas费用,接着调用pay_rent收取存储空间的租用费用。代码在 srml/contracts/src/rent.rstry_evictpay_rent都是走try_evict_or_and_pay_rent。该函数涉及费用计算,因此在合约收费中介绍。支付rent费用后,若合约状态变更为tomestone,则直接返回错误信息。

如果value不为0的话,会调用transfervalue传给dest账户(不一定是合约账户 )。

然后调用nested.overlay.get_code_hash(account)得到合约账户对应的合约哈希,根据dest_code_hash执行合约。nested.loader.load_main(&dest_code_hash)会得到包含合约的调用接口callexecutableload_main调用了load_code,它会比较 schedule 的版本,之前在 put_code的最后是写入了两个存储,一个是原始代码,一个是原始代码预处理后的 prefab_module。如果当前版本大于已经预处理好的版本, 那么需要重新预处理,否则直接返回已经存储的 prefab_moduleload_init 最终返回 WasmExecutable 结构体 executable

然后与合约部署时一样,将返回的 executable放到 WasmVm 执行 execute。通过call执行完相应函数后,进行合约账户的余额检查,如果低于账户存在的最小额,将合约删除。

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

合约收费

存储收费

设计到存储收费的主要是在srml/contracts/src/rent.rs中的try_evict_or_and_pay_rent函数。

instantiate时,我们传入了endowment这个值,令合约账户中拥有资产。根据合约账户的balance大小,可以根据RentDepositOffset计算得到一个免费的存储空间大小。计算公式为free_storage = balance / RentDepositOffset

而如果合约大小超过了这个免费空间,那么需要按RentByteFee收费,该值的单位为每块每字节。所以需要支付的租用费用计算公式为:

rent = effective_storage_size(超过的空间) * RentByteFee * blocks_passed(距离上一次支付)
可见,如果一个合约资产不足以完全存储合约代码,随着时间的流逝,最终它的资产会减少到无法维持Alive状态,从而变更为Tombstone,甚至被直接删除(太久未被调用)。这是一个合理的设计,使得runtime中不会存储太多合约,存储空间得到充分利用。

在我们instantiate_contract时,默认设置rent_allowancebalance能支持的最大值。该值会随着每次支付rent而逐渐减少。所以实际上,设置rent_allowance这个值也可以限制合约需要的资产门槛。

状态为tombstone的合约,在后面可以用状态为Alive的合约retore_to进行替换。注意,当合约状态变更为tombstone时,合约数据已经被删除,所以用该方法对依赖数据的合约进行更新是不可行的。

操作的gas费用

写在开头:所有的gas费用,在操作调用一开始,就根据两者(操作时传入的gas_limit和链设定的gas_price)的乘积,从发起交易者的账户中收取了,然后存在gas_meter这个gas“管家”中。当操作完成时,剩余的gas会调用refund_unused_gas(...)进行返还。

接下来对不同的操作,进行gas费用收取的剖析:

  1. instantiate:根据schedule中设定的instantiate_base_cost

    impl<T: Trait> Token<T> for ExecFeeToken {
     type Metadata = Config<T>;
     #[inline]
     fn calculate_amount(&self, metadata: &Config<T>) -> Gas {
         match *self {
             ExecFeeToken::Call => metadata.schedule.call_base_cost,
             ExecFeeToken::Instantiate => metadata.schedule.instantiate_base_cost,
         }
     }
    }
    gas_meter.charge(self.config, ExecFeeToken::Instantiate)
    
  1. call:根据schedule中设定的call_base_cost

    gas_meter.charge(self.config, ExecFeeToken::Call)
    
  1. transfer:对不同的transfer_kind有不同的收取标准。费用标准在Config中设定,编写链的时候在impl contracts::Trait for Runtime{...}时写入相应的值。

    impl<T: Trait> Token<T> for TransferFeeToken<BalanceOf<T>> {
     type Metadata = Config<T>;
    
     #[inline]
     fn calculate_amount(&self, metadata: &Config<T>) -> Gas {
         let balance_fee = match self.kind {
             TransferFeeKind::ContractInstantiate => metadata.contract_account_instantiate_fee,
             TransferFeeKind::AccountCreate => metadata.account_create_fee,
             TransferFeeKind::Transfer => metadata.transfer_fee,
         };
         approx_gas_for_balance(self.gas_price, balance_fee)
     }
    }
    // 对不同操作进行gas收费
    if gas_meter.charge(ctx.config, token).is_out_of_gas() {
     return Err("not enough gas to pay transfer fee");
    }
    
Cause 收取标准
部署合约(Instantiate contract_account_instantiate_fee
Call时,dest账户不存在 account_create_fee
Call时,合约账户存在 transfer_fee

注意,InstantiateCall时创建的账户是不同的,Instantiate创建的是合约账户,Call创建的是普通账户。

另外,在instantiatecall中,都会调用transfer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容