Está en la página 1de 21

CAPÍTULO 10

Redes

La red peer-to-peer en la que se ejecuta Bitcoin es lo que le da mucha solidez. Al


momento de escribir este artículo, más de 65,000 nodos se están ejecutando en la red
y son com- comunicando constantemente.
La red Bitcoin es una red de difusión o red de chismes. Cada nodo está anunciando
diferentes transacciones, bloques y pares que conoce. El proto- col es rico y tiene
muchas características que se le han agregado a lo largo de los años.
Una cosa a tener en cuenta sobre el protocolo de red es que no es crítico para el
consenso. Los mismos datos se pueden enviar de un nodo a otro utilizando algún otro
protocolo y la cadena de bloques en sí no se verá afectada.
Con eso en mente, trabajaremos en este capítulo para solicitar, recibir y validar- data
de encabezados de bloque utilizando el protocolo de red.

Mensajes de red
Todos los mensajes de red se ven como Figura 10-1..
Los primeros 4 bytes son siempre los mismos y se conocen como la magia de la red.
Los bytes mágicos son comunes en la programación de redes, ya que la
comunicación es asíncrona y puede ser intermitente. Los bytes mágicos le dan al
receptor del mensaje un lugar para comenzar si la comunicación se interrumpe (por
ejemplo, por la señal de caída de su teléfono). También son útiles para identificar la
red. No querría que un nodo de Bitcoin se conecte a un nodo de Litecoin, por
ejemplo. Por lo tanto, un nodo Litecoin tiene una magia diferente. El testnet de
Bitcoin también tiene una magia diferente,0b110907, a diferencia de la magia de la
red principal de Bitcoin, f9beb4d9.

177
Figura 10-1. Un mensaje de red: el sobre que contiene la carga útil real

Los siguientes 12 bytes son el campo de comando, o una descripción de lo que


realmente transporta la carga útil. Hay muchos comandos diferentes; se puede ver
una lista exhaustiva en eldocumentación. El campo de comando está destinado a ser
legible por humanos y este par- El mensaje ticular es la "versión" de la cadena de
bytes en ASCII con relleno de 0 bytes.
Los siguientes 4 bytes son la longitud de la carga útil en little-endian. Como vimos
en los capítulos5 5 y 9 9, la longitud de la carga útil es necesaria ya que la carga útil
es variable. 232 es aproximadamente 4 mil millones, por lo que las cargas útiles
pueden ser tan grandes como 4 GB, aunque el cliente de referencia rechaza cualquier
carga útil de más de 32 MB. En el mensaje de la Figura 10-1, nuestra carga útil es de
101 bytes.
Los siguientes 4 bytes son el campo de suma de verificación. El algoritmo de suma
de comprobación es una elección extraña, ya que son los primeros 4 bytes del
hash256 de la carga útil. Es una elección extraña porque las sumas de verificación
del protocolo de red normalmente están diseñadas para tener capacidad de corrección
de errores y hash256 no tiene ninguna. Dicho esto, hash256 es común en el resto del
protocolo de Bitcoin, que probablemente sea la razón por la que se usa aquí.
El código para manejar los mensajes de red requiere que creemos una nueva clase:
NETWORK_MAGIC = si'\ xf9 \ xbe \ xb4 \ xd9'
TESTNET_NETWORK_MAGIC = si'\ x0b \ x11 \
x09 \ x07'

clase NetworkEnvelope:

def __en eso__(yo, mando, carga útil, testnet=Falso):


yo.mando = mando
yo.carga útil = carga útil
Si testnet:
yo.magia = TESTNET_NETWORK_MAGIC
más:
yo.magia = NETWORK_MAGIC

def __repr__(yo):
regreso '{}: {}'.formato(
yo.mando.descodificar(ascii),
178 El | Capítulo 10: Redes
yo.carga útil.maleficio(),
)

Ejercicio 1
Escribe el analizar gramaticalmente método para NetworkEnvelope.

Ejercicio 2
Determine cuál es este mensaje de red:
f9beb4d976657261636b000000000000000000005df6e0e2

Ejercicio 3
Escribe el publicar por fascículos método para NetworkEnvelope.

Analizando la carga útil


Cada comando tiene una especificación de carga útil separada. Figura 10-2. es el
pago analizado- cargar para versión.

Figura 10-2. Versión analizada

Los campos están destinados a proporcionar información suficiente para que dos
nodos puedan comunicarse.

Analizando la carga útil


El |
179
El primer campo es la versión del protocolo de red, que especifica qué mensajes se
pueden comunicar. El campo de servicio proporciona información sobre las
capacidades disponibles.-capaz de conectar nodos. El campo de marca de tiempo es
de 8 bytes (en oposición a 4 bytes en el encabezado del bloque) y es la marca de
tiempo de Unix en little-endian.
Las direcciones IP pueden ser IPv6, IPv4 o OnionCat (una asignación de TOR's
.cebolladirecciones a IPv6). Si es IPv4, los primeros 12 bytes
son00000000000000000000ffffy los últimos 4 bytes son la IP. El puerto es de 2
bytes en little-endian. El valor predeterminado en mainnet es 8333, que se asigna
a8d20 en hechizo little-endian.
Un nonce es un número utilizado por un nodo para detectar una conexión consigo
mismo. El agente de usuario identifica el software que se está ejecutando. La altura o
el último campo de bloque ayuda al otro nodo a saber en qué bloque se sincroniza un
nodo.
El relé se usa para los filtros Bloom, que veremos en Capítulo 12.
Estableciendo algunos valores predeterminados razonables, nuestro
VersionMessage la clase se ve así:
clase VersionMessage:
mando = si'versión'

def __en eso__(yo, versión=70015, servicios=0 0, marca de


tiempo=Ninguna,
servicios_receptor=0 0,
receptor_ip=si'\ x00 \ x00 \ x00 \ x00',
puerto_receptor=8333,
servicios_envío=0 0,
sender_ip=si'\ x00 \ x00 \ x00 \ x00',
puerto_sensor=8333,
mientras tanto=Ninguna, agente de
usuario=si'/programmingbitcoin:0.1/',
último_bloque=0 0, relé=Falso):
yo.versión = versión
yo.servicios = servicios
Si marca de tiempo es Ninguna:
yo.marca de tiempo = En t(hora.hora())
más:
yo.marca de tiempo = marca de tiempo
yo.servicios_receptor = servicios_receptor
yo.receptor_ip = receptor_ip
yo.puerto_receptor = puerto_receptor
yo.servicios_envío = servicios_envío
yo.sender_ip = sender_ip
yo.puerto_sensor = puerto_sensor
Si mientras tanto es Ninguna:
yo.mientras tanto = int_to_little_endian(randint(0 0, 2**
**64), 8)
más:
yo.mientras tanto = mientras tanto
yo.agente de usuario = agente de usuario
yo.último_bloque = último_bloque
yo.relé = relé
En este punto, necesitamos una forma de serializar este mensaje.

180 El | Capítulo 10: Redes


Ejercicio 4
Escribe el publicar por fascículos método para VersionMessage.

Apretón de manos de red


El apretón de manos de la red es cómo los nodos establecen comunicación:

• A quiere conectarse a B y envía un mensaje de versión.


• B recibe el mensaje de versión, responde con un mensaje verack y envía su
propio mensaje de versión.
• A recibe la versión y los mensajes verack y devuelve un mensaje verack.
• B recibe el mensaje verack y continúa la comunicación.

Una vez que finaliza el apretón de manos, A y B pueden comunicarse como quieran.
Tenga en cuenta que aquí no hay autenticación, y depende de los nodos verificar
todos los datos que reciben. Si un nodo envía una transacción o un bloque incorrecto,
puede esperar que lo baneen o deshabilite- conectado.

Conectando a la red
La comunicación de red es complicada debido a su naturaleza asincrónica. Para
experimentar, podemos establecer una conexión a un nodo en la red de forma
síncrona:
> importar enchufe
> desde red importar NetworkEnvelope, VersionMessage
> anfitrión = 'testnet.programmingbitcoin.com'
> Puerto = 18333
> enchufe = enchufe.enchufe(enchufe.AF_INET, enchufe.SOCK_STREAM)
> enchufe.conectar((anfitrión, Puerto))
> corriente = enchufe.makefile('rb', Ninguna)
> versión = VersionMessage()
> sobre = NetworkEnvelope(versión.mando, versión.publicar por
fascículos())
> enchufe.Envia todo(sobre.publicar por fascículos())
> mientras Cierto:
... nuevo mensaje = NetworkEnvelope.analizar
gramaticalmente(corriente)
... impresión(nuevo mensaje)

Este es un servidor que configuré para Testnet. El puerto testnet es 18333 por
defecto.

Creamos una secuencia para poder leer desde el socket. Una secuencia realizada
de esta manera se puede pasar a todos los métodos de análisis.

El primer paso del apretón de manos es enviar un mensaje de versión.


Apretón de manos de red
El |
181
Ahora enviamos el mensaje en el sobre correcto.

Esta línea leerá cualquier mensaje que ingrese a través de nuestro socket
conectado.
Conectando de esta manera, no podemos enviar hasta que hayamos recibido y no
podamos responder intelli-suavemente a más de un mensaje a la vez. Una
implementación más robusta usaría una biblioteca asincrónica (como asyncio en
Python 3) para enviar y recibir sin ser bloqueado.
También necesitamos una clase de mensaje verack, que crearemos aquí:
clase VerAckMessage:
mando = si'verack'

def __en eso__(yo):


pasar

@classmethod
def analizar gramaticalmente(cls, s):
regreso cls()

def publicar por fascículos(yo):


regreso si''

UNA VerAckMessage Es un mensaje de red mínimo.


Ahora automaticemos esto creando una clase que maneje la comunicación por nosotros:
clase SimpleNode:

def __en eso__(yo, anfitrión, Puerto=Ninguna, testnet=Falso, Inicio


sesión=Falso):
Si Puerto es Ninguna:
Si testnet:
Puerto = 18333
más:
Puerto = 8333
yo.testnet = testnet
yo.Inicio sesión = Inicio sesión
yo.enchufe = enchufe.enchufe(enchufe.AF_INET,
enchufe.SOCK_STREAM)
yo.enchufe.conectar((anfitrión, Puerto))
yo.corriente = yo.enchufe.makefile('rb', Ninguna)

def enviar(yo, mensaje):


'' 'Enviar un mensaje al nodo conectado' ''
sobre = NetworkEnvelope(
mensaje.mando, mensaje.publicar por fascículos(),
testnet=yo.testnet)
Si yo.Inicio sesión:
impresión('enviando: {}'.formato(sobre))
yo.enchufe.Envia todo(sobre.publicar por fascículos())

def leer(yo):
'' 'Leer un mensaje del socket' ''
182 El | Capítulo 10: Redes
sobre = NetworkEnvelope.analizar gramaticalmente(yo.corriente,
testnet=yo.testnet)
Si yo.Inicio sesión:
impresión('recibiendo: {}'.formato(sobre))
regreso sobre

def esperar_para(yo, * *mensaje_clases):


'' 'Espere uno de los mensajes en la
lista' '' mando = Ninguna
command_to_class = {metro.mando: metro para metro en
mensaje_clases} mientras mando no en
command_to_class.llaves():
sobre = yo.leer()
mando = sobre.mando
Si mando == VersionMessage.mando:
yo.enviar(VerAckMessage())
elif mando == PingMessage.mando:
yo.enviar(PongMessage(sobre.carga útil))
regreso command_to_class[mando].analizar
gramaticalmente(sobre.corriente())

los enviarEl método envía un mensaje a través del socket. los mando propiedad
y publicar por fascículos se espera que existan métodos en el mensaje
objeto.

los leer El método lee un nuevo mensaje del socket.

los esperar_paraEl método nos permite esperar cualquiera de varios


comandos (específicamente, clases de mensajes). Junto con la naturaleza
sincrónica de esta clase, un método como este facilita la programación. Un nodo
de fuerza comercial defi- Nitely no use algo como esto.
Ahora que tenemos un nodo, podemos estrechar la mano con otro nodo:
> desde red importar SimpleNode, VersionMessage
> nodo = SimpleNode('testnet.programmingbitcoin.com', testnet=Cierto)
> versión = VersionMessage()
> nodo.enviar(versión)
> verack_received = Falso
> version_received = Falso
> mientras no verack_received y no version_received:
... mensaje = nodo.esperar_para(VersionMessage, VerAckMessage)
... Si mensaje.mando == VerAckMessage.mando:
... verack_received = Cierto
... más:
... version_received = Cierto
... nodo.enviar(VerAckMessage())

La mayoría de los nodos no se preocupan por los campos en versióncomo la


dirección IP Podemos estafar- conecta con los valores predeterminados y todo
estará bien.

Comenzamos el apretón de manos enviando el mensaje de versión.


Conectando a la red El |
183
Solo terminamos cuando recibimos tanto verack como versión.

Esperamos recibir un verack para nuestra versión y la versión del otro nodo. Sin
embargo, no sabemos en qué orden llegarán.

Ejercicio 5
Escribe el apretón de manos método para SimpleNode.

Obtener encabezados de bloque


Ahora que tenemos código para conectarnos a un nodo, ¿qué podemos hacer?
Cuando cualquier nodo se conecta por primera vez a la red, los datos más cruciales
para obtener y verificar son los encabezados de bloque. Para nodos completos, la
descarga de los encabezados de bloque les permite sincronizar-solicite bloques
completos de múltiples nodos, paralelizando la descarga de los bloques. Para clientes
livianos, descargar encabezados les permite verificar la prueba de trabajo en cada
bloque. Como veremos enCapítulo 11, los clientes ligeros podrán obtener pruebas de
inclusión a través de la red, pero eso requiere que los clientes ligeros tengan los
encabezados de bloque.
Los nodos nos pueden dar los encabezados de bloque sin ocupar mucho ancho de
banda. El COM- mand para obtener los encabezados de bloque se llama
getheadersy parece Figura 10-3..

Figura 10-3. Getheaders analizados

Al igual que con la versión, comenzamos con la versión del protocolo, luego el
número de grupos de encabezado de bloque en esta lista (este número puede ser más
de 1 si hay una división de cadena), luego el encabezado de bloque inicial y
finalmente el encabezado de bloque final. Si especificamos que el bloque final
sea000 ... 000, estamos indicando que queremos tantos como el otro nodo nos
dará. El número máximo de encabezados que podemos recuperar es de 2,000, o casi
un solo período de ajuste de dificultad (2,016 bloques).
Así es como se ve la clase:
clase GetHeadersMessage:
mando = si'getheaders'

def __en eso__(yo, versión=70015, num_hashes=1,


184 El | Capítulo 10: Redes
bloque_inicio=Ninguna, bloque_final=Ninguna):
yo.versión = versión
yo.num_hashes = num_hashes
Si bloque_inicio es Ninguna:
aumento Error de tiempo de ejecución('se requiere un
bloque de inicio')
yo.bloque_inicio = bloque_inicio
Si bloque_final es Ninguna:
yo.bloque_final = si'\ x00' * * 32
más:
yo.bloque_final = bloque_final

Para los propósitos de este capítulo, vamos a suponer que el número de grupos
de encabezado de bloque es 1. Una implementación más robusta manejaría más
que un solo grupo de bloque, pero podemos descargar los encabezados de bloque
usando un solo grupo.

Se necesita un bloque de inicio, de lo contrario no podemos crear un mensaje


adecuado.

El bloque final suponemos que es nulo, o tantos como el servidor nos enviará si
no está definido.

Ejercicio 6
Escribe el publicar por fascículos método para GetHeadersMessage.

Respuesta de encabezados
Ahora podemos crear un nodo, un apretón de manos y luego pedir algunos
encabezados:
> desde io importar BytesIO
> desde bloquear importar Bloquear, GENESIS_BLOCK
> desde red importar SimpleNode, GetHeadersMessage
> nodo = SimpleNode('mainnet.programmingbitcoin.com', testnet=Falso)
> nodo.apretón de manos()
> génesis = Bloquear.analizar
gramaticalmente(BytesIO(GENESIS_BLOCK))
> getheaders =
GetHeadersMessage(bloque_inicio=génesis.picadillo())
> nodo.enviar(getheaders)

Ahora necesitamos una forma de recibir los encabezados del otro nodo. El otro nodo
enviará de vuelta elencabezadosmando. Esta es una lista de encabezados de bloque
(Figura 10-4.), que ya aprendimos a analizar en Capítulo 9. losHeadersMessage La
clase puede aprovechar esto al analizar.
Respuesta de
encabezados El |
185
Figura 10-4. Encabezados analizados

El mensaje de encabezado comienza con el número de encabezados como varint, que es


un número de 1 a 2,000 inclusive. Cada encabezado de bloque, sabemos, es de 80 bytes.
Luego tenemos el número de transacciones. El número de transacciones en el mensaje de
encabezado es siempre
1. Esto puede ser un poco confuso al principio, ya que solo pedimos los encabezados
y no las transacciones. La razón por la que los nodos se molestan en enviar el número
de transacciones es porque el mensaje de encabezado debe ser compatible con el
formato del mensaje de bloque, que es el encabezado del bloque, el número de
transacciones y luego la transacción-las propias Al especificar que el número de
transacciones es 0, podemos usar el mismo motor de análisis que al analizar un
bloque completo:
clase HeadersMessage:
mando = si'encabezados'

def __en eso__(yo, bloques):


yo.bloques = bloques

@classmethod
def analizar gramaticalmente(cls, corriente):
num_headers = read_varint(corriente)
bloques = []
para _ _ en rango(num_headers):
bloques.adjuntar(Bloquear.analizar
gramaticalmente(corriente))
num_txs = read_varint(corriente)
Si num_txs ! = 0 0:
aumento Error de tiempo de ejecución('número de txs
no 0')
regreso cls(bloques)

Cada bloque se analiza con el Bloquear de la clase analizar


gramaticalmente método, usando la misma secuencia que tenemos.

El número de transacciones siempre es 0 y es un remanente del análisis de


bloques.

Si no obtuvimos 0, algo está mal.


Dada la conexión de red que hemos configurado, podemos descargar los
encabezados, verificar su prueba de trabajo y validar los ajustes de dificultad del
encabezado de bloque de la siguiente manera:
186 El | Capítulo 10: Redes
> desde io importar BytesIO
> desde red importar SimpleNode, GetHeadersMessage, HeadersMessage
> desde bloquear importar Bloquear, GENESIS_BLOCK, LOWEST_BITS
> desde ayudante importar calcular_bits_nuevos
> anterior = Bloquear.analizar
gramaticalmente(BytesIO(GENESIS_BLOCK))
> first_epoch_timestamp = anterior.marca de tiempo
> bits esperados = LOWEST_BITS
> contar = 1
> nodo = SimpleNode('mainnet.programmingbitcoin.com', testnet=Falso)
> nodo.apretón de manos()
> para _ _ en rango(19):
... getheaders =
GetHeadersMessage(bloque_inicio=anterior.picadillo())
... nodo.enviar(getheaders)
... encabezados = nodo.esperar_para(HeadersMessage)
... para encabezamiento en encabezados.bloques:
... Si no encabezamiento.check_pow():
... aumento Error de tiempo de ejecución('mal PoW en el bloque
{}'.formato(contar))
... Si encabezamiento.prev_block ! = anterior.picadillo():
... aumento Error de tiempo de ejecución('bloqueo discontinuo en
{}'.formato(contar))
... Si contar % 2016 == 0 0:
... time_diff = anterior.marca de tiempo - first_epoch_timestamp
... bits esperados = calcular_bits_nuevos(anterior.pedacitos,
time_diff)
... impresión(bits esperados.maleficio())
... first_epoch_timestamp = encabezamiento.marca de tiempo
... Si encabezamiento.pedacitos ! = bits esperados:
... aumento Error de tiempo de ejecución('bits defectuosos en el
bloque {}'.formato(contar))
... anterior = encabezamiento
... contar + = 1
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
ffff001d
6ad8001d
28c4001d
71be001d

Verifique que la prueba de trabajo sea válida.

Verifique que el bloque actual sea posterior al anterior.


Respuesta de
encabezados El |
187
Compruebe que los bits / objetivo / dificultad es lo que esperamos en función del
cálculo de época anterior.

Al final de la época, calcule los siguientes bits / objetivo / dificultad.

Almacene el primer bloque de la época para calcular los bits al final de la época.
Tenga en cuenta que esto no funcionará en testnet ya que el algoritmo de ajuste de
dificultad es diferente-ent. Para asegurarse de que se puedan encontrar bloques
consistentemente para las pruebas, si no se ha encontrado un bloque en testnet en 20
minutos, la dificultad se reduce a 1, por lo que es muy fácil encontrar un bloque. Esto
se configura de esta manera para permitir que los evaluadores puedan mantener los
bloques de construcción en la red sin costosos equipos de minería. Un ASIC USB de
$ 30 generalmente puede encontrar algunos bloques por minuto en la dificultad
mínima.

Conclusión
Hemos logrado conectarnos a un nodo en la red, apretón de manos, descargar los
encabezados de bloque y verificar que cumplen con las reglas de consenso. En el
próximo capítulo, nos enfocamos en obtener información sobre transacciones que
nos interesan de otro nodo de manera privada pero comprobable.

188 El | Capítulo 10: Redes

También podría gustarte