Skip to main content

共享与所有权对象

在 Sui 上,对象可以是共享的(可被任何交易读写)或所有权的(仅由其所有者签署的交易进行读写)。许多应用程序可以使用只使用共享对象或仅拥有对象的解决方案进行构建,每种解决方案都有权衡需要权衡的利弊。

只使用所有权对象的交易从最终性的角度获益良好,因为它们不需要经过共识。另一方面,仅对象所有者能够访问该对象的事实使得需要处理由多方拥有的对象的流程变得复杂,并且需要协调链外访问非常热门的对象。

访问一个或多个共享对象的交易需要共识以对这些对象进行读取和写入排序。这会导致稍高的 gas 成本,并增加延迟;访问更多共享对象或特别受欢迎的对象的交易可能会由于争用而增加延迟。但允许多个地址访问同一个共享对象的表达力,以及允许链协调对热门对象的访问的简单性是其优势。

总的来说,对延迟或 gas 成本非常敏感、不需要处理复杂的多方交易、或已经需要链外服务的应用程序,可以受益于仅使用所有权对象的设计;而需要在多方之间协调的应用程序可能更喜欢使用共享对象。

有关 Sui 支持的对象类型的更多信息,请参阅对象所有权

示例:托管

托管示例通过在两种样式中实现相同的应用程序,演示了共享对象和所有权对象之间的权衡。它实现了一个服务,允许两个地址在不信任的情况下相互交换对象(“交易”),并由服务将它们的对象托管。

Locked<T>Key

代码示例

两种实现都使用了用于锁定值的原语,提供了以下接口:

module escrow::lock {
public fun lock<T: store>(obj: T, ctx: &mut TxContext): (Locked<T>, Key);
public fun unlock<T: store>(locked: Locked<T>, key: Key): T
}

任何 T: store 都可以被锁定,以获取一个 Locked<T> 和相应的 Key,反之亦然,可以消耗锁定的值及其对应的键来获取包装的对象。

该接口提供的重要属性是,锁定的值在未解锁的情况下无法修改(并在稍后重新锁定它们)。因为解锁会消耗键,通过记住它们被锁定时的键的 ID,可以检测到对锁定的值的篡改。这可以防止在交换中的一方更改其提供的对象以减小其值的情况。

所有权对象

代码示例

使用所有权对象实现的通过托管进行交换的协议始于双方都锁定其各自的对象。这将用于证明在达成交换协议后对象未经篡改。如果任一方在此阶段不希望继续,它们只需解锁其对象。

假设双方都愿意继续,下一步需要第三方担任托管人的角色。托管人持有等待它们的对应物品到达的物品,当它们到达时,将它们匹配以完成交换。

public fun create<T: key + store>(
key: Key,
locked: Locked<T>,
exchange_key: ID,
recipient: address,
custodian: address,
ctx: &mut TxContext,
) {
let escrow = Escrow {
id: object::new(ctx),
sender: tx_context::sender(ctx),
recipient,
exchange_key,
escrowed_key: object::id(&key),
escrowed: lock::unlock(locked, key),
};

transfer::transfer(escrow, custodian);
}

create 函数准备了 Escrow 请求并将其发送给 custodian。由此方提供的对象被锁定并随其键传递,被请求的对象由其被锁定的键的 ID 识别。在准备请求时,提供的对象被解锁,同时记住其键的 ID。

尽管托管方被信任保持活动性(如果它拥有交换的两侧,则完成交换,如果需要则返回对象),但在 Move 中仍然保持所有其他正确性属性:尽管托管方拥有被交换的两个对象,但它们被允许执行的唯一有效操作是将它们与其正确的对应物品匹配以完成交换,或者将它们返回:

public fun swap<T: key + store, U: key + store>(
obj1: Escrow<T>,
obj2: Escrow<U>,
) {
let Escrow {
id: id1,
sender: sender1,
recipient: recipient1,
exchange_key: exchange_key1,
escrowed_key: escrowed_key1,
escrowed: escrowed1,
} = obj1;

let Escrow {
id: id2,
sender: sender2,
recipient: recipient2,
exchange_key: exchange_key2,
escrowed_key: escrowed_key2,
escrowed: escrowed2,
} = obj2;

object::delete(id1);
object::delete(id2);

// Make sure the sender and recipient match each other
assert!(sender1 == recipient2, EMismatchedSenderRecipient);
assert!(sender2 == recipient1, EMismatchedSenderRecipient);

// Make sure the objects match each other and haven't been modified
// (they remain locked).
assert!(escrowed_key1 == exchange_key2, EMismatchedExchangeObject);
assert!(escrowed_key2 == exchange_key1, EMismatchedExchangeObject);

// Do the actual swap
transfer::public_transfer(escrowed1, recipient1);
transfer::public_transfer(escrowed2, recipient2);
}

swap 函数检查发送方和接收方是否匹配,以及每一方是否想要对方提供的对象,通过比较它们各自的键 ID。如果托管方尝试将两个不相关的托管请求匹配在一起进行交换,交易将不成功。

共享对象

代码示例

在共享对象情况下,协议不太对称,但仍以第一方锁定希望交换的对象开始。然后,第二方可以查看被锁定的对象,如果他们决定要与之交换,他们通过创建一个交换请求来表示他们的兴趣:

public fun create<T: key + store>(
escrowed: T,
exchange_key: ID,
recipient: address,
ctx: &mut TxContext
) {
let escrow = Escrow {
id: object::new(ctx),
sender: tx_context::sender(ctx),
recipient,
exchange_key,
escrowed: option::some(escrowed),
};

transfer::public_share_object(escrow);
}

这次,交换请求是一个共享对象,并直接接受正在托管的对象(未锁定)。该请求记住了发送它的地址(如果交换尚未发生,则允许其取回对象),以及预期的接收方,然后预期接收方将通过提供最初锁定的对象来继续交换:

public fun swap<T: key + store, U: key + store>(
escrow: &mut Escrow<T>,
key: Key,
locked: Locked<U>,
ctx: &TxContext,
): T {
assert!(option::is_some(&escrow.escrowed), EAlreadyExchangedOrReturned);
assert!(escrow.recipient == tx_context::sender(ctx), EMismatchedSenderRecipient);
assert!(escrow.exchange_key == object::id(&key), EMismatchedExchangeObject);

let escrowed1 = option::extract<T>(&mut escrow.escrowed);
let escrowed2 = lock::unlock(locked, key);

// Do the actual swap
transfer::public_transfer(escrowed2, escrow.sender);
escrowed1
}

尽管 Escrow 请求是一个可被任何人访问的共享对象,但 Move 接口确保只有最初的发送方和预期的接收方可以成功地与其交互。swap 检查锁定的对象是否与创建 Escrow 时请求的对象匹配(再次通过比较键的 ID),并假设预期的接收方想要托管的对象(如果不是,他们就不会调用 swap)。

假设所有检查都通过,Escrow 中持有的对象被提取并返回给第一方,使共享请求对象为空(但仍然存在)。由第一方提供的锁定对象也被解锁并发送给第二方,完成交换。

比较

此主题探讨了两种在两个对象之间实现交换的方式。在两种情况下,有一个阶段其中一方发出了请求,而另一方尚未回应。在这一点上,双方都可能想访问 Escrow 对象:一方取消交换,另一方完成交换。

在一种情况下,该协议仅使用所有权对象,但需要一个托管人充当中介。这避免了共识的所有成本和延迟,但涉及更多步骤,并需要信任第三方保持活动性。

在另一种情况下,对象在链上由共享对象托管。这需要共识,并在交换结束时在链上留下一个共享对象(以不可检索的存储折扣的形式消耗 gas),但涉及的步骤更少,而且没有第三方。