Доброго времени суток. За время работы над проектом Steepshot накопилось немало полезной информации по работе с API блокчейнов на базе Graphene (таких как Steem и Golos). Решил объединить все найденное и накопанное в одной статье, думаю эта информация будет полезна разработчикам которые хотят интегрировать свои приложения c блокчейном на основе Graphene, особенно учитывая то, что по плану документация появится только к началу 2018 года (см. Steemit roadmap).
Первое с чего начнем - определим параметры сети к которой подключаемся.
Параметры сети
WebSocket
Все взаимодействие между клиентом и сервером происходит с использование технологии веб-сокетов + в качестве обертки используется JSON-RPC (https://en.wikipedia.org/wiki/JSON-RPC)? что в сумме дает нам возможность асинхронного взаимодействия. Обе технологии достаточно хорошо описаны и реализованы на большинстве популярных языков.
Адреса для подключения:
- Steem - wss://steemd.steemit.com
- Golos - wss://ws.golos.io
chain_id
У каждой блокчейн сети есть свой уникальный 32-байтный ключ. Он передается при каждой транзакции в сеть, и входит в состав шифруемого сообщения. Таким образом в сеть защищена от случайных транзакций.
Идентификаторы сетей:
- Steem 0000000000000000000000000000000000000000000000000000000000000000
- Golos 782a3039b478c839e4cb0c941ff4eaeb7df40bdd68bd441afd444b9da763de12
Символические обозначения
Кроме ключей, блокчейны как правило различаются своими символическими обозначениями
Рабочее имя | Steemit | Golos |
---|---|---|
prefix | STM | GLS |
steem_symbol | STEEM | GOLOS |
sbd_symbol | SBD | GBG |
vests_symbol | VESTS **1 **2 | GESTS |
Они используются, в том числе и в транзакциях, при передаче значений типа money. Поэтому для языков, которые не поддерживают данный тип (как например C#) рекомендую написать свою реализацию, чтобы потом не складывать теплое с мягким.
Кроме того, для избежания ошибок в обработке дат и чисел связанных с настройками глобализации в Steepshot были сразу добавлены дополнительные параметры:
- CultureInfo - Стандартный C# класс для управления параметрами глобализации (необходим для корректной работы с датами, временем, числами)
- JsonSerializerSettings - то же, что и CultureInfo только используется в сторонней библиотеке Newtonsoft.Json для разбора ответов от блокчейн.
Общение с сетью
Все запросы в сеть можно разделить на 2 типа:
GET запросы - не изменяют состояния сети (работают только на чтение). Запросы могут проходить в произвольной форме и не требуют подписей, т.е. могут быть выполнены кем угодно даже из браузера. По следующим ссылкам можно посмотреть описание некоторых GET запросов, а также немного информации об отправляемом и принимаемом формате сообщений:
- https://steemit.com/steem/@klye/an-introduction-to-steemd-api-calls-functions-and-usage
- https://golos.id/ru--otkrytyij-kod/@asuleymanov/opisanie-api-golos-chast-1
- https://steemit.github.io/steemit-docs
- https://steemdb.com
- https://steemd.com
POST запросы – это запросы которые вносят изменения в сеть. Они должны быть оформлены в виде транзакции и подписаны пользовательским приватным ключом.
Рассмотрим более детально, что такое транзакция и как её сделать. Хороший пример о том, как построить транзакцию, описан в статье от @xeroc : https://steemit.com/steem/@xeroc/steem-transaction-signing-in-a-nutshell. Чтобы не повторять информацию из статьи, кратко опишу основные моменты и дополню примерами построения различных операций.
Итак, что же такое транзакция
Транзакция — минимальный неделимый блок информации, отправляемый клиентом на сервер с целью добавления в данных в блок.
Транзакция содержит в себе обязательные поля:
- ref_block_num — номер блока, беззнаковое число 16 бит.
- ref_block_prefix — префикс блока, беззнаковое число 32 бита.
- expiration — время жизни транзакции (обычно это 30 сек с момента формирования транзакции).
- operations — массив список отправляемых операций.
- extensions — массив возможных дополнительных параметров.
- signatures — массив подписей.
Как получить данные для заполнения полей
- перед составлением транзакций необходимо совершить get запрос на сервер.
Так выглядит сырой запрос через сокет:
{"method":"get_dynamic_global_properties","params":[],"jsonrpc":"2.0","id":0}
В ответ будет возвращен блок данных о текущем состоянии сети:
{
"id": 0,
"head_block_number": 13506599,
"head_block_id": "00ce18271e38c48379c4744702be5202d42b2d23",
"time": "2017-07-08T15:23:09",
"current_witness": "clayop",
"total_pow": 514415,
"num_pow_witnesses": 172,
"virtual_supply": "253041799.029 STEEM",
"current_supply": "251230822.919 STEEM",
"confidential_supply": "0.000 STEEM",
"current_sbd_supply": "3276055.783 SBD",
"confidential_sbd_supply": "0.000 SBD",
"total_vesting_fund_steem": "179261723.004 STEEM",
"total_vesting_shares": "370713143905.498356 VESTS",
"total_reward_fund_steem": "0.000 STEEM",
"total_reward_shares2": "0",
"pending_rewarded_vesting_shares": "226872178.104164 VESTS",
"pending_rewarded_vesting_steem": "109617.757 STEEM",
"sbd_interest_rate": 0,
"sbd_print_rate": 10000,
"average_block_size": 7086,
"maximum_block_size": 65536,
"current_aslot": 13564063,
"recent_slots_filled": "340282366920938463463374607431768211455",
"participation_count": 128,
"last_irreversible_block_num": 13506579,
"max_virtual_bandwidth": "5986734968066277376",
"current_reserve_ratio": 20000,
"vote_power_reserve_rate": 10
}
На основании этого набора заполним необходимые поля нашей транзакции:
ref_block_num = head_block_number & 0xffff = 6183 (13506599 = 0xCE1827 берем младшие 2 байта (& 0xffff) получаем 0x1827 = 6183
ref_block_prefix = head_block_id ( берем младшие байты с 12 по 15 и переводим в число) = 2210674718
(переводим строку в массив байт 0x00ce18271e38c48379c4744702be5202d42b2d23 и берем младшие байты с 12 по 15. 0x1e38c483 = 2210674718)
expiration = time + 30 сек. = "2017-07-08T15:23:39"
Остальные параметры заполняются пользовательскими данными.
operations
Типов операций достаточно много. Полный список можно посмотреть тут: https://github.com/steemit/steem/blob/master/libraries/protocol/include/steemit/protocol/operations.hpp.
Как видно из файла static_variant является перечислением. Т.е. у каждой операции есть свой порядковый номер. Это важно, т.к. он участвует в формировании подписи транзакции.
В виду большого количества операций, описание их всех (а также описание их полей) выходит за рамки данной статьи.
Сериализация транзакции
Перед отправкой транзакция шифруется приватным ключом пользователя. Но перед этим необходимо сформировать само шифруемое сообщение (что в может оказаться и не таким простым делом).
В качестве примера рассмотрим транзакцию, добавляющую бенефициара.
Пример сырого запроса к сокету:
{
"method": "call",
"params":
[ 3,
"broadcast_transaction",
{
"ref_block_num": 34294,
"ref_block_prefix": 3707022213,
"expiration": "2016-04-06T08:29:27",
"operations":
[
[
"comment_options",
{
"author": "author_test7",
"permlink": "permlink_test8",
"max_accepted_payout": "1000000.000 SBD",
"percent_steem_dollars": 10000,
"allow_votes": true,
"allow_curation_rewards": true,
"extensions":
[
[
0,
{ "beneficiaries":
[
{
"account": "account_test9",
"weight": 2000
},
{
"account": "account_test10",
"weight": 5000
}
]
}
]
]
}
]
],
"extensions": [],
"signatures": ["***********************************"]
}
],
"jsonrpc": "2.0",
"id": 0
}
Пример длинный, но он хорошо отображает сериализацию основных типов + сюрприз.
В сериализованном виде сообщение выглядит как последовательность байт:
0000000000000000000000000000000000000000000000000000000000000000f68585abf4dce7c8045701130c617574686f725f74657374370e7065726d6c696e6b5f746573743800ca9a3b000000000353424400000000102701010100020d6163636f756e745f7465737439d0070e6163636f756e745f746573743130881300
32 байта нулей впереди сообщения, не что иное как ключ сети (chain_id). Его нету в Json, но он участвует в шифровании.
Для большей наглядности разобьем сообщение на составляющие:
Итак, что же тут происходит.
Как можно заметить, в формировании сообщения не участвуют названия полей, что означает, что нельзя менять порядок их следования.
Все значения сериализуются согласно их типу данных:
Тип | Значение |
---|---|
bool | 1 byte |
byte | Как есть (1 byte) |
Int16 / UInt16 | 2 byte |
Int32 / Uint32 / float | 4 byte |
Int64 / Uint64 / double | 8 byte |
DateTime | Берется как число тиков начиная с 01.01.1970 4 byte |
Array*** | На выходе должен получиться массив байт состоящий из префикса (размер массива) и из непосредственно массива сообщения. См. подробности под таблицей***. |
String | 1. строку необходимо перевести в формат UTF8 после чего в байты. В C# это можно сделать одной командой Encoding.UTF8.GetBytes. 2. полученный массив обрабатываем как описано в рецепте для Array. |
Money ("1000000.000 SBD") | Состоит из: 1) значения 1000000000 - 8 bytes, 2) порядок точности 3 — 1 bytes, 3) наименование валюты - 7 bytes (в отличие от шифрования строк, не указывается длина слова, вместо этого размер резервируется заранее в количестве 7 bytes (независимо от длины названия)). Итого получается 16 байт. |
Operation и составные типы объектов | Все составные объекты записываются как последовательность полей простых типов. В качестве особого случае можно отметить объекты-операции, они кроме непосредственно полей (отображаемых в json) содержат поле типа операции (конкретные значения смотеть тут), которое используется только для сериализации. |
*** Array
Для этого:
1.) Получаем размер массива, полученное число переводим в байты и записываем в выходной массив. При этом для перевода числа в байты используется функция:
def varint(n):
""" Varint encoding """
data = b''
while n >= 0x80:
data += bytes([(n & 0x7f) | 0x80])
n >>= 7
data += bytes([n])
return data
Источник: https://github.com/xeroc/python-graphenelib/blob/master/graphenebase/types.py
2.) Последовательно переводим и добавляем в выходной массив все элементы входного массива
Создание подписи
Финальная часть обработки транзакции — это составление подписи по полученному сериализованному сообщению.
Под подписью понимают некий уникальный массив байт, который был получен при помощи алгоритма шифрования. В нашем случае используется ECDSA (Elliptic Curve Digital Signature Algorithm) под названием Secp256k1.
Есть несколько готовых реализаций данного алгоритма:
- https://github.com/sipa/secp256k1 – пока самый быстрый из найденных. Написан на Си
- http://www.bouncycastle.org
- https://github.com/Chainers/Cryptography.ECDSA
- https://github.com/warner/python-ecdsa
Как правило, на подписание подается не само сообщение, а его хэш. Так для steem/golos используется SHA256. А для для биткоина SHA256 применяется даже два раза подряд...
Полученную подпись (или подписи если необходимо использовать несколько пользовательских ключей) добавляем в поле "signatures" выходного сообщения.
На этом процесс формирования транзакции можно считать завершенным, осталось передать её серверу.
Отдельно отмечу, что в steem api есть метод verify_authority его удобно использовать для проверки реализации кода операций и валидации подписи транзакции без непосредственного добавления её в блок. Это может быть полезно для составления автоматизированных тестов.
Общий статус разработки .Net-библиотек для подписания транзакций
Название | Описание | Акт. версия |
---|---|---|
Cryptography.ECDSA | Реализация ECDSA для подписания транзакций | 2.0 |
Ditch | Создание и отправка транзакций в блокчейн | 2.1.2 |
Ранее опубликовано
(Прогресс работы команды по созданию opensource .NET библиотеки для подписания транзакций на Graphene блокчейнах)
- C# библиотека Ditch 2.0
- С# библиотека Cryptography.ECDSA 2.0/2.1
- (ANN) C# библиотека Ditch 1.0
- (ANN) С# библиотека Cryptography.ECDSA 1.0
Вы более основательно покопались в коде.
Я бы хотел как нибудь связаться с Вами. У меня есть вопросы на которые Вы надеюсь сможете мне более доходчиво ответить.
Приветствуем. Это очень просто. Пройдите по ссылке указанной внизу статьи. Мы отвечали и отвечаем на вопросы. Всегда интересно услышать отзывы.
Великолепный пост) Вот честно говоря как купил себе первый компьютер начал юзать VS и шарп. Но вскоре перекльчился на UNIX=)
За шарп спасибо
Заметил ошибку:
"наименование валюты - 10 bytes" -> должно быть -> "наименование валюты - 7 bytes"
и как следствие:
"в количестве 10 bytes" > "в количестве 7 bytes"
"Итого получается 19 байт." > "Итого получается 16 байт."
Статья исправлена
@steepshot Поздравляю! Вы добились некоторого прогресса на Голосе и были награждены следующими новыми бейджами:
Награда за общую выплату получил
Вы можете нажать на любой бейдж, чтобы увидеть свою страницу на Доске Почета.
Чтобы увидеть больше информации о Доске Почета, нажмите здесь
Если вы больше не хотите получать уведомления, ответьте на этот комментарий словом
стоп
chain_id можно с ноды взять
Да. Используя команду get_config там в ответе параметр STEEMIT_CHAIN_ID
Ваш пост поддержали следующие Инвесторы Сообщества "Добрый кит":
losos, cats, kibela, strecoza, fetta, svetlanaaa, midnight, dimarss, vik, shuler, vadbars, vasilisapor2, nefer, guepetto, renat242, romannn, romapush, drim, gryph0n, voltash, karusel1, exan, mahadev, bobrik, kvg, blondinka, prost, mixtura, bombo, mr-nikola, makcum52, mrramych, bospo, nerengot, sergiusduke, igrinov, ifingramota, foxycat
Поэтому я тоже проголосовал за него!
Узнать подробности о сообществе можно тут:
Разрешите представиться - Кит Добрый
Правила
Инструкция по внесению Инвестиционного взноса
Вы тоже можете стать Инвестором и поддержать проект!!!
Если Вы хотите отказаться от поддержки Доброго Кита, то ответьте на этот комментарий командой "!нехочу"