- PVSM.RU - https://www.pvsm.ru -

Bitcoin in a nutshell — Protocol

Транзакции — это чуть ли не самый "главный" объект в сети Bitcoin, да и в других блокчейнах тоже. Поэтому я решил, что если и писать про них целую главу, то тогда нужно рассказать и показать вообще все, что можно. В частности то, как они строятся и работают на уровне протокола.

Ниже я объясню, каким образом формируется транзакция, покажу как она подписывается и продемонстрирую механизм общения между нодами.

meme

Table of content

  1. Keys and address
  2. Searching for nodes
  3. Version handshake
  4. Setting up a connection
  5. Making transaction
  6. Signing transaction
  7. Sniff & spoof
  8. Sending transaction
  9. Links

Keys and address

Для начала создадим новую пару ключей и адрес. Как это делается я рассказывал в главе Bitcoin in a nutshell — Cryptography [1], так что здесь все должно быть понятно. Для ускорения процесса возьмем вот этот набор инструментов для Bitcoin [2], написанный самим Виталиком Бутериным [3], хотя при желании вы можете воспользоваться уже написанными фрагментами кода [1].

$ git clone https://github.com/vbuterin/pybitcointools
$ cd pybitcointools
$ sudo python setup.py install
$ python
Python 2.7.12 (default, Jul  1 2016, 15:12:24) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from bitcoin import *
>>> private_key = "28da5896199b85a7d49b0736597dd8c0d0c0293f130bf3e3e1d102e0041b1293"
>>> public_key = privtopub(private_key)
>>> public_key
'0497e922cac2c9065a0cac998c0735d9995ff42fb6641d29300e8c0071277eb5b4e770fcc086f322339bdefef4d5b51a23d88755969d28e965dacaaa5d0d2a0e09'
>>> address = pubtoaddr(public_key)
>>> address
'1LwPhYQi4BRBuuyWSGVeb6kPrTqpSVmoYz'

Я скинул [4] на адрес 1LwPhYQi4BRBuuyWSGVeb6kPrTqpSVmoYz 0.00012 BTC, так что теперь можно экспериментировать по полной программе.

Searching for nodes

Вообще говоря, это хорошая задача на подумать: как найти других участников сети при том, что сеть децентрализована? Подробнее про это можете почитать здесь [5], скажу заранее, совсем децентрализованного решения пока что не существует.

Я покажу два способа. Первый — это DNS seeding. Суть в том, что есть некоторые доверенные адреса, такие как:

  • bitseed.xf2.org
  • dnsseed.bluematt.me
  • seed.bitcoin.sipa.be
  • dnsseed.bitcoin.dashjr.org
  • seed.bitcoinstats.com

Они захардкожены в chainparams.cpp [6] и командой nslookup можно получить от них адреса нод.

$ nslookup bitseed.xf2.org
Non-authoritative answer:
Name:   bitseed.xf2.org
Address: 76.111.96.126
Name:   bitseed.xf2.org
Address: 85.214.90.1
Name:   bitseed.xf2.org
Address: 94.226.111.26
Name:   bitseed.xf2.org
Address: 96.2.103.25
...

Другой способ не такой умный и на практике не используется, но в учебных целях он подходит даже лучше. Заходим на Shodan [7], регистрируемся, авторизуемся и в строке поиска пишем port:8333. Это стандартный порт для bitcoind, в моем случае нашлось примерно 9.000 нод:

shodan

Version handshake

Установка соединения между нодами начинается с обмена двумя сообщениями. Первым отправляется version message [8], а в качестве ответа на него используется verack message [9]. Вот иллюстрация процесса version handshake из Bitcoin wiki [10]:

When the local peer L connects to a remote peer R, the remote peer will not send any data until it receives a version message.

  • L -> R Send version message with the local peer's version
  • R -> L Send version message back
  • R Sets version to the minimum of the 2 versions
  • R -> L Send verack message
  • L Sets version to the minimum of the 2 versions

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

Setting up a connection

Bitcoin in a nutshell — Protocol - 3

Каждое сообщение в сети должно представляться [11] в виде magic + command + lenght + checksum + payload, за это отвечает функция makeMessage. Этой функцией мы еще воспользуемся, когда будем отправлять транзакцию.

В коде будет постоянно использоваться библиотека struct [12]. Она отвечает за то, чтобы представлять параметры в правильном формате. Например struct.pack("q", timestamp) записывает текущее UNIX время в long long int, как этого и требует протокол.

import time
import socket
import struct
import random
import hashlib

def makeMessage(cmd, payload):
    magic = "F9BEB4D9".decode("hex") # Main network ID
    command = cmd + (12 - len(cmd)) * "0"
    length = struct.pack("I", len(payload))
    check = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
    return magic + command + length + check + payload

def versionMessage():
    version = struct.pack("i", 60002)
    services = struct.pack("Q", 0)
    timestamp = struct.pack("q", time.time())

    addr_recv = struct.pack("Q", 0)
    addr_recv += struct.pack(">16s", "127.0.0.1")
    addr_recv += struct.pack(">H", 8333)

    addr_from = struct.pack("Q", 0)
    addr_from += struct.pack(">16s", "127.0.0.1")
    addr_from += struct.pack(">H", 8333)

    nonce = struct.pack("Q", random.getrandbits(64))
    user_agent = struct.pack("B", 0) # Anything
    height = struct.pack("i", 0) # Block number, doesn't matter

    payload = version + services + timestamp + addr_recv + addr_from + nonce + user_agent + height

    return payload

if __name__ == "__main__":

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(("93.170.187.9", 8333))

    sock.send(makeMessage("version", versionMessage()))
    sock.recv(1024) # receive version message
    sock.recv(1024) # receive verack message

Теперь открываем Wireshark, ставим фильтр bitcoin или tcp.port == 8333 и смотрим на получившиеся пакеты. Если все сделано верно, то, во-первых, будет верно определен протокол, user-agent, block start height и так далее. Во вторых, как и обещалось, вам прилетит ответ в виде сообщений version и verack. Теперь, когда соединение установлено, можно начинать работу.

wireshark

Making transaction

Перед созданием транзакции еще раз открываем спецификацию [13] и внимательно ее придерживаемся. Отклонение на 1 байт уже делает транзакцию невалидной, так что нужно быть предельно аккуратным.

Для начала зададим адреса, приватный ключ и хэш транзакции [4], на которую мы будем ссылаться:

previous_output = "60ee91bc1563e44866c66937b141e9ef4615a272fa9d764b9468c2a673c55e01"
receiver_address = "1C29gpF5MkEPrECiGtkVXwWdAmNiQ4PBMH"
my_address = "1LwPhYQi4BRBuuyWSGVeb6kPrTqpSVmoYz"
private_key = "28da5896199b85a7d49b0736597dd8c0d0c0293f130bf3e3e1d102e0041b1293"

Далее создадим транзакцию в raw виде, то есть пока что неподписанную. Для этого достаточно просто следовать спецификации:

def txnMessage(previous_output, receiver_address, my_address, private_key):
    receiver_hashed_pubkey= base58.b58decode_check(receiver_address)[1:].encode("hex")
    my_hashed_pubkey = base58.b58decode_check(my_address)[1:].encode("hex")

    # Transaction stuff
    version = struct.pack("<L", 1)
    lock_time = struct.pack("<L", 0)
    hash_code = struct.pack("<L", 1)

    # Transactions input
    tx_in_count = struct.pack("<B", 1)
    tx_in = {}
    tx_in["outpoint_hash"] = previous_output.decode('hex')[::-1]
    tx_in["outpoint_index"] = struct.pack("<L", 0)
    tx_in["script"] = ("76a914%s88ac" % my_hashed_pubkey).decode("hex")
    tx_in["script_bytes"] = struct.pack("<B", (len(tx_in["script"])))
    tx_in["sequence"] = "ffffffff".decode("hex")

    # Transaction output
    tx_out_count = struct.pack("<B", 1)

    tx_out = {}
    tx_out["value"]= struct.pack("<Q", 1000) # Send 1000 satoshis
    tx_out["pk_script"]= ("76a914%s88ac" % receiver_hashed_pubkey).decode("hex")
    tx_out["pk_script_bytes"]= struct.pack("<B", (len(tx_out["pk_script"])))

    tx_to_sign = (version + tx_in_count + tx_in["outpoint_hash"] + tx_in["outpoint_index"] +
                  tx_in["script_bytes"] + tx_in["script"] + tx_in["sequence"] + tx_out_count +
                  tx_out["value"] + tx_out["pk_script_bytes"] + tx_out["pk_script"] + lock_time + hash_code)

Заметьте, что в поле tx_in["script"] написано отнюдь не <Sig> <PubKey>, как вы, наверное, ожидали. Вместо этого указан блокирующий скрипт выхода, на который мы ссылаемся, в нашем случае это OP_DUP OP_HASH160 dab3cccc50d7ff2d1d2926ec85ca186e61aef105 OP_EQUALVERIFY OP_CHECKSIG.

BTW нет никакой разницы между привычным OP_DUP OP_HASH160 dab3cccc50d7ff2d1d2926ec85ca186e61aef105 OP_EQUALVERIFY OP_CHECKSIG и 76a914dab3cccc50d7ff2d1d2926ec85ca186e61aef105s88ac — во втором случае просто используется специальная кодировка [14] для экономии места:

0x76 = OP_DUP
0xa9 = OP_HASH160
0x14 = далее следует 14 байт информации
dab3cccc50d7ff2d1d2926ec85ca186e61aef105s88ac
...

Signing transaction

Теперь самое время подписать транзакцию, здесь все довольно просто:

hashed_raw_tx = hashlib.sha256(hashlib.sha256(tx_to_sign).digest()).digest()
sk = ecdsa.SigningKey.from_string(private_key.decode("hex"), curve = ecdsa.SECP256k1)
vk = sk.verifying_key
public_key = ('4' + vk.to_string()).encode("hex")
sign = sk.sign_digest(hashed_raw_tx, sigencode=ecdsa.util.sigencode_der)

После того, как получена подпись для raw transaction, можно заменить unlocking script на настоящий и привести транзакцию к окончательному виду:

sigscript = sign + "1" + struct.pack("<B", len(public_key.decode("hex"))) + public_key.decode("hex")

real_tx = (version + tx_in_count + tx_in["outpoint_hash"] + tx_in["outpoint_index"] +
struct.pack("<B", (len(sigscript) + 1)) + struct.pack("<B", len(sign) + 1) + sigscript +
tx_in["sequence"] + tx_out_count + tx_out["value"] + tx_out["pk_script_bytes"] + tx_out["pk_script"] + lock_time)

return real_tx

Sniff & spoof

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

Но если вы внимательно читали, то запомнили, что мы подписываем ненастоящую транзакцию, которая в конечном итоге будет отправлена другим нодам, а ее модификацию, где в unlocking script указан locking script из выхода, на который мы ссылаемся. В принципе понятно, почему это происходит: в настоящий unlocking script должна быть записана эта самая подпись, и получается замкнутый круг: для правильной подписи нужен правильный unlocking script, для правильного unlocking script нужна правильная подпись. Так что Сатоши пошел на компромисс и разрешил пользоваться не совсем "настоящими" подписями.

Поэтому может случится так, что кто-нибудь в сети поймает наше сообщение, изменит unlocking script и отправит отредактированное сообщение дальше. Никто из нод не сможет этого проверить, потому что подпись не "защищает" unlocking script. Эта уязвимость называется Transaction malleability, подробнее про нее вы можете почитать здесь [15] или посмотреть доклад с Black Hat USA 2014 — Bitcoin Transaction Malleability Theory in Practice [16].

TL;DR Если вы пользуетесь стандартными скриптами вроде P2PKH, то вам ничего не грозит. В противном случае стоит быть аккуратным.

Sending transaction

Отправка транзакции в сеть производится точно так же, как и в случае с version message:

if __name__ == "__main__":

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(("70.68.73.137", 8333))

    sock.send(makeMessage("version", versionMessage()))
    sock.recv(1024) # version
    sock.recv(1024) # verack

    # Transaction options
    previous_output = "60ee91bc1563e44866c66937b141e9ef4615a272fa9d764b9468c2a673c55e01"
    receiver_address = "1C29gpF5MkEPrECiGtkVXwWdAmNiQ4PBMH"
    my_address = "1LwPhYQi4BRBuuyWSGVeb6kPrTqpSVmoYz"
    private_key = "28da5896199b85a7d49b0736597dd8c0d0c0293f130bf3e3e1d102e0041b1293"

    txn = txnMessage(previous_output, receiver_address, my_address, private_key)
    print "Signed txn:", txn

    sock.send(makeMessage("tx", txn))
    sock.recv(1024)

Запускаем получившийся код и бежим смотреть на пакеты. Если все сделано верно, то в качестве ответа на ваше сообщение придет inv message [17] (в противном случае был бы reject message [18]). Интересный факт — каждая нода, при получении свежей транзакции проверяет ее на валидность (процесс описан в Bitcoin in a nutshell — Mining [19]), поэтому если вы где-то ошиблись, то вас об этом мгновенно оповестят:

success

Уже через несколько секунд после отправления транзакции в сеть, ее можно будет отследить [20], правда сначала она будет числиться неподтвержденной. Потом, спустя какое-то время (вплоть до нескольких часов), транзакция будет включена в блок.

Если вы к тому времени не закроете Wireshark плюс в сообщении version укажете текущую высоту блокчейна, то вам прийдет уведомление о новом блоке в виде все того же inv message, но на этот раз с TYPE = MSG_BLOCK (я его закрыл, поэтому ниже скриншот из блога Ken Shirriff [21]):

msg_block

В Data hash вы можете видеть длинную строку, которая на самом деле является заголовком нового блока в little endian форме. В данном случае это блок #279068 [22] с заголовком 0000000000000001a27b1d6eb8c405410398ece796e742da3b3e35363c2219ee. Куча ведущих нулей — не случайность, а результат майнинга, о котором я расскажу отдельно.

Но перед этим вам нужно разобраться с самим блокчейном, блоками, их заголовками и так далее. Поэтому следующая глава: Bitcoin in a nutshell — Blockchain [19]

Автор: Pavlov_dog

Источник [29]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/235064

Ссылки в тексте:

[1] Bitcoin in a nutshell — Cryptography: https://habrahabr.ru/post/319868/

[2] набор инструментов для Bitcoin: https://github.com/vbuterin/pybitcointools

[3] Виталиком Бутериным: https://ru.wikipedia.org/wiki/%D0%91%D1%83%D1%82%D0%B5%D1%80%D0%B8%D0%BD,_%D0%92%D0%B8%D1%82%D0%B0%D0%BB%D0%B8%D0%BA

[4] скинул: https://blockchain.info/tx/60ee91bc1563e44866c66937b141e9ef4615a272fa9d764b9468c2a673c55e01

[5] здесь: http://bitcoin.stackexchange.com/questions/3536/how-do-bitcoin-clients-find-each-other

[6] chainparams.cpp: https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L120

[7] Shodan: https://shodan.io

[8] version message: https://en.bitcoin.it/wiki/Protocol_documentation#version

[9] verack message: https://en.bitcoin.it/wiki/Protocol_specification#verack

[10] Bitcoin wiki: https://en.bitcoin.it/wiki/Version_Handshake

[11] должно представляться: https://en.bitcoin.it/wiki/Protocol_documentation#Message_structure

[12] struct: https://docs.python.org/2/library/struct.html

[13] спецификацию: https://en.bitcoin.it/wiki/Protocol_specification#tx

[14] специальная кодировка: https://github.com/richardkiss/pycoin/blob/master/pycoin/tx/script/opcodes.py#L29

[15] здесь: https://en.bitcoin.it/wiki/Transaction_Malleability

[16] Bitcoin Transaction Malleability Theory in Practice: https://www.youtube.com/channel/UCbbgnifxfH-nqx6z9XQ963Q

[17] inv message: https://en.bitcoin.it/wiki/Protocol_documentation#inv

[18] reject message: https://en.bitcoin.it/wiki/Protocol_documentation#reject

[19] Bitcoin in a nutshell — Mining: #

[20] отследить: https://blockchain.info/ru/address/1LwPhYQi4BRBuuyWSGVeb6kPrTqpSVmoYz

[21] Ken Shirriff: http://www.righto.com/

[22] #279068: https://blockchain.info/ru/block-height/279068

[23] Bitcoins the hard way: Using the raw Bitcoin protocol: http://www.righto.com/2014/02/bitcoins-hard-way-using-raw-bitcoin.html

[24] Analyzing Bitcoin Network Traffic Using Wireshark: https://www.samkear.com/networking/analyzing-bitcoin-network-traffic-wireshark

[25] How the Bitcoin protocol actually works: http://www.michaelnielsen.org/ddi/how-the-bitcoin-protocol-actually-works/

[26] How does a Bitcoin node find its peers?: https://www.quora.com/Bitcoin-How-does-a-Bitcoin-node-find-its-peers

[27] Bitcoin Developer Reference. P2P network: https://bitcoin.org/en/developer-reference#p2p-network

[28] Redeeming a raw transaction step by step: http://bitcoin.stackexchange.com/questions/32628/redeeming-a-raw-transaction-step-by-step-example-required

[29] Источник: https://habrahabr.ru/post/319862/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best