我们为何创建Sui Move

Sui 是第一个在集成Move 的原始设计基础上进行改进的区块链,我们将分享这些改进的具体实例。

我们为何创建Sui Move

请注意,"Sui Move "一词已经过时。取而代之的是 "Move onSui"或简称 "Move"。

Move 诞生于 2018 年 Libra 项目早期--Mysten 的两位创始人(埃文和我)也是 Libra 的创始团队成员。在我们决定创建一种新语言之前,早期的 Libra 团队深入研究了现有的智能合约用例和语言,以了解开发人员想要做什么,以及现有语言在哪些方面无法实现。我们发现的关键问题是,智能合约的核心是资产和访问控制,但早期的智能合约语言缺乏这两方面的类型/值表示。Move 的假设是,如果我们为这些关键概念提供一流的抽象,就能显著提高智能合约的安全性和智能合约程序员的工作效率--为手头的任务提供正确的词汇会改变一切。多年来,许多人都为Move 的设计和实现做出了贡献,该语言也从一个关键的想法发展成为一种平台无关的智能合约语言,其大胆的目标是成为 "web3 的 JavaScript"。

今天,我们很高兴地宣布,在将Move 整合到Sui 的过程中,我们取得了里程碑式的进展:Sui Move 功能完备,有先进的工具支持,并有大量的文档和示例,包括:

  • 使用Sui Move 对象编程的系列教程
  • Sui Move 基础知识、设计模式和示例食谱
  • 增强型 VSCode 插件,支持代码理解和错误诊断,由 MystenMove 团队开发!
  • 将Move 的构建、测试、软件包管理、文档生成和Move Prover 集成在一起。 sui CLI
  • 包括可替代代币、NFT、DeFi 和游戏在内的一系列例子

当我们在 2021 年底开始开发Sui 时,我们重新审视了Move ,反思了哪些早期的设计决策已经过时,以及如何改进Move 以充分利用Sui的独特功能。我们以前曾撰文介绍过Sui Move 在语言层面的新内容,但还没有深入探讨引入这些差异的动机。本文章的其余部分将以示例驱动的方式详细介绍这一问题。

等等,还有不同的Move's?

Move 是一种跨平台的嵌入式语言。核心语言本身非常简单:它有通用概念,如结构体、整数和地址,但没有特定于区块链的概念,如账户、交易、时间、加密等。这些功能必须由集成Move 的区块链平台提供。重要的是,这些区块链不需要自己的Move的分叉--每个平台都使用相同的Move 虚拟机、字节码验证器、编译器、求证器、包管理器和 CLI,但通过构建在这些核心组件之上的代码来添加特定于区块链的功能。

Diem 是第一个嵌入Move 的区块链,随后基于Move 的区块链(包括 0L、StarCoin 和 Aptos)在很大程度上都采用了 Diem 风格的方法。虽然Diem式Move ,但Diem的许可性质和Diem区块链的某些实现细节(尤其是存储模型)使得一些基本的智能合约用例难以实现。特别是,Move 和 Diem 的原始设计早于 NFT 的流行,而且有一些怪癖,使得 NFT 相关用例的实现特别棘手。

在这篇文章中,我们将通过三个这样的示例来展示最初的 Diem 式Move 嵌入的一个问题,并描述我们如何在Sui Move 中解决这个问题。我们假设您对Move 有一些基本的了解,但希望有编程背景的人都能理解其中的要点。

无障碍大规模资产创建

批量创建和分发资产的能力对于入职和吸引 Web3 用户都至关重要。也许 Twitch 流媒体想要分发纪念版 NFT,也许创作者想要发送特别活动的门票,也许游戏开发者想要向所有玩家空投新物品。

下面是一个(失败的)尝试,用 Diem 风格Move 编写大规模铸造资产的代码。该代码将收件人地址向量作为输入,为每个地址生成资产,并尝试转移资产。

struct CoolAsset { id: GUID, creation_date: u64 } has key, store
public entry fun mass_mint(creator: &signer, recipients: vector<address>) {
  assert!(signer::address_of(creator) == CREATOR, EAuthFail);
  let i = 0;
  while (!vector::is_empty(recipients)) {
    let recipient = vector::pop_back(&mut recipients);
    assert!(exists<Account>(recipient), ENoAccountAtAddress);
    let id = guid::create(creator);
    let creation_date = timestamp::today();
    // error! recipient must be `&signer`, not `address`
    move_to(recipient, CoolAsset { id, creation_date })
}

在 Diem 风格的Move 中,全局存储以(地址、类型名称)对为键,即每个地址最多只能存储一个给定类型的资产。因此,行 move_to(recipient, CoolAsset { ...} 正试图转移 酷资产 存储在 受援国 地址

但是,该代码在以下行将无法编译 move_to(recipient, ...).关键问题在于,在 Diem 风格的Move 中,不能发送类型为 酷资产 地址 A 除非:

  1. A地址发送交易,在A处创建账户
  2. 的所有者 A 发送一个事务,以明确选择接收类型为 酷资产

光是接收资产就需要进行两次交易!对 Diem 来说,这样做是合理的,因为 Diem 是一个有权限的系统,需要严格限制账户的创建,并防止账户因存储系统的限制而持有过多资产。但是,对于一个希望将资产分配作为入驻机制的开放系统来说,或者说对于像以太坊和类似区块链那样允许资产在用户之间自由流动的开放系统来说,这种限制是非常大的[1]。

现在,让我们看看Sui Move 中的相同代码:

struct CoolAsset { id: VersionedID, creation_date: u64 } has key
public entry fun mass_mint(recipients: vector<address>, ctx: &mut TxContext) {
  assert!(tx_context::sender(ctx) == CREATOR, EAuthFail);
  let i = 0;
  while (!vector::is_empty(recipients)) {
    let recipient = vector::pop_back(&mut recipients);
    let id = tx_context::new_id(ctx);
    let creation_date = tx_context::epoch(); // Sui epochs are 24 hours
    transfer(CoolAsset { id, creation_date }, recipient)
  }
}

Sui Move的全局存储空间,以对象 ID 为关键字。每个带有 密钥 能力是一个 "Sui 对象",必须有一个全局唯一的 id 领域。而不是使用受限的 移动到 Sui Move 引入了一个 调动 基元,可用于任何Sui 对象。在引擎盖下,该基元映射 id酷资产 在全局存储中,并添加元数据以表明该值为 受援国.

Sui 版本的一个有趣特性是 大众铸币厂 因为它与所有其他事务(包括调用 大众铸币厂!).Sui 运行时会注意到这一点,并通过拜占庭一致广播 "快速通道 "发送调用此函数的事务,而无需达成共识。这些事务既可以提交,也可以并行执行!这不需要程序员的任何努力--他们只需编写上面的代码,运行时就会处理剩下的事情。

也许巧妙的是,该代码的琰变体并非如此--即使上面的代码正常工作,也会同时出现 exists<Account>guid::create 调用会与其他交易产生争执。 GUID或触摸 账户 资源。在某些情况下,可以重写 Diem 风格的Move 代码来避免争议点,但许多 Diem 风格的Move 的惯用写法会带来一些微妙的瓶颈,从而阻碍并行执行。

土著资产所有权和转让

让我们用一种能实际编译和运行的变通方法来扩展 Diem 风格的Move 代码。惯用的方法是 "封装模式":因为 Bob 不能直接 移动到 a 酷资产 发送到爱丽丝的地址,我们会要求爱丽丝 "选择接收 酷资产首先发布一个封装类型 酷资产商店 的集合类型 (表格) 内。爱丽丝可以通过调用 选项_输入 功能。然后,我们添加代码,让 Bob 移动一个 酷资产 从他的 酷资产商店 进入爱丽丝的 酷资产商店.

在这段代码中,让我们增加一个新的变化:我们将只允许 酷资产在创建后至少 30 天内转让。这种政策对于(例如)希望阻止投机者购买/炒作活动门票的创作者来说非常重要,这样真正的粉丝就更容易以合理的价格获得门票。

struct CoolAssetStore has key {
  assets: Table<TokenId, CoolAsset>
}
public fun opt_in(addr: &signer) {
  move_to(addr, CoolAssetHolder { assets: table::new() }
}
public entry fun cool_transfer(
  addr: &signer, recipient: address, id: TokenId
) acquires CoolAssetStore {
  // withdraw
  let sender = signer::address_of(addr);
  assert!(exists<CoolAssetStore>(sender), ETokenStoreNotPublished);
  let sender_assets = &mut borrow_global_mut<CoolAssetStore>(sender).assets; 
  assert!(table::contains(sender_assets, id), ETokenNotFound);
	let asset = table::remove(&sender_assets, id);
  // check that 30 days have elapsed
  assert!(time::today() > asset.creation_date + 30, ECantTransferYet)
	// deposit
	assert!(exists<CoolAssetStore>(recipient), ETokenStoreNotPublished);
  let recipient_assets = &mut borrow_global_mut<CoolAssetStore>(recipient).assets; 
  assert!(table::contains(recipient_assets, id), ETokenIdAlreadyUsed);
  table::add(recipient_assets, asset)
}

这段代码可以工作。但要实现将资产从 Alice 转移到 Bob 这个简单的目标,这个方法就相当复杂了!我们再来看看Sui Move 变体:

public entry fun cool_transfer(
  asset: CoolAsset, recipient: address, ctx: &mut TxContext
) {
  assert!(tx_context::epoch(ctx) > asset.creation_date + 30, ECantTransferYet);
  transfer(asset, recipient)
}

这段代码要短得多。这里需要注意的关键是 cool_transfer 是一个 条目 函数(即Sui 运行时可通过事务直接调用该函数),但它的参数类型为 酷资产 作为输入。这又是Sui 运行时的魔法在起作用!一个事务包括它要操作的一组对象 ID 和Sui 运行时:

  • 将 ID 转换为对象值(无需使用 借用全球互变删除表格 上述 Diem 式代码中的部分)
  • 检查对象是否为事务发送者所有(无需使用 签名者::address_of 部分 + 相关代码)。这一部分特别有趣,我们很快就会解释: Sui 中,安全的对象所有权检查是运行时的一部分!
  • 根据调用函数的参数类型检查对象值的类型 cool_transfer
  • 将对象值和其他参数绑定到 cool_transfer 并调用函数

这样,Sui Move 程序员就可以跳过 "提款 "逻辑部分的模板,直接跳到有趣的部分:检查 30 天到期政策。同样,"存款 "部分也通过Sui Move 调动 结构。最后,无需引入像 酷资产商店 Sui 全局存储允许一个地址存储任意数量的给定类型的值。

需要指出的另一个不同之处是,"琰式 "有 5 种方式: 1. cool_transfer 可以中止(即传输失败并在未完成传输的情况下向用户收取气体费用),而Sui Move cool_transfer 只能以一种方式终止:当违反 30 天政策时。

将对象所有权检查卸载到运行时不仅在人机工程学方面,而且在安全性方面都是一个重大胜利。在运行时级别安全地实现这些功能,可以避免在构造过程中实现这些检查(或完全忘记这些检查!)的错误。

最后,请注意Sui Move 入口点函数签名 cool_transfer( asset: CoolAsset, ...) 为我们提供了很多关于该函数要做什么的信息(相比之下,Diem 风格的函数签名则更不透明)。我们可以把这个函数看作是请求允许传输 酷资产而不同的函数 f(asset: &mut CoolAsset, ...) 要求允许写入(但不允许传输) 酷资产g(asset: &CoolAsset, ...) 只要求读取权限。

由于这些信息可直接在函数签名中获得(无需执行或静态分析!),因此钱包和其他客户端工具可直接使用这些信息。在Sui 钱包中,我们正在研究 人类可读的签署请求 利用这些结构化函数签名,向用户提供 iOS/Android 风格的权限提示。钱包可以这样说:"此交易请求允许读取您的 酷资产写下你的 资产收集并将您的 音乐会门票.继续吗?"。

人工可读签名请求解决了许多现有平台(包括使用 Diem 式Move! 的平台)上存在的大量攻击向量,在这些平台上,钱包用户必须在不了解其可能产生的影响的情况下盲目签署交易。我们认为,降低钱包体验的危险性是促进加密钱包成为主流的关键一步,因此我们设计了Sui Move ,通过启用人类可读签名请求等功能来支持这一目标。

捆绑异质资产

最后,我们来看一个关于捆绑不同类型资产的例子。这是一个相当常见的用例:程序员可能希望将不同类型的 NFT 打包成一个集合,将物品捆绑在一起在市场上出售,或者为现有物品添加附件。具体来说,假设我们有以下情况:

  • 爱丽丝定义了一个 人物 游戏中使用的对象
  • 爱丽丝希望支持使用稍后创建的不同类型的第三方配件来装饰她的角色
  • 任何人都应该能够创建附件,但拥有 人物 应决定是否添加附件。
  • 转让 人物 应自动转移其所有附件。

这一次,让我们从Sui Move 代码开始。我们将利用Sui 运行时内置的对象所有权功能的另一个方面: 一个对象可以被另一个对象拥有.每个对象都有一个唯一的所有者,但一个父对象可以拥有任意数量的子对象。父/子对象关系是通过使用 传输到对象 函数的同级 调动 函数。

// in the Character module, created by Alice
struct Character has key {
  id: VersionedID,
  favorite_color: u8,
  strength: u64,
  ...
}
/// The owner of `c` can choose to add `accessory`
public entry fun accessorize<T: key>(c: &mut Character, accessory: T) {
  transfer_to_object(c, accessory)
}
// ... in a module added later by Bob
struct SpecialShirt has key {
  id: VersionedID,
  color: u8
}
public entry fun dress(c: &mut Character, s: Shirt) {
  // a special shirt has to be the character's favorite color
  assert!(character::favorite_color(c) == s.color, EBadColor);
  character::accessorize(c, shirt)
}
// ... in a  module added later by Clarissa
struct Sword has key {
  id: VersionedID,
  power: u64
}
public entry fun equip(c: &mut Character, s: Sword) {
  // a character must be very strong to use a powerful sword
  assert!(character::strength(c) > sword.power * 2, ENotStrongEnough);
  character::accessorize(c, s)
}

在该代码中, 人物 模块包括一个 点缀 函数,让角色的所有者可以添加一个任意类型的配件对象作为子对象。这样,鲍勃和克拉丽莎就可以创建自己的配件类型,它们具有不同的属性和功能,这些都是爱丽丝没有预料到的,但却建立在爱丽丝已经完成的基础上。例如,鲍勃的衬衫只有在角色最喜欢的颜色时才能装备,而克拉丽莎的剑只有在角色足够强壮时才能使用。

在琰式Move 中,不可能实现这种设想。以下是几种实施策略的失败尝试:

// attempt 1
struct Character {
  // won't work because every Accessory would need to be the same type + have
  // the same fields. There is no subtyping in Move.
  // Bob's shirt needs a color, and Clarissa's sword needs power--no standard
  // representation of Accessory can anticipate everything devs will want to
  // create
  accessories: vector<Accessory>
}
// attempt 2
struct Character {
  // perhaps Alice anticipates the need for a Sword and a Shirt up front...
  sword: Option<Sword>,
  shirt: Option<Shirt>
  // ...but what happens when Daniel comes along later and wants to add Pants?
}
// attempt 3
// Does not support accessory compositions. For example: how do we represent a 
// Character with Pants and a Shirt, but no Sword?
struct Shirt { c: Character }
struct Sword { s: Shirt }
struct Pants { s: Sword }

关键问题在于,在琰式Move 中:

  • 只支持同质集合(如第一次尝试所示),但配件从根本上说是异质的
  • 对象之间的关联只能通过 "封装"(即把一个对象存储在另一个对象内部)来创建;但可以封装的对象集必须事先定义(如第二次尝试),或者以不支持附件组合的临时方式添加(如第三次尝试)。

结束语

Sui 是第一个在如何使用Move 方面与最初的 Diem 设计大相径庭的平台。设计能充分利用Move 和平台独特功能的嵌入式设计既是一门艺术,也是一门科学,需要对Move 语言和底层区块链功能有深入的了解。我们对Sui Move 所取得的进步以及它将带来的新用例感到非常兴奋!

[1] 支持迪琰式Move 政策("必须选择接收给定类型的资产")的另一个论点是,这是一种很好的垃圾邮件防范机制。然而,我们认为垃圾邮件的防范属于应用层。垃圾邮件不需要用户花费真金白银进行交易来选择接收资产,而是可以通过丰富的用户自定义策略和自动垃圾邮件过滤器在(例如)钱包层轻松解决。