Работаем с биткоином на Elixir. Часть 1
Недавно меня с головой захватил волшебный мир биткоин. Жажде знаний не было предела, и утолить её помогла замечательная книга «Mastering Bitcoin» Андреаса Антонопулоса и полное погружение в биткоин-разработку. Книга подробно освещает технические основы биткоин, но ничто так не помогает в изучении нового дела, как практика.
Простенькое приложение на Эликсире для управления полной биткоин-нодой и связи с ней через интерфейс JSON-RPC, по-моему, – отличный «Hello, World!». Поехали!
Где взять полную ноду
Чтобы установить соединение с полной нодой Bitcoin Core, нужно сначала где-то её раздобыть. Достаточно просто запустить свою ноду локально, ведь общедоступные ноды с открытым JSON-RPC интерфейсом можно пересчитать по пальцам.
Установим демона bitcoind
и настроим его в файле bitcoin.config
:
rpcuser=<username>
rpcpassword=<password>
Определяемые значения <username>
и <password>
будем использовать для аутентификации при отправке запросов к биткоин-ноде.
По завершении настройки самое время запустить полную ноду:
bitcoind -conf=<path to bitcoin.config> -daemon
Запустившись, демон полной ноды начнёт подключаться к пирам, загружать и верифицировать транзакции в блоках.
Убедимся, что всё работает как нужно:
bitcoin-cli getinfo
Данная команда возвратит основную информацию о ноде, включая её версию и количество полученных и верифицированных блоков. Загрузка и верификация всего блокчейна может занять несколько дней, ну а мы пока продолжим работать над нашим проектом.
Интерфейс JSON-RPC
Биткоин-нода работает через интерфейс JSON-RPC, который можно использовать для извлечения информации о блокчейне и взаимодействия с нодой.
Любопытно, что инструмент bitcoin-cli
, который мы использовали ранее для получения информации о ноде работает поверх JSON-RPC API. Список всех возможных RPC-команд ноды можно увидеть, вызвав bitcoin-cli help
или полистав Bitcoin Wiki.
Протокол JSON-RPC получает входящие команды через HTTP-сервер, а значит, можно обойтись без bitcoin-cli
и прописать эти RPC-команды самостоятельно.
Например, запустим getinfo вручную с помощью
curl`:
curl --data-binary '{"jsonrpc":"1.0","method":"getinfo","params":[]}'\
http://<user>:<pass>@localhost:8332/
Аналогичным образом можно выполнять такие команды в любой среде программирования с HTTP-клиентом, например, в Эликсире!
Разработка Эликсир-приложения
Продумав стратегию взаимодействия с полной биткоин-нодой, займёмся Эликсир-приложением.
Создадим новый проект и обновим mix.exs
, чтобы добавить в зависимости библиотеку poison
, которая понадобится для шифрования и расшифровки JSON-объектов, и httpoison
– один из лучших HTTP-клиентов для Эликсира.
defp deps do
[
{:httpoison, "~> 0.13"},
{:poison, "~> 3.1"}
]
end
Теперь, когда мы закончили с частью, отвечающей за кодогенерацию, перейдём к реализации взаимодействия с биткоин-нодой.
Начнём работать с модулем HelloBitcoin
и первым делом поставим заглушку для функции getinfo
:
defmodule HelloBitcoin do
def getinfo do
raise "TODO: Implement getinfo"
end
end
Для простоты будем взаимодействовать с этим модулем через iex -S mix
. Прежде чем приступить к следующему шагу, давайте убедимся, что всё работает правильно.
Вызов заглушки HelloBitcoin.getinfo
должен повлечь за собой исключение времени выполнения:
iex(1)> HelloBitcoin.getinfo
HelloBitcoin.getinfo
** (RuntimeError) TODO: Implement getinfo
(hello_bitcoin) lib/hello_bitcoin.ex:4: HelloBitcoin.getinfo/0
Отлично. Ошибка. Как и должно быть.
Построение команды GetInfo
Теперь наполним функцию getinfo
содержимым.
Повторюсь: нам необходимо послать HTTP-запрос методом POST
к HTTP-серверу биткоин-ноды (обычно слушающему по http://localhost:8332
) и передать JSON-объект, содержащий команду GetInfo
и необходимые параметры.
Оказалось, что httpoison
справляется с таким заданием в два счёта:
def getinfo do
with url <- Application.get_env(:hello_bitcoin, :bitcoin_url),
command <- %{jsonrpc: "1.0", method: "getinfo", params: []},
body <- Poison.encode!(command),
headers <- [{"Content-Type", "application/json"}] do
HTTPoison.post!(url, body, headers)
end
end
Сначала получим url
из ключа bitcoin_url
в конфигурации приложения. Адрес должен находиться в файле config/config.exs
и указывать на локальную ноду:
config :hello_bitcoin, bitcoin_url: "http://<user>:<password>@localhost:8332"
Далее, создадим словарь, представляющий нашу JSON-RPC-команду. В данном случае в поле method
прописываем "getinfo"
, а поле params
оставляем пустым. И последнее, сформируем тело запроса, преобразовав команду в формат JSON с помощью Poison.encode!
.
Вызов HelloBitcoin.getinfo
должен возвратить успешный ответ от биткоин-ноды с кодом состояния 200
, а также результат команды getinfo
в формате JSON:
%HTTPoison.Response{
body: "{\"result\":{\"version\":140200,\"protocolversion\":70015,\"walletversion\":130000,\"balance\":0.00000000,\"blocks\":482864,\"timeoffset\":-1,\"connections\":8,\"proxy\":\"\",\"difficulty\":888171856257.3206,\"testnet\":false,\"keypoololdest\":1503512537,\"keypoolsize\":100,\"paytxfee\":0.00000000,\"relayfee\":0.00001000,\"errors\":\"\"},\"error\":null,\"id\":null}\n",
headers: [{"Content-Type", "application/json"}, {"Date", "Thu, 31 Aug 2017 21:27:02 GMT"}, {"Content-Length", "328"}],
request_url: "http://localhost:8332",
status_code: 200
}
Прекрасно.
Расшифруем полученный JSON-текст в body
и получим результат:
HTTPoison.post!(url, body)
|> Map.get(:body)
|> Poison.decode!
Теперь результаты вызова HelloBitcoin.getinfo
, полученные от bitcoind
, будут представлены в более удобном виде:
%{"error" => nil, "id" => nil,
"result" => %{"balance" => 0.0, "blocks" => 483001, "connections" => 8,
"difficulty" => 888171856257.3206, "errors" => "",
"keypoololdest" => 1503512537, "keypoolsize" => 100, "paytxfee" => 0.0,
"protocolversion" => 70015, "proxy" => "", "relayfee" => 1.0e-5,
"testnet" => false, "timeoffset" => -1, "version" => 140200,
"walletversion" => 130000}}
Обратите внимание, что необходимые нам данные ("result"
), обернуты в словарь, содержащий метаданные о самом запросе. Эти метаданные содержат строку с возможной ошибкой и идентификатор запроса.
Перепишем функцию getinfo
так, чтобы она включала обработку ошибок и возвращала фактические данные в случае безошибочного выполнения запроса:
with url <- Application.get_env(:hello_bitcoin, :bitcoin_url),
command <- %{jsonrpc: "1.0", method: "getinfo", params: []},
{:ok, body} <- Poison.encode(command),
{:ok, response} <- HTTPoison.post(url, body),
{:ok, metadata} <- Poison.decode(response.body),
%{"error" => nil, "result" => result} <- metadata do
result
else
%{"error" => reason} -> {:error, reason}
error -> error
end
Теперь при отсутствии ошибок функция getinfo
будет возвращать кортеж {:ok, result}
, содержащий результат RPC-вызова, а в обратном случае мы получим кортеж {:error, reason}
с описанием ошибки.
Обобщение команд
В похожей манере можно реализовать и другие RPC-команды блокчейна, например, getblockhash
:
def getblockhash(index) do
with url <- Application.get_env(:hello_bitcoin, :bitcoin_url),
command <- %{jsonrpc: "1.0", method: "getblockhash", params: [index]},
{:ok, body} <- Poison.encode(command),
{:ok, response} <- HTTPoison.post(url, body),
{:ok, metadata} <- Poison.decode(response.body),
%{"error" => nil, "result" => result} <- metadata do
{:ok, result}
else
%{"error" => reason} -> {:error, reason}
error -> error
end
end
Вызвав getblockhash
с нулевым индексом, получим первый блок цепочки.
HelloBitcoin.getblockhash(0)
{:ok, "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"}
Функция getblockhash
работает верно, и она практически идентична функции getinfo
.
Чтобы избежать дублирования кода, выделим общую функциональную часть в новую вспомогательную функцию bitcoin_rpc
:
defp bitcoin_rpc(method, params \\ []) do
with url <- Application.get_env(:hello_bitcoin, :bitcoin_url),
command <- %{jsonrpc: "1.0", method: method, params: params},
{:ok, body} <- Poison.encode(command),
{:ok, response} <- HTTPoison.post(url, body),
{:ok, metadata} <- Poison.decode(response.body),
%{"error" => nil, "result" => result} <- metadata do
{:ok, result}
else
%{"error" => reason} -> {:error, reason}
error -> error
end
end
Теперь переопределим функции getinfo
и getblockhash
в соответствии с функцией bitcoin_rpc
:
def getinfo, do: bitcoin_rpc("getinfo")
def getblockhash(index), do: bitcoin_rpc("getblockhash", [index])
Можно видеть, что bitcoin_rpc
представляет собой полноценный RPC-интерфейс для биткоин, позволяющий с лёгкостью выполнять любые RPC-команды.
Если вам интересно попробовать осуществить всё вышеперечисленное на своей машине, то исходники проекта можно найти на GitHub.
Заключение
Ну вот и подошла к концу достаточно длинная статья, объясняющая относительно простую идею. Полная нода биткоин предоставляет интерфейс JSON-RPC, доступ к которому можно получить, используя любой язык (например, Эликсир) или стек. Биткоин-разработка – удивительно занимательная вещь, в которую интересно углубиться ещё сильнее.
Следующая часть серии статей о работе с биткоином на Эликсире по ссылке.