BitrootBlog
Retour au site ↗
© 2026 Bitroot · Le contenu est fourni à titre informatif et ne constitue pas un conseil financier, juridique, fiscal ou d'investissement.
Charte éditorialeRetour au site
← Tous les articles
Fondamentaux EVM·2026/09/27·Environ 18 min

État global et arbre de Merkle Patricia : comment le stateRoot engage tout

Le stateRoot d'un en-tête de bloc ne fait que 32 octets, et pourtant il engage l'ensemble des comptes, des soldes et du stockage des contrats du réseau. Nous démontons ici la structure des nœuds du Merkle Patricia Trie et la conception du trie de stockage rattaché aux comptes, expliquons comment les preuves de Merkle permettent la vérification hors chaîne, et identifions qui supporte le coût du gonflement de l'état.

L'en-tête de bloc d'Ethereum contient un champ de 32 octets, le stateRoot. Il ne renferme aucune donnée de compte, mais prétend fixer d'un seul coup l'ensemble des soldes, des nonce, du code et du stockage des contrats de tous les comptes du réseau : si ce hachage correspond, vous savez de quelle version de l'état il s'agit. Ce champ est le hachage racine de l'arbre de Merkle Patricia (MPT), et la seule promesse que la couche d'exécution fait à l'égard de l'état global.

Pour mesurer le poids de cette promesse, il faut décomposer trois choses : la structure de l'arbre, la raison pour laquelle le hachage racine peut lier tout l'état, et ce qu'il en coûte d'entretenir cet arbre. La troisième est le point de départ des discussions ultérieures sur le sharding de l'état et sur le remplacement de l'arbre d'état.

Trois tries dans un même en-tête de bloc

L'en-tête de bloc d'Ethereum porte trois racines de trie qui décrivent le résultat de l'exécution : stateRoot (trie de l'état global), transactionsRoot (trie des transactions) et receiptsRoot (trie des reçus). Le livre jaune les note H_r, H_t et H_e. Depuis la mise à niveau Shanghai, l'en-tête comporte en plus un withdrawalsRoot (H_w) décrivant la liste des retraits, dont la structure est semblable à celle du trie des transactions, à ceci près que chaque retrait transporte peu de données et que le nombre de retraits par bloc est faible. Le livre jaune range parmi les conditions de validité de l'en-tête le fait que « H_r soit égal à la racine d'état obtenue après avoir exécuté dans l'ordre toutes les transactions du bloc, puis tous les retraits ». Le stateRoot décrit un état cumulé ; les autres racines ne décrivent que le bloc courant.

trieclévaleurcycle de vie
trie de l'état globalkeccak256(adresse du compte)quadruplet de compte encodé en RLPmis à jour en continu d'un bloc à l'autre
trie des transactionsrlp(numéro de la transaction dans le bloc)rlp(transaction) ; pour une transaction typée, préfixe de type concaténé à la transaction encodéeun par bloc, plus modifié ensuite
trie des reçusrlp(numéro de la transaction dans le bloc)reçu typé, ou rlp([status, cumulativeGasUsed, logsBloom, logs])un par bloc, plus modifié ensuite

Le nombre de feuilles des tries des transactions et des reçus est proportionnel au nombre de transactions du bloc : vérifier qu'une transaction a bien été incluse coûte donc indépendamment de la longueur de la chaîne, uniquement en fonction de la taille du bloc. Le trie de l'état global est unique à l'échelle du réseau et croît avec le nombre de comptes et d'emplacements de stockage. Le logsBloom de l'en-tête est agrégé à partir des adresses et des topic des journaux figurant dans les reçus ; il sert de filtre probabiliste pour les requêtes. C'est un accélérateur d'index, pas une promesse.

Les adresses entrent dans le trie hachées, et un compte est un quadruplet

Dans le trie de l'état global, la clé est keccak256(adresse du compte) et la valeur le quadruplet de compte [nonce, balance, storageRoot, codeHash] encodé en RLP. L'adresse elle-même n'entre pas dans le trie : c'est son hachage de 256 bits qui y entre, 32 octets soit 64 nibbles (demi-octets), donc au plus 64 niveaux de la racine à une feuille.

Ces quatre champs ont une sémantique précise. nonce est le nombre de transactions émises par le compte, auquel s'ajoute, pour un compte de contrat, le nombre de contrats qu'il a créés. balance est le solde exprimé en wei. codeHash est le keccak256 du code du compte ; le code lui-même est stocké dans la base d'état sous sa forme hachée, si bien que deux contrats au bytecode identique partagent les mêmes données. storageRoot est la racine d'un autre trie, qui n'appartient qu'à ce compte.

La structure prend donc la forme d'un arbre dans l'arbre : la feuille du trie de l'état global contient le storageRoot d'un compte, et cette racine pointe vers le trie de stockage propre à ce compte. Dans ce trie de stockage, la clé est keccak256(numéro de slot sur 32 octets) et la valeur l'encodage RLP du contenu du slot (256 bits). Un slot dont la valeur est nulle équivaut, dans la spécification, à un slot inexistant ; réécrire un slot à 0 a donc pour effet, au niveau de l'état, de le supprimer.

Hacher d'abord la clé fait dépendre la forme de l'arbre de la distribution des hachages, que l'attaquant ne peut plus modeler en choisissant ses clés. Si les clés étaient directement des adresses ou des numéros de slot, un attaquant pourrait sélectionner un lot de clés partageant un long préfixe et comprimer un sous-arbre en une chaîne très profonde, amplifiant le coût des accès et des preuves ; une fois les clés uniformément dispersées par keccak256, cette construction n'est plus réalisable. Dans son argumentation sur la nouvelle structure d'arbre, le brouillon EIP-8297 retient lui aussi le fait que la position soit déterminée par le hachage, ce qui maintient l'équilibre, comme justificatif de conception. Le prix à payer est l'irréversibilité du numéro de slot : un contrat ne peut pas remonter depuis le trie de stockage aux slots qu'il a écrits, et l'extérieur ne peut les interroger qu'un par un, à partir de numéros de slot connus.

Deux constantes de valeur vide valent d'être mémorisées, car les preuves et les vérifications qui suivent les utilisent : la racine d'un trie vide et le hachage d'un code de compte vide.

# pip install eth-utils rlp
import rlp
from eth_utils import keccak

# Racine du trie vide : keccak de la chaîne d'octets vide encodée en RLP
print(keccak(rlp.encode(b"")).hex())
# 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421

# Hachage d'un code de compte vide
print(keccak(b"").hex())
# c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470

Trois types de nœuds et un drapeau de deux bits : comment les chemins de 64 niveaux sont raccourcis

Le MPT est un arbre 16-aire (hexary), pas un arbre binaire. L'annexe D du livre jaune définit trois types de nœuds plus un nœud vide. Un nœud branch comporte 17 éléments : les 16 premiers correspondent aux 16 valeurs possibles du nibble suivant, le 17e étant réservé au cas où la clé se termine ici. Un nœud extension est un couple [encodedPath, key] qui saute un préfixe commun d'au moins deux nibbles. Un nœud leaf est un couple [encodedPath, value] qui porte la partie restante de la clé et la valeur terminale. Le nœud vide est représenté par une chaîne d'octets vide. La spécification contient aussi un invariant facile à oublier : un nœud branch ne comportant qu'un seul élément non nul n'est pas autorisé. C'est précisément pour cela qu'un même ensemble de paires clé-valeur n'admet qu'un seul encodage, et donc que le hachage racine a une valeur déterminée.

Les chemins sont organisés par nibble et non par octet, ce qui impose un encodage compact logeant dans le premier nibble deux informations : la parité de la longueur du chemin et le type de nœud :

Premier nibbleBinaireType de nœudLongueur du chemin
00000extensionpaire
10001extensionimpaire
20010leafpaire
30011leafimpaire

Lorsque la longueur est paire, un nibble 0 est ajouté après le premier nibble pour que le nombre total de nibbles de l'encodage soit pair et que le résultat puisse être converti en chaîne d'octets. L'implémentation de référence donnée par ethereum.org est la suivante (la valeur de retour est ici convertie en bytes) :

def compact_encode(hexarray):
    # Le chemin d'un nœud leaf se termine par 16 : détecté, on le retire et on pose le drapeau leaf
    term = 1 if hexarray[-1] == 16 else 0
    if term:
        hexarray = hexarray[:-1]
    oddlen = len(hexarray) % 2
    flags = 2 * term + oddlen                 # Le premier nibble encode à la fois le type et la parité
    if oddlen:
        hexarray = [flags] + hexarray
    else:
        hexarray = [flags] + [0] + hexarray   # Longueur paire : on ajoute un nibble 0
    return bytes(16 * hexarray[i] + hexarray[i + 1] for i in range(0, len(hexarray), 2))

La compression des chemins explique pourquoi la profondeur théorique de 64 nibbles est loin d'être saturée sur le mainnet réel : dans sa section de motivation, le brouillon EIP-8297 estime la profondeur maximale actuelle du trie des comptes à environ 12 niveaux. Il s'agit d'une estimation avancée par le brouillon lui-même, et non d'un jeu de mesures sur le mainnet ; moins il y a de niveaux, plus les preuves sont courtes.

La façon dont les nœuds se référencent obéit à la règle des 32 octets. Si l'encodage RLP d'un nœud enfant fait moins de 32 octets, son contenu est inliné directement dans le nœud parent ; s'il atteint ou dépasse 32 octets, le parent n'écrit que keccak(RLP(nœud enfant)) comme référence. Cette règle économise un grand nombre de lectures ponctuelles de petits nœuds, au prix, lors de la vérification d'une preuve, d'une recomposition des nœuds selon leur forme d'encodage réelle, sans pouvoir supposer que chaque niveau correspond à une requête indépendante dans la base.

Le stateRoot est une promesse portant sur tout l'état

L'opération qui replie toute la structure en un seul hachage tient en une ligne : prendre le keccak256 de l'encodage RLP du nœud racine. Le livre jaune donne une raison très directe lorsqu'il décrit l'état global : le nœud racine dépend cryptographiquement de toutes les données qu'il contient, ce hachage peut donc servir d'identifiant sûr pour l'état de tout le système.

La solidité de la promesse vient du mode de référencement. Un parent référence un enfant par le hachage de cet enfant, si bien qu'un seul bit modifié dans une feuille change le nœud qui la contient, puis, de proche en proche, tous les niveaux jusqu'à la racine. Parce qu'elle repose sur la résistance aux collisions de keccak256, trouver deux états différents produisant la même racine équivaut à trouver une collision de hachage.

Trois propriétés en découlent. Le hachage racine détermine de façon unique un ensemble de paires clé-valeur. Un ancien état peut être récupéré à partir de sa racine, car les nœuds sont adressés par leur contenu et leur structure est immuable : tant que les nœuds restent dans la base, connaître l'ancienne racine suffit à restaurer l'état de l'époque. Le vérificateur peut lui aussi reconstruire la promesse le long du chemin : les hachages frères présents sur le chemin lui suffisent pour recalculer d'une feuille jusqu'à la racine, et l'annexe D du livre jaune note O(log N) le besoin en espace d'une preuve.

La définition retenue dans l'en-tête fixe en outre un instant précis : le stateRoot est la racine d'état « après exécution de toutes les transactions et de tous les retraits du bloc, et après application du traitement final ». Il engage l'ensemble de paires clé-valeur à cet instant, pas la manière dont cet état est devenu ce qu'il est : des historiques différents peuvent converger vers le même état, et une même racine peut réapparaître dans plusieurs blocs consécutifs. Il n'engage pas non plus la disponibilité de l'état. Le hachage racine ne contient par lui-même aucune donnée de nœud ; pour vérifier un compte, il faut se procurer séparément les nœuds du chemin.

De la racine à un compte : comment s'utilise une preuve de Merkle

Une preuve de compte est une liste de nœuds qui part du nœud racine et descend niveau par niveau en suivant les nibbles de keccak256(adresse) : à chaque niveau, on tombe soit sur l'élément correspondant d'un branch, soit sur le préfixe de chemin d'un extension ou d'un leaf. Le vérificateur procède mécaniquement : il recalcule keccak(RLP(nœud)) sur le premier nœud et vérifie qu'il est égal au stateRoot connu ; après décodage, il suit le nibble pour trouver la référence suivante et répète l'opération jusqu'à la feuille ; la valeur contenue dans la feuille est le compte encodé en RLP. Prouver l'absence d'un compte est également possible ; l'essentiel est de fournir le dernier nœud correspondant du chemin : s'il s'agit d'un branch, la branche correspondante est vide ; s'il s'agit d'un leaf, son chemin diverge du chemin cible sur un nibble. Une preuve de stockage exige un second parcours, dont le point de départ n'est plus le stateRoot mais le storageRoot du compte.

L'EIP-1186 normalise cette procédure sous la forme de eth_getProof ; voici un exemple de requête (l'adresse et le numéro de slot sont remplaçables) :

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_getProof",
  "params": [
    "0x7f0d15c7faae65896648c8273b6d7e43f58fa842",
    ["0x0000000000000000000000000000000000000000000000000000000000000000"],
    "latest"
  ]
}

Dans la valeur de retour, accountProof est un tableau de nœuds partant du stateRoot, tandis que chaque entrée de storageProof part du storageRoot du compte. Les deux ne sont que des données : c'est au vérificateur de recalculer pour décider si elles sont dignes de confiance.

C'est le principe de travail de clients légers comme Helios : le client léger de la couche de consensus commence par valider l'en-tête du bloc auprès de la chaîne balise, en obtient le payload de la couche d'exécution authentifié et le stateRoot, demande ensuite eth_getProof à un RPC quelconque non fiable, puis effectue localement la vérification MPT. La section de motivation de l'EIP-1186 mentionne elle aussi que ces preuves permettent à des objets connectés et à des applications mobiles de vérifier des données de comptes et de stockage issues de sources non fiables, à partir d'un seul hachage de bloc digne de confiance.

Les limites de la promesse : preuves, anciennes racines et clients légers

La portée d'une preuve a une limite claire. Elle ne couvre que le chemin prouvé et ne permet pas d'en déduire le reste de l'état ; son volume croît avec la profondeur du chemin et la largeur des branches. Le brouillon EIP-8297 illustre cet ordre de grandeur par une estimation : en partant d'une profondeur maximale du trie des comptes d'environ 12 niveaux, une branche de compte demande 12 niveaux de 15 hachages frères chacun, soit 15 × 32 × 12 = 5 760 octets ; si les 60 M de gaz étaient entièrement consacrés à toucher un octet unique d'un grand nombre de codes de contrats différents, sans découpage du code, le brouillon estime le volume de preuve à environ 1,8 GB. Ce sont là des estimations avancées par le brouillon lui-même, non revérifiées par des mesures sur le mainnet : la conclusion directionnelle peut être retenue, les valeurs chiffrées ne doivent pas servir de référence. C'est aussi pourquoi le MPT est jugé peu adapté aux preuves de validité : encodage RLP, hachage Keccak, structure d'arbre dans l'arbre, et impossibilité de prouver le code par segments.

Les preuves portant sur d'anciennes racines ne sont pas disponibles indéfiniment. La promesse reste dans l'en-tête de bloc, mais sa réalisation dépend de la façon dont le client conserve l'état. Depuis la v1.16, l'archive de type path-based de Geth enregistre les différences historiques de l'état aplati, ce qui permet de lire des états passés, mais la v1.16.x ne sait pas produire de preuve de Merkle pour une ancienne racine ; il faut attendre la v1.17 pour que la conservation explicite de l'historique des tries permette les preuves historiques ; l'archive de type hash-based classique conserve les anciens nœuds de trie et peut produire une preuve pour n'importe quelle ancienne racine. Le coût en disque est traité dans la section suivante. La sémantique de la promesse relève du consensus ; la manière de l'honorer relève des choix d'implémentation.

Les clients légers de la couche d'exécution ont eux aussi connu un recul. Sur le mainnet, le protocole LES a longtemps cessé de fonctionner de manière fiable après la fusion, et Geth a supprimé le code correspondant fin 2023. La vérification hors chaîne prend aujourd'hui le plus souvent la forme d'une combinaison « la couche de consensus authentifie la racine, la couche d'exécution fournit la preuve » ; or le client léger de la couche de consensus valide l'en-tête de bloc de la chaîne balise et ne réexécute pas les transactions.

Le gonflement de l'état : la facture de la promesse tombe sur le disque

Le pouvoir expressif du stateRoot est indépendant de la taille de l'état, mais son coût d'entretien lui est directement corrélé. Chaque mise à jour de l'état réécrit tout le chemin des feuilles à la racine : plus l'état est profond et large, plus une transaction exige de lectures et d'écritures ; un nouveau nœud qui veut rattraper la chaîne doit en outre télécharger cet état dans son intégralité.

Une analyse publiée sur le forum de recherche d'Ethereum en novembre 2025 donne un ordre de grandeur. D'après l'article, en mai 2025, un nœud Geth ne portant que l'état représentait une base de données non compressée d'environ 340 GiB ; après le relèvement de la limite de gaz de 30 M à 36 M, la croissance quotidienne médiane de l'état est passée d'environ 102 MiB à environ 205 MiB. La page du projet bloatnet situe le seuil critique à 650 GB et indique qu'à l'approche de cette taille, le temps d'accès à l'état augmente d'environ 40 % et que l'occupation mémoire comme le temps de synchronisation se dégradent nettement ; la page ne fournit pas de données de référence reproductibles, la formulation retenue ici reprend donc le discours du projet lui-même, et celui-ci parle de GB et non de GiB. L'analyse du forum extrapole ensuite selon trois trajectoires de limite de gaz — prudente, de référence, agressive (respectivement 200 M, 400 M et 700 M à la mi-2027) — et aboutit à une taille totale de l'état comprise entre 686 GiB et 1,08 TiB à la mi-2027. Il s'agit d'une extrapolation de scénarios, non d'un fait acquis : les valeurs dépendent de l'implémentation du client, des stratégies de compression et d'élagage, ainsi que de l'évolution réelle de la limite de gaz.

Du point de vue de l'exploitation des nœuds, le besoin en disque d'un nœud complet Geth (synchronisation snap) recensé par ethereum.org dépasse 500 GB ; selon la communication officielle de Geth, un nœud d'archive de type path-based demande environ 2 TB, la conservation des données historiques de trie environ 6,5 TB et le type hash-based plus de 20 TB, tandis que les besoins d'archive tous clients confondus recensés par ethereum.org vont de 3 TB à plus de 12 TB. Ces chiffres dépendent de l'implémentation du client, du mode de compression, de la stratégie d'élagage et de la limite de gaz ; les comparer directement d'une implémentation à l'autre n'a pas de sens. La taille de l'état n'est d'ailleurs pas la taille de l'historique on-chain : les transactions et les reçus passés sont une autre facture.

L'état du mainnet Ethereum ne fait que croître : une fois écrits, comptes et emplacements de stockage occupent l'espace de façon permanente, et faute de mécanisme d'expiration ou de loyer de l'état, la pression s'accumule dans un seul sens. Les pistes d'atténuation se répartissent en gros en deux familles : changer la structure d'arbre, par exemple l'arbre de Verkle discuté de longue date ainsi que l'arbre binaire partitionné (Partitioned Binary Tree, brouillon EIP-8297) encore au stade de brouillon ; ou bien découper l'état pour qu'un nœud n'en maintienne qu'une partie. La seconde est le sujet d'articles ultérieurs ; on se contente ici d'exposer l'origine de la pression. Pour la disposition des slots côté contrat, voir 0.19《Storage Layout》.

Sources

  • Ethereum Yellow Paper : la fonction de transition d'état de la section 2, les champs d'en-tête de bloc et la validité globale du chapitre 4 (définition et conditions de vérification du stateRoot), la définition des nœuds MPT de l'annexe D, l'encodage hex-prefix, la règle d'inlining à 32 octets et l'espace de preuve O(log N) de la section D.1.
  • ethereum.org: Merkle Patricia Trie : les types de nœuds, l'implémentation de référence de l'encodage compact, la définition des clés et des valeurs des trois tries, l'encodage des valeurs des transactions et des reçus.
  • ethereum.org: Ethereum accounts : la formulation officielle du storageRoot et du codeHash.
  • EIP-1186: RPC-Method to get Merkle Proofs : les champs de eth_getProof, la manière de prouver une absence et les cas d'usage ; le storageHash vide (0x56e81f…) et le codeHash vide (0xc5d246…) de l'exemple officiel servent aussi à recouper les deux constantes vides du bloc de code du corps de l'article.
  • EIP-8297: Partitioned Binary Tree (Draft, créée le 2026-06-11, fonction de hachage non encore arrêtée) : la profondeur maximale d'environ 12 niveaux du trie des comptes, la preuve de branche de 5 760 octets, le volume de preuve d'environ 1,8 GB dans le pire cas, et les raisons pour lesquelles le MPT est peu adapté aux preuves de validité. Ces éléments sont signalés dans le corps de l'article comme des estimations avancées par le brouillon lui-même.
  • Ethereum Research: State growth scenarios and the impact of repricings (2025-11-19) : la taille d'état de 340 GiB, la croissance quotidienne de 102 MiB à 205 MiB, et l'extrapolation de scénarios à l'horizon 2027 selon trois trajectoires de limite de gaz. L'article relève de l'analyse de scénarios, non d'un fait acquis.
  • Bloatnet Initiative : le seuil critique de 650 GB et l'augmentation d'environ 40 % du temps d'accès à l'état selon la communication du projet, la page ne joignant aucune donnée de référence reproductible.
  • go-ethereum: Archive mode : les valeurs de 2 TB et 6,5 TB pour l'archive path-based, plus de 20 TB pour l'archive hash-based, et la différence de prise en charge des preuves historiques entre la v1.16.x et la v1.17.
  • ethereum.org: Ethereum archive node et Spin up your own Ethereum node : les fourchettes de besoin en disque des nœuds d'archive et des nœuds complets tous clients confondus (archive de 3 TB à plus de 12 TB ; synchronisation snap de Geth : plus de 500 GB).
  • go-ethereum PR #28586 : la suppression de LES et du code de client léger associé.
  • a16z crypto: Building Helios : la voie du client léger où la couche de consensus authentifie le stateRoot et la couche d'exécution effectue la vérification locale au moyen de eth_getProof.

Pour aller plus loin

La tension entre décentralisation et performance : exigences des validateurs, matériel et répartition géographique Associés Exécution parallèle multi-moteur de Bitroot : ordonnancement, sharding et surface de conflit Associés Pourquoi un EVM monothread plafonne le TPS : histoire de la congestion et modèle d'exécution Suivant
← PrécédentLa mécanique du gaz en détail : unité de mesure, marché des frais et interruption d'exécution
Sommaire
Trois tries dans un même en-tête de blocLes adresses entrent dans le trie hachées, et un compte est un quadrupletTrois types de nœuds et un drapeau de deux bits : comment les chemins de 64 niveaux sont raccourcisLe stateRoot est une promesse portant sur tout l'étatDe la racine à un compte : comment s'utilise une preuve de MerkleLes limites de la promesse : preuves, anciennes racines et clients légersLe gonflement de l'état : la facture de la promesse tombe sur le disqueSourcesPour aller plus loin
Réglages de lecture