Далее мы рассмотрим в деталях основные характеристики языка Move и в чем его ключевые различия с другим, уже популярным языком для смарт-контрактов — Solidity (на платформе Ethereum). Материал основан на изучении доступного он-лайн 26-страничного whitepaper-а.
Введение
Move — это исполняемый язык байт-кода, который используется для выполнения пользовательских транзакций и смарт-контрактов. Обратите внимание на два момента:
- В то время как Move является языком байт-кода, который может напрямую выполняться на виртуальной машине Move, Solidity (язык смарт-контрактов в Ethereum) — язык более высокого уровня, который сначала компилируется в байт-код перед выполнением в EVM (Ethereum Virtual Machine).
- Move можно использовать не только для реализации смарт-контрактов, но и для пользовательских транзакций (подробнее об этом будет дальше), в то время как Solidity — это язык только для смарт-контрактов.
Перевод выполнен командой проекта INDEX Protocol. Ранее мы уже переводили большой материал, описывающий проект Libra, теперь настала очередь чуть более подробно взглянуть на язык Move. Перевод выполнен совместно с читательом coolsiu
Ключевой особенностью Move является возможность определять пользовательские типы ресурсов с семантикой, основанной на линейной логике: ресурс никогда не может быть скопирован или неявно удален, только перемещен. Функционально, это схоже с возможностями языка Rust. Значения в Rust могут быть назначены только одному имени за раз. Присвоение значения другому имени делает его недоступным под предыдущим именем.
Для примера, следующий фрагмент кода выведет ошибку: Use of moved value ‘x’. Это потому, что в Rust нет сборки мусора. Когда переменные выходят из области видимости, память, на которую они ссылаются, также освобождается. Проще говоря, может быть только один «владелец» данных. В этом примере x является первоначальным владельцем, а затем y становится новым владельцем. Подробнее о таком поведении здесь.
Представление цифровых активов в открытых системах
Существует два свойства физических активов, которые трудно представить в цифровом виде:
- Редкость (Дефицитность, в оригинале — scarcity). Количество активов (эмиссия) в системе должна быть контролируемой. Необходимо запретить дублирование существующих активов, а создание новых — это привилегированная операция.
- Контроль доступа. Участник системы должен иметь возможность защитить активы с помощью политик контроля доступа.
Эти две характеристики, которые естественны для физических активов, нужно реализовать и для цифровых объектов, если мы хотим считать их активами. Например, редкий металл — имеет естественный дефицит, и только у вас есть к нему доступ (держа в руках, например) и вы можете его продать или потратить.
Чтобы проиллюстрировать, как мы пришли к двум этим свойствам, давайте начнем со следующих предложений:
Предложение № 1: простейшее правило без дефицита и контроля доступа
- G[K]:=n обозначает обновление числа, доступного по ключу К в глобальном состоянии блокчейна, новым значением n.
- transaction ⟨Alice, 100⟩ означает установку баланса счета Алисы на 100.
Приведенное выше решение имеет несколько серьезных проблем:
- Алиса может получить неограниченное количество монет, просто отправляя transaction ⟨Alice, 100⟩.
- Монеты, которые Алиса посылает Бобу, бесполезны, так как Боб мог отправлять себе неограниченное количество монет, используя ту же технику.
Предложение № 2: Учитываем дефицит
Теперь мы отслеживаем ситуацию, чтобы количество монет Ka было как минимум, равно n перед транзакцией перевода. Тем не менее, хотя это решает проблему дефицита, нет никакой информации о том, кто может отправлять монеты Алисы (пока что это может сделать каждый, главное не нарушать правило ограничения количества).
Предложение № 3: Объединяем дефицит и контроль доступа
Мы решаем эту проблему с помощью механизма цифровой подписи verify_sig перед проверкой баланса, что означает, что Алиса использует свой закрытый ключ для подписания транзакции и подтверждения того, что она является владельцем своих монеты.
Языки программирования блокчейна
Существующие языки блокчейна сталкиваются со следующими проблемами (все они были решены в Move (прим.: к сожалению, автор статьи аппелирует только к Ethereum в своих сравнениях, поэтому стоит воспринимать их лишь в таком контексте. Например, большинство из нижесказанного также решено и в EOS)):
Непрямое представление активов. Актив кодируется с использованием целого числа, но целочисленное значение — это не то же самое, что актив. На самом деле, нет типа или значения, представляющего биткойн / эфир / <Любая Монета>! Это делает трудным и подверженным ошибкам написание программ, использующих активы. Паттерны, такие как передача активов в/из процедур или хранение активов в структурах требуют специальной поддержки от языка.
Дефицит нерасширяем. Язык представляет только один дефицитный актив. Кроме того, средства защиты от дефицита жестко зашита непосредственно в самой семантике языка. Разработчик, если он хочет создать пользовательский актив, должен сам тщательно контролировать все аспекты ресурса. Это как раз проблемы смарт-контрактов Ethereum.
Пользователи выпускают свои активы, токены стандарта ERC-20, используя целые числа для определения как стоимости, так и общей эмиссии. Всякий раз, когда создаются новые токены, код смарт-контракта должен самостоятельно проверять соблюдения правил эмиссии. Кроме того, непрямое представление активов приводит, в ряде случаев, к серьезным ошибкам — дублированию, двойным тратам или даже полной потере активов.
Отсутствие гибкого контроля доступа. Единственная политика контроля доступа, применяемая сейчас — это схема подписи с использованием асимметричной криптографии. Как и защита от дефицита, политика контроля доступа глубоко заложена в семантике языка. Но как расширить язык, чтобы позволить программистам определять собственные политики контроля доступа — это, зачастую, очень нетривиальная задача.
Это также верно для Ethereum, где смарт-контракты не имеют родной поддержки криптографии для контроля доступа. Разработчики должны вручную прописать контроль доступа, например, используя модификатор onlyOwner.
Несмотря на то, что я большой поклонник Ethereum, я считаю, что свойства активов должны изначально нативно поддерживаться языком в целях безопасности. В частности, передача Ether в смарт-контракт включает динамическую диспетчеризацию, которая привела к появлению нового класса ошибок, известных как повторный входа (re-entrancy vulnerabilities). Динамическая диспетчеризация здесь означает, что логика выполнения кода будет определяться во время выполнения (динамическая), а не во время компиляции (статическая).
Таким образом, в Solidity, когда контракт A вызывает функцию контракта B, контракт B может запускать код, который не был предусмотрен разработчиком контракта A, что может привести к уязвимостям повторного входа (контракт A случайно выполняет функцию контракта B, чтобы снять деньги до фактического вычета остатков из учетной записи).
Основы дизайна языка Move
Ресурсы первого порядка
Если говорить высокоуровнево, то взаимодействие между модулями/ресурсами/процедурами в языке Move очень напоминают отношения между классами/обьектами и методами в ООП-языках.
Модули в Move аналогичны смарт-контрактам в других блокчейнах. Модуль объявляет типы ресурсов и процедуры, которые задают правила для создания, уничтожения и обновления объявленных ресурсов. Но все это — лишь условности (“жаргон”) в Move. Чуть позже мы проиллюстрируем этот момент.
Гибкость
Move добавляет гибкости Libra через скрипты. Каждая транзакция в Libra включает в себя скрипт, который фактически является основной процедурой транзакции. Скрипт может выполнять или одно заданное действие, например, платежи по указанному списку получателей, или переиспользовать другие ресурсы — например, вызывая процедуру, в которой задана общая логика. Вот почему скрипты транзакций Move предлагают большую гибкость. Скрипт может использовать как одноразовые, так и повторяющиеся варианты поведения, в то время как Ethereum может выполнять только повторяющиеся сценарии (вызывая один метод метод смарт-контракта). Причина, по которой он назван «многократным», заключается в том, что функции смарт-контракта могут выполняться несколько раз. (прим.: здесь момент очень тонкий. С одной стороны, скрипты транзакций в виде псевдо-байткода, есть и в Bitcoin. С другой, как я понял — Move расширяет этот язык, по сути, до уровня полноценного языка смарт-контрактов).
Безопасность
Исполняемый формат Move — это байт-код, который, с одной стороны, язык более высокого уровня, чем ассемблер, но низкоуровневее, чем исходный код. Байт-код проверяется в ран-тайме (on-chain) на наличие ресурсов, типы и безопасности памяти с помощью верификатора байт-кода, а затем выполняется интерпретатором. Такой подход позволяет Move предоставлять безопасность, характерную для исходного кода, но без процесса компиляции и необходимости добавлять компилятор в систему. Сделать Move языком байт-кода — это действительно хорошее решение. Его нет нужды компилировать из исходников, как в случае с Solidity, не нужно беспокоиться о возможных сбоях или атаках на инфраструктуру компилятора.
Верифицируемость
Мы нацелены на выполнение как можно более легких проверок, так как все это идет on-chain (прим.: он-лайн, в процессе выполнения каждой транзакции, поэтому любая задержка приводит к замедлению всей сети), однако изначально дизайн языка готов к использованию и off-chain средств статической верификации. Хотя это и более предпочтительно, но пока разработка средств верификации (как отдельного тулкита) отложена на будущее, и сейчас поддерживается только динамическая верификация в ран-тайме (on-chain).
Модульность
Модули Move обеспечивают абстракцию данных и локализуют критические операции над ресурсами. Инкапсуляция, обеспечиваемая модулем, в сочетании с защитой, обеспечиваемой системой типов Move, гарантирует, что свойства, установленные для типов модуля, не могут быть нарушены кодом вне модуля. Это достаточно продуманный дизайн абстракции, означающий, что данные внутри контракта могут изменяться только в рамках выполнение контракта, но не извне.
Обзор Move
Пример скрипта транзакции демонстрирует, что злонамеренные или неосторожные действия программиста вне модуля не могут нарушить безопасность ресурсов модуля. Далее мы рассмотрим примеры того, как модули, ресурсы и процедуры используются для программирования блокчейна Libra.
Peer-to-Peer платежи
Заданное в amount количество монет будет переведено с баланса отправителя к получателю.
Здесь есть несколько новых моментов (выделено красными надписями):
- 0x0: адрес учетной записи, где хранится модуль
- Currency: название модуля
- Coin: тип ресурса
- Значение coin, возвращаемое процедурой, является значением ресурса, тип которого 0x0.Currency.Coin
- move(): значение не может быть использовано снова
- copy(): значение может быть использовано позже
Разбираем код: на первом шаге отправитель вызывает процедуру с именем withdraw_from_sender из модуля, хранящегося в 0x0.Currency. На втором этапе отправитель переводит средства получателю, перемещая значение ресурса монеты в процедуру депозита модуля 0x0.Currency.
Вот три примера ошибок в коде, которые будут отклонены проверками:
Дублирование средств путем изменения вызова move(coin) на copy(coin). Ресурсы могут быть только перемещены. Попытка дублировать количество ресурса (например, вызывая copy(coin) в приведенном выше примере) приведет к ошибке во время проверки байт-кода.
Переиспользование средств, указав move(coin) дважды . Добавление строки 0x0.Currency.deposit (copy (some_other_payee), move (coin)) к примеру выше позволит отправителю «потратить» монеты дважды — первый раз с получателем платежа, а второй с some_other_payee. Это нежелательное поведение, невозможное с физическим активом. К счастью, Move отклонит эту программу.
Потеря средств из-за отказа в move(coin). Если не переместить ресурс (например, удалив строку, содержащую move(coin)), будет вызвана ошибка проверки байт-кода. Это защищает программистов Move от случайной или злонамеренной утери средств.
Модуль Currency
Каждая аккаунт может содержать 0 или более модулей (изображенных в виде прямоугольников) и одно или несколько значений ресурсов (изображенных в виде цилиндров). Например, учетная запись по адресу 0x0 содержит модуль 0x0.Currency и значение ресурса типа 0x0.Currency.Coin. Учетная запись по адресу 0x1 имеет два ресурса и один модуль; Учетная запись по адресу 0x2 имеет два модуля и одно значение ресурса.
Некоторые моменты:
- Скрипт транзакции атомарный — или полностью выполняться, или никак.
- Модуль — это долгоживущий кусок кода, глобально доступный.
- Глобальное состояние структурировано как хеш-таблица, где ключем будет адрес аккаунта
- Учетные записи могут содержать не более одного значения ресурса данного типа и не более одного модуля с заданным именем (учетная запись по адресу 0x0 не может содержать дополнительный ресурс 0x0.Currency.Coin или другой модуль с именем Currency)
- Адрес декларируемого модуля является частью типа (0x0.Currency.Coin и 0x1.Currency.Coin — это отдельные типы, которые нельзя использовать взаимозаменяемо)
- Программисты могут хранить несколько экземпляров данного типа ресурса в учетной записи, определяя свой кастомный ресурс — (resource TwoCoins {c1: 0x0.Currency.Coin, c2: 0x0.Currency.Coin})
- Вы можете ссылаться на ресурс по его имени без конфликтов, например, вы можете ссылаться на два ресурса, используя TwoCoins.c1 и TwoCoins.c2.
Объявление ресурса Coin
Модуль с именем Currency и типом ресурса с именем Coin
Некоторые моменты:
- Coin — это структура с одним полем типа u64 (64-разрядное целое без знака)
- Только процедуры модуля Currency могут создавать или уничтожать значения типа Coin.
- Другие модули и скрипты могут записывать или ссылаться на поле значения только через открытые процедуры, предоставляемые модулем.
Реализация депозита
Эта процедура принимает ресурс Coin в качестве входных данных и объединяет его с ресурсом Coin, хранящимся на счете получателя:
- Уничтожение входного ресурса Coin и запись ее значения.
- Получение ссылки на уникальный ресурс Coin, хранящийся на аккаунте получателя.
- Изменение значения количества Coin на величину, переданную в параметре при вызове процедуры.
Некоторые моменты:
- Unpack, BorrowGlobal — встроенные процедуры
- Unpack<T> это единственный способ удалить ресурс типа T. Процедура принимает ресурс на вход, уничтожает его и возвращает значение, ассоциированное с полями ресурса.
- BorrowGlobal<T> принимает адрес в качестве ввода и возвращает ссылку на уникальный экземпляр T, опубликованный (принадлежащий) этому адресу
- &mut Coin это ссылка на ресурс Coin
Реализация withdraw_from_sender
Эта процедура:
- Получает ссылку на уникальный ресурс Coin, привязанный к аккаунту отправителя
- Уменьшает значение ресурса Coin по ссылке на указанную сумму
- Создает и возвращает новый ресурс Coin с обновленным балансом.
Некоторые моменты:
- Deposit может быть вызван кем угодно, но withdraw_from_sender имеет доступ только к монетам вызывающего аккаунта
- GetTxnSenderAddress схоже с msg.sender в Solidity
- RejectUnless схоже с require в Solidity. Если эта проверка неудачна, исполнение транзакции останавливается и все изменения откатываются.
- Pack<T> это так же встроенная процедура, которая создает новый ресурс типа Т.
- Также как Unpack<T>, Pack<T> может вызываться только внутри модуля, где описан ресурс T
Заключение
Мы разобрали основные характеристики языка Move, сравнили его с Ethereum, а также ознакомились с основным синтаксисом скриптов. В завершение, я настоятельно рекомендую полистать оригинальный white paper. Он включает в себя множество деталей, касающихся принципов проектирования языка программирования, а также множество полезных ссылок.
Автор: Александр Лозовюк