CSM v2升级:提升社区质押模块的灵活性与去中心化

lido__ 发布于 2025-06-17 阅读 10

本文是Lido贡献者针对社区质押模块(CSM)v2升级的技术愿景提案。核心目标是增强模块的灵活性与去中心化,支持不同类型的节点运营商(如独立社区质押者、专业运营商等)并为其提供差异化条件。提案提出引入Entry Gates和Extensions,使节点运营商的加入路径可定制和扩展;通过参数注册表(CSParametersRegistry)实现按运营商类型配置奖励、押金、性能门槛等参数;引入优先级队列,让特定社区质押者能优先获得存款;新增坏性能strikes系统,自动识别并惩罚持续表现不佳的验证者;更新性能预言机指标,综合考量验证者的证明、区块提议和同步委员会职责;并支持EIP-7002执行层触发退出。此外还提出推荐计划以吸引更多独立质押者,并计划移除单独的slash上报机制。整体设计旨在让CSM适应不同参与者需求,提升协议的安全性和效率。

这是部分 Lido 贡献者对 CSM 重大升级 CSM v2 的一份愿景。欢迎社区提供意见和评论!

image

首个版本的社区质押模块(Community Staking Module, CSM)自 2024 年 10 月起已在主网上线。应当启动关于 CSM 第二版本的研究和开发,以确保及时交付与 Pectra 硬分叉相关的更新以及已经发现的各项改进。

范围

CSM 新版本中提议实现若干重要功能和改进。以下为功能的简短摘要:

image

关于 EIP-7251 的说明:提高 MAX_EFFECTIVE_BALANCE

随着 EIP-7251 的引入,以太坊验证者可以被整合为大型(2048 ETH)验证者,或从一开始就以大型验证者的形式创建。由于对等节点数量的减少,这将显著降低 P2P 网络的负载。然而,由于无法为大型验证者指定“自定义上限”,质押者只能在小型(32 ETH)和大型(2048 ETH)验证者之间选择。就整体协议资本效率而言,只有当整合后的大型验证者余额 $\ge 1700$ ETH 时,将小型验证者整合为大型验证者或创建大型验证者才有意义(参见 Lido 贡献者的研究结果)。CSM 名称中的“Community”一词意味着该模块的目标受众是平均运行约 10-20 个验证者的社区质押者。这意味着对大多数社区质押者而言,整合对协议来说将是资本低效的,尽管仍需进一步分析以了解可能的整合对 CSM 验证者格局的整体影响(例如,0x02 验证者对运行 $X$ 个验证者、在需要时执行部分提款等相关运营 Gas 成本的影响等)。同时,考虑到每个验证者的 bond 要求较低(目前为 1.3 ETH,理论上在 CSM v2 中还有可能降低),社区质押者不会从整合中获得太多好处。鉴于此,并且由于如果没有底层 Lido on Ethereum 协议的相应变更,CSM 无法支持大型验证者,因此建议在以后(例如与 CSM v3 一起)再考虑对大型验证者的支持。

然而,一旦获得更多关于每位 Operator 平均验证者数量的真实数据,并且 Lido on Ethereum 协议引入对大型验证者的支持,未来可能会重新考虑支持由 EIP-7251 启用的 0x02 类型验证者。

关于 Preconfirmations 支持的说明

Preconfirmations 是以太坊领域的热门话题。目前正在研究在 Lido 协议层面实现 preconfirmations 支持。根据研究成果,应考虑将附加功能纳入 CSM v2。

产品原理

在深入探讨 CSM v2 的功能之前,提供简短的产品原理至关重要。

Entry Gates 与 Extensions

这一概念使 CSM 的准入变得模块化和可扩展。Entry Gates 允许为 CSM 提供无限的策展/自动化/自定义准入路径。Extensions 允许第三方在 CSM 之上构建其产品,并且只需极低的安全和信任假设,因为核心 CSM 代码保障了核心协议的安全。

Priority Queues

该功能对于提高独立社区质押者在协议中的参与度至关重要。与 permissionless(未知)operators 相比,它能让已识别的社区质押者以更快捷的方式加入协议,并直接影响 Lido on Ethereum 协议的去中心化程度。

参数注册表

这是支持不同 Node Operator 类型所需的简单使能器。它允许 CSM 适应不断变化的市场条件,并为每种类型提供单独的处理。

不良表现 strikes

质押协议需要有某种方式来保护自身免受恶意或表现不佳的 Node Operators 的侵害。在 permissionless 系统中,这只能自动完成。strikes 系统是一种自动化工具,可防止潜在的表现问题对协议的整体表现和健康产生重大影响。

Node Operator 类型

显而易见,CSM 参与者可以分为几种不同的类型:

  • Permissionless(未知)参与者;
  • 已识别的独立社区质押者;
  • 专业 Node Operators(例如使用自有资金、客户资金运行,或在此基础上提供 Validators as a Service 产品);
  • 其他。

定义这些类型以及识别特定 Node Operator 所属类型的机制,意味着诸如 Node Operator 奖励份额、表现阈值、存款队列优先级、不良表现驱逐参数、与以太坊网络无关的罚金(keyRemovalChargeElRewardStealingAdditionalFine)等参数都可以在一个模块中进行定制,而不必拥有多个不同的模块来迎合不同的 NO 细分群体。

CSM v2 的主要精神是继续支持社区质押者,并通过为不同的 Node Operator 类型提供个性化条件,尤其是社区质押者来增加其数量。

为了实现这种特殊对待,应引入下面列出的若干功能。

Entry Gates 与 Extensions

CSModule.sol 目前有几种创建 Node Operators 的方法。在 Early Adoption(EA)期间,这些方法只允许 EA 列表中的成员加入。EA 期结束后,这些方法变为 permissionless。这种方法在定制化方面极为有限。因此,我们提出了 Entry Gates 和 Extensions 的概念。

为实现这一概念,CSModule.sol 中的 Node Operator 创建方法应设为 permissioned。

注意:CSModule.sol 层面上 Node Operator 创建方法设为 permissioned 并不意味着不允许 permissionless 进入,只是它将向技术栈更深一层移动。

只有相应角色(CREATE_NODE_OPERATOR_ROLE)的成员才能调用这些方法。这些角色成员就是我们所说的 Entry Gates 和 Extensions。

Entry Gates

image

Entry Gates 是允许用户加入 CSM 的智能合约。有两种类型的 entry gates:

  • Permissionless
  • Permissioned

Entry Gate 可以为使用它创建的 operators 分配自定义的 Node Operator 类型(由 bondCurveId 定义)。

Permissioned entry gate 可以允许现有的 Node Operators 证明其有资格获得相应的 Node Operator 类型,并将其现有的 CSM Node Operator 升级为该类型(对应的 bondCurveId)。

Entry Gates 示例和代码片段

Permissionless entry gate

这是一个无状态合约,代理 addNodeOperator* 调用而不进行额外操作。它通过强制上传 keys 来维护不变式。

PermissionlessGate.sol

import { ICSAccounting } from "./interfaces/ICSAccounting.sol";
import { ICSModule, NodeOperatorManagementProperties } from "./interfaces/ICSModule.sol";

contract PermissionlessGate {
    /// @dev Curve ID 是来自 accounting 合约的默认 bond curve ID
    ///      无需显式设置
    uint256 public immutable CURVE_ID;

    ICSModule public immutable CSM;

    constructor(address csm) {
        CSM = ICSModule(csm);
        CURVE_ID = CSM.accounting().DEFAULT_BOND_CURVE_ID();
    }

    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        address referrer
    ) external payable returns (uint256 nodeOperatorId) {
        nodeOperatorId = CSM.createNodeOperator(
            msg.sender,
            managementProperties,
            referrer
        );

        CSM.addValidatorKeysETH{ value: msg.value }({
            from: msg.sender,
            nodeOperatorId: nodeOperatorId,
            keysCount: keysCount,
            publicKeys: publicKeys,
            signatures: signatures
        });
    }

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        address referrer
    ) external returns (uint256 nodeOperatorId) {  ...  }

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        address referrer
    ) external returns (uint256 nodeOperatorId) {  ...  }
}
Vetted Entry Gate

一个仅允许通过 Merkle Tree 审核通过的地址加入的 entry gate。此外,它为创建的 Node Operator 设置特殊的 Node Operator 类型(bondCurveId)。

VettedGate.sol

import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import { ICSModule, NodeOperatorManagementProperties, NodeOperator } from "./interfaces/ICSModule.sol";
import { ICSAccounting } from "./interfaces/ICSAccounting.sol";

contract VettedGate {
    uint256 public immutable CURVE_ID;

    ICSModule public immutable CSM;

    /// @dev 合格成员 Merkle Tree 的根
    bytes32 public treeRoot;

    mapping(address => bool) internal _consumedAddresses;

    constructor(
        bytes32 _treeRoot,
        uint256 curveId,
        address csm,
        address admin
    ) {
        CSM = ICSModule(csm);
        CURVE_ID = curveId;
        _setTreeRoot(_treeRoot);
    }

    /// @dev 首先检查 msg.sender 的资格
    ///      设置 bond curve。这需要在 CSM 侧授予 `SET_BOND_CURVE` 角色
    ///      然后,按照给定 curve 的 bond 要求上传 keys
    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        bytes32[] calldata proof,
        address referrer
    ) external payable returns (uint256 nodeOperatorId) {
        _consume(proof);

        nodeOperatorId = CSM.createNodeOperator(
            msg.sender,
            managementProperties,
            referrer
        );
        CSM.setBondCurve(nodeOperatorId, CURVE_ID);
        CSM.addValidatorKeysETH{ value: msg.value }({
            from: msg.sender,
            nodeOperatorId: nodeOperatorId,
            keysCount: keysCount,
            publicKeys: publicKeys,
            signatures: signatures
        });
    }

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId) {  ...  }

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId) {  ...  }

    /// @dev 为符合条件的 Node Operator 申领 bond curve
    ///      检查 msg.sender 是否为 Node Operator 的安全地址
    function claimBondCurve(
        uint256 nodeOperatorId,
        bytes32[] calldata proof
    ) external {
        NodeOperator memory nodeOperator = CSM.getNodeOperator(nodeOperatorId);
        address nodeOperatorAddress = nodeOperator.extendedManagerPermissions
            ? nodeOperator.managerAddress
            : nodeOperator.rewardAddress;
        if (nodeOperatorAddress != msg.sender) revert NotAllowedToClaim();
        _consume(proof);

        CSM.setBondCurve(nodeOperatorId, CURVE_ID);
    }

    function isConsumed(address member) public view returns (bool) {
        return _consumedAddresses[member];
    }

    function verifyProof(
        address member,
        bytes32[] calldata proof
    ) public view returns (bool) {
        return MerkleProof.verifyCalldata(proof, treeRoot, hashLeaf(member));
    }

    function hashLeaf(address member) public pure returns (bytes32) {
        return keccak256(bytes.concat(keccak256(abi.encode(member))));
    }

    function _consume(bytes32[] calldata proof) internal {
        if (isConsumed(msg.sender)) revert AlreadyConsumed();
        if (!verifyProof(msg.sender, proof)) revert InvalidProof();
        _consumedAddresses[msg.sender] = true;
        emit Consumed(msg.sender);
    }

    function _setTreeRoot(bytes32 _treeRoot) internal {
        treeRoot = _treeRoot;
        emit TreeRootSet(_treeRoot);
    }
}

Extensions

image

与 entry gates 类似,extensions 是允许 Node Operators 加入 CSM 的智能合约。Extensions 也以某种形式抽象了 Node Operator 管理。Extension 可以将自身设置为 CSM Node Operator 的 manager 和/或 reward address 来实现这一点。这允许 extension 实现自定义的 Node Operator 管理原则,例如从 Node Operator 奖励中抽取奖励份额,或只允许上传经过验证的验证者 keys。

DVT 驱动的 extension 是一个很好的但并非详尽的例子。由于 DVT 假定单个验证者由集群参与者共同操作,因此 DVT 驱动的 extension 可以管理各个集群参与者并分享奖励。DVT 驱动的 extension 的另一个可能方面是链上 key 验证,确保只有通过 DKG 创建的 keys 才能通过此 extension 上传到 CSM。

Extensions 示例和代码片段

ExtensionExample.sol

contract ExtensionExample {
    /// @dev Extension 需要保存真实的 Node Operator 地址以便与之交互
    ///      Extension 应有相应的方法来更改这些地址。它可以与 CSM 本身使用相同的系统,也可以是特定的系统
    struct ExtensionNodeOperator {
        uint64 nodeOperatorId;
        address managerAddress;
        address rewardAddress;
    }

    mapping(uint64 => ExtensionNodeOperator) public extensionNodeOperators;

    /// @dev 为符合条件的成员分配的 bond curve 的 Id
    uint256 public immutable CURVE_ID;

    /// @dev Community Staking Module 的地址
    ICSModule public immutable CSM;

    constructor(
        uint256 curveId,
        address csm,
        uint256 extension_share_bp
    ) {
        CSM = ICSModule(csm);
        CURVE_ID = curveId;
    }

    /// @dev 主要区别在于合约本身充当 node operator
    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        bytes32[] calldata proof,
        address referrer
    ) external payable returns (uint256 nodeOperatorId) {
        nodeOperatorId = CSM.createNodeOperator(
            address(this),
            managementProperties,
            referrer
        );

        extensionNodeOperators[nodeOperatorId] = ExtensionNodeOperator({
            nodeOperatorId: uint64(nodeOperatorId),
            managerAddress: msg.sender,
            rewardAddress: msg.sender
        });

        CSM.setBondCurve(nodeOperatorId, CURVE_ID);
        CSM.addValidatorKeysETH{ value: msg.value }({
            from: address(this),
            nodeOperatorId: nodeOperatorId,
            keysCount: keysCount,
            publicKeys: publicKeys,
            signatures: signatures
        });
    }

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId) { ... }

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId) { ... }

    function claimRewardsStETH(uint64 nodeOperatorId,
        uint256 stETHAmount,
        uint256 cumulativeFeeShares,
        bytes32[] memory rewardsProof
    ) external {
        ExtensionNodeOperator storage extensionNodeOperator = extensionNodeOperators[nodeOperatorId];
        if (extensionNodeOperator.managerAddress != msg.sender) revert NotAuthorized();

        /// 拉取奖励以计算给定份额的确切数量
        uint256 distributedShares = CSM.accounting().pullFeeRewards(
            nodeOperatorId,
            cumulativeFeeShares,
            rewardsProof
        );

        uint256 claimedShares = CSM.claimRewardsStETH(
            nodeOperatorId,
            stETHAmount,
            0,
            new bytes32[](0)
        );

        // 对分配的份额做 extension 想做的任何事,例如在合约余额中保存一部分
        // 然后将剩余部分转移到 node operator 的 reward address
    }

    /// @dev 所有其他仅允许 NO 地址调用的 CSM 方法
    ///      也应在此处暴露,例如:
    ///      - 上传更多 keys
    ///      - removeKeys
    ///      - compensateELRewardsStealingPenalty
}

对 CSM 交互的影响

Node Operator 的创建将拥有与当前类似的接口。然而,被调用的合约将不同。默认情况下,假定 CSM 将拥有两个原生 entry gates,但 entry gates 的妙处在于,它们可以在以后添加或修改,而无需更改核心合约。例如,这将允许多种不同的机制(通过专用 entry gates 运作)来识别独立的社区质押者,或更新 entry gate 用于检查资格所依据的“主数据”等。

  • Permissionless entry gate;
  • 独立社区质押者 entry gate;

Permissionless entry gate 将拥有简化的 Node Operator 创建接口(无 proof 参数):

接口

    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        address referrer
    ) external payable returns (uint256 nodeOperatorId);

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        address referrer
    ) external returns (uint256 nodeOperatorId);

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        address referrer
    ) external returns (uint256 nodeOperatorId);

独立社区质押者 entry gate 将继承现有的 CSM Node Operators 创建接口:

接口

    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        bytes32[] calldata proof,
        address referrer
    ) external payable returns (uint256 nodeOperatorId);

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId);

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId);

除了 Node Operator 创建之外,Identified Community stakers entry gate 还将为现有的 CSM Node Operators 提供一种方法,用于在以下情况下申领自定义 Node Operator 类型(有利的 bond curve):他们在 CSM v2 之前创建了 CSM Node Operator,并且在 CSM v2 启动时或之后被纳入更新后的 Identified community stakers 列表:

接口

    function claimBondCurve(
        uint256 nodeOperatorId,
        bytes32[] calldata proof
    ) external;

所有其他与 CSM 的交互将保持不变,除了 addKeys 方法接口的微小更改(添加了 from 参数):

接口

    function addValidatorKeysETH(
        address from,
        uint256 nodeOperatorId,
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures
    ) external payable;

    function addValidatorKeysStETH(
        address from,
        uint256 nodeOperatorId,
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        ICSAccounting.PermitInput calldata permit
    ) external;

    function addValidatorKeysWstETH(
        address from,
        uint256 nodeOperatorId,
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        ICSAccounting.PermitInput calldata permit
    ) external;

参数注册表

为了在 CSM v2 中支持不同的 Node Operator 类型,需要一个由两部分组成的特殊技术解决方案:

  • 类型成员的存储和管理;
  • 为 CSM 中与 Node Operator 相关的参数设置自定义值的能力

后者可以通过将现有参数移动并将新参数添加到一个单独的 registry 合约来解决,该合约将为每个 Node Operator 类型存储每个参数的单独值。第一部分由上述 Entry Gates 和 Extensions 解决。

由于 Node Operator 类型众多,并非所有类型都需要一套完整的自定义参数,因此 registry 中应存储每个参数的强制默认值,重要的是,这些默认值的设置应考虑到默认 NO 类型为“permissionless & unknown”的最“保守”方法。如果特定参数和 Node Operator 类型组合未设置自定义值,则应使用默认值。

建议将此合约命名为 CSParametersRegistry。该合约的总体方案如下:

image

应包含哪些参数?

目前,CSM 有若干参数可以迁移到 CSParametersRegistry

  • keyRemovalCharge - 从 CSM 存款队列中删除验证者 key 时收取的 stETH 数量;
  • elRewardsStealingAdditionalFine - 在 CSM 验证者提议区块时,在其窃取或错误引导的 ETH EL 奖励金额上增加的 ETH 数量;
  • performanceLeeway - 以 BP 为单位的一个值或一组按 key 区间划分的值(参见“取决于验证者 key 数量的参数”),用于 CSM Performance Oracle 计算 Node Operator 奖励分配的表现余量;

在这些参数之上,CSM v2 的新功能中还可能出现几个新参数,例如:

  • priorityQueueParams - priority queue ID 以及 Node Operator 有资格在优先存款队列中占用的席位数量;
  • rewardShare - 以 BP 为单位的一个值或一组按 key 区间划分的值(参见“取决于验证者 key 数量的参数”),确定 Node Operator 有资格从 Staking Router 分配到的模块奖励份额。所有未分配的奖励将返还给 Lido DAO 金库;
  • strikesParams - 新的 strikes 系统 的参数(lifetime、threshold);
  • badPerformancePenalty - 如果验证者因系统性不良表现而被从协议中驱逐,将被没收的罚金;
  • performanceDutyCoefficients - CSM Performance Oracle 用于计算表现评级的一组系数(参见“更新的 CSM Performance Oracle 指标”部分)。
  • keysLimit - 特定类型的 Node Operator 被允许拥有的活跃 keys 数量。这也适用于 key 上传。它可以用于 Pro-operator 类型,以限制如果给予他们特殊条件(通过具有特定 Node Operator 类型的 Entry Gate 加入)时对协议去中心化可能产生的影响。

与 Node Operator 类型相关的主要参数是 bondCurveId。该参数定义 Node Operator 类型,并且仍将存储在 CSAccounting.sol 中。创建新的 Node Operator 类型实际上意味着创建新的 bond curve。

取决于验证者 key 数量的参数

若干参数,即 performanceLeewayrewardShare,应根据 Node Operator 控制的验证者 key 数量而定,以允许有限的优惠值。performanceLeewayrewardShare 都设置为 Node Operator 控制的总验证者 key 数量的函数。

例如,对于给定 Node Operator 的前 10 个验证者 key,rewardShare 为 staking router 分配给模块的奖励的 100%;对于接下来的 10 个 keys,为 90%;对于其余 keys($>20$),为 80%

image

Priority Queues

对于 CSM v2,需要找到一种方式,让不同的 Node Operator 类型能够在 permissionless(未知)operators 之前获得存款。

CSModule 跟踪一个预定义的 priority queues 列表。每个队列由其索引标识,该索引同时用作其优先级指示器。索引为 0 的队列具有最高优先级。模块具有固定数量的队列,受部署时常量 LOWEST_PRIORITY 的限制。因此,模块可以使用 $[0; \mathrm{LOWEST_PRIORITY}]$ 范围内的任何队列,其中 LOWEST_PRIORITY 保留用作默认队列。

image

CSM v2 向模块组成中引入了一个单独的合约,称为 CSParametersRegistry,因此各个 priority queues 的配置存储在 registry 中。队列配置如下所示:

struct QueueConfig {
    uint32 priority;
    uint32 maxDeposits;
}

在此结构中,priority 指示使用哪个队列,maxDeposits 表示 Node Operator 使用 priority queue 可以获得存款的验证者 key 的最大数量。

Node Operator 可以被分配到由其 curveID 标识的组,因此每个 curveID 都有其 priority queue 配置。除了保留的队列外,配置不限于使用特定队列。我们可以设想以下设置:

curveID    QueueConfig
0          (p0, 10)
1          (p1, 10)
2          (p0, 50)
3          (p3, 10)
...

将 keys 添加到队列

Node Operator 添加新 keys 的机制如下:

  1. 获取与 Node Operator 的 curveID 关联的 QueueConfig
  2. 检查在 QueueConfig.maxDeposits 限制以下是否存在未存款且未排队的 keys,可以放入优先级为 QueueConfig.priority 的队列。
  3. 尽可能多地将 keys 添加到优先级为 QueueConfig.priority 的 priority queue,其余 keys 添加到默认队列,即 LOWEST_PRIORITY

image

请注意,Node Operator 使用其 priority queue 最多可以获得 maxDeposits 个存款。换句话说,以下场景是实际存在的:

  1. NO 上传 10(maxDeposits)个 keys 并将它们放入 priority queue。
  2. NO 改变主意并移除所有 keys。
  3. NO 在 priority queue 中的批次被跳过。
  4. NO 再次上传 10 个 keys 并将它们放入 priority queue,因为它们尚未从该队列获得任何存款。

从队列获取存款数据

机制很简单。CSModule 按照优先级顺序($0 \rightarrow \mathrm{LOWEST_PRIORITY}$)遍历队列,并依次处理队列中的批次:当一个队列耗尽时,使用下一个队列来获取批次。

image

队列清理

清理机制与存款数据检索类似。队列按优先级排序和处理,无法存款的批次将从其所在的队列中移除。

从旧方案的迁移

为了将当前队列迁移到新机制,为队列专门分配了一个单独的优先级,任何 curveID 都不能使用该优先级。LEGACY_QUEUE_PRIORITY 设置为 LOWEST_PRIORITY - 1。因此,我们有以下可用优先级范围:$[0; \mathrm{LOWEST_PRIORITY}-2]$。最终,legacy queue 将全部获得存款,预留的优先级将在 CSM 的下一个版本(v3)中释放使用。

image

对于有资格获得 priority queue 席位的 Node Operators,将提供一种特殊方法,将符合条件数量的 keys 从 legacy queue 迁移到新的 priority queue。

如果 Node Operator 有资格获得 priority queue 中的 $N$ 个席位,但已经有 $N$ 个或更多 keys 获得存款,则上述迁移方法将不起作用。详细信息请参见“将 keys 添加到队列”部分。

不良表现 strikes

当前版本 CSM 中未解决的问题之一是驱逐表现不佳的验证者。尽管这些验证者不会获得 Node Operator 奖励,但 bond rebases 将继续存在,此类验证者将对 Lido on Ethereum 协议的整体 APR 产生负面影响。因此,与其他 permissionless 协议相比,对 Lido on Ethereum 协议进行理论攻击的成本降低了。

正如 CSM Architecture 所附文档中所述,解决该问题的一种最佳方式是在 CSM 中引入不良表现 strikes 系统。

Strikes 分配

建议由单一角色负责表现 strikes 的分配 - CSM Performance Oracle。

每个 frame 一次,CSM Performance Oracle 提供一个额外的 tree root,其中包含验证者“strikes”的信息。一个 strike 意味着验证者在该 frame 中的表现低于阈值。在更新此 tree 时,CSM Performance Oracle 会考虑旧 tree 中的先前值。所有早于 strikesLifetime 个 oracle frames(例如 6 个 frames)的 strikes 都会被丢弃。

Strikes tree 的叶子形式为 {noID, validatorPubkey, [strikeTimestamps]}

将 strikes 分配给验证者而非 Node Operators 的主要原因是保持表现测量的一致性。目前,CSM Performance Oracle 单独考虑验证者的表现。因此,strikes 也应该是验证者的属性,以确保精确驱逐表现不佳的验证者。

必须注意,strikes 不是罚金,而是不良表现的指标,Node Operators 应将其视为改进其表现的信号。

固定不良表现罚金

在最初的 CSM 提案中,使用了“performance tax”这一术语。然而,该值可能难以准确计算。将最初的术语更名为“bad performance penalty”并使其成为一个固定值似乎是合理的,如果验证者因足够数量的 strikes 而被驱逐,将从 Node Operators 的 bond 中没收该值。

建议为“bad performance penalty”设置一个固定的可配置值(可选地按 Node Operator 类型单独设置),由 Lido DAO 在网络条件变化时设置/更新实际值。这种方法使 Lido 协议能够保持“bad performance penalty”的最新状态。

因 strikes 而驱逐

一旦 strikes 数量达到 strikesThreshold(例如 6 个月内 3 次 strikes),permissionless 方法可以触发验证者退出,并从 Node Operator 的 bond 中没收“bad performance penalty”。

驱逐参数由 Lido DAO 决定

由于在乐观审核方法中,CSM keys 存储中的 Node Operator key 索引可能会发生变化(被删除的 key 与 keys 存储中的最后一个 key 交换),因此需要向 permissionless 方法提供 Node Operator 存储中的当前 key 索引,并检查 leaf 中的 key 与存储中的 key 是否相同。

function ejectBadPerformingValidator(uint64 noId, bytes32 proof, uint256 keyIndex, bytes32 strikesData) {
	validatorKey = getNoKey(noId, keyIndex);
	checkKey(proof, validatorKey);
	assertStrikesCount(strikesData);
	checkProof(proof, strikesData);
	requestEjection(validatorKey);
	confiscateEjectionFee(noId, strikesData);
	confiscateBadPerfPenalty(noId, strikesData);
}

由于使用 EIP-7002 驱逐验证者需要费用,该费用应从 Node Operator 的 bond 中没收,并转移给方法调用者以覆盖相应的运营支出。

在获得最后一个 strike 之前退出

Node Operators 可以决定在达到阈值前的最后一个 strike 之前退出其验证者,这将使他们避免被没收“bad performance penalty”。

必须注意,所有直接损失都将被没收,而且在表现不佳的 frames 期间,无论如何都不会分配质押奖励。

鉴于 strike 系统的目标是“在保持表现余量的同时保护协议免受系统性不良表现者的影响”,允许表现不佳的验证者自愿离开协议是合理的,这实际上减少了协议中表现不佳的验证者数量。

此外,在验证者退出时核算已分配的 strikes 将需要在提款报告流程和 CSM Performance Oracle 之间建立直接连接,这实际上使退出变为 permissioned,并严重依赖 CSM Performance Oracle 的运作。

更新的 CSM Performance Oracle 指标

目前,CSM Performance Oracle 使用已包含的 attestations(不考虑包含延迟和正确性)比率作为 CSM 验证者的表现代理:

$$ P_{validator} = \frac{A_{included}}{A_{assigned}} $$

该指标是验证者活跃度的极佳代理,因为每个以太坊验证者应该在每个 epoch 提交一次 attestation。然而,当前方法中未计入另外两项验证者职责:区块提议(block proposals)和同步委员会(sync committee)参与。这些职责比 attestations 的频率低得多,但对网络至关重要。随着 CSM 的发展并在 Lido on Ethereum 协议中占据质押份额,需要更健壮和准确的表现代理。

统一表现评级

以已完成职责与分配职责之比的形式表示表现率已被证明是健壮且易于理解的。建议保留这种方法,同时用区块提议和同步委员会职责来丰富它。所得公式将采用以下形式:

$$ P_{validator} = C_a \times A_{eff} + C_b \times B_{eff} + C_s \times S_{eff} $$

其中

$C_a, C_b, C_s$ 是权重系数,且

$A_{eff}, B_{eff}, S_{eff}$ 分别是 attestations、block proposals 和 sync committee 的有效性评级。

beaconcha.in 使用类似的方法来计算验证者有效性

默认权重系数

默认权重系数定义如下(根据 eth2book):

$$ C_a = 5464, \quad C_b = 864, \quad C_s = 264 $$

然而,这些系数假设在分配了全部 3 项职责的长期时间内验证者可以获得奖励。由于 CSM Performance Oracle 的 frame 相对较短,因此需要考虑在 frame 内并非所有职责都被分配的场景。

全部三项职责

$C_a = 5464, \quad C_b = 864, \quad C_s = 264$

Attestations 和区块提议

$C_a = 5462, \quad C_b = 862, \quad C_s = 0$

Attestations 和 sync committee

$C_a = 5456, \quad C_b = 0, \quad C_s = 256$

仅 Attestations

$C_a = 1, \quad C_b = 0, \quad C_s = 0$

自定义权重系数

$C_a, C_b, C_s$ 可以针对给定的 Node Operator 类型具有自定义值。这允许进行特殊的表现评级计算。例如,Identified Community Stakers 可能在 block proposals 或 sync committee 方面具有较低的系数。

有效性评级

Attestations

计算 attestation 有效性最通用的方法是:

$$ A_{eff} = \frac{A_{actualReward}}{A_{maximalReward}} $$

实际的 attestation 奖励使用此处描述的复杂算法计算。可以看到,实际奖励取决于多个因素,如投票正确性和包含延迟。考虑到 Node Operators 在家中而不是在顶级数据中心验证,这些因素并不完全受 Node Operators 控制。以太坊客户端的选择也可能影响 attestation 奖励表现。

由于 CSM 主要专注于在家中验证的 operators,因此值得将 attestation 提交率计算为 attestation 有效性。

$$ A_{eff} = \frac{A_{included}}{A_{assigned}} $$

这种方法容易受到边缘情况的影响,即在提交率高的同时出现很大比例的错误 attestation 投票。然而,这种情况只有在明确的恶意意图下才能实现,因为当前所有以太坊客户端都是为了最大性能而设计的,除非有特定的代码或配置修改,否则不会表现出这种行为。任何恶意情况仍然可以通过 beaconcha.inrated.network 等工具检测出来,并通过 Node Operator 惩罚和从验证者集合中驱逐来应对。

区块提议

对于区块提议,情况简单明了。Node Operators 应维护其设置,使其即使从遥远的地点运营也能及时提交有效的区块。这一要求源于区块生产是任何验证者的关键职责这一事实。与 attestations 中单个无效或延迟投票不会干扰网络不同,区块提议是一项二元职责。区块要么被提议,要么没有被提议。因此,区块提议有效性可以计算如下:

$$ B_{eff} = \frac{B_{proposed}}{B_{assigned}} $$

Sync committee

Sync committee 是验证者非常罕见的职责。该职责的有效性可以计算为:

$$ S_{eff} = \frac{S_{included}}{S_{assigned} - B_{missed}} $$

其中

$S_{assigned}$ 表示分配给特定验证者的 sync committee 投票,且

$B_{missed}$ 是在分配给特定验证者的 sync committee 职责期间,网络错过的区块/槽位数量。

从分母中减去错过的区块至关重要,因为 sync committee 投票只能包含在已提议的区块中,而区块提议者与 sync committee 参与者不相关。

平均网络表现

使用单个验证者表现的公式,平均网络表现可以计算为所有单个验证者表现的平均值。

$$ P_{network} = \frac{\sum_{i=0}^{N_{validators}} P_{validator_i}}{N_{validators}} $$

然而,为每个验证者计算单独表现可能消耗大量资源。此外,我们需要考虑活跃验证者数量的变化。为了简化计算,可以使用替代的近似公式:

$$ P_{network} = C_a \times A_{eff}^{network} + C_b \times B_{eff}^{network} + C_s \times S_{eff}^{network} $$

其中:

$$ A_{eff}^{network} = \frac{A_{included}^{network}}{A_{assigned}^{network}} $$

$$ B_{eff}^{network} = \frac{B_{proposed}^{network}}{Slots_{frame}} $$

$$ S_{eff}^{network} = \frac{S_{included}^{network}}{(Slots_{frame} - Slots_{frame}^{missed}) \times 512} $$

总结

鉴于以上所有计算,最终算法如下:

  • CSM Performance Oracle 收集网络中所有验证者的 attestations、区块提议和 sync committee 参与数据;

  • CSM Performance Oracle 使用默认权重系数和以下公式计算网络平均“统一表现评级”:

    $$ P_{network} = C_a \times \frac{A_{included}^{network}}{A_{assigned}^{network}} + C_b \times \frac{B_{proposed}^{network}}{Slots_{frame}} + C_s \times \frac{S_{included}^{network}}{(Slots_{frame} - B_{frame}^{missed}) \times 512} $$

  • CSM Performance Oracle 使用自定义权重系数(由 Node Operator 类型决定)和以下公式计算每个 CSM 验证者的“统一表现评级”:

    $$ P_{validator}^{CSM} = C_a \times \frac{A_{included}}{A_{assigned}} + C_b \times \frac{B_{proposed}}{B_{assigned}} + C_s \times \frac{S_{included}}{S_{assigned} - B_{missed}} $$

  • CSM Performance Oracle 将 $P_{validator}^{CSM}$ 与 $P_{network} - P_{threshold}$ 进行比较,以做出关于奖励分配的决策;

未来演进

CSM v2 将为 Node Operator 类型引入自定义表现阈值。此功能将使表现评级计算更加精确(包括投票准确性和包含延迟),同时仅针对社区质押者将表现阈值保持在足够低的水平。如果没有单独的阈值,低阈值的精确表现指标将允许在数据中心(DCs)运行的验证者出现显著停机,这对协议和整个网络而言是净负面影响。另一方面,高表现阈值的精确指标将不允许具有上述限制的社区质押者获得奖励。

尽管 CSM Performance Oracle 的更新将在 CSM v2 中交付,但建议采用“分阶段”的方法,首先实现简化的 attestation 有效性核算。这将缩短开发和发布周期。同时,它也有助于收集更多关于 CSM 验证者表现的真实数据,并就为社区质押者降低表现阈值的数值做出明智的决策。

EIP-7002 支持

执行层可触发退出(Execution Layer Triggerable Exits, EIP-7002)将允许质押协议为提款凭证指向其合约的验证者请求退出。然而,这种退出请求方式需要费用。因此,现有的使用验证者私钥签署相应消息来请求退出的方法仍然更可取。

有几种情况 CSM 可能需要使用 EL 可触发退出:

  • 为被 VEBO 请求退出但未及时退出的验证者触发退出;
  • 为长时间表现出不可接受表现的验证者触发退出;
  • 允许 CSM Node Operators 在验证者私钥丢失的情况下为其自己的验证者触发退出;

第一种情况应在 VEBO 内部解决,因为在因提款覆盖或 targetLimit 导致的退出请求情况下,CSM 没有关于 VEBO 请求的验证者 keys 的信息。然而,VEBO 应通过“hook”方法告知 CSM 正在为延迟验证者触发的退出,以便 CSM 可以惩罚 Node Operator 的 bond。该罚金不应被销毁,而应转移到 Lido DAO 金库,以覆盖 VEBO 为触发退出请求而支付的费用。

CSM 必须直接发起第二种和第三种情况,因为退出应针对特定 key 触发。这意味着 Lido on Ethereum 对 EIP-7002 的实现应能够直接请求针对特定验证者 key 触发退出。

在因 strikes 而驱逐的情况下,所有相应的罚金应在驱逐时执行。值得考虑向方法调用者提供小费以补偿 Gas 成本 + 溢价。该小费应从 Node Operator 的 bond 中没收。

对于自愿退出,模块不应施加任何额外罚金,假定 Node Operators 将自行支付退出的全部费用。

还建议允许 DAO(通过投票或 EasyTrack)明确请求驱逐 CSM 验证者。

Bring your friend to CSM 计划

为了吸引更多 Identified Community Stakers(ICS)加入 CSM,建议引入一个可选的推荐计划,名为“Bring your friend to CSM”。

如果一个 ICS(推荐人,referrer)邀请另一个 ICS(被推荐人,referral),推荐人可以从邀请中获得一些好处。可能的好处结构如下:

  • 邀请一个,获得独家周边的促销代码(在 CSM UI 上);
  • 邀请两个,获得优惠的 NO 类型,不是 16 个验证者,而是 20 个验证者,并增加奖励(在链上申领)。

邀请意味着在创建 NO 时,被推荐人在交易中指定推荐人的地址。该信息记录在链上。具有已记录邀请的 Node Operators 可以获得所述的好处。

被推荐人和推荐人都应通过识别流程并获得 ICS 通行证。

对于现有的 Node Operators,不能指定推荐人,只能为在创建时符合 ICS Node Operator 类型资格的新 Node Operators 指定。

推荐计划由多个季节(seasons)组成。在每个季节开始时,设置一个可作为奖励获得的 Node Operator 类型和一个推荐阈值。这些参数在一个季节内不能更改。邀请推荐人获得的积分仅在季节内计算和有效。优惠的 Node Operator 类型只能在季节持续期间申领。当新季节开始时,所有先前收集的推荐积分将被清零。

移除单独的 slashing 报告

对于 32 ETH 验证者,初始 slashing 罚金将随 EIP-7251 从 1 ETH 降低到 $\frac{1}{128}$ ETH。考虑到相关的 Gas 成本,在 Pectra 之后报告初始 slashing 将变得无意义。

有关解决方案空间的更多详细信息,请参见此处

  • 原文链接: hackmd.io/@lido/csm-v2-t...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论