白皮书 · 13
性能分析
参考实现在零个、一个和两个钩子下的实测 Gas;成本由什么驱动;以及部署指南。
方法
以下数据来自 scripts/gas-report-standard.js,在 Hardhat 的进程内网络上针对已编译的参考实现运行(solc 0.8.22、viaIR,ARexToken 使用 optimizer.runs = 1),时间为 2026 年九月。“热”指与上一次转移使用相同的发送方/接收方/id 组合(存储槽在该区块中已被访问过)。钩子:EligibilityHook(CHECK + ELIGIBILITY)和 ExpiryHook(CHECK)。可用以下命令复现:
npx hardhat run scripts/gas-report-standard.js
结果
xychart-beta
title "按钩子数量统计的热转移 Gas(单位:千)"
x-axis ["无钩子", "资格钩子", "资格钩子 + 到期钩子"]
y-axis "Gas(千)" 0 --> 140
bar [66.8, 91.2, 119.6]成本随钩子数量线性增长:每个钩子对每个 id 增加一次外部 check 调用,资格钩子还要再加上 canSend 和 canReceive。该表给出了每一项操作的成本。
| 操作 | 0 个钩子 | 1 个钩子 | 2 个钩子 |
|---|---|---|---|
createBatch(新 id,铸造 1000) | 264,096 | 283,831 | 299,396 |
safeTransferFrom(首次,冷) | 80,687 | 108,266 | 136,682 |
safeTransferFrom(热) | 66,790 | 91,166 | 119,582 |
transform 拆分 1 → 2(两个新 id) | 564,422 | 595,707 | 636,858 |
setFrozenTokens | 54,188 | 54,188 | 54,188 |
forcedTransfer | 80,152 | 89,909 | 99,569 |
burn | 66,621 | 83,001 | 93,395 |
每种钩子各装一个时的热转移(规则的边际成本):
xychart-beta horizontal
title "单个钩子在热转移上的边际 Gas(单位:千)"
x-axis ["DeliveryHook", "Traceability(发出)", "EligibilityHook", "ExpiryHook", "SeasonalHook", "PerishabilityHook"]
y-axis "新增 Gas(千)" 0 --> 35
bar [12.3, 17.4, 24.4, 25.2, 27.5, 31.0]持久化模式下的 TraceabilityHook 增加 184.5k,为保持图表比例而未列入图中。
| 钩子 | Gas | 相对无钩子的 Δ |
|---|---|---|
| EligibilityHook | 91,166 | +24,376 |
| ExpiryHook | 92,023 | +25,233 |
| SeasonalHook | 94,324 | +27,534 |
| PerishabilityHook | 97,813 | +31,023 |
| DeliveryHook(无进行中的交付) | 79,076 | +12,286 |
| TraceabilityHook,仅发出事件 | 84,152 | +17,362 |
| TraceabilityHook,持久化 | 251,273 | +184,483 |
成本由什么驱动
- 没有钩子的转移在热状态下约花费 67k,即 ERC-1155 加上供应量跟踪,再加上对批次存在性/状态/冻结的读取以及管线的簿记(数据暂存、锁)。这是下限。
- 每个钩子在一次转移上大约增加 25–30k:一次
check调用(视图、跨合约),对于资格钩子还要加上canSend和canReceive调用。成本与钩子数量以及批量转移中的 id 数量成线性关系。若在调用每个规则合约之前,先用supportsInterface对每种能力逐一探测,则每个 id 会增加若干次静态调用;由于标志在注册时已被缓存,这一开销并不存在。 createBatch的成本主要由存储决定:Batch结构体(六个字加一个字符串)以及 ERC-1155 的余额和供应量存储槽。这是每个批次一次性的成本。transform会创建新的 id:拆分为两个需要支付两条批次记录、两次铸造和一次销毁,外加对每一笔变动的钩子调用(OP_TRANSFORM)。拆分为 n 个大致相当于n × createBatch加一次销毁;把 n 个合并为一个大致相当于n次销毁加一次 createBatch。- 强制执行成本很低:冻结只涉及一个存储槽;强制转移的成本等于一次转移的成本,加上解冻写入和事件。
transferCheck/canTransfer是视图函数:在链下调用免费,并且与交易走同一条代码路径。
该设计带来了什么
| 能力探测式设计 | A-REX Core(ARexToken) | |
|---|---|---|
| 含 n 个规则合约的转移 | 在任何逻辑运行之前,需要 id 数 × 模块数 ×(supportsInterface 探测 + 钩子调用) | id 数 × 钩子数 × 1 次调用(资格钩子另加 2 次);标志在注册时缓存 |
| 拒绝 | 字符串回滚(复制到内存中,不可索引) | 带有钩子地址和 bytes32 代码的自定义错误 |
| 谱系 | 不可用 | 每个输出批次约 280k |
| 冻结 / 强制转移 | 不可用 | 54k / 80k |
左列描述的是 A-REX Core 所取代的设计(ADR-004、ADR-005);只有右列是实测的。
部署指南
- 以金额计的成本是
gas × gas price × token price,而后两项在不同的链之间、不同的日子之间会相差数个数量级。仅作示例:在 0.01 gwei、每个 ETH 3,000 USD 的条件下,带两个钩子的热转移(119,582 Gas)约花费 0.0036 USD,一次createBatch(264,096 Gas)约花费 0.008 USD;在 Ethereum 主网上按 20 gwei 计算,同样的操作约花费 7.2 USD 和 15.8 USD。在承诺采用某个单位经济模型之前,应当在目标链上进行测量。 - 批量转移:
safeBatchTransferFrom摊薄了固定成本;钩子成本仍随 id 数量而增长。 - 钩子排序:把拒绝率高、成本低的钩子放在前面;一次拒绝会在更昂贵的钩子运行之前终止管线。
- 钩子数量:三到五个即可满足大多数签发方;每增加一个钩子,都会带来可测量的每次转移成本和一个治理面。
- 元数据:保持
uri简短(一个 CID 或一个 Digital Link);该字符串存储在链上。 - 链下读取:提交之前先使用
transferCheck;回滚的交易仍然会消耗 Gas。
吞吐量
瓶颈不在代币本身,而在链。一次转移就是一笔交易;产生 n 个输出的一次转化,是包含 n 次铸造的一笔交易。索引谱系需要读取 BatchTransformed 事件;这些事件每次操作发出一次,并带有完整的数量列表,因此索引器无需从逐笔转移事件中重建任何内容。
合约大小
ARexToken 部署后为 24,136 字节(上限 24,576),使用 optimizer.runs = 1 编译。其他合约都很小:ARexAttestations 为 2,708;钩子介于 1,358(ExpiryHook)和 6,215(DeliveryHook)之间,TraceabilityHook 为 4,076;扩展介于 3,716 和 4,588 之间。向代币本身添加功能在设计上就受到大小的限制——这正是钩子和配套合约存在的原因。