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.
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.
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
Primeiro, dois fatos rápidos que você precisa saber sobre o programa Bitcoin:
- Ele roda na porta
8333(normalmente) - Ele usa TCP para comunicação
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:
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 │
└─────────────┴──────────────┴───────────────┴──────────┴─────────────────────────────────────┘
- Magic Bytes: Um conjunto único de bytes usado para identificar o início de uma nova mensagem. São sempre os mesmos. Como você lê um fluxo de bytes da conexão TCP, é útil identificar quando uma nova mensagem começa.
- Comando: Indica o tipo de mensagem sendo enviada. É um campo de 12 bytes contendo a codificação ASCII do nome do tipo de mensagem. Aqui diz que estamos enviando uma mensagem "version".
ASCII
- Tamanho: O tamanho do payload a seguir. Indica quantos bytes você precisa ler do socket para obter a mensagem completa.
- Checksum: Uma pequena impressão digital do payload. Permite verificar rapidamente que os dados não foram adulterados em trânsito. É criado fazendo o hash duplo do payload e pegando os primeiros 4 bytes do resultado.
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:
- Versão do Protocolo: A versão do protocolo que o seu nó entende.
- Services: Uma lista de serviços opcionais que seu nó oferece (campo de 64 bits / bit field). Pode deixar como zero se estiver só testando.
- Tempo: O horário do seu computador como timestamp Unix.
- IP/Porta Remotos: O IP (em formato IPv6) e a porta do nó ao qual você acha que está se conectando.
- IP/Porta Locais: O que você acha que é o seu IP/porta locais (não são realmente usados pelo nó remoto).
- Nonce: Um número aleatório que pode ser usado para detectar conexões consigo mesmo. Pode deixar como zero.
- User Agent: Uma string para identificar a marca/modelo do seu nó (ex.: "/Satoshi:22.0.0/"). Pode deixar em branco, mas ainda precisa colocar um byte
00. - Último Bloco: A altura do bloco do topo da sua blockchain local. Deixe como zero se não tiver blocos.
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.
O handshake é basicamente um processo de 2 etapas:
- Iniciamos a comunicação enviando nossa mensagem "version", e eles respondem com a própria mensagem "version".
- 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.
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.
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_TX02 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.
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:
- Seu próprio nó. Se você baixa e roda seu próprio nó Bitcoin Core localmente, pode conectar a ele no IP
127.0.0.1. - bitnodes.io – Um site útil que lista os nós disponíveis na rede Bitcoin que ele consegue encontrar.
- DNS Seeds. Há servidores DNS mantidos por desenvolvedores confiáveis do Bitcoin Core que retornam IPs de nós completos confiáveis (ex.:
seed.bitcoin.sipa.be,seed.bitcoin.sprovoost.nl).
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