Michelson - это язык смарт-контрактов блокчейна Tezos. Грубо говоря, Майкельсон для Tezos так же, как виртуальная машина Ethereum для Ethereum. Оба языка основаны на стеке, что означает, что вычисления выполняются путем изменения последовательности элементов данных (стека) в соответствии с некоторой последовательностью инструкций (программа).

Штабелеукладчики

Наиболее заметное различие между Майкельсоном и EVM заключается в том, что Майкельсон написан в удобочитаемом текстовом формате, тогда как операции EVM представлены в байтах. Например, если вы посмотрите таблицу кодов операций для EVM, вы увидите, что код операции 01 берет два числа (из верхней части стека) и складывает их. Эквивалентная операция у Майкельсона записывается как ADD.

(Честно говоря, код операции сложения 01 в EVM имеет ADD в качестве промежуточного мнемонического представления)

На изображении выше у нас есть стек, который мы можем записать как

20 : 7 : 13 : 45 : []

где : - наш разделитель элементов, а [] обозначает нижнюю часть стека.

На иллюстрации мы применяем к стеку операцию ADD, которая имеет следующее определение:

ADD / a : b : S => (a + b) : S

На простом английском языке это определение гласит: «Операция ADD удаляет два верхних элемента стека (a и b в определении), а затем помещает элемент (a + b) обратно на вершину стека:

ADD / 20 : 7 : 13 : 45 : [] => 
    (20 + 7) : 13 : 45 : [] => 
          27 : 13 : 45 : []

Все вычисления с помощью Майкельсона работают аналогичным образом на основе этого процесса мутации стека. Мы использовали ADD в приведенном выше примере, но мы также могли изменить стек с помощью других арифметических операций, таких как вычитание или умножение, или логических операций, таких как NOT, AND, OR. Мы можем напрямую управлять стеком, явно помещая в него данные, меняя местами или дублируя элементы. У нас есть структуры потока управления, такие как LOOP или IF. Мы можем выполнять некоторые криптографические операции, такие как хеширование или проверка подписей, и мы можем взаимодействовать с блокчейном, инициируя передачу токенов или создавая учетные записи. У Майкельсона много разных операций.

Типы

Второе важное различие между Майкельсоном и EVM заключается в том, что элементы данных Майкельсона типизированы.

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

Если значение 1 имеет тип int (для целого числа), то мы знаем, что можем безопасно выполнять числовое сложение с ним, но не можем безопасно выполнить индексацию списка.

Для значения "foobar" с типом string ситуация обратная. Добавление числа в список 1 + "foobar" не является четко определенным, потому что сложение - это операция с целыми числами (в большинстве языков некоторые языки перегружают оператор +, чтобы означать числовое сложение, когда его аргументы являются числами, и конкатенацию, когда его аргументы являются строками)

Типы полезны, потому что они позволяют интерпретатору Майкельсона исключать программы, которые могут иметь проблемное поведение. Например, для натурального числа nat попытка вычесть большее nat из меньшего nat, например 4 - 5, приведет к ошибке. Затем программист может определить во время тестирования, является ли эта ошибка результатом нежелательной операции, неправильных аргументов или следует изменить сигнатуру типа значений. Ключевым моментом является то, что эта ошибка возникла на ранней стадии и помешала запуску программы вообще, а не для того, чтобы возможная ошибка ускользнула от тестирования незамеченной только для того, чтобы вызвать проблемы позже в производственной среде.

Вообще говоря, типы позволяют программисту более подробно сообщать о своих намерениях машине и позволяют машине общаться с программистом, когда выполнение отклоняется от этих намерений.

Как настроить среду Майкельсона

Хорошо, теперь, когда мы немного рассмотрели теорию работы Майкельсона, давайте поработаем руками немного кода.

Установка клиента Tezos

Самый простой способ поиграть с Майкельсоном - установить клиент Tezos с помощью докера. В вашем терминале введите следующие команды:

$ wget https://gitlab.com/tezos/tezos/raw/alphanet/scripts/alphanet.sh 
$ chmod +x ./alphanet.sh 
$ ./alphanet.sh start

Это должно выдать кучу результатов, которые вы можете пока игнорировать. Важно то, что в клиенте Tezos есть среда исполнения Майкельсона, которую мы можем использовать для тестирования наших программ.

Кроме того, вы можете собрать Tezos из исходного кода, следуя инструкциям в Tezos Docs.

Привет, Тезос

Откройте ваш любимый редактор и напишите следующую программу helloTezos.tz в том же каталоге, в котором вы поместили alphanet.sh скрипт.

# helloTezos.tz
parameter unit; 
storage string; 
code {DROP; 
      PUSH string "Hello Tezos!"; 
      NIL operation; PAIR;};

Сначала мы проверим, правильно ли написан сценарий:

$ ./alphanet.sh client typecheck script container:helloTezos.tz

Мы можем увидеть больше информации, выдаваемой средством проверки типов, добавив флаг --details:

$ ./alphanet.sh client typecheck script container:helloTezos.tz --details

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

Запуск скриптов

Теперь, когда мы знаем о проверках типов программы, мы собираемся запустить ее. Команда для указания клиенту tezos запустить программу Майкельсона (в песочнице):

$ ./alphanet.sh client run script <path> on storage <data> and input <data>

где <path> - это путь к исходному тексту программы (поскольку мы используем докер, перед ним будет стоять container:), а <data> - некоторое значение Майкельсона.

Ниже мы рассмотрим, что означают storage и input. А пока попробуйте запустить:

$ ./alphanet.sh client run script container:helloTezos.tz on storage '""' and input Unit

Это должно вернуться:

storage "Hello Tezos!" emitted operations

Поздравляем, вы только что выполнили свой первый смарт-контракт в Майкельсоне!

Соглашение о вызове контракта Майкельсона

Теперь давайте подробно рассмотрим, как работает контракт:

Все смарт-контракты Майкельсона - это функции, которые принимают два аргумента, входной параметр и значение хранилища, и возвращают пару из списка операций и значения хранилища. Значение хранения фактически является возвращаемым значением, а список операций подобен продолжению, если, например, контракт был вызван другим контрактом.

Существует множество различных способов обозначения подписей типов, но вот как выглядит подпись типа Майкельсона контракта:

lambda (pair 'parameter 'storage) (pair (list operation) 'storage)

Лично я предпочитаю обозначение типов, больше похожее на Haskell, но обе подписи эквивалентны:

contract :: (Parameter p, Storage s) -> ([Operation], Storage s)

Начальный и возвращаемый типы helloTezos

Давайте еще раз посмотрим на helloTezos.tz:

# helloTezos.tz
parameter unit; 
storage string; 
code {DROP; 
      PUSH string "Hello Tezos!"; 
      NIL operation; PAIR;};

Строки parameter unit и storage string определяют типы двух аргументов контракта. Если мы конкретизируем приведенную выше подпись для общего типа контрактов Майкельсона, с 'parameter как unit и 'storage как string, мы получим тип нашего конкретного helloTezos.tz контракта:

lambda (pair unit string) (pair (list operation) string)

Первоначальный стек контракта Майкельсона - это пара аргументов (pair 'parameter 'storage), поэтому helloTezos.tz начинается со стека типа:

:: (pair unit string) : []

В командной строке мы запустили

$ ./alphanet.sh client run script container:helloTezos.tz on storage '""' and input Unit

'""' - это синтаксис командной строки для "", пустой строки, а Unit - это конструктор данных единственного жителя типа unit. Имейте в виду, что здесь ввод и параметр являются синонимами. Итак, наш начальный стек имеет конкретное значение:

(Pair Unit "") : []

который имеет тип

:: (pair unit string) : []

Различие между Pair и pair в том, что Pair - это конструктор данных, а pair - это тип. Конструкторы данных в Michelson начинаются с заглавной буквы, а все типы - в нижнем регистре.

Теперь у нас есть начальное значение и тип возвращаемого значения, и мы можем думать о выполнении нашего контракта как о последовательности операций, которые преобразуют наше начальное значение в возвращаемое значение, которое соответствует нашему возвращаемому типу.

Другими словами, мы начали с

(Pair Unit "") : [] :: (pair unit string) : []

и мы хотим закончить с

??? :: (pair (list operation) string) : []

привет, казнь Тезоса

К счастью, наш контракт довольно короткий, всего 4 операции, поэтому мы можем пройти этапы этого преобразования здесь. Операции записываются после заголовка code в helloTezos.tz:

# helloTezos.tz 
code {DROP; 
      PUSH string "Hello Tezos!"; 
      NIL operation; PAIR;};

Если мы запишем эту последовательность операций и начальные значения и типы из предыдущего раздела, мы получим полное состояние нашей стековой машины Майкельсона:

STATE 
code DROP; PUSH string "Hello Tezos!"; NIL operation; PAIR; 
stack (Pair Unit "") : [] 
type :: (pair unit string) : []

УРОНИТЬ

Операция DROP удаляет (или «отбрасывает») верхний элемент стека и имеет следующее определение (слегка измененное из спецификации Майкельсона)

code DROP 
stack _ : S => S 
type :: _ : 'A -> 'A

где _ - это подстановочный знак, соответствующий любым операциям, значениям или типам

Применяя это к нашему состоянию, наше новое состояние:

STATE 
code PUSH string "Hello Tezos!"; NIL operation; PAIR; 
stack [] 
type :: []

ТОЛКАТЬ

Операция PUSH добавляет значение определенного типа на вершину стека и имеет следующее определение:

code PUSH 'a (x :: 'a) 
stack S => x : S 
type 'A -> 'a : 'A

Наша конкретная инструкция PUSH string "Hello Tezos!", поэтому преобразование конкретизируется как:

code PUSH string ("Hello Tezos!" :: string) 
stack S => "Hello Tezos!" : S 
type 'A -> string : 'A

который при применении дает нам новое состояние:

STATE 
code NIL operation; PAIR; 
stack "Hello Tezos!" : [] 
type :: string : []

Ноль

Операция NIL добавляет пустой список определенного типа на вершину стека и имеет следующее определение:

code NIL 'a 
stack S => {} : S 
type 'A -> list 'a : 'A

который при применении дает нам новое состояние:

STATE 
code PAIR; 
stack {} : "Hello Tezos!" : [] 
type :: list operation : string : []

ПАРА

Операция PAIR удаляет два верхних элемента стека, создает пару из них и помещает пару в стек. Он имеет следующее определение:

code PAIR 
stack a : b : S => (Pair a b) : S 
type 'a : 'b : 'A -> pair 'a 'b : 'A

который при применении дает нам новое состояние:

STATE 
code 
stack (Pair {} "Hello Tezos!") : [] 
type :: pair (list operation) string : []

Конец

Сейчас мы не работаем, и наш стек имеет тип pair (list operation) string : [], который нам и нужен. Поскольку тип соответствует нашему ожидаемому возвращаемому типу, контракт возвращает значения в нашем (pair 'operations 'storage):

storage 
  "Hello Tezos!" 
emitted operations

Заключение

На этом завершается часть I нашего руководства Майкельсона. Теперь вы должны знать

  • основы работы стековой машины
  • как установить клиент Tezos
  • как составить и оформить простой договор Майкельсона
  • какие типы Майкельсона, значения и операции и несколько простых примеров каждого из них

Во второй части мы напишем больше контрактов и рассмотрим их выполнение (хотя и с меньшей тщательностью), введя больше операций, типов и конструкторов данных.

Упражнения для читателя

Упражнение 1. Измените наш helloTezos.tz файл так, чтобы он выводил "Hello World!" вместо "Hello Tezos. Назовите этот файл helloWorld.tz

Упражнение 2: Теперь измените наш helloTezos.tz файл так, чтобы он принимал строковый аргумент в качестве ввода и вывода. "Hello <input>". Вызовите этот файл helloInput.tz. Для этого вам нужно знать две дополнительные операции:

code CAR # select left-hand of pair 
stack (Pair a _) : S => a : S
type pair 'a 'b : 'A -> 'a : 'A
code CONCAT # string concatenate 
stack a : b => a ^ b : S 
type string : string : 'A -> string: 'A 
  where a ^ b concatenates the end of a to the beginning of b

Упражнение 3: Выполните то же самое, что и упражнение 2, за исключением того, что теперь контракт принимает единицу в качестве входного параметра и выводит "Hello " соединенными с его начальным хранилищем. Итак, running$./alphanet.sh client run script container:hellotezos.tz on storage '"bar"' and input 'Unit'

должен выводить «Hello bar». Назовите этот файл helloStorage.tz

Вам нужно будет знать еще одну операцию:

code CDR # select right-hand of pair 
stack (Pair _ b) : S => b : S 
type pair 'a 'b : 'A -> 'b : 'A

Упражнение 4: Напишите контракт, который объединяет входную строку и строку хранения и выводит "Hello <input><storage>". назовите этот файлhelloInputAndStorage.tz

Вам нужно будет знать:

code DUP # Duplicate the top of the stack
stack x : S => x : x : S
type :: 'a : 'A -> 'a : 'a : 'A
code DIP ops # Runs code protecting the top of the stack
stack x : S => x : S' 
  where ops / S => S' 
type :: 'b : 'A -> 'b : 'C 
 iff ops :: [ 'A -> 'C ]

Вы также можете использовать SWAP вместо DIP

SWAP # Exchange the top two elements of the stack
stack x : y : S => y : x : S 
type :: 'a : 'b : 'A -> 'b : 'a : 'A

Первоначально опубликовано на странице https://gitlab.com/camlcase-dev/michelson-tutorial/tree/master/01

Получайте лучшие предложения по программному обеспечению прямо в свой почтовый ящик