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.
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 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
Primeiro de tudo, 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. 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.
- Veja Encontrando Nós se você ainda não tem um IP para se conectar. O método mais fácil é conectar ao seu próprio nó local (
127.0.0.1), ou você pode tentar conectar ao nó rodando neste servidor, se preferir (162.120.69.182). - Você pode usar esta ferramenta do Bitnodes.io para verificar se um nó remoto está aceitando conexões de entrada.
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:
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.
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
- Magic Bytes: Este é um conjunto único de bytes usado para identificar o início de uma nova mensagem. Eles são sempre os mesmos. Veja bem, você vai estar lendo um fluxo de bytes da sua conexão TCP ao receber mensagens, então é útil ser capaz de identificar quando uma nova mensagem começa. Esse conjunto de bytes de aparência aleatória foi especificamente escolhido para que seja improvável que ele apareça em qualquer outro lugar de uma mensagem.
- Comando: Indica o tipo de mensagem que está sendo enviada. Você pode enviar diferentes tipos de mensagem no protocolo Bitcoin, e elas contêm diferentes tipos de informação. É um campo de 12 bytes contendo a codificação ASCII do nome do tipo de mensagem. O deste exemplo diz que estamos enviando uma mensagem "version", que é usada para enviar informações sobre nós mesmos a outro nó.
ASCII
- Tamanho: Este é o tamanho do payload que vem a seguir. Indica quantos bytes você precisa ler do socket para obter a mensagem completa que está sendo enviada.
- Checksum: Esta é uma pequena impressão digital do payload. Ela nos permite verificar rapidamente que os dados no payload não foram adulterados durante o trânsito. É criada fazendo o hash duplo do payload e, em seguida, 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 │ 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:
- Versão do Protocolo: Esta é a versão do protocolo que o nosso nó entende. Versões diferentes do protocolo têm mensagens diferentes, então, ao informar nossa versão de protocolo, deixamos o outro nó saber com que tipo de mensagens podemos trabalhar.
- Services: Esta é uma lista de serviços opcionais que o seu nó pode oferecer. É um bit field de 64 bits, em que cada bit pode ser definido como 1 para indicar um serviço diferente que você oferece (veja esta tabela para uma lista completa). Por exemplo, definir o primeiro bit (à direita) indica que você é um nó completo (full node) e pode fornecer todos os blocos da blockchain. Você pode deixar isto como zero se estiver só testando.
- Tempo: O horário do seu computador como um timestamp Unix (o número de segundos desde 01 de janeiro de 1970).
- Services (Remoto): Esta é uma lista de serviços opcionais que você acha que o nó ao qual está se conectando pode oferecer. Tem a mesma estrutura do campo "Services" principal acima. Não tenho certeza de por que isto é útil ou se é realmente usado para alguma coisa, então deixo como zero.
- IP Remoto: Este é o endereço IP do nó ao qual você acha que está se conectando. Está em formato IPv6 (você pode converter facilmente entre IPv4 e IPv6 se precisar). Acho que isto também não é crucial, mas defino como o IP ao qual estou me conectando mesmo assim.
- Porta Remota: Esta é a porta no nó ao qual você acha que está se conectando. Eu só deixo como o padrão 8333.
- Services (Local): Esta é a lista de serviços que o seu nó oferece. Não tenho certeza de por que isto se repete.
- IP Local: Este é o que você acha que é o seu IP local. É o seu endereço IP em formato IPv6 (de novo, você pode converter entre IPv4 e IPv6 se precisar). Isto não é realmente usado pelo nó remoto, então você pode definir como quiser. Eu só defino como localhost (127.0.0.1).
- Porta Local: Esta é a porta local a partir da qual você está se comunicando. De novo, acho que isto não é crucial, mas deixo como o padrão 8333.
- Nonce: Um número gerado aleatoriamente que pode ser usado para detectar conexões consigo mesmo mais tarde. Você pode deixar como zero se não for necessário, e ele será simplesmente ignorado. O termo "nonce" é abreviação de "number used once" (número usado uma vez), a menos que você seja britânico, caso em que não significa nada disso.
- User Agent: Uma string personalizada que você pode usar para identificar a marca e o modelo do seu nó na rede. O Bitcoin Core usa uma string como "/Satoshi:22.0.0/", mas você pode colocar "Awesome Node 5000" ou algo do tipo, se preferir. Você pode ver esses user agents por conta própria ao rodar
bitcoin-cli getpeerinfo. Você pode deixar este campo em branco se quiser, mas lembre-se apenas de que ainda vai precisar colocar um byte00neste campo para indicar que você não forneceu nenhum byte a seguir. - Último Bloco: A altura do bloco do topo da sua blockchain local. Deixe como zero se você não tiver nenhum bloco ou não se importar em compartilhar nenhum.
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:
Então o handshake é basicamente um processo de 2 etapas:
- Nós iniciamos a comunicação enviando nossa mensagem "version", e eles respondem com a própria mensagem "version".
- 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:
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).
- Para "enviar" mensagens, só escrevemos bytes na nossa conexão de socket TCP.
- Para "receber" mensagens, só lemos bytes do mesmo socket.
Código
Aqui está um código Ruby mostrando como construir manualmente cada mensagem e como escrever/ler bytes de/para a conexão de socket:
Código (Ruby) — enviando/recebendo o handshake
Código Ruby para enviar a mensagem version, receber a version e a verack do nó remoto, e enviar nossa própria verack. Os comentários do código estão em inglês (como no original).
# 1. Send Version Message
# Prepare version message
version = message
# Write the message to the socket (the protocol sends and receives messages in raw bytes)
socket.write [version].pack("H*")
puts "version->"
puts version
puts
# 2. Receive Version Message
# Read the message header response from the socket
magic_bytes = socket.read(4)
command = socket.read(12)
size = socket.read(4)
checksum = socket.read(4)
# View the message header
puts "<-version"
puts "magic_bytes: " + magic_bytes.unpack("H*").join # convert raw bytes to hexadecimal characters
puts "command: " + command.to_s # to_s automatically converts raw bytes to ASCII characters
puts "size: " + size.unpack("V").join # V = 32-byte unsigned, little-endian
puts "checksum: " + checksum.unpack("H*").join
# Read the message payload
size = size.unpack("V").join.to_i
payload = socket.read(size)
# View the message payload
puts "payload: " + payload.unpack("H*").join
puts
# 3. Receive Verack Message (verack = version acknowledged)
# Read the message header response from the socket
magic_bytes = socket.read(4)
command = socket.read(12)
size = socket.read(4)
checksum = socket.read(4)
# View the message header
puts "<-verack"
puts "magic_bytes: " + magic_bytes.unpack("H*").join # convert raw bytes to hexadecimal characters
puts "command: " + command.to_s # to_s automatically converts raw bytes to ASCII characters
puts "size: " + size.unpack("V").join # V = 32-byte unsigned, little-endian
puts "checksum: " + checksum.unpack("H*").join
# Read the message payload (there shouldn't be any)
size = size.unpack("V").join.to_i
payload = socket.read(size)
# View the message payload (there shouldn't be any)
puts "payload: " + payload.unpack("H*").join
puts
# 4. Send Verack Message
# Create verack message
payload = '' # verack has no payload, it's just a message header
magic_bytes = 'f9beb4d9'
command = ascii2hex('verack')
size = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum = checksum(payload)
verack = magic_bytes + command + size + checksum + payload
# Write the message to the socket
socket.write [verack].pack("H*")
puts "verack->"
puts "magic_bytes: " + magic_bytes
puts "command: " + 'verack'
puts "size: " + size.to_i(16).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts Strings e Bytes. Ao enviar dados "pela rede" (over the wire), você precisa converter todos os seus dados em bytes brutos. Nos exemplos de código que dei, mesmo que pareça que estou trabalhando com bytes, na verdade estou manipulando strings compostas de caracteres hexadecimais que representam bytes. É aí que entra a função pack(), pois ela permite converter strings em bytes de verdade. Sua linguagem de programação vai ter algo parecido.
Programação de Sockets. A forma como você escreve/lê bytes de/para um socket vai ser diferente de uma linguagem de programação para outra, então pode levar um tempo para se acostumar se você nunca fez isso antes.
De todo jeito, depois que você recebeu aquela mensagem "verack" (e enviou a sua própria de volta), o handshake está completo. E, se tudo funcionou corretamente, o nó vai começar a te enviar alguns novos tipos de mensagem...
4. Recebendo Mensagens
O nó ao qual você acabou de se conectar vai te enviar continuamente novas mensagens após o handshake. Então, para continuar recebendo essas mensagens, tudo o que precisamos fazer é continuar lendo do socket em um laço.
É assim que as novas mensagens vão se parecer:
Você pode receber algumas mensagens diferentes antes das mensagens "inv" mostradas no diagrama acima, dependendo de qual versão do protocolo você estiver usando. Vou simplesmente ignorá-las por enquanto, já que não são criticamente importantes.
Vou explicar o que são essas mensagens "inv" e como responder a elas daqui a pouco. Mas, por enquanto, vou só mostrar como continuar lendo mensagens do nó ao qual você se conectou:
Código
O código a seguir é parecido com o código de antes, exceto que desta vez o colocamos em um laço para ler continuamente do socket.
Código (Ruby) — lendo mensagens em laço
Laço que lê continuamente do socket, identifica os magic bytes e exibe cada mensagem recebida. Os comentários do código estão em inglês (como no original).
# Keep reading messages
loop do
# Create an empty buffer to help us find the next stream of magic bytes (the start of a new message)
buffer = ''
# Keep looping to read bytes from the socket
loop do
# Read one byte at the time
byte = socket.read(1)
# Check that we haven't been disconnected from the node.
if byte.nil?
puts "Read a nil byte from the socket. Looks like the remote node has disconnected from us. We probably failed the handshake too many times, or didn't respond to enough pings. No worries, try connecting to another node for the time being instead."
exit
end
# Add each byte to the temporary buffer
buffer += byte.unpack("H*").join unless byte.nil? # do not do anything if we got a nil byte for some reason
# Check the buffer when it reaches 4 bytes
if (buffer.size == 8) # 8 hexadecimal characters = 4 bytes
# See if the buffer matches the magic bytes
if (buffer == 'f9beb4d9')
# If we've got the magic bytes we're looking for, go ahead and read the full message from the socket
command = socket.read(12).to_s.delete("\x00") # convert to ascii and remove any empty bytes
size = socket.read(4).unpack("V").join.to_i # convert to an integer
checksum = socket.read(4).unpack("H*").join # convert to hexadecimal string of bytes
payload = socket.read(size).unpack("H*").join # use the size from the header to read the payload, then convert to a hexadecimal string
# Print the message
puts "<-#{command}"
puts "magic_bytes: " + buffer
puts "command: " + command
puts "size: " + size.to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Break out of the loop for reading a single message
break
end
# Reset the buffer and keep looking for a stream of magic bytes
buffer = ''
end
end
end Agora podemos continuar lendo dados desse nó para sempre, ou pelo menos até o nosso computador travar aleatoriamente e eu acabar perdendo todo o código que escrevi na última hora porque esqueci de salvar.
5. Pedindo Transações e Blocos
Um nó não vai te enviar abertamente todas as novas transações e blocos que recebeu. Em vez disso, para economizar banda, ele vai te enviar uma lista de hashes das transações e blocos mais recentes que recebeu em mensagens "inv" (inventory/inventário).
Você pode então responder a essas mensagens "inv" listando todas as transações e blocos específicos que quer, com mensagens "getdata".
Então, depois que você enviou sua mensagem "getdata", o nó vai te enviar as transações e blocos completos que você pediu em mensagens "tx" e "block" subsequentes:
Inv
O payload de uma mensagem "inv" se parece com isto:
Payload: (inv)
┌─────────────┬───────────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Nome │ Formato │ Tamanho │ Bytes Exemplo │
├─────────────┼───────────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Contagem │ compact size │ variável │ 01 │
│ Inventário │ inventory vector │ variável │ 01 00 00 00 aa 32 5e 91 22 aa 39 ca 18 c7 5a ab e2 a3 ce af 98 02 ac d1 a4 07 20 92 5b fd 77 ff f5 8e d8 21 │
└─────────────┴───────────────────┴──────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Inventário
A parte "Inventário" do payload é, ela mesma, outra estrutura de dados. Mas é bem simples: é só uma lista de hashes de transação e/ou hashes de bloco:
Inventário:
┌─────────┬───────────────┬───────┬─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Nome │ Formato │ Tam. │ Bytes Exemplo │
├─────────┼───────────────┼───────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Tipo │ little-endian │ 4 │ 01 00 00 00 │
│ Hash │ bytes │ 32 │ aa 32 5e 91 22 aa 39 ca 18 c7 5a ab e2 a3 ce af 98 02 ac d1 a4 07 20 92 5b fd 77 ff f5 8e d8 21 │
└─────────┴───────────────┴───────┴─────────────────────────────────────────────────────────────────────────────────────────────────┘
Tipos:
* 01 00 00 00 = MSG_TX (Hash de Transação)
* 02 00 00 00 = MSG_BLOCK (Hash de Bloco)
Getdata
A mensagem "getdata" com que você responde tem exatamente a mesma estrutura da mensagem "inv" (o que é conveniente):
Payload: (getdata)
┌─────────────┬───────────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Nome │ Formato │ Tamanho │ Bytes Exemplo │
├─────────────┼───────────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Contagem │ compact size │ variável │ 01 │
│ Inventário │ inventory vector │ variável │ 01 00 00 00 aa 32 5e 91 22 aa 39 ca 18 c7 5a ab e2 a3 ce af 98 02 ac d1 a4 07 20 92 5b fd 77 ff f5 8e d8 21 │
└─────────────┴───────────────────┴──────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Então, se você quer todas as transações e blocos do "inv", pode simplesmente responder com o mesmo payload na sua mensagem "getdata". Ou, se não quiser todos, basta construir um payload com uma lista dos hashes de transação/bloco que você quer.
De todo jeito, depois de enviar sua mensagem "getdata", o nó vai prosseguir e te enviar cópias completas das transações e blocos que você pediu em mensagens "tx" e "block" individuais em resposta.
Código
Aqui está um código Ruby que responde a toda mensagem "inv" com uma mensagem "getdata" pedindo tudo o que está no payload:
Código (Ruby) — respondendo a "inv" com "getdata"
Laço de leitura que responde a cada mensagem "inv" com uma mensagem "getdata" pedindo todo o conteúdo do payload. Os comentários do código estão em inglês (como no original).
# Keep reading messages
loop do
# Create an empty buffer to help us find the next stream of magic bytes (the start of a new message)
buffer = ''
# Keep looping to read bytes from the socket
loop do
# Read one byte at the time
byte = socket.read(1)
# Check that we haven't been disconnected from the node.
if byte.nil?
puts "Read a nil byte from the socket. Looks like the remote node has disconnected from us. We probably failed the handshake too many times, or didn't respond to enough pings. No worries, try connecting to another node for the time being instead."
exit
end
# Add each byte to the temporary buffer
buffer += byte.unpack("H*").join unless byte.nil? # do not do anything if we got a nil byte for some reason
# Check the buffer when it reaches 4 bytes
if (buffer.size == 8) # 8 hexadecimal characters = 4 bytes
# See if the buffer matches the magic bytes
if (buffer == 'f9beb4d9')
# If we've got the magic bytes we're looking for, go ahead and read the full message from the socket
command = socket.read(12).to_s.delete("\x00") # convert to ascii and remove any empty bytes
size = socket.read(4).unpack("V").join.to_i # convert to an integer
checksum = socket.read(4).unpack("H*").join # convert to hexadecimal string of bytes
payload = socket.read(size).unpack("H*").join # use the size from the header to read the payload, then convert to a hexadecimal string
# Print the message
puts "<-#{command}"
puts "magic_bytes: " + buffer
puts "command: " + command
puts "size: " + size.to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Respond to all inv messages with getdata messages
if command == "inv"
# Set new command name
command = "getdata"
# Use the same payload as the one we got from the inv message
payload = payload
# Create message
magic_bytes = 'f9beb4d9'
command_hex = ascii2hex(command)
size = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum = checksum(payload)
message = magic_bytes + command_hex + size + checksum + payload
# Print the message header and payload
puts "#{command}->"
puts "magic_bytes: " + magic_bytes
puts "command: " + command
puts "size: " + (payload.size/2).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Send the message (convert from hexadecimal string to raw bytes first)
socket.write [message].pack("H*")
end
# Break out of the loop for reading a single message
break
end
# Reset the buffer and keep looking for a stream of magic bytes
buffer = ''
end
end
end E é assim que você pode obter as transações e blocos mais recentes de um nó real na rede.
Se você chegou até aqui e está tudo funcionando, você descobriu como conectar e se comunicar com um nó bitcoin do zero. Tudo daqui pra frente envolve apenas construir diferentes tipos de mensagem.
Aqui está uma lista completa das mensagens que os nós Bitcoin podem enviar uns aos outros.
6. Mantendo a Conexão
Uma última coisa antes de você ir: o nó ao qual você acabou de se conectar vai ocasionalmente te enviar mensagens "ping" para ver se você ainda está lá. Então, se você quer manter a conexão viva, vai precisar responder com mensagens "pong" em tempo hábil.
Ping
A partir da versão de protocolo 60001, cada mensagem "ping" contém um número aleatório como seu payload:
Payload: (ping)
┌─────────────┬─────────┬──────┬─────────────────────────┐
│ Nome │ Formato │ Tam. │ Bytes Exemplo │
├─────────────┼─────────┼──────┼─────────────────────────┤
│ Nonce │ bytes │ 8 │ 88 c8 49 39 65 b6 41 69 │
└─────────────┴─────────┴──────┴─────────────────────────┘
Pong
Sua mensagem "pong" de resposta só precisa conter o mesmo número no seu payload também:
Payload: (pong)
┌─────────────┬─────────┬──────┬─────────────────────────┐
│ Nome │ Formato │ Tam. │ Bytes Exemplo │
├─────────────┼─────────┼──────┼─────────────────────────┤
│ Nonce │ bytes │ 8 │ 88 c8 49 39 65 b6 41 69 │
└─────────────┴─────────┴──────┴─────────────────────────┘
Então, adicionando um último ajuste ao nosso laço, agora podemos manter a conexão aberta e receber transações e blocos para sempre:
Código
Código (Ruby) — respondendo a "ping" com "pong" (e "inv" com "getdata")
Laço final que responde a "inv" com "getdata" e a "ping" com "pong", mantendo a conexão viva. Os comentários do código estão em inglês (como no original).
# Keep reading messages
loop do
# Create an empty buffer to help us find the next stream of magic bytes (the start of a new message)
buffer = ''
# Keep looping to read bytes from the socket
loop do
# Read one byte at the time
byte = socket.read(1)
# Check that we haven't been disconnected from the node.
if byte.nil?
puts "Read a nil byte from the socket. Looks like the remote node has disconnected from us. We probably failed the handshake too many times, or didn't respond to enough pings. No worries, try connecting to another node for the time being instead."
exit
end
# Add each byte to the temporary buffer
buffer += byte.unpack("H*").join unless byte.nil? # do not do anything if we got a nil byte for some reason
# Check the buffer when it reaches 4 bytes
if (buffer.size == 8) # 8 hexadecimal characters = 4 bytes
# See if the buffer matches the magic bytes
if (buffer == 'f9beb4d9')
# If we've got the magic bytes we're looking for, go ahead and read the full message from the socket
command = socket.read(12).to_s.delete("\x00") # convert to ascii and remove any empty bytes
size = socket.read(4).unpack("V").join.to_i # convert to an integer
checksum = socket.read(4).unpack("H*").join # convert to hexadecimal string of bytes
payload = socket.read(size).unpack("H*").join # use the size from the header to read the payload, then convert to a hexadecimal string
# Print the message
puts "<-#{command}"
puts "magic_bytes: " + buffer
puts "command: " + command
puts "size: " + size.to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Respond to all inv messages with getdata messages
if command == "inv"
# Set new command name
command = "getdata"
# Use the same payload as the one we got from the inv message
payload = payload
# Create message
magic_bytes = 'f9beb4d9'
command_hex = ascii2hex(command)
size = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum = checksum(payload)
message = magic_bytes + command_hex + size + checksum + payload
# Print the message header and payload
puts "#{command}->"
puts "magic_bytes: " + magic_bytes
puts "command: " + command
puts "size: " + (payload.size/2).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Send the message (convert from hexadecimal string to raw bytes first)
socket.write [message].pack("H*")
end
# Respond to all ping messages with pong messages
if command == "ping"
# Set new command name
command = "pong"
# Use the same payload as the one we got from the ping message
payload = payload
# Create message
magic_bytes = 'f9beb4d9'
command_hex = ascii2hex(command)
size = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum = checksum(payload)
message = magic_bytes + command_hex + size + checksum + payload
# Print the message header and payload
puts "#{command}->"
puts "magic_bytes: " + magic_bytes
puts "command: " + command
puts "size: " + (payload.size/2).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Send the message (convert from hexadecimal string to raw bytes first)
socket.write [message].pack("H*")
end
# Break out of the loop for reading a single message
break
end
# Reset the buffer and keep looking for a stream of magic bytes
buffer = ''
end
end
end Funções. Eu repeti o mesmo código nos meus exemplos de código para manter tudo o mais legível possível. Seria melhor colocar o código para ler mensagens e enviar mensagens em suas próprias funções.
7. Encontrando Nós
Não sabe onde encontrar um nó ao qual você possa se conectar? Aqui estão alguns lugares que você pode tentar:
- Seu próprio nó. Se você baixar e rodar seu próprio nó Bitcoin Core no seu computador local, pode conectar a ele no endereço IP
127.0.0.1. Ou, se estiver hospedando-o em um servidor remoto, use o IP daquele servidor. - bitnodes.io – Este é um site útil que lista todos os nós disponíveis na rede Bitcoin que ele consegue encontrar.
- DNS Seeds. Há alguns servidores DNS mantidos por desenvolvedores confiáveis do Bitcoin Core que retornam alguns IPs de nós completos (full nodes) confiáveis. Você pode consultar esses DNS seeds usando qualquer ferramenta online de "DNS lookup". Aqui estão alguns exemplos de DNS Seeds:
seed.bitcoin.sipa.be– Pieter Wuillednsseed.bitcoin.dashjr.org– Luke Dashjrseed.bitcoin.sprovoost.nl– Sjors Provoost
Você pode realizar uma requisição DNS a um DNS seed pela linha de comando com: nslookup seed.bitcoin.sipa.be. Observe que isso pode não funcionar se você estiver usando uma VPN.
8. Resumo
Conectar a um nó do zero é uma forma legal de começar a programar no Bitcoin. Permite ver como os nós se comunicam uns com os outros, e te dá acesso ao vivo às transações e blocos mais recentes da rede.
Você pode conectar a um nó a partir de praticamente qualquer linguagem de programação que você quiser. Tudo o que você precisa é conseguir fazer conexões TCP e ter o IP e o número da porta de um computador rodando um nó bitcoin. Se você está rodando o bitcoin localmente, o IP vai ser 127.0.0.1 e a porta vai ser 8333 (por padrão).
A parte mais complicada de longe é descobrir como construir as mensagens. Você precisa colocar todos os bytes brutos de dados na ordem correta, porque, mesmo que erre só um byte, o nó para o qual você está enviando mensagens não vai te entender. E isso pode ser um processo um tanto frustrante até você acertar. Mas, depois que você enviou aquela primeira mensagem corretamente, todos os outros tipos de mensagem ficam muito mais fáceis de construir.
Obter minha primeira transação bruta de um nó bitcoin real, usando um script que escrevi do zero, foi uma das conquistas mais satisfatórias da minha carreira de programação.
Boa sorte.
Programa completo (Ruby)
O programa completo deste tutorial: conecta a um nó, faz o handshake e fica lendo mensagens (respondendo a ping e inv). Os comentários do código estão em inglês (como no original).
# Sockets are in the standard library in Ruby
require 'socket'
# Open a TCP connection to an IP and port
socket = TCPSocket.open("127.0.0.1", 8333) # local computer = 127.0.0.1
require 'digest' # needed for creating checksums
# Handy functions for getting data in the right format for messages
def hexadecimal(number)
return number.to_s(16)
end
def size(data, size)
return data.rjust(size*2, '0') # pad the left of the data out with zeros up to a specific number of bytes (2 hexadecimal chars = 1 byte)
end
def reversebytes(bytes)
return bytes.scan(/../).reverse.join() # grab each 2 characters (1 byte) as an array, reverse the array, then join back together
end
def ascii2hex(string)
# Convert each character in the string to its hexadecimal byte representation
bytes = string.each_byte.map {|c| c.to_s(16) }.join()
# Pad up to 12 bytes (keeping the bytes for the ascii string on the left)
return bytes.ljust(24, '0')
end
def checksum(bytes)
# Hash the data twice
hash = Digest::SHA256.digest(Digest::SHA256.digest([bytes].pack("H*"))).unpack("H*")[0]
# Return the first 4 bytes (8 characters)
return hash[0...8]
end
# Create the payload for a version message
payload = reversebytes(size(hexadecimal(70014), 4)) # protocol version
payload += reversebytes(size(hexadecimal(0), 8)) # services e.g. (1<<3 | 1<<2 | 1<<0)
payload += reversebytes(size(hexadecimal(1640961477), 8)) # time
payload += reversebytes(size(hexadecimal(0), 8)) # remote node services
payload += "00000000000000000000ffff2e13894a" # remote node ipv6 (https://dnschecker.org/ipv4-to-ipv6.php)
payload += size(hexadecimal(8333), 2) # remote node port
payload += reversebytes(size(hexadecimal(0), 8)) # local node services
payload += "00000000000000000000ffff7f000001" # local node ipv6
payload += size(hexadecimal(8333), 2) # local node port
payload += reversebytes(size(hexadecimal(0), 8)) # nonce
payload += "00" # user agent (compact_size, followed by ascii bytes)
payload += reversebytes(size(hexadecimal(0), 4)) # last block
# Create the message header
magic_bytes = 'f9beb4d9'
command = ascii2hex('version') # 76 65 72 73 69 6F 6E 00 00 00 00 00
size = reversebytes(size(hexadecimal(payload.length/2), 4)) # 55 00 00 00
checksum = checksum(payload)
header = magic_bytes + command + size + checksum
# Combine the header and payload
message = header + payload
# 1. Send Version Message
# Prepare version message
version = message
# Write the message to the socket (the protocol sends and receives messages in raw bytes)
socket.write [version].pack("H*")
puts "version->"
puts version
puts
# 2. Receive Version Message
# Read the message header response from the socket
magic_bytes = socket.read(4)
command = socket.read(12)
size = socket.read(4)
checksum = socket.read(4)
# View the message header
puts "<-version"
puts "magic_bytes: " + magic_bytes.unpack("H*").join # convert raw bytes to hexadecimal characters
puts "command: " + command.to_s # to_s automatically converts raw bytes to ASCII characters
puts "size: " + size.unpack("V").join # V = 32-byte unsigned, little-endian
puts "checksum: " + checksum.unpack("H*").join
# Read the message payload
size = size.unpack("V").join.to_i
payload = socket.read(size)
# View the message payload
puts "payload: " + payload.unpack("H*").join
puts
# 3. Receive Verack Message (verack = version acknowledged)
# Read the message header response from the socket
magic_bytes = socket.read(4)
command = socket.read(12)
size = socket.read(4)
checksum = socket.read(4)
# View the message header
puts "<-verack"
puts "magic_bytes: " + magic_bytes.unpack("H*").join # convert raw bytes to hexadecimal characters
puts "command: " + command.to_s # to_s automatically converts raw bytes to ASCII characters
puts "size: " + size.unpack("V").join # V = 32-byte unsigned, little-endian
puts "checksum: " + checksum.unpack("H*").join
# Read the message payload (there shouldn't be any)
size = size.unpack("V").join.to_i
payload = socket.read(size)
# View the message payload (there shouldn't be any)
puts "payload: " + payload.unpack("H*").join
puts
# 4. Send Verack Message
# Create verack message
payload = '' # verack has no payload, it's just a message header
magic_bytes = 'f9beb4d9'
command = ascii2hex('verack')
size = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum = checksum(payload)
verack = magic_bytes + command + size + checksum + payload
# Write the message to the socket
socket.write [verack].pack("H*")
puts "verack->"
puts "magic_bytes: " + magic_bytes
puts "command: " + 'verack'
puts "size: " + size.to_i(16).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Keep reading messages
loop do
# Create an empty buffer to help us find the next stream of magic bytes (the start of a new message)
buffer = ''
# Keep looping to read bytes from the socket
loop do
# Read one byte at the time
byte = socket.read(1)
# Check that we haven't been disconnected from the node.
if byte.nil?
puts "Read a nil byte from the socket. Looks like the remote node has disconnected from us. We probably failed the handshake too many times, or didn't respond to enough pings. No worries, try connecting to another node for the time being instead."
exit
end
# Add each byte to the temporary buffer
buffer += byte.unpack("H*").join unless byte.nil? # do not do anything if we got a nil byte for some reason
# Check the buffer when it reaches 4 bytes
if (buffer.size == 8) # 8 hexadecimal characters = 4 bytes
# See if the buffer matches the magic bytes
if (buffer == 'f9beb4d9')
# If we've got the magic bytes we're looking for, go ahead and read the full message from the socket
command = socket.read(12).to_s.delete("\x00") # convert to ascii and remove any empty bytes
size = socket.read(4).unpack("V").join.to_i # convert to an integer
checksum = socket.read(4).unpack("H*").join # convert to hexadecimal string of bytes
payload = socket.read(size).unpack("H*").join # use the size from the header to read the payload, then convert to a hexadecimal string
# Print the message
puts "<-#{command}"
puts "magic_bytes: " + buffer
puts "command: " + command
puts "size: " + size.to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Respond to all inv messages with getdata messages
if command == "inv"
# Set new command name
command = "getdata"
# Use the same payload as the one we got from the inv message
payload = payload
# Create message
magic_bytes = 'f9beb4d9'
command_hex = ascii2hex(command)
size = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum = checksum(payload)
message = magic_bytes + command_hex + size + checksum + payload
# Print the message header and payload
puts "#{command}->"
puts "magic_bytes: " + magic_bytes
puts "command: " + command
puts "size: " + (payload.size/2).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Send the message (convert from hexadecimal string to raw bytes first)
socket.write [message].pack("H*")
end
# Respond to all ping messages with pong messages
if command == "ping"
# Set new command name
command = "pong"
# Use the same payload as the one we got from the ping message
payload = payload
# Create message
magic_bytes = 'f9beb4d9'
command_hex = ascii2hex(command)
size = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum = checksum(payload)
message = magic_bytes + command_hex + size + checksum + payload
# Print the message header and payload
puts "#{command}->"
puts "magic_bytes: " + magic_bytes
puts "command: " + command
puts "size: " + (payload.size/2).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Send the message (convert from hexadecimal string to raw bytes first)
socket.write [message].pack("H*")
end
# Break out of the loop for reading a single message
break
end
# Reset the buffer and keep looking for a stream of magic bytes
buffer = ''
end
end
end