计算Moonbeam上的交易费用¶
概览¶
与Moonbeam上用于发送转账的以太坊和Substrate API类似,Moonbeam上的Substrate和EVM也有不同的交易费用模型,开发者应知道何时需要计算和继续追踪其交易的交易费用。
首先,以太坊上的交易都会消耗gas,gas是根据交易的复杂性和数据存储需求计算得出的。与之相对,Substrate交易使用“weight”这个概念来计算交易费用。在本教程中,您将学习如何计算Substrate和以太坊的交易费用。在以太坊的部分您还会学到Moonbeam与以太坊在交易费计算上的关键差异。
Moonbeam与以太坊的主要差异¶
Moonbeam和以太坊的交易费计算模型有一些主要差异,开发者在开发Moonbeam时应当注意这些不同:
-
Moonbeam交易费计算模型中使用的gas是通过Substrate extrinsic weight计算得来。首先将Substrate extrinsic weight数值映射为 25000 的固定因子,计算得出交易的gas unit; 然后将该值与单位价格相乘来计算总gas费用。这个费用模型意味着通过以太坊API实现基本交易,比如转账,可能会比通过Substrate API更便宜。
-
与EVM不同,除gas之外Moonbeam交易还包含一些其他指标,其中很重要的一个就是proof size。Proof size是中继链验证节点验证Moonbeam state变换时所需的存储空间。当一个交易的proof size超过限制(区块proof size的25%)时,该交易将抛出“Out of Gas”错误(即便 gasometer 中还有剩余gas)。此附加指标还会影响交易的退款(refund)。Moonbeam的退款是根据交易执行后使用最多的资源计算得出,如果一个交易消耗的proof size大于残留的gas,则退款数额将基于proof size计算。
-
Moonbeam实现了MBIP-5中定义的一个新机制,该机制限制了区块能使用的存储上限,并且如果一个交易会造成存储数据增加,那它将需要支付更多gas。此功能目前仅在Moonbase Alpha上启用。
MBIP-5概述¶
MBIP-5 是一个为了更好应对网络存储增长而提出的关于Moonbeam交易费机制的改动。与以太坊不同,MBIP-5 通过提高特定交易的gas以及限制单个区块的储存总量来控制网络存储增长速度。
这个提案将影响以下三类交易:合约部署(导致链上state增加);创建新存储条目的交易;以及创建新帐户的预编译合约调用。
单个区块的存储增长限制为40KB,它定义了单个区块中所有交易造成储存量增长的上限。
储存单位(bytes)与gas的转换率为:
转化率 = 区块gas上限 / (区块储存上限 * 1024 Bytes)
鉴于Moonbase的区块gas上限为15,000,000,区块存储增长上限为40KB,gas与存储的比率为366,计算方法如下:
比率 = 15000000 / (40 * 1024)
比率 = 366
然后,您可以用交易的实际存储增长(以byte为单位)乘以gas与存储的比率,来计算该交易实际需要额外支付的gas单位。例如,如果执行交易使存储增加了 500 byte,则可以使用以下公式来计算额外gas
额外Gas = 500 * 366
额外Gas = 183000
我们可以通过在以太坊与Moonbeam分别部署两个不同的合约并且对比他们的gas预算来感受这个MBIP造成的主要影响,部署的两个合约一个修改链上的储存状态,另一个不修改。例如下面这个合约会在链上存储一个名字,然后使用这个名字来发送一个消息。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SayHello {
mapping(address => string) public addressToName;
constructor(string memory _name) {
addressToName[msg.sender] = _name;
}
// Store a name associated to the address of the sender
function setName(string memory _name) public {
addressToName[msg.sender] = _name;
}
// Use the name in storage associated to the sender
function sayHello() external view returns (string memory) {
return string(abi.encodePacked("Hello ", addressToName[msg.sender]));
}
}
你可以将这个合约部署到Moonbeam的测试网Moonbase Alpha, 以及以太网的测试网Sepolia, 或者直接与已部署好的合约交互:
0xDFF8E772A9B212dc4FbA19fa650B440C5c7fd7fd
0x8D0C059d191011E90b963156569A8299d7fE777d
接下来,您可以使用eth_estimateGas
方法来获取调用每个网络上的setName
和sayHello
函数的gas预估值。为此,您需要准备每个交易的bytecode,bytecode中包括了函数选择器,以及setName
函数的_name
参数。下面的实例将名称设置为“Chloe”:
0xc47f00270000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000543686c6f65000000000000000000000000000000000000000000000000000000
0xef5fb05b
现在, 您可以使用下面这个curl命令来获取Moonbase Alpha上的这个合约的gas预估值:
curl https://rpc.api.moonbase.moonbeam.network -H "Content-Type:application/json;charset=utf-8" -d \
'{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_estimateGas",
"params":[{
"to": "0xDFF8E772A9B212dc4FbA19fa650B440C5c7fd7fd",
"data": "0xc47f00270000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000543686c6f65000000000000000000000000000000000000000000000000000000"
}]
}'
curl https://rpc.api.moonbase.moonbeam.network -H "Content-Type:application/json;charset=utf-8" -d \
'{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_estimateGas",
"params":[{
"to": "0xDFF8E772A9B212dc4FbA19fa650B440C5c7fd7fd",
"data": "0xef5fb05b"
}]
}'
在Sepolia上, 您能够在data
使用同样的bytecode,您只需要修改RPC URL与合约地址:
curl https://sepolia.publicgoods.network -H "Content-Type:application/json;charset=utf-8" -d \
'{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_estimateGas",
"params":[{
"to": "0x8D0C059d191011E90b963156569A8299d7fE777d",
"data": "0xc47f00270000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000543686c6f65000000000000000000000000000000000000000000000000000000"
}]
}'
curl https://sepolia.publicgoods.network -H "Content-Type:application/json;charset=utf-8" -d \
'{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_estimateGas",
"params":[{
"to": "0x8D0C059d191011E90b963156569A8299d7fE777d",
"data": "0xef5fb05b"
}]
}'
在写这篇文章的时候, 这两个网络的gas预估值如下所示:
Method | Gas Estimate |
---|---|
setName |
45977 |
sayHello |
25938 |
Method | Gas Estimate |
---|---|
setName |
21520 |
sayHello |
21064 |
您会看到在Sepolia上,这两个调用的gas估计值非常相似,而在Moonbase Alpha上,这两个调用之间存在明显的差异,并且修改存储的setName
调用比sayHello
调用使用更多的 gas。
以太坊API交易费用¶
要计算通过以太坊API在Moonbeam交易产生的费用,可以使用以下计算公式:
GasPrice = BaseFee + MaxPriorityFeePerGas < MaxFeePerGas ?
BaseFee + MaxPriorityFeePerGas :
MaxFeePerGas;
Transaction Fee = (GasPrice * TransactionWeight) / 25000
Transaction Fee = (GasPrice * TransactionWeight) / 25000
Transaction Fee = (GasPrice * TransactionWeight) / 25000
以下部分更详细地描述了计算交易费用的每个组成部分。
基础费用¶
BaseFee
是在传送交易时被收取的最小费用,数值由网络本身设置。EIP-1559中引入的Base Fee
是由网络自设的一个值。Moonbeam有自己的动态费用机制计算基础费用,它是根据区块拥塞情况来进行调整。从runtime 2300(运行时2300)开始,动态费用机制已推广到所有基于Moonbeam的网络。
每个网络的最低gas价格(Minimum Gas Price)如下:
变量 | 值 |
---|---|
Minimum Gas Price | 125 Gwei |
变量 | 值 |
---|---|
Minimum Gas Price | 1.25 Gwei |
变量 | 值 |
---|---|
Minimum Gas Price | 0.125 Gwei |
要计算动态基本费用,请使用以下计算:
BaseFee = NextFeeMultiplier * 125000000000 / 10^18
BaseFee = NextFeeMultiplier * 1250000000 / 10^18
BaseFee = NextFeeMultiplier * 125000000 / 10^18
通过以下端点,可以从Substrate Sidecar API检索NextFeeMultiplier
的值:
GET /pallets/transaction-payment/storage/nextFeeMultiplier?at={blockId}
Sidecar的pallet端点返回与pallet相关的数据,例如pallet存储中的数据。您可以在Sidecar官方文档中阅读更多关于pallet端点的信息。需要从存储中获取的手头数据是nextFeeMultiplier
,它可以在transaction-payment
pallet中找到。存储的nextFeeMultiplier
值可以直接从Sidecar存储结构中读取。读取结果为JSON对象,相关嵌套结构如下:
RESPONSE JSON Storage Object:
|--at
|--hash
|--height
|--pallet
|--palletIndex
|--storageItem
|--keys
|--value
相关数据将存储在JSON对象的value
键中。该值是定点数据类型,因此实际值是通过将value
除以10^18
得到的。这就是为什么BaseFee
的计算包括这样的操作。
GasPrice,MaxFeePerGas和MaxPriorityFeePerGas¶
GasPrice
为用于指定在EIP-1559前遗留交易的Gas价格。MaxFeePerGas
和MaxPriorityFeePerGas
在EIP-1559与BaseFee
一同出现。MaxFeePerGas
定义了允许支付以Gas为单位的最大费用,为BaseFee
和MaxPriorityFeePerGas
的总和。MaxPriorityFeePerGas
为由交易的传送者配置的最大优先费用,用于在区块中激励优先处理该交易。
尽管Moonbeam与以太坊兼容,但它的核心还是基于Substrate的链,并且优先级在Substrate中的工作方式与在以太坊中不同。在Substrate中,交易并不按Gas价格确定优先顺序。为了解决这个问题,Moonbeam使用了修改后的优先级系统,该系统使用以太坊优先的解决方案重新确定Substrate交易的优先级。Substrate交易仍会经历有效性过程,在此过程中会为其分配交易标签、寿命和优先级。然后,原始优先级将被基于每Gas交易费用的新优先级覆盖,该费用源自交易的小费和权重。如果交易是以太坊交易,则根据优先费设置优先级。
值得注意的是,优先级并不是负责确定区块中交易顺序的唯一组件。其他组件(例如交易的寿命)也在排序过程中发挥作用。
适用交易类型的GasPrice
, MaxFeePerGas
和MaxPriorityFeePerGas
的值可以根据Sidecar API页面描述的结构从Block JSON对象读取,特定区块中以太坊交易的数据可以从以下区块端点中提取:
GET /blocks/{blockId}
相关值的路径也被截短后复制在下方:
EVM字段 | JSON对象字段 |
---|---|
MaxFeePerGas | extrinsics[extrinsic_number].args.transaction.eip1559.maxFeePerGas |
MaxPriorityFeePerGas | extrinsics[extrinsic_number].args.transaction.eip1559.maxPriorityFeePerGas |
EVM字段 | JSON对象字段 |
---|---|
GasPrice | extrinsics[extrinsic_number].args.transaction.legacy.gasPrice |
EVM字段 | JSON对象字段 |
---|---|
GasPrice | extrinsics[extrinsic_number].args.transaction.eip2930.gasPrice |
交易权重¶
TransactionWeight
是一类Substrate机制,用于衡量给定交易在一个区块内执行所需的执行时间。对于所有交易类型,TransactionWeight
可以在相关extrinsic的事件下获取,其中method
字段设置如下:
pallet: "system", method: "ExtrinsicSuccess"
随后,TransactionWeight
将被映射至Block JSON对象的以下字段中:
extrinsics[extrinsic_number].events[event_number].data[0].weight
费用记录端点¶
Moonbeam网络实施eth_feeHistory
JSON-RPC端点作为对EIP-1559支持的一部分。
eth_feeHistory
返回一系列的历史gas信息,可供您参考和计算在提交EIP-1559交易时为MaxFeePerGas
和MaxPriorityFeePerGas
字段设置的内容。
以下curl示例将使用eth_feeHistory
返回从各自Moonbeam网络上的最新区块开始的最后10个区块的gas信息:
curl --location \
--request POST 'INSERT_RPC_API_ENDPOINT' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_feeHistory",
"params": ["0xa", "latest"]
}'
curl --location \
--request POST 'INSERT_RPC_API_ENDPOINT' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_feeHistory",
"params": ["0xa", "latest"]
}'
curl --location \
--request POST 'https://rpc.api.moonbase.moonbeam.network' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_feeHistory",
"params": ["0xa", "latest"]
}'
curl --location \
--request POST 'http://127.0.0.1:9944' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_feeHistory",
"params": ["0xa", "latest"]
}'
计算交易费用的示例代码¶
以下代码片段使用Axios HTTP客户端来为最终区块查询Sidecar端点/blocks/head
。随后,根据交易类型(以太坊API:legacy、EIP-1559或EIP-2930标准以及Substrate API)计算区块中所有交易的交易费用,以及区块中的总交易费用。
以下代码示例仅用于演示目的,代码需进行修改并进一步测试后才可正式用于生产环境。
您可以将以下代码片段用于任何基于Moonbeam的网络,但您需要相应地修改baseFee
。您可以参考基本费用部分以获取每个网络的计算结果。
import axios from 'axios';
// This script calculates the transaction fees of all transactions in a block
// according to the transaction type (for Ethereum API: legacy, EIP-1559 or
// EIP-2930 standards, and Substrate API) using the dynamic fee mechanism.
// It also calculates the total fees in the block
// Endpoint to retrieve the latest block
const endpointBlock = 'http://127.0.0.1:8080/blocks/head';
// Endpoint to retrieve the latest nextFeeMultiplier
const endpointPallet =
'http://127.0.0.1:8080/pallets/transaction-payment/storage/nextFeeMultiplier?at=';
// Endpoint to retrieve the node client's information
const endpointNodeVersion = 'http://127.0.0.1:8080/node/version';
// Define the minimum base fee for each network
const baseFee = {
moonbeam: 125000000000n,
moonriver: 1250000000n,
moonbase: 125000000n,
};
async function main() {
try {
// Create a variable to sum the transaction fees in the whole block
let totalFees = 0n;
// Find which Moonbeam network the Sidecar is pointing to
const responseClient = await axios.get(endpointNodeVersion);
const network = responseClient.data.clientImplName;
// Retrieve the block from the Sidecar endpoint
const responseBlock = await axios.get(endpointBlock);
// Retrieve the block height of the current block
console.log('Block Height: ' + responseBlock.data.number);
// Due to a current bug, use the previous block's base fee
// to match the on-chain data
// Find the block's nextFeeMultiplier
const prevBlock = Number(responseBlock.data.number) - 1;
const responsePallet = await axios.get(endpointPallet + prevBlock);
// Iterate through all extrinsics in the block
responseBlock.data.extrinsics.forEach((extrinsic) => {
// Create an object to store transaction information
let transactionData = new Object();
// Set the network field
transactionData['network'] = network;
// Filter for Ethereum Transfers
if (
extrinsic.method.pallet === 'ethereum' &&
extrinsic.method.method === 'transact'
) {
// Iterate through the events to get non type specific parameters
extrinsic.events.forEach((event) => {
if (
event.method.pallet === 'ethereum' &&
event.method.method === 'Executed'
) {
// Get Transaction Hash
transactionData['hash'] = event.data[2];
}
if (
event.method.pallet === 'system' &&
event.method.method === 'ExtrinsicSuccess'
) {
// Add correction weight if needed to Transaction Weight!
transactionData['weight'] = BigInt(event.data[0].weight.refTime);
}
});
// Get the transaction type and type specific parameters and compute the
// transaction fee
if (extrinsic.args.transaction.legacy) {
transactionData['txType'] = 'legacy';
transactionData['gasPrice'] = BigInt(
extrinsic.args.transaction.legacy.gasPrice
);
transactionData['txFee'] =
(transactionData['gasPrice'] * transactionData['weight']) / 25000n;
} else if (extrinsic.args.transaction.eip1559) {
transactionData['txType'] = 'eip1599';
transactionData['maxFeePerGas'] = BigInt(
extrinsic.args.transaction.eip1559.maxFeePerGas
);
transactionData['maxPriorityFeePerGas'] = BigInt(
extrinsic.args.transaction.eip1559.maxPriorityFeePerGas
);
// Update based on the network you're getting tx fees for
transactionData['baseFee'] =
(BigInt(responsePallet.data.value) * baseFee.moonbeam) /
BigInt('1000000000000000000');
// Gas price dependes on the MaxFeePerGas and MaxPriorityFeePerGas set
transactionData['gasPrice'] =
transactionData['baseFee'] +
transactionData['maxPriorityFeePerGas'] <
transactionData['maxFeePerGas']
? transactionData['baseFee'] +
transactionData['maxPriorityFeePerGas']
: transactionData['maxFeePerGas'];
transactionData['txFee'] =
(transactionData['gasPrice'] * transactionData['weight']) / 25000n;
} else if (extrinsic.args.transaction.eip2930) {
transactionData['txType'] = 'eip2930';
transactionData['gasPrice'] = BigInt(
extrinsic.args.transaction.eip2930.gasPrice
);
transactionData['txFee'] =
(transactionData['gasPrice'] * transactionData['weight']) / 25000n;
}
// Increment totalFees
totalFees += transactionData['txFee'];
// Display the tx information to console
console.log(transactionData);
}
// Filter for Substrate transactions, check if the extrinsic has a
// 'TransactionFeePaid' event
else {
extrinsic.events.forEach((event) => {
if (
event.method.pallet === 'transactionPayment' &&
event.method.method === 'TransactionFeePaid'
) {
transactionData['txType'] = 'substrate';
transactionData['txFee'] = event.data[1];
transactionData['tip'] = event.data[1];
}
if (
event.method.pallet === 'system' &&
event.method.method === 'ExtrinsicSuccess'
) {
transactionData['weight'] = event.data[0].weight.refTime;
}
});
}
});
// Output the total amount of fees in the block
console.log('Total fees in block: ' + totalFees);
} catch (err) {
console.log(err);
}
}
main();
Substrate API交易费用¶
本教程假设您通过Substrate API Sidecar服务与Moonbeam区块交互。也有其他与Moonbeam区块交互的方式,例如使用Polkadot.js API library。检索区块后,两种方式的逻辑都是相同的。
您可以参考Substrate API Sidecar页面获取关于安装和运行自己的Sidecar服务实例,以及如何为Moonbeam交易编码Sidecar区块的更多细节。
请注意,此页面信息假定您运行的是版本14.1.1 的Substrate Sidecar REST API。
所有关于通过Substrate API发送的交易费用数据的信息都可以从以下区块端点中提取:
GET /blocks/{blockId}
区块端点将返回与一个或多个区块相关的数据。您可以在Sidecar官方文档上阅读有关区块端点的更多信息。读取结果为JSON对象,相关嵌套结构如下所示:
RESPONSE JSON Block Object:
...
|--number
|--extrinsics
|--{extrinsic_number}
|--method
|--signature
|--nonce
|--args
|--tip
|--hash
|--info
|--era
|--events
|--{event_number}
|--method
|--pallet: "transactionPayment"
|--method: "TransactionFeePaid"
|--data
|--0
|--1
|--2
...
对象映射总结如下:
交易信息 | JSON对象字段 |
---|---|
Fee paying account | extrinsics[extrinsic_number].events[event_number].data[0] |
Total fees paid | extrinsics[extrinsic_number].events[event_number].data[1] |
Tip | extrinsics[extrinsic_number].events[event_number].data[2] |
交易费用相关信息可以在相关extrinsic的事件下获取,其中method
字段设置如下:
pallet: "transactionPayment", method: "TransactionFeePaid"
随后,将用于支付此extrinsic的总交易费用映射至Block JSON对象的以下字段中:
extrinsics[extrinsic_number].events[event_number].data[1]
| Created: September 15, 2022