Почему мы создали Sui Move

Move родился в 2018 году, на заре проекта Libra — два основателя Mysten (Эван и я) также входили в команду основателей Libra. Прежде чем мы приняли решение о создании нового языка, ранняя команда Libra интенсивно изучала существующие варианты использования и языки смарт-контрактов, чтобы понять, что разработчики хотели сделать и где существующие языки не работали. Ключевая проблема, которую мы определили, заключается в том, что смарт-контракты касаются активов и контроля доступа, а в ранних языках смарт-контрактов отсутствуют представления типа/значения для обоих. Гипотеза Move заключается в том, что если мы предоставим первоклассные абстракции для этих ключевых понятий, мы сможем значительно повысить как безопасность смарт-контрактов, так и производительность программистов смарт-контрактов — наличие правильного словаря для поставленной задачи меняет все. За прошедшие годы многие люди внесли свой вклад в разработку и реализацию Move, поскольку язык превратился из ключевой идеи в язык смарт-контрактов, не зависящий от платформы, со смелой целью стать «JavaScript для web3».

Сегодня мы рады объявить об важной вехе в нашей интеграции Move в Sui: Sui Move является полной функциональностью, поддерживается расширенными инструментами и содержит обширную документацию и примеры, в том числе:

Серия руководств по программированию с объектами Sui Move. Сборник основ Sui Move, шаблонов проектирования и образцов. Расширенный плагин VSCode с поддержкой понимания кода и диагностики ошибок, созданный командой Mysten Move!
Интеграция сборок, тестов, управления пакетами, создания документации и Move Prover с удобным интерфейсом командной строки.
Набор примеров, включая взаимозаменяемые токены, NFT, DeFi и игры.

Когда мы начали работать над Sui в конце 2021 года, мы по-новому взглянули на Move и подумали о том, какие ранние дизайнерские решения не устарели, а также о том, как можно улучшить Move, чтобы использовать уникальные функции Sui. Ранее мы писали о том, что нового в Sui Move на уровне языка, но мы не углублялись в мотивацию введения этих различий. В оставшейся части этого поста этот запрос подробно рассматривается на примерах.

Подождите, есть разные Move?

Move — это кроссплатформенный встроенный язык. Сам базовый язык очень прост: в нем есть общие понятия, такие как структуры, целые числа и адреса, но нет специфических для блокчейна понятий, таких как учетные записи, транзакции, время, криптография и т. д. Эти функции должны быть предоставлены блокчейном. платформа, которая интегрирует Move. Важно отметить, что этим блокчейнам не нужен собственный форк Move — каждая платформа использует одну и ту же виртуальную машину Move, средство проверки байт-кода, компилятор, средство проверки, диспетчер пакетов и интерфейс командной строки, но добавляет специфичные для блокчейна функции с помощью кода, построенного поверх этих основных компонентов. .Diem был первым блокчейном, в котором был реализован Move, и последующие цепочки на основе Move (включая 0L, StarCoin и Aptos) в основном использовали подход в стиле Diem.

Хотя Move в стиле Diem обладает некоторыми приятными качествами, как разрешенный характер Diem, так и некоторые детали реализации блокчейна Diem (в частности, модель хранения) затрудняют реализацию некоторых фундаментальных вариантов использования смарт-контрактов. В частности, оригинальные проекты Move и Diem предшествовали взрыву популярности NFT и имеют некоторые особенности, которые делают варианты использования, связанные с NFT, особенно сложными для реализации.

В этом посте мы рассмотрим три таких примера, демонстрирующих проблему с исходным встраиванием Move в стиле Diem, и покажем, как мы решили эту проблему в Sui Move. Мы предполагаем некоторое базовое понимание Move, но надеемся, что ключевые моменты будут понятны всем, кто имеет опыт программирования.

Бесконфликтное создание массовых активов

Возможность массового создания и распространения ресурсов крайне важна как для адаптации, так и для вовлечения пользователей Web3. Возможно, стример Twitch хочет распространять памятные NFT, создатель хочет разослать билеты на специальное мероприятие или разработчик игры хочет раздать новые предметы всем своим игрокам.

Вот (неудачная) попытка написать код для массовой чеканки актива в стиле Move Diem. Этот код принимает вектор адресов получателей в качестве входных данных, генерирует актив для каждого из них и пытается передать актив.

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 })
}

В Move в стиле Diem глобальное хранилище определяется парами (адрес, имя типа), то есть каждый адрес может хранить не более одного актива данного типа. Таким образом, строка move_to(recipient, CoolAsset { … } пытается передать CoolAsset, сохраняя его под адресом получателя.

Однако этот код не скомпилируется в строке move_to(recipient, …). Основная проблема заключается в том, что в Move в стиле Diem вы не можете отправить значение типа CoolAsset на адрес A, если только:

  1. Адрес, отличный от A, отправляет транзакцию для создания учетной записи в A
  2. Владелец A отправляет транзакцию, чтобы явно согласиться на получение объектов типа CoolAsset.

Это две транзакции только для того, чтобы получить актив! Решение действовать таким образом имело смысл для Diem, которая была разрешенной системой, которая должна была тщательно ограничивать создание учетных записей и не допускать, чтобы учетные записи содержали слишком много активов из-за ограничений в системе хранения. Но это чрезвычайно ограничительно для открытой системы, которая хочет использовать распределение активов в качестве механизма адаптации или просто позволить активам свободно перемещаться между пользователями, как это происходит в Ethereum и подобных блокчейнах [1].

Теперь давайте посмотрим на тот же код в Sui Move:

struct CoolAsset { id: VersionedID, creation_date: u64 } has keypublic 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 с идентификаторами объектов. Каждая структура с ключевой способностью является «объектом Sui», который должен иметь глобально уникальное поле идентификатора. Вместо использования ограниченной конструкции move_to Sui Move вводит примитив передачи, который можно использовать с любым объектом Sui. Под капотом этот примитив сопоставляет id с CoolAsset в глобальном хранилище и добавляет метаданные, чтобы указать, что значение принадлежит получателю.

Интересным свойством версии mass_mint для Sui является то, что она коммутирует со всеми другими транзакциями (включая те, которые вызывают mass_mint!). Среда выполнения Sui заметит это и отправит транзакции, вызывающие эту функцию, через византийский согласованный широковещательный «быстрый путь», который не требует консенсуса. Такие транзакции можно как совершать, так и выполнять параллельно! Это не требует никаких усилий со стороны программиста — он просто пишет приведенный выше код, а среда выполнения позаботится обо всем остальном.

Возможно, тонко, это не так для варианта Diem этого кода — даже если приведенный выше код работает, вызовы exists<Account> и guid::create создали бы точки раздора с другими транзакциями, генерирующими GUID или касающимися ресурса Account. В некоторых случаях можно переписать код Move в стиле Diem, чтобы избежать спорных моментов, но многие идиоматические способы написания Move в стиле Diem создают тонкие узкие места, которые мешают параллельному выполнению.

Собственное владение активами и их передача

Давайте расширим код Move в стиле Diem обходным решением, которое действительно будет компилироваться и запускаться. Идиоматический способ сделать это — «шаблон-оболочка»: поскольку Боб не может напрямую перейти к CoolAsset на адрес Алисы, мы просим Алису «согласиться» на получение CoolAsset, сначала опубликовав тип-оболочку CoolAssetStore с типом коллекции (таблица ) внутри. Алиса может сделать это, вызвав функцию opt_in. Затем мы добавляем код, который позволяет Бобу перемещать CoolAsset из его CoolAssetStore в CoolAssetStore Алисы.

В этом коде давайте добавим еще одну особенность: мы разрешаем передачу CoolAsset только в том случае, если с момента их создания прошло не менее 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)
}

Этот код работает. Но это довольно сложный способ выполнить простую задачу передачи актива от Алисы к Бобу! Опять же, давайте посмотрим на вариант 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 через транзакцию), но имеет параметр типа CoolAsset в качестве входных данных. Это магия Sui снова в действии! Транзакция включает в себя набор идентификаторов объектов, с которыми она хочет работать, и среду выполнения Sui:

  • Преобразовывает идентификаторы в значения объекта (удаляя необходимость в частяхзаимствования_global_mut и table_remove в приведенном выше коде в стиле Diem)
  • Проверяет, что объект принадлежит отправителю транзакции (удаление необходимости в части signer::address_of + связанный код выше). Эта часть особенно интересна, как мы вскоре объясним: в Sui проверка владения безопасным объектом является частью среды выполнения!
  • Проверяет типы значений объекта на соответствие типам параметров вызванной функции cool_transfer.
  • Привязывает значения объекта и другие аргументы к параметрам cool_transfer и вызывает функцию

Это позволяет программисту Sui Move пропустить стандартную часть логики «снятия средств» и сразу перейти к интересной части: проверке 30-дневной политики истечения срока действия. Точно так же часть «депозита» значительно упрощается с помощью конструкции передачи Sui Move, описанной выше. И, наконец, нет необходимости вводить тип-оболочку, такой как CoolAssetStore, с внутренней коллекцией — глобальное хранилище Sui с индексом id позволяет хранить по адресу произвольное количество значений с заданным типом.

Еще одно отличие, на которое следует обратить внимание, заключается в том, что есть 5 способов, которыми cool_transfer в стиле Diem может прерваться (т. е. выйти из строя и взимать плату с пользователя за газ, не завершив передачу), тогда как cool_transfer Sui Move может прерваться только одним способом: когда 30 Дневная политика нарушена.

Перенос проверки владения объектом в среду выполнения — это большой выигрыш не только с точки зрения эргономики, но и с точки зрения безопасности. Безопасная реализация этого на уровне времени выполнения предотвращает ошибки реализации этих проверок (или их полное забвение!) при построении.

Наконец, обратите внимание, как сигнатура функции точки входа Sui Move cool_transfer(asset: CoolAsset, …) дает нам много информации о том, что функция собирается делать (в отличие от сигнатуры функции в стиле Diem, которая более непрозрачна). ). Мы можем думать об этой функции как о запросе разрешения на передачу CoolAsset, тогда как другая функция f(asset: &mut CoolAsset, …) запрашивает разрешение на запись (но не передачу) CoolAsset, а g(asset: &CoolAsset, .. .) запрашивает только разрешения на чтение.

Поскольку эта информация доступна непосредственно в сигнатуре функции (не требуется выполнения или статического анализа!), она может использоваться непосредственно кошельком и другими клиентскими инструментами. В кошельке Sui мы работаем над удобочитаемыми запросами на подпись, которые используют эти подписи структурированных функций для предоставления пользователю запроса разрешений в стиле iOS/Android. Кошелек может сказать что-то вроде: «Эта транзакция запрашивает разрешение на чтение вашего CoolAsset, запись вашей AssetCollection и передачу вашего ConcertTicket. Продолжить?”.

Удобочитаемые запросы на подпись направлены на массовый вектор атак, присутствующий на многих существующих платформах (включая те, которые используют Move в стиле Diem!), где пользователи кошельков должны вслепую подписывать транзакции, не понимая, каковы могут быть их последствия. Мы считаем, что сделать работу с кошельком менее опасной — это ключевой шаг к массовому внедрению криптокошельков, и мы разработали Sui Move для поддержки этой цели, предоставив такие функции, как удобочитаемые запросы на подпись.

Объединение разнородных активов

Наконец, давайте рассмотрим пример, касающийся объединения активов разных типов. Это довольно распространенный вариант использования: программист может захотеть упаковать NFT разных типов в коллекцию, объединить элементы для продажи вместе на рынке или добавить аксессуары к существующему элементу. Конкретно, допустим, у нас есть следующий сценарий:

  • Алиса определила объект Character для использования в игре.
  • Алиса хочет поддерживать аксессуары своего персонажа с помощью сторонних аксессуаров разных типов, созданных позже.
  • Любой должен иметь возможность создать аксессуар, но решать, добавлять аксессуар или нет, должен владелец Персонажа.
  • При переносе персонажа должны автоматически передаваться все его аксессуары.

На этот раз давайте начнем с кода Sui Move. Мы воспользуемся еще одним аспектом встроенной функции владения объектами среды выполнения Sui: объект может принадлежать другому объекту. Каждый объект имеет уникального владельца, но родительский объект может иметь произвольное количество дочерних объектов. Отношения между родительскими и дочерними объектами создаются с помощью функции transfer_to_object, родственной функции передачи, представленной выше.

// 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)
}

В этом коде модуль Character включает функцию accessorize, которая позволяет владельцу персонажа добавлять вспомогательный объект произвольного типа в качестве дочернего объекта. Это позволяет Бобу и Клариссе создавать свои собственные типы аксессуаров с другими атрибутами и функциями, которые Алиса не ожидала, но опираться на то, что Алиса уже сделала. Например, рубашку Боба можно надеть только в том случае, если это любимый цвет персонажа, а меч Клариссы можно использовать только в том случае, если персонаж достаточно силен, чтобы владеть им.

В Move в стиле Diem такой сценарий реализовать невозможно. Вот несколько неудачных попыток реализации стратегий:

// 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 в стиле Diem:

  • Поддерживаются только однородные коллекции (как показывает первая попытка), но аксессуары принципиально разнородны.
  • Ассоциации между объектами могут быть созданы только посредством «обертывания» (т. е. хранения объекта внутри другого объекта); но набор объектов, которые можно обернуть, должен быть определен заранее (как во второй попытке) или добавлен специальным образом, который не поддерживает вспомогательную композицию (как в третьей попытке).

Вывод:

Sui — первая платформа, которая существенно отличается от оригинального дизайна Diem в том, как она использует Move. Разработка встраивания, в полной мере использующего Move и уникальные функции платформы, — это и искусство, и наука, требующая глубокого понимания как языка Move, так и базовых возможностей блокчейна. Мы очень рады успехам, достигнутым Sui Move, и новым вариантам его использования!

[1] Еще один аргумент в пользу политики Move в стиле Diem, согласно которой «необходимо подписаться на получение ресурса определенного типа», заключается в том, что это хороший механизм предотвращения спама. Однако мы считаем, что предотвращение спама относится к прикладному уровню. Вместо того, чтобы просить пользователей отправлять транзакции, которые стоят реальных денег, чтобы согласиться на получение актива, спам легко устраняется (например) на уровне кошелька с богатыми пользовательскими политиками и помощью автоматических спам-фильтров.

Created by wonder.jpiggy#7100

--

--