Практика разработки на C# для блокчейнов на Graphene

Programming Practice ru.PNG Доброго времени суток. За время работы над проектом 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 запросов, а также немного информации об отправляемом и принимаемом формате сообщений:

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
Table_d.PNG (переводим строку в массив байт 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, но он участвует в шифровании.
Для большей наглядности разобьем сообщение на составляющие:
Table_d2.PNG Итак, что же тут происходит.
Как можно заметить, в формировании сообщения не участвуют названия полей, что означает, что нельзя менять порядок их следования.
Все значения сериализуются согласно их типу данных:
Тип | Значение
———— | ————-
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
Money («1000000.000 SBD») | Состоит из: 1) значения 1000000000 — 8 bytes, 2) порядок точности 3 — 1 bytes, 3) наименование валюты — 10 bytes (в отличие от шифрования строк, не указывается длина слова, вместо этого размер резервируется заранее в количестве 10 bytes (независимо от длины названия)). Итого получается 19 байт.
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 блокчейнах)

source