上一期我们分享了来自本体技术团队的一篇文章的第一部分:本体技术视点 | 一文读懂Substrate的合约机制(一),分析了 Substrate 的合约机制。本期我们将继续围绕 Substrate 的合约存储的收租机制、Wasm 合约限制、合约对外部交易的接口等方面展开分析。
图片来源于网络
合约存储的收租机制
合约的收租考虑了不少功能点:
1. 合约账户余额比较多的可以免收一定的存储费用;
2. 为避免有些不用存储的合约收不到租金而一直留在链上占资源,设置了一个存储大小起步值;
3. 合约在不够支付租金后,清理存储,进入 tombstone 状态;
4. 检测合约是否要进入 tombstone 是需要由外部交易触发,因此成功举报的有奖励,出块人优先;
具体的实现由下面几个参数控制:
代码语言:javascript复制pub trait Trait: frame_system::Trait {
/// 合约进入tombstone状态时所需要的的最小充值余额
type TombstoneDeposit: Get<BalanceOf<Self>>;
/// 存储大小起步值
type StorageSizeOffset: Get<u32>;
/// 每个byte的存储费用,按每个区块计
type RentByteFee: Get<BalanceOf<Self>>;
// 豁免单个byte的存储需要的余额
type RentDepositOffset: Get<BalanceOf<Self>>;
/// 奖励给成功举报使某个合约进入tombstone的账户的额度,这个奖励是系统增发的。这个激励可以尽快地清理合约占用的资源。
type SurchargeReward: Get<BalanceOf<Self>>;
/// 系统给出块人举报某合约要进入tombstone状态的优先权,对于普通用户需要滞后的区块偏移量
type SignedClaimHandicap: Get<Self::BlockNumber>;
// ...
}
单个区块的租金计算公式如下:
代码语言:javascript复制fn compute_fee_per_block(
free_balance: &BalanceOf<T>,
contract: &AliveContractInfo<T>
) -> BalanceOf<T> {
// 免费的存储byte大小
let free_storage = free_balance
.checked_div(&T::RentDepositOffset::get())
.unwrap_or_else(Zero::zero);
// 对于value为空的情况,按1byte计算
let empty_pairs_equivalent = contract.empty_pair_count;
let effective_storage_size = <BalanceOf<T>>::from(
contract.storage_size T::StorageSizeOffset::get() empty_pairs_equivalent,
)
.saturating_sub(free_storage);
effective_storage_size
.checked_mul(&T::RentByteFee::get())
.unwrap_or_else(|| <BalanceOf<T>>::max_value())
}
Wasm 合约的限制
为了广泛的实用性,Wasm 规范有很多地方不能限制得太死,因此部署在区块链上的 Wasm 合约都需要进一步的限制,避免资源被滥用,造成 dos 攻击。contract 模块里定义了一个 Schedule 的结构来设置各种限制参数和上面提到的计费指令收费,提供了一个 update_schedule 方法,允许 root 用户对限制和计费参数进行更新。参数结构定义如下:
代码语言:javascript复制pub struct Schedule<T: Trait> {
/// 版本号,每次更新必须递增
pub version: u32,
/// 是否可以使用`seal_println`,一般只用于dev链
pub enable_println: bool,
/// 描述wasm合约本身的限制
pub limits: Limits,
/// 每个wasm指令执行的收费
pub instruction_weights: InstructionWeights<T>,
/// 每个导入函数调用的收费
pub host_fn_weights: HostFnWeights<T>,
}
pub struct Limits {
/// event允许的最大topic数目
pub event_topics: u32,
// 允许的wasm stack中元素个数的最大值
pub stack_height: u32,
// wasm合约允许声明的最大全局变量数
pub globals: u32,
// wasm函数允许的最大参数数量
pub parameters: u32,
/// 合约允许的最大内存页数,一页是64Kb
pub memory_pages: u32,
// 允许table中最大的元素个数,当前wasm的table中只允许放置funcref,用于间接调用。
pub table_size: u32,
/// br_table指令中允许的最大字面量值的个数
pub br_table_size: u32,
/// PRNG生成时的最大bytes数.
pub subject_len: u32,
/// 合约允许的最大code byte数
pub code_size: u32,
}
03
合约对外部交易的接口
合约代码的部署
为了合约代码的复用, substrate 采用部署 code 和实例化分开的方式,因此同一份代码可以实例化多个合约实例。合约的部署通过发送交易执行 put_code 完成。主要步骤如下:
1. 加载当前的 schedule ;
2. 根据 schedule 检查 code 是否满足限制要求;
3. 对合约进行预处理:插入 gas 计费指令和 stack 高度检查指令;
4. 根据原始 code 计算出 code hash ,保存原始的 code 和预处理之后的 code ;
合约的实例化
合约的实例化可以通过发送交易调用 instantiation 完成,函数签名如下:
代码语言:javascript复制pub fn instantiate(origin, endowment: BalanceOf<T>, gas_limit: Gas, code_hash: CodeHash<T>,
data: Vec<u8>, salt: Vec<u8>) -> DispatchResultWithPostInfo;
其中, endowment 表示要给生成的合约账户转账的额度, salt 用于合约地址的生成。实例化的过程如下:
1. 计算新合约的地址:hash(sender code_hash salt);
2. 生成新合约的 trie_id ,初始化新的合约账户;
3. 给新合约账户转账,额度为 endowment ;
4. 加载部署合约 code 时生成的预处理合约代码;
5. 以新合约的存储为上下文,以 data 为参数,执行合约的 deploy 方法;
6. 检查合约的余额是否少于 subsistence_threshold ,确保合约不会因为余额过低直接被删除;
合约的调用
通过发送交易执行 call 方法发起合约调用,签名如下:
代码语言:javascript复制pub fn call( origin,
dest: <T::Lookup as StaticLookup>::Source, //可以查询合约地址的参数
value: BalanceOf<T>, gas_limit: Gas, data: Vec<u8>) -> DispatchResultWithPostInfo ;
调用的过程如下:
1. 加载合约信息,确保是 alive 的;
2. 给合约转账,额度为 value ;
3. 加载预处理过的合约,调用其中的 call 函数。