📜 ⬆️ ⬇️

Bitcoin in a nutshell - Protocol

Transactions are almost the most "main" object in the Bitcoin network, and in other blockchains too. So I decided that if I wrote a whole chapter about them, then I had to tell and show everything that was possible. In particular, how they are built and work at the protocol level.


Below I will explain how a transaction is formed, show how it is signed and demonstrate the mechanism of communication between nodes.


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


First, create a new key pair and address. How this is done, I told in the chapter Bitcoin in a nutshell - Cryptography , so here everything should be clear. To speed up the process, let's take this bitcoin toolkit , written by Vitalik Buterin himself , although if you wish, you can use already written code snippets .


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

I dropped to the address 1LwPhYQi4BRBuuyWSGVeb6kPrTqpSVmoYz 0.00012 BTC, so now you can experiment with the full program.


Searching for nodes


Generally speaking, this is a good task to think about: how to find other members of the network, while the network is decentralized? You can read more about this here , I will say in advance, there is no decentralized solution yet.


I will show two ways. The first is DNS seeding . The bottom line is that there are some trusted addresses, such as:



They are hardcovered in chainparams.cpp and the nslookup command can get node addresses from them.


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

The other method is not so clever and is not used in practice, but for educational purposes it is even better suited. Go to Shodan , register, log in, and in the search bar, write port:8333 . This is the standard port for bitcoind , in my case there were about 9.000 nodes:


shodan


Version handshake


Establishing a connection between nodes begins with the exchange of two messages. The version message is sent first, and the verack message is used as the response. Here is an illustration of the version handshake process from the Bitcoin wiki :


If you are on a remote peer, you will receive a 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

This is done primarily to ensure that nodes know which version of the protocol their “interlocutor” uses and can communicate in the same language.


Setting up a connection



Each message on the network should be represented as magic + command + lenght + checksum + payload , the makeMessage function is responsible for this. We still will use this function when we send transaction.


The code will constantly use the struct library. It is responsible for presenting the parameters in the correct format. For example, struct.pack("q", timestamp) writes the current UNIX time to a long long int , as required by the protocol.


 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 

Now open Wireshark, set a bitcoin filter or tcp.port == 8333 and look at the resulting packets. If everything is done correctly, then, first of all, the protocol, user-agent , block start height and so on will be correctly defined. Secondly, as promised, you will receive a response in the form of version and verack messages . Now that the connection is established, you can begin work.


wireshark


Making transaction


Before creating a transaction, open the specification again and follow it closely. Deviation by 1 byte already makes the transaction invalid, so you need to be extremely careful.


To begin with, let's set addresses, private key and transaction hash, to which we will refer:


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

Next, create a transaction in raw form, that is, unsigned so far. To do this, simply follow the specifications:


 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) 

Note that in the tx_in["script"] field it is not written as <Sig> <PubKey> , as you probably expected. Instead, a blocking exit script is specified , which we refer to , in our case it is OP_DUP OP_HASH160 dab3cccc50d7ff2d1d2926ec85ca186e61aef105 OP_EQUALVERIFY OP_CHECKSIG .


BTW, there is no difference between the usual OP_DUP OP_HASH160 dab3cccc50d7ff2d1d2926ec85ca186e61aef105 OP_EQUALVERIFY OP_CHECKSIG


 0x76 = OP_DUP 0xa9 = OP_HASH160 0x14 =   14   dab3cccc50d7ff2d1d2926ec85ca186e61aef105s88ac ... 

Signing transaction


Now it's time to sign the transaction, everything is pretty simple here:


 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) 

After the raw transaction signature is obtained, you can replace the unlocking script with the real one and bring the transaction to its final form:


 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


Here we need to clarify one detail. I think you understand why we generally sign transactions. This is done so that no one can change our message and send it further through the network, because the message signature will change, and so on.


But if you carefully read, remember that we are signing a false transaction, which will eventually be sent to other nodes, and its modification, where the unlocking script indicates the locking script from the output that we refer to. In principle, it is clear why this happens: this signature must be written into a real unlocking script, and a vicious circle is obtained: a correct unlocking script is needed for a correct signature, a correct signature is needed for a correct unlocking script. So Satoshi compromised and allowed the use of not quite “real” signatures.


Therefore, it may happen that someone on the network catches our message, changes the unlocking script and sends the edited message further. None of the nodes will be able to verify this, because the signature does not "protect" the unlocking script. This vulnerability is called Transaction malleability , you can read more about it here or watch the report from Black Hat USA 2014 - Bitcoin Transaction Malleability Theory in Practice .


TL; DR If you use standard scripts like P2PKH, then nothing threatens you. Otherwise it is worth being careful.


Sending transaction


Sending a transaction to the network is done in the same way as in the case of the 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) 

Run the resulting code and run to look at the packages. If everything is done correctly, then an inv message will come to your message (otherwise there would be a reject message ). An interesting fact is that every node, when it receives a fresh transaction, checks it for validity (the process is described in Bitcoin in a nutshell - Mining ), so if you make a mistake somewhere, then you will be instantly notified about this:


success


Within a few seconds after the transaction is sent to the network, it will be possible to track it , though at first it will be listed as unconfirmed. Then, after some time (up to several hours), the transaction will be included in the block.


If by that time you do not close Wireshark plus, in the version message you will indicate the current height of the blockchain, then you will receive a notification about the new block in the form of the same inv message , but this time with TYPE = MSG_BLOCK (I closed it, so the screenshot below is from the blog Ken Shirriff ):


msg_block


In Data hash you can see a long line, which is actually the title of a new block in little endian form. In this case, this is block # 279068 with the title 0000000000000001a27b1d6eb8c405410398ece796e742da3b3e35363c2219ee . A bunch of leading zeros is not an accident, but the result of mining, which I will discuss separately.


But before that you need to deal with the blockchain itself, the blocks, their titles, and so on. Therefore the next chapter: Bitcoin in a nutshell - Blockchain




')

Source: https://habr.com/ru/post/319862/


All Articles