Bitcoin in a nutshell — Protocol

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


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


    meme


    Book



    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, так что здесь все должно быть понятно. Для ускорения процесса возьмем вот этот набор инструментов для Bitcoin, написанный самим Виталиком Бутериным, хотя при желании вы можете воспользоваться уже написанными фрагментами кода.


    $ 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'

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


    Searching for nodes


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


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


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

    Они захардкожены в chainparams.cpp и командой 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, регистрируемся, авторизуемся и в строке поиска пишем port:8333. Это стандартный порт для bitcoind, в моем случае нашлось примерно 9.000 нод:


    shodan


    Version handshake


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


    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



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


    В коде будет постоянно использоваться библиотека struct. Она отвечает за то, чтобы представлять параметры в правильном формате. Например 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)) * "\00"
        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


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


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


    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 — во втором случае просто используется специальная кодировка для экономии места:


    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 = ('\04' + vk.to_string()).encode("hex")
    sign = sk.sign_digest(hashed_raw_tx, sigencode=ecdsa.util.sigencode_der)

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


    sigscript = sign + "\01" + 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, подробнее про нее вы можете почитать здесь или посмотреть доклад с Black Hat USA 2014 — Bitcoin Transaction Malleability Theory in Practice.


    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 (в противном случае был бы reject message). Интересный факт — каждая нода, при получении свежей транзакции проверяет ее на валидность (процесс описан в Bitcoin in a nutshell — Mining), поэтому если вы где-то ошиблись, то вас об этом мгновенно оповестят:


    success


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


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


    msg_block


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


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



    • +14
    • 16,2k
    • 3
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 3
    • 0

      Здесь, похоже, ошибка — лишняя s после ключа:


      BTW нет никакой разницы между привычным OP_DUP OP_HASH160 dab3cccc50d7ff2d1d2926ec85ca186e61aef105 OP_EQUALVERIFY OP_CHECKSIG и 76a914dab3cccc50d7ff2d1d2926ec85ca186e61aef105s88ac
      • 0
        Надеялся прочитать про дерево Меркля, но ни тут ни в предыдущей статье ничего не было на эту тему (
        • 0
          Это будет в следующей главе про Blockchain :)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.