Diem 链上账户恢复机制深度解析:RecoveryAddress 模块与 KeyRotationCapability 委托模型
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。
该资源的设计带来三个关键性质:
- 防恢复环(recovery cycle):
publish时强制要求资源持有者自己的KeyRotationCapability被放入rotation_caps的第一个位置。这保证了不会出现"A 是 B 的恢复账户、B 又是 A 的恢复账户"的互相依赖死锁。 - 永不空表:由于第一个元素永远是资源持有者自己,
rotation_caps恒非空,简化了后续的查找逻辑与形式化证明。 - 单例性:
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)
执行流程与中止条件:
- VASP 校验:
VASP::is_vasp(addr)检查调用者是否为 Parent 或 Child VASP(其实现为is_parent(addr) || is_child(addr),见 VASP.move)。非 VASP 账户调用将返回ENOT_A_VASP。 - 防环校验:
DiemAccount::key_rotation_capability_address(&rotation_cap)必须等于addr,即传入的轮换能力必须属于恢复账户自身。若传入的是其他账户的能力,说明恢复账户的密钥轮换权已被委托给了别人(或调用者传错了参数),会构成潜在依赖环,返回EKEY_ROTATION_DEPENDENCY_CYCLE。 - 幂等性校验:
exists<RecoveryAddress>(addr)必须为假,否则返回ERECOVERY_ADDRESS(错误类别为ALREADY_PUBLISHED)。 - 发布资源:通过
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)
执行要点:
- 恢复地址存在性校验:同上,返回
ERECOVERY_ADDRESS。 - 调用者权限校验:
sender必须是to_recover(被恢复账户自己,语义上"原主有权轮换自己的密钥")或recovery_address(恢复资源持有者,语义上"管家有权轮换任何已委托账户的密钥"),二者满足其一即可,否则返回ECANNOT_ROTATE_KEY。注意:这里的account签名者只是作为权限凭证,实际的轮换操作使用存储在恢复资源中的KeyRotationCapability完成——这正是委托模型的核心。 - 线性查找:遍历
rotation_caps,通过DiemAccount::key_rotation_capability_address(cap) == &to_recover找到目标账户对应的能力凭证,然后调用DiemAccount::rotate_authentication_key(cap, new_key)。 - 查找失败中止:遍历完仍找不到目标,返回
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 账户恢复场景包含以下步骤:
- 初始化:VASP 选定一个"冷备份"账户(例如仅离线保管私钥的 Parent 或专用账户),调用
create_recovery_address将其设为恢复地址。此时rotation_caps中只有它自己的能力。 - 委托注册:该 VASP 下的其他 Child 账户逐个调用
add_recovery_rotation_capability,把自己的KeyRotationCapability委托给恢复地址。委托后这些账户自身不再持有轮换能力。 - 冷存储:恢复地址的认证密钥对应的私钥被离线封存("buried in the mountain"),日常业务不受影响。
- 应急恢复:当某个 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 密钥治理体系的关键一环,其设计有三点值得借鉴:
- 能力凭证作为一等公民:密钥轮换权被建模为可转移的
KeyRotationCapability值,天然支持"所有权与使用权分离"的委托模型; - 业务约束写入运行时断言与形式化规范双层防线:同 VASP 限制、防环设计、容量上限、持久性不变式,既在代码中强制,又用 Move Prover 证明,双重保障资金安全;
- 面向真实风控场景:256 个注册上限、永久委托、冷备份恢复地址等设计,均对应金融机构对密钥分级管理、灾难恢复的实际诉求。
对于希望深入研究的读者,建议按以下路径继续阅读仓库源码:先从 RecoveryAddress.move 理解模块本体,再对照 DiemAccount.move 中的 KeyRotationCapability 定义与轮换实现、VASP.move 中的 VASP 判定逻辑,最后通过 AccountAdministrationScripts.move 掌握三条交易脚本的完整调用链,即可形成对该恢复机制的端到端理解。