Red (Networking)
Cómo conectarte y comunicarte con un nodo de la red Bitcoin
El código introductorio de esta página funciona para nodos hasta la v26.2.
Bitcoin Core v27.0 (publicado en abril de 2024) y versiones posteriores usan por defecto el protocolo versión 2 (BIP 324). Los mensajes subyacentes son los mismos; la diferencia es que ahora van cifrados, y eso no se cubre en esta guía.
Si ejecutas un nodo v27.0 o superior, todavía puedes comunicarte con él usando el código de ejemplo de esta página si defines -v2transport=0 (desactivando el protocolo v2 y usando el antiguo v1).
Aquí tienes una guía rápida sobre cómo conectarte y comunicarte con un nodo de la red Bitcoin.
0. Introducción
Bitcoin es un programa de computadora. Puedes descargarlo gratis.
Se ejecuta en un puerto abierto en tu computadora, lo que significa que cualquiera puede conectarse a él y comunicarse por Internet.
8333 por defecto.Cuando ejecutas Bitcoin, usa puertos para conectarse a otras computadoras que ejecutan el mismo programa. Así que, cuando hay muchas personas ejecutando Bitcoin, terminas con una red de computadoras conectadas y comunicándose.
Lo bueno de Bitcoin es que puedes escribir tu propio programa básico para conectarte a un nodo, si quieres. Solo necesitas saber hablar su idioma.
En esta guía te mostraré cómo conectarte a un nodo Bitcoin usando Ruby. Ruby es un lenguaje sencillo, así que deberías poder traducir el código al lenguaje que prefieras.
1. Conexión
Primero, dos datos rápidos que necesitas saber sobre el programa Bitcoin:
- Se ejecuta en el puerto
8333(normalmente) - Usa TCP para comunicarse
Así que todo lo que necesitas para conectarte a un nodo Bitcoin es la dirección IP de la computadora donde se está ejecutando y la capacidad de hacer conexiones TCP desde tu lenguaje de programación.
TCP = Transmission Control Protocol. Es una forma de que dos computadoras se comuniquen por Internet (una saluda primero, la otra responde con otro saludo, etc.). Tu computadora usó TCP cuando descargó esta página. No necesitas saber cómo funcionan estos protocolos: solo necesitas saber que Bitcoin usa TCP.
2. Mensajes
Un "mensaje" es solo un bloque de datos estructurados que los nodos Bitcoin se envían entre sí por la red. Todos tienen el mismo formato:
Aquí tienes un ejemplo de cómo se ve un mensaje Bitcoin real:
Cabecera: F9BEB4D976657273696F6E0000000000550000002C2F86F3
Payload: 7E1101000000000000000000C515CF6100000000000000000000000000000000000000000000FFFF2E13894A208D000000000000000000000000000000000000FFFF7F000001208D00000000000000000000000000
Cuando construyes un mensaje para enviarlo a otro nodo, básicamente tomas datos legibles por humanos (como números y texto) y los conviertes en bytes legibles por la computadora, que pueden enviarse por la red de forma más eficiente.
Así que el truco para enviar mensajes en Bitcoin es colocar todos los datos en el formato correcto. Voy a usar un mensaje del tipo "version" como primer ejemplo, porque es el primer mensaje que quieres enviar a un nodo Bitcoin después de conectarte a él.
Version
Cabecera (Header)
La cabecera contiene un resumen del mensaje, y su estructura es la misma para cualquier mensaje en el protocolo Bitcoin:
Cabecera (mensaje version)
┌─────────────┬──────────────┬───────────────┬──────────┬─────────────────────────────────────┐
│ Nombre │ Datos Ex. │ Formato │ Tamaño │ 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 │
│ Tamaño │ 85 │ little-endian │ 4 │ 55 00 00 00 │
│ Checksum │ │ bytes │ 4 │ F7 63 9C 60 │
└─────────────┴──────────────┴───────────────┴──────────┴─────────────────────────────────────┘
- Magic Bytes: Un conjunto único de bytes usado para identificar el inicio de un nuevo mensaje. Siempre son los mismos. Como lees un flujo de bytes de la conexión TCP, es útil identificar cuándo empieza un nuevo mensaje.
- Comando: Indica el tipo de mensaje que se está enviando. Es un campo de 12 bytes que contiene la codificación ASCII del nombre del tipo de mensaje. Aquí indica que estamos enviando un mensaje "version".
ASCII
- Tamaño: El tamaño del payload que viene a continuación. Indica cuántos bytes necesitas leer del socket para obtener el mensaje completo.
- Checksum: Una pequeña huella digital del payload. Permite verificar rápidamente que los datos no fueron alterados en tránsito. Se crea haciendo el hash doble del payload y tomando los primeros 4 bytes del resultado.
Payload
El payload contiene el contenido principal del mensaje. Los distintos tipos de mensajes tienen estructuras diferentes para sus payloads. Aquí está el payload de un mensaje "version":
Payload (mensaje version):
┌───────────────────┬────────────────┬──────────────────────────┬─────────┐
│ Nombre │ Datos Ejemplo │ Formato │ Tamaño │
├───────────────────┼────────────────┼──────────────────────────┼─────────┤
│ Versión Protocolo │ 70014 │ little-endian │ 4 │
│ Services │ 0 │ bit field, little-endian │ 8 │
│ Tiempo │ 1640961477 │ little-endian │ 8 │
│ Services (Remoto) │ 0 │ bit field, little-endian │ 8 │
│ IP Remoto │ 46.19.137.74 │ ipv6, big-endian │ 16 │
│ Puerto Remoto │ 8333 │ big-endian │ 2 │
│ Services (Local) │ 0 │ bit field, little-endian │ 8 │
│ IP Local │ 127.0.0.1 │ ipv6, big-endian │ 16 │
│ Puerto Local │ 8333 │ big-endian │ 2 │
│ Nonce │ 0 │ little-endian │ 8 │
│ User Agent │ "" │ compact size, ascii │ compact │
│ Último Bloque │ 0 │ little-endian │ 4 │
└───────────────────┴────────────────┴──────────────────────────┴─────────┘
Un mensaje "version" es uno de los más complejos de Bitcoin, pero solo porque contiene mucha información. Es un buen punto de partida, porque si puedes construir un mensaje "version", puedes construir cualquier mensaje del protocolo Bitcoin. Resumen de los campos:
- Versión del Protocolo: La versión del protocolo que entiende tu nodo.
- Services: Una lista de servicios opcionales que ofrece tu nodo (campo de 64 bits / bit field). Puedes dejarlo en cero si solo estás probando.
- Tiempo: La hora de tu computadora como marca de tiempo Unix.
- IP/Puerto Remotos: La IP (en formato IPv6) y el puerto del nodo al que crees que te estás conectando.
- IP/Puerto Locales: Lo que crees que es tu IP/puerto local (en realidad no lo usa el nodo remoto).
- Nonce: Un número aleatorio que puede usarse para detectar conexiones contigo mismo. Puedes dejarlo en cero.
- User Agent: Una cadena para identificar la marca/modelo de tu nodo (por ejemplo, "/Satoshi:22.0.0/"). Puede dejarse en blanco, pero aún así necesitas incluir un byte
00. - Último Bloque: La altura del bloque superior de tu blockchain local. Déjalo en cero si no tienes bloques.
Código (Ruby) — construyendo el mensaje version
Ejemplo en Ruby para construir un mensaje version (con funciones auxiliares para formatear los datos):
require 'digest' # necesario para crear checksums
# Funciones auxiliares para dejar los datos en el formato correcto para los mensajes
def hexadecimal(number)
return number.to_s(16)
end
def size(data, size)
return data.rjust(size*2, '0') # rellena a la izquierda con ceros hasta un nº de bytes (2 chars hex = 1 byte)
end
def reversebytes(bytes)
return bytes.scan(/../).reverse.join() # toma cada 2 chars (1 byte), invierte y vuelve a unir
end
def ascii2hex(string)
# Convierte cada carácter de la cadena a su representación en bytes hexadecimales
bytes = string.each_byte.map {|c| c.to_s(16) }.join()
# Rellena hasta 12 bytes (manteniendo los bytes de la cadena ASCII a la izquierda)
return bytes.ljust(24, '0')
end
def checksum(bytes)
# Hace hash de los datos dos veces
hash = Digest::SHA256.digest(Digest::SHA256.digest([bytes].pack("H*"))).unpack("H*")[0]
# Devuelve los primeros 4 bytes (8 caracteres)
return hash[0...8]
end
# Crea el payload de un mensaje version
payload = reversebytes(size(hexadecimal(70014), 4)) # versión del protocolo
payload += reversebytes(size(hexadecimal(0), 8)) # servicios (ej.: 1<<3 | 1<<2 | 1<<0)
payload += reversebytes(size(hexadecimal(1640961477), 8)) # tiempo
payload += reversebytes(size(hexadecimal(0), 8)) # servicios del nodo remoto
payload += "00000000000000000000ffff2e13894a" # ipv6 del nodo remoto
payload += size(hexadecimal(8333), 2) # puerto del nodo remoto
payload += reversebytes(size(hexadecimal(0), 8)) # servicios del nodo local
payload += "00000000000000000000ffff7f000001" # ipv6 del nodo local
payload += size(hexadecimal(8333), 2) # puerto del nodo local
payload += reversebytes(size(hexadecimal(0), 8)) # nonce
payload += "00" # user agent (compact_size + bytes ascii)
payload += reversebytes(size(hexadecimal(0), 4)) # último bloque
# Crea la cabecera del mensaje
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 cabecera y payload
message = header + payload Y así es como queda nuestro mensaje final "version" como una cadena de bytes hexadecimales:
F9BEB4D976657273696F6E0000000000550000002C2F86F37E1101000000000000000000C515CF6100000000000000000000000000000000000000000000FFFF2E13894A208D000000000000000000000000000000000000FFFF7F000001208D00000000000000000000000000 3. Handshake
El handshake es el proceso que establece la comunicación entre dos dispositivos de red.
Antes de empezar a recibir datos, necesitamos hacer un "handshake". Ese handshake es solo una secuencia de mensajes que intercambiamos para empezar.
El handshake es básicamente un proceso de 2 pasos:
- Iniciamos la comunicación enviando nuestro mensaje "version", y ellos responden con su propio mensaje "version".
- Luego envían un mensaje "verack" (version acknowledgement) confirmando que recibieron nuestro mensaje version, y nosotros terminamos enviando un mensaje "verack" de vuelta.
El orden de los mensajes en el handshake es importante. Si te equivocas de orden, el handshake falla y el otro nodo rechaza tu conexión. Siempre puedes intentarlo de nuevo, pero si te equivocas demasiadas veces, podrías ser baneado temporalmente (solo conecta a otro nodo mientras tanto).
Verack
"Verack" es una cabecera de mensaje simple, sin payload:
Mensaje 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
Tamaño 4 bytes 00 00 00 00 (payload de tamaño 0)
Checksum 4 bytes 5D F6 E0 E2
Hexadecimal: F9BEB4D976657261636B000000000000000000005DF6E0E2
Un mensaje "verack" siempre es el mismo.
4. Recepción de Mensajes
El nodo al que acabas de conectarte enviará continuamente nuevos mensajes después del handshake. Así que, para seguir recibiendo, basta con seguir leyendo del socket en un bucle.
5. Solicitud de Transacciones y Bloques
Un nodo no te enviará abiertamente todas las nuevas transacciones y bloques que recibió. En su lugar, para ahorrar banda, te envía una lista de hashes de las transacciones y bloques más recientes en mensajes "inv" (inventory/inventario).
Luego puedes responder a esos mensajes "inv" indicando las transacciones y bloques específicos que quieres, con mensajes "getdata". Después de eso, el nodo te envía las transacciones y bloques completos en mensajes "tx" y "block" posteriores.
Inv
El payload de un mensaje "inv" contiene un Contador (compact size) seguido de uno o más vectores de Inventario. Cada elemento de inventario es un Tipo (4 bytes little-endian) + un Hash (32 bytes):
Tipos:
01 00 00 00 = MSG_TX (Hash de Transacción)
02 00 00 00 = MSG_BLOCK (Hash de Bloque)
Getdata
El mensaje "getdata" con el que respondes tiene exactamente la misma estructura que el mensaje "inv". Así que, si quieres todas las transacciones y bloques del "inv", puedes responder con el mismo payload. Si no quieres todos, construye un payload solo con los hashes que deseas.
Aquí tienes una lista completa de los mensajes que los nodos Bitcoin pueden intercambiar.
6. Mantener la Conexión
Una última cosa: el nodo al que te conectaste ocasionalmente envía mensajes "ping" para comprobar si sigues ahí. Así que, si quieres mantener la conexión viva, necesitas responder con mensajes "pong" a tiempo.
A partir de la versión de protocolo 60001, cada mensaje "ping" contiene un número aleatorio de 8 bytes (Nonce) como payload. Tu mensaje "pong" de respuesta solo necesita contener ese mismo número en el payload.
7. Encontrar Nodos
¿No sabes dónde encontrar un nodo al que conectarte? Aquí tienes algunos lugares para probar:
- Tu propio nodo. Si descargas y ejecutas tu propio nodo Bitcoin Core localmente, puedes conectarte a él en la IP
127.0.0.1. - bitnodes.io – Un sitio útil que lista los nodos disponibles en la red Bitcoin que consigue encontrar.
- DNS Seeds. Hay servidores DNS mantenidos por desarrolladores confiables de Bitcoin Core que devuelven IPs de nodos completos confiables (por ejemplo:
seed.bitcoin.sipa.be,seed.bitcoin.sprovoost.nl).
Puedes hacer una consulta DNS a un DNS seed en la línea de comandos con: nslookup seed.bitcoin.sipa.be.
8. Resumen
Conectarte a un nodo desde cero es una forma buena de empezar a programar en Bitcoin. Te permite ver cómo se comunican los nodos y te da acceso en vivo a las transacciones y bloques más recientes de la red.
Puedes conectarte a un nodo en prácticamente cualquier lenguaje. Solo necesitas poder hacer conexiones TCP y tener la IP y el puerto de una computadora que esté ejecutando un nodo (localmente, IP 127.0.0.1 y puerto 8333).
La parte más complicada con diferencia es descubrir cómo construir los mensajes. Necesitas poner todos los bytes en el orden correcto, porque si te equivocas en un byte, el nodo no te va a entender. Pero, después de enviar correctamente el primer mensaje, todos los demás tipos de mensajes se vuelven mucho más fáciles.
Obtener mi primera transacción bruta de un nodo Bitcoin real, usando un script que escribí desde cero, fue uno de los logros más satisfactorios de mi carrera como programador.
Buena suerte.
Programa completo (Ruby)
El programa completo de este tutorial: se conecta a un nodo, hace el handshake y sigue leyendo mensajes (respondiendo a ping e inv). Los comentarios del código están en inglés (como en el original).
# Los sockets están en la librería estándar de Ruby
require 'socket'
# Abre una conexión TCP a una IP y un puerto
socket = TCPSocket.open("127.0.0.1", 8333) # computadora local = 127.0.0.1
require 'digest' # necesario para crear checksums
# Funciones útiles para obtener datos en el formato correcto para los mensajes
def hexadecimal(number)
return number.to_s(16)
end
def size(data, size)
return data.rjust(size*2, '0') # rellena a la izquierda con ceros hasta un nº específico de bytes (2 chars hex = 1 byte)
end
def reversebytes(bytes)
return bytes.scan(/../).reverse.join() # toma cada 2 caracteres (1 byte) en un array, invierte el array y vuelve a unir
end
def ascii2hex(string)
# Convierte cada carácter de la cadena a su representación hexadecimal en bytes
bytes = string.each_byte.map {|c| c.to_s(16) }.join()
# Rellena hasta 12 bytes (manteniendo los bytes de la cadena ASCII a la izquierda)
return bytes.ljust(24, '0')
end
def checksum(bytes)
# Hace hash de los datos dos veces
hash = Digest::SHA256.digest(Digest::SHA256.digest([bytes].pack("H*"))).unpack("H*")[0]
# Devuelve los primeros 4 bytes (8 caracteres)
return hash[0...8]
end
# Crea el payload de un mensaje version
payload = reversebytes(size(hexadecimal(70014), 4)) # versión del protocolo
payload += reversebytes(size(hexadecimal(0), 8)) # servicios, por ejemplo (1<<3 | 1<<2 | 1<<0)
payload += reversebytes(size(hexadecimal(1640961477), 8)) # hora
payload += reversebytes(size(hexadecimal(0), 8)) # servicios del nodo remoto
payload += "00000000000000000000ffff2e13894a" # ipv6 del nodo remoto (https://dnschecker.org/ipv4-to-ipv6.php)
payload += size(hexadecimal(8333), 2) # puerto del nodo remoto
payload += reversebytes(size(hexadecimal(0), 8)) # servicios del nodo local
payload += "00000000000000000000ffff7f000001" # ipv6 del nodo local
payload += size(hexadecimal(8333), 2) # puerto del nodo local
payload += reversebytes(size(hexadecimal(0), 8)) # nonce
payload += "00" # user agent (compact_size, seguido de bytes ascii)
payload += reversebytes(size(hexadecimal(0), 4)) # último bloque
# Crea la cabecera del mensaje
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
# Combina la cabecera y el payload
message = header + payload
# 1. Enviar mensaje Version
# Prepara el mensaje version
version = message
# Escribe el mensaje en el socket (el protocolo envía y recibe mensajes en bytes crudos)
socket.write [version].pack("H*")
puts "version->"
puts version
puts
# 2. Recibir mensaje Version
# Lee la respuesta de la cabecera del mensaje desde el socket
magic_bytes = socket.read(4)
command = socket.read(12)
size = socket.read(4)
checksum = socket.read(4)
# Muestra la cabecera del mensaje
puts "<-version"
puts "magic_bytes: " + magic_bytes.unpack("H*").join # convierte bytes crudos a caracteres hexadecimales
puts "command: " + command.to_s # to_s convierte automáticamente bytes crudos a ASCII
puts "size: " + size.unpack("V").join # V = entero sin signo de 32 bits, little-endian
puts "checksum: " + checksum.unpack("H*").join
# Lee el payload del mensaje
size = size.unpack("V").join.to_i
payload = socket.read(size)
# Muestra el payload
puts "payload: " + payload.unpack("H*").join
puts
# 3. Recibir mensaje Verack (verack = version acknowledged)
# Lee la respuesta de la cabecera del mensaje desde el socket
magic_bytes = socket.read(4)
command = socket.read(12)
size = socket.read(4)
checksum = socket.read(4)
# Muestra la cabecera del mensaje
puts "<-verack"
puts "magic_bytes: " + magic_bytes.unpack("H*").join # convierte bytes crudos a caracteres hexadecimales
puts "command: " + command.to_s # to_s convierte automáticamente bytes crudos a ASCII
puts "size: " + size.unpack("V").join # V = entero sin signo de 32 bits, little-endian
puts "checksum: " + checksum.unpack("H*").join
# Lee el payload del mensaje (no debería haber ninguno)
size = size.unpack("V").join.to_i
payload = socket.read(size)
# Muestra el payload del mensaje (no debería haber ninguno)
puts "payload: " + payload.unpack("H*").join
puts
# 4. Enviar mensaje Verack
# Crea el mensaje verack
payload = '' # verack no tiene payload, solo una cabecera de mensaje
magic_bytes = 'f9beb4d9'
command = ascii2hex('verack')
size = reversebytes(size(hexadecimal(payload.size/2), 4))
checksum = checksum(payload)
verack = magic_bytes + command + size + checksum + payload
# Escribe el mensaje en el 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
# Sigue leyendo mensajes
loop do
# Crea un buffer vacío para ayudarnos a encontrar el siguiente flujo de bytes mágicos (el inicio de un nuevo mensaje)
buffer = ''
# Sigue leyendo bytes del socket
loop do
# Lee un byte a la vez
byte = socket.read(1)
# Verifica que no nos hayan desconectado del nodo.
if byte.nil?
puts "Leí un byte nulo desde el socket. Parece que el nodo remoto se desconectó de nosotros. Probablemente fallamos el handshake demasiadas veces o no respondimos a suficientes pings. No pasa nada, prueba conectarte a otro nodo por ahora."
exit
end
# Agrega cada byte al buffer temporal
buffer += byte.unpack("H*").join unless byte.nil? # no hagas nada si por alguna razón obtuvimos un byte nulo
# Revisa el buffer cuando alcance 4 bytes
if (buffer.size == 8) # 8 caracteres hexadecimales = 4 bytes
# Comprueba si el buffer coincide con los bytes mágicos
if (buffer == 'f9beb4d9')
# Si tenemos los bytes mágicos que buscamos, leemos el mensaje completo del socket
command = socket.read(12).to_s.delete("\x00") # convierte a ASCII y elimina bytes vacíos
size = socket.read(4).unpack("V").join.to_i # convierte a entero
checksum = socket.read(4).unpack("H*").join # convierte a string hexadecimal de bytes
payload = socket.read(size).unpack("H*").join # usa el tamaño de la cabecera para leer el payload, luego convierte a string hexadecimal
# Imprime el mensaje
puts "<-#{command}"
puts "magic_bytes: " + buffer
puts "command: " + command
puts "size: " + size.to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Responde a todos los mensajes inv con mensajes getdata
if command == "inv"
# Define el nuevo nombre del comando
command = "getdata"
# Usa el mismo payload que recibimos del mensaje inv
payload = payload
# Crea el mensaje
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
# Imprime la cabecera y el payload del mensaje
puts "#{command}->"
puts "magic_bytes: " + magic_bytes
puts "command: " + command
puts "size: " + (payload.size/2).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Envía el mensaje (convierte primero de cadena hexadecimal a bytes crudos)
socket.write [message].pack("H*")
end
# Responde a todos los mensajes ping con mensajes pong
if command == "ping"
# Define el nuevo nombre del comando
command = "pong"
# Usa el mismo payload que recibimos del mensaje ping
payload = payload
# Crea el mensaje
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
# Imprime la cabecera y el payload del mensaje
puts "#{command}->"
puts "magic_bytes: " + magic_bytes
puts "command: " + command
puts "size: " + (payload.size/2).to_s
puts "checksum: " + checksum
puts "payload: " + payload
puts
# Envía el mensaje (convierte primero de cadena hexadecimal a bytes crudos)
socket.write [message].pack("H*")
end
# Sale del bucle de lectura de un único mensaje
break
end
# Reinicia el buffer y sigue buscando una secuencia de bytes mágicos
buffer = ''
end
end
end