Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

共享对象

共享对象(Shared Object)是 Sui 中唯一一种允许任何人以可变方式访问的所有权类型。与地址所有对象的独占控制不同,共享对象没有特定的所有者——任何地址都可以在交易中读取或修改它。这使得共享对象成为构建去中心化市场、流动性池、投票系统等多方交互应用的核心构建块。

本章将深入探讨共享对象的创建、使用、性能影响,以及设计共享对象时需要注意的陷阱。

创建共享对象

与不可变对象类似,共享对象也有两种创建方式。

transfer::share_object

在定义对象类型的模块内部使用,对象只需具有 key 能力:

module examples::share_demo;

public struct Registry has key {
    id: UID,
    entries: vector<vector<u8>>,
}

public fun create(ctx: &mut TxContext) {
    let registry = Registry {
        id: object::new(ctx),
        entries: vector::empty(),
    };
    transfer::share_object(registry);
}

transfer::public_share_object

可以在任何模块中调用,但要求对象同时具有 keystore 能力:

module examples::public_share_demo;

public struct Pool has key, store {
    id: UID,
    balance: u64,
}

public fun create_pool(ctx: &mut TxContext): Pool {
    Pool {
        id: object::new(ctx),
        balance: 0,
    }
}

public fun share_pool(pool: Pool) {
    transfer::public_share_object(pool);
}

共享对象的核心特性

任何人可访问

共享对象可以被任何地址在交易中引用。在交易中,你可以通过以下方式使用共享对象:

  • &T(不可变引用):读取数据
  • &mut T(可变引用):读取和修改数据
  • T(按值):只在销毁对象时使用

共识排序

由于多个交易可能同时尝试修改同一个共享对象,Sui 需要通过共识机制对这些交易进行排序。这意味着涉及共享对象的交易延迟高于地址所有对象的交易。

不可逆转

一旦对象被共享,就永远不能

  • 转移给某个地址(变为地址所有)
  • 冻结(变为不可变)
  • 只能通过销毁来“移除“

完整示例:共享计数器

以下是一个经典的共享计数器示例:

module examples::shared_counter;

const ENotCreator: u64 = 0;

public struct Counter has key {
    id: UID,
    value: u64,
    owner: address,
}

public fun create_and_share(ctx: &mut TxContext) {
    let counter = Counter {
        id: object::new(ctx),
        value: 0,
        owner: ctx.sender(),
    };
    transfer::share_object(counter);
}

/// Anyone can increment the counter
public fun increment(counter: &mut Counter) {
    counter.value = counter.value + 1;
}

/// Anyone can read the value
public fun value(counter: &Counter): u64 {
    counter.value
}

/// Only the creator can destroy the shared counter
public fun destroy(counter: Counter, ctx: &TxContext) {
    assert!(counter.owner == ctx.sender(), ENotCreator);
    let Counter { id, value: _, owner: _ } = counter;
    id.delete();
}

示例解析

  1. 创建即共享create_and_share 在同一个函数中创建并共享计数器。注意我们保存了创建者的地址(owner),以便后续进行权限检查。
  2. 任何人可递增increment 接受 &mut Counter,任何地址都可以调用它来增加计数值。
  3. 任何人可读取value 接受 &Counter,纯读取操作。
  4. 权限控制销毁:虽然共享对象可以被任何人访问,但我们在 destroy 中通过 assert! 检查只有创建者可以销毁它。

共享对象的删除

共享对象是可以被删除的,这是一个常见的误解需要澄清。删除共享对象需要:

  1. 按值(T 方式接收共享对象
  2. 解构对象,删除其 UID
module examples::shared_deletion;

const ENotCreator: u64 = 0;

public struct SharedBox has key {
    id: UID,
    content: vector<u8>,
    creator: address,
}

public fun create(content: vector<u8>, ctx: &mut TxContext) {
    let box_obj = SharedBox {
        id: object::new(ctx),
        content,
        creator: ctx.sender(),
    };
    transfer::share_object(box_obj);
}

/// 销毁共享对象 - 注意参数类型是 SharedBox(按值),不是 &mut SharedBox
public fun destroy(box_obj: SharedBox, ctx: &TxContext) {
    assert!(box_obj.creator == ctx.sender(), ENotCreator);
    let SharedBox { id, content: _, creator: _ } = box_obj;
    id.delete();
}

当在交易中使用共享对象并按值传递时,Sui 会检查该对象确实是共享的,并通过共识路径执行交易。

性能影响与优化策略

共识带来的延迟

涉及共享对象的交易需要经过 Sui 的共识协议进行排序。这个过程虽然在 Sui 中已经高度优化,但仍然比快速路径慢。具体差异:

  • 快速路径(地址所有/不可变对象):通常在几百毫秒内完成
  • 共识路径(共享对象):通常需要 2-3 秒

热点问题

如果一个共享对象被大量交易同时访问和修改,它会成为热点(hotspot),限制系统吞吐量。常见热点场景:

  • 全局计数器
  • 单一的流动性池
  • 集中式的订单簿

优化策略

策略一:最小化共享对象的使用

尽可能将数据存储在地址所有对象中,只在必要时使用共享对象:

module examples::minimize_shared;

/// 不好的设计:所有用户数据存在一个共享对象中
public struct BadUserStore has key {
    id: UID,
    users: vector<address>,
    balances: vector<u64>,
}

/// 好的设计:每个用户有自己的对象(地址所有)
public struct UserAccount has key {
    id: UID,
    balance: u64,
}

/// 只在需要多方交互时使用共享对象
public struct Marketplace has key {
    id: UID,
    listings: vector<Listing>,
}

public struct Listing has store {
    seller: address,
    price: u64,
    item_id: address,
}

策略二:分片

将一个大的共享对象拆分为多个:

module examples::sharding;

/// 不好的设计:单一全局计数器
public struct GlobalCounter has key {
    id: UID,
    count: u64,
}

/// 好的设计:分区计数器
public struct ShardedCounter has key {
    id: UID,
    shard_id: u8,
    count: u64,
}

/// 创建多个分片
public fun create_shards(ctx: &mut TxContext) {
    let mut i: u8 = 0;
    while (i < 10) {
        let shard = ShardedCounter {
            id: object::new(ctx),
            shard_id: i,
            count: 0,
        };
        transfer::share_object(shard);
        i = i + 1;
    };
}

/// 用户根据某种规则选择一个分片来递增
public fun increment_shard(shard: &mut ShardedCounter) {
    shard.count = shard.count + 1;
}

策略三:读写分离

对于读多写少的场景,考虑使用不可变对象存储只读数据,共享对象只负责写操作:

module examples::read_write_split;

use std::string::String;

/// 不可变对象:存储产品目录(只读)
public struct ProductCatalog has key {
    id: UID,
    products: vector<String>,
}

/// 共享对象:存储订单(需要读写)
public struct OrderBook has key {
    id: UID,
    orders: vector<Order>,
}

public struct Order has store, drop {
    buyer: address,
    product_index: u64,
    quantity: u64,
}

共享对象的安全考虑

权限控制

共享对象可以被任何人访问,因此必须在函数逻辑中实现权限控制

module examples::shared_security;

const ENotAdmin: u64 = 0;
const EInsufficientBalance: u64 = 1;

public struct Treasury has key {
    id: UID,
    balance: u64,
    admin: address,
}

public fun create(ctx: &mut TxContext) {
    let treasury = Treasury {
        id: object::new(ctx),
        balance: 0,
        admin: ctx.sender(),
    };
    transfer::share_object(treasury);
}

/// 任何人可以存款
public fun deposit(treasury: &mut Treasury, amount: u64) {
    treasury.balance = treasury.balance + amount;
}

/// 只有管理员可以取款
public fun withdraw(
    treasury: &mut Treasury,
    amount: u64,
    ctx: &TxContext,
): u64 {
    assert!(treasury.admin == ctx.sender(), ENotAdmin);
    assert!(treasury.balance >= amount, EInsufficientBalance);
    treasury.balance = treasury.balance - amount;
    amount
}

重入安全

与以太坊不同,Sui 的交易模型天然防止重入攻击。每个交易是原子性的,在一个交易内对共享对象的修改不会被其他交易“中途“观察到。

前置交易(Front-running)

由于共享对象的交易需要共识排序,理论上存在前置交易(front-running)的风险——矿工/验证者可以在看到你的交易后抢先提交自己的交易。在设计金融协议时需要考虑这一点。

共享对象 vs 其他所有权类型

特性Address-ownedSharedImmutable
访问权限仅所有者任何人任何人(只读)
修改所有者可修改任何人可修改不可修改
删除所有者可删除可删除(需权限检查)不可删除
转移可转移不可转移不可转移
执行路径快速路径共识路径快速路径
并发性低(独占)需要排序高(无限并行)

小结

共享对象是 Sui 中实现多方交互的关键机制,但也带来了性能和安全方面的挑战:

  • 创建方式:通过 share_object(模块内部)或 public_share_object(需要 store 能力)。
  • 任何人可访问:共享对象可以被任何地址在交易中使用,支持读写操作。
  • 共识排序:涉及共享对象的交易需要经过共识,延迟高于快速路径。
  • 不可逆转:共享状态是不可逆的,但共享对象可以被销毁。
  • 权限控制:必须在合约逻辑中自行实现,因为任何人都能调用函数。
  • 性能优化:最小化共享对象的使用、分片、读写分离是常见的优化策略。

在设计 Sui 应用时,应该审慎使用共享对象——只在确实需要多方交互时才使用,其余数据尽量存储在地址所有或不可变对象中。