ACS1 Method Fee收取过程
概述
ACS1对用户收取手续费的方式有以下特点:
交易费来自两处:通过这笔交易的大小(tx size)计算出一个数额,称之为Size Fee;开发者通过实现acs1.proto中提供的GetMethodFee方法,为具体的方法指定一个数额,称之为Base Fee。开发者可以通过后者把某个方法设置为免收Size Fee(IsSizeFeeFree字段),如果此时Base Fee也设置为0,则意味着这个方法可以免费调用。
全局可以设置哪些代币允许作为手续费(目前只有ELF,可以通过MultiToken合约的SetSymbolsToPayTxSizeFee方法修改)。
对于通过tx size计算出手续费的过程,其计算方程的系数(coefficients)存储在Token合约中,但是其计算过程放在链下,并不在合约内。
其实一开始是放在合约里的,但是发现如果方程过于复杂,会明显拖慢交易执行速度,于是做了一个链下环境和合约环境配合的机制。
每个aelf节点都会完成数值计算,并与真正收取交易费的交易的执行结果进行对比,如果区块中实际收取的交易费与本地计算结果不一致,则会拒绝同步这个区块。
手续费终归要汇总到一个地址中,但是如果要在每一个交易执行过程中都把手续费落实进这个收费账户,那么这些交易就都无法并行执行了,因此交易费的收取分为三个步骤:
先从用户账户中扣除手续费,抛出一个收取手续费的事件;
在一个区块执行结束后,汇总这些手续费事件,计算出手续费;
在生成下一个区块的时候,将计算好的手续费作为参数,生成一个用来收手续费的系统交易,在这个系统交易中,上一个区块所收取的手续费才会打入收费账户中。
使用Pre-plugin收取用户交易费 - ChargeTransactionFees交易
下面是使用交易Pre-plugin插件生成一个ChargeTransactionFees交易作为某个交易的前置交易的过程。
前置交易体现在PlainTransactionExecutingService。ExecutePluginOnPreTransactionStageAsync这个方法调用的位置。
下图的function的参数取自Excuted Data,这是一些通过执行合约后,能够持久保存在StateDb中的数据(会指定一个key,方便未来获取)。
function本身是一个多项式的结构,是一系列的(a/b)x^c相加,只需要提供若干组(a, b, c),就可以构造出来这个多项式。

ChargeTransactionFees方法逻辑
ChargeTransactionFees方法的主线任务有两步:
统计账单(bill变量);
基于账单扣除原交易Sender的手续费,如果扣费成功,则返回成功,否则返回收费失败和失败原因:
Transaction fee not enough.。
其中,bill的类型为TransactionFeeBill:
生成bill的过程也是分两步:
使用
Context.Call调用原交易要调用的合约的GetMethodFee方法,得到合约开发者设定的针对原交易MethodName的交易费信息methodFees,类型为MethodFees:
然后根据methodFees往账单bill中添加Base Fee。
如果methodFees.IsSizeFeeFree不为true,就进一步计算原交易的大小,得到Size Fee,加入bill。
顺便提及一个新feature:在bill计算完成后,实际收费之前,会根据用户剩余免费额度,重新调整bill。
费用计算公式
前文提到,费用计算的公式(function)的参数是(以交易的形式)执行合约方法后设置进Executed Data中的。要了解参数设置的时机,需要了解aelf系统中的一个事件:BlockAcceptedEvent。
这个事件是在FullBlockchainExecutingService.ExecuteBlocksAsync方法中,也就是在同步一个区块的过程中,抛出的。抛出这个事件的时候,指定的区块已经同步完毕,aelf系统试图通过这个事件,通知其他订阅者执行区块同步完成之后,其他模块做出相应响应的逻辑。这里的其他模块的订阅者,其中之一就是TransactionFeeCalculatorCoefficientUpdatedLogEventProcessor,用来监听刚刚完成同步的区块中,是否存在费用计算公式更新的事件。
更新公式参数的方法有两个:
UpdateCoefficientsForSender:更新ACS1的size fee的计算公式
UpdateCoefficientsForContract:更新ACS8的资源币费用的计算公式
这两个方法的执行都会抛出CalculateFeeAlgorithmUpdated事件。
另外一个与手续费收取相关的订阅者是TransactionFeeChargedLogEventProcessor,用来监听ChargeTransactionFees方法中抛出的TransactionFeeCharged事件,下一节中就会展开介绍它。
与其他*LogEventProcessor一致,TransactionFeeCalculatorCoefficientUpdatedLogEventProcessor也是筛选区块自己感兴趣的事件(就是代码里重写GetInterestedEventAsync方法),并从每一个筛选到的事件中提取信息,做出处理。这里筛选的是CalculateFeeAlgorithmUpdated事件,这个事件里面就包含了function的新的参数:
每一个CalculateFeePieceCoefficients,对应前文说过的(a/b)x^c的一组(a, b, c);
每个function由多组(a, b, c)组成,得到CalculateFeeCoefficients;CalculateFeeCoefficients的fee_token_type只是作为区分function的标识;
AllCalculateFeeCoefficients就是所有function的参数了。
在TransactionFeeCalculatorCoefficientUpdatedLogEventProcessor.ProcessLogEventAsync中,解析function们的参数,通过CalculateFunctionProvider.AddCalculateFunctions方法,作为Executed Data最终放进StateDb中(先缓存起来,随着lib持久化进StateDb)。
汇总交易手续费 - ClaimTransactionFees交易
在ChargeTransactionFees过程中,只是将手续费从原交易的Sender名下扣除,还没有统一打入某一个账户中。ClaimTransactionFees用于统计上一个区块中通过ACS1收取到的所有手续费。这里展开说一下与ClaimTransactionFees交易相关的两个问题,其一是这个交易的生成,其二是汇入收费账号的方式(主侧链不一样)。ClaimTransactionFees交易是作为系统交易生成的。在BP生产区块的时候,都会执行一遍ClaimFeeTransactionGenerator.GenerateTransactionsAsync,生成一个ClaimTransactionFees交易,作为nonCancellableTransactions之一,打包进区块中。这个交易的详情:
From:BP的地址
To:MultiToken合约的地址
MethodName:ClaimTransactionFees
Params:上个区块中通过ACS1收取到的费用的汇总,是TotalTransactionFeesMap类型:
获取TotalTransactionFeesMap实例是另一段故事。与上一节提到的TransactionFeeCalculatorCoefficientUpdatedLogEventProcessor类似。前置区块中,收取手续费的事件是TransactionFeeCharged:
TransactionFeeChargedLogEventProcessor用于批量地处理该事件:取出区块中的每一个TransactionFeeCharged事件,从中提取出每个事件指示的收取的手续费,放进totalTxFeesMap中;totalTxFeesMap就是一个TotalTransactionFeesMap类型的实例。最终,将上面得到的totalTxFeesMap通过TotalTransactionFeesMapProvider.SetTotalTransactionFeesMapAsync方法放进Executed Data中。
作为BP,就通过TotalTransactionFeesMapProvider.GetTotalTransactionFeesMapAsync,在ClaimFeeTransactionGenerator里生成ClaimTransactionFees这个系统交易(见ClaimFeeTransactionGenerator);
作为非BP,也通过TotalTransactionFeesMapProvider.GetTotalTransactionFeesMapAsync,来验证新区块中收取的手续费跟自己本地计算的结果是否一致(见ClaimTransactionFeesValidationProvider)。
最终根据MultiToken合约是部署在主链上还是在侧链上,决定手续费的去向(MultiToken合约的TransferTransactionFeesToFeeReceiver私有方法)。
如果是主链,则通过Treasury合约的Donate方法,打进Treasury分红池;
如果是侧链,直接Transfer给提前设置好的FeeReceiver(用
SetFeeReceiver方法设置)。
总结
用一个例子作为总结。
我的一笔Transfer交易被打包在了高度为116301529的区块(点击Transactions选项卡,最后一个就是);
这个Transfer交易抛出了一个TransactionFeeCharged事件,与此同时,手续费已经从我的账户中扣掉了:
这个TransactionFeeCharged事件会在高度116301529的区块被同步完成后,交由TransactionFeeChargedLogEventProcessor处理,生成一个totalTxFeesMap,放进Executed Data中;
BP在生产高度116301530的区块的过程中,从Executed Data取出来totalTxFeesMap,生成了一笔ClaimTransactionFees交易,这笔交易的参数就是totalTxFeesMap:
随着ClaimTransactionFee交易的打包和执行,我的那笔交易的手续费就算收取完成了。
Last updated