Подписывайтесь

и не пропускайте новые спецпроекты!

Криптохомяки: смарт-контракт изнутри

 

Криптохомяки: смарт-контракт изнутри

Для разработки блокчейн-протоколов и распределенных приложений используются разные языки и парадигмы программирования. Одни системы поддерживают «классические» языки, такие как Python, C и JavaScript. Другие — внедряют собственные языки для разработки смарт-контрактов вроде Solidity в сети Ethereum.

Язык программирования Sophia от æternity — другой пример собственного языка программирования в блокчейн-проекте. Sophia относится к функциональным языкам программирования, о которых мы писали в одном из прошлых спецпроектов.

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

Но главное — сделаете собственного криптохомяка, которого можно будет распечатать и повесить на холодильник сразу после прочтения этого материала.

Это один из нескольких спецпроектов ForkLog, подготовленных при поддержке æternity. В прошлых совместных материалах мы рассказали о функциональном программировании в блокчейн-разработке и о роли ASIC-майнинга в криптоиндустрии.

Спонсор спецпроекта — æternity — блокчейн-платформа третьего поколения для создания децентрализованных приложений и масштабируемых смарт-контрактов. Блокчейн æternity взаимодействуют с внешним миром через оракулы — специальные узлы, которые получают и верифицируют информацию извне сети. Кроме того, в æternity работает система ончейн-управления, построенная на принципах «жидкой демократии» (liquid democracy).

Блокчейн æternity написан на функциональном языке программирования Erlang, а для разработки тьюринг-полных смарт-контрактов команда проекта разработала собственный функциональный язык Sophia. Согласно документации æternity, благодаря особенностям функциональной парадигмы Sophia упрощает создание корректного структурированного кода, в котором проще отследить побочные эффекты.

Присоединиться к русскоязычному Telegram-чату æternity:
Присоединиться

Разбор смарт-контрактов

Sophia — функциональный язык программирования, родственный ReasonML. Команда æternity разработала Sophia для создания тьюринг-полных смарт-контрактов для виртуальных машин æternity и Ethereum. Solidity же относится к императивным языкам программирования и больше похож на JavaScript.

С точки зрения пользователя примеры контрактов на Sophia и Solidity работают одинаково: запрашивают имя и выводят на экран изображение хомяка. С точки зрения разработчика сходство не такое сильное, но скоро вы сами все увидите.

Для начала введите имя для хомяка:

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

Solidity
Sophia

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 использует более математическое обозначение “^”.

pragma solidity ^0.4.25;
contract CryptoHamster { … }
event NewHamster(uint hamsterId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Hamster {
    string name;
    uint dna;
}
Hamster[] public hamsters;
function _createHamster(string _name, uint _dna) private {
     uint id = hamsters.push(Hamster(_name, _dna)) - 1;
     emit NewHamster(id, _name, _dna);
}
function _generateRandomDna(string _str) private returns (uint) {
    uint rand = uint(keccak256(abi.encodePacked(_str)));
    return rand % dnaModulus;
}
function createRandomHamster(string _name) public {
    uint randDna = _generateRandomDna(_name);
    _createHamster(_name, randDna);
}
contract CryptoHamster =
datatype event = NewHamster(indexed int, string, indexed int)
record state = { hamsters : map(string, int), next_id : int }
public stateful function init() = { hamsters = {}, next_id = 0 }
public function createHamster(hamsterName: string) =
    createHamsterByNameDNA(hamsterName, generateDNA(hamsterName))
public function getHamsterDNA(hamsterName: string) : int =
    state.hamsters[hamsterName = 0]
private function createHamsterByNameDNA(name: string, dna: int) =
    Chain.event(NewHamster(state.next_id, name, dna))
    put(state{hamsters[name] = dna, next_id = (state.next_id + 1)})
private function generateDNA(name : string) : int =
    String.sha3(name) mod 10 ^ 16
Шаг 1
/
Шаг 2
/
Шаг 3
/
Шаг 4
/
Шаг 5
/
Шаг 6
/
Шаг 7
/
Шаг 8
/
Шаг 9
Шаг 1
/
Шаг 2
/
Шаг 3
/
Шаг 4
/
Шаг 5
/
Шаг 6
/
Шаг 7
/
Шаг 8
Чтобы создать нового хомяка, введите новое имя

Что дальше?

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

Теперь, как и договаривались, вы можете сохранить, распечатать и выслать друзьям своего криптохомяка. И, конечно, мы не ограничиваем вас только одним

Присоединиться к русскоязычному Telegram-чату æternity:
Присоединиться

Послесловие

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

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

Подписывайтесь

и не пропускайте новые спецпроекты!

Автор — Кшиштоф Шпак
Редактор — Татьяна Оттер
Дизайнер — Дмитрий Бойко
Разработчик — Александр Володарский
Проектный менеджер — Константин Голубев
Руководитель — Влад Лихута