Почему мы создали Sui Move
Основные моменты, рассмотренные в этой статье:
- Sui Move полностью укомплектован и готов к написанию безопасных и эффективных смарт-контрактов.
- Sui — это первый блокчейн, который улучшил первоначальный дизайн Diem в том, как он интегрирует Move, и мы делимся конкретными примерами этих улучшений.
Введение
Move родился в 2018 году, на заре проекта Libra — два основателя Mysten (Эван и я) также входили в команду основателей Libra. Прежде чем мы приняли решение о создании нового языка, ранняя команда Libra интенсивно изучала существующие варианты использования смарт-контрактов и языки, чтобы понять, что разработчики хотели сделать и где существующие языки не работали. Ключевая проблема, которую мы определили, заключается в том, что смарт-контракты связаны с активами и контролем доступа, а в ранних языках смарт-контрактов отсутствуют представления типа/значения для обоих. Гипотеза Move состоит в том, что если мы предоставим первоклассные абстракции для этих ключевых понятий, мы сможем значительно повысить как безопасность смарт-контрактов, так и производительность программистов смарт-контрактов — наличие правильного словаря для поставленной задачи меняет все. С годами,
Сегодня мы рады объявить об важной вехе в нашей интеграции Move в Sui: Sui Move является полной функциональностью, поддерживается расширенными инструментами и содержит обширную документацию и примеры, в том числе:
- Серия руководств по программированию с объектами Sui Move .
- Поваренная книга основ Sui Move , шаблонов проектирования и примеров
- Расширенный плагин VSCode с поддержкой понимания кода и диагностики ошибок, созданный командой Mysten Move!
- Интеграция сборок Move, тестов, управления пакетами, создания документации и Move Prover с
sui
интерфейсом командной строки . - Набор примеров , включая взаимозаменяемые токены, NFT, DeFi и игры.
Когда мы начали работать над Sui в конце 2021 года, мы по-новому взглянули на Move и подумали о том, какие ранние дизайнерские решения не устарели, а также о том, как можно улучшить Move, чтобы использовать уникальные функции Sui. Ранее мы писали о том, что нового в Sui Move на уровне языка, но мы не углублялись в мотивацию введения этих различий. В оставшейся части этого поста этот запрос подробно рассматривается на примерах.
Подождите, есть разные Move's?
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, create_date: u64} имеет ключ, хранилищепубличная запись fun mass_mint(создатель: &signer, получатели: vector<address>) { assert!(signer::address_of(creator) == CREATOR, EAuthFail); пусть я = 0; while (!vector::is_empty(получатели)) { пусть получатель = vector::pop_back(&mut получатели); assert!(exists<Account>(получатель), ENoAccountAtAddress); пусть id = guid::create(создатель); пусть create_date = timestamp::today(); // ошибка! получатель должен быть `&signer`, а не `address` move_to(recipient, CoolAsset { id, created_date }) }
В Move в стиле Diem глобальное хранилище определяется парами (адрес, имя типа), то есть каждый адрес может хранить не более одного актива данного типа. Таким образом, строка move_to(recipient, CoolAsset { ... }
пытается передать CoolAsset
, сохраняя ее под recipient
адресом.
Однако этот код не скомпилируется в строке move_to(recipient, …)
. Основная проблема заключается в том, что в Move в стиле Diem вы не можете отправить значение типа CoolAsset
на адрес A , если только:
- Адрес, отличный от A , отправляет транзакцию для создания учетной записи в A
- Владелец A отправляет транзакцию, чтобы явно согласиться на получение объектов типа
CoolAsset
Это две сделки только для того, чтобы получить актив! Решение действовать таким образом имело смысл для Diem, которая была системой с ограниченным доступом, которая должна была тщательно ограничивать создание учетных записей и не допускать, чтобы учетные записи содержали слишком много активов из-за ограничений в системе хранения. Но это чрезвычайно ограничительно для открытой системы, которая хочет использовать распределение активов в качестве механизма адаптации или просто позволить активам свободно перемещаться между пользователями, как это происходит в Ethereum и подобных блокчейнах [1].
Теперь давайте посмотрим на тот же код в Sui Move:
struct CoolAsset {id: VersionedID, create_date: u64} имеет ключпубличная запись fun mass_mint (получатели: вектор <адрес>, ctx: &mut TxContext) { assert!(tx_context::sender(ctx) == CREATOR, EAuthFail); пусть я = 0; while (!vector::is_empty(получатели)) { пусть получатель = vector::pop_back(&mut получатели); пусть id = tx_context::new_id(ctx); пусть create_date = tx_context::epoch(); // Эпохи Sui - это 24-часовая передача (CoolAsset { id, create_date }, получатель) } }
Глобальное хранилище Sui Move с идентификаторами объектов. Каждая структура со key
способностью является «объектом Sui», который должен иметь глобально уникальное id
поле. Вместо использования ограниченной move_to
конструкции Sui Move вводит transfer
примитив, который можно использовать с любым объектом Sui. Под капотом этот примитив сопоставляется id
с CoolAsset
глобальным хранилищем и добавляет метаданные, чтобы указать, что значение принадлежит recipient
.
Интересным свойством версии Sui mass_mint
является то, что она коммутирует со всеми другими транзакциями (включая те, которые вызывают mass_mint
!). Среда выполнения Sui заметит это и отправит транзакции, вызывающие эту функцию, через византийский согласованный широковещательный «быстрый путь», который не требует консенсуса. Такие транзакции можно как совершать, так и выполнять параллельно! Это не требует никаких усилий со стороны программиста — он просто пишет приведенный выше код, а среда выполнения позаботится обо всем остальном.
Возможно, в тонком плане это не так для варианта Diem этого кода — даже если приведенный выше код работает, оба вызова exists<Account>
и guid::create
вызовут конфликты с другими транзакциями, генерирующими GUID
s или касающимися Account
ресурса. В некоторых случаях можно переписать код Move в стиле Diem, чтобы избежать спорных моментов, но многие идиоматические способы написания Move в стиле Diem создают тонкие узкие места, которые мешают параллельному выполнению.
Собственное владение активами и их передача
Давайте расширим код Move в стиле Diem с помощью обходного пути, который действительно будет компилироваться и запускаться. Идиоматический способ сделать это — «шаблон обертки»: поскольку Боб не может напрямую обратиться move_to
к CoolAsset
адресу Алисы, мы просим Алису «согласиться» на получение CoolAsset
, сначала опубликовав тип оболочки CoolAssetStore
с типом коллекции ( Table
) внутри. Алиса может сделать это, вызвав opt_in
функцию. Затем мы добавляем код, который позволяет Бобу переместить a CoolAsset
из его CoolAssetStore
в CoolAssetStore
.
В этом коде давайте добавим еще одно замечание: мы разрешаем CoolAsset
передавать файлы только в том случае, если с момента их создания прошло не менее 30 дней. Такая политика важна для создателей, которые (например) хотят отговорить спекулянтов от покупки/перепродажи билетов на мероприятия, чтобы истинным фанатам было легче получить их по разумной цене.
struct CoolAssetStore имеет ключ { активы: Table<TokenId, CoolAsset> }public fun opt_in(addr: &signer) { move_to(addr, CoolAssetHolder { assets: table::new() } }публичная запись fun cool_transfer( addr: &signer, Receiver: address, id: TokenId ) Acquis CoolAssetStore { // отозвать let sender = signer::address_of(addr); утверждать!(существует<CoolAssetStore>(отправитель), ETokenStoreNotPublished); let sender_assets = &mutзаимствовать_global_mut<CoolAssetStore>(sender).assets; assert!(table::contains(sender_assets, id), ETokenNotFound); пусть актив = table::remove(&sender_assets, id); // проверка того, что прошло 30 дней assert!(time::today() > assets.creation_date + 30, ECantTransferYet) // депозит assert!(exists<CoolAssetStore>(recipient), ETokenStoreNotPublished); пусть получатель_активов = &mut заимствовать_global_mut<CoolAssetStore>(получатель).assets; assert!(table::contains(recipient_assets, id), ETokenIdAlreadyUsed); table::add(recipient_assets, актив) }
Этот код работает. Но это довольно сложный способ выполнить простую задачу передачи актива от Алисы к Бобу! Опять же, давайте посмотрим на вариант Sui Move:
публичная запись fun cool_transfer ( актив: CoolAsset, получатель: адрес, ctx: &mut TxContext ) { assert!(tx_context::epoch(ctx) > assets.creation_date + 30, ECantTransferYet); передача (актив, получатель) }
Этот код намного короче. Здесь важно отметить, что cool_transfer
это entry
функция (это означает, что она может быть вызвана непосредственно средой выполнения Sui через транзакцию), но CoolAsset
в качестве входных данных она имеет параметр типа. Это магия Sui снова в действии! Транзакция включает в себя набор идентификаторов объектов, с которыми она хочет работать, и среду выполнения Sui:
- Преобразует идентификаторы в значения объекта (удаляя необходимость в частях
borrow_global_mut
иtable_remove
в коде в стиле Diem выше) - Проверяет, что объект принадлежит отправителю транзакции (удаление необходимости в
signer::address_of
части + связанном коде выше). Эта часть особенно интересна, как мы вскоре объясним: в Sui проверка владения безопасным объектом является частью среды выполнения! - Проверяет типы значений объекта на соответствие типам параметров вызываемой функции.
cool_transfer
- Связывает значения объекта и другие аргументы с параметрами
cool_transfer
и вызывает функцию
Это позволяет программисту Sui Move пропустить стандартную часть логики «снятия средств» и сразу перейти к интересной части: проверке 30-дневной политики истечения срока действия. Точно так же часть «депозита» значительно упрощается с помощью transfer
конструкции Sui Move, описанной выше. И, наконец, нет необходимости вводить тип-оболочку, как CoolAssetStore
в случае с внутренней коллекцией — глобальное хранилище Sui, индексированное по идентификатору, позволяет адресу хранить произвольное количество значений с заданным типом.
Еще одно отличие, на которое следует обратить внимание, заключается в том, что существует 5 способов прерывания в стиле Diem cool_transfer
(т. е. отказ и взимание платы с пользователя за газ без завершения передачи), в то время как Sui Move cool_transfer
может прерваться только одним способом: когда 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
объект, который будет использоваться в игре - Алиса хочет поддерживать аксессуары своего персонажа с помощью сторонних аксессуаров разных типов, созданных позже.
- Любой должен иметь возможность создать аксессуар, но владелец
Character
должен решить, добавлять аксессуар или нет. - При переносе
Character
должны автоматически передаваться все его аксессуары.
На этот раз начнем с кода Sui Move. Мы воспользуемся преимуществом еще одного аспекта встроенной функции владения объектами среды выполнения Sui: объект может принадлежать другому объекту . Каждый объект имеет уникального владельца, но родительский объект может иметь произвольное количество дочерних объектов. Отношения между родительскими и дочерними объектами создаются с помощью transfer_to_object
функции, родственной transfer
функции, представленной выше.
// в модуле Character, созданном Алисой struct Character has key { id: VersionedID, Favorite_color: u8, Strength : u64, ... }/// Владелец `c` может добавить общедоступную запись `accessor` fun accessorize<T: key>(c: &mut Character, accessor: T) { transfer_to_object(c, accessor) }// ... в модуле, добавленном позже Бобом struct SpecialShirt имеет ключ { id: VersionedID, color: u8 }public entry fun dress(c: &mut Character, s: Shirt) { // специальная рубашка должна быть любимого цвета персонажа assert!(character::favorite_color(c) == s.color, EBadColor); character::accessorize(c, рубашка) }// ... в модуле, добавленном позже Клариссой struct Sword has key { id: VersionedID, power: u64 }public entry fun equip(c: &mut Character, s: Sword) { // персонаж должен быть очень сильным, чтобы использовать мощный меч assert!(character::strength(c) > sword.power * 2, ENotStrongEnough); характер:: аксессуары (с, с) }
В этом коде Character
модуль включает accessorize
функцию, которая позволяет владельцу персонажа добавлять вспомогательный объект произвольного типа в качестве дочернего объекта. Это позволяет Бобу и Клариссе создавать свои собственные типы аксессуаров с другими атрибутами и функциями, которые Алиса не ожидала, но основываться на том, что Алиса уже сделала. Например, рубашку Боба можно надеть только в том случае, если это любимый цвет персонажа, а меч Клариссы можно использовать только в том случае, если персонаж достаточно силен, чтобы владеть им.
В Move в стиле Diem такой сценарий реализовать невозможно. Вот несколько неудачных попыток реализации стратегий:
// попытка 1 struct Character { // не сработает, потому что каждый аксессуар должен быть одного типа + иметь // одинаковые поля. В Move нет подтипов. // Рубашке Боба нужен цвет, а мечу Клариссы нужна сила — стандарта нет // представление аксессуара может предвидеть все, что разработчики захотят // создать аксессуары: vector<Accessory> }// попытка 2 struct Character { // возможно, Алиса предвидит потребность в Мече и Рубашке впереди... меч: Option<Sword>, shirt: Option<Shirt> // ...но что происходит, когда появляется Дэниел позже и хочет добавить штаны? }// попытка 3 // Не поддерживает вспомогательные композиции. Например: как нам представить // персонажа в штанах и рубашке, но без меча? struct Shirt { c: Персонаж } struct Sword { s: Рубашка } struct Pants { s: Sword }
Ключевые проблемы заключаются в том, что в Move в стиле Diem:
- Поддерживаются только однородные коллекции (как показывает первая попытка), но аксессуары принципиально разнородны.
- Ассоциации между объектами могут быть созданы только посредством «обертывания» (т. е. хранения объекта внутри другого объекта); но набор объектов, которые можно обернуть, должен быть определен заранее (как во второй попытке) или добавлен специальным образом, который не поддерживает вспомогательную композицию (как в третьей попытке).
Вывод
Sui — первая платформа, которая существенно отличается от оригинального дизайна Diem в том, как она использует Move. Разработка встраивания, в полной мере использующего Move и уникальные функции платформы, — это одновременно и искусство, и наука, требующая глубокого понимания как языка Move, так и лежащих в его основе возможностей блокчейна. Мы очень рады успехам, достигнутым Sui Move, и новым вариантам его использования!
[1] Еще один аргумент в пользу политики Move в стиле Diem, согласно которой «необходимо подписаться на получение ресурса определенного типа», заключается в том, что это хороший механизм предотвращения спама. Однако мы считаем, что предотвращение спама относится к прикладному уровню. Вместо того, чтобы просить пользователей отправлять транзакции, которые стоят реальных денег, чтобы согласиться на получение актива, спам легко устраняется (например) на уровне кошелька с богатыми определяемыми пользователем политиками и помощью автоматических спам-фильтров.
Узнать больше о Sui
- Сайт Sui — https://sui.io/
- Портал разработчиков Sui — https://docs.sui.io/
- Переместить язык программирования
Стройте вместе с нами!!
Мы в восторге от того, что ждет нас в будущем, и приглашаем создателей и строителей присоединиться к нам.