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.
Programa completo (Ruby)

O programa completo deste tutorial: conecta a um nó, faz o handshake e fica lendo mensagens (respondendo a ping e inv).

# Sockets estão na biblioteca padrão do Ruby
require 'socket'

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

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

# 1. Enviar Mensagem Version

# Preparar mensagem version
version = message

# Escrever a mensagem no socket (o protocolo envia e recebe mensagens em bytes brutos)
socket.write [version].pack("H*")
puts "version->"
puts version
puts


# 2. Receber Mensagem Version

# Ler o cabeçalho de resposta do socket
magic_bytes = socket.read(4)
command     = socket.read(12)
size        = socket.read(4)
checksum    = socket.read(4)

# Exibir o cabeçalho da mensagem
puts "<-version"
puts "magic_bytes: " + magic_bytes.unpack("H*").join  # converter bytes brutos para caracteres hexadecimais
puts "command:     " + command.to_s                   # to_s converte automaticamente bytes brutos para caracteres ASCII
puts "size:        " + size.unpack("V").join          # V = 32-bit unsigned, little-endian
puts "checksum:    " + checksum.unpack("H*").join

# Ler o payload da mensagem
size = size.unpack("V").join.to_i
payload = socket.read(size)

# Exibir o payload da mensagem
puts "payload:     " + payload.unpack("H*").join
puts


# 3. Receber Mensagem Verack (verack = version acknowledged)

# Ler o cabeçalho de resposta do socket
magic_bytes = socket.read(4)
command     = socket.read(12)
size        = socket.read(4)
checksum    = socket.read(4)

# Exibir o cabeçalho da mensagem
puts "<-verack"
puts "magic_bytes: " + magic_bytes.unpack("H*").join  # converter bytes brutos para caracteres hexadecimais
puts "command:     " + command.to_s                   # to_s converte automaticamente bytes brutos para caracteres ASCII
puts "size:        " + size.unpack("V").join          # V = 32-bit unsigned, little-endian
puts "checksum:    " + checksum.unpack("H*").join

# Ler o payload da mensagem (não deve haver nenhum)
size = size.unpack("V").join.to_i
payload = socket.read(size)

# Exibir o payload da mensagem (não deve haver nenhum)
puts "payload:     " + payload.unpack("H*").join
puts

# 4. Enviar Mensagem Verack

# Criar mensagem verack
payload     = '' # verack não tem payload, é só um cabeçalho de mensagem
magic_bytes = 'f9beb4d9'
command     = ascii2hex('verack')
size        = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum    = checksum(payload)
verack      = magic_bytes + command + size + checksum + payload

# Escrever a mensagem no 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

# Continuar lendo mensagens
loop do

    # Criar um buffer vazio para ajudar a encontrar o próximo fluxo de magic bytes (o início de uma nova mensagem)
    buffer = ''

    # Continuar em laço lendo bytes do socket
    loop do

        # Ler um byte por vez
        byte = socket.read(1)

        # Verificar se fomos desconectados do nó.
        if byte.nil?
            puts "Leu um byte nil do socket. Parece que o nó remoto se desconectou de nós. Provavelmente falhamos o handshake muitas vezes, ou não respondemos a pings suficientes. Sem problemas, tente conectar em outro nó por enquanto."
            exit
        end

        # Adicionar cada byte ao buffer temporário
        buffer += byte.unpack("H*").join unless byte.nil? # não fazer nada se recebemos um byte nil por algum motivo

        # Verificar o buffer quando atingir 4 bytes
        if (buffer.size == 8) # 8 caracteres hexadecimais = 4 bytes

            # Ver se o buffer corresponde aos magic bytes
            if (buffer == 'f9beb4d9')

                # Se encontramos os magic bytes, ler a mensagem completa do socket
                command     = socket.read(12).to_s.delete("\x00")  # converter para ascii e remover bytes vazios
                size        = socket.read(4).unpack("V").join.to_i # converter para inteiro
                checksum    = socket.read(4).unpack("H*").join     # converter para string hexadecimal de bytes
                payload     = socket.read(size).unpack("H*").join  # usar o tamanho do cabeçalho para ler o payload, depois converter para string hexadecimal

                # Exibir a mensagem
                puts "<-#{command}"
                puts "magic_bytes: " + buffer
                puts "command:     " + command
                puts "size:        " + size.to_s
                puts "checksum:    " + checksum
                puts "payload:     " + payload
                puts

                # Responder a todas as mensagens inv com mensagens getdata
                if command == "inv"

                    # Definir novo nome do comando
                    command = "getdata"

                    # Usar o mesmo payload que recebemos da mensagem inv
                    payload = payload

                    # Criar mensagem
                    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

                    # Exibir o cabeçalho e o payload da mensagem
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Enviar a mensagem (converter de string hexadecimal para bytes brutos primeiro)
                    socket.write [message].pack("H*")

                end

                # Responder a todas as mensagens ping com mensagens pong
                if command == "ping"

                    # Definir novo nome do comando
                    command = "pong"

                    # Usar o mesmo payload que recebemos da mensagem ping
                    payload = payload

                    # Criar mensagem
                    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

                    # Exibir o cabeçalho e o payload da mensagem
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Enviar a mensagem (converter de string hexadecimal para bytes brutos primeiro)
                    socket.write [message].pack("H*")

                end

                # Sair do laço de leitura de uma única mensagem
                break

            end

            # Resetar o buffer e continuar procurando um fluxo de magic bytes
            buffer = ''

        end

    end

end
Conteúdo

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.

# 1. Enviar Mensagem Version

# Preparar mensagem version
version = message

# Escrever a mensagem no socket (o protocolo envia e recebe mensagens em bytes brutos)
socket.write [version].pack("H*")
puts "version->"
puts version
puts

# 2. Receber Mensagem Version

# Ler o cabeçalho de resposta do socket
magic_bytes = socket.read(4)
command     = socket.read(12)
size        = socket.read(4)
checksum    = socket.read(4)

# Exibir o cabeçalho da mensagem
puts "<-version"
puts "magic_bytes: " + magic_bytes.unpack("H*").join  # converter bytes brutos para caracteres hexadecimais
puts "command:     " + command.to_s                   # to_s converte automaticamente bytes brutos para caracteres ASCII
puts "size:        " + size.unpack("V").join          # V = 32-bit unsigned, little-endian
puts "checksum:    " + checksum.unpack("H*").join

# Ler o payload da mensagem
size = size.unpack("V").join.to_i
payload = socket.read(size)

# Exibir o payload da mensagem
puts "payload:     " + payload.unpack("H*").join
puts

# 3. Receber Mensagem Verack (verack = version acknowledged)

# Ler o cabeçalho de resposta do socket
magic_bytes = socket.read(4)
command     = socket.read(12)
size        = socket.read(4)
checksum    = socket.read(4)

# Exibir o cabeçalho da mensagem
puts "<-verack"
puts "magic_bytes: " + magic_bytes.unpack("H*").join  # converter bytes brutos para caracteres hexadecimais
puts "command:     " + command.to_s                   # to_s converte automaticamente bytes brutos para caracteres ASCII
puts "size:        " + size.unpack("V").join          # V = 32-bit unsigned, little-endian
puts "checksum:    " + checksum.unpack("H*").join

# Ler o payload da mensagem (não deve haver nenhum)
size = size.unpack("V").join.to_i
payload = socket.read(size)

# Exibir o payload da mensagem (não deve haver nenhum)
puts "payload:     " + payload.unpack("H*").join
puts

# 4. Enviar Mensagem Verack

# Criar mensagem verack
payload     = '' # verack não tem payload, é só um cabeçalho de mensagem
magic_bytes = 'f9beb4d9'
command     = ascii2hex('verack')
size        = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum    = checksum(payload)
verack      = magic_bytes + command + size + checksum + payload

# Escrever a mensagem no 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.

# Continuar lendo mensagens
loop do

    # Criar um buffer vazio para ajudar a encontrar o próximo fluxo de magic bytes (o início de uma nova mensagem)
    buffer = ''

    # Continuar em laço lendo bytes do socket
    loop do

        # Ler um byte por vez
        byte = socket.read(1)

        # Verificar se fomos desconectados do nó.
        if byte.nil?
            puts "Leu um byte nil do socket. Parece que o nó remoto se desconectou de nós. Provavelmente falhamos o handshake muitas vezes, ou não respondemos a pings suficientes. Sem problemas, tente conectar em outro nó por enquanto."
            exit
        end

        # Adicionar cada byte ao buffer temporário
        buffer += byte.unpack("H*").join unless byte.nil? # não fazer nada se recebemos um byte nil por algum motivo

        # Verificar o buffer quando atingir 4 bytes
        if (buffer.size == 8) # 8 caracteres hexadecimais = 4 bytes

            # Ver se o buffer corresponde aos magic bytes
            if (buffer == 'f9beb4d9')

                # Se encontramos os magic bytes, ler a mensagem completa do socket
                command     = socket.read(12).to_s.delete("\x00")  # converter para ascii e remover bytes vazios
                size        = socket.read(4).unpack("V").join.to_i # converter para inteiro
                checksum    = socket.read(4).unpack("H*").join     # converter para string hexadecimal de bytes
                payload     = socket.read(size).unpack("H*").join  # usar o tamanho do cabeçalho para ler o payload, depois converter para string hexadecimal

                # Exibir a mensagem
                puts "<-#{command}"
                puts "magic_bytes: " + buffer
                puts "command:     " + command
                puts "size:        " + size.to_s
                puts "checksum:    " + checksum
                puts "payload:     " + payload
                puts

                # Sair do laço de leitura de uma única mensagem
                break

            end

            # Resetar o buffer e continuar procurando um fluxo de 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.

# Continuar lendo mensagens
loop do

    # Criar um buffer vazio para ajudar a encontrar o próximo fluxo de magic bytes (o início de uma nova mensagem)
    buffer = ''

    # Continuar em laço lendo bytes do socket
    loop do

        # Ler um byte por vez
        byte = socket.read(1)

        # Verificar se fomos desconectados do nó.
        if byte.nil?
            puts "Leu um byte nil do socket. Parece que o nó remoto se desconectou de nós. Provavelmente falhamos o handshake muitas vezes, ou não respondemos a pings suficientes. Sem problemas, tente conectar em outro nó por enquanto."
            exit
        end

        # Adicionar cada byte ao buffer temporário
        buffer += byte.unpack("H*").join unless byte.nil? # não fazer nada se recebemos um byte nil por algum motivo

        # Verificar o buffer quando atingir 4 bytes
        if (buffer.size == 8) # 8 caracteres hexadecimais = 4 bytes

            # Ver se o buffer corresponde aos magic bytes
            if (buffer == 'f9beb4d9')

                # Se encontramos os magic bytes, ler a mensagem completa do socket
                command     = socket.read(12).to_s.delete("\x00")  # converter para ascii e remover bytes vazios
                size        = socket.read(4).unpack("V").join.to_i # converter para inteiro
                checksum    = socket.read(4).unpack("H*").join     # converter para string hexadecimal de bytes
                payload     = socket.read(size).unpack("H*").join  # usar o tamanho do cabeçalho para ler o payload, depois converter para string hexadecimal

                # Exibir a mensagem
                puts "<-#{command}"
                puts "magic_bytes: " + buffer
                puts "command:     " + command
                puts "size:        " + size.to_s
                puts "checksum:    " + checksum
                puts "payload:     " + payload
                puts

                # Responder a todas as mensagens inv com mensagens getdata
                if command == "inv"

                    # Definir novo nome do comando
                    command = "getdata"

                    # Usar o mesmo payload que recebemos da mensagem inv
                    payload = payload

                    # Criar mensagem
                    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

                    # Exibir o cabeçalho e o payload da mensagem
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Enviar a mensagem (converter de string hexadecimal para bytes brutos primeiro)
                    socket.write [message].pack("H*")

                end

                # Sair do laço de leitura de uma única mensagem
                break

            end

            # Resetar o buffer e continuar procurando um fluxo de 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.

# Continuar lendo mensagens
loop do

    # Criar um buffer vazio para ajudar a encontrar o próximo fluxo de magic bytes (o início de uma nova mensagem)
    buffer = ''

    # Continuar em laço lendo bytes do socket
    loop do

        # Ler um byte por vez
        byte = socket.read(1)

        # Verificar se fomos desconectados do nó.
        if byte.nil?
            puts "Leu um byte nil do socket. Parece que o nó remoto se desconectou de nós. Provavelmente falhamos o handshake muitas vezes, ou não respondemos a pings suficientes. Sem problemas, tente conectar em outro nó por enquanto."
            exit
        end

        # Adicionar cada byte ao buffer temporário
        buffer += byte.unpack("H*").join unless byte.nil? # não fazer nada se recebemos um byte nil por algum motivo

        # Verificar o buffer quando atingir 4 bytes
        if (buffer.size == 8) # 8 caracteres hexadecimais = 4 bytes

            # Ver se o buffer corresponde aos magic bytes
            if (buffer == 'f9beb4d9')

                # Se encontramos os magic bytes, ler a mensagem completa do socket
                command     = socket.read(12).to_s.delete("\x00")  # converter para ascii e remover bytes vazios
                size        = socket.read(4).unpack("V").join.to_i # converter para inteiro
                checksum    = socket.read(4).unpack("H*").join     # converter para string hexadecimal de bytes
                payload     = socket.read(size).unpack("H*").join  # usar o tamanho do cabeçalho para ler o payload, depois converter para string hexadecimal

                # Exibir a mensagem
                puts "<-#{command}"
                puts "magic_bytes: " + buffer
                puts "command:     " + command
                puts "size:        " + size.to_s
                puts "checksum:    " + checksum
                puts "payload:     " + payload
                puts

                # Responder a todas as mensagens inv com mensagens getdata
                if command == "inv"

                    # Definir novo nome do comando
                    command = "getdata"

                    # Usar o mesmo payload que recebemos da mensagem inv
                    payload = payload

                    # Criar mensagem
                    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

                    # Exibir o cabeçalho e o payload da mensagem
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Enviar a mensagem (converter de string hexadecimal para bytes brutos primeiro)
                    socket.write [message].pack("H*")

                end

                # Responder a todas as mensagens ping com mensagens pong
                if command == "ping"

                    # Definir novo nome do comando
                    command = "pong"

                    # Usar o mesmo payload que recebemos da mensagem ping
                    payload = payload

                    # Criar mensagem
                    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

                    # Exibir o cabeçalho e o payload da mensagem
                    puts "#{command}->"
                    puts "magic_bytes: " + magic_bytes
                    puts "command:     " + command
                    puts "size:        " + (payload.size/2).to_s
                    puts "checksum:    " + checksum
                    puts "payload:     " + payload
                    puts

                    # Enviar a mensagem (converter de string hexadecimal para bytes brutos primeiro)
                    socket.write [message].pack("H*")

                end

                # Sair do laço de leitura de uma única mensagem
                break

            end

            # Resetar o buffer e continuar procurando um fluxo de 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.