本文提出了Signal-Boost,一种使现有rollup能够与以太坊主网实现同步组合性的方法,无需成为基于L1的rollup。它通过请求前推送模型,允许rollup查询L1状态并将其推送到SignalService合约,结合EIP-7702智能账户和顶部区块执行,从而实现L1数据的实时访问,并解锁了诸多L1和L2之间的新用例。
_该团队的研究重点是 Fabric 和 Commit-Boost。感谢 EF Research、Nethermind、Espresso、Taiko、OpenZeppelin、Spire、ETHGas、Gattaca、L2Beat、Labrys、Luban、Puffer 和 Interstate 的个人提供的反馈、贡献和审查。反馈、贡献和审查并不意味着认可。_
Nethermind 的研究通过两条链上的配对消息合约实现 same-slot L1→L2 消息传递。这允许 rollup 在同一 slot 内对某些 L1 操作(例如 L2 存款)做出反应。这足以实现同步和原子的跨链捆绑,“例如从 L1 存入 ETH,在 L2 上将其换成 USDC,然后提取回 L1。”
重要的是,他们得出了一个关键的见解,即“L2 提议者不必是 L1 构建者。”这意味着这些交互不需要基于排序,而是需要 L2 和 L1 之间一定程度的协调。
但是,它有一个主要的限制:L2 无法读取任意的 L1 状态,只能读取已写入 L1 消息合约的内容。
Gwyneth 提出的 Ultra transactions 实现了 L1 和Based Rollup 之间的 SC。
XCALLOPTIONS
预编译启用了无缝跨域调用,包括在 L2 域中模拟和执行 L1“元交易”的能力,从而实现了同步的 L1->L2 调用。Ultra transaction 模型的两个关键见解是:
Same-slot 消息传递本质上是 push-based:L1 必须显式地将数据写入 SignalService
合约,以便 L2 在同一 slot 中读取它。相比之下,ToB 执行启用了 pull-based 访问,其中 L2 可以同步读取任意的 L1 状态——但只能在区块的最开始,限制了用例。
在 EIP-7814 等提案生效或对技术栈进行彻底修改以支持 L1 元交易之前,push-based signaling 是在区块中期执行期间访问当前 L1 数据的最佳方式。
与 L1 的 SC 要求 rollup 读取未确认的 L1 状态,而不是已确认或最终确定的状态。从未最终确定的 L1 数据构建其状态的 Rollup 受到 L1 重组风险的影响,即先前消耗的 L1 输入可能不再存在,从而导致相关的 L2 状态发生变异,他们必须接受或选择回滚 rollup 的状态。
为了缓解这种情况,大多数 rollup 故意滞后于 L1 head。例如,OP Stack 使用 4 个区块的 SequencerConfDepth
,而其他 rollup 则等到 L1 最终确定。此缓冲区有助于 L1 输入在消耗前稳定,使 rollup 能够提供更可信的预确认。但是,引入任何滞后都与 L1 SC 根本不兼容。
这不一定是一个二元决策,而是一个范围:
Signal-Boost 的目标是该范围的中间,以便在 L1 重组风险合理时允许按需 SC,但默认选择风险较低的选项。
我们的目标是让 L2 实时接收并响应 L1 状态。Same-slot 消息传递通过允许 L2 在同一 L1 slot 中消耗 L1 上写入的数据来实现这一点。但这仅适用于显式写入 SignalService
合约的 L1 数据。
这造成了一个关键的限制:像 Chainlink 或 Uniswap 这样的协议需要主动将其数据推送到 L2,这需要更改其合约逻辑。
Signal-Boost 通过采用 request-before-push 模型来解决此问题。任何人都可以查询任意的 L1 view 函数,并以可验证的方式将结果推送到 SignalService
合约,而不是要求 L1 合约发出信号。在实践中,这在 L2 排序器完成时尤其有用。
通过解锁对实时 L1 数据的访问,而无需更改上游合约,这大大提高了 same slot 消息传递的效用。但是,它带来了新的挑战:如果信号在 slot 中期发生更改会发生什么?L2 用户如何响应或验证?
为了解决这些问题,Signal-Boost 结合了 Ultra transactions 的思想,包括 EIP-7702 智能帐户用于委托执行,以及 ToB 包含用于状态保证。
SignalBoost
合约为了使 same-slot 消息传递更容易采用,我们需要一种将 L1 数据转换为 L2 可以信任的可验证信号的方法。SignalBoost
合约通过允许任何人查询 L1 view 函数并将结果以下游 L2 合约可以立即使用的格式在链上提交来实现此功能。
你准备一个要在 L1 上进行的只读查询列表,例如:
查询被编码为 SignalRequest
// SignalRequest 的定义
struct SignalRequest {
// 要查询的 L1 合约
address target;
// view 函数的函数选择器
bytes4 selector;
// 任意函数输入
bytes input;
}
你将此列表提交给 SignalBoost 合约,该合约:
signalRequestsRoot
)发布到 L1 上的 SignalService
合约// SignalBoost L1 合约中的函数
function writeSignals(SignalRequest[] memory requests) external {
bytes32[] memory leaves = new bytes32[](requests.length);
for (uint256 i = 0; i < requests.length; i++) {
// 使用 selector 和 input 编码调用
bytes memory payload = abi.encodeWithSelector(
requests[i].selector,
requests[i].input
);
// 调用 view 函数
(bool success, bytes memory output) = requests[i].target.call(payload);
require(success, "SignalBoost: call failed");
// 创建 Merkle 叶子节点
leaves[i] = keccak256(abi.encode(requests[i], output));
}
// 计算 Merkle 根
bytes32 signalRequestsRoot = merklize(leaves);
// 写入 SignalService 合约
SignalService.sendSignal(signalRequestsRoot);
}
signalRequestsRoot
从 L2 开发人员的角度来看,他们可以验证 L1 output
来自正确的 L1 合约调用。以下是一个读取 L1 Oracle 数据的示例,该数据可以被其他 L2 合约使用。
// L2 上的示例函数
function readL1Pricefeed(bytes calldata output, bytes32 signalRequestsRoot, bytes[] calldata proof) external returns (uint256) {
// 验证信号已写入 L2 SignalService
require(SignalService.verifySignal(signalRequestsRoot), "Signal not found");
// 为此上下文重建 SignalRequest
SignalRequest memory request = SignalRequest {
target: L1_ORACLE_ADDRESS,
selector: bytes4(keccak256("getPrice()")),
input: bytes("")
};
// 重建 Merkle 叶子节点
bytes32 leaf = keccak256(abi.encode(request, output));
// 验证 Merkle 证明
require(verifyProof(signalRequestsRoot, leaf, proof), "Invalid Merkle proof";
// 解码 Oracle 价格
uint256 price = abi.decode(output, (uint256));
return price;
}
为了确保信号尽可能与 L1 保持最新,我们希望在发布 L2 批处理之前立即执行 SignalService.writeSignals()
。
使用 EIP-7702 智能帐户有助于协调这一点。它们允许 L2 排序器将其自己的交易与预签名的用户交易捆绑在一起(假设用户的智能帐户支持委托),所有这些都在单个 L1 调用中完成。这允许原子性和对执行顺序的严格控制。
方便的是,这与 OP Stack 中不将其 blob 发布到可能允许捆绑的合约的排序器兼容,而无需修改技术栈。
存在一个微妙但重要的风险:SignalService.writeSignals()
的输出可能会根据包含它的捆绑包在 L1 区块中_执行的位置_而变化。
如果此输出与 L2 排序器预期的输出不同,则 L2 合约使用的 signalRequestsRoot
和相应的 proof
将无效,导致 L2 交易回滚。
有几种方法可以缓解这种情况:
捆绑包可以放置在 L1 区块的末尾,以确保它反映所有先前的状态更改。但是,这限制了它的实用性。那时,等待下一个区块并异步访问最终确定的 L1 状态会更简单。
L1 提议者可以承诺执行 L1 区块,以便捆绑包产生预期的 signalRequestsRoot
。如果他们偏离,他们可能会被削减。这提供了一种经济保证,不依赖于特定的区块位置。
在 ToB 执行捆绑包可确保来自先前 L1 区块的干净的后状态。如果 SignalService.writeSignals()
在捆绑包中的所有相关交易之后运行,则生成的 signalRequestsRoot
将反映最新的 L1 状态。另请注意:
基于排序器可以强制执行其捆绑包以预期的结果执行,因为它们对 L1 和 L2 具有写锁。从本质上讲,他们可以给自己与上述 2) 和 3) 相同的保证,但无需与任何人协调。由于 Signal-Boost 的目标之一是允许现有 rollup 实现与 L1 的 SC,因此我们将在本文的其余部分中假设我们使用 ToB 执行。
通过 SignalBoost
、EIP-7702 智能帐户和 ToB 执行,我们现在拥有从 L2 合约实时访问任意 L1 数据的关键要素。
但是,仍然存在一个挑战:当信号在同一 slot 中写入时,L2 交易如何访问其对应的信号证明?
由于 signalRequestsRoot
仅在执行 SignalBoost.writeSignals()
后才知道,因此用户无法提前包含其 Merkle 证明。有几种解决方法:
用户预先签署他们的 L2 交易,允许排序器在执行期间注入 signalRequestsRoot
和 proof
。这使得交易可以在运行时完全构建,同时仍然强制执行正确性。如果信号或证明无效,则 L2 交易将回滚。
在 L1 slot 期间,用户可以向排序器查询最新的 signalRequestsRoot
和相应的 proof
。这避免了任何特殊的智能帐户逻辑,但需要用户和排序器之间的协调。
了解 signalRequestsRoot
和所有 Merkle 证明后,排序器可以简单地将所有证明发布到验证然后存储 output
数据的 L2 合约。然后,L2 合约可以直接从 L2 存储访问输出,而无需了解 signalRequestsRoot
或证明。这完全抽象了 L2 用户的细节,但将成本负担推给了排序器。
假设中间选项,使用 same-slot 消息传递 的端到端流程为:
SignalRequests
SignalBoost.writeSignals()
的调用捆绑在一起signalRequestsRoot
并生成 Merkle 证明。signalRequestsRoot
被导入到 L2 的 SignalService
合约中signalRequestsRoot
和其特定信号的 proof
signalRequestsRoot
Signal-Boost 解锁了从 L2 合约同步访问任意 L1 数据,而无需进行重大的上游协议更改。这实现了一系列新的用例,例如:
用户可以通过以下方式在其贷款之间无缝迁移 L2 借贷协议:
所有这些都在单个 L1 slot 中以原子方式执行。没有 Oracle 复制或延迟桥接,只有与 L1 的同步可组合性和实时证明!
充分利用 same-slot 消息传递和 ToB 执行的双重套利:
SignalBoost
合约将更新的 Oracle 数据从 L1 转发到 L2即使没有实时证明,也可以使用意图进行 same-slot 消息传递即时 L1<>L2 提款 如 Nethermind 所述。
这些示例仅仅是冰山一角,任何依赖于及时访问 L1 数据的 L2 合约都可以从中受益。
OP Stack 没有 SignalService
合约或锚定交易的概念,这些交易在 same-slot 消息传递设计中使用。相反,OP Stack 的推导管道读取通过 CrossDomainMessenger.sendMessage()
函数传输的所有 L1 消息(L1 存款/任意函数调用),并将它们插入到 L2 区块的顶部。
可以调整 SignalBoost
合约以使用 signalRequestsRoot
值调用 sendMessage()
,而不是写入 SignalService
合约。无需更改即可添加锚定交易,因为 OP Stack 已经支持类似的功能。
此外:
batchSubmitterAddress
提交。SequencerConfDepth
值需要设置为 0
才能实时跟踪 L1 head。TL;DR 更改一个配置值并调整 SignalBoost
合约
不会。Signal-Boost 有助于扩大同步可组合性的市场,但Based Rollup 仍然提供独特的优势:
不会。Signal-Boost 旨在实现低摩擦采用。Rollup 不需要彻底改革其技术栈或进行重大协议升级。排序器可以选择加入,并且仅在需要同步访问 L1 状态时才构建捆绑包。
应该注意的是,不同的技术栈的摩擦会更少,具体取决于它们当前如何处理 L1 输入数据和重组。
保证 ToB 执行需要一个简单的包含预确认,与 L1 或 L2 执行预确认相比,它更容易证明安全故障(请参见 PoC Tobasco)。
可以放宽 ToB 要求并使用执行预确认来保证在 L1 上报告预期的 signalRequestsRoot
。这也很容易证明,但这需要预确认者执行的更大程度的完善。
不,Signal-Boost 与排序无关。无论是使用基于排序,经典排序,共享排序还是 rollup 生态系统(即 SuperChain),rollup 都保留对交易排序,MEV 捕获和费用市场的完全主权。Signal-Boost 仅仅是一种允许 rollup 同步访问 L1 状态的技术。
Signal-Boost 的设计考虑了L1 ↔ L2 的可组合性,允许排序器向用户提供对 L1 状态的同步访问。对此最简单的实现是使用中心化排序器,也就是说,存在重要的细微差别:
Ultra transactions 提供了跨链可组合性的最完整的愿景。它们假定 L1 和 rollup 技术栈已修改为支持诸如 XCALLOPTIONS
预编译之类的功能以及与 L1 的 EVM 等效性,从而允许执行在域之间流畅地移动。这使得任意的 L1 ↔ L2 函数调用,对状态的同步访问以及由单个证明验证的捆绑交易的原子执行成为可能。
Signal-Boost 是此想法的简化但务实的改编,目的是使某些技术今天可供现有 rollup 使用。
Same-slot 消息传递 通过将数据写入 L2 可以在同一 slot 中读取的消息合约来实现 L1 → L2 通信。它不支持任意的 L1 读取,仅支持已写入 L1 SignalService
合约的信号。Signal-Boost 采用了相同的技术,但试图通过 SignalBoost
合约来解决其局限性。
是的,但有一个重要的警告。从严格意义上讲,Signal-Boost 实现了同步可组合性:L1 和 L2 状态可以在同一 L1 slot 中交互,并且一个域可以读取另一个域的状态并以原子方式做出反应。这满足了 Jon Charbonneau 使用的定义,其中同步性意味着 same-slot 协调,而可组合性意味着反应性状态访问。
Ultra transactions、实时证明和 EIP-7814 等提案允许完全的双向可组合性:任意的跨域读取和写入/交错的函数调用,这严格来说比 Signal-Boost 可以提供的更好。
重组风险是实时互操作性的代价。
一个潜在的解决方案是按需模型:rollup 默认情况下使用安全缓冲区运行以减轻 L1 重组风险,但是在需要同步 L1 访问时可以暂时将缓冲区减少到 0。在 OP Stack 的情况下,这可以通过动态调整排序器配置中的 SequencerConfDepth
值来实现。
是的,一些发展可能会降低 L1 重组的频率。从上下文中看,大多数重组是由缓慢或较晚的区块传播引起的,通常是由于时序游戏或带宽限制(在此处阅读更多 here)。
是的,ToB 是优质区块房地产。
幸运的是,没有什么可以阻止将正常的 MEV 交易插入到 Signal-Boost 捆绑包的顶部,从而允许排序器抵消成本。此外,如果将来实施了 EIP-7814,它将允许任意的 L1 读取而无需 SignalService
合约。
Based Rollup 不需要 ToB 或执行预确认。 由于它们对 L1 区块进行排序,因此它们可以确保没有意外的交易会使预期的 signalRequestsRoot
无效,从而使它们可以在区块中的任何位置安全地执行 Signal-Boost 捆绑包。
- 原文链接: ethresear.ch/t/signal-bo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!