Криптохомяки: смарт-контракт изнутри
Криптохомяки: смарт-контракт изнутри
Для разработки блокчейн-протоколов и распределенных приложений используются разные языки и парадигмы программирования. Одни системы поддерживают «классические» языки, такие как Python, C и JavaScript. Другие — внедряют собственные языки для разработки смарт-контрактов вроде Solidity в сети Ethereum.
Язык программирования Sophia от æternity — другой пример собственного языка программирования в блокчейн-проекте. Sophia относится к функциональным языкам программирования, о которых мы писали в одном из прошлых спецпроектов.
В этом спецпроекте мы рассмотрим язык Sophia на примере реального смарт-контракта и сравним получившийся код с его аналогом на Solidity. Вы познакомитесь с базовыми элементами смарт-контрактов на обоих языках и увидите, чем отличаются функциональные и объектно-ориентированные языки в блокчейн-разработке.
Но главное — сделаете собственного криптохомяка, которого можно будет распечатать и повесить на холодильник сразу после прочтения этого материала.
Это один из нескольких спецпроектов ForkLog, подготовленных при поддержке æternity. В прошлых совместных материалах мы рассказали о функциональном программировании в блокчейн-разработке и о роли ASIC-майнинга в криптоиндустрии.
Спонсор спецпроекта — æternity — блокчейн-платформа третьего поколения для создания децентрализованных приложений и масштабируемых смарт-контрактов. Блокчейн æternity взаимодействуют с внешним миром через оракулы — специальные узлы, которые получают и верифицируют информацию извне сети. Кроме того, в æternity работает система ончейн-управления, построенная на принципах «жидкой демократии» (liquid democracy).
Блокчейн æternity написан на функциональном языке программирования Erlang, а для разработки тьюринг-полных смарт-контрактов команда проекта разработала собственный функциональный язык Sophia. Согласно документации æternity, благодаря особенностям функциональной парадигмы Sophia упрощает создание корректного структурированного кода, в котором проще отследить побочные эффекты.
Разбор смарт-контрактов
Sophia — функциональный язык программирования, родственный ReasonML. Команда æternity разработала Sophia для создания тьюринг-полных смарт-контрактов для виртуальных машин æternity и Ethereum. Solidity же относится к императивным языкам программирования и больше похож на JavaScript.
С точки зрения пользователя примеры контрактов на Sophia и Solidity работают одинаково: запрашивают имя и выводят на экран изображение хомяка. С точки зрения разработчика сходство не такое сильное, но скоро вы сами все увидите.
Для начала введите имя для хомяка:
Теперь давайте посмотрим, как это работает.
Solidity
Sophia
Контракт на Solidity версии 0.4.0 и позднее начинается с объявления версии компилятора. В нашем случае используется версия 0.4.25.
Версия объявляется строкой: pragma solidity ^0.4.25;
Благодаря объявлению версии машина будет знать, какими “правилами” пользоваться при интерпретации кода.
Прежде чем писать код, нужно подобрать имя нашему контракту. Имя объявляется после ключевого слова contract, а в фигурных скобках содержится весь остальной код.
Назовем контракт CryptoHamster: contract CryptoHamster { … }
По соглашению между разработчиками Ethereum имя контракта всегда начинается с заглавной буквы. Так имя контракта можно легко отличить от имен функций и других элементов кода.
События (event) в Ethereum работают как триггеры для приложений. При вызове события аргументы в скобках записываются в журнал (log) транзакции — специальную структуру данных в блокчейне, привязанную к адресу контракта.
event NewHamster() описывает событие с именем NewHamster. Когда это событие вызывается, пользовательский интерфейс реагирует и выводит на экран изображение хомяка с нужными параметрами (uint hamsterId, string name, uint dna).
Тип данных
uint
(unsigned integer) — это 256-битные целые неотрицательные числа.Тип string (строка) содержит текст. Любые символы в значении типа string будут интерпретироваться как текстовые символы.
Теперь нужно объявить переменные, которые будут отвечать за создание нового хомяка.
Мы объявляем две переменные: dnaDigits и dnaModulus. Первая отвечает за количество символов в “ДНК” нашего хомяка. Вторая — dnaModulus — впоследствии позволит ограничить любое uint значение количеством символов из переменной dnaDigits.
“**” — знак возведения в степень.
Дальше нужно задать “скелет” будущего хомяка — шаблон, который содержит все, что нам нужно знать о хомяке: его имя и “ДНК”.
Такой шаблон задается ключевым словом struct. Hamster — имя шаблона.
Свойство name — имя хомяка — задается типом переменной string.
Свойство dna — “ДНК” будущего хомяка — задается типом переменной uint.
Новым хомякам нужно будет где-то жить. Для этого мы создаем и объявляем массив hamsters. В нем будут храниться все объекты, которые подходят под шаблон (struct) Hamster, который мы создали раньше.
Массивы нужны для объединения нескольких элементов. В Solidity используется два типа массивов: фиксированные и динамические. Фиксированный массив имеет определенный размер (length).
Массив с фиксированным размером 5, способный вместить 5 элементов записывается так:
uint[2] fixedArray
В этой строке сначала задается тип данных в массиве (uint), затем в квадратных скобках объявляется размер массива ([2]), затем — имя массива (fixedArray).
Динамический массив задается почти так же, но без указания размера в квадратных скобках:
uint[] dynamicArray
Мы используем динамический массив с именем hamsters, который должен включать объекты типа Hamster. Параметр public позволяет всем желающим получить доступ к содержимому массива — хомякам — снаружи контракта.
Здесь мы объявлем одну из функций контракта. В Solidity все функции начинаются со слова function.
_createHamster — имя функции. По соглашению между разработчиками с нижнего подчеркивания начинаются все “приватные” (private) функции.
В круглых скобках “()” содержатся параметры функции. В нашем случае мы задаем два параметра: один — строка string с именем name хомяка и целое неотрицательное целое число uint с “ДНК” dna хомяка.
В фигурных скобках {} содержится тело функции — инструкции, которые будут выполняться при вызове функции.
Переменная с именем id типа uint — это адрес создаваемого хомяка в массиве hamsters. Метод .push добавляет новый элемент в конец массива hamsters. Инструкция hamsters.push возвращает длину массива. Отнимаем от длины массива 1 и получаем порядковый номер хомяка.
Ключевое слово emit вызывает событие NewHamster. Такой способ вызова используется с версии Solidity 0.4.21. До этого события вызывались аналогично функциям, что стало одной из причин взлома TheDAO и хардфорка в сети Ethereum.
Как и в прошлом шаге, здесь мы задаем новую приватную функцию с именем _generateRandomDna и единственным параметром с именем _str типа string.
Параметр _str — строка с именем хомяка.
Эта функция должна вызываться только изнутри нашего контракта. Для этого нужен параметр private.
returns (uint) говорит о том, что функция должна вернуть целое неотрицательное число.
В фигурных скобках — тело функции.
uint rand — переменная типа uint с именем rand. Значение переменной rand — это результат хеширования строки с именем хомяка (_str), полученной на вход.
Процедура (abi.encodePacked(_str)) переводит строку _str в байтовое представление, которое потом хешируется через keccak256 и приводится к типу uint.
return rand % dnaModulus; — команда, которая заставляет функцию выдать результат деления по модулю (%) числа rand на число dnaModulus. Благодаря модульному делению длина результата, который функция вернет, всегда равна значению переменной dnaDigits. В нашем случае — 16 символов.
Два предыдущих шага описывают внутренние “служебные” функции нашего контракта. К ним нельзя обратиться извне контракта, они вызываются другими инструкциями внутри контракта.
Параметр _str — строка с именем хомяка.
Эта функция должна вызываться только изнутри нашего контракта. Для этого нужен параметр private.
В нашем случае к ним обращается функция createRandomHamster — публичная (public) функция, которую может вызвать любой желающий извне контракта. Эта функция вызывает все инструкции для создания нового хомяка с именем _name: функцию генерации “ДНК” хомяка _generateRandomDna по введенному имени (_name) и функцию создания самого хомяка по имени и “ДНК” (_name, randDna)
Как и в Solidity, структурная единица кода в Sophia — контракт.
Сначала контракт нужно объявить ключевым словом contract и дать ему имя. В нашем случае имя контракта — CryptoHamster. Знак = в конце строки показывает, что дальше задан код контракта.
В отличие от Solidity в Sophia не используются фигурные скобки. Вместо этого иерархия задается отступами, как в Python.
События (event) в Sophia похожи на события в Solidity.
Для создания и описания событий используется ключевая фраза datatype event. После знака “=” начинается описание события. NewHamster — имя события, а (indexed int, string, indexed int) — три типа параметров этого события.
Параметры событий в Sophia могут иметь от 0 до 3 индексированных (indexed) полей. Такие поля должны иметь тип, эквивалентный 32-байтному (например, bool, int или address).
Cобытия также могут содержать необязательное неиндексированное сообщение типа string.
Чтобы данные контракта сохранялись от вызова к вызову, контракту нужно задать состояние (state), которое записывается в блокчейн.
В нашем случае в глобальном состоянии контракта хранится свойство hamsters, которое содержит таблицу соответствий строки с именем хомяка типа string “ДНК” хомяка - значения типа int, а также свойство next_id типа int.
В Sophia тип переменной указывается уже после имени, что отличает его от Solidity и многих других мейнстримных языков программирования.
Функция init задает изначальное состояние (state) контракта. Благодаря атрибуту public эту функцию можно вызвать извне при создании контракта.
Все функции, которые как-то меняют состояние (state) контракта, должны быть помечены ключевым словом stateful.
Результат выполнения функции — изначальное, “чистое” состояние контракта — записывается в блокчейн.
В этом случае после вызова функции состояние (state) будет содержать пустой объект (record) hamsters и свойство next_id равное 0.
Функции в Sophia тоже объявляются ключевым словом function, за которым задается имя функции. Наша функция будет называться createHamster.
После этого следует параметр функции — hamsterName типа string. После знака = на строку ниже задается тело функции.
createHamsterByNameDNA(hamsterName, generateRandomDNA()) — вызов функции createHamsterByNameDNA с двумя параметрами: hamsterName и generateRandomDNA(hamsterName).
Второй параметр функции createHamsterByNameDNA() — результат вызова функции generateDNA(). Это особенность многих функциональных языков — в них функция может играть роль параметра.
Публичная функция с именем getHamsterDNA принимает на вход параметр hamsterName типа string и возвращает значения типа int.
При выполнении функции мы обращаемся к свойству hamsters, который находится в глобальном состоянии контракта (state).
Операция state.hamsters[hamsterName] запрашивает данные из глобального состояния контракта (state), описанного в самом начале.
Приватная функция createHamsterByNameDNA служит для создания хомяка на основе двух параметров: имени (name) типа string и “ДНК” (dna) типа int.
В теле функции при помощи встроенной в язык функции Chain.event() вызывается описанное выше событие NewHamster.
При помощи оператора put мы обновляем состояние (state) нашего контракта: присваиваем конкретному хомяку, выбранному при помощи hamsters[name], его “ДНК” и добавляем 1 к значению переменной next_id.
Приватная функция generateDNA() имеет единственный параметр name типа string и возвращает значение типа int.
String.sha3(name) — это встроенный метод объекта string, который хеширует входящую строку name.
Затем результат хеширования делится по модулю на 1016.
В Sophia знаки арифметических операций отличаются от Solidity. В последнем деление по модулю обозначается при помощи знака “%”, в то время как Sophia использует оператор mod. Аналогично, знак возведения в степень в Solidity выглядит как “**”, а Sophia использует более математическое обозначение “^”.
uint dnaModulus = 10 ** dnaDigits;
string name;
uint dna;
}
uint id = hamsters.push(Hamster(_name, _dna)) - 1;
emit NewHamster(id, _name, _dna);
}
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
uint randDna = _generateRandomDna(_name);
_createHamster(_name, randDna);
}
createHamsterByNameDNA(hamsterName, generateDNA(hamsterName))
state.hamsters[hamsterName = 0]
Chain.event(NewHamster(state.next_id, name, dna))
put(state{hamsters[name] = dna, next_id = (state.next_id + 1)})
String.sha3(name) mod 10 ^ 16
Что дальше?
Мы разобрали код смарт-контракта, который преобразует текстовую строку с именем в сид для создания уникального криптохомяка. Опубликуйте этот код в блокчейне и вы получите децентрализованное приложение, аналогичное коду в основе нашего генератора хомяков.
Теперь, как и договаривались, вы можете сохранить, распечатать и выслать друзьям своего криптохомяка. И, конечно, мы не ограничиваем вас только одним
Послесловие
Язык программирования — это прежде всего инструмент со своими особенностями и спектром применения. В случае с Sophia самые явные особенности — нативная поддержка системы оракулов и каналов состояний в сети æternity. Кроме того, Sophia остается одним из немногих функциональных языков, задействованных в разработке смарт-контрактов.
В этом спецпроекте мы коснулись только малой части возможностей и функций Sophia и Solidity на упрощенном наглядном примере. Рассказать о всех подробностях разработки в одном материале невозможно, Рассказать о всех подробностях разработки в одном материале невозможно. В этом спецпроекте мы коснулись только малой части возможностей и функций Sophia и Solidity на упрощенном примере. На самом деле с разработкой смарт-контрактов все куда интереснее, но об этом в следующих спецпроектах.