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

В то же время я убедился, что использование Common Lisp каким-то образом будет полезно как для Tinka, так и для меня, и мы решили пойти на это и разработать на нем наше программное обеспечение.

Краткое (почти) резюме

Далее следует много контента (особенно кода), и я подумал, что было бы неплохо включить краткое описание:

  • В этом посте описаны подробности библиотеки Common Lisp для взаимодействия с блокчейном Ethereum, eth-cl
  • eth-cl была написана в контексте игрового проекта (точнее, интерактивной истории)
  • Целью было поговорить с Ethereum на самом низком уровне, через json-rpc.
  • Библиотека включает в себя следующие возможности: создание учетной записи, развертывание контракта, взаимодействие с контрактом (т. е. вызовы функций для развернутых контрактов) и работа с базовыми транзакциями.

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

Сделай сам

При решении новой проблемы (в играх или где-то еще) часто возникает соблазн использовать стороннее программное обеспечение. Во многих случаях это имеет смысл: зачем изобретать велосипед?

Хотя иногда полезно сделать это самостоятельно.

Примерно через неделю изучения Web3 (и Ethereum в частности) мы решили, что лучше всего попытаться создать небольшую игру, работающую на блокчейне (на самом деле это могло быть частью руководства по ethereum. орг). Нужно было многое раскрыть с технической точки зрения, и часто нет ничего лучше, чем создавать работающее программное обеспечение для обучения.

Я написал очень простую PvP-игру «Крестики-нолики» и запустил ее в тестовой сети. Было здорово увидеть, как что-то работает, и почувствовать процесс развертывания чего-то в цепочке.

Однако была одна проблема: поскольку очень многое было абстрагировано библиотеками, которые я использовал (web3.py, а также довольно большим количеством различных фреймворков), я на самом деле очень мало понимал, как то, что я написал, работало под капотом. . Например, мне было очень трудно по-настоящему понять взаимосвязь между потреблением газа и транзакциями.

Поскольку я был убежден, что нам нужно действительно четко понять, как работает Ethereum, я решил написать библиотеку для взаимодействия с цепочкой. Конечно, это будет сделано с использованием Common Lisp.

Требования

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

Идея заключалась в том, чтобы предоставить авторам инструменты и конвейеры для создания этих интерактивных историй и их самостоятельной публикации в блокчейне, а также предоставить конечным пользователям клиентов для просмотра, покупки и «прослушивания» этих книг. Дополнительный контент (например, изображения) должен был быть загружен на IPFS для создания действительно децентрализованной платформы.

Требования к библиотеке были относительно простыми (и очевидными, поскольку это своего рода «оно» с точки зрения того, что означает взаимодействие с Ethereum, поскольку реальные вещи происходят в смарт-контрактах):

  • Создание учетных записей (мы хотели делать бесплатные игры, поэтому нужны были «временные» адреса Ethereum)
  • Создание контрактов из скомпилированного кода Solidity
  • Вызов функций этих контрактов и декодирование ответов.
  • Развертывание контрактов в Ethereum

Хотя мы пытались сделать как можно больше с нуля, некоторые вещи оказались слишком сложными:

  • Параметры кодирования и декодирования. Это было просто то, на что я не нашел времени, так как нужно было многое сделать.
  • Подписание сделок. Криптографическая библиотека Ironclad, которую мы использовали, не раскрывала все, что нам нужно, и я изо всех сил пытался внести в нее изменения, чтобы это сделать.

Первоначально мы добились этого, используя Geth в локальной частной цепочке, но мне не удалось заставить его подписывать транзакции для других цепочек. В итоге я использовал урезанную версию web3.py (eth_keys.backends.native.ecdsa для подписи необработанных транзакций и eth_abi для параметров кодирования и декодирования) и разговаривал с ней через микросервис (конечно, правильным способом было бы каким-то образом поговорить с Common Lisp на Python, но это было намного проще, поскольку мы использовали библиотеку только в контексте контейнеров Docker, развернутых на AWS).

Счет

Аккаунт — это приватный ключ secp256k1 (соответствующая литература есть здесь), из которого мы можем получить публичный ключ и адрес.

При использовании Ironclad создание нового закрытого ключа сводилось к простому вызову (ironclad:make-private-key :secp256k1 :x (ironclad:random-data 32)).

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

Вот полный код:

Затем мы можем использовать следующие три простые функции для доступа к данным:

ПКП

Поскольку мы хотели напрямую общаться с цепочкой, мы собирались сделать это через интерфейс json-rpc. Благодаря макросам Common Lisp я мог легко генерировать функции, вызывающие различные RPC (используя Dexador для отправки HTTP-запросов и jzon для кодирования и декодирования в JSON).

Затем я смог определить функции RPC следующим образом:

Например, eth_getBalance расширяется до:

Это был нижний уровень библиотеки (на самом деле существует существующая библиотека web3.lisp, выполняющая то же самое).

Транзакции

Углубление транзакций было полезно по трем причинам:

  • Это позволило мне взглянуть на правильное определение транзакции и ее формат (а также понять различные версии).
  • Я смог гораздо лучше понять газ, вместо того, чтобы пытаться отправлять различные значения, пока он не заработает.
  • Это позволило мне увидеть, как они выглядят в необработанном виде, что полезно, поскольку при игре с Ethereum часто приходится иметь дело с длинными шестнадцатеричными строками. Умение сказать, что они представляют, очень полезно.

Первым шагом было определение структуры данных, в которой будут храниться объекты транзакции. Как обычно, я начал со списка свойств и в конечном итоге перешел к классу. Формат основан на определении, указанном здесь, т.е. транзакции EIP-1559.

Как видите, это просто раскрывает поля из этого определения. При создании нового экземпляра транзакции мы инициализируем max-fee-per-gas из данных цепочки:

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

Отправка транзакции в цепочку производилась в два этапа:

  • Во-первых, установите nonce и оцените стоимость газа (eth/get-transaction-count и eth/estimate-gas в полезных данных транзакции).
  • Во-вторых, закодируйте и отправьте транзакцию

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

Первое, что нужно было сделать, — это сгладить транзакцию, по сути, объединив все ее данные в одну шестнадцатеричную строку. Это то, что делает transaction->flat.

Затем эта сведенная транзакция должна быть закодирована RLP. Код кажется немного загадочным (без каламбура), но на самом деле он довольно прост. Это скорее вопрос следования документации, чем чего-либо еще, и кодирования параметров в правильном формате.

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

Получить хеш транзакции тогда просто:

Наконец, функция encode-transaction:

После того как мы закодировали нашу транзакцию, мы можем отправить ее с помощью eth/send-raw-transaction. Это абстрагировано в следующих общих функциях и методах:

Одной из причин использования методов была возможность специализировать поведение для определенных типов транзакций: например, нам нужно было поддерживать то, что мы называли «параллельными» транзакциями, и гарантировать, что nonce всегда будет уникальным, что выглядело так:

Мы также хотели иметь возможность блокировать майнинговые транзакции.

Я, конечно, также написал эквивалентные функции декодирования:

Контракты

Наконец, на самом высоком уровне у нас есть контракты. Нам нужно было добиться следующего:

  • Загрузка данных смарт-контракта из скомпилированных файлов Solidity
  • Развертывание контрактов в цепочке
  • Вызов функций по контрактам

Сначала мы помещаем данные контракта в определение класса:

Развертывание контракта, конечно, осуществляется посредством транзакции. Чтобы упростить работу, мы определяем класс deploy-transaction, который заполняет данные из скомпилированного контракта при создании экземпляра.

Чтобы создать deploy-transaction, нам нужно объединить байт-код контракта с закодированными параметрами его конструктора, если он есть (кодирование выполняется с помощью крошечного подмножества web3.py, о котором я упоминал выше). Самый интересный момент — это функция arguments, которая работает со структурами и выравнивает их.

Вызов функции по контракту абстрагирует две отдельные концепции:

  • Вызов функции pure или view приводит к вызову eth/call.
  • Вызов функции другого типа приводит к отправке новой транзакции в цепочку для майнинга.

call-transaction — это оболочка транзакции, которая вызывает функцию. Это сбивает с толку, поскольку он используется как для вызовов только для чтения (т. е. функций pure или view), так и для других. Создание транзакции вызова формирует нужную полезную нагрузку в зависимости от параметров, которые необходимо передать в функцию контракта.

«Клиентская» функция call сначала создает call-transaction и связанные с ним данные (большая часть работы связана с кодированием параметров). Затем вызывается eth/call, чтобы получить результат этой транзакции. Если вызов функции доступен только для чтения, это все, что нам нужно. В противном случае клиентский код должен отправить возвращенную транзакцию в цепочку для майнинга.

Как вы можете видеть, мы сигнализируем об ошибке, если транзакция вызывает функцию pure или view, поскольку в ее майнинге нет необходимости.

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

Заключение

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