Diem 链上账户恢复机制深度解析:RecoveryAddress 模块与 KeyRotationCapability 委托模型

原创2026-09-21 12:52:461,736 阅读
文章标签:区块链金融科技

Diem 链上账户恢复机制深度解析:RecoveryAddress 模块与 KeyRotationCapability 委托模型

本文以 Diem 区块链框架(Diem Blockchain Framework)中的 0x1::RecoveryAddress 模块为核心,深入剖析其为 VASP(Virtual Asset Service Provider,虚拟资产服务提供商)设计的链上账户恢复机制:如何通过委托 KeyRotationCapability 让一个"冷备份"恢复账户集中管理多个 VASP 账户的认证密钥轮换。读完本文,你将掌握 RecoveryAddress 资源的完整结构、publish / add_rotation_capability / rotate_authentication_key 三个核心函数的实现细节与中止条件、它与 DiemAccount、VASP 模块的协作关系,以及对应的三个交易脚本的完整调用链。

背景:为什么 VASP 需要链上账户恢复机制

在 Diem 的账户模型里,每个账户的全局状态中保存着一个 32 字节的 authentication_key(认证密钥),它由账户地址与公钥共同决定。一旦密钥丢失或泄露,账户就面临永久失控或被盗的风险。为此,DiemAccount 模块 提供了密钥轮换能力,其核心是一个可移动的凭证:

/// The holder of KeyRotationCapability for account_address can rotate the authentication key for
/// account_address (i.e., write to account_address/DiemAccount/authentication_key).
/// There is at most one KeyRotationCapability in existence for a given address.
struct KeyRotationCapability has store {
    /// Address that KeyRotationCapability was associated with when it was created.
    /// This field does not change.
    account_address: address,
}

(定义见 DiemAccount.move)

KeyRotationCapability 绑定创建时的账户地址(account_address 字段不可变),持有人在账户存在的前提下即可调用 DiemAccount::rotate_authentication_key 写入新的认证密钥。关键设计是:每个地址同时最多只存在一个 KeyRotationCapability,它可以通过 extract_key_rotation_capability 从账户的 DiemAccount 资源中抽取出来、被保管在别的地址下,从而实现"密钥轮换权的委托"。

对于 VASP 而言,风险场景尤为现实:一个 VASP 往往拥有一个 Parent 账户和多个 Child 账户(见 VASP.move 中 ParentVASP / ChildVASP 资源),若其中某个账户的私钥丢失,整条资金链路的恢复将非常困难。RecoveryAddress 模块正是为此设计的——它允许同一 VASP 下的所有账户,把自己的密钥轮换权集中委托给一个"恢复账户",平时这个恢复账户的认证密钥可以"埋进山里"(buried in the mountain),只有在需要恢复某个账户时才启用。

RecoveryAddress 资源:恢复能力的存储载体

模块的核心是定义在 RecoveryAddress.move 中的全局资源:

/// A resource that holds the `KeyRotationCapability`s for several accounts belonging to the
/// same VASP. ...
struct RecoveryAddress has key {
    rotation_caps: vector<KeyRotationCapability>
}

rotation_caps 是一个 KeyRotationCapability 向量,存储了被委托给该恢复地址的所有账户的密钥轮换能力。对应生成文档见 RecoveryAddress.md。

该资源的设计带来三个关键性质:

  1. 防恢复环(recovery cycle):publish 时强制要求资源持有者自己的 KeyRotationCapability 被放入 rotation_caps 的第一个位置。这保证了不会出现"A 是 B 的恢复账户、B 又是 A 的恢复账户"的互相依赖死锁。
  2. 永不空表:由于第一个元素永远是资源持有者自己,rotation_caps 恒非空,简化了后续的查找逻辑与形式化证明。
  3. 单例性:RecoveryAddress 是 has key 资源,每个地址最多发布一个;重复 publish 会直接中止。

常量与错误码速查

模块共定义 7 个错误码常量和 1 个上限常量(源码见 RecoveryAddress.move),与生成文档 RecoveryAddress.md 完全一致:

常量名 值 含义 触发函数
ENOT_A_VASP 0 只有 VASP 账户才能创建恢复地址 publish
EKEY_ROTATION_DEPENDENCY_CYCLE 1 会产生密钥轮换依赖环 publish
ECANNOT_ROTATE_KEY 2 签名者无权轮换该账户密钥 rotate_authentication_key
EINVALID_KEY_ROTATION_DELEGATION 3 只有同一 VASP 内的账户才能委托轮换权 add_rotation_capability
EACCOUNT_NOT_RECOVERABLE 4 在恢复资源中找不到目标账户 rotate_authentication_key
ERECOVERY_ADDRESS 5 RecoveryAddress 资源状态异常(未发布或重复发布) 三个函数
EMAX_KEYS_REGISTERED 6 已达到单个恢复地址允许注册的密钥数量上限 add_rotation_capability
MAX_REGISTERED_KEYS 256 单个恢复地址最多可注册的密钥数(上限常量) add_rotation_capability

错误码统一通过 Errors::invalid_argument、Errors::already_published、Errors::not_published、Errors::limit_exceeded 等错误类别包装返回,便于链下客户端根据错误类别(如 INVALID_ARGUMENT、ALREADY_PUBLISHED)与错误原因码双重定位问题。

函数一:publish —— 初始化恢复地址

publish 是恢复机制的入口,它把 recovery_account 自身的 KeyRotationCapability 抽取出来并发布到 RecoveryAddress 资源中:

public fun publish(recovery_account: &signer, rotation_cap: KeyRotationCapability) {
    let addr = Signer::address_of(recovery_account);
    // Only VASPs can create a recovery address
    assert(VASP::is_vasp(addr), Errors::invalid_argument(ENOT_A_VASP));
    // put the rotation capability for the recovery account itself in `rotation_caps`. This
    // ensures two things:
    // (1) It's not possible to get into a "recovery cycle" where A is the recovery account for
    //     B and B is the recovery account for A
    // (2) rotation_caps is always nonempty
    assert(
        *DiemAccount::key_rotation_capability_address(&rotation_cap) == addr,
         Errors::invalid_argument(EKEY_ROTATION_DEPENDENCY_CYCLE)
    );
    assert(!exists<RecoveryAddress>(addr), Errors::already_published(ERECOVERY_ADDRESS));
    move_to(
        recovery_account,
        RecoveryAddress { rotation_caps: Vector::singleton(rotation_cap) }
    )
}

(实现见 RecoveryAddress.move)

执行流程与中止条件:

  1. VASP 校验:VASP::is_vasp(addr) 检查调用者是否为 Parent 或 Child VASP(其实现为 is_parent(addr) || is_child(addr),见 VASP.move)。非 VASP 账户调用将返回 ENOT_A_VASP。
  2. 防环校验:DiemAccount::key_rotation_capability_address(&rotation_cap) 必须等于 addr,即传入的轮换能力必须属于恢复账户自身。若传入的是其他账户的能力,说明恢复账户的密钥轮换权已被委托给了别人(或调用者传错了参数),会构成潜在依赖环,返回 EKEY_ROTATION_DEPENDENCY_CYCLE。
  3. 幂等性校验:exists<RecoveryAddress>(addr) 必须为假,否则返回 ERECOVERY_ADDRESS(错误类别为 ALREADY_PUBLISHED)。
  4. 发布资源:通过 move_to 将 RecoveryAddress { rotation_caps: Vector::singleton(rotation_cap) } 存入恢复账户地址,初始向量恰好包含恢复账户自己的能力。

形式上,PublishEnsures 规范保证了发布后 len(rotation_caps) == 1 且首个元素就是传入的 rotation_cap;PublishAbortsIf 规范把上述三条中止条件全部声明为 aborts_if(见 RecoveryAddress.move),供 Move Prover 形式化验证。

函数二:add_rotation_capability —— 注册被恢复账户

初始化恢复地址后,同一 VASP 下的其他账户需要把自己的轮换能力"贡献"进来:

public fun add_rotation_capability(to_recover: KeyRotationCapability, recovery_address: address)
acquires RecoveryAddress {
    // Check that `recovery_address` has a `RecoveryAddress` resource
    assert(exists<RecoveryAddress>(recovery_address), Errors::not_published(ERECOVERY_ADDRESS));
    // Only accept the rotation capability if both accounts belong to the same VASP
    let to_recover_address = *DiemAccount::key_rotation_capability_address(&to_recover);
    assert(
        VASP::is_same_vasp(recovery_address, to_recover_address),
        Errors::invalid_argument(EINVALID_KEY_ROTATION_DELEGATION)
    );

    let recovery_caps = &mut borrow_global_mut<RecoveryAddress>(recovery_address).rotation_caps;
    assert(
        Vector::length(recovery_caps) < MAX_REGISTERED_KEYS,
        Errors::limit_exceeded(EMAX_KEYS_REGISTERED)
    );

    Vector::push_back(recovery_caps, to_recover);
}

(实现见 RecoveryAddress.move)

三个关键约束:

  • 恢复地址必须已初始化:recovery_address 下必须已存在 RecoveryAddress 资源,否则返回 ERECOVERY_ADDRESS(NOT_PUBLISHED)。
  • 同 VASP 约束:VASP::is_same_vasp(recovery_address, to_recover_address) 要求两个地址都属于 VASP 且拥有相同的 Parent 地址(实现为 is_vasp(addr1) && is_vasp(addr2) && parent_address(addr1) == parent_address(addr2),见 VASP.move)。这从机制上杜绝了跨机构委托密钥轮换权的风险,返回 EINVALID_KEY_ROTATION_DELEGATION。
  • 容量上限:rotation_caps 长度必须小于 MAX_REGISTERED_KEYS = 256,超出后返回 EMAX_KEYS_REGISTERED(错误类别 LIMIT_EXCEEDED)。

注意此函数的两个入参:to_recover 是调用者(被恢复账户)通过 extract_key_rotation_capability 抽取出的能力凭证,recovery_address 是恢复账户的地址。函数通过 borrow_global_mut 获取可变借用并 Vector::push_back 追加能力。

函数三:rotate_authentication_key —— 执行恢复轮换

这是整个机制的"落地点",负责把 to_recover 账户的认证密钥真正轮换为 new_key:

public fun rotate_authentication_key(
    account: &signer,
    recovery_address: address,
    to_recover: address,
    new_key: vector<u8>
) acquires RecoveryAddress {
    // Check that `recovery_address` has a `RecoveryAddress` resource
    assert(exists<RecoveryAddress>(recovery_address), Errors::not_published(ERECOVERY_ADDRESS));
    let sender = Signer::address_of(account);
    assert(
        // The original owner of a key rotation capability can rotate its own key
        sender == to_recover ||
        // The owner of the `RecoveryAddress` resource can rotate any key
        sender == recovery_address,
        Errors::invalid_argument(ECANNOT_ROTATE_KEY)
    );

    let caps = &borrow_global<RecoveryAddress>(recovery_address).rotation_caps;
    let i = 0;
    let len = Vector::length(caps);
    while (i < len)
    {
        let cap = Vector::borrow(caps, i);
        if (DiemAccount::key_rotation_capability_address(cap) == &to_recover) {
            DiemAccount::rotate_authentication_key(cap, new_key);
            return
        };
        i = i + 1
    };
    // Couldn't find `to_recover` in the account recovery resource; abort
    abort Errors::invalid_argument(EACCOUNT_NOT_RECOVERABLE)
}

(实现见 RecoveryAddress.move)

执行要点:

  1. 恢复地址存在性校验:同上,返回 ERECOVERY_ADDRESS。
  2. 调用者权限校验:sender 必须是 to_recover(被恢复账户自己,语义上"原主有权轮换自己的密钥")或 recovery_address(恢复资源持有者,语义上"管家有权轮换任何已委托账户的密钥"),二者满足其一即可,否则返回 ECANNOT_ROTATE_KEY。注意:这里的 account 签名者只是作为权限凭证,实际的轮换操作使用存储在恢复资源中的 KeyRotationCapability 完成——这正是委托模型的核心。
  3. 线性查找:遍历 rotation_caps,通过 DiemAccount::key_rotation_capability_address(cap) == &to_recover 找到目标账户对应的能力凭证,然后调用 DiemAccount::rotate_authentication_key(cap, new_key)。
  4. 查找失败中止:遍历完仍找不到目标,返回 EACCOUNT_NOT_RECOVERABLE。

底层轮换函数 DiemAccount::rotate_authentication_key(见 DiemAccount.move)有两个额外约束:

  • 目标账户必须存在(exists_at 校验,NOT_PUBLISHED 错误);
  • new_key 长度必须恰好为 32 字节,否则返回 EMALFORMED_AUTHENTICATION_KEY。

轮换成功后直接写 sender_account_resource.authentication_key = new_authentication_key,完成认证密钥更新。形式上,RotateAuthenticationKeyEnsures 保证 DiemAccount::authentication_key(to_recover) == new_key,RotateAuthenticationKeyAbortsIf 把全部中止条件(资源未发布、账户不存在、新密钥长度非法、能力未委托、调用者无权限)声明为规范(见 RecoveryAddress.move)。

与交易脚本的集成:三条完整调用链

RecoveryAddress 是底层模块,实际使用需通过 Diem Framework 的交易脚本层。在 AccountAdministrationScripts.move 中有三个对应的 public(script) 函数,构成完整的生命周期:

1. 创建恢复地址:create_recovery_address

public(script) fun create_recovery_address(account: signer) {
    RecoveryAddress::publish(&account, DiemAccount::extract_key_rotation_capability(&account))
}

(见 AccountAdministrationScripts.move)

该脚本把"抽取能力"与"发布资源"两步合并:先通过 DiemAccount::extract_key_rotation_capability 从账户中抽取唯一的轮换能力(若此前已被抽取过会返回 EKEY_ROTATION_CAPABILITY_ALREADY_EXTRACTED),再交给 RecoveryAddress::publish。执行成功后,该账户成为恢复地址,可继续接收同 VASP 账户的委托。

2. 注册被恢复账户:add_recovery_rotation_capability

public(script) fun add_recovery_rotation_capability(to_recover_account: signer, recovery_address: address) {
    RecoveryAddress::add_rotation_capability(
        DiemAccount::extract_key_rotation_capability(&to_recover_account), recovery_address
    )
}

(见 AccountAdministrationScripts.move)

发起者是 to_recover_account(被恢复账户自己):它先抽取自己的轮换能力并永久让渡给恢复地址(此后 DiemAccount 资源中的 key_rotation_capability 字段变为 None),再注册到 recovery_address。执行成功后,恢复账户与被恢复账户双方都获得轮换被恢复账户认证密钥的权限。

3. 恢复执行:rotate_authentication_key_with_recovery_address

public(script) fun rotate_authentication_key_with_recovery_address(
        account: signer,
        recovery_address: address,
        to_recover: address,
        new_key: vector<u8>
        ) {
    RecoveryAddress::rotate_authentication_key(&account, recovery_address, to_recover, new_key)
}

(见 AccountAdministrationScripts.move)

该脚本是恢复机制面向普通用户的最终入口:任何持有恢复账户或被恢复账户签名权的实体,都可以提交一笔交易,将 to_recover 的认证密钥更新为 new_key(必须为 32 字节)。这三条脚本的完整中止条件说明(错误类别 × 错误原因 × 描述)也均记录在 AccountAdministrationScripts.move 的文档注释中。

典型恢复流程:从初始化到应急恢复

综合以上模块与脚本,一个完整的 VASP 账户恢复场景包含以下步骤:

  1. 初始化:VASP 选定一个"冷备份"账户(例如仅离线保管私钥的 Parent 或专用账户),调用 create_recovery_address 将其设为恢复地址。此时 rotation_caps 中只有它自己的能力。
  2. 委托注册:该 VASP 下的其他 Child 账户逐个调用 add_recovery_rotation_capability,把自己的 KeyRotationCapability 委托给恢复地址。委托后这些账户自身不再持有轮换能力。
  3. 冷存储:恢复地址的认证密钥对应的私钥被离线封存("buried in the mountain"),日常业务不受影响。
  4. 应急恢复:当某个 Child 账户密钥丢失时,取出恢复私钥,构造一笔 rotate_authentication_key_with_recovery_address 交易,把该 Child 账户的认证密钥轮换为新密钥,账户随即恢复控制权。

整个过程无需任何中心化机构介入,完全在链上通过资源与能力凭证的委托完成;同时由于 RecoveryAddress 资源不可被删除(见下文持久性不变式),恢复能力具有永久性。

形式化规范:Move Prover 支撑的安全性保证

模块的规范部分(见 RecoveryAddress.move)通过 Move Prover 可验证的不变式,为上述机制提供了数学层面的保证:

  • 初始化不变式(Initialization):任意恢复地址的 rotation_caps 长度大于 0,且首个元素的 account_address 等于恢复地址自身——即"恢复地址永远持有自己的能力",从根上杜绝恢复环。
  • 资源持久性(Persistence of Resource):一旦地址成为恢复地址,在任何交易后它仍然是恢复地址(old(spec_is_recovery_address(addr)) ==> spec_is_recovery_address(addr))。
  • 能力持久性(Persistence of KeyRotationCapability):若恢复地址此前持有某账户的能力,则更新后仍然持有——委托一旦建立便不可撤销,防止"幽灵能力"导致的安全漏洞。
  • 角色一致性(Consistency Between Resources and Roles):只有 VASP 地址可以持有 RecoveryAddress 资源(spec_is_recovery_address(addr) ==> VASP::is_vasp(addr)),与 publish 的运行时校验相互印证。

配套的辅助规范函数 spec_is_recovery_address、spec_get_rotation_caps、spec_holds_key_rotation_cap_for 分别对应运行时逻辑,便于证明工具在抽象层面推理。这些规范同样完整呈现在生成文档 RecoveryAddress.md 的 Module Specification 章节中。

总结与设计启示

RecoveryAddress 模块是 Diem 链上 VASP 密钥治理体系的关键一环,其设计有三点值得借鉴:

  1. 能力凭证作为一等公民:密钥轮换权被建模为可转移的 KeyRotationCapability 值,天然支持"所有权与使用权分离"的委托模型;
  2. 业务约束写入运行时断言与形式化规范双层防线:同 VASP 限制、防环设计、容量上限、持久性不变式,既在代码中强制,又用 Move Prover 证明,双重保障资金安全;
  3. 面向真实风控场景:256 个注册上限、永久委托、冷备份恢复地址等设计,均对应金融机构对密钥分级管理、灾难恢复的实际诉求。

对于希望深入研究的读者,建议按以下路径继续阅读仓库源码:先从 RecoveryAddress.move 理解模块本体,再对照 DiemAccount.move 中的 KeyRotationCapability 定义与轮换实现、VASP.move 中的 VASP 判定逻辑,最后通过 AccountAdministrationScripts.move 掌握三条交易脚本的完整调用链,即可形成对该恢复机制的端到端理解。

登录后查看全文
diem