本文主要介绍了,区块链应用为什么存在交易费用,Substrate 交易费用的组成,以及如何设计更合理的交易费用。
通过本文,你将学到:
在传统物联网(Web 2.0)时代,使用微信、微博、淘宝等互联网应用时,终端用户不需要直接费用,而是由服务提供方利用用户的个人信息、产生的数据、注意力等来变现,典型的方式有
在使用以上所说的 Web 2.0 服务时,用户数据的所有者是服务提供方。在过去的 20 年里,虽然我们享受了互联网应用带来的好处,但也时刻“品尝”着隐私泄露、数据主权丢失带来的恶果。
区块链应用将服务的各个组件完全透明化,用户数据的所有权归属于个人,而不是应用的开发者或者任何其他的第三方。用户通过持有私钥掌握着数据,只有持有私钥的人才可以解锁和转移数据,敏感数据往往可以通过加密防止被窃取。
任何区块链应用的出现和流行都离不开这些利益相关方:
“天下没有免费的晚餐”,终端用户在享受自由的应用服务同时,需要支付相应的服务费用,也就是交易费用,因为服务是由交易触发的。这些费用可以用来激励相关方更加有效的协作,从而提供更优质的服务。
交易费用的另一个目的是在网络和计算资源有限的条件下,高效地调节这些资源的利用率,而不至于被网络中的垃圾交易所浪费。
在不同的应用场景中,对资源消耗的成本估算不尽相同,合理地设计交易费用可以实现参与方的共赢,推动应用的普及。
如前文所述,交易费用的目的主要是:
注:本文不考虑通证的通胀和其它的激励措施。
节点和开发团队对交易费用分成,具体的比例由各方根据实际情况协调,并通过链上治理的方式进行动态的调整。
在区块链网络中,典型的资源和相应的费用设计方式如下:
Substrate 作为一个通用的区块链应用开发框架,充分考虑了上面提到的各种因素。Substrate 设计的交易费用由以下几部分组成:
总费用 = 基本费用 +(字节费用 + 权重费用)*(1 + 动态调节费率)+ 小费
用于支付的货币由 transaction-payment 模块的Currency类型指定,通常由Balances模块给出。
即TransactionBaseFee,是每笔交易(特例请参考下面,通过 pays_fee 设置无付费的交易)都需支付的费用,定义在 transaction-payment 模块中,在 runtime 初始化时进行配置,并可以随着 runtime 的升级进行更新。基本费用的合理设置,可以有效的减少垃圾交易,例如Kusama网络的基本费用目前设置为 0.01 ksm。
在处理区块大小的限制时,Substrate 引入了最大区块长度和字节费用,system 模块定义了最大区块长度(MaximumBlockLength),transaction-payment 模块定义了每字节的费用(TransactionByteFee),总的字节费用为:
字节费用 = 每字节费用 * 字节数
和基本费用相同的是,每字节费用也是配置在可升级的 runtime 代码中。字节数的计算是按照交易的结构体通过SCALE编码之后的长度,应用开发者无需过多的关注。以 Kusama 网络为例,相关的设置如下:
在有限的区块生成时间和链上状态的限制下,权重被用来定义交易产生的计算复杂度即所消耗的计算资源,以及占据的链上状态。system 模块定义了区块的总权重(MaximumBlockWeight)。为了保证在网络繁忙的情况下,依然能够实现对区块链应用有效合理的管理,Substrate 引入了两种不同级别的交易类型,既 Normal 和 Operational。Normal 类型的交易是由网络中的普通用户提交,Operational 类型的交易是由网络中的管理员或者管理委员会共同触发。区块资源如长度和总权重按照一定比例在这两种类型的交易中进行分配,这一比例称为可用区块比(AvailableBlockRatio)。Kusama 网络的设置为:
交易(也称为可调用函数)权重设置的四种方式为:
第一,缺省,使用权重的默认值 10,000,参考代码。
第二,设置固定权重值和交易级别,SimpleDispatchInfo定义了固定权重值的几种方式,
如何使用固定权重值,演示代码如下,完整代码请参考example pallet:
// 固定权重的Normal交易
#[weight = SimpleDispatchInfo::FixedNormal(10_000)]
fn accumulate_dummy(origin, increase_by: T::Balance) -> DispatchResult {
// --snip--
}
// 固定权重的Operational交易
#[weight = SimpleDispatchInfo::FixedOperational(2_000_000)]
fn accumulate_dummy(origin, increase_by: T::Balance) -> DispatchResult {
// --snip--
}
第三,自定义权重计算方法,根据可调用函数的参数进行动态计算,需要一个自定义的结构体,实现 WeighData
、 ClassifyDispatch
和 PaysFee
接口。
pays_fee
为 false,来避免收取任何交易费用(小费除外),适用于 Operational 交易存在权重值,但不收取交易费的场景。如何自定义一个权重计算方法,example pallet 中对应的演示代码为:
// The rules of `WeightForSetDummy` are as follows:
// - The final weight of each dispatch is calculated as the argument of the call multiplied by the
// parameter given to the `WeightForSetDummy`'s constructor.
// - assigns a dispatch class `operational` if the argument of the call is more than 1000.
struct WeightForSetDummy<T: pallet_balances::Trait>(BalanceOf<T>);
impl<T: pallet_balances::Trait> WeighData<(&BalanceOf<T>,)> for WeightForSetDummy<T>
{
fn weigh_data(&self, target: (&BalanceOf<T>,)) -> Weight {
let multiplier = self.0;
(*target.0 * multiplier).saturated_into::<Weight>()
}
}
impl<T: pallet_balances::Trait> ClassifyDispatch<(&BalanceOf<T>,)> for WeightForSetDummy<T> {
fn classify_dispatch(&self, target: (&BalanceOf<T>,)) -> DispatchClass {
if *target.0 > <BalanceOf<T>>::from(1000u32) {
DispatchClass::Operational
} else {
DispatchClass::Normal
}
}
}
impl<T: pallet_balances::Trait> PaysFee<(&BalanceOf<T>,)> for WeightForSetDummy<T> {
fn pays_fee(&self, _target: (&BalanceOf<T>,)) -> bool {
true
}
}
/// A type alias for the balance type from this module's point of view.
type BalanceOf<T> = <T as pallet_balances::Trait>::Balance;
如何使用一个自定义的权重计算方法:
#[weight = WeightForSetDummy::<T>(<BalanceOf<T>>::from(100u32))]
fn set_dummy(origin, #[compact] new_value: T::Balance) {
// --snip--
}}
第四,使用 Substrate 预定义的FunctionOf
结构体,适用于只有权重需要自定义进行计算,而交易级别固定的情况。FunctionOf 接收三个数据,a) 一个根据参数计算权重的 closure 表达式; b) 固定交易级别或计算交易级别的 closure; c) 设置 pays_fee
的布尔值。
如何使用 FunctionOf 结构体:
// weight = a x 10 + b
#[weight = FunctionOf(|args: (&u32, &u32)| args.0 * 10 + args.1, DispatchClass::Normal, true)]
fn f11(_origin, _a: u32, _eb: u32) { unimplemented!(); }
#[weight = FunctionOf(|_: (&u32, &u32)| 0, DispatchClass::Operational, true)]
fn f12(_origin, _a: u32, _eb: u32) { unimplemented!(); }
注意:合理的权重值需要通过性能测试来获取,可以参考PR Weight annotation;可调用函数的文档中也要明确给出复杂度的计算公式,有多少存储类操作等。
权重值需要转换为权重费用,transaction-payment 模块中给出了转换方式的定义WeightToFee,在 runtime 模块初始化时给出具体的实现代码,例如在 Kusama 网路,WeightToFee的实现为:
pub struct WeightToFee;
impl Convert<Weight, Balance> for WeightToFee {
fn convert(x: Weight) -> Balance {
// in Polkadot a weight of 10_000 (smallest non-zero weight) to be mapped to 10^7 units of
// fees (1/10 CENT), hence:
Balance::from(x).saturating_mul(1_000)
}
}
节点的 runtime 代码中,需要配置 TargetBlockFullness
参数,通常为 25%,即在网络平稳运行的过程中,区块资源的使用比例应该稳定在 25% 左右。当当前区块资源使用超过 25% 时,将下一区块动态调节费率设置为正,增加交易费用;当资源使用率不足 25% 时,将下一区块的动态调节费率设置为负,减少交易费用,鼓励交易的发生。这一规则的实现依赖 transaction-payment 模块的FeeMultiplierUpdate,Kusama 对应的实现代码请参考这里。
西方文化中一个特别之处是,当享用别人提供的优质服务时,会主动给出小费,这种思想也出现在 Substrate 的设计之中。和现实生活中的小费概念相同,它不是必须的,具体数量由交易发送者任意决定,并且完全由区块生产者获得,而交易费用的其它组成部分会根据一定的比例分配进入“国库”。
通过本文,你已经对交易费用有了基本的认识,以及如何合理地使用 Substrate 提供的交易费用规则。关于交易费用,已经有人在进行一些新的尝试,如:
官方文档:substrate.dev
Parity 介绍:parity.io
Substrate 源码:paritytech/substrate
Polkadot 源码:paritytech/polkadot
Relay-chain transaction fees and per-block transaction limits
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!