学习
核心概念
对象
对象

Rooch Object

Rooch 中的 Object 是一种箱子(Box)模式的 Object。创建一个 Object 相当于在状态空间中创建了一个箱子,可以将类型 T 的实例封装在箱子中,而 ObjectID 是这个箱子的地址。此外,这个箱子还支持动态添加状态,即 Object 的动态字段特性。

如果我们将智能合约的状态空间比作程序的堆内存,Object 类似于一个智能指针,提供了对状态的引用和操作,以及生命周期的管理。

Object 的所有权

Objectowner 代表 Object 属于哪个账户地址,通过 owner 可以将 Object 分为两种:

  1. SystemOwnedObject: owner0x0 的 Object。Object 创建后默认为 SystemOwnedObject
  2. UserOwnedObject: owner 为非 0x0 的 Object。Object 被转让给某个用户后,owner 会被设置为该用户的地址。

Object 的生命周期

创建 Object

通过调用 object::new 方法可以创建出类型为 T 的 Object。

module moveos_std::object {
    #[private_generics(T)]
    public fun new<T: key>(value: T): Object<T>;
}
  • 该方法受 private_generics(T) 保护,所以只有 T 所在的模块才能调用该方法。T 模块的开发者可以决定是否提供将 T 封装到 Object 中的方法。
  • T 必须拥有 key ability。
  • 该 Object 的 ObjectID 是系统自动分配的全局唯一 ID。

同时还提供了两种特殊的 Object 创建方式,不自动分配 ID,而是通过预先确定的算法来生成 ID,叫做 Named Object。

module moveos_std::object {
    #[private_generics(T)]
    public fun new_named_object<T: key>(value: T): Object<T>;

    #[private_generics(T)]
    public fun new_account_named_object<T: key>(account: address, value: T): Object<T>;
}
  • NamedObject: 用类型 T 的类型名来生成 ObjectID。生成的公式为 sha3(type_name<T>())。一般用于全局唯一的 Object,比如 0x2::timestamp::Timestamp
  • Account NamedObject: 是用 account 地址和 T 类型名来生成 ObjectID。生成的公式为 sha3(account + type_name<T>())。一般用于每个用户只有一个的 Object,比如 0x3::coin_store::CoinStore<CoinType>

操作 Object

所有权转让

将 Object 转让给 new_owner

module moveos_std::object {
    public fun transfer<T: key + store>(self: Object<T>, new_owner: address);
}

owner 将属于自己的 Object 通过 object_id 拿出来

module moveos_std::object {
    public fun take_object<T: key + store>(owner: &signer, object_id: ObjectID): Object<T>;
}

注意:当 Object 被拿出来后,owner 会被设置为 0x0,这时候 Object 就变成了 SystemOwnedObject

以上方法的 T 都必须拥有 key + store ability,我们可以把这种类型的 Object 称为 PublicObject,用户可以自己转让 PublicObject 的所有权。

如果是只有 key ability 的 Object,我们可以称为 PrivateObject,用户无法直接转让 PrivateObject 的所有权,需要借助 T 所在的模块提供的接口来转让 PrivateObject 的所有权。

module moveos_std::object {
    #[private_generics(T)]
    public fun take_object_extend<T: key>(object_id: ObjectID): (address, Object<T>);
}

take_object_extend 方法受 private_generics(T) 保护,只有 T 所在的模块才能调用,开发者可以决定是否提供将 PrivateObject 转让给其他用户的方法。

Object 的引用

Object<T> 有两种引用方式,一种是只读引用 &Object<T>,一种是可变引用 &mut Object<T>

我们可以通过 object::borrow(&Object<T>) 方法获取到 &T,通过 object::borrow_mut(&mut Object<T>) 获取到 &mut T。至于获取到 &T&mut T 后,可以进行哪些操作,这个由 T 的模块来定义。

有两种方法获取 Object 的引用:

  1. 通过 entry 方法传递进来。
entry fun my_entry(obj: &Object<MyStruct>, obj_mut: &mut Object<MyStruct>) {
    // do something 
}
  1. 通过 borrow_objectborrow_mut_object 方法获取。
module moveos_std::object {
    public fun borrow_object<T: key>(object_id: ObjectID): &Object<T>;

    public fun borrow_mut_object<T: key>(owner: &signer, object_id: ObjectID): &mut Object<T>;
}
  • 注意,Rooch 中的所有 Object 都是读公开的,任何人都可以通过 ObjectID 获取到任意的 &Object<T>
  • Object 的所有者可以通过 object::borrow_mut_object 获取 &mut Object<T> 引用。

给开发者的扩展方法:

module moveos_std::object {
    #[private_generics(T)]
    public fun borrow_mut_object_extend<T: key>(object_id: ObjectID): &mut Object<T>;
}
  • T 所在的模块可以通过 ObjectID 获取任意 &mut Object<T> 引用,除非该 Object 被冻结。

共享的(Shared)和冻结的(Frozen) Object

SystemOwnedObject Object<T> 有两种状态,一种是 shared,一种是 frozen

  • SharedObject:任何人都可以直接获取到 &mut Object<T> 引用。
  • FrozenObject:任何人都无法获取到 &mut Object<T> 引用,包括 T 所在的模块。

通过以下方法可以将 Object<T> 变为 SharedObject

module moveos_std::object {
    public fun to_shared<T: key>(self: Object<T>);
}

要获取 SharedObject 的可变引用,需要通过 entry 参数传递或者 object 提供的方法:

module moveos_std::object {
    public fun borrow_mut_object_shared<T: key>(object_id: ObjectID): &mut Object<T>;
}

通过以下方法可以将 Object<T> 变为 FrozenObject

module moveos_std::object {
    public fun to_frozen<T: key>(self: Object<T>);
}

注意:一旦 Object 变为 frozen 或者 shared,Object 就自动归属于 SystemOwnedObject,任何人都无法直接获取到 Object<T> 的实例,只能通过引用来操作 Object。

嵌套的 Object

Object<T> 本身拥有 store ability,所以可以嵌套在另外的结构体中作为字段,或者保存到 vectorTable 等容器中。

struct Avatar has key {
    head: Object<Head>,
    body: Object<Body>,
}

上面的例子中,Object<Head>Object<Body>Avatar 结构体的字段,这两个对象归属于 Avatar 这个结构体,如果 Object<Avatar> 被转让给其他用户,那么 Object<Head>Object<Body> 也会随着 Object<Avatar> 一起转让给其他用户。

  • 被嵌套的 Object 依然会在 Object Storage 中,可以通过前面描述的引用获取方法来获取 Object<T> 的引用。
  • 被嵌套的 Object 一定是 SystemOwnedObject

删除 Object

通过以下方法可以删除 Object:

module moveos_std::object {
    #[private_generics(T)]
    public fun remove<T: key>(self: Object<T>): T;
}

删除 Object 后会返回 Object 中封装的数据,只有 T 所在的模块才能调用该方法。

综上所述,针对不同状态的 Object 的操作,不同的用户具有不同的权限。以下是合约开发者和普通用户对不同类型 Object,使用 moveos_std::object 提供的方法可以进行的操作:

  • 合约开发者
objectownervalue abilitiestransferborrow muttake valueremove
sharedSystemOwnedObjectnot required×××
frozenSystemOwnedObjectnot required××××
publicUserOwnedObjectkey, store
privateUserOwnedObjectkey
  • 普通用户
objectownervalue abilitiestransferborrow muttake valueremove
sharedSystemOwnedObjectnot required×××
frozenSystemOwnedObjectnot required××××
publicUserOwnedObjectkey, store×
privateUserOwnedObjectkey×××

Object RPC

通过 rooch_getState RPC 接口可以获取到 ObjectEntity 的数据。

curl -H "Content-Type: application/json" -X POST \
--data '{"jsonrpc":"2.0","method":"rooch_getStates","params":["/object/0x2::timestamp::Timestamp", {"decode":true}],"id":1}' \
https://dev-seed.rooch.network
{
  "jsonrpc": "2.0",
  "result": [
    {
      "value": "0x711ab0301fd517b135b88f57e84f254c94758998a602596be8ae7ba56a0d14b3000000000000000000000000000000000000000000000000000000000000000004002db02e34050600",
      "value_type": "0x2::object::ObjectEntity<0x2::timestamp::Timestamp>",
      "decoded_value": {
        "abilities": 0,
        "type": "0x2::object::ObjectEntity<0x2::timestamp::Timestamp>",
        "value": {
          "flag": 4,
          "id": "0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca",
          "owner": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "value": {
            "abilities": 8,
            "type": "0x2::timestamp::Timestamp",
            "value": {
              "milliseconds": "1694571540000000"
            }
          }
        }
      }
    }
  ],
  "id": 1
}

Object 相关的方法列表

object 模块提供以下函数,可以对 Object 进行操作:

Object 函数#[private_generics<T>]说明
object::new<T: key>(T): Object<T>true创建 Object,将 T 封装到 Object 中,返回 Object<T>
object::new_named_object<T: key>(T): Object<T>trueObjectObjectIDT 类型生成
object::new_account_named_object<T: key>(address, T): Object<T>trueObjectObjectID 由 address 和 T 类型生成
object::borrow_object<T: key>(ObjectID): &Object<T>false通过 ID 借用 Object<T> 的只读引用
object::borrow_mut_object<T: key>(&signer, ObjectID): &mut Object<T>falseowner(&signer) 通过 ID 借用 Object<T> 的可变引用
object::borrow_mut_object_shared<T: key>(ObjectID): &mut Object<T>false通过 ID 借用共享 Object<T> 的可变引用
object::borrow_mut_object_extend<T: key>(ObjectID): &mut Object<T>true给开发者的扩展方法,T 所在的模块可以通过 ObjectID 获取任意 &mut Object<T>
object::exists_object(ObjectID): boolfalse通过 ObjectID 检测 Object 是否存在
object::id<T>(&Object<T>): ObjectIDfalse获取 ObjectID
object::owner<T: key>(&Object<T>): addressfalse获取拥有者的地址
object::borrow<T: key>(&Object<T>): &Tfalse&Object 借用 T 的只读引用
object::borrow_mut<T: key>(&mut Object<T>): &mut Tfalse通过 &mut Object 借用 T 的可变引用
object::transfer<T: key + store>(Object<T>, address)falseObject<T> 所有权转移给 address
object::transfer_extend<T: key>(Object<T>, address)true给开发者的扩展方法,将 Object<T> 所有权转移给 address
object::to_shared<T: key>(Object<T>)falseObject<T> 变为 SharedObject,任何人都可以直接获取到 &mut Object<T>
object::is_shared<T: key>(&Object<T>): boolfalse判断 Object<T> 是否为 SharedObject
object::to_frozen<T: key>(Object<T>)falseObject<T> 变为 FrozenObject,任何人都无法获取到 &mut Object<T>
object::is_frozen<T: key>(&Object<T>): boolfalse判断 Object<T> 是否为 FrozenObject
object::remove<T: key>(Object<T>): Ttrue删除 Object<T>,并返回其中的 T,只有 T 所在的模块才能删除 Object<T>

以上函数中,如果 #[private_generics<T>] 列为 true,表明只有 T 所在的模块才能调用。

Object 的 dynamic fields

Rooch 为 object 提供了管理动态字段的能力。动态字段是指将 Resource 或者 Object 以 key, value 的形式储存在 Object 中。特别是,key 可以是异质的,即不受 key 类型的限制。更具体的说,Object 可以被当作 Table (opens in a new tab)Bag (opens in a new tab) 来使用。

Rooch object 的提供了两种类型的动态字段:常规类型和Object类型。

常规类型的动态字段是指任何具有 store ability 的类型存放在 object 下;Object 类型的动态字段是将子 Object 对象存放在 object 下。

💡

注意:由于 Object 类型本身也具有 store ability,那把整个 Obejct<T> 作为一个普通的字段存放在 object 下和使用 Object 类型字段有什么区别?

  1. 如果通过 new_with_parent 创建的子 object,是属于父 object 的子对象,与父 object 在同一个 SMT 子树下。这对于整个父 object 的状态迁移,查询等管理都很便捷。
  2. 如果在全局创建的 object,即使通过 add_field 放到 object 的动态字段中了,它实际上也是属于 global object,它的状态树处于 Root 根下。

常规类型动态字段相关方法列表

方法说明
add_field<T: key, K: copy + drop, V: store>(obj: &mut Object<T>, key: K, val: V)添加一个动态字段到对象。如果已经存在相同的键,则会中止。字段本身不会存储在对象中,并且不能从对象中发现。
borrow_field<T: key, K: copy + drop, V: store>(obj: &Object<T>, key: K): &V获取对象中键对应的值的不可变引用。如果没有对应的键,则会中止。
borrow_field_with_default<T: key, K: copy + drop, V: store>(obj: &Object<T>, key: K, default: &V): &V获取对象中键对应的值的不可变引用。如果没有对应的键,则返回默认值。
borrow_mut_field<T: key, K: copy + drop, V: store>(obj: &mut Object<T>, key: K): &mut V获取对象中键对应的值的可变引用。如果没有对应的键,则会中止。
borrow_mut_field_with_default<T: key, K: copy + drop, V: store + drop>(obj: &mut Object<T>, key: K, default: V): &mut V获取对象中键对应的值的可变引用。如果没有对应的键,则插入键值对(key, default),然后返回对应的值的可变引用。
remove_field<T: key, K: copy + drop, V: store>(obj: &mut Object<T>, key: K): V从对象中移除键对应的字段,并返回字段的值。如果没有对应的键,则会中止。
contains_field<T: key, K: copy + drop>(obj: &Object<T>, key: K): bool如果对象中存在键对应的字段,则返回true,否则返回false
contains_field_with_type<T: key, K: copy + drop, V: store>(obj: &Object<T>, key: K): bool如果对象中存在键对应的字段,并且字段的值类型为V,则返回true,否则返回false
upsert_field<T: key, K: copy + drop, V: store + drop>(obj: &mut Object<T>, key: K, value: V)如果对象中存在键对应的字段,则更新字段的值。如果没有对应的键,则插入键值对(key, value)。
field_size<T: key>(obj: &Object<T>): u64返回对象中字段的数量,即键值对的数量。

Object 类型动态字段相关方法列表

方法说明
new_with_parent<P: key, T: key>(parent: &mut Object<P>, v: T): Object<T>向对象添加一个新的子对象字段,返回新添加的子对象。只有共享对象可以添加子对象字段。
new_with_parent_and_id<P: key, ID:drop, T: key>(parent: &mut Object<P>, id: ID, v: T): Object<T>使用自定义ID向对象添加一个新的子对象字段,返回新添加的子对象。只有共享对象可以添加子对象字段。

Rooch Object, Sui Object, Aptos Object 的比较

Sui Object

  • Sui Object 是一种特殊的 struct 要求该 struct 必须拥有 key ability, 同时第一个字段必须是 UID,Object 是虚拟机和存储提供的,Move 中并不存在 Object 类型。Rooch 中的 Object 是在 Move 中定义的类型。
  • Sui Object 由外部系统索引,合约内并没有提供通过 ID 获取 Object 的方法,只能通过参数传递。Rooch 同时提供两种方式。
  • Sui Object 如果发生嵌套或者保存到其他容器中,Object 在全局 Object Storage 就不可见。而 Rooch 中的 Object 嵌套或者保存到其他容器中,Object 依然在全局 Object Storage 中可访问。

Aptos Object

  • Aptos Object 底层是一种特殊的账户,该账户的 addressObjectID
  • Object<T> 代表对 Object 的引用,可以 copydrop,而 Rooch 中 Object<T> 只有一个实例,不可以 copydrop
  • Aptos Object 通过 DeleteRefExtendRefTransferRef 来表达对 Object 不同的操作权限,而 Rooch Object 通过只读引用,可变引用以及实例来区分不同的权限。
💡

TODO: This part of this document needs to be improved

参考链接

  1. Rooch Object API document (opens in a new tab)
  2. Rooch Object Source code (opens in a new tab)
  3. Sui Object (opens in a new tab)
  4. Aptos Object (opens in a new tab)
  5. Storage Abstraction
  6. Hot Potato (opens in a new tab)