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 v1).

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.

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.

1. Conectando

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

Primeiro, 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.

Estas são as portas em que você normalmente encontra o Bitcoin rodando:

mainnet =  8333
testnet = 18333
regtest = 18444

TCP = Transmission Control Protocol. É uma forma de dois computadores se comunicarem pela Internet (um diz olá primeiro, o outro responde olá, etc.). Seu computador usou TCP quando baixou esta página. Você não precisa saber como esses protocolos funcionam: só 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 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

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

Portanto, o truque para enviar mensagens no Bitcoin é colocar um monte de dados no formato correto. 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.

Version

Cabeçalho (Header)

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

Cabeçalho (mensagem version)
┌─────────────┬──────────────┬───────────────┬──────────┬─────────────────────────────────────┐
│ Nome        │ Dados Ex.    │ Formato       │ Tamanho  │ 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                         │
└─────────────┴──────────────┴───────────────┴──────────┴─────────────────────────────────────┘

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                  │ Tamanho │
├───────────────────┼────────────────┼──────────────────────────┼─────────┤
│ Versão Protocolo  │ 70014          │ little-endian            │       4 │
│ Services          │ 0              │ bit field, little-endian │       8 │
│ Tempo             │ 1640961477     │ little-endian            │       8 │
│ Services (Remoto) │ 0              │ bit field, little-endian │       8 │
│ IP Remoto         │ 46.19.137.74   │ ipv6, big-endian         │      16 │
│ Porta Remota      │ 8333           │ big-endian               │       2 │
│ Services (Local)  │ 0              │ bit field, little-endian │       8 │
│ IP Local          │ 127.0.0.1      │ ipv6, big-endian         │      16 │
│ Porta Local       │ 8333           │ big-endian               │       2 │
│ Nonce             │ 0              │ little-endian            │       8 │
│ User Agent        │ ""             │ compact size, ascii      │ compact │
│ Último Bloco      │ 0              │ little-endian            │       4 │
└───────────────────┴────────────────┴──────────────────────────┴─────────┘

Uma mensagem "version" é uma das mais complexas do Bitcoin, mas só porque contém muita informação. É um bom ponto de partida, porque, se você consegue construir uma mensagem "version", consegue construir qualquer mensagem do protocolo Bitcoin. Resumo dos campos:

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

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

F9BEB4D976657273696F6E0000000000550000002C2F86F37E1101000000000000000000C515CF6100000000000000000000000000000000000000000000FFFF2E13894A208D000000000000000000000000000000000000FFFF7F000001208D00000000000000000000000000

3. Handshake

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

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

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

O handshake é basicamente um processo de 2 etapas:

  1. 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" (version acknowledgement) confirmando que receberam nossa mensagem version, e nós terminamos enviando uma mensagem "verack" de volta.

A ordem das mensagens no handshake é importante. Se você errar a ordem, o handshake falha e o outro nó rejeita sua conexão. Você sempre pode tentar de novo, mas, se errar demais, pode ser temporariamente banido (basta conectar a outro nó enquanto isso).

Verack

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

Mensagem Verack:
Magic Bytes  4 bytes  F9 BE B4 D9
Comando     12 bytes  "verack" -> 76 65 72 61 63 6B 00 00 00 00 00 00
Tamanho      4 bytes  00 00 00 00  (payload de tamanho 0)
Checksum     4 bytes  5D F6 E0 E2

Hexadecimal: F9BEB4D976657261636B000000000000000000005DF6E0E2

Uma mensagem "verack" é sempre a mesma.

4. Recebendo Mensagens

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

Diagrama mostrando as mensagens que um nó recebe logo após conectar a outro nó.

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 te envia uma lista de hashes das transações e blocos mais recentes em mensagens "inv" (inventory/inventário).

Você pode então responder a essas mensagens "inv" listando as transações e blocos específicos que quer, com mensagens "getdata". Depois disso, o nó te envia as transações e blocos completos 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" contém uma Contagem (compact size) seguida de um ou mais vetores de Inventário. Cada item de inventário é um Tipo (4 bytes little-endian) + um Hash (32 bytes):

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". Então, se você quer todas as transações e blocos do "inv", pode responder com o mesmo payload. Se não quiser todos, construa um payload só com os hashes que deseja.

Transações SegWit

Para pedir os dados completos de novas transações segwit (incluindo os dados de testemunha), você precisa mudar o campo tipo no inventário da sua mensagem "getdata":

  • 01 00 00 40 = MSG_WITNESS_TX
  • 02 00 00 40 = MSG_WITNESS_BLOCK

Você deve fazer essa mudança em todas as mensagens "getdata" para garantir que está obtendo os dados completos tanto de transações segwit quanto legadas.

Aqui está uma lista completa das mensagens que os nós Bitcoin podem trocar.

6. Mantendo a Conexão

Uma última coisa: o nó ao qual você se conectou ocasionalmente envia mensagens "ping" para ver se você ainda está lá. Então, se você quer manter a conexão viva, precisa responder com mensagens "pong" em tempo hábil.

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

A partir da versão de protocolo 60001, cada mensagem "ping" contém um número aleatório de 8 bytes (Nonce) como payload. Sua mensagem "pong" de resposta só precisa conter o mesmo número no payload.

7. Encontrando Nós

Não sabe onde encontrar um nó para se conectar? Aqui estão alguns lugares para tentar:

Você pode fazer uma requisição DNS a um DNS seed na linha de comando com: nslookup seed.bitcoin.sipa.be.

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 e te dá acesso ao vivo às transações e blocos mais recentes da rede.

Você pode conectar a um nó em praticamente qualquer linguagem. Só precisa conseguir fazer conexões TCP e ter o IP e a porta de um computador rodando um nó (localmente, IP 127.0.0.1 e porta 8333).

A parte mais complicada de longe é descobrir como construir as mensagens. Você precisa colocar todos os bytes na ordem correta, porque, se errar um byte, o nó não vai te entender. Mas, depois de enviar a primeira mensagem corretamente, todos os outros tipos de mensagem ficam muito mais fáceis.

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