Rede (Networking)

Como conectar e se comunicar com um nó da rede Bitcoin

O código introdutório desta página funciona para nós até a v26.2.

O Bitcoin Core v27.0 (lançado em abril de 2024) e acima usam o protocolo versão 2 (BIP 324) por padrão. As mensagens subjacentes são as mesmas; a diferença é que agora elas são criptografadas, o que este guia não cobre.

Se você roda um nó v27.0 ou acima, ainda pode se comunicar com ele usando o código de exemplo desta página definindo -v2transport=0 (desabilitando o protocolo v2 e rodando o antigo protocolo v1 no lugar).

Aqui está um guia rápido de como conectar e se comunicar com um nó da rede Bitcoin.

Animação de terminal mostrando uma conexão a um nó bitcoin e as mensagens sendo enviadas.

0. Introdução

O Bitcoin é um programa de computador. Você pode baixá-lo de graça.

Ele roda em uma porta aberta no seu computador, o que significa que qualquer um pode se conectar a ele e se comunicar com ele pela Internet.

Diagrama mostrando uma conexão a um computador por uma porta.
Os computadores se conectam uns aos outros por "portas". O Bitcoin usa a porta 8333 por padrão.

Quando você roda o Bitcoin, ele usa portas para se conectar a outros computadores rodando o mesmo programa. Então, quando você tem muitas pessoas rodando o Bitcoin, acaba com uma rede de computadores conectados e se comunicando uns com os outros.

Diagrama mostrando nós na rede Bitcoin se comunicando uns com os outros.
Os computadores na rede Bitcoin compartilham as transações e os blocos mais recentes uns com os outros.

De todo jeito, a coisa legal do Bitcoin é que você pode escrever o seu próprio programa básico para se conectar a um nó, se quiser. Você só precisa saber falar a língua dele.

Neste guia, vou te mostrar como conectar a um nó bitcoin usando Ruby. Ruby é uma linguagem simples, então você deve conseguir traduzir o código para a linguagem que preferir usar. Eu pessoalmente gosto de Ruby.

E pode acreditar: se eu consigo conectar a um nó Bitcoin, qualquer um consegue.

1. Conectando

Diagrama mostrando uma conexão a um nó Bitcoin pela porta 8333 e um IP local.

Primeiro de tudo, dois fatos rápidos que você precisa saber sobre o programa Bitcoin:

Então tudo o que você precisa para conectar a um nó Bitcoin é o endereço IP do computador onde ele está rodando e a capacidade de fazer conexões TCP a partir da sua linguagem de programação. Por exemplo:

# Sockets fazem parte da biblioteca padrão do Ruby
require 'socket'

# Abre uma conexão TCP para um IP e uma porta
socket = TCPSocket.open("162.120.69.182", 8333) # computador local = 127.0.0.1

E pronto, temos uma conexão a um nó Bitcoin.

Mas isso por si só é bastante chato. Para começar a receber dados (como transações e blocos de verdade), você precisa começar enviando algumas mensagens primeiro.

TCP = Transmission Control Protocol. É só uma forma de dois computadores se comunicarem pela Internet (um diz olá primeiro, o outro responde olá de volta, etc.). Por exemplo, seu computador usou TCP quando você baixou esta página. Outro protocolo é o UDP, mas esse é menos comum. Você não precisa saber como esses protocolos funcionam: tudo o que precisa saber é que o Bitcoin usa TCP.

2. Mensagens

Uma "mensagem" é só um pedaço estruturado de dados que os nós Bitcoin enviam uns aos outros pela rede. Todas elas têm o mesmo formato:

Diagrama de uma mensagem de rede sendo enviada de um nó Bitcoin para outro.

Aqui está um exemplo de como uma mensagem Bitcoin real se parece:

Cabeçalho: F9BEB4D976657273696F6E0000000000550000002C2F86F3
Payload:   7E1101000000000000000000C515CF6100000000000000000000000000000000000000000000FFFF2E13894A208D000000000000000000000000000000000000FFFF7F000001208D00000000000000000000000000

Isso parece um amontoado de jargão agora, mas vai fazer sentido daqui a pouco.

Quando você constrói uma mensagem para enviar a outro nó, basicamente está pegando dados normais legíveis por humanos (como números e texto) e os convertendo em bytes legíveis por computador, que podem ser enviados pela rede de forma mais eficiente.

Portanto, o truque para enviar mensagens no Bitcoin é só colocar um monte de dados no formato correto.

Então vou começar mostrando a você a estrutura básica de um cabeçalho e de um payload de mensagem, e depois vou mostrar como construir um você mesmo. Vou usar uma mensagem do tipo "version" como primeiro exemplo, pois é a primeira mensagem que você quer enviar a um nó Bitcoin após conectar a ele.

A mensagem “version” fornece informações sobre o nó transmissor ao nó receptor no início de uma conexão. Até que ambos os pares tenham trocado mensagens “version”, nenhuma outra mensagem será aceita.
developer.bitcoin.org

Version

Cabeçalho (Header)

O cabeçalho contém um resumo da mensagem, e sua estrutura é a mesma para toda mensagem no protocolo Bitcoin.

Aqui está como um cabeçalho se parece para uma mensagem "version":

Cabeçalho: (mensagem version)
┌─────────────┬──────────────┬───────────────┬───────┬─────────────────────────────────────┐
│ Nome        │ Dados Ex.    │ Formato       │ Tam.  │ Bytes                               │
├─────────────┼──────────────┼───────────────┼───────┼─────────────────────────────────────┤
│ Magic Bytes │              │ bytes         │     4 │ F9 BE B4 D9                         │
│ Comando     │ "version"    │ bytes ascii   │    12 │ 76 65 72 73 69 6F 6E 00 00 00 00 00 │
│ Tamanho     │ 85           │ little-endian │     4 │ 55 00 00 00                         │
│ Checksum    │              │ bytes         │     4 │ F7 63 9C 60                         │
└─────────────┴──────────────┴───────────────┴───────┴─────────────────────────────────────┘
Campos

Payload

O payload contém o conteúdo principal da mensagem. Tipos diferentes de mensagem têm estruturas diferentes para seus payloads.

Aqui está o payload de uma mensagem "version":

Payload (mensagem version):
┌───────────────────────┬─────────────────────┬────────────────────────────┬─────────┬─────────────────────────────────────────────────┐
│ Nome                  │ Dados Exemplo       │ Formato                    │    Tam. │ Bytes Exemplo                                   │
├───────────────────────┼─────────────────────┼────────────────────────────┼─────────┼─────────────────────────────────────────────────┤
│ Versão do Protocolo   │ 70014               │ little-endian              │       4 │ 7E 11 01 00                                     │
│ Services              │ 0                   │ bit field, little-endian   │       8 │ 00 00 00 00 00 00 00 00                         │
│ Tempo                 │ 1640961477          │ little-endian              │       8 │ C5 15 CF 61 00 00 00 00                         │
│ Services (Remoto)     │ 0                   │ bit field, little-endian   │       8 │ 00 00 00 00 00 00 00 00                         │
│ IP Remoto             │ 46.19.137.74        │ ipv6, big-endian           │      16 │ 00 00 00 00 00 00 00 00 00 00 FF FF 2E 13 89 4A │
│ Porta Remota          │ 8333                │ big-endian                 │       2 │ 20 8D                                           │
│ Services (Local)      │ 0                   │ bit field, little-endian   │       8 │ 00 00 00 00 00 00 00 00                         │
│ IP Local              │ 127.0.0.1           │ ipv6, big-endian           │      16 │ 00 00 00 00 00 00 00 00 00 00 FF FF 7F 00 00 01 │
│ Porta Local           │ 8333                │ big-endian                 │       2 │ 20 8D                                           │
│ Nonce                 │ 0                   │ little-endian              │       8 │ 00 00 00 00 00 00 00 00                         │
│ User Agent            │ ""                  │ compact size, ascii        │ compact │ 00                                              │
│ Último Bloco          │ 0                   │ little-endian              │       4 │ 00 00 00 00                                     │
└───────────────────────┴─────────────────────┴────────────────────────────┴─────────┴─────────────────────────────────────────────────┘

O campo Services é um bit field e o User Agent usa compact size. O Tamanho usa o formato little-endian.

Uma mensagem "version" é uma das mensagens mais complexas que você pode enviar no Bitcoin, mas isso só porque ela contém muita informação. Ainda assim, é um bom lugar para começar, porque, se você consegue construir uma mensagem "version", consegue construir qualquer mensagem do protocolo Bitcoin.

Campos

Aqui está o que cada um dos campos individuais significa para esta mensagem em particular:

Como eu disse, esta é uma das mensagens mais complicadas, então não deixe que ela te desencoraje de tentar conectar a um nó. Dê uma chance.

Código

Aqui está um código de exemplo para construir uma mensagem "version" em Ruby:

Código (Ruby) — construindo a mensagem version

Exemplo em Ruby para construir uma mensagem version (com funções auxiliares para formatar os dados):

require 'digest' # necessário para criar checksums

# Funções auxiliares para deixar os dados no formato certo para as mensagens
def hexadecimal(number)
    return number.to_s(16)
end

def size(data, size)
    return data.rjust(size*2, '0') # preenche a esquerda com zeros até um nº de bytes (2 chars hex = 1 byte)
end

def reversebytes(bytes)
    return bytes.scan(/../).reverse.join() # pega cada 2 chars (1 byte), inverte e junta de novo
end

def ascii2hex(string)
    # Converte cada caractere da string para sua representação em bytes hexadecimais
    bytes = string.each_byte.map {|c| c.to_s(16) }.join()

    # Preenche até 12 bytes (mantendo os bytes da string ascii à esquerda)
    return bytes.ljust(24, '0')
end

def checksum(bytes)
    # Faz o hash dos dados duas vezes
    hash = Digest::SHA256.digest(Digest::SHA256.digest([bytes].pack("H*"))).unpack("H*")[0]

    # Retorna os primeiros 4 bytes (8 caracteres)
    return hash[0...8]
end

# Cria o payload de uma mensagem version
payload  = reversebytes(size(hexadecimal(70014), 4))         # versão do protocolo
payload += reversebytes(size(hexadecimal(0), 8))             # serviços (ex.: 1<<3 | 1<<2 | 1<<0)
payload += reversebytes(size(hexadecimal(1640961477), 8))    # tempo
payload += reversebytes(size(hexadecimal(0), 8))             # serviços do nó remoto
payload += "00000000000000000000ffff2e13894a"                # ipv6 do nó remoto
payload += size(hexadecimal(8333), 2)                        # porta do nó remoto
payload += reversebytes(size(hexadecimal(0), 8))             # serviços do nó local
payload += "00000000000000000000ffff7f000001"                # ipv6 do nó local
payload += size(hexadecimal(8333), 2)                        # porta do nó local
payload += reversebytes(size(hexadecimal(0), 8))             # nonce
payload += "00"                                              # user agent (compact_size + bytes ascii)
payload += reversebytes(size(hexadecimal(0), 4))             # último bloco

# Cria o cabeçalho da mensagem
magic_bytes = 'f9beb4d9'
command     = ascii2hex('version')                                 # 76 65 72 73 69 6F 6E 00 00 00 00 00
size        = reversebytes(size(hexadecimal(payload.length/2), 4)) # 55 00 00 00
checksum    = checksum(payload)
header      = magic_bytes + command + size + checksum

# Junta cabeçalho e payload
message = header + payload

A parte mais complicada é garantir que você converta os dados nos bytes corretos e na ordem correta. É aí que entram todas aquelas funções utilitárias no código acima. Mas, depois que você pega o jeito de converter para hexadecimal e de converter ordens de bytes para little-endian, não é tão ruim.

E é assim que nossa mensagem "version" final fica como uma string de bytes hexadecimais:

F9BEB4D976657273696F6E0000000000550000002C2F86F37E1101000000000000000000C515CF6100000000000000000000000000000000000000000000FFFF2E13894A208D000000000000000000000000000000000000FFFF7F000001208D00000000000000000000000000

Então agora que sabemos como construir uma mensagem, podemos começar a nos comunicar com o nó ao qual acabamos de nos conectar.

3. Handshake

O handshake é o processo que estabelece a comunicação entre dois dispositivos de rede.

Antes de podermos começar a receber dados, precisamos fazer um "handshake". Esse handshake é só uma sequência de mensagens que enviamos uns aos outros para começar.

No protocolo Bitcoin, o handshake funciona assim:

Diagrama da sequência de mensagens no handshake do protocolo Bitcoin.

Então o handshake é basicamente um processo de 2 etapas:

  1. Nós iniciamos a comunicação enviando nossa mensagem "version", e eles respondem com a própria mensagem "version".
  2. Eles então enviam uma mensagem "verack" confirmando que receberam nossa mensagem version, e nós terminamos enviando uma mensagem "verack" de volta para eles.

E é só isso.

A ordem das mensagens no handshake é importante. Se você errar a ordem, o handshake vai falhar e o outro nó vai rejeitar sua conexão. Você sempre pode tentar de novo, mas, se errar o handshake vezes demais, pode ser temporariamente banido. Se isso acontecer, você sempre pode conectar a outro nó enquanto isso.

Preparação das mensagens

Precisamos enviar duas mensagens para realizar o handshake:

  1. Mensagem Version
  2. Mensagem Verack

Nós já preparamos nossa mensagem "version", então vamos criar uma mensagem "verack".

Verack

O "verack" é um cabeçalho de mensagem simples, sem payload:

Mensagem Verack:
┌─────────────┬──────────────┬───────────────┬───────┬─────────────────────────────────────┐
│ Nome        │ Dados Ex.    │ Formato       │ Tam.  │ Bytes Exemplo                       │
├─────────────┼──────────────┼───────────────┼───────┼─────────────────────────────────────┤
│ Magic Bytes │              │ bytes         │     4 │ F9 BE B4 D9                         │
│ Comando     │ "verack"     │ bytes ascii   │    12 │ 76 65 72 61 63 6B 00 00 00 00 00 00 │
│ Tamanho     │ 0            │ little-endian │     0 │ 00 00 00 00                         │
│ Checksum    │              │ bytes         │     4 │ 5D F6 E0 E2                         │
└─────────────┴──────────────┴───────────────┴───────┴─────────────────────────────────────┘

Hexadecimal: F9BEB4D976657261636B000000000000000000005DF6E0E2

Uma mensagem "verack" é sempre a mesma.

Enviando e recebendo mensagens

Agora que temos nossas mensagens prontas, só precisamos enviá-las ao nó ao qual nos conectamos (e receber mensagens de volta deles).

Código

Aqui está um código Ruby mostrando como construir manualmente cada mensagem e como escrever/ler bytes de/para a conexão de socket:

Código (Ruby) — enviando/recebendo o handshake

Código Ruby para enviar a mensagem version, receber a version e a verack do nó remoto, e enviar nossa própria verack. Os comentários do código estão em inglês (como no original).

# 1. Send Version Message

# Prepare version message
version = message

# Write the message to the socket (the protocol sends and receives messages in raw bytes)
socket.write [version].pack("H*")
puts "version->"
puts version
puts

# 2. Receive Version Message

# Read the message header response from the socket
magic_bytes = socket.read(4)
command     = socket.read(12)
size        = socket.read(4)
checksum    = socket.read(4)

# View the message header
puts "<-version"
puts "magic_bytes: " + magic_bytes.unpack("H*").join  # convert raw bytes to hexadecimal characters
puts "command:     " + command.to_s                   # to_s automatically converts raw bytes to ASCII characters
puts "size:        " + size.unpack("V").join          # V = 32-byte unsigned, little-endian
puts "checksum:    " + checksum.unpack("H*").join

# Read the message payload
size = size.unpack("V").join.to_i
payload = socket.read(size)

# View the message payload
puts "payload:     " + payload.unpack("H*").join
puts

# 3. Receive Verack Message (verack = version acknowledged)

# Read the message header response from the socket
magic_bytes = socket.read(4)
command     = socket.read(12)
size        = socket.read(4)
checksum    = socket.read(4)

# View the message header
puts "<-verack"
puts "magic_bytes: " + magic_bytes.unpack("H*").join  # convert raw bytes to hexadecimal characters
puts "command:     " + command.to_s                   # to_s automatically converts raw bytes to ASCII characters
puts "size:        " + size.unpack("V").join          # V = 32-byte unsigned, little-endian
puts "checksum:    " + checksum.unpack("H*").join

# Read the message payload (there shouldn't be any)
size = size.unpack("V").join.to_i
payload = socket.read(size)

# View the message payload (there shouldn't be any)
puts "payload:     " + payload.unpack("H*").join
puts

# 4. Send Verack Message

# Create verack message
payload     = '' # verack has no payload, it's just a message header
magic_bytes = 'f9beb4d9'
command     = ascii2hex('verack')
size        = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum    = checksum(payload)
verack      = magic_bytes + command + size + checksum + payload

# Write the message to the socket
socket.write [verack].pack("H*")
puts "verack->"
puts "magic_bytes: " + magic_bytes
puts "command:     " + 'verack'
puts "size:        " + size.to_i(16).to_s
puts "checksum:    " + checksum
puts "payload:     " + payload
puts

Strings e Bytes. Ao enviar dados "pela rede" (over the wire), você precisa converter todos os seus dados em bytes brutos. Nos exemplos de código que dei, mesmo que pareça que estou trabalhando com bytes, na verdade estou manipulando strings compostas de caracteres hexadecimais que representam bytes. É aí que entra a função pack(), pois ela permite converter strings em bytes de verdade. Sua linguagem de programação vai ter algo parecido.

Programação de Sockets. A forma como você escreve/lê bytes de/para um socket vai ser diferente de uma linguagem de programação para outra, então pode levar um tempo para se acostumar se você nunca fez isso antes.

De todo jeito, depois que você recebeu aquela mensagem "verack" (e enviou a sua própria de volta), o handshake está completo. E, se tudo funcionou corretamente, o nó vai começar a te enviar alguns novos tipos de mensagem...

4. Recebendo Mensagens

O nó ao qual você acabou de se conectar vai te enviar continuamente novas mensagens após o handshake. Então, para continuar recebendo essas mensagens, tudo o que precisamos fazer é continuar lendo do socket em um laço.

É assim que as novas mensagens vão se parecer:

Diagrama mostrando as mensagens do protocolo Bitcoin que um nó vai receber logo após conectar a outro nó.

Você pode receber algumas mensagens diferentes antes das mensagens "inv" mostradas no diagrama acima, dependendo de qual versão do protocolo você estiver usando. Vou simplesmente ignorá-las por enquanto, já que não são criticamente importantes.

Vou explicar o que são essas mensagens "inv" e como responder a elas daqui a pouco. Mas, por enquanto, vou só mostrar como continuar lendo mensagens do nó ao qual você se conectou:

Código

O código a seguir é parecido com o código de antes, exceto que desta vez o colocamos em um laço para ler continuamente do socket.

Código (Ruby) — lendo mensagens em laço

Laço que lê continuamente do socket, identifica os magic bytes e exibe cada mensagem recebida. Os comentários do código estão em inglês (como no original).

# Keep reading messages
loop do

    # Create an empty buffer to help us find the next stream of magic bytes (the start of a new message)
    buffer = ''

    # Keep looping to read bytes from the socket
    loop do

        # Read one byte at the time
        byte = socket.read(1)

        # Check that we haven't been disconnected from the node.
        if byte.nil?
            puts "Read a nil byte from the socket. Looks like the remote node has disconnected from us. We probably failed the handshake too many times, or didn't respond to enough pings. No worries, try connecting to another node for the time being instead."
            exit
        end

        # Add each byte to the temporary buffer
        buffer += byte.unpack("H*").join unless byte.nil? # do not do anything if we got a nil byte for some reason

        # Check the buffer when it reaches 4 bytes
        if (buffer.size == 8) # 8 hexadecimal characters = 4 bytes

            # See if the buffer matches the magic bytes
            if (buffer == 'f9beb4d9')

                # If we've got the magic bytes we're looking for, go ahead and read the full message from the socket
                command     = socket.read(12).to_s.delete("\x00")  # convert to ascii and remove any empty bytes
                size        = socket.read(4).unpack("V").join.to_i # convert to an integer
                checksum    = socket.read(4).unpack("H*").join     # convert to hexadecimal string of bytes
                payload     = socket.read(size).unpack("H*").join  # use the size from the header to read the payload, then convert to a hexadecimal string

                # Print the message
                puts "<-#{command}"
                puts "magic_bytes: " + buffer
                puts "command:     " + command
                puts "size:        " + size.to_s
                puts "checksum:    " + checksum
                puts "payload:     " + payload
                puts

                # Break out of the loop for reading a single message
                break

            end

            # Reset the buffer and keep looking for a stream of magic bytes
            buffer = ''

        end

    end

end

Agora podemos continuar lendo dados desse nó para sempre, ou pelo menos até o nosso computador travar aleatoriamente e eu acabar perdendo todo o código que escrevi na última hora porque esqueci de salvar.

5. Pedindo Transações e Blocos

Um nó não vai te enviar abertamente todas as novas transações e blocos que recebeu. Em vez disso, para economizar banda, ele vai te enviar uma lista de hashes das transações e blocos mais recentes que recebeu em mensagens "inv" (inventory/inventário).

Você pode então responder a essas mensagens "inv" listando todas as transações e blocos específicos que quer, com mensagens "getdata".

Então, depois que você enviou sua mensagem "getdata", o nó vai te enviar as transações e blocos completos que você pediu em mensagens "tx" e "block" subsequentes:

Diagrama mostrando a sequência de mensagens para pedir transações e blocos no protocolo Bitcoin.

Inv

O payload de uma mensagem "inv" se parece com isto:

Payload: (inv)
┌─────────────┬───────────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Nome        │ Formato           │ Tamanho  │ Bytes Exemplo                                                                                                │
├─────────────┼───────────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Contagem    │ compact size      │ variável │ 01                                                                                                           │
│ Inventário  │ inventory vector  │ variável │ 01 00 00 00 aa 32 5e 91 22 aa 39 ca 18 c7 5a ab e2 a3 ce af 98 02 ac d1 a4 07 20 92 5b fd 77 ff f5 8e d8 21  │
└─────────────┴───────────────────┴──────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Inventário

A parte "Inventário" do payload é, ela mesma, outra estrutura de dados. Mas é bem simples: é só uma lista de hashes de transação e/ou hashes de bloco:

Inventário:
┌─────────┬───────────────┬───────┬─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Nome    │ Formato       │ Tam.  │ Bytes Exemplo                                                                                   │
├─────────┼───────────────┼───────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Tipo    │ little-endian │     4 │ 01 00 00 00                                                                                     │
│ Hash    │ bytes         │    32 │ aa 32 5e 91 22 aa 39 ca 18 c7 5a ab e2 a3 ce af 98 02 ac d1 a4 07 20 92 5b fd 77 ff f5 8e d8 21 │
└─────────┴───────────────┴───────┴─────────────────────────────────────────────────────────────────────────────────────────────────┘

Tipos:

* 01 00 00 00 = MSG_TX    (Hash de Transação)
* 02 00 00 00 = MSG_BLOCK (Hash de Bloco)

Getdata

A mensagem "getdata" com que você responde tem exatamente a mesma estrutura da mensagem "inv" (o que é conveniente):

Payload: (getdata)
┌─────────────┬───────────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Nome        │ Formato           │ Tamanho  │ Bytes Exemplo                                                                                                │
├─────────────┼───────────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Contagem    │ compact size      │ variável │ 01                                                                                                           │
│ Inventário  │ inventory vector  │ variável │ 01 00 00 00 aa 32 5e 91 22 aa 39 ca 18 c7 5a ab e2 a3 ce af 98 02 ac d1 a4 07 20 92 5b fd 77 ff f5 8e d8 21  │
└─────────────┴───────────────────┴──────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Então, se você quer todas as transações e blocos do "inv", pode simplesmente responder com o mesmo payload na sua mensagem "getdata". Ou, se não quiser todos, basta construir um payload com uma lista dos hashes de transação/bloco que você quer.

De todo jeito, depois de enviar sua mensagem "getdata", o nó vai prosseguir e te enviar cópias completas das transações e blocos que você pediu em mensagens "tx" e "block" individuais em resposta.

Código

Aqui está um código Ruby que responde a toda mensagem "inv" com uma mensagem "getdata" pedindo tudo o que está no payload:

Código (Ruby) — respondendo a "inv" com "getdata"

Laço de leitura que responde a cada mensagem "inv" com uma mensagem "getdata" pedindo todo o conteúdo do payload. Os comentários do código estão em inglês (como no original).

# Keep reading messages
loop do

    # Create an empty buffer to help us find the next stream of magic bytes (the start of a new message)
    buffer = ''

    # Keep looping to read bytes from the socket
    loop do

        # Read one byte at the time
        byte = socket.read(1)

        # Check that we haven't been disconnected from the node.
        if byte.nil?
            puts "Read a nil byte from the socket. Looks like the remote node has disconnected from us. We probably failed the handshake too many times, or didn't respond to enough pings. No worries, try connecting to another node for the time being instead."
            exit
        end

        # Add each byte to the temporary buffer
        buffer += byte.unpack("H*").join unless byte.nil? # do not do anything if we got a nil byte for some reason

        # Check the buffer when it reaches 4 bytes
        if (buffer.size == 8) # 8 hexadecimal characters = 4 bytes

            # See if the buffer matches the magic bytes
            if (buffer == 'f9beb4d9')

                # If we've got the magic bytes we're looking for, go ahead and read the full message from the socket
                command     = socket.read(12).to_s.delete("\x00")  # convert to ascii and remove any empty bytes
                size        = socket.read(4).unpack("V").join.to_i # convert to an integer
                checksum    = socket.read(4).unpack("H*").join     # convert to hexadecimal string of bytes
                payload     = socket.read(size).unpack("H*").join  # use the size from the header to read the payload, then convert to a hexadecimal string

                # Print the message
                puts "<-#{command}"
                puts "magic_bytes: " + buffer
                puts "command:     " + command
                puts "size:        " + size.to_s
                puts "checksum:    " + checksum
                puts "payload:     " + payload
                puts

                # Respond to all inv messages with getdata messages
                if command == "inv"

                    # Set new command name
                    command = "getdata"

                    # Use the same payload as the one we got from the inv message
                    payload = payload

                    # Create message
                    magic_bytes = 'f9beb4d9'
                    command_hex = ascii2hex(command)
                    size        = reversebytes(size(hexadecimal(payload.size/2), 4))
                    checksum    = checksum(payload)
                    message     = magic_bytes + command_hex + size + checksum + payload

                    # Print the message header and payload
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Send the message (convert from hexadecimal string to raw bytes first)
                    socket.write [message].pack("H*")

                end

                # Break out of the loop for reading a single message
                break

            end

            # Reset the buffer and keep looking for a stream of magic bytes
            buffer = ''

        end

    end

end

E é assim que você pode obter as transações e blocos mais recentes de um nó real na rede.

Se você chegou até aqui e está tudo funcionando, você descobriu como conectar e se comunicar com um nó bitcoin do zero. Tudo daqui pra frente envolve apenas construir diferentes tipos de mensagem.

Aqui está uma lista completa das mensagens que os nós Bitcoin podem enviar uns aos outros.

6. Mantendo a Conexão

Uma última coisa antes de você ir: o nó ao qual você acabou de se conectar vai ocasionalmente te enviar mensagens "ping" para ver se você ainda está lá. Então, se você quer manter a conexão viva, vai precisar responder com mensagens "pong" em tempo hábil.

Diagrama mostrando a sequência de mensagens para manter uma conexão viva no protocolo Bitcoin via mensagens ping e pong.

Ping

A partir da versão de protocolo 60001, cada mensagem "ping" contém um número aleatório como seu payload:

Payload: (ping)
┌─────────────┬─────────┬──────┬─────────────────────────┐
│ Nome        │ Formato │ Tam. │ Bytes Exemplo           │
├─────────────┼─────────┼──────┼─────────────────────────┤
│ Nonce       │ bytes   │    8 │ 88 c8 49 39 65 b6 41 69 │
└─────────────┴─────────┴──────┴─────────────────────────┘

Pong

Sua mensagem "pong" de resposta só precisa conter o mesmo número no seu payload também:

Payload: (pong)
┌─────────────┬─────────┬──────┬─────────────────────────┐
│ Nome        │ Formato │ Tam. │ Bytes Exemplo           │
├─────────────┼─────────┼──────┼─────────────────────────┤
│ Nonce       │ bytes   │    8 │ 88 c8 49 39 65 b6 41 69 │
└─────────────┴─────────┴──────┴─────────────────────────┘

Então, adicionando um último ajuste ao nosso laço, agora podemos manter a conexão aberta e receber transações e blocos para sempre:

Código

Código (Ruby) — respondendo a "ping" com "pong" (e "inv" com "getdata")

Laço final que responde a "inv" com "getdata" e a "ping" com "pong", mantendo a conexão viva. Os comentários do código estão em inglês (como no original).

# Keep reading messages
loop do

    # Create an empty buffer to help us find the next stream of magic bytes (the start of a new message)
    buffer = ''

    # Keep looping to read bytes from the socket
    loop do

        # Read one byte at the time
        byte = socket.read(1)

        # Check that we haven't been disconnected from the node.
        if byte.nil?
            puts "Read a nil byte from the socket. Looks like the remote node has disconnected from us. We probably failed the handshake too many times, or didn't respond to enough pings. No worries, try connecting to another node for the time being instead."
            exit
        end

        # Add each byte to the temporary buffer
        buffer += byte.unpack("H*").join unless byte.nil? # do not do anything if we got a nil byte for some reason

        # Check the buffer when it reaches 4 bytes
        if (buffer.size == 8) # 8 hexadecimal characters = 4 bytes

            # See if the buffer matches the magic bytes
            if (buffer == 'f9beb4d9')

                # If we've got the magic bytes we're looking for, go ahead and read the full message from the socket
                command     = socket.read(12).to_s.delete("\x00")  # convert to ascii and remove any empty bytes
                size        = socket.read(4).unpack("V").join.to_i # convert to an integer
                checksum    = socket.read(4).unpack("H*").join     # convert to hexadecimal string of bytes
                payload     = socket.read(size).unpack("H*").join  # use the size from the header to read the payload, then convert to a hexadecimal string

                # Print the message
                puts "<-#{command}"
                puts "magic_bytes: " + buffer
                puts "command:     " + command
                puts "size:        " + size.to_s
                puts "checksum:    " + checksum
                puts "payload:     " + payload
                puts

                # Respond to all inv messages with getdata messages
                if command == "inv"

                    # Set new command name
                    command = "getdata"

                    # Use the same payload as the one we got from the inv message
                    payload = payload

                    # Create message
                    magic_bytes = 'f9beb4d9'
                    command_hex = ascii2hex(command)
                    size        = reversebytes(size(hexadecimal(payload.size/2), 4))
                    checksum    = checksum(payload)
                    message     = magic_bytes + command_hex + size + checksum + payload

                    # Print the message header and payload
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Send the message (convert from hexadecimal string to raw bytes first)
                    socket.write [message].pack("H*")

                end

                # Respond to all ping messages with pong messages
                if command == "ping"

                    # Set new command name
                    command = "pong"

                    # Use the same payload as the one we got from the ping message
                    payload = payload

                    # Create message
                    magic_bytes = 'f9beb4d9'
                    command_hex = ascii2hex(command)
                    size        = reversebytes(size(hexadecimal(payload.size/2), 4))
                    checksum    = checksum(payload)
                    message     = magic_bytes + command_hex + size + checksum + payload

                    # Print the message header and payload
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Send the message (convert from hexadecimal string to raw bytes first)
                    socket.write [message].pack("H*")

                end

                # Break out of the loop for reading a single message
                break

            end

            # Reset the buffer and keep looking for a stream of magic bytes
            buffer = ''

        end

    end

end

Funções. Eu repeti o mesmo código nos meus exemplos de código para manter tudo o mais legível possível. Seria melhor colocar o código para ler mensagens e enviar mensagens em suas próprias funções.

7. Encontrando Nós

Não sabe onde encontrar um nó ao qual você possa se conectar? Aqui estão alguns lugares que você pode tentar:

Você pode realizar uma requisição DNS a um DNS seed pela linha de comando com: nslookup seed.bitcoin.sipa.be. Observe que isso pode não funcionar se você estiver usando uma VPN.

8. Resumo

Conectar a um nó do zero é uma forma legal de começar a programar no Bitcoin. Permite ver como os nós se comunicam uns com os outros, e te dá acesso ao vivo às transações e blocos mais recentes da rede.

Você pode conectar a um nó a partir de praticamente qualquer linguagem de programação que você quiser. Tudo o que você precisa é conseguir fazer conexões TCP e ter o IP e o número da porta de um computador rodando um nó bitcoin. Se você está rodando o bitcoin localmente, o IP vai ser 127.0.0.1 e a porta vai ser 8333 (por padrão).

A parte mais complicada de longe é descobrir como construir as mensagens. Você precisa colocar todos os bytes brutos de dados na ordem correta, porque, mesmo que erre só um byte, o nó para o qual você está enviando mensagens não vai te entender. E isso pode ser um processo um tanto frustrante até você acertar. Mas, depois que você enviou aquela primeira mensagem corretamente, todos os outros tipos de mensagem ficam muito mais fáceis de construir.

Obter minha primeira transação bruta de um nó bitcoin real, usando um script que escrevi do zero, foi uma das conquistas mais satisfatórias da minha carreira de programação.

Boa sorte.

Programa completo (Ruby)

O programa completo deste tutorial: conecta a um nó, faz o handshake e fica lendo mensagens (respondendo a ping e inv). Os comentários do código estão em inglês (como no original).

# Sockets are in the standard library in Ruby
require 'socket'

# Open a TCP connection to an IP and port
socket = TCPSocket.open("127.0.0.1", 8333) # local computer = 127.0.0.1

require 'digest' # needed for creating checksums

# Handy functions for getting data in the right format for messages
def hexadecimal(number)
    return number.to_s(16)
end

def size(data, size)
    return data.rjust(size*2, '0') # pad the left of the data out with zeros up to a specific number of bytes (2 hexadecimal chars = 1 byte)
end

def reversebytes(bytes)
    return bytes.scan(/../).reverse.join() # grab each 2 characters (1 byte) as an array, reverse the array, then join back together
end

def ascii2hex(string)
    # Convert each character in the string to its hexadecimal byte representation
    bytes = string.each_byte.map {|c| c.to_s(16) }.join()

    # Pad up to 12 bytes (keeping the bytes for the ascii string on the left)
    return bytes.ljust(24, '0')
end

def checksum(bytes)
    # Hash the data twice
    hash = Digest::SHA256.digest(Digest::SHA256.digest([bytes].pack("H*"))).unpack("H*")[0]

    # Return the first 4 bytes (8 characters)
    return hash[0...8]
end

# Create the payload for a version message
payload  = reversebytes(size(hexadecimal(70014), 4))         # protocol version
payload += reversebytes(size(hexadecimal(0), 8))             # services e.g. (1<<3 | 1<<2 | 1<<0)
payload += reversebytes(size(hexadecimal(1640961477), 8))    # time
payload += reversebytes(size(hexadecimal(0), 8))             # remote node services
payload += "00000000000000000000ffff2e13894a"                # remote node ipv6 (https://dnschecker.org/ipv4-to-ipv6.php)
payload += size(hexadecimal(8333), 2)                        # remote node port
payload += reversebytes(size(hexadecimal(0), 8))             # local node services
payload += "00000000000000000000ffff7f000001"                # local node ipv6
payload += size(hexadecimal(8333), 2)                        # local node port
payload += reversebytes(size(hexadecimal(0), 8))             # nonce
payload += "00"                                              # user agent (compact_size, followed by ascii bytes)
payload += reversebytes(size(hexadecimal(0), 4))             # last block

# Create the message header
magic_bytes = 'f9beb4d9'
command     = ascii2hex('version')                                 # 76 65 72 73 69 6F 6E 00 00 00 00 00
size        = reversebytes(size(hexadecimal(payload.length/2), 4)) # 55 00 00 00
checksum    = checksum(payload)
header      = magic_bytes + command + size + checksum

# Combine the header and payload
message = header + payload

# 1. Send Version Message

# Prepare version message
version = message

# Write the message to the socket (the protocol sends and receives messages in raw bytes)
socket.write [version].pack("H*")
puts "version->"
puts version
puts


# 2. Receive Version Message

# Read the message header response from the socket
magic_bytes = socket.read(4)
command     = socket.read(12)
size        = socket.read(4)
checksum    = socket.read(4)

# View the message header
puts "<-version"
puts "magic_bytes: " + magic_bytes.unpack("H*").join  # convert raw bytes to hexadecimal characters
puts "command:     " + command.to_s                   # to_s automatically converts raw bytes to ASCII characters
puts "size:        " + size.unpack("V").join          # V = 32-byte unsigned, little-endian
puts "checksum:    " + checksum.unpack("H*").join

# Read the message payload
size = size.unpack("V").join.to_i
payload = socket.read(size)

# View the message payload
puts "payload:     " + payload.unpack("H*").join
puts


# 3. Receive Verack Message (verack = version acknowledged)

# Read the message header response from the socket
magic_bytes = socket.read(4)
command     = socket.read(12)
size        = socket.read(4)
checksum    = socket.read(4)

# View the message header
puts "<-verack"
puts "magic_bytes: " + magic_bytes.unpack("H*").join  # convert raw bytes to hexadecimal characters
puts "command:     " + command.to_s                   # to_s automatically converts raw bytes to ASCII characters
puts "size:        " + size.unpack("V").join          # V = 32-byte unsigned, little-endian
puts "checksum:    " + checksum.unpack("H*").join

# Read the message payload (there shouldn't be any)
size = size.unpack("V").join.to_i
payload = socket.read(size)

# View the message payload (there shouldn't be any)
puts "payload:     " + payload.unpack("H*").join
puts

# 4. Send Verack Message

# Create verack message
payload     = '' # verack has no payload, it's just a message header
magic_bytes = 'f9beb4d9'
command     = ascii2hex('verack')
size        = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum    = checksum(payload)
verack      = magic_bytes + command + size + checksum + payload

# Write the message to the socket
socket.write [verack].pack("H*")
puts "verack->"
puts "magic_bytes: " + magic_bytes
puts "command:     " + 'verack'
puts "size:        " + size.to_i(16).to_s
puts "checksum:    " + checksum
puts "payload:     " + payload
puts

# Keep reading messages
loop do

    # Create an empty buffer to help us find the next stream of magic bytes (the start of a new message)
    buffer = ''

    # Keep looping to read bytes from the socket
    loop do

        # Read one byte at the time
        byte = socket.read(1)

        # Check that we haven't been disconnected from the node.
        if byte.nil?
            puts "Read a nil byte from the socket. Looks like the remote node has disconnected from us. We probably failed the handshake too many times, or didn't respond to enough pings. No worries, try connecting to another node for the time being instead."
            exit
        end

        # Add each byte to the temporary buffer
        buffer += byte.unpack("H*").join unless byte.nil? # do not do anything if we got a nil byte for some reason

        # Check the buffer when it reaches 4 bytes
        if (buffer.size == 8) # 8 hexadecimal characters = 4 bytes

            # See if the buffer matches the magic bytes
            if (buffer == 'f9beb4d9')

                # If we've got the magic bytes we're looking for, go ahead and read the full message from the socket
                command     = socket.read(12).to_s.delete("\x00")  # convert to ascii and remove any empty bytes
                size        = socket.read(4).unpack("V").join.to_i # convert to an integer
                checksum    = socket.read(4).unpack("H*").join     # convert to hexadecimal string of bytes
                payload     = socket.read(size).unpack("H*").join  # use the size from the header to read the payload, then convert to a hexadecimal string

                # Print the message
                puts "<-#{command}"
                puts "magic_bytes: " + buffer
                puts "command:     " + command
                puts "size:        " + size.to_s
                puts "checksum:    " + checksum
                puts "payload:     " + payload
                puts

                # Respond to all inv messages with getdata messages
                if command == "inv"

                    # Set new command name
                    command = "getdata"

                    # Use the same payload as the one we got from the inv message
                    payload = payload

                    # Create message
                    magic_bytes = 'f9beb4d9'
                    command_hex = ascii2hex(command)
                    size        = reversebytes(size(hexadecimal(payload.size/2), 4))
                    checksum    = checksum(payload)
                    message     = magic_bytes + command_hex + size + checksum + payload

                    # Print the message header and payload
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Send the message (convert from hexadecimal string to raw bytes first)
                    socket.write [message].pack("H*")

                end

                # Respond to all ping messages with pong messages
                if command == "ping"

                    # Set new command name
                    command = "pong"

                    # Use the same payload as the one we got from the ping message
                    payload = payload

                    # Create message
                    magic_bytes = 'f9beb4d9'
                    command_hex = ascii2hex(command)
                    size        = reversebytes(size(hexadecimal(payload.size/2), 4))
                    checksum    = checksum(payload)
                    message     = magic_bytes + command_hex + size + checksum + payload

                    # Print the message header and payload
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Send the message (convert from hexadecimal string to raw bytes first)
                    socket.write [message].pack("H*")

                end

                # Break out of the loop for reading a single message
                break

            end

            # Reset the buffer and keep looking for a stream of magic bytes
            buffer = ''

        end

    end

end