API part 4
Автор: @asuleymanov
В предыдущей части описания я обещал приоткрыть завесу тайны над тем как именно можно самому добавлять записи в блокчейн. Собственно я опишу команды из раздела Network_Brodcast_API и Login_API. И попробую рассказать о процедуре генерации транзакции, её подписи и отправки в блокчейн.

Команды раздела Network_Brodcast_API

broadcast_block Параметры:"method":"broadcast_block", "params":["signed_block"], "id":0 Описание: Скорее всего данная команда должна загружать в блокчейн собранный и подписанный блок. Но проверить его мне не удалось.
broadcast_transaction Параметры:"method":"broadcast_transaction", "params":["trx"], "id":1 Описание: Транзакция будет проверяться на достоверность в локальной базе данных до начала трансляции. Если он не может применяться локально, будет вызвана ошибка, и транзакция не будет транслироваться.
broadcast_transaction_synchronous Параметры:"method":"broadcast_transaction_synchronous", "params":["trx"], "id":2 Описание: Этот вызов не будет возвращен до тех пор, пока транзакция не будет включена в блок.
broadcast_transaction_with_callback Параметры:"method":"broadcast_transaction_with_callback", "params":["confirmationCallback","trx"], "id":3 Описание: Эта версия широковещательной транзакции регистрирует метод обратного вызова, который будет вызываться, когда транзакция включена в блок. Метод обратного вызова включает идентификатор транзакции, номер блока и номер транзакции в блоке.

Команды раздела Login_API

login Параметры:"method":"login", "params":["username","password"], "id":0 Описание: Позволяет подключаться к учетным записям в сети GOLOS.
get_api_by_name Параметры:"method":"get_api_by_name", "params":["apiname"], "id":1 Описание: Возвращает уникальный идентификатор API по его имени.Пример идентификатора "login_api" или "follow_api".
get_version Параметры:"method":"get_version", "params":[], "id":2 Описание: Возвращает данные о версии компонентов блокчейн

Процедура подписания и отправки

Для примера я возьму самую простую операцию голосования. С её помощью я разберу как происходит генерация транзакции и её подписание.

Операция

Первым шагом мы создаем операцию. Если посмотреть на саму операцию то она имеет вид
1
['vote',
2
{'author': 'author',
3
'permlink': 'permlink',
4
'voter': 'voter',
5
'weight': weight}]
Copied!
Тут можно четко определить:
    тип операции vote(голосование)
    собственно саму публикацию за которую производиться голосование (параметры author и permlink)
    кто голосует (параметр voter)
    и соответственно вес голоса который он готов отдать (параметр weight)

Транзакция

Следующим шагом мы создаем непосредственно транзакцию с необходимой нам операцией. Транзакция может содержать от одной до нескольких операций. В нашем случае это 1 операция голосования и сгенерированная транзакция выглядит так:
1
trx = {'ref_block_num': 36029,
2
'ref_block_prefix': 1164960351,
3
'expiration': '2017-06-13T12:24:17',
4
'operations': [['vote',
5
{'author': 'xeroc',
6
'permlink': 'piston',
7
'voter': 'xeroc',
8
'weight': 10000}]],
9
'extensions': [],
10
'signatures': [],
11
}
Copied!
Можно заметить что наша операция является частью массиваoperations. Давайте разберем те поля в транзакции которые у нас появились, но нам неизвестны.
    Параметр
    ref_block_num
    указывает на номер предыдущего блока.
    Параметр
    ref_block_prefix
    получаем из идентификатора блока, а именно последние 4 байта.
    Параметр
    expiration
    проставляет время истечения действия транзакции. Если до этого времени не произошло включение транзакции в блок, то она считается недействительной.
    Параметр
    extensions
    расширение.
    Параметр
    signatures
    собственно подпись. Данный параметр проставляется в момент подписания.
Примет получения параметровref_block_numиref_block_prefixна python:
1
DGP = get_dynamic_global_properties()
2
ref_block_num = DGP["head_block_number"]
3
&0xFFFF
4
ref_block_prefix = struct.unpack_from("<I", unhexlify(DGP["head_block_id"]), 4)[0]
Copied!
Цель этих двух параметров для предотвращения повторения запросов.

Сериализация (приведение к жесткой структуре)

Прежде чем переходить непосредственно к процессу подписи. Надо сериализовать (привести к жесткой структуре) транзакцию. Это необходимо чтобы на стороне ноды было проще проверить правильность подписания транзакции. Т.е. после такой обработки мы будем точно знать порядок в каком располагались поля при подписании. При этом из процесса сериализации исключается параметрsignaturesиextensions.
И так на примере нашей транзакции порядок будет таким (Все примеры будут на языке python):
    1.
    Создание буфера
    buf = b""
    2.
    Добавление в буфер параметра
    ref_block_num
    buf += struct.pack("<H", trx["ref_block_num"])
    3.
    Добавление в буфер параметра
    ref_block_prefix
    buf += struct.pack("<I", trx["ref_block_prefix"])
    4.
    Добавление в буфер параметра
    expiration
    . Сам параметр при этом преобразуется в целое число (uint32)
1
timeformat = '%Y-%m-%dT%H:%M:%S%Z'
2
buf += struct.pack("<I", timegm(time.strptime((trx["expiration"] + "UTC"), timeformat)))
Copied!
    1.
    Добавление в буфер количество операций в нашей транзакции
    buf += bytes(varint(len(trx["operations"])))
    2.
    Добавление непосредственно полей операции. Первое что добавляем это код операции. Он определяется из массива начинающегося с 0.
    Список операций из исходников
1
vote_operation,
2
comment_operation,
3
transfer_operation,
4
transfer_to_vesting_operation,
5
withdraw_vesting_operation,
6
limit_order_create_operation,
7
limit_order_cancel_operation,
8
feed_publish_operation,
9
convert_operation,
10
account_create_operation,
11
account_update_operation,
12
witness_update_operation,
13
account_witness_vote_operation,
14
account_witness_proxy_operation,
15
pow_operation,
16
custom_operation,
17
report_over_production_operation,
18
delete_comment_operation,
19
custom_json_operation,
20
comment_options_operation,
21
set_withdraw_vesting_route_operation,
22
limit_order_create2_operation,
23
challenge_authority_operation,
24
prove_authority_operation,
25
request_account_recovery_operation,
26
recover_account_operation,
27
change_recovery_account_operation,
28
escrow_transfer_operation,
29
escrow_dispute_operation,
30
escrow_release_operation,
31
pow2_operation,
32
escrow_approve_operation,
33
transfer_to_savings_operation,
34
transfer_from_savings_operation,
35
cancel_transfer_from_savings_operation,
36
custom_binary_operation,
37
decline_voting_rights_operation,
38
reset_account_operation,
39
set_reset_account_operation,
40
41
/// virtual operations below this point
42
fill_convert_request_operation,
43
author_reward_operation,
44
curation_reward_operation,
45
comment_reward_operation,
46
liquidity_reward_operation,
47
interest_operation,
48
fill_vesting_withdraw_operation,
49
fill_order_operation,
50
shutdown_witness_operation,
51
fill_transfer_from_savings_operation,
52
hardfork_operation,
53
comment_payout_update_operation
Copied!
Далее идут поля непосредственно из операции в определенном порядке. На данный момент я смог выявить порядок только для 3-х операций.
Операцияvote
1
(voter)
2
(author)
3
(permlink)
4
(weight)
Copied!
Операцияcomment(эта одна операция но может выполнять два действия. Непосредственно комментировать и создавать новую публикацию)
1
(parent_author) // если этот параметр пустой то это считается созданием новой публикации
2
(parent_permlink)
3
(author)
4
(permlink)
5
(title)
6
(body)
7
(json_metadata)
Copied!
В нашем случае это выглядит в таком виде :
1
if op[0] == "vote":
2
opdata = op[1]
3
buf += (varint(len(opdata["voter"])) + bytes(opdata["voter"], "utf-8"))
4
buf += (varint(len(opdata["author"])) + bytes(opdata["author"], "utf-8"))
5
buf += (varint(len(opdata["permlink"])) + bytes(opdata["permlink"], "utf-8"))
6
buf += struct.pack("<h", int(opdata["weight"]))
Copied!

Результат

Приблизительный разбор результата: Во- первых , параметр ref_block_num( 36029) bd8c..............................................................
и параметр ref_block_prefix( 1164960351) ....5fe26f45......................................................
Затем мы добавим время истечения транзакцииexpiration2016-08-08T12:24:17 ............f179a857..............................................
После этого нам нужно добавить количество операций (01) ....................01............................................
И непосредственно саму операцию: Идентификатор операции (00) ......................00..........................................
Голосующийvoter ........................057865726f63..............................
Автор публикацииauthor ....................................057865726f63..................
Ссылка на публикациюpermlink ................................................06706973746f6e....
и вес голоса10000 ..............................................................1027
Результат сериализации предстает приблизительно в таком виде: bd8c5fe26f45f179a8570100057865726f63057865726f6306706973746f6e1027

Подписание

И вот самая тяжелая часть всего процесса. Для меня это пока тоже темный лес, но попробую описать хоть как то. Для подписания транзакции нам понадобится:
    Сериализованный буфер
    Идентификатор цепи chainid (STEEM:
    0000000000000000000000000000000000000000000000000000000000000000
    ; GOLOS
    782a3039b478c839e4cb0c941ff4eaeb7df40bdd68bd441afd444b9da763de12
    )
    Секретный ключ (в нашем случае posting key)
Вместо того чтобы подписывать непосредственно саму транзакцию мы подпишем 256SHA хэш, так называемый дайджест сообщения. Получить его можно таким способом:
1
message = unhexlify(chainid) + buf
2
digest = hashlib.sha256(message).digest()
Copied!
Теперь мы используя секретные ключи подписываем нашу транзакцию. Каждый секретный ключ приведет к одной подписи , которая должна быть добавлена в параметр signatures первоначальной транзакции. В нашем случае, мы просто работаем с одним закрытым ключом, представленным в WIF. Получим фактический двоичный закрытый ключ от WIF (для простоты используется класс PrivateKey от steembase.account)
1
wifs = ["5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8"]
2
sigs = []
3
for wif in wifs:
4
p = bytes(PrivateKey(wif)) # binary representation of private key
5
sk = ecdsa.SigningKey.from_string(p, curve=ecdsa.SECP256k1)
Copied!
Теперь реализовываем цикл , который имеет решающее значение , поскольку система принимает только канонические подписи , и мы не имеем никакого способа узнать является ли подпись канонической. При этом мы получаем детерминированный параметр для подписания ECDSA и при создании этого параметра будем добавлять наш счетчик цикла в дайджест перед хэшированием.
1
cnt = 0
2
i = 0
3
while 1 :
4
cnt += 1
5
# Deterministic k
6
#
7
k = ecdsa.rfc6979.generate_k(
8
sk.curve.generator.order(),
9
sk.privkey.secret_multiplier,
10
hashlib.sha256,
11
hashlib.sha256(digest + bytes([cnt])).digest())
Copied!
Подпись генерируется с помощью соответствующего вызова подписи ECDSA:
1
# Sign message
2
3
sigder = sk.sign_digest(
4
digest,
5
sigencode=ecdsa.util.sigencode_der,
6
k=k)
Copied!
Теперь создаем подпись,и проверяем является ли она канонической. Если это так, то мы разрываем цикл и продолжаем:
1
# Reformating of signature
2
3
r, s = ecdsa.util.sigdecode_der(sigder, sk.curve.generator.order())
4
signature = ecdsa.util.sigencode_string(r, s, sk.curve.generator.order())
5
6
# Make sure signature is canonical!
7
8
lenR = sigder[3]
9
lenS = sigder[5 + lenR]
10
if lenR is 32 and lenS is 32 :
11
# ........
Copied!
После того, как мы обеспечили каноничность, мы добавляем дополнительные параметры. Это упрощает проверку подписи , так как она привязывает подпись к одному уникальному открытому ключу. Без этих параметров системе необходимо будет проверить несколько открытых ключей вместо одного. Мы добавляем 4 и 27 , чтобы осталась совместимость с другими протоколами и получаем нашу подпись.
1
# Derive the recovery parameter
2
3
i = recoverPubkeyParameter(digest, signature, sk.get_verifying_key())
4
i += 4 \# compressed
5
i += 27 \# compact
6
break
Copied!
После получения канонической подписи, мы форматируем её в шестнадцатеричном представлении и добавляем к нашей транзакции в параметрsignatures. Этот вид подписи , называется компактной подписью.
1
trx["signatures"].append(
2
hexlify(
3
struct.pack("<B", i) +
4
signature
5
).decode("ascii")
6
)
Copied!

Отправка

После довольно сложной процедуры подписания, отправка кажется элементарным действием. Для того чтобы быть уверенными что проблем при отправке нету лучше всего использовать командуbroadcast_transaction_synchronous(так как при её использовании система возвращает ответ), а параметр для команды будет являться непосредственноtrx. В ответе будет вот такая структура:
1
ID string `json:"id"`
2
BlockNum uint32 `json:"block_num"`
3
TrxNum uint32 `json:"trx_num"`
4
Expired bool `json:"expired"`
Copied!
Этим постом я завершаю цикл по разбору GOLOS API
Историческая справка
      Начало разбора команд из раздела Database_Api
      Окончание разбора команд из раздела Database_Api
      Разбор команд из разделов Market_History_API и Follow_API
Last modified 1yr ago