Мы в компании NEO NSPCC делаем распределенное децентрализованное хранилище для экосистемы NEO. Мы находимся в Петербурге, и здесь рассказываем про блокчейн NEO, например, проводим хакатоны. На хакатонах я и мои коллеги наблюдали за участниками и заметили, как незнакомому с NEO инженеру сложно начать использовать его для практических целей — писать смарт-контракты. У сообщества невыдающаяся документация, а на русском ее нет вовсе. Я решила это исправить, и написала супер-подробный туториал для тех, кто хочет начать работать со смарт-контрактами NEO.
FAQ
В качестве пролога хочу привести наивные вопросы, которые возникали у меня и моих коллег, когда мы только начинали работать с NEO-блокчейном, и ответов на них. Можно рассматривать его как короткое описание той части экосистемы NEO, с которой соприкасается автор смарт-контрактов.
Что такое privnet? Какие еще бывают сети?
Privnet (private net) — это локально развернутый блокчейн NEO, предназначенный для тестов. В privnet’е для пользователя поднимается сеть из нескольких консенсус-узлов, вспомогательные сервисы, а еще есть консоль для работы с блокчейном (np-prompt) и тестовый кошелек с большим количеством токенов, NEO и GAS. В privnet можно протестировать свой смарт-контракт, понаблюдать за принятием блоков по консенсусу через Neo-Scan, потренироваться составлять транзакции — короче, поисследовать, как работает NEO.
В экосистеме NEO есть еще одна сеть — testnet. Testnet развернут примерно на пятнадцати узлах в США и Китае (посмотреть можно здесь, выбрать в правом верхнем углу testnet). Эта сеть, как можно понять из названия, предназначена для интеграционного тестирования, в основном, смарт-контрактов или регистрации своих токенов — монеток на базе NEO. В отличие от privnet’а, огороженной от внешнего мира песочницы, транзакции из testnet доступны публично, что дает возможность протестировать смарт-контракты в среде, более приближенной к реальности.
И, наконец, mainnet — основная сеть NEO-блокчейна, в которой совершаются транзакции и эмитируются, то есть выпускаются, реальные токены.
Почему узлов в privnet’е четыре, а не два и не 100500?
Для принятия блоков в цепочку NEO блокчейн использует алгоритм достижения распределенного консенсуса. Чтобы алгоритм начал работать, необходим кворум — не менее четырех узлов в сети.
Какие технологии использует NEO? На каких языках можно написать смарт-контракт?
Оригинальный NEO написан на C#. Есть реализации на Java и Python, мои коллеги работают над реализацией на Go. Смарт-контракты можно писать на C#, Java, Python, Javascript, и, для самых модных, на Kotlin.
Существует NEO-кошелек и NEO-нода для Windows, но в этой статье я буду рассматривать реализации под Linux, поскольку для разработки и дебага это наилучшее решение.
Поднятие окружения
На момент написания статьи самая поздняя стабильная версия NEO — 2.9.4. Развернем privnet из этого репозитория. Для поднятия окружения необходим установленный docker-compose.
Склонируем репозиторий и перейдем в нужную директорию:
git clone https://github.com/CityOfZion/neo-local && cd neo-local/
Дадим команду на поднятие окружения. Она запустит нужные контейнеры и подготовит консоль:
sudo make start
После поднятия должны увидеть вот такие docker-контейнеры:
- четыре бегущих консенсус-узла: neo-cli-privatenet-[1:4]
- монитор блокчейна и его инфраструктура: neo-scan-api, neo-scan-sync, postgres. По адресу http://127.0.0.1:4000 можно увидеть состояние локального блокчейна: созданные блоки, транзакции в них, адреса кошельков и т.п.
- форма для запроса тестовых токенов neo-local-faucet по адресу http://127.0.0.1:4002
- notifications_server: API-сервер, расположенный по адресу http://127.0.0.1:8080 и отдающий данные о состоянии блокчейна (по ссылке список доступных эндпоинтов)
- neo-python: консоль для взаимодействия с блокчейном, в которой будет происходить дальнейшая работа
При успешном запуске на экране должно быть так:
Видим версию NEO и лог о том, что создалась новая база для хранения блоков. Кроме этого, видим задеплоенный тестовый смарт-контракт.
Внизу экрана будет строка, показывающая, сколько блоков из ныне доступных вычитано. Что это означает? Существует понятие “высоты блокчейна”: это номер самого позднего блока в цепочке. Privnet создает блокчейн не с нуля, а запускается на заранее подготовленном блокчейне некоторой высоты (примерно шесть тысяч блоков). Эти блоки состоят из транзакций, которые приводят сеть в нужное состояние, до которого privnet и должен дойти, последовательно вычитывая блоки.
Подождем синхронизации блокчейна, то есть, пока числа слева и справа от слэша в строке станут одинаковыми. После этого можно начинать работать с консолью.
Откроем кошелек, чтобы оплачивать транзакции в сети (пароль coz):
wallet open neo-privnet.wallet
neo-privnet.wallet — это относительный путь к файлу с кошельком. После открытия кошелька можно выполнить команду wallet и увидеть его адрес в сети, публичный ключ и синхронизированные балансы токенов NEO и NEOGas (подробнее по ссылке).
Окружение поднято, теперь можно написать смарт-контракт и задеплоить его в сеть.
Альтернативный способ подготовить окружение
Промпт, в который мы попали после выполнения команды make start, представляет собой CLI на python, запущенный внутри docker-контейнера. Как попасть в промпт вручную, если вы, например, случайно вышли из контейнера? Зайдем в контейнер для работы с консолью:
sudo docker exec -it neo-python /bin/bash
В контейнере выполним команду
np-prompt -p
, которая инициализирует блокчейн и запустит промпт для взаимодействия с ним. Ключ -р означает, что мы стартуем клиент для работы сетью privnet.
Прежде, чем деплоить смарт-контракт, рекомендую выполнить команды
config sc-events on
config sc-debug-notify on
В результате логи блокчейна будут выводиться в консоль, что поможет понять, в каком состоянии находятся транзакции по деплойменту или вызову смарт-контракта.
Простой смарт-контракт
Предлагаю начать с контракта без параметров, который что-нибудь печатает в консоль. Пример такого контракта на python:
# neo-boa -- это компилятор смарт-контрактов, написанных на python, # в байт-коды виртуальной машины NEO;
# импортируем из него системный вызов Runtime.Notify;
# название вызова красноречиво описывает его функцию -- уведомлять
# пользователя о событиях в процессе выполнения контрактаfrom boa.interop.Neo.Runtime import Notify
def Main():
Notify("hello world")
Задеплоим смарт-контракт. Для этого скопировать файл с кодом в docker-контейнер, где запущена консоль:
sudo docker cp sc.py neo-python:/neo-python
В консоли выполним команду:
sc build /neo-python/sc.py
Она скомпилирует код контракта в байт-код виртуальной машины NEO. В результате получим файл в формате .avm.
Задеплоим контракт командой:
sc deploy /neo-python/sc.avm False False False 07 05
Остановимся подробнее на волшебных числах и булевых переменных. Это параметры деплоя смарт-контракта, смысл которых можно посмотреть, выполнив команду sc deploy help. Хочу обратить внимание на два последних параметра — это коды входных и выходных параметров контракта. Пройдя по ссылке и интерпретировав коды, можно обнаружить, что наш контракт на вход принимает строку, а возвращает массив байт. Это странно, поскольку функция Main() ничего не принимает, да и не возвращает тоже ничего. Логично было бы использовать параметр ff — Void, но с таким параметром задеплоить контракт интерпретатор не позволит. При вызове контракта можно не указывать никакого параметра, либо указывать пустую строку. Если вы все сделали правильно, вам предложат ввести имя контракта, ваши контактные данные и другую информацию. За деплоймент контракта нужно будет заплатить, поэтому вас попросят ввести пароль от кошелька.
Далее, нужно узнать хэш контракта — это шестнадцатеричное число, которое можно увидеть в логах при деплое контракта.
Вызвать контракт по его хэшу:
sc invoke 0xef87ab3f689fb591eaad0f72d88723eb79cf92ee
В логах видим, что в результате вызова контракта системный вызов Notify вывел в консоль “hello world”.
Смарт-контракт с хранением данных
Теперь сделаем контракт посложнее, будем сохранять данные о его вызове в блокчейне. Возьмем для примера такой код:
from boa.interop.Neo.Runtime import Log, Notify
from boa.interop.Neo.Storage import Get, Put, GetContext
def Main(op, args):
context = GetContext()
key = 'key'
if op == 'get':
value = Get(context, key)
Notify(["read from storage", value])
return value
if op == 'put':
if len(args) == 0:
Notify("i have no value to put")
return False
Put(context, key, args[0])
return True
Этот контракт умеет записывать и считывать данные по ключу “key”.
Задеплоим контракт командой:
sc deploy sc.avm True False False 0710 05
Обратите внимание, что эта команда для деплоя отличается от предыдущей: теперь параметр needs_storage мы указали как True.
Вызовем контракт командой:
sc invoke d8b7ede1ebca90b80aa4b709bbb538c33ee515c6 put [15]
Передаем параметры, которые указали при деплое контракта: строку и массив.
В логах видим число 15, которое передали как аргумент для put, только в шестнадцатиричном виде: x0f. В Results видим единицу — так интерпретируется строка return True из кода контракта.
Дождемся, пока данные запишутся в блокчейн, и попробуем получить их. Вызовем контракт с операцией get. Несмотря на то, что для получения значения по ключу параметр передавать не нужно, я все равно передаю пустой массив, поскольку контракт его ожидает.
sc invoke d8b7ede1ebca90b80aa4b709bbb538c33ee515c6 get []
В Results видим то самое число 15, которое сохраняли. Контракт работает, как и ожидается.
Отладка смарт-контракта
Напоследок расскажу о полезной команде sc build_run, которая позволяет проверить, корректно ли написан контракт, и вызвать его, при этом не отправляя транзакцию в блокчейн.
В параметры команде нужно передавать сначала аргументы для билда, а следом — аргументы для вызова контракта.
Для примера возьмем вышеозначенный контракт со стораджем. Вызовем его с неправильным набором аргументов (упущен массив после строки):
sc build_run sc.py True False False 0710 05 put
В результате видим длинный лог ошибок, который должен навести нас на мысль, что мы что-то делаем неправильно. Подобный лог мы получим, если попытаемся с-build-run-нить контракт с синтаксическими ошибками, с неверно используемыми системными вызовами — в общем, большинство проблем, которые могут возникнуть со смарт-контрактом в privnet, можно отловить при помощи отладки с build_run.