Deep EVM #11: Jump Tables en Huff — Despacho de Funciones O(1) Sin Overhead de Solidity
Engineering Team
El problema del despacho de funciones
Cuando llamas a un contrato, los primeros 4 bytes del calldata son el selector de función — un hash keccak256 truncado de la firma. El contrato necesita mapear este selector al código correspondiente.
Solidity genera un despacho lineal: compara el selector contra cada función hasta encontrar coincidencia. Con N funciones, esto es O(N) en el peor caso.
// Solidity genera (simplificado):
if selector == 0xa9059cbb jump transfer // 3+3+10 = 16 gas
if selector == 0x70a08231 jump balanceOf // 16 gas
if selector == 0x18160ddd jump totalSupply // 16 gas
// ... más comparaciones ...
revert
Para un contrato con 50 funciones, la función más “lejana” paga 50 * 16 = 800 gas solo en despacho. En MEV, esto es inaceptable.
Jump tables: O(1) constante
Una jump table usa el selector directamente como índice para saltar a la posición correcta. Sin comparaciones, sin branches — un solo salto.
Concepto
- Tomar los primeros N bits del selector
- Usarlos como offset en una tabla almacenada en el bytecode
- Saltar directamente a la dirección indicada
Implementación en Huff
#define jumptable DISPATCH_TABLE {
balanceOf // selector 0x70a08231 -> posición 0
transfer // selector 0xa9059cbb -> posición 1
totalSupply // selector 0x18160ddd -> posición 2
}
#define macro MAIN() = takes(0) returns(0) {
// Leer selector
0x00 calldataload 0xe0 shr
// Buscar en la jump table
__tablestart(DISPATCH_TABLE) // [table_offset, selector]
// ... lógica de lookup ...
}
Huff proporciona __tablestart y __tablesize para referenciar jump tables embebidas en el bytecode.
Hash perfecto para selectores
Para un conjunto conocido de selectores, podemos encontrar una función hash que mapee cada selector a un índice único sin colisiones:
#define macro PERFECT_DISPATCH() = takes(0) returns(0) {
0x00 calldataload 0xe0 shr // [selector]
// Función hash perfecta (precalculada offline):
// h(selector) = (selector * MAGIC_NUMBER) >> SHIFT
// donde MAGIC_NUMBER y SHIFT se eligen para evitar colisiones
dup1
0x0f and // Usar los últimos 4 bits como índice
0x02 shl // Multiplicar por 4 (cada entrada = 4 bytes)
__tablestart(DISPATCH_TABLE)
add // [table_addr]
// Leer la dirección de salto desde la tabla
0x00 codecopy // Copiar 2 bytes del bytecode
0x00 mload // Cargar la dirección
0xf0 shr // Alinear
jump // Saltar directamente
}
Benchmarks
| Método | Gas (mejor caso) | Gas (peor caso) | Gas (promedio, 20 funciones) |
|---|---|---|---|
| Solidity lineal | 16 | 320 | 168 |
| Solidity binary search | 48 | 80 | 64 |
| Huff jump table | 35 | 35 | 35 |
La jump table de Huff tiene coste constante sin importar cuántas funciones tenga el contrato. Para contratos con muchas funciones (como routers de DEX), el ahorro es masivo.
Jump tables packed vs padded
Packed table
Cada entrada ocupa solo 2 bytes (dirección de salto). Más compacto, pero requiere calcular offsets.
Padded table
Cada entrada ocupa 32 bytes. Más desperdicio, pero el lookup es trivial con multiplicación.
// Packed: 2 bytes por entrada
// Offset = index * 2
// Más compacto, menos gas en codecopy
// Padded: 32 bytes por entrada
// Offset = index * 32
// Más simple, pero bytecode más grande
Consideraciones prácticas
- Colisiones de hash — La función hash perfecta debe ser verificada exhaustivamente offline
- Selectores desconocidos — Siempre incluir un fallback para selectores que no coincidan
- Tamaño del bytecode — Jump tables grandes pueden aumentar el coste de despliegue
- Mantenimiento — Añadir funciones requiere recalcular la función hash perfecta
Aplicaciones en MEV
Los bots de MEV son el caso de uso principal para jump tables:
- Un router que ejecuta arbitraje a través de 50+ pools necesita despacho O(1)
- La latencia de 12 segundos por bloque significa que cada gas ahorrado se convierte en ventaja competitiva
- Los contratos de ejecución MEV típicamente tienen 3-5 funciones de punto de entrada, pero el patrón se escala a cualquier número
Conclusión
Las jump tables transforman el despacho de funciones de O(N) a O(1), eliminando una fuente significativa de overhead en contratos con muchas funciones. Para desarrolladores de MEV y protocolos de alto rendimiento, este patrón es esencial.