Aller au contenu principal
BlockchainMar 28, 2026

Deep EVM #11 : Tables de saut Huff — Dispatch de fonctions O(1) sans overhead Solidity

OS
Open Soft Team

Engineering Team

Le problème du dispatcher de Solidity

Quand vous appelez un contrat Solidity, la première chose que l’EVM exécute est le dispatcher de fonctions. Solidity génère une chaîne if-else linéaire qui compare les 4 premiers octets du calldata (le sélecteur de fonction) à chaque sélecteur connu.

Pour un contrat avec N fonctions, c’est du O(N) — le pire cas vérifie tous les N sélecteurs. Chaque comparaison coûte environ 22 gas. Un contrat avec 20 fonctions gaspille jusqu’à 440 gas rien que sur le dispatch.

Pour un contrat de bot MEV appelé des millions de fois, ces 400+ unités de gas par appel s’accumulent en vrai ETH.

L’approche table de saut : O(1)

Une table de saut mappe un sélecteur de fonction directement à un offset de code en utilisant l’arithmétique, pas la comparaison. L’idée est empruntée à l’architecture CPU — les GOTO calculés sont utilisés depuis les années 1960.

Le concept :

  1. Extraire le sélecteur de fonction du calldata (4 octets).
  2. Utiliser l’arithmétique pour calculer une destination de saut à partir du sélecteur.
  3. Sauter directement à cette destination.

Pas de comparaisons, pas de branchements, temps constant quel que soit le nombre de fonctions.

Approche 1 : encodage de sélecteur minimal

Si votre contrat a un petit nombre de fonctions (1-8), vous pouvez assigner manuellement les sélecteurs en minant des sélecteurs vanity où le premier ou les deux premiers octets encodent un petit entier unique :

#define macro DISPATCHER() = takes(0) returns(0) {
    0x00 calldataload       // [calldata_word]
    0xe0 shr                // [selector]

    // Extraire l'octet de routage — premier octet du sélecteur
    0x18 shr                // [first_byte]

    // Chaque entrée dans notre table fait 2 octets (PUSH2 offset)
    0x02 mul                // [offset_in_table]
    __tablestart(JumpTable) // [table_start, offset_in_table]
    add                     // [entry_address]
    
    // Charger la destination de saut de 2 octets depuis la table
    0x00 codecopy           // copier 2 octets du code vers la mémoire
    0x00 mload              // [jump_dest (rembourré à 32 octets)]
    0xf0 shr                // [jump_dest]
    jump                    // GOTO corps de la fonction
}

Approche 2 : table de code compacte

Pour une densité maximale, compactez la table de saut directement dans le bytecode en utilisant __tablestart et __tablesize. Huff supporte nativement les tables de saut :

#define jumptable__packed SELECTOR_TABLE {
    fn_swap
    fn_transfer
    fn_approve
    fn_balance
}

Comparaison de gas

FonctionsSolidity (if-else)Solidity (binaire)Table de saut Huff
222-44 gas22-44 gas15 gas
422-88 gas22-66 gas15 gas
822-176 gas22-88 gas15 gas
1622-352 gas22-110 gas15 gas
3222-704 gas22-132 gas15 gas

Le coût de la table de saut est constant : CALLDATALOAD (3) + SHR (3) + arithmétique (3-6) + JUMP (8) = environ 15-18 gas. Il ne change jamais, quel que soit le nombre de fonctions.

Minage de sélecteurs vanity

Pour que l’approche table de saut fonctionne, vous avez besoin de sélecteurs de fonction avec des octets de routage prévisibles. Vous pouvez les miner :

import hashlib
import itertools

target_byte = 0x00  # premier octet désiré du sélecteur
base_name = "swap"

for suffix in itertools.count():
    name = f"{base_name}{suffix}(uint256,address)"
    selector = hashlib.sha3_256(name.encode()).digest()[:4]
    if selector[0] == target_byte:
        print(f"Trouvé: {name} -> 0x{selector.hex()}")
        break

En pratique, nous utilisons cast sig de Foundry ou un outil Rust qui brute-force les noms de fonctions pour trouver des sélecteurs avec les préfixes désirés.

Impact sur la taille du bytecode

La taille du bytecode affecte directement le coût de déploiement (200 gas par octet via CREATE). Comparaison :

ApprocheBytecode runtimeGas de déploiement
Solidity (8 foncs)~800 octets160 000 gas
Table de saut Huff (8 foncs)~200 octets40 000 gas
Huff minimal (2 foncs)~61 octets12 200 gas

Limitations

  1. ABI non standard — Les outils externes (Etherscan, portefeuilles) ne peuvent pas décoder vos calldata sans définitions ABI personnalisées.
  2. Minage de sélecteurs — Nécessite un travail initial et contraint le nommage des fonctions.
  3. Coût de maintenance — Huff est plus difficile à auditer et modifier que Solidity.

Résumé

Les tables de saut remplacent la chaîne de dispatch O(N) de Solidity par des sauts calculés O(1). Les économies de gas se cumulent sur des millions d’appels — un avantage significatif pour les contrats haute fréquence. Dans le prochain article, nous explorerons les patterns Huff avancés : exécution adaptative, authentification multi-opérateur et astuces de disposition mémoire.