Move对象 - Aptos-Foundation

AIP-10 提议在链上单个地址的全局访问 Move 对象,用于存储异构资源集。对象提供了一个丰富的能力模型,允许细粒度的资源控制和所有权管理。通过利用帐户模型的各个方面,对象可以直接发出事件,从而更丰富地理解链上操作。

AIP-10 - 移动对象

摘要

此 AIP 提议移动对象,以便全局访问链上单个地址存储的异构资源集合。对象提供了一种丰富的能力模型,可以实现细粒度的资源控制和所有权管理。通过利用账户模型的各个方面,对象可以直接发出事件,从而更深入地了解链上操作。

动机

对象模型允许 Move 将复杂类型表示为存储在单个地址内的一组资源。在对象模型中,NFT 或 Token 可以将常见的 Token 数据放置在 Token 资源中,将对象数据放置在 ObjectCore 资源中,然后根据需要专门化为其他资源,例如,Player 对象可以在游戏中定义一个玩家。ObjectCore 本身既存储当前所有者的 address,也存储用于创建事件流的相应数据。

对象模型通过使用由不同访问器或引用实现的、能力框架来提高类型安全性和数据访问,这些访问器或引用定义并分发各种能力,从而实现丰富的操作。这些能力包括:

  • ConstructorRef 允许创建所有其他能力,允许将资源添加到对象,并且只能在创建对象时访问。
  • Object<T> 指向包含 T 资源的某个对象。这对于存储对资源的引用以进行反向查找非常有用。
  • DeleteRef 允许持有者从存储中删除对象。
  • ExtendRef 允许持有者获得访问 signer 的权限以添加新资源。
  • TransferRef 允许持有者在创建后将新资源转移到对象。

如果存在于模块中的 DeleteRef 允许创建者或所有者销毁 Token(如果存在)。而 TransferRef 可以在模块内使用,以定义确定何时以及由谁可以转移对象的逻辑,也可以将其赠送出去,并将其视为转移对象能力的基础。

此外,对象还支持对象的可组合性,允许对象拥有其他对象。每个对象都在其状态中存储其所有者的身份。所有者可以通过在其自己的存储中创建并存储 Object<T> 来跟踪其拥有的对象。从而实现了对象模型中无缝的双向导航。

基本原理

现有的 Aptos 数据模型强调在 Move 中使用 store ability。Store 允许一个 struct 存在于存储在全局存储中的任何 struct 中,并用 key ability 标记。因此,数据可以存在于任何 struct 中的任何位置和任何地址。虽然这提供了很大的灵活性,但它也有许多限制:

  • 无法保证数据可访问,例如,它可以放置在用户定义的资源中,这可能会违反对该数据的期望,例如,创建者试图销毁放入用户定义的 store 中的 NFT。这可能会让用户和这些数据的创建者感到困惑。
  • 不同类型的数据可以通过 any 存储到单个数据结构(例如,map、vector)中,但对于复杂的数据类型,any 会在 Move 中产生额外的成本,因为每次访问都需要反序列化。如果 API 开发人员期望特定的 any 字段更改其表示的类型,这也可能导致混淆。
  • 虽然资源账户允许更大的数据自主性,但对于对象来说,这样做效率低下,并且没有利用资源组。
  • 数据不能递归组合,Move 目前对递归数据结构有限制。此外,经验表明,真正的递归数据结构可能导致安全漏洞。
  • 无法从 entry function 轻松引用现有数据,例如,支持字符串验证需要很多行代码。表中的每个 key 都可能非常独特,并且专门支持 entry function 会变得很复杂。
  • 事件不能从数据中发出,而是从可能与数据无关的账户中发出。
  • 转移逻辑仅限于各个模块中提供的 API,通常需要在发送者和接收者上加载资源,从而增加不必要的成本开销。

替代方案

在这个过程中,考虑了以下几种替代方案:

  • 使用 OwnerRef 而不是在对象中定义所有权。OwnerRef 提供了一种自然的“嵌套”对象方法。但是,OwnerRef 需要额外的存储空间,仍然需要访问对象才能提供门控存储,并且在可删除对象的情况下会成为弱引用。
  • 拥有一个显式的对象存储。Move 的其他实现实现了动态字段、bags 和其他机制来跟踪所有权。这需要额外的存储空间,限制了可删除性,并且不可迭代。虽然某些应用可能适合这样做,但 Move 对象的目标是使开发人员能够决定他们喜欢的路径,同时尽可能保持客观。
  • 允许对象拥有 store。直接在对象上拥有 store 违反了对象的原则,即始终可以全局访问。
  • 更广泛的引用或能力集。有很多机会可以投资于更丰富的手段来访问和管理对象。与其在此时规定一套最佳实践,不如推迟到对象在社区中有足够的烘焙时间。最终,本文定义的对象核心应该足以支持适用于所有应用的高级功能的任何发展方向。
  • 添加对撤销现有引用或能力的支持。具有 store ability 的每个引用都可以放入任何模块中,目的是允许创建者决定对象如何随时间演变。因此,实际上不存在通用的方法来撤销特定能力。能力可以授予给账户或模块,并存储在任何地方。目的是保持它们的灵活性,以免影响存储或增加使用过程中的额外摩擦。当然,要谨慎,因为对能力的幼稚使用可能会导致不良行为。

规范

概述

构建对象时考虑了以下几点:

  • 简化的存储接口,支持将异构资源集合存储在一起。这使得数据类型可以共享一个共同的核心数据层(例如,Token),同时具有更丰富的扩展(例如,演唱会门票、剑)。
  • 全局可访问的数据和所有权模型,使创建者和开发者能够决定数据的应用和生命周期。
  • 可扩展的编程模型,支持利用包括 Token 在内的核心框架的用户应用个性化。
  • 支持直接发出事件,从而提高与对象关联的事件的可发现性。
  • 通过利用资源组来提高 Gas 效率,避免昂贵的反序列化和序列化成本,并支持可删除性,从而考虑到底层系统。

对象生命周期

  • 实体在顶级对象类型上调用 create。
  • 顶级对象类型在其直接祖先上调用 create。
  • 重复此步骤,直到最顶层的祖先是 Object struct,它定义了 create_object 函数。
  • create_object 生成一个新地址,将 Object struct 存储在该地址的 ObjectGroup 资源组中,并返回一个 ConstructorRef
  • 之前调用的 create 函数可以使用 ConstructorRef 获取 signer,将适当的 struct 存储在 ObjectGroup 资源中,进行任何其他修改,然后将 ConstructorRef 返回到堆栈中。
  • 在对象创建堆栈中,任何模块都可以定义所有权、删除、可转移性和可变性等属性。

核心对象

对象存储在 ObjectGroup 资源组中。这允许对象中的其他资源为了数据局部性和数据成本节省而并置。请注意,对象中的所有资源不必都并置在 ObjectGroup 中。这留给对象的开发者来定义他们的数据布局。

##[resource_group(scope = global)]
struct ObjectGroup { }

核心 ObjectCore 由以下 Move struct 表示:

##[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct ObjectCore has key {
    /// 用于 guid 以保证全局唯一对象并创建事件流
    guid_creation_num: u64,
    /// 拥有此对象的地址(对象或帐户)
    owner: address,
    /// 对象转移是一种常见操作, 这允许禁用和启用
    /// 传输。绕过 TransferRef 的使用。
    allow_ungated_transfer: bool,
    /// 所有权转移时发出的事件。
    transfer_events: event::EventHandle<TransferEvent>,
}

对象

每个对象都存储在由 Object<T> 表示的自己的地址中。底层地址可以从用户提供的输入或从当前账户的全局唯一 ID 生成器 (guid.move) 生成。此外,地址生成利用了与现有账户地址生成不同的域分隔符:sha3_256(address_of(creator) | seed | DOMAIN_SEPaRATOR)

  • GUID
    • seed 是由 guid.move 生成的当前 GUID:bcs::to_bytes(account address | u64)
    • 可以删除,因为无法重新创建该对象。
    • DOMAIN_SEPaRATOR 是 0xFD。
  • 用户指定
    • seed 由用户定义,例如字符串。
    • 无法删除,因为可以重新创建该对象,至少,这种冲突可能会影响正确的事件编号排序。
    • DOMAIN_SEPaRATOR 是 0xFE。

Capabilities

  • Object<T>
    • 指向对象的指针,address 的包装器
    • Abilities:copy, drop, store
    • 数据布局:{ inner: address }
  • ConstructorRef
    • 在创建时提供给对象创建者
    • 可以创建任何其他引用类型
    • Abilities:drop
    • 数据布局:{ self: ObjectId, can_delete: bool }
  • DeleteRef
    • 用于从 ObjectGroup 中删除对象
    • 一个对象可以有多个
    • 如果对象地址是从用户输入生成的,则无法创建
    • Abilities:drop, store
    • 数据布局:{ self: ObjectId }
  • ExtendRef
    • 用于创建事件或将其他资源移动到对象存储中。
    • Abilities:drop, store
    • 数据布局:{ self: ObjectId }
  • TransferRef
    • 用于创建 LinearTransferRef,因此可以进行所有权转移。
    • Abilities:drop, store
    • 数据布局:{ self: ObjectId }
  • LinearTransferRef
    • 用于执行转移。
    • 强制执行实体只能转移一次,假设他们没有直接访问 TransferRef 的权限
    • Abilities:drop
    • 数据布局:{ self: ObjectId, owner: address }

API 函数

用于 addressObjectId 之间转换的函数:

/// 从给定的地址生成一个对象。这会验证 T 是否存在。
public fun address_to_object<T: key>(object: address): Object<T>;

/// 返回 ObjectId 中的地址。
public fun object_address<T>(object: &Object<T>): address;

/// 从源材料派生对象地址:sha3_256([创建者地址 | seed | 0xFE])。
/// 对象需要与 create_resource_address 区分开来
public fun create_object_address(source: &address, seed: vector<u8>): ObjectId;

用于创建对象的函数:

/// 创建一个新的命名对象并返回 ConstructorRef。通过知道用于创建它们的、用户生成的 seed,可以全局查询命名对象。命名对象无法删除。
public fun create_named_object(creator: &signer, seed: vector<u8>): ConstructorRef;

/// 从账户生成的 GUID 创建一个新对象。
public fun create_object_from_account(creator: &signer): ConstructorRef;

/// 从对象生成的 GUID 创建一个新对象。
public fun create_object_from_object(creator: &signer): ConstructorRef;

/// 生成 DeleteRef,可用于从全局存储中删除对象。
public fun generate_delete_ref(ref: &ConstructorRef): DeleteRef;

/// 生成 ExtendRef,可用于向对象添加新事件和资源。
public fun generate_extend_ref(ref: &ConstructorRef): ExtendRef;

/// 生成 TransferRef,可用于管理对象转移。
public fun generate_transfer_ref(ref: &ConstructorRef): TransferRef;

/// 为 ConstructorRef 创建一个 signer
public fun generate_signer(ref: &ConstructorRef): signer;

/// 返回 ConstructorRef 中的地址
public fun object_from_constructor_ref<T: key>(ref: &ConstructorRef): Object<T>;

用于向对象添加新事件和资源的函数:

/// 为对象创建一个 guid,通常用于事件
public fun create_guid(object: &signer): guid::GUID;

/// 生成一个新的事件Handle。
public fun new_event_handle<T: drop + store>(object: &signer): event::EventHandle<T>;

用于删除对象的函数:

/// 返回 DeleteRef 中的地址。
public fun object_from_delete<T: key>(ref: &DeleteRef): Object<T>;

/// 从指定的对象中删除全局存储。
public fun delete(ref: DeleteRef);

用于扩展对象的函数:

/// 为 ExtendRef 创建一个 signer
public fun generate_signer_for_extending(ref: &ExtendRef): signer;

用于转移对象的函数:

/// 禁用直接转移,只能通过 TransferRef 触发转移
public fun disable_ungated_transfer(ref: &TransferRef);

/// 启用直接转移。
public fun enable_ungated_transfer(ref: &TransferRef);

/// 为一次性转移创建 LinearTransferRef。这要求生成时的所有者是转移时的所有者。
public fun generate_linear_transfer_ref(ref: TransferRef): LinearTransferRef;

/// 使用 LinearTransferRef 转移到目标地址。
public fun transfer_with_ref(ref: LinearTransferRef, to: address);

/// 如果 allow_ungated_transfer 设置为 true,则可以使用此 entry function 进行转移。
public entry fun transfer_call(owner: &signer, object: address, to: address);

/// 如果 allow_ungated_transfer 设置为 true,则转移给定的对象。请注意,这允许嵌套对象的所有者转移该对象,只要在层次结构的每个阶段都启用了 allow_ungated_transfer。
public fun transfer<T>(owner: &signer, object: Object<T>, to: address);

/// 将给定的对象转移到另一个对象。有关更多信息,请参见 `transfer`。
public fun transfer_to_object<O, T>(owner: &signer, object<O>: Object, to: Object<T>);

用于验证所有权的函数:

/// 返回当前所有者。
public fun owner<T: key>(object: Object<T>): address;

/// 如果提供的地址是当前所有者,则返回 true。
public fun is_owner<T: key>(object: Object<T>, owner: address): bool;

API 事件

/// 每当对象的所有者字段更改时发出。
struct TransferEvent has drop, store {
    object: address,
    from: address,
    to: address,
}

参考实现

https://github.com/aptos-labs/aptos-core/pull/5976 中的第一个提交

风险和缺点

开放讨论的领域包括:

  • 资源添加和删除是否应该计算对象中的资源数量,以确保删除完成?
  • 我们是否应该将基于用户输入的对象标记为已删除,而不是完全不支持它?这样 DeleteRef 在更高层仍然有用。
  • 对象是否应该跟踪已生成哪些引用,以及它们是否应该是指向热数据的指针?或者,是否应该发出事件?
  • 指向对象的 Object<T> 是否应该阻止其被删除?

当前的 base object 可以扩展以支持所有这些用例。决定是偏向于限制较少的一方,以便在对象标准发展后可以采取措施。

未来潜力

  • 允许 entry function 指定与对象关联的(预期)能力,可以通过属性或允许传入某些 Ref 类型。
  • 允许用户指定可以将哪些资源转移给他们。这可以通过以下方式实现:1) 支持用户对象存储,该存储表示用户已接受的对象,或者 2) 在帐户结构中设置一个标志,指示用户是否愿意接受对象。
  • Alias<T>
    • 假设模块 hero 定义了多个版本的 struct(HeroV1HeroV2、...)
    • 应用程序只关心引用模块 hero 中的 Hero 概念,而不关心特定版本。
    • 由于借用全局语义,API 永远无法显示版本化的 Hero,模块本身可以期望 Object<Hero> 并分派到 Hero 的相关版本。
  • 禁用转移事件
    • 对象可能在某些时候用于创建丰富的数据结构,并且事件的成本可能不可忽略。
    • 在 Gas优化之后,这与执行成本相比实际产生的费用还有待观察。
  • 唯一的资源命名容器对象
    • 用于可互换资产的对象容器会导致多个无法找到的副本,除非提供了作为事务输入的显式引用。
    • 让我们提供一个新的对象地址方案:account_address | type_info<T>() | 0xFC
    • 这些地址上的对象通过 create_typed_object<T>(account: &signer, &_proof: T) 创建,这确保了这些对象实际上将包含 T,即可互换资产的存储。
    • 现在,任何其他对象都可以在链上读取此内容,而无需任何事务输入,例如,balance<0x1::aptos_coin::AptosCoin>(addr) 将从存储在 addr | 0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin> | 0xFC 中的 0x1::coin::CoinStore 对象读取。
    • 地址计算可能不可忽略,因此支持显式寻址的 API 也将是有益的。

建议的实施时间表

  • 一个简单的实现已经完成,可以进行审查。
  • 假设总体反馈是积极的,这可能会进展到 2 月的测试网版本。
  • 如果进展顺利,可能会在 3 月份在主网上发布。
  • 原文链接: github.com/aptos-foundat...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
aptos-foundation
aptos-foundation
江湖只有他的大名,没有他的介绍。