VEBO 改进
本文是 Lido 针对验证器退出总线预言机(VEBO)的设计改进提案。核心内容包括两方面:一是新增“增强退出模式”(Boosted Exit Requests),允许质押模块在提款队列(WQ)没有需求时,也能信号 VEBO 要求特定运营商退出验证器,主要通过将 targetLimit 的布尔参数改为枚举模式(禁用、平滑退出、增强退出)实现,便于未来无许可模块应对运营商债券不足等场景;二是在质押路由器中引入“目标份额”区间机制,将原 targetShare 改名为 stakeShareLimit,并新增 priorityExitShareThreshold,当模块份额超过该阈值时,其验证器会获得更高的退出优先级,避免模块份额因其他模块退出而超限。文章还详细描述了退出排序算法中新增的“增强目标限制”与“模块份额”谓词,以及两阶段退出流程(覆盖 WQ 需求和增强退出)。兼容性测试已通过,对链下工具和链上集成的破坏性有限。

🎯 TL;DR
本提案建议对验证者退出总线预言机(VEBO)中的验证者退出算法进行修改。在质押模块方面,建议增加一种能力,使模块能够向 VEBO 发出信号,表明需要从某个节点运营商退出验证者,而不考虑提款队列(WQ)中的需求。在质押路由器方面,建议在选择验证者退出优先级时考虑模块的份额。
本文假设读者熟悉 VEBO 的当前设计:
- https://docs.lido.fi/contracts/validators-exit-bus-oracle
- https://docs.lido.fi/guides/oracle-spec/validator-exit-bus
🌟 动机
加速退出请求
当前 VEBO 设计假定,只有在存在用户提款请求时,才能请求验证者退出。虽然协议有工具可对特定节点运营商的验证者退出请求进行优先级排序,但没有工具可以在不考虑 WQ 需求的情况下,请求某个节点运营商退出验证者。
不考虑用户提款请求的退出请求,对即将推出的无需许可模块非常重要,这样可以在节点运营商保证金减少时,向 VEBO 发出需要退出验证者的信号。在用户提款请求不足的情况下,它还可用于节点运营商的退场,或减少其在池中的份额。
这促使我们考虑在协议中添加一种特殊模式,以便在不考虑 WQ 需求的情况下,从节点运营商退出验证者。
目标份额
质押路由器的当前设计在模块之间分配质押时,使用每个模块的 targetShare 参数。然而,在选择请求哪些验证者退出的算法中,并未考虑该参数。这可能导致一种情况:某个模块达到了其目标份额,之后有大量验证者从其他模块退出,导致该模块的份额上升并超过目标份额。由于模块的份额可能是基于安全考虑而选择的,因此建议设计一种机制,不仅在质押分配时考虑该参数,在请求验证者退出时也考虑它。
💅 拟议设计
加速退出请求
当前 VEBO 设计的前提是,只有在 WQ 合约中存在用户提款请求时,才会请求验证者退出。模块可以使用 targetLimit 机制向 VEBO 发出信号,表明从特定节点运营商退出验证者的优先级。
本提案建议允许模块向 VEBO 发出信号,表明需要向特定节点运营商发送退出验证者的请求,而无需考虑 WQ 中的需求。为此,建议修改当前的 targetLimit 工具,增加一种额外的加速退出模式。具体而言,建议将参数 bool isTargetLimitActive 替换为 uint8 targetLimitMode,其取以下值之一:
0– 禁用1– 限制质押,平滑退出模式2– 限制质押,加速退出模式
下面更详细地介绍目标限制模式:
禁用。 该模式意味着没有任何限制。节点运营商在接收新质押方面不受限制,在选择验证者退出时也没有额外优先级。
平滑退出模式。 节点运营商对活跃验证者数量设有上限。只要节点运营商的活跃验证者数量不超过 targetLimit,该运营商就按常规条件接收质押。一旦达到该值,运营商将停止接收新质押(应在模块层面实现)。如果节点运营商的活跃密钥数量超过 targetLimit,则该运营商的验证者将按目标退出验证者数量获得退出优先级。
加速退出模式。 与平滑模式类似,但不考虑 WQ 中的需求。该运营商的验证者将按目标退出验证者数量获得退出优先级,并在不考虑 WQ 需求的情况下被请求退出。
添加新模式不会影响每份报告中验证者退出的总体限制。如果需要加速退出大量验证者,将通过多份报告来完成。
本提案建议对 IStakingModule 接口进行如下修改:
interface IStakingModule {
function getNodeOperatorSummary(uint256 _nodeOperatorId) external view returns (
uint8 targetLimitMode, // 原为 isTargetLimitActive
uint256 targetValidatorsCount,
uint256 stuckValidatorsCount,
uint256 refundedValidatorsCount,
uint256 stuckPenaltyEndTimestamp,
uint256 totalExitedValidators,
uint256 totalDepositedValidators,
uint256 depositableValidatorsCount
);
function updateTargetValidatorsLimits(
uint256 _nodeOperatorId,
uint8 _targetLimitMode, // 原为 _isTargetLimitActive
uint256 _targetLimit
) external;
}
对 updateTargetValidatorsLimits 方法的修改不会破坏与 EasyTrack 工厂 UpdateTargetValidatorLimits 的向后兼容性,该工厂用于在 Simple DVT 模块中设置限制。尽管如此,仍建议对其进行更新,以配合 IStakingModule 接口的变更。
interface IUpdateTargetValidatorLimits {
struct TargetValidatorsLimit {
uint256 nodeOperatorId;
uint8 targetLimitMode; // 原为 isTargetLimitActive
uint256 targetLimit;
}
}
将 bool isTargetLimitActive 改为 uint8 targetLimitMode 会影响视图方法 getNodeOperatorSummary 的返回值,该返回值可能被外部集成和链下工具使用。测试表明向后兼容性保持不变:https://github.com/lidofinance/sr-1.5-compatibility-tests。除禁用模式外,基于旧接口的解码器会将所有其他模式解释为已启用的 targetLimit 模式。
链下部分的变更在 退出顺序 一节中描述。
目标份额
当验证者从某个模块退出时,该模块的份额会减少,而其他模块的份额会增加。这可能导致某个模块的份额显著超过其目标份额。

上图展示了验证者从模块 $A$ 退出的情况,导致模块之间的份额重新分配:模块 $A$ 的份额减少,而模块 $B$ 的份额增加。
最直接、最简单的解决方案可能是在验证者退出优先级排序时考虑目标份额。然而,该方案存在一个问题——协议的 TVL 具有一定波动性,且不同模块中的节点运营商重新加入验证者的成本各不相同。这些差异可能体现在节点运营商本身(solo staker 或大公司)、流程自动化程度以及技术特性(普通验证者或 DVT)等方面。TVL 的波动加上不同的重新加入成本,可能导致协议在重新加入成本较高的模块中定期轮换验证者——先请求退出,过一段时间后又进行质押。
鉴于上述问题以及直接方案的缺点,本提案建议将模块的目标份额表示为一个数值范围:stakeShareLimit 和 priorityExitShareThreshold,其中 $stakeShareLimit \le priorityExitShareThreshold$。
较低的值 stakeShareLimit 表示在模块之间分配质押时,可以分配给某个模块的最大份额。该参数其实就是当前的 targetShare。尽管如此,仍建议对其重命名,因为当前名称并未完全反映其实质。
较高的值 priorityExitShareThreshold 表示模块的份额阈值,一旦超过该阈值,该模块中的验证者退出将被优先处理。

这两个值使得模块在任意时刻处于以下三种状态之一:
模块未达到份额上限
$currentShare < stakeShareLimit$。本提案对该状态不做任何更改。一切照旧:
- 质押路由器优先将质押分配给处于此状态的模块
- 处于此状态的模块中的验证者在退出时没有任何额外优先级
模块已达到质押份额上限
$stakeShareLimit \le currentShare \le priorityExitShareThreshold$。本提案对该状态不做任何更改。一切照旧:
- 质押路由器不会向处于此状态的模块分配质押
- 处于此状态的模块中的验证者在退出时没有任何额外优先级
模块超过优先退出阈值
$priorityExitShareThreshold < currentShare$。该状态才会受到本提案变更的影响。
- 质押路由器不会向处于此状态的模块分配质押
- 处于此状态的模块中的验证者具有更高的退出优先级
本提案建议对 Staking Router 合约接口进行如下修改:
interface IStakingRouter {
struct StakingModule {
uint24 id;
address stakingModuleAddress;
uint16 stakingModuleFee;
uint16 treasuryFee;
uint16 stakeShareLimit; // 原为 targetShare 参数
uint8 status;
string name;
uint64 lastDepositAt;
uint256 lastDepositBlock;
uint256 exitedValidatorsCount;
uint16 priorityExitShareThreshold; // 新参数
}
event StakingModuleStakeShareLimitSet(uint256 indexed stakingModuleId, uint256 stakeShareLimit, address setBy);
event StakingModulePriorityExitShareThresholdSet(uint256 indexed stakingModuleId, uint256 priorityExitShareThreshold, address setBy);
function updateStakingModule(
uint256 _stakingModuleId,
uint256 _stakeShareLimit, // 原为 _targetShare 参数
uint256 _priorityExitShareThreshold, // 新参数
uint256 _stakingModuleFee,
uint256 _treasuryFee
) external;
function addStakingModule(
string calldata _name,
address _stakingModuleAddress,
uint256 _stakeShareLimit, // 原为 _targetShare 参数
uint256 _priorityExitShareThreshold, // 新参数
uint256 _stakingModuleFee,
uint256 _treasuryFee
) external;
}
向后兼容性
对 StakingModule 结构体的更改会影响某些视图方法的返回值,这些返回值可能被外部集成和链下工具使用:
- getStakingModule
- getStakingModules
- getStakingModuleDigests
- getAllStakingModuleDigests
测试表明,无论是对链下工具还是可能的链上集成,向后兼容性都得以保持:https://github.com/lidofinance/sr-1.5-compatibility-tests。修改后的返回值可以使用标准的 Solidity 工具和 ethers 库正确解码。返回值中的新增字节会被忽略。
链下部分的变更在 退出顺序 一节中描述。
退出顺序
本提案建议分两个阶段选择退出验证者:
- 覆盖 WQ 需求
- 加速退出
第 1 阶段:覆盖 WQ 需求
该阶段沿用 VEBO 的当前设计。为了确定请求哪些验证者退出,链下预言机会从可退出的 Lido 验证者排序列表中逐个选取项,直到退出验证者和未来奖励覆盖 WQ 中的需求,或达到每份报告的限制为止。
验证者按以下谓词顺序进行排序:
- 延迟验证者数量最少的节点运营商所对应的验证者
- 目标加速退出验证者数量最多的节点运营商所对应的验证者
- 目标平滑退出验证者数量最多的节点运营商所对应的验证者
- 模块份额偏离度最大的模块中的验证者
- 质押权重最高的节点运营商所对应的验证者
- 验证者数量最多的节点运营商所对应的验证者
- 索引最低的验证者
本提案的变更影响第 2、3、4 条谓词(加粗显示),其余谓词保持不变。下面详细说明这些变化的谓词:
加速目标限制谓词。 如果 targetLimitMode 设置为加速退出,则结果为要退出的目标验证者数量;在其他情况下,该谓词返回 0。返回值越高,验证者退出的优先级越高。
def predicate_boosted_target_limit():
if operator.target_limit_mode == TARGET_LIMIT_MODE.BOOSTED_EXITS:
return max(operator.predictable_validators_count - operator.target_limit, 0)
return 0
平滑目标限制谓词。 如果 targetLimitMode 设置为平滑限制,则结果为要退出的目标验证者数量;在其他情况下,该谓词返回 0。返回值越高,验证者退出的优先级越高。
def predicate_smooth_target_limit():
if operator.target_limit_mode == TARGET_LIMIT_MODE.SMOOTH_EXITS:
return max(operator.predictable_validators_count - operator.target_limit, 0)
return 0
模块份额谓词。 该谓词会优先让验证者数量超过退出阈值的模块中的验证者退出。分配给某模块的预期验证者数量超出退出阈值的部分越多,该模块中验证者的退出优先级就越高。
预期验证者是指在所有效果生效后模块中将存在的活跃验证者数量:所有已请求退出的验证者均已退出,近期进行的质押存款已转化为活跃验证者。
def predicate_module_share():
module_max_validators = total_validators * priority_exit_share_threshold
if module_predicted_validators > module_max_validators:
# 模块中验证者数量超出退出阈值越多,退出优先级越高
return module_max_validators - module_predicted_validators
return 0
每完成一次验证者选择迭代后,节点运营商和验证者的状态都会发生变化——先前已发送退出请求的验证者会被标记,随后验证者数组会重新排序。该阶段结束后,状态会传递给下一阶段——加速退出,因此前一阶段已请求的验证者在后续阶段中也会被考虑。
- 目标限制谓词现在被拆分为两个:加速退出和平滑退出
- 与
targetLimitMode设置为平滑限制的运营商相比,targetLimitMode设置为加速退出的运营商所对应的验证者具有更高的退出优先级 - 新的模块份额谓词优先让验证者数量超过退出阈值所允许范围的模块退出验证者
第 2 阶段:加速退出
第二阶段预设:无论 WQ 中的需求如何,需要退出的验证者都会被请求退出。对第一步之后的节点运营商和验证者状态进行过滤,仅保留 targetLimitMode 设置为加速退出的运营商所对应的验证者。
验证者按目标退出验证者数量进行排序。每完成一次验证者选择迭代后,节点运营商和验证者的状态都会发生变化——先前已发送退出请求的验证者会被标记,随后验证者数组会重新排序。选择会持续进行,直到数组中不再有元素,或每份报告中退出的运营商总数达到上限为止。
该阶段只有一个排序谓词:
- 目标加速退出验证者数量最多的节点运营商所对应的验证者
- 索引最低的验证者
下面看几个不同的示例。珊瑚色表示由于节点运营商启用了加速目标限制而被请求退出的验证者,灰色表示按常规顺序请求退出的验证者。

在上面的示例中,只有算法的第一阶段在起作用。WQ 中的需求一部分由加速退出覆盖,另一部分由按常规顺序排列的其他验证者覆盖。第二阶段返回 0 个需要退出的验证者,因为此时所有处于加速退出状态的运营商的验证者都已被请求退出。

在上面的示例中,算法的两个阶段都在起作用。WQ 中的需求完全由加速退出覆盖,之后算法的第二阶段继续退出验证者,直到目标加速退出验证者数量等于 0。

在上面的示例中,启用了加速退出模式的运营商存在延迟退出的验证者。因此,协议转而从其他运营商退出验证者,以覆盖 WQ 中的需求。需求被覆盖后,算法的第二阶段从启用了加速退出模式的运营商处请求退出了下一批验证者。
- 该谓词使用上一阶段的状态,以确保所有先前请求的验证者都被纳入计算
- 验证者的选择仅从目标加速退出验证者中进行
- 如果启用了加速退出模式的运营商延迟退出,WQ 中的需求将由其他运营商覆盖
🙈 超出范围
退出特定验证者
当前方案不涵盖请求退出某运营商特定验证者的场景。验证者按照索引从小到大的顺序被请求退出,即按照它们被存入的顺序。然而,在某些情况下,可能需要不按存入顺序请求验证者退出,例如验证者私钥丢失,或者 DAO 怀疑验证者的私钥可能已被泄露。
退出特定验证者的功能在另一项研究中讨论,并由优先退出总线(PEB)功能覆盖。该功能允许按优先级顺序请求验证者退出,且无需考虑 WQ 中的需求。
💖 致谢
本文档部分基于 Raman S 和 KRogLA 的工作:
- 原文链接: hackmd.io/@lido/BJXRTxMR...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~