CSM 乐观审核队列

lido__ 发布于 2024-04-05 阅读 10

本文介绍了CSM(Community Staking Module)中存款队列的实现细节。该队列采用连续数组与头指针结构,节点运营商将密钥批次加入队列,并允许在队列中优化或移除密钥。文章详细定义了运营商的数据结构(包括已添加和已审核密钥的指针)、队列的批次结构(包含节点ID、密钥数量和下一项索引),并解释了乐观审核机制:无需显式审核,通过预言机报告无效密钥并调整指针。在获取密钥时,模块遍历队列,根据可存入验证者计数(depositableValidatorsCount)和入队计数(enqueuedCount)来控制批次消费、跳过空项及清理队列。最后介绍了节点运营商的队列规范化、添加和移除密钥的具体操作,确保队列与状态一致。

image

让我们把 CSM 质押队列想象成 QC 处的密钥盒传送带。检查员在其轮班期间尽可能多地检查盒子。节点操作员上传密钥后,就排到队尾,将其盒子放入队列中,盒子里装着他已上传的尽可能多的密钥。在轮到该盒子之前,节点操作员可以自由更改或移除其中损坏的密钥。但如果检查员检查盒子时发现了损坏的密钥,它会取出完好的密钥,并将其余的退还给节点操作员。操作员随后将下一批密钥放到传送带的末端。

Operator

每个节点操作员都有自己的已上传密钥序列。CSM 存储指向这些密钥的指针:

struct Operator {
  	/* 游标 */
    uint256 totalAddedKeys;
    uint256 totalVettedKeys;

    /* ... 省略无关字段 ... */

    /* 计数器 */
    uint256 enqueuedCount;
    uint256 depositableValidatorsCount;
}

Queue definition

队列是一个连续的项目数组,带有一个指向其头部的指针。新项目被添加到数组的末尾。

struct Queue {
    uint128 head;
    uint128 length;
    mapping(uint128 => Batch) queue; // 本质上是一个数组
}

每个项目都具有以下结构:

struct Batch {
    uint64    noId;      // 节点操作员的 ID
    uint64    keysCount; // 操作员在队列中占用的位置数量(参见 `enqueuedCount`)
    uint128   next;      // 队列中下一个项目的索引
}

noId 外,任何项目都可能按模块需要被修改。

通过递增 head 值,可以从 headlength-1 遍历队列。遍历时会参考批次(batch)的 next 字段,将 head 设置为该字段的值。

队列容量无法预先获知,可以通过遍历队列来确定。

Optimistic vetting

这里假设不需要显式的密钥审查(vetting),而是由密钥验证预言机(oracle)报告无效密钥。采用这种方法时,totalVettedKeystotalAddedKeys 指针在节点操作员被取消审查(unvetting)之前是相等的。此时,totalVettedKeys 指针会被设置为节点操作员密钥序列中最后一个有效密钥的索引。

要恢复 totalVettedKeystotalAddedKeys 指针的相等性,节点操作员需要移除一个密钥。可以预期,如果问题未解决,密钥验证预言机会再次报告,totalVettedKeys 指针也会被再次下调。

Keys obtaining

模块遍历队列以获取项目。每个项目都会被处理,并检查可以从该项目对应的节点操作员处请求获得多少密钥。depositableValidatorsCount 充当计数器,表示总共可以从该节点操作员处获取多少密钥。该值由模块的业务逻辑重新计算,因此不在本文讨论范围内。从节点操作员处获取密钥时,应当减少 depositableValidatorsCount 计数器。

节点操作员的 enqueuedCount 计数器显示当前队列中有多少位置分配给了该节点操作员。因此,在消耗了该节点操作员的一个批次(例如通过获取用于质押的密钥)后,该计数器应相应减少已质押或跳过的密钥数量。

如果一次质押请求了某个项目可能返回的所有密钥,则该项目应从队列中出队。

如果一次质押请求的密钥少于该项目可能返回的数量,则项目的 keysCount 字段会减少所请求的密钥数量,下一次迭代应从同一项目开始。

可能存在多个不返回密钥的项目。算法会跳过这些项目;但如果空项目过多,则应有一种无需许可的方法来清理队列。该方法将队列的 head 指针向前推进,越过空项目,并可选地利用 next 字段一次性跳过多个项目。

Node operator's actions

Queue normalization

如果有若干密钥符合入队条件,就会调用 normalizeQueue

function normalizeQueue(no) {
    if (no.enqueuedCount < no.depositableValidatorsCount) {
        enqueue(no.depositableValidatorsCount - no.enqueuedCount);
        no.enqueuedCount = no.depositableValidatorsCount;
    }
}

每当模块的业务逻辑更新某个节点操作员的 depositableValidatorsCount 时,都应当为该操作员调用 normalizeQueue,以确保它能为其符合条件的密钥收到质押。

Adding new key

当节点操作员上传新密钥时,如果队列当时没有专门分配给该操作员的未使用槽位(由操作员的 enqueuedCount 字段指示),则该密钥会被入队:

function addKeys(keys, count) {
    storeKeys(keys);
    // 否则操作员处于未审查状态,不要移动 vettedKeys
    // 指针以避免预言机的额外报告
    if (no.totalVettedKeys == no.totalAddedKeys) {
    	no.totalVettedKeys += count;
    }
    no.totalAddedKeys += count;
    normalizeQueue(no); // 在内部检查 `enqueuedCount`
    moduleNonce++;
}

Removing a key

节点操作员移除密钥时,其专用槽位会原样保留,直到队列越过这些未使用的槽位。在这种情况下,enqueuedCount 字段会按跳过的槽位数量递减。移除密钥会乐观地将 totalVettedKeys 设置为与 totalAddedKeys 相等:

function removeKey(key) {
    remove(key);
    no.totalAddedKeys--;
    no.totalVettedKeys = no.totalAddedKeys;
    normalizeQueue(no);
    moduleNonce++;
}
  • 原文链接: hackmd.io/@lido/ryw2Qo5i...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论