Deep EVM #10 : Gestion de pile Huff — takes(), returns() et l'art du dup/swap
Engineering Team
Le modèle mental de la machine à pile
L’EVM est une machine à pile. Il n’y a pas de registres, pas de variables nommées — juste une pile dernier-entré-premier-sorti de mots de 32 octets, profonde de 1024 emplacements. Chaque opcode empile, dépile ou réarrange des éléments sur cette pile. Si vous ne pouvez pas tenir l’état actuel de la pile dans votre tête, vous produirez du bytecode bogué. Cet article porte sur la construction de ce modèle mental.
Convention de notation
Tout au long de cet article (et dans les commentaires Huff), nous représentons l’état de la pile avec des crochets où l’élément le plus à gauche est le sommet :
// [sommet, deuxième, troisième, ..., fond]
0x01 // [1]
0x02 // [2, 1]
add // [3]
Chaque macro Huff devrait avoir un commentaire de pile après chaque opcode. Ce n’est pas optionnel — c’est la seule façon d’auditer la correction.
DUP : dupliquer les éléments de la pile
L’EVM fournit DUP1 à DUP16. DUPn copie le n-ième élément depuis le sommet et l’empile. La pile grandit de 1.
// Pile : [a, b, c, d]
dup1 // [a, a, b, c, d] — copier le sommet
dup3 // [c, a, a, b, c, d] — copier le 3e depuis le sommet
Coût en gas : 3 gas pour tout DUPn. C’est l’une des opérations les moins chères de l’EVM.
Quand utiliser DUP
DUP est votre outil pour les lectures non destructives. Beaucoup d’opcodes consomment leurs arguments (ADD dépile deux, empile un), donc si vous avez besoin d’une valeur plus tard, dupliquez-la avant de la fournir à un opcode consommateur.
#define macro SAFE_SUB() = takes(2) returns(1) {
// takes: [a, b] — calculer a - b, revert si b > a
dup2 dup2 // [a, b, a, b]
lt // [a < b?, a, b]
revert_underflow jumpi // [a, b]
sub // [a - b]
done jump
revert_underflow:
0x00 0x00 revert
done:
}
Notez le dup2 dup2 — nous dupliquons a et b parce que lt les consommera, mais nous avons encore besoin des originaux pour le sub.
SWAP : réarranger la pile
L’EVM fournit SWAP1 à SWAP16. SWAPn échange l’élément du sommet avec le (n+1)-ième élément. La taille de la pile reste la même.
// Pile : [a, b, c, d]
swap1 // [b, a, c, d] — échanger le sommet avec le 2e
swap3 // [d, a, c, b] — échanger le sommet avec le 4e
Coût en gas : 3 gas pour tout SWAPn.
La limitation de profondeur 16
DUP et SWAP n’atteignent que 16 niveaux de profondeur. Si une valeur est à la position 17 ou plus, vous ne pouvez pas y accéder avec un seul opcode. C’est une contrainte dure de l’EVM.
Stratégies pour les piles profondes :
- Restructurez votre logique pour garder les valeurs nécessaires près du sommet.
- Utilisez la mémoire comme espace temporaire. Stockez une valeur avec
MSTORE, récupérez-la plus tard avecMLOAD. Coûte 3+3=6 gas vs 3 pour DUP, mais brise la barrière de profondeur. - Divisez la macro en macros plus petites qui opèrent chacune sur moins d’éléments de pile.
#define macro STASH_TO_MEMORY() = takes(1) returns(0) {
// takes: [value]
0x80 mstore // [] — ranger à 0x80 (espace scratch)
}
#define macro RECALL_FROM_MEMORY() = takes(0) returns(1) {
0x80 mload // [value]
}
Dans les contrats MEV, nous réservons souvent 0x80..0xc0 comme zone scratch pour les valeurs qui autrement pousseraient la pile au-delà de 16.
Patterns courants
Pattern 1 : conserver une valeur à travers une opération consommatrice
// Voulons : calculer le hash de x, mais garder x
// Pile : [x]
dup1 // [x, x]
0x00 mstore // [x] — memory[0] = x
0x20 0x00 // [0, 32, x]
keccak256 // [hash, x]
Pattern 2 : faire tourner trois éléments
Vous avez [a, b, c] et voulez [c, a, b] :
swap2 // [c, b, a]
swap1 // [c, a, b]
2 opcodes, 6 gas. Il n’y a pas de rotation en un seul opcode dans l’EVM.
Pattern 3 : nettoyer les éléments indésirables
// Pile : [result, garbage1, garbage2]
swap1 pop // [result, garbage2]
swap1 pop // [result]
Pattern 4 : dupliquer une paire
// Pile : [a, b]
dup2 // [b, a, b]
dup2 // [a, b, a, b]
Discipline de visualisation de la pile
Quand vous écrivez du Huff, adoptez cette discipline :
- Commentez chaque ligne avec l’état de la pile après exécution.
- Vérifiez takes/returns — comptez les éléments à l’entrée et à la sortie.
- Tracez chaque branche — à chaque JUMPI, les deux chemins (pris et non pris) doivent laisser la pile dans un état valide.
- Surveillez la dérive de pile — si le corps d’une boucle n’équilibre pas parfaitement les empilements et dépilements, la pile croîtra ou rétrécira à chaque itération.
Débogage des erreurs de pile
Les bugs les plus courants en Huff :
- Sous-débordement de pile — Dépiler depuis une pile vide. L’EVM reverte à l’exécution.
- Déséquilibre de pile au JUMP — Un JUMPDEST atteint depuis deux chemins différents attend des états de pile différents.
- Erreur de un dans DUP/SWAP —
dup3vsdup4quand vous avez ajouté un push supplémentaire plus tôt.
huffc a un flag --stack-check qui effectue une analyse de pile basique :
huffc src/Contract.huff -r --stack-check
Résumé
La gestion de pile est la compétence fondamentale du développement Huff. DUP pour les lectures non destructives, SWAP pour le réordonnancement, et la mémoire pour les valeurs au-delà de la profondeur 16. Commentez chaque ligne avec l’état de la pile. Vérifiez chaque branche. Dans le prochain article, nous utiliserons ces compétences pour construire un dispatcher de fonctions O(1) avec des tables de saut compactes.