- PVSM.RU - https://www.pvsm.ru -
Транзакции — это чуть ли не самый "главный" объект в сети Bitcoin, да и в других блокчейнах тоже. Поэтому я решил, что если и писать про них целую главу, то тогда нужно рассказать и показать вообще все, что можно. В частности то, как они строятся и работают на уровне протокола.
Ниже я объясню, каким образом формируется транзакция, покажу как она подписывается и продемонстрирую механизм общения между нодами.
Для начала создадим новую пару ключей и адрес. Как это делается я рассказывал в главе 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, так что теперь можно экспериментировать по полной программе.
Вообще говоря, это хорошая задача на подумать: как найти других участников сети при том, что сеть децентрализована? Подробнее про это можете почитать здесь [5], скажу заранее, совсем децентрализованного решения пока что не существует.
Я покажу два способа. Первый — это DNS seeding. Суть в том, что есть некоторые доверенные адреса, такие как:
Они захардкожены в 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 нод:
Установка соединения между нодами начинается с обмена двумя сообщениями. Первым отправляется 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
Это делается в первую очередь для того, чтобы ноды узнали, какой версией протокола пользуется их "собеседник" и могли общаться на одном языке.
Каждое сообщение в сети должно представляться [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. Теперь, когда соединение установлено, можно начинать работу.
Перед созданием транзакции еще раз открываем спецификацию [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
...
Теперь самое время подписать транзакцию, здесь все довольно просто:
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
Здесь нужно пояснить одну деталь. Я думаю вы понимаете, зачем мы вообще подписываем транзакции. Это делается для того, чтобы никто не смог изменить наше сообщение и отправить его дальше по сети, потому что изменится подпись сообщения и так далее.
Но если вы внимательно читали, то запомнили, что мы подписываем ненастоящую транзакцию, которая в конечном итоге будет отправлена другим нодам, а ее модификацию, где в 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, то вам ничего не грозит. В противном случае стоит быть аккуратным.
Отправка транзакции в сеть производится точно так же, как и в случае с 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]), поэтому если вы где-то ошиблись, то вас об этом мгновенно оповестят:
Уже через несколько секунд после отправления транзакции в сеть, ее можно будет отследить [20], правда сначала она будет числиться неподтвержденной. Потом, спустя какое-то время (вплоть до нескольких часов), транзакция будет включена в блок.
Если вы к тому времени не закроете Wireshark плюс в сообщении version укажете текущую высоту блокчейна, то вам прийдет уведомление о новом блоке в виде все того же inv message, но на этот раз с TYPE = MSG_BLOCK
(я его закрыл, поэтому ниже скриншот из блога Ken Shirriff [21]):
В 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
Нажмите здесь для печати.