本次审计对 Mantle V2 的代码变更进行了安全分析,重点关注了 OP 栈的更新(包括与 EigenDA 集成的批量处理、rollup、以及 EigenDA 服务),op-geth 的更新(包括 Blob 交易、Meta 交易、以及 Gas 消耗的 bug 修复),以及新增的用于 sec256r1 曲线签名验证的预编译合约。审计发现了潜在的效率问题、安全风险以及代码需要完善的地方。
TypeLayer 时间线: 从 2024-01-06 到 2024-01-27 语言: Go 问题总数: 10 (5 个已解决) 严重性问题: 0 (0 个已解决) 高严重性问题: 0 (0 个已解决) 中等严重性问题: 1 (1 个已解决) 低严重性问题: 6 (3 个已解决) 注释 & 补充信息: 3 (1 个已解决)
本次审计分为三个部分:
mantle-v2
├── op-batcher
│ ├── batcher
│ │ ├── channel_builder.go
│ │ ├── channel_manager.go
│ │ ├── config.go
│ │ ├── driver.go
│ │ └── driver_da.go
│ ├── cmd
│ │ └── main.go
│ ├── flags
│ │ └── flags.go
│ └── metrics
│ ├── metrics.go
│ └── noop.go
├── op-e2e
│ └── actions
│ └── l2_verifier.go
├── op-node
│ ├── flags
│ │ └── flags.go
│ ├── metrics
│ │ └── metrics.go
│ ├── node
│ │ ├── client.go
│ │ ├── config.go
│ │ └── node.go
│ ├── rollup
│ │ ├── da
│ │ │ ├── datastore.go
│ │ │ └── interfaceRetrieverServer/server.pb.go
│ │ ├── derive
│ │ │ ├── calldata_source.go
│ │ │ ├── l1_retrieval.go
│ │ │ └── pipeline.go
│ │ ├── driver
│ │ │ └── driver.go
│ │ └── types.go
│ └── service.go
├── op-program/client/driver/driver.go
└── op-service
├── client
│ └── http.go
├── eigenda
│ ├── cli.go
│ ├── codec.go
│ ├── config.go
│ ├── da.go
│ ├── da_proxy.go
│ ├── derivation.go
│ └── metrics.go
├── eth
│ ├── blob.go
│ ├── blobs_api.go
│ ├── ether.go
│ ├── id.go
│ └── types.go
├── proto
│ ├── gen/op_service/v1/calldata.pb.go
│ └── src/op_service/v1/calldata.proto
├── retry
│ ├── operation.go
│ └── strategies.go
├── sources/l1_beacon_client.go
├── txmgr
│ ├── cli.go
│ └── txmgr.go
└── upgrade
└── mantle_upgrade.go
op-geth
├── consensus/misc/eip1559.go
├── core
│ ├── genesis.go
│ ├── mantle_upgrade.go
│ ├── state_transition.go
│ ├── txpool/txpool.go
│ ├── types
│ │ ├── deposit_tx.go
│ │ ├── meta_transaction.go
│ │ ├── transaction.go
│ │ ├── transaction_signing.go
│ │ ├── tx_access_list.go
│ │ ├── tx_dynamic_fee.go
│ │ └── tx_legacy.go
├── internal
│ └── ethapi
│ ├── api.go
│ └── transaction_args.go
├── light/txpool.go
└── params/confighttp.go
op-geth
├── core
│ ├── genesis.go
│ ├── mantle_upgrade.go
│ ├── txpool/txpool.go
│ └── vm
│ ├── contracts.go
│ └── evm.go
├── crypto
│ └── secp256r1
│ ├── publickey.go
│ └── verifier.go
└── params
├── config.go
└──protocol_params.go
op-geth
├── core
│ ├── mantle_upgrade.go
│ ├── state_transition.go
│ ├── txpool/txpool.go
| └── types/meta_transaction.go
└── light/txpool.go
Mantle V2 是以太坊的 Layer 2 (L2) 扩展解决方案,它使用欺诈证明而不是有效性证明来保证其安全性。该协议旨在提供低交易费用和高吞吐量,同时保持完全的 EVM 兼容性。Mantle V2 构建于以太坊之上,使用了 OP Stack,因此与 Optimism 有许多相似之处。有关 Mantle 的更多详细信息,请参阅我们之前的审计报告 这里,这里,这里 和 这里。在此差异审计中,审查的更改可分为三类:
对 op-stack 的更新侧重于通过使用 EigenDA 解决数据可用性 (DA) 问题,从而增强可扩展性和可靠性。这些更新可以分为三个主要升级:
NewEigenDAClient
促进了与 EigenDA 代理的通信,从而能够检索 blobs 及其关联的承诺,并在必要时对其进行验证。其他值得注意的更新包括:
对 op-geth 实施进行了一些更改。这些更改可以分为三个主要类别:与 Blob 交易相关的更新、与元交易相关的更新以及 bug 修复。
Blob 交易:已添加一个新文件 eip4844.go
。它的函数处理 EIP-4844 定义的 blob 交易的标头验证,并计算此类交易的基础费用。此外,通过实施 Cancun 签名器引入了对此类交易的签名支持。Blob 交易旨在包含在 L1 区块中,与 L2 执行层无关。因此,此文件中的函数未在 op-geth 代码的其他位置使用。
元交易:元交易已被禁用,因为 Mantle 团队计划在未来的升级中实施 EIP-7702 作为替代方案。
Gas Bug 修复:修复了一个与具有待处理交易的发送者的交易 gas 计算相关的 bug。
代理管理员所有者更新:Mantle 系统合约的代理合约的代理管理员所有者之前设置不正确。现在,这个问题已通过在节点级别实施所有者地址的更新来解决。
sec256r1
签名的新预编译合约Mantle 按照 RIP-7212 实施了一个新的预编译合约 (p256Verify
),用于验证 sec256r1
曲线上的 ECDSA 签名。sec256r1
曲线被许多现代设备广泛采用和支持,因此添加此预编译合约非常方便。
p256Verify
类似于 ecrecover
,主要区别在于椭圆曲线的选择和预期的参数。ecrecover
验证 sec256k1
曲线上的签名。此外,虽然 ecrecover
将消息哈希以及签名的三个字段 v
、r
和 s
作为参数,并且可以从中恢复签名者的公钥,但 p256verify
期望提供消息的哈希、签名 (r,s)
,以及签名者的公钥 (x,y)
。
当提供无效参数时,例如 r
或 s
超出范围,或者公钥不代表曲线的有效点时,预编译合约不会 revert,而是返回 false
。此外,正如 RIP-7212 中所述,预编译合约允许可延展的签名,如果需要,将延展性检查添加到包装器库中。调用此新预编译合约的 gas 成本已设置为 3450,略高于 ecrecover
。为了实施输入验证和签名验证,预编译合约依赖于标准的 Go 库,如 crypto/ecdsa
和 crypto/elliptic
。这些库被广泛使用,并且被认为对于本次审计的目的而言是安全的。
本次审计侧重于审查 Mantle 链各个组件的更改。由于这是一次差异审计,我们仅评估了修改后的代码,因此无法保证未更改部分的正确性。
在“op-stack”中,我们假设由 Mantle 托管的 EigenDA 代理服务器 运行正常,同时验证 DA 证书和 KZG 承诺。我们还假设参数和服务器配置正确,以确保 EigenDA 服务与 EigenDA 之间的无缝通信,EigenDA 分散过程受到积极监控,并且在发生故障时,系统会自动调整 SkipEigenDARpc
开关,以便更快地回退到以太坊 blobs。
在“op-geth”部分,我们发现了一些硬编码的值,主要与计划的升级和规则更改的时间戳有关。在审计期间,许多这些值尚未设置,并标记为 TODO。我们假设这些值将在部署前正确配置。此外,一个新的代理管理员所有者将被分配给管理 Mantle 大多数系统合约的代理合约。此地址将有权随意更新系统合约,特别是那些控制与 L1 的桥接机制的合约。指定的管理员地址预计将由一个多重签名控制,并且被认为是受信任的,以确保系统正常运行。
loopEigenDa
循环中低效的错误处理和超时BatchSubmitter
的 loopEigenDa
函数 尝试在重试循环中调用 disperseEigenDaData
,该循环将持续固定的重试次数(EigenRPCRetryNum
)或直到达到超时时间(DisperseBlobTimeout
)。但是,此实现忽略了正确处理 disperseEigenDaData
返回的错误。循环仅仅记录错误并重试,而没有对错误进行分类或采取任何措施,即使在由于永久性或严重错误导致操作无法成功的情况下,也会有效地等待整个 DisperseBlobTimeout
持续时间。
这种疏忽会导致资源利用率低下和不必要的延迟。在发生严重错误的情况下(例如,无效的输入数据或持续存在的配置问题),循环仍将继续重试,直到超时过期。这会导致计算时间的浪费、rollup 过程中潜在的瓶颈以及交易数据提交的延迟。此外,此行为会模糊日志中错误的根本原因,因为相同的错误会重复记录而没有可操作的解决方案。
为了解决此问题,请考虑增强错误处理逻辑,以区分瞬时错误(例如,网络问题)和永久性错误(例如,无效数据)。对于瞬时错误,循环可以继续重试,并可能实施指数退避以减少随时间推移的重试频率。对于永久性错误,循环应立即退出,以避免不必要的延迟。此外,应在每次重试迭代的开始时执行超时检查,以确保重试不会超出配置的超时时间。
更新: 已在 pull request #192 中解决。Mantle 团队实施了对瞬时错误和永久性错误之间更细粒度的区分。将重试瞬时错误,而 EigenDA 调用错误等永久性错误将立即退出,需要手动调查。
在 loopEigenDa
函数 中,txAggregatorForEigenDa
函数 执行 交易数据的 RLP 编码,以根据 RollupMaxSize
验证其大小。但是,该方法仅返回原始的、未编码的交易数据(txsData
)。随后,在 loopEigenDa
中,相同的数据被传递给 disperseEigenDaData
,并在其中 再次进行 RLP 编码。这种重复的 RLP 编码过程是不必要的且效率低下,因为可以直接重用来自 txAggregatorForEigenDa
的编码数据。
这种冗余引入了计算开销,尤其是在处理大型数据集时,因为 RLP 编码会消耗大量资源。重复编码过程实际上使此操作所需的时间和 CPU 周期加倍,这可能会导致性能下降。此外,当前的实现引入了不必要的复杂性和不一致性,因为 RLP 编码的数据被丢弃而不是被重用,这使得数据流更难跟踪和维护。
为了解决这个问题,请考虑更新 txAggregatorForEigenDa
函数以同时返回 RLP 编码的数据(transactionByte
)和原始交易数据(txsData
)。然后,loopEigenDa
函数可以直接在 disperseEigenDaData
中使用编码数据,从而无需进行第二次编码。此更改减少了计算开销,简化了逻辑,并确保了对编码数据的一致处理。通过避免冗余操作,可以显着提高代码的整体性能和可维护性。
更新: 已解决,不是一个问题。Mantle 团队表示:
我们在以下代码中测试了
rlp.EncodeToBytes
对 4MiB 数据(是典型 Mantle blob 大小的 4 倍)的性能。测试结果表明,每次 EncodeToBytes 操作的平均时间消耗为 0.43 毫秒。
bash pkg: github.com/ethereum-optimism/optimism/op-batcher/batcher
BenchmarkRLPEncoding
BenchmarkRLPEncoding-8
2767 438315 ns/op 5434873 B/op 2 allocs/op
与单个 loopEigenDa 操作大约 1 分钟的持续时间相比,对性能的影响可以忽略不计。因此,我们认为这不会导致任何性能问题。
func BenchmarkRLPEncoding(b *testing.B) {
// Generate 40 random 128KB chunks
data := make([][]byte, 40)
for i := range data {
data[i] = make([]byte, 128*1024)
_, err := rand.Read(data[i])
if err != nil {
b.Fatalf("Failed to generate random data: %v", err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := rlp.EncodeToBytes(data)
if err != nil {
b.Fatalf("RLP encoding failed: %v", err)
}
}
}
blobTxCandidates
中对单个大数据帧的不充分处理在 blobTxCandidates
函数 中,代码假设超过大小限制(se.MaxBlobDataSize * MaxblobNum
)的数据是由多个帧的聚合产生的。但是,它没有考虑到单个帧(frameData
)可能单独超过最大大小的可能性。发生这种情况时,该函数会将 超大帧追加 到 dataInTx
并继续执行,而无需任何特殊处理,这可能会导致在 blob 创建期间出现意外行为。
如果单个帧超过了最大 blob 大小,则该函数不会将其拆分为更小的块或显式处理错误。这可能导致生成具有空 blobs 的交易候选者,或 blob 数量超过配置的最大 blob 数量(MaxblobNum
)的交易候选者,该数量目前设置为 4。
考虑为超过最大大小的单个帧引入显式处理。在将帧追加到 dataInTx
之前,请检查帧本身是否超过大小限制。如果超过,则记录错误并返回相应的错误消息。或者,考虑实施逻辑以在继续操作之前将超大帧拆分为更小的块。另一种解决方案是确保单个帧始终适合单个 blob 交易,方法是强制配置的 MaxFrameSize
小于最大大小(se.MaxBlobDataSize * MaxblobNum
)。添加此检查将确保对所有极端情况进行可靠处理。
更新: 已在 pull request #194 中解决。Mantle 团队表示:
引入了一项检查以确保单个帧不大于
MaxBlobDataSize * MaxblobNum
。
disperseEigenDaData
中承诺的冗余编码和解码在 disperseEigenDaData
函数中,从 EigenDA.DisperseBlob
检索的 承诺 首先使用 DecodeCommitment
在 DisperseBlob
中解码,然后返回到 disperseEigenDaData
,并在其中 使用 EncodeCommitment
重新编码。这导致了不必要的计算开销,因为承诺经历了额外的编码-解码周期,而该周期没有为该过程增加任何价值。这种冗余增加了计算成本,并降低了 blob 分散过程的效率。额外的编码和解码操作引入了不必要的处理时间,这可能会影响批次提交的整体性能。
为了优化该过程,请考虑让 DisperseBlob
同时返回承诺的编码版本和解码版本,从而无需 disperseEigenDaData
重新编码它,同时仍然能够从解码的承诺中提取信息并 将编码版本添加到 calldata 帧中。
更新: 已解决,不是一个问题。Mantle 团队表示:
在我们的测试中,每次解码的平均时间为 0.0013 毫秒,每次编码的平均时间为 0.0004 毫秒。此外,
DecodeCommitment
和EncodeCommitment
不是高频操作,因此它们对性能的影响非常小。我们认为这不是一个问题。
rollup 服务 利用 RetrieveBlob
从 EigenDA 检索 blob,以便重建 L2 状态。当承诺的长度为 0 时,此 RetrieveBlob
函数 调用 EigenDA 客户端的 RetrieveBlob
函数。此函数通过 gRPC 直接与 EigenDA 分散器通信,以基于 BatchHeaderHash
和 BlobIndex
检索 blob,而不是使用 RetrieveBlobWithCommitment
函数 使用的 EigenDA 代理。
使用 EigenDA 代理 而不是 EigenDA 分散器直接检索 blob 具有多个优点,例如 KZG 验证确保数据正确性和 DA 证书验证确保由坏 DA 证书表示的数据不会成为规范链的一部分。通过直接使用分散器,不会执行这些验证检查,这可能导致在使用 EigenDA 检索 blob 时无法重建正确的 L2 状态。请注意,这仅在 承诺长度等于零 时发生。
虽然这通常不应该发生,因为 batcher 不会将空承诺发布到 L1,但请考虑添加一项检查以确保 Rollup 服务中 没有使用承诺长度为 0 调用 RetrieveBlob
函数。
更新: 已确认,将解决。Mantle 团队表示:
RetrieveBlob 中与 EigenDA Disperser 的直接通信是与 sepolia 测试网上历史 EigenDA 集成版本保留的兼容性代码。它不会在主网上执行。我们将在下一次协议升级中删除这些代码。
在 transaction_signing.go
中,有三个函数负责返回链的适当签名者类型:LatestSignerForChainID
,LatestSigner
和 MakeSigner
。虽然这些函数的输入不同,但它们应返回相同的签名者类型。
LatestSignerForChainID
:仅接受链 ID 作为参数。LatestSigner
:接受链配置。MakeSigner
:同时接受链配置和区块号。但是,存在不一致之处。LatestSignerForChainID
函数返回 cancunSigner
,这意味着 Cancun 升级预计将是 Mantle 采用的最新 op-geth 更新。相比之下,即使链已升级到 Cancun,其他两个函数也会返回 londonSigner
而不是 cancunSigner
。
londonSigner
和 cancunSigner
之间的唯一区别是后者支持 Blob 交易。但是,Blob 交易旨在发布在 L1 上,而不是包含在 L2 区块中。事实上,此类交易会从 L2 [txpool
](https://github.com/mantlenetworkio/op-geth/blob/4a20fa61a79e4c4cd61138e467332368d7fc8虽然在 Mantle 中配置了 2 秒的固定区块时间,这可以更准确地预测未来的区块时间,但这种对精确计时的依赖仍然存在风险。
考虑用一种更灵活和健壮的方法来代替当前的时间戳匹配逻辑。例如,如果区块时间戳大于或等于 ProxyAdminUpgradeTime
且尚未执行升级,则可以执行所有者升级。
更新: 已确认,未解决。Mantle 团队已决定保留简单的 ProxyAdmin
所有者升级逻辑。该团队将提前宣布升级,让节点有足够的时间在 ProxyAdminUpgradeTime
之前进行升级。该团队告诉我们,他们预计只有少数节点未能及时升级,这些节点可以重新同步以与正确的系统视图保持一致。
ECDSA 签名已知是可延展的,因为 (r,s)
和 (r,-s)
都是同一消息 m
的有效签名。为了消除这种不良属性,实现通常强制执行 s <= N/2
,其中 N
是椭圆曲线群的阶(N := curve.Params().N
)。这自然会拒绝第二个有效签名 (r,-s)
,对于该签名 -s=N-s > N/2
(因为 s <= N/2
)。
p256Verify 的实现 没有对 s
的值应用上述的可延展性检查。这是有意的,并且受到 RIP-7212 标准 的推动,该标准声明如下:
“[...] NIST FIPS 186-5 规范不包括可延展性检查。我们在这里 [RIP-7212 中] 对其进行了匹配,以便最大限度地与现有的庞大 NIST P-256 生态系统兼容。包装器库应该默认添加可延展性检查,并清楚地标识包装原始预编译调用的函数(精确的 NIST FIPS 186-5 规范,没有可延展性检查)。例如,
P256.verifySignature
和P256.verifySignatureWithoutMalleabilityCheck
。添加可延展性检查非常简单,并且只会消耗极少的 gas。”
考虑明确记录 p256Verify
的实现有意不进行可延展性检查,并参考 RIP-7212 标准的相关部分以了解其动机。
更新: 已解决。Mantle 团队提供了明确的文档,声明新的 p256Verify
预编译不执行任何可延展性检查。
在与 PR #93 相关的代码库的各个部分中,发现了多个缺少注释和改进文档的机会的实例:
在 contracts.go
中缺少一个注释,用于描述 PrecompiledContractsMantleEverest
映射,类似于其他映射(例如,Berlin 版本的注释)。
newPublicKey
函数在两种情况下返回 nil:首先,当至少一个坐标无效时(即,x
或 y
或两者都为 nil
),其次,当该点不在曲线上时。
Verify
函数 没有明确检查 r,s
输入是否在正确的范围内 1<= r,s < N
,其中 N
是曲线阶数。相反,它将实际的验证委托给 Go 标准库的 ecdsa.Verify
,该函数隐式地执行检查。
考虑彻底记录任何合约的公共 API 中所有函数(及其参数)。此外,为了增强可读性和可维护性,请考虑在代码库中为行为不明确或不言自明的情况提供清晰的文档。
更新: 已确认,将解决。Mantle 团队表示:
已确认。将在下一个版本中修复。
newPublicKey
函数正确地验证了 x,y
输入,确保:
x,y
不是无效输入 nil,nil
。IsOnCurve(x, y)
,点 x,y
位于 P256 曲线 上。该函数没有明确检查 x,y
是否为 infinity 点,infinity 点通常表示为 0,0
(在仿射坐标中)(例如,如 RIP-7212 中所述)。但是,点 0,0
不位于 P256 曲线 上,因此对 IsOnCurve
的调用将拒绝它。因此,此检查是隐式的。虽然所采用的检查是适当的,但执行椭圆曲线运算的函数 IsOnCurve
在计算上是昂贵的。
为了提高效率,请考虑在调用昂贵的 IsOnCurve
之前对 x,y
实现(更)有效的初步范围检查。此检查类似于在 ecrecover 中所做的检查,并验证 x,y
是否具有预期的字节长度,并且其值是否在由曲线阶数 N := curve.Params().N
确定的预期范围内。
请注意,IsOnCurve
也执行了上述对 x,y
的初步范围检查,因此对于有效点将执行两次。因此,是否实现它的决定取决于预计调用 IsOnCurve
的频率。或者,可以修改 Go 标准库中的 IsOnCurve
实现以删除额外的范围检查。
请记住上述效率权衡,考虑对 x,y
实现初步范围检查。此外,请考虑明确拒绝 x,y=0,0
的情况,或者至少记录检查是隐式的。
更新: 已确认,将解决。Mantle 团队表示:
已确认。将在下一个版本中修复。
在此更新中,Mantle 引入了多个新功能,并对以前的版本进行了改进。值得注意的是,EigenDA 已被实现为主要的数据可用性层,而 Ethereum blobs 被用作后备。此外,还引入了一个新的预编译来验证 sec256r1
上的签名,并且禁用了元交易。此更新还包括一些小的改进和错误修复,从而提高了链的整体鲁棒性。
本次审计发现一项中等严重性和一些低严重性和说明严重性的问题。虽然其中一些更改是从 Optimism 中采用的,但这些更改已进行独立审计。Mantle 团队反应迅速,并提供了对我们问题的详细解答。
- 原文链接: blog.openzeppelin.com/ma...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!