Lido DSM 1.5:存款安全模块设计改进与安全分析
本文针对 Lido 协议中权限无关模块接入 Staking Router 的障碍,提出对存款安全模块(DSM)的设计改进。核心方案包括:由不同角色(治理或安全工具)对不同模块执行密钥 vetting;引入强制性的密钥 unvetting 机制,统一由 DSM 执行;用密钥 unvetting 和软暂停替代模块级暂停,以应对 front-run 攻击;仅在检测到资金被盗时全局暂停所有模块存款;降低社区质押模块(CSM)的单次最大存款数量。文章还分析了新攻击向量(如跨模块密钥重复、守护者合谋)并给出了缓解措施,同时提供了完整的接口变更和参数建议。

🎯 TL;DR
本文档分析了当前阻碍无需许可模块接入 Staking Router 的问题,并提出了解决方案:
- 允许不同模块通过不同的执行者进行密钥审查(key vetting)。
- 引入自动化的取消审查(unvetting)流程,使其成为所有模块的强制要求,并将该角色分配给 DSM。
- 通过新的自动化审查流程缓解密钥替换问题,并对使用治理审查的模块执行取消审查和软暂停。
- 在检测到抢跑攻击(front-run)时,使用密钥取消审查和软暂停来代替模块暂停。
- 在检测到资金被盗时,对所有模块执行存款暂停。
- 减少 Community Staking Module 单次最大存款数量。
本文档假设读者已经熟悉 Deposit Security Module 的当前设计、密钥提交流程 以及密钥审查。
🌟 动机
无需治理的审查(Governanceless Vetting)
随着新的无需许可模块即将引入,问题随之而来:当前通过 DAO 进行密钥审查的方式要求每个操作者都获得治理的明确批准,这是否与这些模块的无需许可模型兼容?
审查问题
当前的存款数据审查流程存在一个漏洞,详见该 issue。恶意节点操作者可以利用该漏洞,将即将被标记为已审查的密钥替换为无效密钥。
随着无需许可模块的出现,这个问题变得更加严峻。在精心筛选的集合中,操作者会面临声誉受损和被移出集合的风险,而对于无需许可的操作者来说,这些制约手段并不存在。因此,当前的方法不适用于新模块,需要重新考虑。
暂停问题
随着无需许可模块即将引入,修改模块暂停机制的问题也随之而来。当前的设计是在任何抢跑攻击尝试时暂停模块。对于无需许可准入的模块,这为任意参与者提供了触发模块暂停并停止该模块中所有操作者存款的可能性。这个问题促使我们重新评估当前的方法,并在操作者层面而非模块层面处理此类攻击尝试。
💅 提议的设计
提议通过添加密钥取消审查流程、允许不同执行者进行密钥审查,以及改变模块暂停的条件来修改当前的存款流程。简而言之,提议的流程可以描述如下:
- 操作者提交密钥
- 密钥被检查
- 如果检查通过——密钥被标记为已审查
- 如果检查失败——向 Data Bus 发送消息
- 密钥进入存款队列
- 存款队列被持续检查
- 如果密钥有效,则进行存款消息签名,消息被聚合,并发送存款交易
- 如果队列中的密钥无效——模块进入软暂停状态,密钥被取消审查
- 如果 Council Daemon 检测到资金被盗,则暂停存款
- 存款交易在链上被检查
- 如果链上检查通过,则存款发生
- 如果检查失败——交易被回滚

提议将某些模块的密钥审查责任分配给 Council Daemon,并在 DSM 中新增一个负责聚合审查消息并投递的执行者。图中的黑色部分显示了新的流程和执行者。
自动化审查(Automated Vetting)
由于没有需要该功能的模块,第一版中未实现
自动化审查详情
Curated 模块中当前的密钥审查流程很难改变。虽然 stakingLimit 已被拆分为 vettedKeys 和 targetLimit,但操作者不仅将审查用于密钥验证流程,还用于活跃密钥管理。然而,通过治理进行审查并不适合无需许可模块,因此提议将审查流程按模块分配给不同的执行者。
对于 Curated 模块,治理仍然可以作为执行者,而对于无需许可模块——则由执行必要检查的安全工具来承担。Lido 协议已经有用于保障存款安全的基础设施——DSM,提议将这一角色分配给 DSM。
DSM 可以承担模块中密钥审查的角色,前提是模块支持必要的接口,并且相应的角色被授予 DSM 合约。
interface IStakingModuleUnderVetting {
function increaseVettedSigningKeysCount(
bytes calldata _nodeOperatorIds,
bytes calldata _vettedSigningKeysCounts
) external;
}
提议 Council Daemon 遍历支持审查接口的模块,检索每个处于_活跃_状态的操作者的数据,检查是否存在未审查的密钥;如发现,则执行以下检查:签名验证、无重复、与之前已存入密钥无交集。检查的详细信息见 检查 部分。

如果所有检查都成功通过,则密钥被视为有效,可以用于存款。通过检查后,应执行以下场景之一:
检查失败

如果检查失败,Council Daemon 向 Data Bus 发送关于发现无效密钥及失败原因的消息。消息按模块、操作者和检查失败类型分组。除现有前端之外,还提议基于这些消息构建监控。
注意:守护进程应缓存无效密钥,并在后续检查周期中忽略它们,以避免 Data Bus 消息杂乱。缓存应包含完整的存款数据,而不仅仅是公钥。
enum UnpassedCheck {
depositDataSignature,
pubkeyDuplicate,
previouslyDeposited
}
interface MessageInvalidKeysToVet extends MessageBase {
blockNumber: number,
blockHash: string,
depositRoot: string,
stakingModuleId: number,
nonce: number,
operatorId: number,
invalidKeyIds: number[],
unpassedCheck: UnpassedCheck,
signature: Signature,
}
- 已提交的密钥在被标记为已审查之前不能用于存款。
- 如果密钥未通过检查,只需通知操作者问题即可;不需要进一步操作。
检查通过

如果检查通过,Council Daemon 向 Data Bus 发送关于已通过检查且可被标记为已审查的密钥的消息。同一模块中多个操作者的审查可以分组到一笔交易中。
interface MessageVetSigningKeys extends MessageBase {
blockHash: string;
blockNumber: number,
depositRoot: string,
stakingModuleId: number,
nonce: number,
nodeOperatorIds: string,
vettedSigningKeysCounts: string,
signature: Signature,
}
Vetting Bot 从 Data Bus 聚合来自不同 Council Daemon 的审查密钥消息并形成法定人数(quorum)。之后,它形成并向 DSM 合约发送交易。提议 DSM 合约为此新增一个方法 vetOperatorsKeys,并将相应角色授予 Staking Router 合约:
interface IDepositSecurityModule {
bytes32 public immutable VET_MESSAGE_PREFIX;
uint256 internal maxOperatorsPerVetting;
function vetOperatorsKeys(
uint256 blockNumber,
bytes32 blockHash,
bytes32 depositRoot,
uint256 stakingModuleId,
uint256 nonce,
uint256[] operatorIds,
uint256[] vettedKeysByOperator,
Signature[] calldata sortedGuardianSignatures
) external;
function getMaxOperatorsPerVetting() external returns (uint256);
function setMaxOperatorsPerVetting(uint256 newValue) external;
}
合约执行链上检查:
- 验证
nonce和depositRoot与当前链上值匹配,以确保合约状态未改变 blockHash与blockNumber处的链上区块哈希匹配,以确保未发生重组- 模块是否处于活跃状态
- Guardian 签名和法定人数
通过 DSM 链上检查后,合约调用 Staking Router 合约上的 increaseStakingModuleVettedKeysCountByNodeOperator 方法:
interface IStakingRouter {
function increaseStakingModuleVettedKeysCountByNodeOperator(
uint256 _stakingModuleId,
bytes calldata _nodeOperatorIds,
bytes calldata _vettedSigningKeysCounts
) external;
}
Council Daemon 可以按同一模块的操作者分组报告。一笔交易中的操作者数量受参数 maxOperatorsPerVetting 限制,每个模块的报告通过单独的交易处理。
审查密钥的消息会在 nonce、deposit_root 改变时失效,或在 255 个区块后失效(255 个区块是链上获取历史 block_hash 的极限)。
- 通过 DSM 进行审查是可选的,每个模块可以选择最适合自己的审查方式:自动化、通过治理,甚至是乐观审查。
- 审查建立在 NodeOperatorRegistry 实现的现有机制之上。
- 提议的流程纠正了漏洞 https://github.com/lidofinance/lido-dao/issues/141,因为它涉及对模块
nonce的签名。
取消审查(Unvetting)
不同的模块可能由不同的执行者审查密钥,其中可能包括乐观审查,即上传的密钥立即被视为已审查。在抢跑攻击尝试的情况下,密钥可能随时间变得无效。因此,引入统一的、对所有模块强制要求的密钥取消审查机制非常重要。
提议将减少已审查密钥数量的角色授予 DSM,并修改每个模块必须支持的 IStakingModule 接口。因此,取消审查通过 DSM 合约进行,DSM 合约通过 Staking Router 调用模块侧的相应方法。
interface IStakingRouter {
function decreaseStakingModuleVettedKeysCountByNodeOperator(
uint256 _stakingModuleId,
bytes calldata _nodeOperatorIds,
bytes calldata _vettedSigningKeysCounts
) external;
}
interface IStakingModule {
function decreaseVettedSigningKeysCount(
bytes calldata _nodeOperatorIds,
bytes calldata _vettedSigningKeysCounts
) external;
}
Council Daemon 监控所有模块中的密钥状态以及存款合约中的新存款,并在每次合约变更时对存款队列中的密钥执行以下检查:签名验证、无重复、与之前已存入密钥无交集。检查的详细信息见 检查 部分。

如果 Council Daemon 在存款队列中发现无效密钥(即使对于未选择使用 DSM 进行审查而是依赖治理方式的模块),它会向 DSM 合约发送取消审查密钥的交易。
interface IDepositSecurityModule {
bytes32 public immutable UNVET_MESSAGE_PREFIX;
uint256 internal maxOperatorsPerUnvetting;
function unvetSigningKeys(
uint256 blockNumber,
bytes32 blockHash,
uint256 stakingModuleId,
uint256 nonce,
bytes calldata nodeOperatorIds,
bytes calldata vettedSigningKeysCounts,
Signature calldata sig
) external;
function getMaxOperatorsPerUnvetting() external returns (uint256);
function setMaxOperatorsPerUnvetting(uint256 newValue) external;
}
它还会向 Data Bus 发送一条签名消息,当 Guardian 的余额不足以执行交易时,Vetting Bot 可以使用该消息来执行交易。
interface MessageUnvetSigningKeys extends MessageBase {
blockHash: string;
blockNumber: number,
stakingModuleId: number,
nonce: number,
nodeOperatorIds: string,
vettedSigningKeysCounts: string,
signature: Signature,
}
签名意图在模块 nonce 变更时失效,或者当已签名区块的 blockhash 在链上不可达时失效。
Council Daemon 可以按同一模块的操作者分组报告。一笔交易中的操作者数量受参数 maxOperatorsPerUnvetting 限制,每个模块的报告通过单独的交易处理。
由于 Council Daemon 几乎需要同时发送取消审查交易,提议在取消审查方法中检查模块 nonce 后即提前退出,以避免不必要的 Gas 支出。
不需要形成法定人数,一个 Council Daemon 就足以执行密钥取消审查。当前 DSM 暂停设计的基本原则仍然保留——一个诚实的 Guardian 应足以防止其他 Guardian 串通。任何密钥取消审查都被视为事件(incident),会被彻底调查,以排除 Guardian 对操作者的审查。
- 密钥可能因抢跑攻击尝试、或被错误地或通过蓄意攻击标记为已审查,而随时间变得无效。
- 取消审查对所有模块是强制性的。
- 只需要一个 Council Daemon 即可执行取消审查。
乐观审查(Optimistic Vetting)
该设计假设模块内的审查可以以不同方式实现,包括乐观方式。在这种情况下,假设密钥在提交给模块时就被标记为已审查。因此,只要密钥没有问题,总密钥数指针就与操作者的已审查密钥数指针保持同步。同步意味着当新密钥被提交时,已审查指针会随着总指针一起移动。
密钥同步:
total keys
vetted keys
v
0 1 2 3 4 5 6 7 8 9
^
deposited keys
一旦检测到密钥有任何问题,模块中的存款进入软暂停状态,并在 DSM 合约上调用取消审查交易。DSM 合约针对特定操作者调用模块上的 decreaseVettedSigningKeysCount 方法,将指针移动到第一个有效密钥。失同步意味着当新密钥被上传时,已审查指针保持在原位。
密钥失同步:
vetted keys total keys
v v
0 1 2 3 4 5 6 7 8 9
^
deposited keys
取消审查后,操作者失去已审查密钥数与已提交总密钥数之间的同步,直到操作者采取某些行动恢复同步。此类行动可以是删除任何密钥,或调用一个特殊方法,允许操作者向模块发出问题已修复的信号。方法的选择由模块自行决定,提议的设计仅规定以下约束:
-
在模块上调用
decreaseVettedSigningKeysCount应移动已审查密钥指针,并将指针右侧的所有密钥从存款队列中排除。 -
模块必须实现一种机制,在移除无效密钥后恢复已审查密钥与总密钥之间的同步。
-
模块必须有一种机制,阻止操作者恶意利用取消审查程序来耗尽 Council Daemon 的余额。例如,每笔交易收取一定费用,或由治理施加约束。
-
模块可以乐观地审查上传的密钥。取消审查和软暂停保障了存款的安全性。
-
模块应阻止操作者恶意利用取消审查程序。
存款(Deposit)
存款流程本身提议保持不变。DSM 检查存款消息中的签名数据,如果成功,则调用 Lido 合约上的相应方法。

提议将参数 maxDepositsPerBlock 和 minDepositBlockDistance 从 DSM 迁移到 Staking Router 级别。具有不同属性的模块在进行存款时具有不同的风险,因此这些参数对于不同模块可以不同。更多原因将在下面的 Guardian 串通部分和研究文档中分析。
interface IStakingRouter {
struct StakingModule {
uint24 id;
address stakingModuleAddress;
uint16 stakingModuleFee;
uint16 treasuryFee;
uint16 targetShare;
uint8 status;
string name;
uint64 lastDepositAt;
uint256 lastDepositBlock;
uint256 exitedValidatorsCount;
// 从 DSM 迁移的新参数
uint64 maxDepositsPerBlock;
uint64 minDepositBlockDistance;
}
}
向后兼容性
更改 StakingModule 结构体将影响以下视图方法,这些方法可被外部集成和链下工具使用:
- getStakingModule
- getStakingModules
- getStakingModuleDigests
- getAllStakingModuleDigests
测试表明,对于链下工具和可能的链上集成,向后兼容性均得以保持:https://github.com/lidofinance/sr-1.5-compatibility-tests。修改后的方法响应可以被标准 solidity 解码器和 ethers.js 库正确解码。响应中的新字节被忽略。
对于 Curated 模块,提议值保持不变,但降低 Community Staking Module 的 maxDepositsPerBlock 值。提议的值:
| 模块 | maxDepositsPerBlock |
minDepositBlockDistance |
|---|---|---|
| Curated | 150 | 25 |
| Simple DVT | 150 | 25 |
| Community Staking | 30 | 25 |
该值基于研究选择。
提议在 DSM 合约侧添加一个关于存款频率的总体限制。这样,不同模块之间的存款也会有时间间隔,类似于单个模块内的存款。可以认为,DSM 合约的 depositBufferedEther 和 canDeposit 方法中的存款频率检查会使用模块和 DSM 合约中 lastDepositBlock 的最大值。
interface IDepositSecurityModule {
uint256 internal lastDepositBlock;
function getLastDepositBlock() external returns (uint256);
}
- 每个模块有独立的
maxDepositsPerBlock和minDepositBlockDistance。 - Community Staking Module 的
maxDepositsPerBlock被降低。
软暂停(Soft Pause)
Council Daemon 在每个迭代周期、发起存款之前执行密钥检查。如果检测到模块中的密钥有任何问题,Council Daemon 进入软暂停模式——它停止为选定的模块签署存款消息,直到问题解决。同时,还应触发密钥取消审查或完全模块暂停。
暂停(Pause)
具有无需许可准入的新模块的出现,意味着节点操作者数量不受限制且身份未知,这对存款暂停的设计提出了新的约束。此类模块中一个操作者的恶意行为不应对其他参与者产生负面影响。

提议在已经发生抢跑攻击的情况下暂停所有模块的存款,而对于试图窃取用户 ETH 的场景,提议通过将密钥从存款队列中移除来缓解(见 取消审查 部分)。因此,对盗窃尝试的保护变得更加有针对性,针对特定操作者,而不是整个模块。存款暂停仍然保留,并被移到下一层防御,应在发生抢跑攻击时触发,这意味着 Guardian 串通或不可预见的情况。
提议将存款暂停同时应用于所有模块。否则,串通的 Guardian 可以逐个模块地执行盗窃。提议的设计不再允许操作者触发暂停,这消除了按模块隔离暂停的需要,并实现了通用存款暂停机制,降低了 Guardian 串通攻击的风险。
interface IDepositSecurityModule {
function pauseDeposits(
uint256 blockNumber,
Signature memory sig
) external
}
误报的风险仍然存在,在这种情况下,影响将更大,因为暂停将影响所有模块的存款。然而,在 Guardian 串通的情况下对协议的影响仍然显著更大。
考虑误报的最坏情况。计算未考虑许多因素,但可以估算数量级。在 3 天(治理的响应时间)内,每天都不会存入 150,000 ETH(每日质押限额)。这相当于每天启动 4687 个验证者。假设一个验证者的平均奖励为每天 0.0034 ETH,那么 3 天内的总损失将达到 96 ETH = 16 ETH + 32 ETH + 48 ETH。
基于协议当前每天 ~1000 ETH 的收益,这相当于协议在 3 天内损失利润 3.2% = 96 ETH / (3 * 1000 ETH)。
考虑到协议大约在 4 年内达到 1000 万 TVL,平均每日质押量约为 7k(1000 万 / 365 / 4),而根据最近 6 个月的统计,这一数字为每天 8k。这比最坏情况下考虑的限额低 20 倍。这使我们有理由认为,实际数字将比最坏情况低一个数量级。
解除暂停(Unpause)
提议将解除暂停的过程交由治理处理。
Guardian 余额
Council Daemon 在取消审查和暂停操作上会花费 ETH。由于这些操作至关重要,因此绝对有必要监控每个 Daemon 的余额。为此,提议引入 2 个阈值,并组织针对它们的监控和告警:
- 最低余额——足以执行至少 10 次取消审查操作的余额。该数值可以在引入新模块后,根据此类操作的历史频率重新评估。
- 临界低余额——足以暂停所有已连接模块的余额。如果余额低于此阈值,Council Daemon 停止发送除暂停之外的任何交易。
Depositor Bot 和 Pause Bot 具有统一的代码库,并在一个基础设施下使用一个私钥启动。提议在同一代码库中实现 Vetting Bot,并与其一起启动。
提议将所有 DSM 执行者(Council Daemon、Depositor、Vetting 和 Pause Bot)的余额补充工作指派给 Gas Supply Committee。
检查(Checks)
在审查密钥时,以及在模块或存款合约状态变更时,Council Daemon 对密钥执行检查,确保密钥可以安全地用于存款。
签名验证
存款数据由公钥和对存款消息的签名组成。该消息中包含提款凭证、存款金额、域(domain)。Council Daemon 重建预期消息,并检查签名消息是否与预期匹配。
重复检查
Council Daemon 检查所调查的存款数据中的公钥与 Lido 的既有密钥(包括之前已存入的、在存款队列中的或尚未检查的)是否存在重复。此检查的主要任务是将原始密钥与重复密钥区分开来。为简化查找重复项的问题,提议使用 Node Operators Registry 合约中已有的 SigningKeyAdded 事件,并将其引入 IStakingModule 接口:
interface IStakingModule {
event SigningKeyAdded(uint256 indexed operatorId, bytes32 pubkey);
}
让我们考虑可能的场景及其中的行为算法。注意,检查时密钥的状态可能不同(全部或部分密钥可以是已审查或未审查的)。DSM 侧将相应做出反应或不做出反应,以达到所需状态。
同一操作者在同一模块中的重复项。 在这种情况下,索引最低的密钥被视为原始密钥,直到第一个重复项之前的所有密钥都被视为有效:
original duplicate
v v
[0,1,2,3,4,5,6,7,8,9,1]
^
vetted keys
不同操作者之间的重复项。 无论操作者位于同一模块还是不同模块,处理方式相同。上传较早的密钥被视为原始密钥。为此,链下部分会接收每个密钥按操作者和公钥分组的事件,并取最早上传的密钥作为原始密钥。
- 检查重复项中是否存在一个已存入的密钥:
- 是——所有其他密钥都被视为重复项,必须被取消审查。
- 否——进入下一步检查。
- 查找该公钥所有重复密钥的添加事件。检查是否所有密钥都有提交事件:
- 是——进入下一步检查。
- 否——异常情况。在这种情况下,这些密钥不会发生取消审查,模块进入软暂停(不再签署存款消息)。
- 按添加的区块号对所有重复项进行排序。检查最早提交的密钥是否是该区块中唯一的:
- 是——所有其他密钥都被视为重复项,必须被取消审查。
- 否——所有重复项都必须被取消审查。
由于存款数据可以被删除并重新提交,导致同一操作者的同一密钥产生多个 SigningKeyAdded 事件,因此添加事件以最早的为准。
可能存在对密钥提交交易进行抢跑的情况,在这种情况下很难确定谁先谁后,因此提议取消审查整个重复项集合。如果尝试查看日志索引,恶意行为者可以进行后置攻击(back-run)。
这种攻击在经济上意义不大,前提是影响仅限于取消审查操作者最后提交的密钥。然而,如果识别出此类攻击的尝试,提议使用私有内存池(private mempool)来缓解问题。面临此类问题的操作者应能够删除密钥并通过私有内存池提交新密钥。更严格的缓解措施可能包括通过验证者的私钥检查某些签名消息,但此类解决方案会增加验证者私钥被泄露的风险,除非万不得已,不建议使用。
模块在设计时考虑重复项攻击的特性非常重要,这有助于限制来自其他模块或本模块内部操作者的攻击。由于密钥取消审查而对操作者造成的影响应受到限制。理想情况下——仅限于使一个受攻击的密钥失效,或使最后提交的一批密钥失效。
之前已存入(Previously Deposited)
Council Daemon 检查模块中的公钥,确认它们未通过存款合约使用与 Lido 不同的提款凭证被直接存入过。
来自存款事件的签名会被验证,无效签名被拒绝。此类存款在共识层侧会被忽略。过滤此类存款可以排除通过向受攻击操作者的公钥存入 1 ETH(存款合约中的最小存款额)来审查存款队列的可能性。
包含对 Lido 提款凭证存款的存款事件会被忽略,并且不会阻止对队列中密钥的存款,除非这些密钥此前已通过 Lido 存入。此类密钥可以由 Lido 存入而没有任何后果。一旦验证者被激活,捐赠的 ETH 将由提款凭证合约收取(skimmed)。
提款凭证变更
在提款凭证变更的情况下,可以认为所有已提交但尚未存入的密钥都必须被取消审查。此类操作意味着需要在链下工具和合约状态中进行重大更改。因此,该操作预计需要协调配合。此外,Council Daemon 会从 Staking Router 合约读取提款凭证,并使用它来验证签名。如果提款凭证被更改,但仍有模块的已审查密钥的签名是针对旧提款凭证生成的,这将导致所有此类密钥被定期取消审查。
约束
密钥审查仅用于验证密钥对存款的适用性,并受 检查 部分所述检查的约束。其他限制操作者存款的条件必须由模块在合约代码中单独强制执行。此类条件可能包括,例如,存在卡住的密钥、设定的目标限额或不足的保证金(bond)。
DSM 仅对处于活跃(Active)状态的模块执行监控以及审查和取消审查操作;处于其他状态的模块在智能合约层面禁止存款。已停用的操作者也会被忽略。
当批量审查和取消审查操作者时,Council Daemon 必须按索引从小到大对操作者数组进行排序。
接口变更摘要
Staking Router
interface IStakingRouter {
function decreaseStakingModuleVettedKeysCountByNodeOperator(
uint256 _stakingModuleId,
bytes calldata _nodeOperatorIds,
bytes calldata _vettedSigningKeysCounts
) external;
struct StakingModule {
uint24 id;
address stakingModuleAddress;
uint16 stakingModuleFee;
uint16 treasuryFee;
uint16 targetShare;
uint8 status;
string name;
uint64 lastDepositAt;
uint256 lastDepositBlock;
uint256 exitedValidatorsCount;
// 从 DSM 迁移的新参数
uint64 maxDepositsPerBlock;
uint64 minDepositBlockDistance;
}
}
Staking Module
interface IStakingModule {
function decreaseVettedSigningKeysCount(
bytes calldata _nodeOperatorIds,
bytes calldata _vettedSigningKeysCounts
) external;
event SigningKeyAdded(uint256 indexed operatorId, bytes32 pubkey);
}
Deposit Security Module
interface IDepositSecurityModule {
bytes32 public immutable UNVET_MESSAGE_PREFIX;
uint256 internal lastDepositBlock;
uint256 internal maxOperatorsPerUnvetting;
function unvetSigningKeys(
uint256 blockNumber,
bytes32 blockHash,
uint256 stakingModuleId,
uint256 nonce,
bytes calldata nodeOperatorIds,
bytes calldata vettedSigningKeysCounts,
Signature calldata sig
) external;
function pauseDeposits(
uint256 blockNumber,
// stakingModuleId 已被移除
Signature memory sig
) external;
function getLastDepositBlock() external returns (uint256);
function getMaxOperatorsPerUnvetting() external returns (uint256);
function setMaxOperatorsPerUnvetting(uint256 newValue) external;
}
Data Bus 消息
enum UnpassedCheck {
depositDataSignature,
pubkeyDuplicate,
previouslyDeposited
}
interface MessageInvalidKeysToVet extends MessageBase {
blockNumber: number,
blockHash: string,
depositRoot: string,
stakingModuleId: number,
nonce: number,
operatorId: number,
invalidKeyIds: number[],
unpassedCheck: UnpassedCheck,
signature: Signature,
}
interface MessageVetSigningKeys extends MessageBase {
blockHash: string;
blockNumber: number,
depositRoot: string,
stakingModuleId: number,
nonce: number,
nodeOperatorIds: string,
vettedSigningKeysCounts: string,
signature: Signature,
}
interface MessageUnvetSigningKeys extends MessageBase {
blockHash: string;
blockNumber: number,
stakingModuleId: number,
nonce: number,
nodeOperatorIds: string,
vettedSigningKeysCounts: string,
signature: Signature,
}
⚔️ 新的攻击向量
重复项
随着多个模块的出现,一个模块的操作者可以利用协议中已存入密钥必须唯一的属性来影响另一个模块操作者的状态。例如,模块 A 的操作者可以上传模块 B 中已有的密钥,协议应对此采取措施。这个问题在单个模块内的操作者之间也同样存在,并且由于无需许可模块的出现而变得更加尖锐,因为协议对这些操作者的制约手段更少。
缓解措施包括更仔细地识别原始密钥和报告重复项,详见 检查 部分。
Guardian 串通的新条件
随着无需许可模块的出现,有必要重新考虑串通场景,因为引入无需许可模块和具有 FIFO 的模块可能会改变攻击模式。
考虑一个潜在场景:
- Guardian 串通足够数量以构成法定人数来发起攻击
- 恶意 Guardian 生成带有 Lido 提款凭证的有效存款数据,并上传到具有无需许可准入的模块,在模块合约上为每个验证者锁定一笔保证金。在最坏的情况下,当模块实现 FIFO 队列时,所有攻击密钥将按顺序排列
- 恶意 Guardian 通过不签署存款消息来拖延其他模块的存款,从而积累缓冲并将队列推进到他们的密钥(假设每个模块的目标限额允许持有这么多质押量)
- 在等待队列到达攻击密钥并且缓冲量很大之后,恶意 Guardian 将准备好的每笔 1 ETH 的存款(使用他们自己的提款凭证)发送到存款合约
- 之后他们签署存款消息并通过 DSM 合约执行存款
至少一个诚实的 Guardian 通过以下方式缓解潜在攻击:
- 在检测到抢跑攻击尝试时发送取消审查密钥交易。如果攻击执行得正确且交易被捆绑在一起,这可能无济于事
- 在检测到已完成的抢跑攻击时暂停存款
DSM 对存款频率和单次存款数量有限制。当前值:交易之间间隔 25 个区块,Curated 每次最多 150 个密钥,CSM 每次最多 30 个密钥。因此,至少一个诚实的 Guardian 有足够的时间窗口做出反应,并且在 Guardian 串通的情况下可被盗的资金量是有限的。
总之: 通过具有 FIFO 的无需许可模块进行攻击比通过 Curated 模块进行攻击更容易。同时,攻击变得成本更高,因为每个验证者都需要保证金。
不同模块属性会改变攻击条件,提议通过每个模块独立的 maxDepositsPerBlock 和 minDepositBlockDistance 参数来缓解。详细信息见 存款 部分。
还提议通过暂停所有模块来缓解一次性攻击的损害。详细信息见 暂停 部分。
审查 DSM 交易
模块操作者可以通过执行任何改变模块 nonce 的密钥操作来抢跑 DSM 交易,从而对其进行审查。攻击成本很高,需要在每个区块中进行一次密钥操作交易,且影响仅限于延迟存款。缓解攻击涉及在 Curated 模块中对操作者拥有制约手段,以及在无需许可模块中对某些操作(如删除密钥)收取费用。
🙈 超出范围
变更的目标是解锁将新模块(如 CSM)添加到 Staking Router,因此提议的变更仅限于最小必要集合。其他可能有用的改进被有意排除在范围之外,但可以在主要范围之外单独进行。
Data Bus 去中心化
当前链下工具的设置支持 Rabbit MQ 和 Kafka 作为数据总线,需要中心化服务器。过渡到去中心化数据总线解决方案可以降低法律和基础设施故障风险,并允许任何人使用来自公开可访问数据总线的签名消息进行存款或执行存款暂停。
此改进独立于主要范围,可以并行进行。将在单独文档中说明。
已存入密钥注册表
此改进保证链上不会对同一密钥进行存款。然而,它并不能完全解决重复项问题,所有检查仍然是必要的。存储所有之前已存入密钥的根可以使 ZK Oracle 中的检查成本更低。
此改进独立于主要范围,可以并行进行。将在单独文档中说明。
BLS 预编译
以太坊中 EIP-2537 的实现可以改进所提交存款数据签名检查的流程,并在链上保证不存在无效签名。然而,这些变更超出范围,因为 DSM 的变更计划在 Prague/Electra 硬分叉之前交付,而该硬分叉可能包含 BLS Precompiled。
预计 DSM 的下一次迭代应包括对 EIP-2537 集成的研究。
最大有效余额
以太坊中 EIP-7251 的实现可能需要显著改变存款流程和记账:重新审视重复项的处理、模块中密钥的记账、Lido 合约中已存入验证者的记账等。提议的 DSM 设计未考虑在实现该 EIP 时协议可能需要的变化。
基于 DVT 的模块
基于 DVT 的模块的出现可能导致重新考虑协议与操作者的关系,因为该技术意味着多个操作者与一个验证者的关系。提议的 DSM 设计未考虑由于基于 DVT 的模块的出现而可能导致的协议变化。
🚧 已知问题
- 审查存在不同的事实来源(已通过单一取消审查来源得到部分缓解),但仍可能导致审查与取消审查之间的竞争(race)。
- 具有相同
nonce的模块密钥替换漏洞仍然存在(Critical-02)。可通过模块的代码审计来缓解。
- 原文链接: hackmd.io/@lido/rJrTnEc2...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~