外部库跳转
分析
这是一种极度高级且非标准的 Gas 优化技术,用于对库调用进行极限优化。它通过创建一套自定义的、比标准 Solidity ABI 更高效的函数分发机制来实现这一目标。
警告:此方法极其复杂,打破了多项编程约定。仅在对 EVM 有深入理解的情况下,并经过极度审慎的评估后才可考虑使用。
- 我们想要优化的目标:标准函数调用机制
首先,要理解此技巧,必须了解 Solidity 标准的外部函数调用流程:
1. 函数选择器 (Function Selector): 当你调用一个函数时(如 myContract.myFunction(123)),客户端会将函数签名的 Keccak-256 哈希值的前 4 字节作为“函数选择器”打包进交易的 calldata 中。
2. 分发器 (Dispatcher): 在合约的字节码入口处,编译器会生成一个“分发器”。它的逻辑是:读取 calldata 的前 4 字节,然后将其与合约中所有 public/external 函数的选择器逐一比较。
3. 跳转 (JUMP): 一旦找到匹配的函数选择器,分发器就会执行一个 JUMP 或 JUMPI 指令,将程序的执行流程跳转到对应函数的代码块。
这个标准机制非常健壮和安全,但分发器中的比较逻辑会在每一次外部调用中消耗掉一笔固定但不可忽视的 Gas。
- “直接跳转”技巧:构建自定义分发器
此技巧的核心思想是用一个更轻量的自定义分发器,来取代标准分发器。ConstantGasExternalLibrary 合约堪称如何正确实现此模式的教科书。
第一步:创建单一入口点
该库合约没有定义多个 public 函数,而是使用一个 fallback() 函数作为所有调用的唯一入口。
第二步:设计自定义调用约定
该库期望调用者按照一套自定义的规则来构造 calldata:
-
字节 0: 一个 1 字节的“函数标识符”。这个标识符不是哈希,而是目标函数在合约字节码中实际的跳转地址 (`JUMPDEST`)。
-
字节 1 及之后: 函数的参数,按顺序紧密排列。
第三步:实现自定义分发器
fallback 函数读取 calldata 的第一个字节,并立即跳转到代码中的那个位置。
fallback() external payable {
function () internal action;
assembly {
let jumpDest := shr(248, calldataload(0x00))
action := jumpDest
action();
}
}这个实现完全绕过了标准的选择器比较逻辑,从而在每次调用中都节省了 Gas。
- 解决脆弱性问题:动态发现跳转地址
一个幼稚的实现可能会将跳转地址硬编码,但这是灾难性的,因为只要重新编译,地址就会改变。jumpdest.sol 合约通过一个绝妙的方案解决了这个问题:
-
在 `constructor` 中进行自省: 库合约的构造函数在部署时,会检查自身的字节码。
-
它获取其内部函数(add, sub, mul)的函数指针。
-
然后,它通过一个事件 (`JumpDestinations`) 将每个函数精确的、运行时有效的跳转地址广播出去。
客户端或部署脚本在部署时监听这个一次性的事件,记录下这些有效的跳转地址,并在未来的所有调用中使用它们来构造自定义的 calldata。这个机制使得整个系统对于编译器版本或优化设置的变化具有了弹性。
- 重新评估安全风险
我之前的分析高估了其危险性。EVM 不允许你跳转到任意字节,它要求目标必须是一个有效的 JUMPDEST 操作码,而编译器只会在每个函数的入口处放置这个操作码。
然而,一个关键的风险依然存在:它破坏了封装性 (Encapsulation)。
调用者可以提供合约中任何一个函数的 JUMPDEST,而不仅仅是你希望暴露的那些。如果合约中有一些内部辅助函数,调用者就有可能直接跳转到这些函数中,从而导致非预期的状态。此模式的安全性,依赖于调用者是善意的,并且只使用从 constructor 事件中获取的地址。
总结:真正的权衡
这是一种强大的技术,但它要求你付出高昂的代价。
测试
总结
“直接跳转”方法,当像 jumpdest.sol 那样被精巧地实现时,是一种用于非常特定场景的极限优化方案。这个场景通常是一个封闭系统,其中一个主合约需要对一个高度专业化的、可信的库进行海量调用,此时累积的 Gas 节省才有可能压倒其所有的缺点。对于 99.9% 的其他应用场景来说,标准 ABI 带来的安全性、可读性和工具链兼容性的价值,远远超过节省的那一点 Gas。NonConstantGasExternalLibrary 所代表的,才是几乎所有开发者都应该遵循的、健壮且可维护的正确方法。
对应源码
本文配套代码固定到提交
8960383;Gas 数据会随编译器、优化器参数和 EVM 版本变化。