Skip to main content

Aptos QA

Aptos 数据模型

Aptos 的 Object 模型和它与传统账户资源模型的区别

https://aptos.dev/standards/aptos-object#comparison-with-the-account-resources-model

让我们用更通俗的语言来解释一下 Aptos 的 Object 模型和它与传统账户资源模型的区别。

Object 模型

想象一下,你有一个玩具箱(Object 模型),你可以在里面放各种各样的玩具(资源),比如小汽车、积木、玩偶等。每种玩具都有自己的小盒子(资源类型),这样它们就不会混在一起。你还有一把特别的钥匙(能力模型),可以决定谁能打开玩具箱,谁能玩哪些玩具,甚至谁能把玩具带走。

比如,你有一个特别的积木(NFT),它放在自己的盒子里(Token 资源),还有一个说明它是谁的(ObjectCore 资源),以及一些特别的积木块(其他资源),这样你就可以根据自己的想法来组合它们。

传统账户资源模型

现在,想象一下你有一个大房间(账户资源模型),里面到处都是玩具,没有放在玩具箱里。这样虽然看起来你有很多自由度,可以随便放玩具,但实际上会很乱。有时候你想找一个特定的玩具(数据访问),可能会找不到;有时候你想确保某个玩具只能你玩(类型混合),但因为它们都混在一起,别人也可能拿去玩。

而且,如果你想告诉别人你有一个新的玩具(事件发射),你得先清理一下房间,找到那个玩具,然后才能告诉大家。如果你想把一个玩具给别人(转移逻辑),你可能需要先找到玩具,然后还得确保对方也有地方放这个玩具,这样做起来就很麻烦。

总结

所以,Object 模型就像一个有组织的玩具箱,让你可以更好地管理和控制你的玩具(资源)。它让你知道每个玩具在哪里,谁可以玩它们,以及如何安全地分享它们。而传统的账户资源模型就像一个大房间,虽然自由度高,但管理起来很混乱,而且有时候会很不方便。

Object vs ObjectGroup

这段描述了 Aptos 的 Object 模型中对象(Object)的结构和如何使用它来创建和管理资源。下面是一个简化的解释:

ObjectGroup 资源组

  • 资源组定位:一个对象(Object)被存储在一个名为 ObjectGroup 的资源组中。这样做的目的是为了让对象内的相关资源能够被放在一起,从而优化数据访问效率和节省数据存储成本。

  • 灵活的数据布局:开发者可以自行决定哪些资源需要放在 ObjectGroup 中,以获得数据局部性的好处。

    ObjectCore 资源

  • 基础属性:每个对象都有一个核心资源 ObjectCore,它包含了一些基本属性,如全局唯一标识符(用于创建事件流)、所有者地址、是否允许无门槛转移,以及转移事件的事件句柄。

  • 关键代码ObjectCore 中的代码定义了这些属性,并且标记了它是 ObjectGroup 的一部分。

    扩展对象

  • 自定义资源:创建对象后,开发者可以向对象中添加额外的资源。例如,一个交易所可以为每个流动性池创建一个对象,并添加一个资源来追踪池的流动性。

  • 示例代码:在代码示例中,LiquidityPool 结构体代表了流动性池,它包含了代表资产的对象引用和储备量。这个资源也是 ObjectGroup 的一部分,意味着它与 ObjectCore 存储在同一个位置,这样做可以提高存储和数据操作的效率。

    创建流动性池

  • 构造函数:示例中的 create_liquidity_pool 函数展示了如何创建一个流动性池对象。它使用了一系列的 Move 语言操作来初始化一个新的 LiquidityPool 资源,并将它与创建者的账户关联起来。

  • 交易所签名者:函数中使用了 exchange_signer 来代表交易所的权限,它是创建流动性池对象的必要条件。

  • 对象构造和注册:通过 object::create_object_from_accountobject::generate_signer 函数创建对象,并使用 move_toLiquidityPool 资源放入正确的存储位置。

    后续扩展

  • 扩展引用:如果交易所模块存储了 ExtendRef,它可以在对象创建后继续添加更多资源。ExtendRef 是一种能力,允许对象在创建后进行扩展。

    总结

这里描述的是如何在 Aptos 平台上使用 Move 语言构建和管理复杂的对象,它们可以包含多个相关资源,并且可以在创建后进行扩展。这种模型旨在提高数据操作的效率和减少存储成本,同时提供灵活的数据管理方式。

这段的意思是,Object 都要放到 ObjectGroup 来组织?ObjectCore 也要放到 ObjectGroup 来组织?

是的,你的理解是正确的。在 Aptos 的 Object 模型中,Object是资源的容器,这些资源被存储在一个单一的地址内。通常情况下,这些相关联的资源会被频繁地一起访问,因此把它们存储在一个地址内是为了数据局部性和节省成本。

ObjectGroup是默认的资源组,当你创建一个 Object 时,它会自动包含一个ObjectGroup。这是一个全局作用域的结构体,用来组织和存储 Object 内的资源。

ObjectCore是每个 Object 都有的核心资源,它包含了 Object 的基本属性,如唯一标识符、所有者地址、转移权限设置和转移事件句柄等。ObjectCore也是ObjectGroup的一部分,这意味着它和其他的资源一起存储在同一地址的存储槽中。

简而言之,ObjectGroup是用来组织一个 Object 内的所有资源的,而ObjectCore是这个组织内的一个核心成员,包含了一些基本的属性信息。这种设计使得相关资源的存储和访问更加高效,同时也便于管理。

Object、ObjectCore、ObjectGroup

虽然我无法直接绘制图形,我可以帮助你设计一个概念图的结构,以便你使用图形工具如 Visio、Lucidchart 或在线绘图工具来绘制。下面是一个描述 Object、ObjectCore、和 ObjectGroup 之间关系的图形设计:

概念图设计

  1. Object(对象)

    • 表示为一个矩形或圆形。
    • 包含一个内部矩形或圆形表示 ObjectCore(对象核心)。
    • 可以标注额外的属性或状态信息在 Object 外围。
  2. ObjectCore(对象核心)

    • 在每个 Object 内部,用一个小的矩形或圆形表示。
    • 标注基本属性如 ID、版本号、所有者信息。
  3. ObjectGroup(对象组)

    • 用一个更大的矩形或圆形表示,包含多个 Object。
    • 可以用虚线或不同颜色区分单个 Object 和 ObjectGroup。

图形示例描述

 [ObjectGroup: 对象组]
|
|---[Object: 对象1]
| |
| |---[ObjectCore: 对象核心]
| | |-- ID: 001
| | |-- 版本号: v1
| | |-- 所有者: 用户A
|
|---[Object: 对象2]
| |
| |---[ObjectCore: 对象核心]
| | |-- ID: 002
| | |-- 版本号: v2
| | |-- 所有者: 用户B
|
|---[Object: 对象3]
|
|---[ObjectCore: 对象核心]
| |-- ID: 003
| |-- 版本号: v1
| |-- 所有者: 用户C

绘制步骤

  1. 开始绘制一个大的矩形或圆形来代表 ObjectGroup。
  2. 在 ObjectGroup 内部绘制多个小的矩形或圆形,每个代表一个 Object。
  3. 在每个 Object 内部,再绘制一个更小的矩形或圆形,标注为 ObjectCore,并填写基本属性。
  4. 可以使用不同的颜色或线型区分 Object 和 ObjectCore,以及它们与 ObjectGroup 的关系。

使用这种结构,你可以清晰地展示 Object、ObjectCore 和 ObjectGroup 之间的关系和层次。希望这个描述能帮助你绘制出有用的图表!

ObjectCore 类似 gorm 的 gorm.Model

是的,您可以把 ObjectCore 比作 Golang ORM 库 GORM 中的 gorm.Model。在 GORM 中,gorm.Model 是一个包含了一些基础字段的结构体,如 ID, CreatedAt, UpdatedAt, DeletedAt,它们是大多数数据库表都会有的字段。当你在你的模型中嵌入 gorm.Model 时,你的模型就自动继承了这些基础字段。

类似地,在 Aptos 的 Move 语言中,ObjectCore 是一个包含了一些基础属性的资源,这些属性对于区块链上的对象来说是通用的。当你在自定义资源中包含 ObjectCore 时,这个资源就自动拥有了这些属性,如全局唯一标识符、所有者地址、转移权限标志等。

这两种情况下,都是为了减少代码重复,提高开发效率,并保持数据结构的一致性。不过,要注意的是,尽管这两个概念在某些方面相似,但它们工作在完全不同的环境和上下文中:ObjectCore 在 Aptos 区块链平台的 Move 语言环境中,而 gorm.Model 在 Golang 的 ORM 环境中。此外,Move 语言的资源模型与 Golang 中的结构体模型在语义上有很大的不同,尤其是在所有权和可变性方面。

Object 操作

https://aptos.dev/standards/aptos-object#object-capabilities-refs

在 Aptos 框架中,对象(Object)的创建和管理依赖于一系列称为“引用”(refs)的能力。当一个对象被创建时,创建函数返回一个临时的ConstructorRef。这个引用是短暂的,意味着它不能被存储在链上的持久存储中。ConstructorRef用于在对象创建过程中添加资源,正如前面节中的例子所展示的。除此之外,ConstructorRef还可以用来生成其他几种管理对象的能力或引用:

  • DeleteRef:通过generate_delete_ref函数生成,可以用来从全局存储中删除对象。
  • ExtendRef:通过generate_extend_ref函数生成,可以用来向对象添加新的事件和资源。
  • TransferRef:通过generate_transfer_ref函数生成,可以用来管理对象的转移。
  • Signer:通过generate_signer函数生成,这是一个代表对象创建者的签名者,用于在对象创建过程中授权操作。

这些引用代表了对对象的不同管理能力,可以被存储起来以便于后续使用。例如,如果一个模块想要在未来向对象添加新的资源,它可以在对象创建时获取一个ExtendRef并将其存储。之后,该模块可以使用这个ExtendRef来扩展对象,即使在对象创建过程已经结束之后。

简单来说,这些引用是管理对象生命周期和功能的工具。它们允许对象的创建者或拥有者在对象创建后继续对其进行操作,如添加资源、转移所有权,甚至删除对象。这些操作都是通过相应的引用来实现的,确保了对象的安全和灵活性。

info

看起来没有 Rust 方便,这里需要先添加引用,在未来某个时候通过 Object API 来操作这个对象

确实,Aptos 的 Move 语言在对象管理方面采用了一种不同于 Rust 的方法。在 Rust 中,所有权和借用是编程语言的核心概念,允许你直接通过变量和引用来管理资源。而 Aptos 的 Move 语言通过显式的能力(capability)系统来管理对象。

在 Move 中,当你创建一个对象时,你必须显式地生成和使用不同的引用(refs)来管理该对象。这些引用是特殊的能力,它们提供了对对象进行特定操作的权限。这种设计模式使得对象的管理更加安全,因为每个操作都需要相应的能力,从而减少了错误或恶意行为的风险。

虽然这种方法可能不如 Rust 的所有权模型直观,但它提供了一种细粒度的权限控制,这在智能合约和区块链环境中是非常重要的。在区块链上,对资源的严格控制可以防止未授权的访问和修改,保证了智能合约的安全性和确定性。

总的来说,Aptos 的 Move 语言通过引用和能力系统提供了一种灵活而安全的方式来管理链上对象,虽然这可能需要开发者适应一种新的编程范式。

#[view] vs entry

在 Move 语言中,#[view]entry 是两种不同的函数属性,它们指示函数的用途和行为。

#[view] 属性:

  • #[view] 属性用于标记一个函数,表明该函数是只读的,不会修改任何全局状态。这类似于其他编程语言中的“只读”或“常量”函数。
  • 使用 #[view] 属性的函数不会产生任何副作用,因此它们可以安全地用于查询全局存储中的数据,而不会改变任何状态。
  • #[view] 函数通常用于获取资源的信息或计算派生值,而不是执行实际的状态变更操作。

entry 属性:

  • entry 属性用于标记一个函数为入口点函数,这意味着该函数可以被外部的交易调用。
  • 具有 entry 属性的函数可以修改全局状态,它们可以创建、更新或销毁资源,或者执行其他有副作用的操作。
  • entry 函数是 Move 智能合约与外界互动的主要方式,它们是执行交易逻辑的关键。

简而言之,#[view] 属性用于标记不会修改状态的只读函数,而 entry 属性用于标记可以被外部调用并可能修改全局状态的入口点函数。这两个属性有助于区分函数的用途和它们对全局状态的影响,从而在智能合约的设计中提供更好的安全性和清晰性。

请注意,Move 语言的设计和属性可能会随着 Move 语言和相应区块链平台的发展而更新。为了获取最准确的信息,应该参考 Move 语言的最新官方文档。

borrow_global 从全局存储中借用这个资源的引用

在 Move 语言中,borrow_globalborrow_global_mut 函数用于从全局存储中借用对资源的引用。它们的语法如下:

borrow_global<T>

用于借用一个不可变的引用(immutable reference)到全局存储中的资源。这意味着你不能通过这个引用修改资源。

borrow_global<T>(addr: address): &T
  • T 是资源类型。
  • addr 是资源所在的账户地址。

borrow_global_mut<T>

用于借用一个可变的引用(mutable reference)到全局存储中的资源。这意味着你可以通过这个引用修改资源。

borrow_global_mut<T>(addr: address): &mut T
  • T 是资源类型。
  • addr 是资源所在的账户地址。

示例

假设你有一个名为 MyResource 的资源定义,并且你想要从全局存储中借用这个资源的引用。以下是如何使用 borrow_globalborrow_global_mut 函数的示例。

module MyModule {
struct MyResource has key {
value: u64,
}

public fun get_resource_value(addr: address): u64 acquires MyResource {
let resource_ref: &MyResource = borrow_global<MyResource>(addr);
resource_ref.value
}

public fun update_resource_value(addr: address, new_value: u64) acquires MyResource {
let resource_ref: &mut MyResource = borrow_global_mut<MyResource>(addr);
resource_ref.value = new_value;
}
}

在这个示例中,get_resource_value 函数使用 borrow_global 来借用 MyResource 的不可变引用,并返回其 value 字段。update_resource_value 函数使用 borrow_global_mut 来借用 MyResource 的可变引用,并更新其 value 字段。

注意,acquires MyResource 是一个 Move 语言特性,它在函数签名中声明了函数可能会读取或修改的全局存储中的资源类型。这有助于在编译时检查代码以确保资源的正确使用和访问控制。

Aptos 数据存储及使用

Aptos move 数据存储在全局资源对象中的,每次要使用都要从全局资源中借用或者是获取能力引用来进行操作?

是的,Aptos 使用 Move 语言的设计哲学,其中数据(特别是资源)存储在全局存储中,并通过账户地址进行索引。资源是 Move 语言中的一等公民,它们具有特定的属性,例如:

  • 资源不能被复制或随意丢弃:这确保了资源的持久性和一致性。
  • 资源的所有权是明确的:每个资源都有一个明确的所有者,即存储该资源的账户。
  • 资源是封装的:资源的内部状态只能通过定义在资源所在模块中的函数进行访问和修改。

为了与全局存储中的资源交互,Move 提供了特定的函数:

  • borrow_global<T>borrow_global_mut<T>:这些函数用于从全局存储中借用对资源的不可变或可变引用。通过这些引用,可以安全地读取或修改资源,而无需将资源移动到当前执行上下文。
  • move_from<T>move_to<T>:这些函数用于从全局存储中移动资源(取出或放入)。

示例

假设你有一个名为 MyResource 的资源,并且你希望更新存储在某个账户中的该资源的状态。你可以使用 borrow_global_mut 函数来获取一个可变引用,然后通过这个引用来修改资源:

module MyModule {
struct MyResource has key {
value: u64,
}

public fun update_value(account: &signer, new_value: u64) acquires MyResource {
let resource_ref = borrow_global_mut<MyResource>(Signer::address_of(account));
resource_ref.value = new_value;
}
}

在这个例子中,update_value 函数首先获取对应账户地址下 MyResource 的可变引用,然后更新其 value 字段。

能力引用(Capability)

Move 语言还支持一种叫做“能力(Capability)”的概念,它是一种抽象,用于表示对某个资源或系统功能的访问权。能力通常以资源的形式存在,可以授予其他账户以允许它们以受控的方式执行某些操作。

例如,如果你想允许其他账户以受限的方式修改 MyResource,你可以创建一个代表这种修改权限的能力资源,并提供一个函数来接受这个能力作为参数来执行修改操作。

总结

在 Aptos 和 Move 语言中,全局资源的管理非常严格,确保了资源的安全性和一致性。每次需要读取或修改资源时,都需要通过借用或获取能力引用来进行操作,这样做可以避免资源的不当复制和非法访问。

允许其他账户以受限方式操作

如果你想允许其他账户以受限的方式修改 MyResource,你可以创建一个代表这种修改权限的能力资源,并提供一个函数来接受这个能力作为参数来执行修改操作。 示例代码

在 Move 语言中,创建一个代表修改权限的能力资源通常涉及到定义一个新的资源类型,该资源类型代表了对另一个资源的特定权限。然后,你可以提供一个或多个函数来发放、管理和使用这些权限。

下面是一个简单的例子,展示了如何创建一个代表 MyResource 修改权限的能力资源 ModifyCapability,以及如何使用这个能力来修改 MyResource

address 0x1 {
module MyModule {
// 定义一个资源,代表对MyResource的修改权限
struct ModifyCapability has key {
// 可能包含一些认证信息或者对应资源的地址
resource_address: address,
}

// 定义MyResource
struct MyResource has key {
value: u64,
}

// 初始化函数,用于设置MyResource和ModifyCapability
public fun initialize(account: &signer) {
// 创建MyResource并存储到调用者的账户下
move_to(account, MyResource { value: 0 });
// 创建ModifyCapability并存储到调用者的账户下
move_to(account, ModifyCapability { resource_address: Signer::address_of(account) });
}

// 函数,用于修改MyResource的值,需要传入ModifyCapability
public fun modify_value(account: &signer, capability: &ModifyCapability, new_value: u64) acquires MyResource {
// 确认能力与资源在同一个账户下
assert!(Signer::address_of(account) == capability.resource_address, 403);

// 借用MyResource的可变引用
let my_resource_ref = borrow_global_mut<MyResource>(capability.resource_address);
// 修改MyResource的值
my_resource_ref.value = new_value;
}

// 函数,用于授予其他账户修改MyResource的权限
public fun grant_modify_capability(account: &signer, recipient: address) acquires ModifyCapability {
// 取出发送者的修改权限
let cap = move_from<ModifyCapability>(Signer::address_of(account));
// 将修改权限存储到接收者的账户下
move_to(Signer::borrow_signer(recipient), cap);
}

// 函数,用于撤销其他账户的修改权限
public fun revoke_modify_capability(account: &signer, target: address) acquires ModifyCapability {
// 从目标账户移除ModifyCapability
let _cap = move_from<ModifyCapability>(target);
// 可以选择将其销毁或者重新存储到某个账户下
}
}
}

在这个例子中:

  • ModifyCapability 是一个资源,代表了对 MyResource 的修改权限。
  • initialize 函数用于创建和存储 MyResourceModifyCapability 到调用者的账户下。
  • modify_value 函数接受一个 ModifyCapability 引用和一个新值,如果验证通过,就会修改 MyResource 的值。
  • grant_modify_capability 函数允许账户所有者将自己的 ModifyCapability 转移到另一个账户。
  • revoke_modify_capability 函数允许账户所有者从另一个账户中移除 ModifyCapability

请注意,这个例子是一个简化的示例,实际应用中可能需要更复杂的逻辑来确保安全性和正确性。例如,你可能需要跟踪哪些账户拥有 ModifyCapability,以便可以有效地撤销它们。此外,你需要确保 ModifyCapability 不能被随意创建,只能由 MyResource 的所有者授予。

事件发射

alt text

Move 语言中的 move_to 函数用于将资源移动到全局存储中与某个地址关联。在您提供的代码中,move_to 用于将 LiquidityPool 资源实例存储在由 liquidity_pool_signer 指定的地址下。这个过程本身并不会触发事件的创建或者发射。

在这段代码中,事件的创建和发射是通过以下步骤完成的:

  1. 创建一个 event_handle,这是一个 EventHandle 类型的资源,它将用于跟踪和发射 CreateLiquidtyPoolEvent 事件。
let event_handle = object::new_event_handle<CreateLiquidtyPoolEvent>(liquidity_pool_signer);
  1. 使用 event::emit 函数发射一个 CreateLiquidtyPoolEvent 事件。这个函数实际上是将一个事件实例添加到 event_handle 关联的事件流中。
event::emit<CreateLiquidtyPoolEvent>(event_handle, CreateLiquidtyPoolEvent {
token_a: token_a,
token_b: token_b,
reserves_a: reserves_a,
reserves_b: reserves_b,
});
  1. 创建 LiquidityPool 资源实例,它包含了流动性池的信息和刚刚创建的 event_handle
let liquidity_pool = LiquidityPool {
token_a: token_a,
token_b: token_b,
reserves_a: reserves_a,
reserves_b: reserves_b,
create_events: event_handle,
};
  1. 最后,使用 move_to 函数将 LiquidityPool 资源实例移动到全局存储中。
move_to(liquidity_pool_signer, liquidity_pool);

在这个流程中,事件的创建和发射发生在 move_to 调用之前。move_to 只是将包含事件句柄的 LiquidityPool 资源实例存储到全局存储中。事件句柄允许后续的事件发射,并且可以被外部的监听者订阅和监听。

注意,您的代码片段中缺少了一些上下文,例如 LiquidityPool 结构的定义,以及 token_atoken_breserves_areserves_b 变量的定义和来源。为了完整性和正确性,这些部分需要在代码中有所体现。

程序有没有可能在 move_to 之前发出事件,在 move_to 执行时候出现异常,会出现不一致现象?

是的,如果在执行 move_to 之前发出了事件,而 move_to 执行时出现异常导致操作失败,那么确实可能出现不一致的现象。这是因为事件已经被发出并记录在区块链上,而资源(在这个例子中是 LiquidityPool)由于异常并没有被成功地存储在全局存储中。

在区块链和智能合约的设计中,事务(transaction)的原子性是非常重要的。原子性确保了事务中的所有操作要么全部成功,要么全部失败。如果在事务中一部分操作成功而另一部分失败,就会导致状态不一致,这在区块链上是不可接受的。

为了防止这种不一致现象,Move 语言和其他智能合约平台通常提供了一种机制来确保事务的原子性。在 Move 中,如果事务中的任何操作失败,整个事务都会被回滚,所有的状态变更(包括事件的发射)都会被撤销。这意味着即使在 move_to 调用之前发出了事件,如果 move_to 失败,整个事务都会回滚,就像什么都没有发生过一样。

因此,在 Move 中,如果 move_to 失败导致异常,那么整个事务将回滚,包括之前发出的事件。这就保证了即使出现异常,区块链的状态也不会出现不一致。这是区块链设计中的关键特性,用以确保状态的一致性和数据的可靠性。

这个事务是 move 对一个 transaction 默认实现的,不需要合约开发者手动实现

是的,Move 语言的设计确保了事务的原子性,这是 Move 虚拟机(VM)的内部机制提供的,并不需要智能合约开发者手动实现。当一个事务在执行过程中遇到任何错误或异常,Move VM 将回滚事务中的所有更改,包括全局存储的写入操作和事件的发射。这意味着合约的状态和事件日志将恢复到事务执行之前的状态。

这种设计模式类似于数据库的事务回滚,它有助于确保数据的一致性和完整性,防止因为部分操作失败而导致的状态不一致问题。因此,合约开发者可以专注于合约逻辑的实现,而不必担心事务原子性的底层细节。