En octubre de 2024, el proyecto Bitcoin Core reveló una Denegación de Servicio debido a que los conjuntos inv-to-send crecían demasiado, de la cual fui autor, para las versiones de Bitcoin Core anteriores a la v25.0. Tengo algunas notas y capturas de pantalla de mi investigación de entonces que quiero que permanezcan aquí. A principios de mayo de 2023, mi infraestructura de monitorización notó este error que afectaba a los nodos de la red principal, lo que me permitió identificar de dónde provenía el problema. El mérito de trabajar en una solución es de Anthony Towns.
El 2 de mayo de 2023, noté que en uno de mis nodos de monitorización, las conexiones entrantes habían disminuido de aproximadamente 1901 a solo 35 en aproximadamente dos días. Normalmente, un nodo mantiene sus ranuras de entrada llenas hasta que se reinicia o pierde la conectividad de red.
Al consultar con otros colaboradores, noté que el nodo tenía una utilización de CPU del 100%. Esto afectó al nodo hasta el punto en que no podía seguir comunicándose con sus pares, lo que resultó en que las conexiones entrantes se agotaran y cayeran. Usando perf top
en el proceso del nodo, pude ver que se estaba gastando mucho tiempo de CPU en CTxMemPool::CompareDepthAndScore()
en el hilo b-msghand
. Grabé el siguiente flamegraph, que muestra que make_heap()
, que llama a CompareDepthAndScore()
, usó más del 45% del tiempo de CPU del proceso.
Al mismo tiempo, había un problema de uso de CPU al 100% abierto y no relacionado con las compilaciones en modo debug de Bitcoin Core. Esto confundió a algunos colaboradores y usuarios que no estaban ejecutando compilaciones en modo debug pero notaron el alto uso de CPU en sus nodos. Si bien el problema del modo debug probablemente solo afectó a algunos desarrolladores, el otro problema de alto uso de CPU afectó a toda la red. Esto incluyó, por ejemplo, a los grupos de minería como AntPool y otros, quienes informaron problemas con sus operaciones de minería debido a que sus nodos no podían procesar los bloques recibidos de manera oportuna.
La observación de los tiempos de ping en toda la red revela el efecto de esta Denegación de Servicio. Dado que el procesamiento de mensajes de Bitcoin Core es de un solo hilo, solo se puede crear o procesar un mensaje a la vez, lo que significa que todos los demás pares tienen que esperar. Los tiempos de espera más largos impactan en el tiempo de respuesta de un ping. La monitorización de Bitcoin de KIT DSN tiene datos sobre pings ICMP y del protocolo Bitcoin. La comparación de estos nos permite determinar cuándo el software del nodo tiene problemas para mantenerse al día con el procesamiento de mensajes. Los datos muestran que el ping ICMP al host no se vio afectado, sin embargo, el ping medio al software del nodo Bitcoin casi se duplicó de aproximadamente 25ms a más de 50ms entre finales de abril y principios de mayo. El ping medio de Bitcoin se disparó a 200ms el 8 de mayo, mientras que el ping ICMP permaneció sin verse afectado.
El efecto también se puede ver al observar los datos de retardo de propagación de bloques recopilados por la monitorización de Bitcoin de KIT DSN. Alrededor del 8 de mayo de 2023, se observa un pico en el retardo de propagación de bloques. El tiempo que tardó el 50% de los nodos alcanzables en anunciar el bloque a sus nodos de monitorización aumentó de menos de un segundo a más de cinco segundos. De manera similar, la medición del 90% se disparó de aproximadamente dos segundos a más de 20 segundos.
La mala propagación de bloques también causa más bloques obsoletos, ya que los grupos de minería minan en sus bloques obsoletos durante más tiempo, mientras que ya existe un nuevo bloque que aún no han visto en la red. Según los datos de mi conjunto de datos de bloques obsoletos, se observaron diez bloques obsoletos durante la semana entre el 3 de mayo (comenzando con el bloque obsoleto 788016) y el 10 de mayo (y terminando con el bloque 789147). Eso es una tasa de aproximadamente 8,84 bloques obsoletos por cada 1000 bloques. En comparación, entre los bloques 800000 y 900000 (aproximadamente dos años), se observaron 73 bloques obsoletos. Esta es una tasa de 0,73 bloques obsoletos por cada 1000 bloques. Este aumento de 10 veces en la tasa de bloques obsoletos probablemente fue causado por la propagación de bloques que se vio significativamente afectada.
¿Por qué la función CTxMemPool::CompareDepthAndScore()
ralentizó el nodo hasta el punto en que tuvo problemas para procesar los mensajes P2P? En Bitcoin Core, el hilo b-msghand
procesa los mensajes P2P. Por ejemplo, pasar los bloques recién recibidos a la validación, responder a los pings, anunciar transacciones a otros pares y mucho más.
La función CTxMemPool::CompareDepthAndScore()
se utiliza al decidir qué transacciones anunciar a un par a continuación. En el protocolo P2P de Bitcoin, las transacciones se anuncian a través de mensajes inv
(inventario). Un anuncio de transacción de Bitcoin Core a un par generalmente contiene hasta 35 entradas wtxid
. Para realizar un seguimiento de qué transacciones anunciar a un par a continuación, hay un conjunto m_tx_inventory_to_send
por par. Contiene las transacciones que el nodo cree que el par aún no ha visto. Al construir un mensaje de inventario para un par, el conjunto se ordena por dependencias de transacción y feerate para priorizar las transacciones de alta feerate y para evitar filtrar el orden en que el nodo se enteró de las transacciones. Para esto, se utiliza la función de comparación CTxMemPool::CompareDepthAndScore()
.
A principios de mayo de 2023, se transmitió una gran cantidad de transacciones relacionadas con los tokens BRC-20. Esto significó que los conjuntos m_tx_inventory_to_send
crecieron más rápido de lo habitual y más grandes de lo habitual. Como resultado, ordenar los conjuntos tomó más tiempo. En la noche del 7 de mayo (UTC), comenzó la acuñación del token VMPX BRC-20, lo que resultó en que se transmitieran más de 300.000 transacciones en 6 horas junto con las otras acuñaciones de tokens BRC-20 en curso. Esto causó los picos en el ping medio y los tiempos de propagación de bloques observados el 8 de mayo.
El efecto se amplifica por los llamados nodos espía que solo escuchan los mensajes inv
y nunca anuncian transacciones por sí mismos. Cuando un par anuncia una transacción a un nodo, el nodo puede eliminarla de su conjunto m_tx_inventory_to_send
, ya que el par la conoce y ya no es necesario anunciarla. Esto significó que los conjuntos para los nodos espía eran aún más grandes y tardaban aún más en ordenarse, ya que se vaciaban más lentamente. Los nodos espía, por ejemplo, LinkingLion y otros, son comunes y, a menudo, tienen múltiples conexiones abiertas a un nodo en paralelo. A veces, cuento más nodos espía asumidos que conexiones de nodos no espía a mis nodos.
La gran cantidad de transacciones que se transmiten, combinada con la amplificación por parte de los nodos espía y la ordenación no óptima de los grandes conjuntos m_tx_inventory_to_send
por CTxMemPool::CompareDepthAndScore()
, hizo que los nodos pasaran mucho tiempo creando nuevos mensajes de inventario para el relevo de transacciones. Dado que el manejo de mensajes es de un solo hilo, la comunicación con otros pares se ralentizó significativamente. Esto llegó a un punto en que los bloques no se procesaron de manera oportuna y algunas conexiones se agotaron.
La solución es doble. Primero, todas las transacciones que se iban a anunciar que ya habían sido minadas o que por alguna otra razón ya no estaban en el mempool, se eliminaron antes de que se ordenara el conjunto m_tx_inventory_to_send
. Anteriormente, estas transacciones se eliminaban solo después de que se ordenaba el conjunto. Esto evita gastar tiempo en ordenar las entradas de transacción que nunca se anunciarán de todos modos y reduce el tamaño del conjunto que se va a ordenar. En segundo lugar, cuando los conjuntos m_tx_inventory_to_send
son grandes, el número de entradas para drenar del conjunto se incrementa dinámicamente en función del tamaño del conjunto. Esto significa que cuando se transmiten muchas transacciones, un nodo anunciará más transacciones a sus pares hasta que los conjuntos sean más pequeños nuevamente. La solución se aplicó en el tiempo para el lanzamiento de la v25.0 a finales de mayo de 2023.
Si bien un conjunto de colaboradores habituales sabía que esto estaba sucediendo, este problema no se comunicó abiertamente al público. El problema de uso de CPU al 100% con el modo debug que se estaba discutiendo al mismo tiempo causó confusión, incluso entre los colaboradores habituales de Bitcoin Core. En ese momento, tenía la sensación de que esto podría y tal vez debería solucionarse en silencio y no necesita mucha publicidad por el momento. En retrospectiva, tal vez ser más público y transparente con el problema también podría haber funcionado. El alto número de transmisiones de BRC-20 solo duró aproximadamente una semana (pero esto no se sabía de antemano) y reiniciar el nodo habría ayudado por un tiempo. Para mitigar el problema, por ejemplo, para los grupos de minería que no pueden actualizar a una versión con la solución de inmediato (debido a que se ejecutan con parches personalizados), se preparó una lista de prohibición de nodos espía, pero no sé si alguna vez se usó.
Si bien no hubo un canal de comunicación dedicado para este evento, se utilizó un canal IRC no listado con colaboradores P2P y se invitó o informó a los colaboradores interesados sobre los eventos a través de mensajes directos. Hasta donde sé, no hubo un canal de respuesta a incidentes y no sé si uno sería útil dada la naturaleza ad hoc y descentralizada del desarrollo de Bitcoin. Ningún colaborador es responsable de la respuesta a incidentes, pero todos pueden ayudar.
Personalmente, estoy feliz de que mi monitorización haya demostrado ser útil para esto. Si bien no tenía configuradas alertas para las conexiones caídas en ese momento y solo lo noté al mirar el panel, fue útil tenerlo. Para identificar el problema, fue útil tener algunos nodos para jugar y ejecutar, por ejemplo, perf top
. La monitorización futura debería incluir tiempos de ping y alertas sobre conexiones caídas.