BLAKE3: el árbol que sí se reparte
BLAKE3 parte la entrada en trozos independientes y los combina en un árbol binario, y de esa independencia salen a la vez los hilos, los carriles vectoriales y una lista concreta de costes.
La lección anterior terminó con un diagnóstico incómodo: la lentitud de SHA-256 sobre ficheros grandes no es un problema de implementación sino la sombra de su estructura, y por tanto no admite optimización sino rediseño. BLAKE3 es exactamente ese rediseño hecho a conciencia y publicado con especificación, implementación de referencia legible y vectores de prueba. Su idea central se puede enunciar en una frase: en lugar de encadenar el estado a lo largo de todo el mensaje, se parte la entrada en trozos que se resumen sin conocerse entre sí, y los resultados se combinan en un árbol binario hasta una raíz. Esa independencia entre trozos es la mercancía que se compra, y todo lo demás —los hilos, los carriles vectoriales, las actualizaciones incrementales y la verificación parcial que veremos en la lección siguiente— son consecuencias suyas. Conviene sin embargo resistir la tentación de contarlo como una historia de progreso sin factura, porque la factura existe, es concreta y hay que ponerla sobre la mesa antes de decidir nada.
- Describir la estructura interna de BLAKE3: trozos, bloques, nodos padre, banderas y valores encadenados.
- Explicar de dónde sale exactamente el paralelismo, distinguiendo el de hilos del de carriles vectoriales.
- Entender la separación de dominios que evita que el árbol sea maleable y por qué es imprescindible.
- Enumerar sin adornos qué se paga por todo esto y en qué escenarios la ventaja se estrecha o desaparece.
El árbol por dentro
La estructura es sorprendentemente pequeña de describir y se puede comprobar leyendo la implementación de referencia del repositorio oficial, que ocupa unas trescientas líneas. La entrada se parte en trozos de mil veinticuatro bytes. Cada trozo se procesa internamente como dieciséis bloques de sesenta y cuatro bytes encadenados entre sí, igual que haría una función clásica, pero esa cadena vive confinada dentro del trozo y nunca sale de él. El resultado de cada trozo es un valor encadenado de ocho palabras de treinta y dos bits, es decir doscientos cincuenta y seis bits.
Los trozos se combinan de dos en dos. Un nodo padre no contiene datos: su bloque de entrada son los sesenta y cuatro bytes que resultan de concatenar el valor encadenado del hijo izquierdo con el del hijo derecho, y su salida es a su vez un valor encadenado de la misma forma. Se repite hasta llegar a un único nodo, y ese nodo, marcado como raíz, produce la salida final. La función de compresión que se usa en los trozos y en los nodos padre es la misma, derivada de BLAKE2s, y ejecuta siete rondas por bloque.
flowchart TB R[raiz con bandera ROOT] --> P1[nodo padre] R --> P2[nodo padre] P1 --> T1[trozo 0 de 1024 bytes] P1 --> T2[trozo 1 de 1024 bytes] P2 --> T3[trozo 2 de 1024 bytes] P2 --> T4[trozo 3 de 1024 bytes] style T1 fill:#a6e3a1,color:#11111b style T2 fill:#a6e3a1,color:#11111b style T3 fill:#a6e3a1,color:#11111b style T4 fill:#a6e3a1,color:#11111b style R fill:#cba6f7,color:#11111b
La pieza que hace todo esto seguro y que suele omitirse en las explicaciones divulgativas son las banderas. Cada llamada a la compresión lleva un campo de bandera que indica si el bloque es el primero de un trozo, si es el último, si se trata de un nodo padre y si es la raíz, además de las banderas propias de los modos con clave. Esa separación de dominios impide que un nodo interno pueda reinterpretarse como una hoja o al revés, que es el ataque clásico contra los árboles de resúmenes construidos con ingenuidad. A ello se suma que cada trozo lleva su contador de posición dentro de la compresión, de modo que dos trozos de contenido idéntico en posiciones distintas no producen el mismo valor encadenado.
// La forma del algoritmo, con las tres decisiones que lo definen
function blake3(entrada) {
const trozos = partirEn(entrada, 1024); // independientes entre si
let nivel = trozos.map((t, i) => cvDeTrozo(t, i)); // el indice entra en la mezcla
while (nivel.length > 1) {
const siguiente = [];
for (let i = 0; i < nivel.length; i += 2) {
siguiente.push(cvDePadre(nivel[i], nivel[i + 1])); // bandera PARENT
}
nivel = siguiente;
}
return salidaRaiz(nivel[0]); // bandera ROOT
}
El contador de trozo merece un párrafo propio porque hace un trabajo que no se ve. Si dos trozos con el mismo contenido produjeran el mismo valor encadenado, un fichero con regiones repetidas tendría subárboles intercambiables y sería posible reordenarlos sin alterar la raíz. Al entrar el índice del trozo en la mezcla, cada hoja queda atada a su posición y esa reordenación deja de existir. Es el mismo razonamiento que hacía falta en el troceado por contenido: la identidad de una parte tiene que incluir dónde está esa parte, o el todo deja de estar determinado por sus partes.
La implementación real no construye el árbol por niveles, porque eso obligaría a tener toda la entrada disponible. Usa una pila de valores encadenados y, cada vez que termina un trozo, fusiona hacia arriba tantos subárboles como ceros tenga por la derecha la cuenta binaria de trozos completados. Es un truco de contabilidad exacto que permite ir consumiendo la entrada en flujo, sin conocer su longitud de antemano, manteniendo en memoria solo un valor por nivel. La pila de la implementación de referencia tiene cincuenta y cuatro huecos, y esa cifra no es arbitraria: dos elevado a cincuenta y cuatro trozos de mil veinticuatro bytes cubren el espacio de dos elevado a sesenta y cuatro bytes de entrada.
// La regla de la pila: fusionar tantos niveles como ceros por la derecha
function anadirTrozo(pila, cv, trozosTotales) {
let actual = cv;
while (trozosTotales % 2 === 0) { // subarbol completo a la izquierda
actual = cvDePadre(pila.pop(), actual);
trozosTotales = Math.floor(trozosTotales / 2);
}
pila.push(actual); // memoria acotada por el logaritmo
}
De dónde sale la velocidad
Hay dos fuentes de paralelismo y conviene no confundirlas, porque operan en escalas distintas y se acumulan. La primera es la evidente: subárboles distintos pueden calcularse en hilos distintos, porque no comparten ningún estado. Un fichero grande se reparte entre todos los núcleos disponibles y el tiempo baja casi proporcionalmente hasta chocar con el ancho de banda de memoria o de disco. La utilidad oficial de línea de órdenes usa varios hilos por defecto, y su documentación afirma que eso la hace un orden de magnitud más rápida que la herramienta equivalente de SHA-256 en hardware de escritorio típico.
La segunda fuente es menos evidente y es la que más aporta dentro de un solo núcleo. Como los trozos son independientes, se pueden colocar varios en los carriles de un registro vectorial y avanzarlos simultáneamente instrucción a instrucción. Un registro de doscientos cincuenta y seis bits sostiene ocho estados de treinta y dos bits en paralelo; uno de quinientos doce bits sostiene dieciséis. Esto es exactamente lo que SHA-256 no podía hacer con un flujo único, y no por falta de registros anchos sino porque no tenía valores independientes que meter en ellos. El repositorio oficial incluye implementaciones optimizadas para SSE2, SSE4.1, AVX2, AVX-512, NEON y WebAssembly, con detección del soporte del procesador en tiempo de ejecución.
Es fácil descartar la ventaja de BLAKE3 diciendo que solo importa si tienes muchos núcleos libres, y esa objeción es válida para el paralelismo de hilos pero no para el otro. El aprovechamiento de carriles vectoriales ocurre dentro de un único hilo, sin coordinación, sin reparto y sin coste de arranque, y por tanto mejora también el caso de un proceso que resume mientras el resto de la máquina hace otra cosa. Al comparar funciones conviene por eso medir siempre dos escenarios: monohilo y multihilo. La brecha en el primero se debe a los carriles; la del segundo suma los núcleos encima.
Merece la pena señalar dónde se detiene esta escalada, porque el límite llega antes de lo que se espera. En cuanto la función es lo bastante rápida, el cuello de botella deja de ser el cálculo y pasa a ser traer los bytes hasta el procesador. A partir de ese punto añadir hilos no mejora nada y la comparación entre funciones se vuelve irrelevante para el tiempo total, porque las dos están esperando al disco. Ese umbral es una noticia excelente y hay que enunciarla: el objetivo práctico no es que el resumen sea infinitamente rápido, sino que deje de aparecer en el perfil, y una vez alcanzado ese punto no hay ninguna ganancia adicional que obtener.
Tres modos y una sola función
Un rasgo de diseño que tiene consecuencias de ingeniería es que BLAKE3 es un solo algoritmo sin variantes, con una salida por defecto de doscientos cincuenta y seis bits que puede extenderse a cualquier longitud, y con tres modos que comparten toda la maquinaria y se distinguen únicamente por banderas y por el valor con el que se inicializa la cadena. El modo normal resume. El modo con clave toma una clave de treinta y dos bytes y produce un código de autenticación sin necesidad de envoltorio, precisamente porque el diseño es seguro frente a extensión de longitud. El modo de derivación de clave toma una cadena de contexto que debe estar escrita en el código, ser globalmente única y específica de la aplicación, y a partir de ella y de material de clave produce subclaves separadas por dominio.
# Un solo algoritmo, tres modos y salida de longitud a peticion
b3sum fichero.bin # 32 bytes por defecto
b3sum --length 64 fichero.bin # salida extendida sin cambiar de funcion
b3sum --derive-key "miapp 2026-08-05 clave de cifrado" < material.bin
El modo de derivación obliga a escribir una cadena de contexto porque el propósito es que dos usos distintos de la misma clave maestra produzcan subclaves que no guardan relación. La recomendación oficial de formato para esa cadena incluye el nombre de la aplicación, una marca temporal fija y el propósito concreto, y el motivo de exigir que esté escrita en el código y no venga de una variable es que si el atacante controla el contexto, controla la separación. Es un ejemplo poco frecuente de una decisión criptográfica correcta impuesta por la forma de la interfaz en lugar de por la documentación.
Qué cuesta
La lista honesta tiene cinco entradas y ninguna es descalificadora, pero todas son reales. La primera es el margen de rondas: la compresión ejecuta siete rondas, frente a las diez de BLAKE2s, que es la función en la que se basa. Parte de la velocidad viene literalmente de ahí, y aunque no hay ningún resultado publicado que amenace la versión completa, un margen menor es un margen menor y quien exija conservadurismo máximo tiene un argumento legítimo.
La segunda es que no hay silicio dedicado. SHA-256 cuenta con instrucciones específicas en los procesadores modernos que implementan varias de sus rondas por instrucción; BLAKE3 depende de instrucciones vectoriales de propósito general. En una máquina con extensiones de SHA y sin registros vectoriales anchos, la comparación se estrecha mucho e incluso puede invertirse para entradas pequeñas.
La tercera es el tamaño mínimo. Por debajo de mil veinticuatro bytes no hay árbol, no hay más de un trozo y no hay nada que repartir. Si tu carga de trabajo son millones de objetos de doscientos bytes, la ventaja estructural de BLAKE3 no se manifiesta y la decisión debe tomarse por otros criterios.
Gana en flujo grande
Hilos y carriles se reparten un fichero grande, y el límite acaba siendo el ancho de banda de memoria y no el algoritmo.
Modos integrados
Clave y derivación vienen de serie, sin HMAC alrededor, porque el diseño no sufre extensión de longitud.
Pierde en objetos diminutos
Con entradas por debajo del trozo no hay árbol que repartir y la ventaja estructural se evapora.
Implementación compleja
La versión rápida son varias rutas por conjunto de instrucciones más detección en ejecución, frente a un algoritmo mucho más simple de portar.
La cuarta entrada es la complejidad de implementación, y conviene medirla bien. La implementación de referencia es pequeña y legible, y precisamente por eso es la que hay que leer para entender el diseño; pero la implementación rápida es otra cosa, con una ruta de código por conjunto de instrucciones, detección del procesador en tiempo de ejecución y multihilo opcional. Esa superficie es mantenible por un equipo especializado y es exactamente el tipo de código donde un error no se manifiesta como fallo sino como resultado incorrecto en una máquina concreta. La respuesta razonable es no escribir la tuya y usar la oficial o un enlace a ella.
La quinta es institucional. BLAKE3 no está en la norma federal, y en un contexto sujeto a certificación esa ausencia decide sola. Sí está especificado en un proyecto comunitario de especificaciones criptográficas, cuenta con vectores de prueba oficiales y con implementaciones en muchos lenguajes, y su adopción incluye herramientas de construcción, gestores de paquetes, sistemas de ficheros y varios proyectos del ecosistema local-first. Eso no equivale a una norma, pero está lejos de ser una curiosidad de laboratorio.
Lo que BLAKE3 hace no es una técnica exclusiva de la criptografía, y verlo así convierte esta lección en una herramienta reutilizable. El patrón general es este: un cálculo secuencial se paraleliza cuando se puede reescribir como una reducción sobre una operación asociativa, porque la asociatividad es precisamente el permiso para reagrupar los paréntesis y por tanto para repartir el trabajo. Una suma, un máximo, una concatenación de estados, la composición de transformaciones afines, la fusión de dos CRDT: todos ellos aceptan la reescritura en árbol y todos se benefician igual. Un resumen encadenado no la aceptaba porque su combinación no era asociativa, era una aplicación de función sobre un estado opaco, y el rediseño consistió exactamente en fabricar una operación asociativa donde no la había: combinar dos valores encadenados en uno solo. La paralelización no se descubrió, se diseñó, y el diseño consistió en cambiar la operación hasta que fuera asociativa. Ahora la parte que separa esto de un ejercicio de programación funcional y lo devuelve a la criptografía. Al reagrupar libremente, un árbol ingenuo permite que la misma secuencia de valores encadenados se reinterprete con otra forma de árbol, y de ahí salen segundas preimágenes sin tocar la función de compresión: basta con presentar un nodo interno como si fuera una hoja. La cura es la separación de dominios, y por eso las banderas de trozo, de padre y de raíz, junto con el contador de posición, no son adorno sino la pieza que hace que la reescritura sea segura. La lección transferible es que cuando conviertas un cálculo secuencial en un árbol, la asociatividad te da el rendimiento y solo la separación de dominios te devuelve la unicidad: sin ella has construido una función más rápida y también más maleable, y ese defecto no aparece en ningún banco de pruebas porque no es un problema de velocidad sino de que dos entradas distintas empiezan a valer lo mismo.
- Resume un fichero de mil veinticuatro bytes y otro de mil veinticinco y observa que el segundo ya construye un nodo padre.
- Mide el mismo fichero grande con un solo hilo y con todos, y separa cuánto aporta cada fuente de paralelismo.
- Implementa la pila de valores encadenados con la regla de los ceros por la derecha y comprueba que reproduce la raíz.
- Quita mentalmente la bandera de nodo padre de tu implementación y construye dos entradas distintas con la misma raíz.
- Compara los tiempos en una máquina con extensiones de SHA y en otra sin ellas, y anota cómo cambia la conclusión.
- Deriva dos subclaves con cadenas de contexto distintas y verifica que no guardan ninguna relación observable.