CSM v2 特性

lido__ 发布于 2025-08-15 阅读 17

本文是 Lido 贡献者提出的社区质押模块(CSM)v2 升级提案草案。CSM v1 已上线主网,v2 旨在为不同节点运营商类型提供定制化条件,并支持 Pectra 硬分叉相关改进。提案介绍了 Entry Gates 和 Extensions 以实现模块化入口,参数注册表以定制各类参数,优先队列以支持独立社区质押者,以及基于 EIP-7002 的性能差评惩罚机制。文章还提出了更新的 CSM 性能预言机指标,综合考虑见证、区块提议和同步委员会职责,并探讨了 EIP-7251 对最大有效余额的影响、移除单独罚没汇报等。

这是由一些 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)验证者之间进行选择。就整体协议资本效率而言,只有合并后的大型验证者余额 $\geq 1700$ ETH 时,将小型验证者合并为大型验证者或创建大型验证者才有意义(参见 Lido 贡献者的研究结果)。CSM 名称中的“Community”(社区)意味着该模块的目标受众是平均运行约 10-20 个验证者的社区质押者。这意味着对他们中的大多数人来说,合并对协议而言可能资本效率较低,尽管仍需要进行进一步分析,以了解可能的合并对 CSM 验证者格局的整体影响(例如,0x02 验证者对运行 $X$ 个验证者的相关运营 Gas 成本、在需要时执行部分提款等的影响)。与此同时,考虑到社区质押者每个验证者所需的 bond 很低(目前为 1.3 ETH,在 CSM v2 中约为 0.7 到 1 ETH),他们也不会从合并中获益太多。有鉴于此,并且由于 CSM 在底层 Lido on Ethereum 协议没有相应变更的情况下无法支持大型验证者,因此建议在 CSM v2 中不添加对大型验证者的支持。

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

关于 Preconfirmations 支持的说明

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

产品逻辑

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

Entry Gates 和 Extensions

这一概念使 CSM 的准入变得模块化和可扩展。Entry Gates 允许为 CSM 提供无限的策展/自动化/自定义准入路径。Extensions 允许第三方在最小的安全和信任假设下基于 CSM 构建产品,因为核心 CSM 代码保护着核心协议。

优先队列

该功能对于提高独立社区质押者在协议中的参与度至关重要。它允许他们不必与无需许可(未知)的操作者竞争即可加入,并直接影响 Lido on Ethereum 协议的去中心化水平。

参数注册表

这是一个简单的使能工具,用于支持不同类型的节点操作者。它允许 CSM 适应不断变化的市场条件,并为每种类型提供单独的处理。

不良表现 strikes

任何质押协议都应维护参与节点操作者的表现。在无需许可的系统中,这只能通过自动化方式完成。Strikes 系统是一种自动化工具,用于防止潜在的表现问题。

节点操作者类型

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

  • 无需许可(未知)的参与者;
  • 已识别的独立社区质押者;
  • 专业节点操作者(例如,使用自有资本、客户资本运行,或在此基础上提供 Validators as a Service 产品);
  • 等等。

定义这些类型以及识别特定节点操作者属于哪种类型的机制意味着,诸如节点操作者奖励份额、表现阈值、存款队列优先级、不良表现驱逐参数、不与以太坊网络相关的惩罚(keyRemovalChargeElRewardStealingAdditionalFine)等参数,都可以在一个模块中定制,而不必为不同的节点操作者细分群体设置多个不同的模块。

CSM v2 的主要愿景是继续支持社区质押者,并通过为不同类型的节点操作者提供个性化条件,尤其是为社区质押者提供个性化条件来增加他们的数量。

为了实现这种特殊待遇,应引入下面列出的几个功能。

Entry Gates 和 Extensions

CSModule.sol 目前有几个创建节点操作者的方法。在 Early Adoption(EA)期间,这些方法只允许 EA 列表中的成员加入。一旦 EA 期结束,这些方法就变得无需许可。这种方法在定制化方面极为受限。因此,提出了 Entry Gates 和 Extensions 的概念。

为了实现这一概念,CSModule.sol 中创建节点操作者的方法应该是需要许可的。

**注意:**在 CSModule.sol 层面,创建节点操作者的方法需要许可并不意味着不允许无需许可的准入,只是将其移动到技术栈中更深一层。

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

Entry Gates

image

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

  • 无需许可
  • 需要许可

Entry Gate 可以为使用它创建的节点操作者分配自定义的节点操作者类型(由 bondCurveId 定义)。

一个需要许可的 entry gate 可以允许现有节点操作者证明他们有资格获得相应的节点操作者类型,并将其现有的 CSM 节点操作者升级为该类型(对应的 bondCurveId)。

Entry Gates 示例和代码片段

无需许可的 entry gate

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

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。此外,它为创建的节点操作者设置了一个特殊的节点操作者类型(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 要求上传密钥
    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 为符合条件的节点操作者认领 bond curve
    ///      检查 msg.sender 是节点操作者的安全地址
    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 是允许节点操作者加入 CSM 的智能合约。Extensions 也以某种形式抽象了节点操作者的管理。一个 extension 可以将自己设置为 CSM 节点操作者的 manager 和/或 reward address 来实现这一点。这允许 extension 实现自定义的节点操作者管理原则,例如从节点操作者奖励中抽取奖励份额,或只允许上传经过验证的验证者密钥。

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

Extensions 示例和代码片段

ExtensionExample.sol

contract ExtensionExample {
    /// @dev extension 需要保存真正的节点操作者地址以便与之交互
    /// 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 主要区别在于合约本身充当节点操作者
    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)
        );

        // 对 distributed shares 做 extension 想做的任何事情,例如将一部分保存在合约余额上
        // 然后将剩余的转账到节点操作者的 reward address
    }

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

对 CSM 交互的影响

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

  • 无需许可的 entry gate;
  • 独立社区质押者 entry gate;

无需许可的 entry gate 将具有简化的节点操作者创建接口(没有 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 节点操作者创建接口:

接口

    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);

除了创建节点操作者之外,已识别社区质押者 entry gate 还将为现有 CSM 节点操作者提供一种方法,允许他们在 CSM v2 之前创建了 CSM 节点操作者,并在 CSM v2 启动时或之后被列入更新后的已识别社区质押者名单时,认领自定义的节点操作者类型(有利的 bond curve):

接口

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

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

接口

    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 中支持不同类型的节点操作者,需要一个由两部分组成的特殊技术解决方案:

  • 类型成员的存储和管理;
  • 能够为 CSM 中与节点操作者相关的参数设置自定义值

后者可以通过将现有参数迁移并添加新参数到一个独立的注册表合约来解决,该合约将为每种节点操作者类型存储每个参数的单独值。第一部分由上述 Entry Gates 和 Extensions 解决。

由于节点操作者类型众多,且并非所有类型都需要一套完整的自定义参数,注册表中应为每个参数存储强制默认值。重要的是,这些默认值的设定应考虑到默认节点操作者类型为“无需许可且未知”,并采取最“保守”的方式。如果特定参数和节点操作者类型组合已设置了自定义值,则应使用该自定义值;否则使用默认值。

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

image

应包括哪些参数?

目前,CSM 有几个参数可以迁移到 CSParametersRegistry

  • keyRemovalCharge - 从 CSM 存款队列中删除验证者密钥所收取的 stETH 数量;
  • elRewardsStealingAdditionalFine - 当 CSM 验证者在提议区块时窃取或错误转移 EL 奖励时,添加到被窃取/错误转移的 ETH 金额中的 ETH 数量;
  • performanceLeeway - 用于 CSM Performance Oracle 计算节点操作者奖励分配的 BP 值或按密钥区间划分的 BP 值集合(参见“依赖验证者密钥数量的参数”);

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

  • priorityQueueParams - 优先队列 ID 以及节点操作者有资格占据的优先存款队列席位数量;
  • rewardShare - BP 值或按密钥区间划分的 BP 值集合,决定节点操作者有资格获得的、由 Staking Router 分配给模块的奖励份额(参见“依赖验证者密钥数量的参数”)。所有未分配的奖励将返还给 Lido DAO 国库;
  • strikesParams - 新的 strikes 系统的参数(生命周期、阈值);
  • badPerformancePenalty - 如果验证者因系统性不良表现而从协议中被驱逐,将被没收的惩罚;
  • performanceDutyCoefficients - 用于 CSM Performance Oracle 计算表现评级的系数集合(参见“更新的 CSM Performance Oracle 指标”部分)。

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

依赖验证者密钥数量的参数

一些参数(即 performanceLeewayrewardShare)应当取决于节点操作者控制的验证者密钥数量,以便实现有限的优惠取值。performanceLeewayrewardShare 都被设置为节点操作者控制的总验证者密钥数量的函数。

例如,给定节点操作者的前 10 个验证者密钥的 rewardShare 为 Staking Router 分配给模块的奖励的 100%;接下来 10 个密钥为 90%,其他密钥(>20)为 80%

image

优先队列

对于 CSM v2,需要找到一种方法,让不同类型的节点操作者能够在无需许可(未知)的操作者之前获得存款。

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

image

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

struct QueueConfig {
    uint32 priority;
    uint32 maxDeposits;
}

在此结构中,priority 指示使用哪个队列,maxDeposits 表示节点操作者可以通过优先队列获得存款的最大验证者密钥数量。

节点操作者可以被分配给由其 curveID 标识的组,因此每个 curveID 都有自己的优先队列配置。该配置不限于使用特定队列,但保留队列除外。我们可以想象以下设置:

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

向队列添加密钥

节点操作者添加新密钥的机制如下:

  1. 获取与节点操作者 curveID 关联的 QueueConfig
  2. 检查是否存在未存款且未入队的密钥,数量低于 QueueConfig.maxDeposits 限制,这些密钥可以被放入优先级为 QueueConfig.priority 的队列中。
  3. 尽可能多地将密钥添加到优先级为 QueueConfig.priority 的优先队列中,其余的添加到默认队列,即优先级为 LOWEST_PRIORITY 的队列。

image

请注意,节点操作者可以使用其优先队列获得最多 maxDeposits 个存款。换句话说,以下场景是实际存在的:

  1. NO 上传 10(maxDeposits)个密钥并将其放入优先队列。
  2. NO 改变主意并移除所有密钥。
  3. NO 在优先队列中的批次被跳过。
  4. NO 再次上传 10 个密钥并将其放入优先队列,因为他们还没有从该队列获得任何存款。

从队列获取存款数据

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

image

队列清理

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

从旧方案迁移

为了将当前队列迁移到新机制,为队列专门分配了一个独立的优先级,任何 curveID 都不能使用该优先级。LEGACY_QUEUE_PRIORITY 设置为 LOWEST_PRIORITY - 1。因此,我们有以下空闲优先级范围:$[0; LOWEST_PRIORITY - 2]$。最终,旧队列将被完全存入,保留的优先级将在 CSM 的下一个版本(v3)中释放使用。

image

对于有资格获得优先队列席位的节点操作者,将提供一个特殊方法,将符合条件的密钥数量从旧队列迁移到新的优先队列。

如果节点操作者有资格获得优先队列中的 $N$ 个席位,但已经有 $N$ 个或更多密钥被存入,上述迁移方法将不起作用。有关详细信息,请参阅“向队列添加密钥”部分。

不良表现 strikes

当前版 CSM 中未解决的问题之一是驱逐表现不佳的验证者。尽管这些验证者不会获得节点操作者的奖励,但 bond 的重定基仍会继续,这些验证者将对 Lido on Ethereum 协议的整体 APR 产生负面影响。因此,与其他无需许可的协议相比,对 Lido on Ethereum 协议进行理论攻击的成本降低了。

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

Strikes 分配

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

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

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

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

需要强调的是,strikes 不是惩罚,而是不良表现的指标,节点操作者应将其视为改善表现的信号。

固定不良表现惩罚

在最初的 CSM 提案中,使用了术语 “performance tax”。然而,这个值可能难以准确计算。将最初的术语重命名为“不良表现惩罚”并使其成为一个固定值似乎是合理的,如果验证者因足够数量的 strikes 而被驱逐,该值将从节点操作者的 bond 中没收。

建议针对“不良表现惩罚”设置一个固定的可配置值(可选地,每种节点操作者类型单独设置),由 Lido DAO 在网络条件变化时负责设置/更新实际值。这样能让 Lido 协议始终维持“不良表现惩罚”的时效性。

因 strikes 而驱逐

一旦 strikes 数量达到 strikesThreshold(例如 6 个月内 3 次 strikes),无需许可的方法可以触发验证者退出,并从节点操作者的 bond 中没收“不良表现惩罚”。

驱逐参数由 Lido DAO 决定

由于 CSM 密钥存储中的节点操作者密钥索引可能会在乐观审查方法中发生变化(删除的密钥与密钥存储中的最后一个密钥交换),因此需要向无需许可的方法提供节点操作者存储中的当前密钥索引,并检查叶子中的密钥与存储中的密钥是否相同。

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 驱逐验证者需要支付费用,该费用应从节点操作者的 bond 中没收,并转移给方法调用者以覆盖相应的运营费用。

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

节点操作者可能会决定在获得达到阈值的最后一个 strike 之前退出其验证者,从而避免被没收“不良表现惩罚”。

需要强调的是,所有直接损失都将被没收,并且无论如何,在表现不佳的帧期间不会分配质押奖励。

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

此外,若要在验证者退出时计入已分配的 strikes,就需要在提款报告流程和 CSM Performance Oracle 之间建立直接连接,这实际上会让退出变成需要许可的流程,并且严重依赖 CSM Performance Oracle 的运行。

更新的 CSM Performance Oracle 指标

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

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

这个指标是验证者活跃度的极好代理指标,因为每个以太坊验证者每个 epoch 都需要提交一次证明。然而,当前方法没有考虑另外两个验证者职责:区块提议和同步委员会参与。这些职责远不如证明频繁,但对网络至关重要。随着 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}$ 分别是证明、区块提议和同步委员会的有效性评级。

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

默认权重系数

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

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

然而,这些系数假设在很长一段时间内所有 3 项职责都被分配。由于 CSM Performance Oracle 的帧相对较短,需要考虑帧内并非所有职责都被分配的情况。

所有三项职责

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

证明和区块提议

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

证明和同步委员会

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

仅证明

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

自定义权重系数

对于给定的节点操作者类型,$C_a, C_b, C_s$ 可以具有自定义值。这允许进行特殊的表现评级计算。例如,已识别社区质押者可能对区块提议或同步委员会具有更低的系数。

有效性评级

证明

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

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

实际证明奖励使用此处描述的复杂算法计算。可以看出,实际奖励取决于多个因素,如投票正确性和包含延迟。鉴于节点操作者在家里而非顶级数据中心进行验证,这些因素并非完全受节点操作者控制。以太坊客户端的选择也可能影响证明奖励表现。

由于 CSM 主要面向在家中运行的验证者,因此值得将证明提交率作为证明有效性来计算。

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

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

区块提议

区块提议的情况简单明了。节点操作者应维护他们的设置,以便即使他们在遥远的位置操作,也能及时提交有效区块。这一要求源于区块生产是任何验证者的重要职责。与单独的无效或延迟投票不会干扰网络的证明不同,区块提议是二元的职责。区块要么被提议,要么没有被提议。因此,区块提议有效性可以计算如下:

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

同步委员会

同步委员会是验证者非常罕见的职责。该职责的有效性可以计算为:

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

从分母中减去错过的区块至关重要,因为同步委员会投票只能包含在提议的区块中,并且区块提议者与同步委员会参与者不相关。

网络平均表现

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

$$ 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 收集网络中所有验证者的证明、区块提议和同步委员会参与数据;

  • 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 使用自定义权重系数(由节点操作者类型决定)和以下公式计算每个 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 将为节点操作者类型引入自定义表现阈值。此功能将使表现评级计算更加精确(包括投票准确性和包含延迟),同时仅针对社区质押者保持足够低的表现阈值。如果没有单独的阈值,低阈值配合精确表现指标会允许在数据中心运行的验证者出现大量停机时间,这对协议和整个网络都是净负面影响。另一方面,精确指标配合高阈值又会让存在上述局限的社区质押者无法获得奖励。

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

EIP-7002 支持

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

在某些情况下,CSM 可能需要使用 EL 可触发退出:

  • 为那些已被 VEBO 请求退出但未及时退出的验证者触发退出;
  • 为那些长期表现出不可接受表现的验证者触发退出;
  • 在验证者私钥丢失的情况下,允许 CSM 节点操作者为其自己的验证者触发退出;

第一种情况应在 VEBO 内解决,因为 CSM 在因提款覆盖或由于 targetLimit 而请求退出时,没有关于 VEBO 请求的验证者密钥的信息。但是,VEBO 应(使用“hook”方法)通知 CSM 正在为延迟验证者触发退出,以便 CSM 可以惩罚节点操作者的 bond。该惩罚不应被销毁,而应转移给 Lido DAO 国库,以覆盖 VEBO 触发退出请求所支付的费用。

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

在因 strikes 被驱逐的情况下,应在驱逐时应用所有相应的惩罚。还值得考虑向方法调用者支付一笔小费,以补偿 Gas 成本和溢价。该小费应从节点操作者的 bond 中没收。

对于自愿退出,模块不应施加任何额外惩罚,因为节点操作者将自行承担退出的全部费用。

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

移除单独的 slashing 报告

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

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

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

相关文章

0 条评论