NOUVEAU : Helius acquiert Light Protocol
Qu’est-ce que la Solana Virtual Machine (SVM) ?
Blog/Fondamentaux

Qu’est-ce que la Solana Virtual Machine (SVM) ?

Developer Experience Engineer0xIchigo sur X0xIchigo sur LinkedIn0xIchigo sur GitHub
67 min de lecture

Un grand merci à Lostin, Alessandro, Brian, Brady et Daniel Cumming pour leur relecture des versions précédentes de ce travail. 

Enseignements pratiques

  • La SVM englobe toute la pile d’exécution des transactions, contrairement à l’EVM, qui désigne sans ambiguïté un exécuteur de bytecode.
  • Le fait d’imposer aux transactions de déclarer les comptes auxquels elles accéderont avant l’exécution permet l’exécution parallèle sur plusieurs cœurs de CPU et la création de marchés de frais localisés.
  • Le code source Rust est compilé par rustc en LLVM IR, puis le backend eBPF de LLVM, et plus précisément le fork sBPF de Solana, le convertit en bytecode sBPF. Tout langage doté d’un frontend LLVM (par exemple C, C++ ou Zig) peut donc servir à écrire des programmes Solana.
  • sBPF est le fork par Solana de l’eBPF de Linux. Dans la pratique, il lui est quasiment identique, hormis quelques ajouts historiques qui doivent toutefois être supprimés. La seule différence notable est que les fonctions eBPF en amont acceptent au maximum 5 arguments, tandis que le fork de Solana peut en accepter davantage.
  • Le bytecode sBPF compilé est stocké dans des fichiers ELF contenant des sections pour les instructions et les constantes, ainsi que des tables de relocalisation. L’éditeur de liens résout les références aux appels système en hachages Murmur3 déterministes de 32 bits et réécrit les fonctions internes sous forme de sauts relatifs pour garantir la portabilité. 
  • Le BPF Loader Upgradeable utilise un modèle à deux comptes pour les mises à niveau sur place et les déploiements, tandis que le futur Loader V4 simplifie ce modèle en n’utilisant qu’un seul compte avec compression facultative. Tout le bytecode fait l’objet d’une vérification statique avant d’être marqué comme exécutable.
  • Les transactions contiennent un tableau d’adresses de comptes assorties de droits de lecture et d’écriture, ainsi que des instructions et des signatures. Ce format structuré permet de détecter les conflits et de planifier en parallèle les transactions qui n’entrent pas en conflit.
  • La Banking Stage du TPU est chargée de planifier en parallèle les transactions qui n’entrent pas en conflit. La Bank charge l’état des comptes depuis AccountsDB. Les BPF Loaders provisionnent des VM sBPF isolées avec des régions mémoire délimitées et des budgets de calcul. Les modifications d’état réussies sont validées de façon atomique, tandis que les échecs entraînent une annulation complète.
  • L’ISA de la SVM constitue la seule spécification formelle : outre le livre blanc d’Alpenglow et la série d’articles initialement rédigés par Toly, il n’existe aucune « spécification de la SVM » unique. L’environnement d’exécution naît de l’interaction entre la Bank, le planificateur, les BPF Loaders et la VM sBPF elle-même.

Introduction

La Solana Virtual Machine (SVM) est aujourd’hui l’un des systèmes les plus mal compris du secteur des blockchains. Contrairement à l’Ethereum Virtual Machine (EVM), qui désigne sans ambiguïté un exécuteur d’opcodes, le terme SVM englobe tout un pipeline d’exécution des transactions, du planificateur de la Banking Stage jusqu’à l’interpréteur de bytecode sBPF. Cette ambiguïté reflète une particularité de l’architecture de Solana : aucune spécification traditionnelle ne définit « la SVM » de manière isolée. La spécification qui s’en rapproche le plus concerne la Solana Virtual Machine Instruction Set Architecture (SVM ISA). Elle décrit comment le bytecode sBPF doit être exécuté, mais ne dit rien sur l’environnement d’exécution au sens large.

Cet article se veut une référence complète sur ce qu’est la SVM, son fonctionnement et les raisons pour lesquelles elle est fondamentalement différente, du point de vue de l’implémentation du validateur Agave d’Anza. L’étude du fonctionnement du client de Firedancer et de son implémentation personnalisée de machine virtuelle conforme à l’ISA de la SVM dépasse le cadre de ce travail.

Nous voulons examiner le code réel plutôt qu’une spécification abstraite. Nous retraçons l’intégralité du pipeline d’exécution : la compilation du code source Rust via LLVM en bytecode sBPF, le déploiement et la vérification des programmes, le provisionnement par l’environnement d’exécution de contextes isolés pour l’exécution parallèle et l’interaction des transactions avec le bytecode déployé.

Les premières sections replacent l’ambiguïté de la SVM dans son contexte et présentent son fonctionnement général. La suite de l’article s’adresse à un public plus technique souhaitant acquérir une compréhension rigoureuse de la couche d’exécution de Solana.

Une définition controversée

Le terme « Solana Virtual Machine » (SVM) a suscité de vifs débats au sein de la communauté, notamment depuis l’apparition d’extensions réseau et d’autres blockchains de couche supérieure construites sur Solana. La controverse porte sur la portée du terme : la SVM se limite-t-elle strictement à l’interpréteur sBPF de bas niveau, ou englobe-t-elle l’ensemble de la pile d’exécution des transactions ? 

La définition étroite considère la SVM comme l’équivalent d’une machine virtuelle (VM) traditionnelle, telle que l’exécuteur d’opcodes de l’EVM. Plus précisément, il s’agit de la machine virtuelle dérivée d’eBPF (rBPF, désormais sBPF) qui interprète et compile à la volée le bytecode. Cette perspective présente la SVM comme un exécuteur sécurisé et fondé sur des registres qui traite des instructions telles que les opérations d’ALU ou les appels système propres à Solana. En substance, la SVM s’inspire du modèle de sécurité de l’eBPF de Linux, mais est adaptée à l’infrastructure blockchain. Cette définition correspond à des expressions telles que SVM ISA (Instruction Set Architecture) dans le code du validateur, où la SVM désigne uniquement la couche VM.

La définition large présente la SVM comme la couche d’exécution complète des transactions d’un validateur Solana. Elle ne se limite pas à l’exécution du bytecode : elle comprend aussi des composants en amont, comme le planificateur de la Banking Stage, la budgétisation des unités de calcul et les mises à jour de l’état via la base de données des comptes, couramment appelée AccountsDB. C’est l’« environnement d’exécution » qui transforme les transactions brutes en modifications d’état validées.

Cette ambiguïté vient du fait que les communications officielles de Solana emploient indifféremment les termes « environnement d’exécution » et « SVM », sans définition unique et figée. Anza a apporté au débat des éclaircissements indispensables, en confirmant explicitement cette ambiguïté tout en défendant une perspective pragmatique, orientée vers l’action et fondée sur l’ingénierie. Sa présentation de la SVM comme un environnement d’exécution piloté par la Bank qui provisionne la VM eBPF propose une définition beaucoup plus large, englobant tout le pipeline, qui permet de formuler une définition appropriée de la SVM.

Cette vision est formalisée dans la spécification officielle de la SVM d’Anza, qui définit la SVM comme « les composants responsables de l’exécution des transactions », regroupés dans une bibliothèque autonome destinée aux validateurs, aux preuves de fraude, aux sidecars et à d’autres usages.

Pour les besoins de cet article, nous pouvons définir la Solana Virtual Machine ainsi :

L’interface découplée de l’environnement d’exécution et le pipeline de traitement des transactions au sein des validateurs Solana, pilotés par le composant Bank, qui orchestrent l’exécution parallèle des instructions et des programmes on-chain en provisionnant une machine virtuelle personnalisée fondée sur eBPF afin d’assurer l’interprétation sécurisée du bytecode, la compilation JIT et la mesure des ressources.

Vue d’ensemble de la SVM

La Solana Virtual Machine (SVM) sert d’environnement d’exécution pour le traitement des transactions qui interagissent avec des programmes on-chain sur l’ensemble du réseau. C’est la couche d’exécution où le code rencontre l’état, l’environnement qui transforme des transactions signées cryptographiquement en modifications d’état validées. 

Pour véritablement comprendre la SVM, nous devons d’abord comprendre ce que signifie une machine virtuelle dans le contexte des blockchains.

Machines virtuelles

Une machine virtuelle (VM) est un logiciel qui virtualise ou émule un système informatique et fournit un environnement d’exécution isolé se comportant comme du matériel physique. Le concept est né des travaux menés par IBM dans les années 1960 sur les systèmes mainframe, qui ont permis à plusieurs utilisateurs d’exécuter différents systèmes d’exploitation sur une même machine physique. Il existe deux grandes catégories de VM : les VM système et les VM de processus. Les premières remplacent une machine réelle, tandis que les secondes sont conçues pour exécuter des programmes dans un environnement indépendant de la plateforme. Dans notre cas, nous nous intéressons aux machines virtuelles système, que nous appellerons désormais « machines virtuelles » ou simplement « VM ».

Les machines virtuelles résolvent plusieurs problèmes fondamentaux. Tout d’abord, elles offrent une couche d’abstraction du matériel. Autrement dit, les programmes écrits pour une VM peuvent s’exécuter sur tout matériel physique prenant en charge cette VM, sans qu’il soit nécessaire de les réécrire. La philosophie « écrire une fois, exécuter partout » de Java en est un bon exemple : le bytecode Java s’exécute de façon identique sous Windows, macOS, Linux et sur d’autres systèmes équipés d’une Java Virtual Machine (JVM).

Les VM apportent également des garanties d’isolation et de sécurité. Chaque instance de VM fonctionne dans un environnement sécurisé, ce qui signifie qu’elle ne peut pas accéder aux ressources du système hôte ni aux autres VM, sauf autorisation explicite. Ainsi, si un programme plante ou contient du code malveillant, les dommages restent circonscrits à cette instance de VM. C’est en raison de ce principe d’isolation que des fournisseurs cloud tels que Google Cloud et AWS utilisent des VM pour séparer les charges de travail de leurs clients.

Les VM offrent aussi des résultats prévisibles. Elles fournissent donc un environnement contrôlé où une même entrée produit toujours la même sortie, quel que soit le matériel sous-jacent. Cette prévisibilité est essentielle au débogage, aux tests et à l’obtention d’un consensus entre systèmes distribués.

Les VM peuvent également être extrêmement performantes. Les VM modernes utilisent la compilation Just-In-Time (JIT) pour réduire au minimum la surcharge de performances. La compilation JIT traduit le bytecode de la VM en code machine natif au moment de l’exécution afin d’atteindre des performances proches du natif, tout en préservant la portabilité et les garanties de sécurité précédemment citées.

Machines virtuelles dans les blockchains

Les blockchains ont adapté le concept de VM pour résoudre un défi inédit : comment faire en sorte que des milliers d’ordinateurs indépendants répartis dans le monde exécutent du code non fiable et parviennent à des résultats identiques ? Les VM servent d’environnement d’exécution déterministe pour exécuter les contrats intelligents, c’est-à-dire les programmes sur Solana, et gérer l’état du réseau, soit l’état actuel de tous les comptes, soldes et autres données du réseau.

Lorsqu’une transaction est soumise à une blockchain, la VM doit :

  • Charger depuis le stockage les données de compte nécessaires.
  • Exécuter le bytecode du programme indiqué dans la transaction.
  • Mesurer la consommation de ressources pour empêcher les boucles infinies ou les attaques par déni de service (DoS).
  • Vérifier que toutes les modifications d’état respectent les règles de consensus prédéfinies du réseau.
  • Enregistrer l’état mis à jour dans le stockage permanent, c’est-à-dire le registre.

Les règles précises qui régissent les transitions d’état sont définies par l’architecture du jeu d’instructions de la VM et les contraintes de l’environnement d’exécution.

Fonctionnement de la SVM

La SVM est un pipeline de sous-systèmes qui collaborent pour exécuter les transactions de manière sûre et efficace. La Bank orchestre l’exécution pour un slot donné, gère l’état des comptes, applique les règles de consensus et assure la coordination entre la Banking Stage et le stockage persistant, c’est-à-dire AccountsDB. Chaque Bank représente l’état de tous les comptes à un slot donné et passe par trois phases de cycle de vie : active (ouverte aux nouvelles transactions), gelée (fermée aux nouvelles transactions, car le slot est terminé) et enracinée (intégrée à la chaîne canonique).

La Banking Stage est l’étape où les transactions sont exécutées au sein de la Transaction Processing Unit (TPU) d’un validateur. Elle reçoit les transactions vérifiées par l’étape SigVerify, les met en mémoire tampon et planifie leur exécution parallèle en détectant les conflits au niveau des verrouillages de comptes. Les threads de travail de la Banking Stage traitent des lots de transactions sans conflit et appellent les méthodes d’exécution de la Bank afin de charger les comptes, de provisionner des instances de VM sBPF pour chaque instruction, d’exécuter le bytecode des programmes et de recueillir les résultats. La Banking Stage continue de traiter des lots de transactions sans conflit jusqu’à ce que la Bank soit gelée à la limite du slot. Notez que les lots sont distincts des entrées, qui sont les unités enregistrées de transactions écrites dans le registre à des fins de réplication et de consensus.

Les BPF Loaders gèrent le cycle de vie des programmes : déploiement, compilation JIT, mises à niveau et exécution. Lorsqu’une instruction cible un programme donné, une VM sBPF est provisionnée avec ses propres régions mémoire et son propre budget de calcul, puis l’exécution est transférée au bytecode du programme.

La VM sBPF est l’environnement d’exécution isolé dans lequel le bytecode du programme s’exécute réellement. Dérivée de l’eBPF de Linux, elle repose sur une architecture dotée de 11 registres à usage général. La VM impose l’isolation de la mémoire au moyen de cinq régions mémoire distinctes, chacune assortie de limites et d’autorisations explicites. Elle mesure aussi la consommation d’unités de calcul pour empêcher toute exécution incontrôlée et distribue les appels système nécessaires aux opérations privilégiées telles que la cryptographie, la journalisation ou les Cross-Program Invocations (CPI).

AccountsDB est la couche d’état persistante où résident toutes les données des comptes. L’état des comptes est chargé avant l’exécution en s’appuyant sur des caches afin d’éviter les lectures répétées sur disque pour les comptes fréquemment consultés. Après une exécution réussie, les mises à jour sont enregistrées dans AccountsDB. En cas d’échec, toutes les modifications d’état sont annulées de façon atomique.

Ensemble, ces composants forment la SVM, un moteur d’exécution découplé et réutilisable.

Ce qui distingue la SVM : la déclaration préalable des comptes

La décision architecturale qui définit la SVM est la suivante : toutes les transactions doivent déclarer explicitement les comptes qu’elles liront et modifieront avant le début de l’exécution. Intégrée au format même des transactions, cette exigence simple ouvre la voie à deux capacités transformatrices qui distinguent Solana : l’exécution parallèle et les marchés de frais localisés.

Exécution parallèle (Sealevel)

Contrairement à l’Ethereum Virtual Machine (EVM), qui traite les transactions de manière séquentielle, une par une, en attendant la fin de chacune avant de passer à la suivante, la SVM permet une mise à l’échelle horizontale en exécutant simultanément plusieurs transactions sur plusieurs cœurs de CPU. Cette parallélisation est possible parce que toutes les transactions Solana déclarent explicitement les comptes qu’elles liront et modifieront avant le début de l’exécution. 

La déclaration des comptes qu’une transaction lira et modifiera permet à l’environnement d’exécution d’analyser les dépendances entre comptes afin de détecter les conflits et de planifier les transactions sans conflit :

  • Les transactions qui touchent des comptes totalement différents peuvent s’exécuter en parallèle sans aucune surcharge de coordination.
  • Les transactions qui se contentent de lire les mêmes comptes peuvent également s’exécuter en parallèle, car les lectures n’entrent pas en conflit.
  • Les transactions qui tentent de modifier les mêmes comptes s’exécutent de manière séquentielle afin d’éviter les conditions de concurrence et de garantir la cohérence de l’état. 

Marchés de frais locaux

Puisque l’environnement d’exécution connaît précisément les comptes auxquels chaque transaction accédera avant son exécution, les frais peuvent être localisés sur des comptes précis au lieu de faire l’objet d’une concurrence globale sur tout le réseau. Ce concept est appelé marchés de frais locaux. 

Sur Ethereum et les autres chaînes EVM, chaque transaction participe à un marché de frais mondial unique : envoyer des ETH à un ami, émettre un NFT ou effectuer une transaction sur Uniswap revient toujours à enchérir pour le même espace de bloc. Une hausse d’activité dans un domaine augmente les frais pour tout le monde, même pour les utilisateurs qui essaient de faire quelque chose de totalement différent. 

Sur Solana, seules les transactions qui accèdent aux mêmes comptes sont en concurrence. Un utilisateur qui transfère des SOL entre deux comptes ne devrait pas avoir à se soucier de l’émission simultanée d’un NFT populaire. Les frais de priorité d’une transaction sont uniquement déterminés par la contention sur les comptes. C’est grâce à cette localisation que les transactions Solana peuvent rester peu coûteuses, même pendant les périodes de forte activité.

Le 10 octobre, par exemple, le marché des cryptomonnaies a connu le plus grand événement de liquidation de son histoire. Malgré une hausse d’activité record, les transactions Solana sont restées relativement peu coûteuses : les frais médians ont atteint 0,007 $, les frais moyens ont brièvement atteint 0,10 $ et les 1 % de transactions les plus chères ont culminé à un peu plus de 1,00 $. Sur la même période, les frais médians d’Ethereum et d’Arbitrum ont dépassé 100 $, tandis que ceux de Base ont culminé à plus de 3 $.

Un changement de paradigme

AspectEVMSVM
ArchitectureVM fondée sur une pileVM fondée sur des registres (dérivée d’eBPF)
ExécutionSéquentielleParallèle (détection des conflits)
Marché des fraisMondialLocalisé (contention par compte)
Déclaration des comptesNon requise au préalableRequise au préalable
ISA~140 opcodes, opérations de pile~100 opcodes, registres de type RISC
Compilation JITFacultative (selon le client)Standard (performances natives)
Modèle d’étatFrais de stockage des contratsBase de données de comptes uniforme
LangagesSolidity/Vyper → bytecode EVMRust/C/C++ → LLVM → sBPF

La SVM représente une approche fondamentalement différente de l’exécution sur blockchain. Bitcoin a introduit la monnaie programmable. Ethereum a introduit les contrats intelligents généralistes et l’exécution arbitraire on-chain. Cependant, les deux sont limités par l’exécution séquentielle et les marchés de frais mondiaux, des choix architecturaux qui imposent des limites fondamentales au débit comme au coût.

La SVM s’affranchit des contraintes traditionnelles et propose un réseau capable de gérer un débit élevé sans sacrifier la programmabilité ni imposer aux utilisateurs des enchères de frais prohibitifs. Le choix d’exiger la déclaration préalable des comptes est simple, mais puissant : il permet l’exécution parallèle sur plusieurs cœurs de CPU et localise les frais sur des marchés propres à chaque compte.

Bien entendu, ce ne sont pas les seules optimisations proposées par Solana par rapport aux autres blockchains. Le mot d’ordre de Solana, Augmenter la bande passante, réduire la latence, et son obsession absolue pour la concrétisation du rêve des marchés de capitaux sur Internet ont donné lieu à diverses optimisations des performances, décisions de conception et implémentations visant à développer un réseau à haut débit.

La suite de cet article examine précisément le fonctionnement de ce système : comment le code source Rust est compilé en bytecode, comment ce bytecode est déployé et vérifié, et comment l’environnement d’exécution provisionne des contextes isolés afin d’exécuter en toute sécurité des milliers de programmes en parallèle, tout en maintenant un déterminisme strict et de solides garanties de sécurité. 

Du code source Rust au bytecode sBPF : le pipeline de compilation

Rust

Rust est la lingua franca du développement de programmes Solana. Des frameworks comme Anchor proposent aux développeurs une approche robuste et structurée pour créer efficacement des programmes sécurisés. solana_program est conçue comme la bibliothèque de base de tous les programmes on-chain. Récemment, Pinocchio, une bibliothèque hautement optimisée et sans dépendance, est devenue l’option privilégiée des développeurs souhaitant créer des programmes Solana natifs. 

Quel que soit le framework ou la bibliothèque utilisés, tous les programmes possèdent un point d’entrée que l’environnement d’exécution appelle lorsque le programme est invoqué. La macro entrypoint de solana_program génère le code standard nécessaire pour démarrer l’exécution du programme, à savoir la désérialisation des entrées, la configuration d’un allocateur global et celle d’un gestionnaire de panique. Pinocchio exporte des macros de point d’entrée qui fonctionnent de manière similaire, mais découplent le point d’entrée de la configuration de l’allocateur de tas et du gestionnaire de panique, ce qui offre davantage de possibilités aux développeurs. 

Anatomie de base d’un programme 

Un programme est un type de compte capable d’exécuter du code. Plus précisément, il s’agit d’un compte exécutable qui stocke un bloc de bytecode sBPF dans un compte détenu par le BPF Loader et doté d’une clé publique unique. Par conception, les programmes sont sans état : toutes les données persistantes résident dans des comptes distincts, que les programmes peuvent lire ou modifier lorsqu’ils sont invoqués.

La SVM attend de tous les programmes qu’ils suivent une structure précise : un point d’entrée qui accepte trois entrées :

  • ID du programme : l’adresse du programme lui-même, utilisée pour les vérifications autoréférentielles, comme la propriété.
  • Comptes : un tableau de métadonnées de comptes, c’est-à-dire les clés publiques, les soldes en lamports, les tampons de données, les propriétaires et les indicateurs. Ils constituent l’« état » que le programme doit lire et modifier.
  • Données d’instruction : une tranche d’octets contenant des données arbitraires issues de la transaction.

Un programme doit traiter ces entrées par l’intermédiaire de son point d’entrée, modifier les comptes inscriptibles concernés, émettre des journaux ou des événements, puis renvoyer un état de réussite indiquant s’il a pu accomplir toutes ces opérations. Cela se résume à une fonction process_instruction.

Voici à quoi ressemble un programme simple écrit en Rust avec le crate solana_program :

simple_rust_program.rs
use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    pubkey::Pubkey,
};

entrypoint!(process_instruction);

pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, Solana!");

    Ok(())
}

En coulisses, tout cela est défini par l’Application Binary Interface (ABI) de la SVM, que nous étudierons plus loin.

Le compilateur Rust et LLVM IR

À l’instar de nombreux autres langages de programmation, Rust est une abstraction de second ordre construite sur le langage assembleur. Il est conçu pour permettre aux humains d’écrire du code sûr, concurrent et lisible sans devoir gérer dans le moindre détail chaque interaction avec le matériel. Cependant, les ordinateurs ne comprennent ni Rust ni aucun autre langage de haut niveau. 

Les ordinateurs comprennent le code machine, c’est-à-dire des instructions binaires adaptées à une architecture ou à une machine virtuelle précise. Tous les programmes sont finalement convertis en code binaire, et cette traduction est elle-même effectuée par des ordinateurs. La compilation est un processus de traduction en plusieurs étapes qui élimine les abstractions de haut niveau, optimise l’efficacité et produit un bytecode exécutable.

rustc est le compilateur officiel de Rust. La plupart des développeurs n’interagissent généralement pas directement avec rustc : ils l’invoquent plutôt par l’intermédiaire de Cargo, le gestionnaire de paquets de Rust. rustc fait néanmoins passer le code source Rust par trois étapes principales avant de produire le bytecode exécutable. Chaque étape élimine des abstractions, impose des règles de sécurité et prépare le code à la transformation suivante :

  • Analyse syntaxique et expansion
  • MIR (Mid-level Intermediate Representation)
  • LLVM IR (Low Level Virtual Machine Intermediate Representation)

Analyse syntaxique et expansion

Le compilateur lit un fichier Rust donné, identifié par l’extension .rs, comme du texte brut. Il recherche des tokens précis, par exemple use, fn, None, impl et &[u8], au cours d’un processus appelé analyse lexicale. La tokenisation lexicale consiste à convertir du texte en tokens lexicaux porteurs de sens appartenant à une catégorie donnée, comme les identifiants, les opérateurs, les séparateurs, les littéraux ou les mots-clés. 

rustc prend ces tokens lexicaux et les convertit en une structure de données appelée Abstract Syntax Tree (AST). Cette arborescence représente la structure imbriquée et hiérarchique du code source Rust : les fonctions contiennent des blocs, les blocs contiennent des expressions, les expressions contiennent des opérateurs, et ainsi de suite. Bien qu’il reste de haut niveau, l’AST fournit une représentation fidèle du code source et de sa logique sous-jacente.

Une fois l’AST construit, le compilateur effectue plusieurs transformations essentielles :

  • Expansion des macros : les macros telles que entrypoint! et println! sont développées en code Rust brut, de sorte que toutes les macros soient converties en nœuds AST ordinaires.
  • Abaissement : la syntaxe abrégée de haut niveau conçue pour rendre le code plus lisible est réécrite sous des formes plus primitives au cours d’un processus appelé abaissement. Une boucle for, par exemple, est transformée en boucle loop avec itération manuelle. Le résultat de cet abaissement est la High-Level Intermediate Representation (HIR).
  • Vérification des emprunts et analyse de sécurité : Rust prend la HIR et effectue la vérification des types, la résolution des traits et l’inférence de types. Ce processus produit la Typed High-Level Intermediate Representation (THIR).

À cette étape, le compilateur traite le code unsafe. Celui-ci permet aux développeurs d’effectuer des opérations qui contournent les garanties de sécurité de Rust, comme le déréférencement de pointeurs bruts, l’appel de fonctions étrangères ou l’implémentation de traits unsafe. Il constitue une échappatoire volontaire offrant un contrôle de bas niveau : certaines règles sont assouplies, mais le code doit toujours être compilable conformément à la sémantique de Rust. Cette possibilité est essentielle pour les programmes Solana, où le code unsafe peut être utilisé avec parcimonie afin d’améliorer les performances d’opérations critiques, comme la désérialisation sans copie des comptes, par exemple dans la structure wrapper de Pinocchio pour un Account.    

Le code unsafe est d’abord identifié après l’analyse lexicale, pendant la construction de l’AST, lorsque les tokens unsafe sont reconnus et marqués comme des nœuds spéciaux. Il est ensuite traité après l’expansion de l’AST, lors de la vérification des types et des emprunts. Le compilateur veille à ce que les opérations unsafe soient limitées aux contextes unsafe et signale une erreur dans le cas contraire, par exemple « impossible de déréférencer un pointeur brut en dehors d’un bloc unsafe ». En revanche, il ne vérifie pas, par exemple, que le code unsafe n’altère pas ou ne gère pas incorrectement la mémoire.

À la fin de cette étape, l’AST développé, désormais sous forme de THIR, constitue une représentation validée et abaissée du code source Rust.

MIR

La THIR est ensuite abaissée vers la Mid-Level Intermediate Representation (MIR), une forme centrée sur Rust qui représente le code source sous la forme d’un graphe de flot de contrôle (CFG) simplifié. Tout le sucre syntaxique et les constructions complexes propres à Rust, comme le filtrage par motif, les traits et les fermetures, sont exprimés sous forme de blocs de base comprenant des affectations et des branchements. Ces blocs de base sont reliés par des sauts, ou plus précisément des Goto, et des branchements, ce qui facilite l’analyse du flux d’un programme. 

La MIR n’est pas strictement indispensable. Le compilateur pourrait simplement abaisser directement la THIR en LLVM IR. Toutefois, la MIR fournit une couche consciente de Rust qui permet au compilateur d’appliquer les règles propres à Rust et d’effectuer des optimisations avant l’application des optimisations génériques de LLVM. La MIR est donc idéale pour les vérifications et transformations qui sont trop haut niveau pour LLVM, mais trop bas niveau pour la THIR, notamment :

  • Vérification des emprunts : les premières vérifications sémantiques interviennent lors de l’analyse des types sur la THIR. Le CFG simplifié de la MIR permet toutefois une vérification complète et précise des emprunts afin d’appliquer toutes les règles de propriété, d’emprunt et de durée de vie.
  • Vérification des déplacements et des libérations : le compilateur veille à ce que toutes les valeurs soient déplacées et libérées conformément aux garanties de sécurité mémoire de Rust, ce qui évite les erreurs d’utilisation après libération.
  • Analyse de l’initialisation : le compilateur vérifie que toutes les variables sont initialisées avant leur utilisation.
  • Inlining et optimisations précoces : le compilateur peut incorporer de petites fonctions, simplifier des expressions arithmétiques et éliminer le code inaccessible.

Dans notre exemple de programme Rust, nous avons précédemment invoqué msg!(“Hello, Solana!”). Il s’agit d’une macro définie dans le crate solana-program qui, pour une expression unique telle qu’une chaîne statique, est développée pendant la phase d’expansion des macros afin d’appeler directement sol_log($msg), où $msg est l’expression. L’appel système sol_log reçoit un pointeur vers les données de la chaîne et sa longueur, puis l’inscrit dans la sortie de la SVM sans la surcharge liée au formatage. Dans la MIR, cela pourrait être simplifié ainsi :

Code
bb0: {
  _0 = const "Hello, Solana!"; // Constant string allocation
  _1 = len(_0); // Compute length
  sol_log(move _0, move _1); // Syscall invocation with explicit moves for ownership
  return = Ok(());
}

Ici,

  • _0 = const “Hello, Solana!”; — la MIR introduit des variables temporaires, ici _0, pour les valeurs intermédiaires. La chaîne est traitée comme une tranche constante allouée dans des données en lecture seule.
  • _1 = len(_0); — la MIR expose une simple opération de longueur sur la tranche afin de permettre un éventuel repliement de constantes.
  • sol_log(move _0, move _1); — l’invocation de l’appel système, qui est traduite en une séquence d’instructions sBPF chargeant les registres et appelant l’ID de l’appel système. Nous expliquerons précisément ce que cela signifie dans les sections suivantes, mais il faut surtout retenir que ces déplacements sont liés à la sémantique de propriété de Rust et qu’ils l’appliquent lors de la compilation.
  • Return = Ok(()); — termine le bloc par un terminateur, signalant une réussite à la SVM.

Grâce à son attention portée à la sémantique de Rust, la MIR constitue l’endroit idéal pour repérer les inefficacités et déboguer les motifs gourmands en CU. Si votre journal contient des chaînes dynamiques, par exemple, la MIR peut détecter des allocations ou boucles supplémentaires susceptibles d’être optimisées. Les développeurs peuvent utiliser la commande cargo rustc -- -Z dump-mir=all pour exporter la MIR.

La MIR garantit que le code est sémantiquement correct, optimisé et débarrassé de toutes les règles propres à Rust avant son abaissement final en LLVM IR.

Notez que cette étape est généralement appelée phase de génération de code. Elle ne repose pas nécessairement sur LLVM. Cependant, LLVM est courant et correspond à ce que la plupart des gens associent à la génération de code Rust. Le compilateur Rust est également fourni avec des backends GCC et Cranelift qui produisent respectivement GIMPLE et CLIF. Nous nous concentrons sur LLVM IR dans le contexte de Solana, mais ce n’est pas toujours le cas pour Rust en général.

LLVM IR

LLVM, qui signifiait à l’origine « Low Level Virtual Machine », désigne un framework de compilation modulaire. Plutôt qu’un compilateur unique, LLVM est une boîte à outils de composants réutilisables permettant de créer des compilateurs, des optimiseurs et des générateurs de code. De nombreux langages, comme Rust, C, C++, Julia, Swift, Brainfuck et Zig, utilisent LLVM pour tirer parti de sa capacité à cibler diverses architectures, des CPU x86 aux ISA virtuelles. 

rustc traduit la MIR en LLVM IR (Low Level Virtual Machine Intermediate Representation), le pont entre la sémantique de Rust et le bytecode qui sera finalement déployé sur Solana. Cette représentation est bien plus proche du code machine, avec des allocations mémoire explicites, c’est-à-dire alloca, ainsi que des stockages, des chargements et des appels de fonction. Elle ne possède aucune notion de propriété, de durée de vie ou de trait, car ces abstractions Rust ont déjà été développées jusqu’à disparaître ; les garanties des étapes précédentes sont conservées par la suite.

Diverses optimisations sont appliquées à cette étape, notamment :

  • Repliement de constantes, c’est-à-dire l’évaluation des constantes au moment de la compilation.
  • Inlining, c’est-à-dire le remplacement d’un appel par le corps de la fonction.
  • Élimination du code mort, c’est-à-dire la suppression des instructions qui n’influencent pas les résultats.
  • Déroulage et vectorisation des boucles, c’est-à-dire la réécriture des boucles pour accélérer leur exécution.

LLVM nous fournit donc :

  • LLVM IR : un format intermédiaire portable semblable à de l’assembleur.
  • Passes d’optimisation : pour créer LLVM IR, LLVM utilise la Static Single Assignment (SSA), qui garantit que chaque variable n’est affectée qu’une seule fois, ce qui permet des optimisations telles que l’inlining et l’élimination du code mort.
  • Générateurs de code : des cibles qui abaissent LLVM IR en véritable code machine, par exemple x86_64, ARM, WebAssembly ou eBPF.

Les programmes Rust sont généralement compilés pour des cibles matérielles telles que x86_64 ou ARM. Toutefois, les programmes Solana ne s’exécutent pas directement sur du matériel. Ils s’exécutent dans la Solana Virtual Machine. Le backend LLVM abaisse donc LLVM IR en bytecode BPF, qui devient sur Solana du bytecode sBPF, un fork d’eBPF qui supprime les fonctionnalités non déterministes et introduit des appels système propres à Solana.

Notez que si Rust est la lingua franca du développement de programmes Solana, tout langage capable de cibler le backend BPF de LLVM, par exemple C, Nim, Swift ou Zig, peut être utilisé.

eBPF

LLVM IR est abaissée vers eBPF, l’ISA fondée sur des registres qui constitue la base de l’environnement d’exécution de Solana. eBPF (Extended Berkeley Packet Filter) est issu du Berkeley Packet Filter (BPF), développé en 1992 par Steven McCanne et Van Jacobson au Lawrence Berkeley Laboratory pour les systèmes Unix Berkeley Software Distribution (BSD). En substance, BPF est un dispositif d’écoute réseau et un filtre de paquets qui permet de capturer et de filtrer des paquets réseau au niveau du système d’exploitation sans copier les données, en s’appuyant sur des qualificateurs. 

eBPF a depuis évolué, ou plutôt été étendu, pour devenir une VM généraliste et isolée au sein du noyau Linux. Ce qu’il rend possible est comparable à ce que JavaScript a apporté au développement web : c’est un moteur de scripts sécurisé pour les noyaux. eBPF permet aux développeurs d’exécuter de petits programmes vérifiés directement dans le noyau Linux avec un jeu d’instructions limité pour des tâches telles que la surveillance des performances, l’observabilité, la sécurité et la mise en réseau.

C’est important, car les développeurs bénéficient ainsi des éléments suivants :

  • Exécution isolée : les programmes eBPF s’exécutent dans une machine virtuelle restreinte au sein du noyau et ne peuvent donc ni faire planter ni altérer la mémoire du noyau.
  • Garanties de sécurité : le bytecode eBPF fait l’objet d’une vérification statique avant son chargement afin de garantir l’absence d’accès mémoire non valide, de sauts hors limites ou d’autres opérations privilégiées, ce qui assure la sécurité sans surcharge à l’exécution.
  • Efficacité : eBPF repose sur des registres plutôt que sur une pile comme l’EVM et peut être compilé en JIT en code machine afin d’atteindre des vitesses proches du natif grâce à sa conception légère, sans la surcharge d’un système d’exploitation complet.
  • Flexibilité : eBPF expose des appels système, également appelés syscalls, qui constituent essentiellement des points d’accès aux fonctionnalités du noyau. Les appels système peuvent être enrichis de nouvelles capacités sans nécessiter la refonte du jeu d’instructions.

Solana avait besoin d’une VM déterministe, sûre et performante pour exécuter des programmes non fiables sur l’ensemble de ses validateurs. eBPF offre un modèle de sécurité éprouvé, une ISA portable et efficace conçue pour exécuter des milliers de programmes légers, ainsi qu’une prise en charge de JIT pour de meilleures performances. Plutôt que d’inventer une toute nouvelle VM, Solana a donc créé un fork d’eBPF nommé sBPF.

sBPF 

À l’origine, Solana Labs a créé un fork de rBPF de Quentin Monnet afin de produire sa propre version de rBPF, garantissant ainsi que chaque validateur disposerait d’un format de bytecode produisant exactement les mêmes résultats lors de l’exécution d’un programme et d’une entrée donnés. 

Un fork d’eBPF était jugé nécessaire, car le consensus de Solana exigeait une exécution déterministe et une utilisation limitée des ressources. Bien qu’eBPF soit lui-même déterministe, Solana avait besoin de garanties supplémentaires et de fonctionnalités propres à la blockchain :

  • Des durées et des coûts d’instruction fixes.
  • Une exécution en espace utilisateur.
  • Un environnement d’exécution déterministe.

Il est notamment conçu pour s’exécuter dans l’espace utilisateur plutôt que dans le noyau, ce qui évite d’avoir besoin de privilèges ou de modifications du noyau. Il peut ainsi être déployé dans divers environnements de système d’exploitation sans accès root ni modules de noyau personnalisés. L’espace utilisateur constitue un choix pratique pour Solana : portabilité, tests et déploiement simplifié. Malgré l’exécution en espace utilisateur, JIT atteint toujours des performances proches du natif. De plus, cette exécution permet de réaliser des tests et du fuzzing sans accès au noyau.

rBPF n’est plus utilisé. Lors de la création d’Anza, l’organisation a créé un fork de rBPF appelé sBPF (Solana Berkeley Packet Filter). Le dépôt GitHub de rBPF, détenu par Solana Labs, a été archivé le 10 janvier 2025.

L’ISA de la SVM

La SVM ISA (Solana Virtual Machine Instruction Set Architecture) est la spécification centrale qui définit la manière dont les VM compatibles avec la SVM, comme sBPF d’Agave ou la réimplémentation de Firedancer, doivent exécuter les programmes. Ce n’est pas la VM elle-même, mais la norme ou le contrat qui garantit la cohérence et la conformité au protocole entre les différentes implémentations de la SVM. C’est la SVM ISA qui impose à eBPF ces contraintes de sécurité et de déterminisme, en supprimant les fonctionnalités centrées sur le noyau tout en ajoutant des fonctions propres à la blockchain.

L’ISA régit les registres, l’encodage des instructions, les opcodes, les classes, les règles de vérification, les conditions de panique et l’Application Binary Interface (ABI). Toute modification de la SVM ISA doit être implémentée au moyen de SIMD afin d’assurer l’évolution contrôlée de ce jeu d’instructions et de garantir une exécution déterministe sur l’ensemble des validateurs.

Registres

Les registres sont de minuscules emplacements de stockage situés dans la VM qui conservent des nombres ou des adresses pendant l’exécution des instructions, à la manière de variables ou de boîtes étiquetées sur un établi. La SVM ISA définit une architecture de registres 64 bits comportant 11 registres à usage général (R0-R10) et un compteur de programme masqué. Les registres ont une largeur de 64 bits pour les entiers et les adresses, ce qui permet de traiter efficacement de grandes valeurs ou des pointeurs. R0 contient les valeurs de retour des fonctions, R1-R5 transmettent les cinq premiers arguments de fonction comme des paramètres, R6-R9 sont préservés par l’appelé et persistent entre les appels de fonction, tandis que R10 sert de pointeur de cadre en lecture seule et indique le cadre de pile actuel. Le compteur de programme masqué suit l’exécution et indique la prochaine instruction à exécuter.

Instructions

Une instruction est une opération unique que la VM sait effectuer, comme « additionner ces deux nombres » ou « sauter à cette ligne de code ». Les instructions suivent une conception de type RISC avec environ 100 opcodes, contre plusieurs milliers dans les architectures CISC telles que x86, ce qui accélère la vérification et rend la compilation JIT efficace.

Les instructions sont encodées sous forme de valeurs de 64 bits au format Little Endian, selon la structure suivante :

  • opcode : 8 bits
  • dst_reg : 4 bits
  • src_reg : 4 bits
  • offset : 16 bits (signé)
  • immediate : 32 bits (signé)

opcode indique l’opération à effectuer, dst_reg la destination du résultat, src_reg la provenance de l’entrée, offset le décalage mémoire à consulter et immediate une constante supplémentaire qui peut être incluse dans l’instruction.

lddw, ou load double word, est la seule instruction large à occuper deux emplacements de 64 bits afin de prendre en charge des valeurs immédiates complètes de 64 bits. 

Les instructions sont regroupées en différentes classes, notamment les opérations mémoire, les opérations arithmétiques ou logiques, les branchements conditionnels et inconditionnels, les appels et retours de fonctions, ainsi que la conversion de boutisme.

Régions mémoire

L’ISA définit cinq régions mémoire, chacune dotée de limites explicites, c’est-à-dire [addr, addr+len], qui déterminent les zones qu’un programme peut lire ou modifier :

  • Code du programme : les instructions compilées elles-mêmes (lecture + exécution).
  • Pile : espace de travail temporaire pour les fonctions (lecture + écriture, généralement 4 Ko par cadre).
  • Tas : mémoire dynamique qu’un programme peut demander (lecture + écriture).
  • Données d’entrée : octets en lecture seule transmis avec une transaction.
  • Données en lecture seule : constantes et valeurs immuables.

Les programmes disposent d’une cartographie prédéfinie de la mémoire virtuelle : le code du programme commence à l’adresse 0x000000000 ou 0x100000000, selon la version de compilation, les cadres de pile commencent à 0x200000000, le tas à 0x300000000 et les données d’entrée à 0x400000000.

Le vérificateur

Le vérificateur effectue une analyse statique avant l’exécution : il examine tous les chemins de code possibles sans exécuter le programme afin d’appliquer les garanties de sécurité dès le chargement plutôt qu’au moment de l’exécution. Il vérifie notamment les points suivants :

  • Aucune instruction inconnue ou non prise en charge.
  • Toutes les cibles de saut correspondent à des limites d’instruction valides et les sauts en arrière sont traités.
  • Aucun chemin de code inaccessible.
  • Les limites de profondeur des appels de fonction sont appliquées.
  • Les divisions ou opérations modulo par zéro sont rejetées statiquement.
  • Les programmes doivent respecter une taille maximale.

Bien qu’utile, le vérificateur n’empêche pas un développeur d’introduire un comportement inattendu. Les développeurs peuvent toujours introduire, par exemple, des erreurs d’utilisation après libération ou de dépassement de tampon dans un programme Solana. 

Conditions de panique

Les conditions de panique sont les cas d’erreur à l’exécution définis par la SVM ISA. Elles comprennent :

  • Des instructions non valides ou non prises en charge.
  • Une division ou une opération modulo par zéro.
  • Un accès mémoire hors limites.
  • Un accès mémoire non valide pour différentes régions mémoire, c’est-à-dire une violation d’autorisation.
  • Un débordement de pile.
  • Un dépassement de la profondeur d’appel.
  • Un dépassement du nombre maximal d’instructions autorisées.
  • Le renvoi d’un code d’erreur par le programme.

ABI

L’Application Binary Interface (ABI) est le contrat de format entre un programme Solana et la SVM. Alors que la section précédente Anatomie de base d’un programme a montré comment cela fonctionne en Rust, c’est-à-dire process_instruction avec trois entrées, l’ABI précise comment ces entrées et sorties sont représentées en mémoire afin que chaque validateur puisse exécuter les programmes de manière déterministe.

À un niveau général, l’ABI définit trois éléments : la convention du point d’entrée, les conventions d’appel et les registres, ainsi que l’organisation de la mémoire.

Chaque programme Solana doit exposer une fonction de point d’entrée. Le loader sérialise les entrées du programme dans l’espace mémoire de la VM selon un ordre canonique : l’ID du programme, le tableau de comptes et les données d’instruction. La VM transmet ensuite des pointeurs vers ces régions au point d’entrée du programme.

Les cinq premiers registres, c’est-à-dire R1-R5, sont réservés aux arguments du point d’entrée, tandis que le registre de retour, R0, contient le code de sortie du programme. Un code de sortie égal à zéro indique une réussite, tandis qu’une valeur non nulle correspond à un échec associé à une InstructionError précise. Tous les programmes renvoient ainsi des codes d’état de manière cohérente. De plus, les paramètres situés après les cinq premiers sont transmis sur la pile. R6-R9 suivent une convention de préservation par l’appelé : les fonctions doivent donc conserver ces valeurs si elles les utilisent.

Les comptes et les données sont sérialisés sous forme de tranches d’octets dans la mémoire linéaire de la VM. Les programmes doivent les désérialiser en types Rust de plus haut niveau (par exemple, AccountInfo, Pubkey). L’ABI impose des limites strictes afin qu’aucun programme ne puisse accéder à la mémoire en dehors des régions qui lui sont allouées.

Ensemble, ces règles font de l’ABI la « colle » qui relie l’expérience de développement de haut niveau à l’ISA de bas niveau. Elle garantit qu’une simple signature de fonction Rust est compilée avec l’utilisation correcte des registres, la bonne disposition de la mémoire et les bons codes de retour, afin que chaque validateur interprète toujours un programme donné exactement de la même manière.

Appels système

L’ISA est volontairement minimale et ne comporte aucun compte ni état intégré. Elle ne fournit pas non plus directement de fonctionnalités de haut niveau, telles que la journalisation, le hachage ou les appels interprogrammes. À la place, elle expose des appels système, des fonctions spéciales intégrées à la VM qui permettent aux programmes d’interagir avec le monde extérieur. Ces appels sont couramment appelés syscalls.

Les syscalls peuvent être considérés comme des API fournies par la VM. Au lieu que chaque programme doive réimplémenter des primitives cryptographiques ou une logique de compte particulières, les syscalls exposent des opérations sûres et standardisées dont le comportement déterministe est garanti sur tous les validateurs.

Les catégories de syscalls courantes comprennent :

  • Journalisation et débogage (par exemple, le syscall sol_log écrit une chaîne UTF-8 dans les journaux du programme et est utilisé en interne par msg!).
  • Appel interprogramme (CPI) (par exemple, sol_invoke_signed permet à un programme d’appeler un autre programme on-chain en lui transmettant des comptes et des données d’instruction, ce qui est essentiel à la composabilité de Solana).
  • Cryptographie (par exemple, les syscalls sol_sha256, sol_keccak256 et sol_ed25519_verify ajoutent tous des primitives cryptographiques rapides et déterministes sans obliger les développeurs à écrire leurs propres implémentations).
  • Utilitaires de mémoire et de compte (c’est-à-dire des syscalls qui exposent des fonctions auxiliaires pour emprunter les données d’un compte, réallouer de la mémoire ou manipuler les allocations du tas appartenant au programme).
  • Budget et comptabilisation des unités de calcul (c’est-à-dire que chaque syscall consomme des CU, comme l’impose le système de comptabilisation de l’environnement d’exécution).

Les syscalls sont invoqués au moyen d’une instruction spéciale CALL_IMM dotée d’un identifiant de hachage unique. Lorsqu’un programme appelle un syscall, la VM sBPF interrompt l’exécution, recherche le hachage dans son registre de syscalls et transmet l’appel à l’implémentation native exécutée dans le code privilégié de l’environnement d’exécution. Les syscalls s’exécutent en dehors du bac à sable avec accès à l’état de l’environnement d’exécution, ce qui diffère totalement de l’exécution des appels de fonction ordinaires au sein d’un programme.

Les syscalls suivent la même ABI que les fonctions ordinaires : les cinq premiers arguments sont transmis dans les registres R1 à R5 et la valeur de retour dans R0. Chaque syscall a un coût fixe en unités de calcul, ce qui garantit une consommation déterministe des ressources. Par exemple, tous les appels au syscall secp256k1_recover consomment 25 000 CU.

Les syscalls constituent une frontière de sécurité contrôlée, car chacun valide ses entrées et vérifie les autorisations pertinentes avant d’effectuer toute opération privilégiée. Par exemple, un syscall d’appel interprogramme (CPI) vérifie que l’appelant dispose des autorisations appropriées pour les comptes transmis.

Notez que de nouveaux syscalls peuvent être ajoutés au moyen de feature gates sans qu’il soit nécessaire de modifier l’ISA elle-même. Solana peut ainsi étendre les capacités de sa VM, notamment en prenant en charge de nouvelles primitives cryptographiques, tout en maintenant la rétrocompatibilité avec les programmes existants.  

Binaire du programme

À la fin de la compilation, toutes les étapes — du code source Rust à LLVM IR, puis à eBPF, à sBPF et à sa conformité avec l’ISA de la SVM — produisent un résultat unique : un binaire de programme. C’est ce binaire qui est réellement déployé sur Solana. 

ELF

Les programmes Solana sont compilés en fichiers Executable and Linkable Format (ELF), un format binaire standard utilisé dans les systèmes de type Unix. Le format ELF sert de conteneur et regroupe tout ce dont la VM a besoin pour exécuter un programme donné, tout en restant indépendant de la plateforme.

Un fichier ELF contient généralement les sections suivantes :

  • Section de bytecode : contient les instructions sBPF compilées dans la section .text.
  • Section de données en lecture seule : contient les constantes, les chaînes statiques et les valeurs immuables dans une section .rodata.
  • Sections BSS et de données : contiennent respectivement les variables globales ou statiques modifiables dans les sections .bss et .data. Notez que Solana n’autorise pas les données modifiables. Autrement dit, l’ELF peut comporter une section .rodata, mais pas les sections BSS et de données. 
  • Tables des symboles et des relocalisations : définissent la manière dont les appels de fonction, les syscalls et les références mémoire sont résolus lors du chargement, dans les sections .symtab et .strtab pour les symboles, et .rel.dyn et .rela.dyn pour les entrées de relocalisation.

Chaque fichier ELF comprend également un en-tête décrivant l’architecture, la largeur des instructions (c’est-à-dire 64 bits), l’endianness (c’est-à-dire petit-boutiste) et l’adresse du point d’entrée.

Édition de liens et relocalisation

Le processus qui transforme la sortie du compilateur en un fichier ELF exécutable unique fait intervenir un dernier composant : l’éditeur de liens. Celui-ci combine plusieurs unités de code compilées en un seul binaire cohérent. Il résout également toutes les références symboliques (c’est-à-dire les espaces réservés) que le compilateur n’a pas pu résoudre. Par exemple :

  • Lorsqu’un programme appelle une fonction telle que sol_log, le compilateur ne sait pas où elle se trouve en mémoire. Il utilise donc un espace réservé.
  • L’éditeur de liens remplace cette référence symbolique par l’identifiant haché unique du syscall (c’est-à-dire un hachage Murmur3 déterministe de 32 bits).
  • De même, les appels entre fonctions internes sont réécrits sous forme de sauts relatifs vers des décalages d’instruction dans la section .text.

Ce processus de réécriture des références symboliques en adresses concrètes ou en identifiants de syscall hachés s’appelle la relocalisation. Il convient toutefois de noter que les relocalisations sont surtout un artefact de la conception initiale des outils plutôt qu’une exigence fondamentale. Il est d’ailleurs prévu de les supprimer entièrement dans de futures versions de la chaîne d’outils afin de simplifier le processus de déploiement.

Cette étape de relocalisation est nécessaire pour garantir que le même binaire ELF s’exécute à l’identique sur tous les validateurs, puisqu’aucune adresse mémoire absolue ni aucun symbole propre au système n’y est intégré. 

De plus, le bytecode dans lequel ces relocalisations ont déjà été appliquées est mis en cache en mémoire. Toutes les exécutions ultérieures utilisent donc le bytecode mis à jour sans avoir à retraiter les relocalisations.

Une fois que l’éditeur de liens a produit un fichier ELF entièrement relocalisé, le programme est prêt à être déployé. Le résultat final est un binaire qui est :

  • Portable : il s’exécute à l’identique sur n’importe quel validateur ou implémentation de la SVM.
  • Déterministe : il ne contient aucun syscall non déterministe ni aucune dépendance au système d’exploitation.
  • Autonome : il contient tout le bytecode et toutes les métadonnées nécessaires à son exécution.

Comment le bytecode est chargé sur Solana

Une fois qu’un programme Solana a été compilé et lié dans un fichier ELF valide, l’étape suivante consiste à le charger sur la blockchain afin qu’il puisse être exécuté par les validateurs. Ce processus, appelé déploiement du programme, implique plusieurs composants qui fonctionnent ensemble : le BPF Loader, les modèles de comptes, la vérification du bytecode et la gestion de l’état.

Le programme BPF Loader

Le BPF Loader est un programme natif qui valide et relocalise les fichiers ELF, puis les marque comme exécutables. Il gère essentiellement le cycle de vie des programmes déployés : il traite les instructions pour initialiser les comptes, écrit le bytecode, déploie les programmes et gère les mises à niveau.

Solana a connu plusieurs versions successives de loaders, chacune améliorant la précédente :

  • BPF Loader : le loader d’origine pour les programmes statiques non évolutifs, qui n’est plus pris en charge. 
  • BPF Loader V2 : un loader simplifié sans instruction de gestion.
  • BPF Loader Upgradeable : le loader actuel, qui a introduit la possibilité de mettre les programmes à niveau.
  • BPF Loader V4 : la dernière version, dotée de meilleures fonctionnalités de déploiement et simplifiant le modèle actuel à deux comptes en un modèle à compte unique.

Architecture de déploiement : modèles de comptes

Modèle de compte actuel

Le loader actuel utilise une architecture à deux comptes pour séparer la logique du programme de ses données. Un programme donné dispose donc de deux comptes : le compte Program et le compte ProgramData.

Le compte Program est un petit compte d’environ 36 octets qui contient des métadonnées et est marqué comme exécutable. Il stocke une référence vers le ProgramData via UpgradeableLoaderState::Program { programdata_address }.

Le compte ProgramData est un compte plus volumineux qui stocke le bytecode ELF proprement dit ainsi que les métadonnées de déploiement (par exemple, le slot et l’adresse de l’autorité de mise à niveau) via UpgradeableLoaderState::ProgramData.

La séparation des deux comptes permet les mises à niveau sur place. Autrement dit, le compte Program conserve la même adresse tandis que le bytecode du compte ProgramData peut être remplacé.

Futur modèle de compte

Loader V4 vise à rationaliser le processus de déploiement grâce à un modèle à compte unique. Le compte du programme stockera directement les métadonnées et le bytecode, ce qui éliminera le besoin d’un compte ProgramData distinct. Les développeurs pourront également stocker une image compressée avec zstd afin de réduire les coûts de loyer.

Comment les programmes Solana sont déployés

Le processus de déploiement consiste à charger un binaire ELF compilé, puis à demander au BPF Loader de le vérifier, de le mettre en cache et de le marquer comme exécutable. Ce processus diffère légèrement entre le loader évolutif et V4 en raison de l’architecture de déploiement décrite dans la section précédente.

BPF Loader Upgradeable

Le processus de déploiement actuel avec le loader évolutif implique l’initialisation d’un compte tampon destiné à préparer le bytecode ELF. Le déployeur envoie une instruction InitializeBuffer au BPF Loader Upgradeable, qui crée un nouveau compte appartenant au loader et définit son état sur UpgradeableLoaderState::Buffer { authority_address }, en enregistrant l’adresse autorisée à écrire dans le tampon. 

Le binaire ELF compilé est chargé dans le tampon par fragments au moyen de l’instruction Write { offset, bytes }. Chaque instruction d’écriture vérifie que le signataire correspond à l’autorité du tampon, s’assure que celui-ci est encore modifiable (c’est-à-dire qu’il n’a pas encore été déployé) et écrit les octets au décalage indiqué après l’en-tête des métadonnées. Notez que les programmes volumineux nécessitent plusieurs instructions Write pour charger l’intégralité du fichier ELF en raison des limites de taille des transactions.

Une fois que le tampon contient l’ELF complet, le déployeur envoie une instruction DeployWithMaxDataLen { max_data_len }. Il s’agit de l’étape la plus complexe de tout le processus, car elle orchestre le déploiement proprement dit, de la validation des comptes à la finalisation de l’état.

Le loader commence par valider tous les comptes impliqués dans le processus de déploiement et vérifie que :

  • Le compte du programme n’est pas initialisé et est exempté de loyer.
  • Le tampon contient des données valides et l’autorité qui l’a initialisé a signé la transaction.
  • La valeur max_data_len est suffisamment grande pour contenir les données du tampon.
  • La taille totale ne dépasse pas MAX_PERMITTED_DATA_LENGTH (c’est-à-dire 10 Mio, soit 10 485 760 octets). 

Le loader crée ensuite le compte ProgramData, dont il dérive l’adresse sous forme de PDA à l’aide de l’ID du programme et de l’ID du loader. Il restitue ensuite les lamports du tampon au payeur, car le compte tampon n’est plus nécessaire après le déploiement. 

En outre, il crée le compte ProgramData par CPI vers le System Program, en allouant suffisamment d’espace pour les métadonnées et les max_data_len octets. Le loader utilise ensuite la graine de bump du PDA pour signer le CPI. 

La macro deploy_program! garantit que le bytecode peut être exécuté en toute sécurité. Elle commence par analyser la structure du fichier ELF afin de valider les octets magiques ELF (c’est-à-dire 0x7f ‘E’ ‘L’ ‘F’) et les en-têtes (c’est-à-dire 64 bits, petit-boutiste), extrait les sections du programme, traite les tables de relocalisation et valide les limites et l’alignement des sections. Le chargement échoue immédiatement si l’ELF est mal formé ou utilise des fonctionnalités non prises en charge.

Le RequisiteVerifier (c’est-à-dire le vérificateur de sBPF) effectue ensuite une analyse statique de tous les chemins d’exécution possibles sans exécuter le programme, afin de prouver sa sûreté avant toute instruction. Le vérificateur impose également les contraintes de l’ISA de la SVM mentionnées précédemment. Si la vérification échoue, le déploiement est rejeté avec InstructionError::InvalidAccountData et le programme n’est jamais marqué comme exécutable.

Une fois la vérification réussie, le bytecode est compilé et mis en cache pour son exécution. La fonction load_program_from_bytes crée un ProgramCacheEntry contenant :

  • Exécutable compilé à la volée : le bytecode sBPF est compilé à la volée (JIT) en code machine natif pour l’architecture CPU du validateur. Cela offre une vitesse d’exécution proche du natif tout en préservant la sécurité.
  • Métadonnées de slot : l’heure de déploiement et l’heure de prise d’effet du programme sont enregistrées respectivement sous deployment_slot et effective_slot. Ce délai empêche l’utilisation des programmes dans le slot où ils sont déployés.
  • Environnement d’exécution : références au registre des syscalls pour indiquer les syscalls disponibles, ainsi que la configuration d’exécution utilisée lors de l’exécution du programme. 

L’entrée de cache est stockée dans program_cache_for_tx_batch, ce qui rend le programme disponible pour les transactions suivantes. Après la vérification et la mise en cache réussies du programme, le loader met à jour l’état des comptes pour finaliser le déploiement. L’état du compte ProgramData est mis à jour afin d’enregistrer la date de déploiement du programme et l’autorité habilitée à le mettre à niveau. Le bytecode ELF est également copié du tampon vers le compte. L’état du compte Program est lui aussi mis à jour pour le relier au compte ProgramData, puis il est marqué comme exécutable. Enfin, la longueur des données du tampon est ramenée à la taille des métadonnées, ce qui efface de fait le bytecode et libère l’espace.

Le programme est désormais entièrement déployé et peut être invoqué par des transactions.

BPF Loader V4

Le BPF Loader V4 rationalise le déploiement en supprimant le besoin d’un compte ProgramData distinct et en permettant au compte du programme de stocker directement le bytecode. Il prend également en charge le stockage d’ELF compressés avec zstd, ce qui réduit considérablement les coûts de loyer tout en permettant une décompression à la demande pendant le chargement.

Le déployeur appelle SetProgramLength { new_size } pour allouer de l’espace aux métadonnées et au bytecode du programme. Pour les nouveaux programmes, cette opération initialise le compte avec l’état LoaderV4State::Retracted, enregistre l’autorité et marque le compte comme exécutable, même s’il ne peut pas encore être invoqué.

Le déployeur écrit ensuite le binaire ELF directement dans le compte du programme au moyen d’instructions Write { offset, bytes }. Ces écritures ne sont autorisées que lorsque le programme se trouve dans l’état Retracted. L’instruction Copy peut également servir à copier le bytecode d’un autre programme, quelle que soit la version du loader, ce qui facilite les migrations.

L’instruction Deploy est ensuite utilisée pour faire passer le programme de l’état Retracted à l’état Deployed. Elle extrait le bytecode du compte du programme au décalage indiqué et exécute exactement le même pipeline de vérification que le BPF Loader Upgradeable (c’est-à-dire analyse ELF, vérification statique, compilation JIT et mise en cache). Si la vérification réussit, l’état du programme est remplacé par LoaderV4Status::Deployed et le slot de déploiement est enregistré.

Le programme est désormais entièrement déployé et peut être invoqué par des transactions.

Loader V4 impose également une période de temporisation entre les transitions d’état (c’est-à-dire le déploiement et la rétractation) afin d’empêcher les attaques par redéploiement. Les programmes ne peuvent pas être déployés ou rétractés dans le slot suivant leur dernier déploiement. Cela empêche les acteurs malveillants de mettre rapidement les programmes à jour pour exploiter des conditions de concurrence ou induire les utilisateurs en erreur, et garantit une atomicité par slot plutôt que des délais sur plusieurs slots. Notez que cette temporisation s’applique aux instructions Deploy et Retract.

Les programmes peuvent également être rendus immuables au moyen de l’instruction Finalize. Celle-ci fait passer le programme de l’état Deployed à l’état Finalized, ce qui signifie qu’il ne peut plus être rétracté ni mis à niveau. Le champ d’autorité est réaffecté pour pointer vers l’adresse d’un programme de « version suivante », ce qui permet des chemins de mise à niveau explicites tout en préservant l’immuabilité du programme.

Fonctionnement de l’exécution dans la SVM

La SVM est le moteur de traitement des transactions au sein des validateurs. Elle exécute les invocations de programmes et met à jour l’état en conséquence. 

Lorsqu’une transaction arrive à un validateur, elle traverse un pipeline en plusieurs étapes : validation, chargement des comptes, exécution du programme dans une VM sBPF isolée, vérification des invariants et validation de l’état. Si toutes les instructions réussissent, les modifications des comptes sont écrites dans AccountsDB. Si une instruction échoue, l’ensemble de la transaction est annulé de manière atomique.

La SVM fonctionne comme un moteur d’exécution découplé. Elle ne gère ni le consensus, ni le réseau, ni l’historique du registre. Elle se concentre exclusivement sur l’exécution sûre, déterministe et efficace des programmes. 

La Bank orchestre l’exécution de la SVM, fournit le contexte de l’environnement d’exécution (par exemple, le blockhash, le loyer et l’ensemble des fonctionnalités) et enregistre les résultats dans le stockage persistant. La SVM gère l’exécution des programmes, du chargement du bytecode à l’application des budgets de calcul. Cette séparation des responsabilités permet de réutiliser la SVM au-delà des validateurs.

Transactions 

Les transactions sont le moteur de Solana, comme de toute blockchain : elles invoquent des programmes pour modifier l’état. 

Une transaction est un ensemble d’instructions qui indiquent les actions à effectuer, les comptes concernés et si les autorisations nécessaires sont présentes. 

Une instruction est une directive correspondant à une seule invocation de programme. C’est la plus petite unité de logique d’exécution et l’unité opérationnelle la plus élémentaire sur Solana.

Les programmes interprètent les données transmises par une instruction pour agir sur les comptes indiqués. Une instruction comprend un ID de programme (c’est-à-dire le programme invoqué), une liste des comptes à lire et à modifier, ainsi que l’entrée transmise au programme.

Une transaction commence lorsqu’un utilisateur définit un objectif, par exemple transférer 10 SOL vers un autre compte. Cette intention est traduite en une instruction demandant au System Program de transférer 10 SOL du compte A vers le compte B. Le compte A est transmis à la transaction en tant que signataire modifiable, et le compte B en tant que compte modifiable. L’instruction est ensuite incluse dans une transaction qui précise également le payeur des frais, les signataires et un blockhash récent.

La transaction est ensuite généralement envoyée à un fournisseur RPC tel que Helius. Le nœud RPC qui la reçoit vérifie alors que toutes les signatures requises sont présentes et valides, que la transaction n’a pas déjà été traitée, que le blockhash récent fourni est encore valide et que la transaction ne dépasse pas la taille maximale (c’est-à-dire 1 232 octets).

Le RPC transmet ensuite la transaction à la Transaction Processing Unit (TPU) du leader actuel. 

La Transaction Processing Unit (TPU)

La Transaction Processing Unit (TPU) est le pipeline d’ingestion et de traitement des transactions au sein des validateurs Solana. Elle comporte plusieurs étapes qui reçoivent, vérifient, planifient et exécutent les transactions avant leur inscription dans le registre de Solana.

Pour les besoins de cet article, nous examinerons en détail la Fetch Stage, la SigVerify Stage et la Banking Stage avant de passer à la Bank et au provisionnement de la VM sBPF. 

Pour une analyse plus détaillée de la TPU, consultez Qualité de service pondérée par le stake : tout ce que vous devez savoir.

La Fetch Stage

La Fetch Stage est la première étape du pipeline de la TPU. Elle reçoit toutes les transactions entrantes via des connexions QUIC, qui utilisent des sockets UDP comme couche de transport sous-jacente, puis les regroupe en lots pour les étapes suivantes.

Trois sockets UDP sont créés :

  • tpu : transactions ordinaires telles que les transferts de tokens, la création de NFT et les interactions avec des programmes.
  • tpu_vote : transactions de vote des validateurs — cela changera avec Alpenglow, lorsque les transactions de vote seront supprimées.
  • tpu_forwards : transactions non traitées transmises par le leader précédent, qui n’a pas pu les traiter à temps.

Ces sockets sont enregistrés dans le service gossip du validateur et stockés dans la structure ContactInfo, ce qui permet aux autres validateurs et aux nœuds RPC de découvrir où envoyer les transactions.

La Fetch Stage lance un thread par socket. Tous exécutent en continu les opérations suivantes :

  • Interroger le socket UDP pour détecter les paquets entrants.
  • Créer un lot de 64 paquets.
  • Envoyer le lot sur son canal non borné.

Des canaux non bornés sont actuellement utilisés pour transmettre les lots aux étapes suivantes, ce qui signifie qu’ils ont une capacité illimitée. Leur utilisation permet également à la Fetch Stage de fonctionner indépendamment de la vitesse de traitement des étapes suivantes. 

Cela évite la perte immédiate de paquets lors des pics de trafic, mais peut causer des problèmes de mémoire : si les étapes suivantes ne suivent pas le rythme de la Fetch Stage, les canaux grossissent sans limite, ce qui peut provoquer des ralentissements ou des plantages par manque de mémoire (OOM). 

Des travaux sont en cours pour mettre en place des canaux bornés avec une contre-pression appropriée, afin que le système puisse signaler la congestion et empêcher une croissance illimitée de la mémoire.

La Fetch Stage crée également un autre thread dédié au traitement des paquets transférés. Ces paquets sont marqués par un indicateur FORWARDED, puis conservés ou supprimés selon le calendrier des leaders :

  • Conservés : si le validateur actuel doit bientôt devenir leader, les paquets transférés sont traités par le canal normal de la TPU et envoyés à l’étape suivante.
  • Supprimés : si le validateur actuel ne doit pas devenir leader prochainement, les paquets transférés sont abandonnés afin d’éviter tout traitement inutile. 

La Fetch Stage utilise un PacketBatchRecycler pour préallouer 1 000 lots de paquets, contenant chacun 1 024 paquets. Cela réduit le coût des allocations mémoire, puisque la mémoire des lots peut être réutilisée au lieu d’allouer de nouveaux lots à chaque fois. Historiquement, ce mécanisme était nécessaire pour l’épinglage de la mémoire CUDA, qui n’est plus fonctionnel et peut être largement considéré comme une dette technique. 

La SigVerify Stage

La SigVerify Stage est la deuxième étape du pipeline de la TPU. Comme son nom l’indique, elle vérifie les signatures. Cette opération intervient tôt dans le pipeline, car la vérification des signatures Ed25519 est coûteuse en calcul, bien que moins coûteuse que l’exécution des transactions. Vérifier les transactions avant leur exécution permet aux validateurs de rejeter les transactions frauduleuses, d’empêcher les attaques par déni de service et de s’assurer que seules les transactions correctement formées atteignent la Banking Stage.

La SigVerify Stage fonctionne comme un thread unique qui reçoit continuellement les paquets des canaux de la Fetch Stage et les traite dans un pipeline de vérification. Malgré ce thread unique, elle exploite un important parallélisme interne.

Par défaut, les signatures sont vérifiées sur le CPU au moyen d’un itérateur parallèle. La vérification est ainsi répartie sur tous les cœurs CPU disponibles, chaque cœur pouvant vérifier indépendamment un sous-ensemble de signatures. 

La vérification des signatures peut également être déportée sur le GPU si des bibliothèques de performances sont détectées via perf_libs::api(). Le GPU ne peut être utilisé que s’il y a au moins 64 paquets et si 90 % d’entre eux sont censés être valides. En effet, la configuration et le transfert vers le GPU entraînent une surcharge de ~15 à 20 ms, tandis que le CPU peut vérifier 64 signatures en ~10 à 20 ms. En pratique, cette fonctionnalité s’est révélée inadaptée à la production en raison de cette latence, qui la rend bien plus lente que la vérification sur CPU pour des charges de travail réalistes. Il est prévu de supprimer ce chemin de code inutilisé.

Le processus de vérification est relativement simple :

  • Recevoir les lots de paquets provenant des canaux non bornés de la Fetch Stage.
  • Supprimer aléatoirement des transactions par délestage si le volume de paquets dépasse 165 000.
  • Supprimer les transactions en double.
  • Éliminer les paquets excédentaires afin qu’aucune adresse IP ne puisse monopoliser la bande passante de vérification.
  • Réduire et réorganiser au préalable les lots afin d’améliorer la localité du cache et de limiter le gaspillage de mémoire.
  • Vérifier les signatures.

Tous les paquets valides passent à la Banking Stage. 

La Banking Stage

La Banking Stage est l’étape où les transactions sont exécutées. Elles y sont mises en mémoire tampon, planifiées et exécutées par des threads de travail parallèles.

Un modèle de planificateur central est utilisé, avec un traitement distinct des transactions de vote et hors vote :

  • Un seul thread de travail traite les transactions de vote et les votes gossip.
  • Quatre threads de travail traitent toutes les transactions hors vote.
  • Un seul thread coordonne la répartition du travail entre les threads de travail.

Les paquets entrants sont désérialisés et mis en mémoire tampon jusqu’à un maximum de 100 000 transactions. Le planificateur reçoit continuellement de nouvelles transactions tout en gérant ce tampon. Lors des opérations de nettoyage de la file, il supprime les transactions expirées ou non valides en examinant jusqu’à 10 000 transactions à la fois.

Le planificateur détermine l’ordre d’exécution des transactions en détectant les conflits (c’est-à-dire les transactions qui cherchent à obtenir des verrous de lecture et d’écriture sur les mêmes comptes). Deux implémentations de planificateur sont disponibles par défaut :

  • PrioGraphScheduler : un planificateur qui construit un graphe de priorité pour détecter les conflits de comptes.
  • GreedyScheduler : le planificateur par défaut, qui utilise une approche FIFO plus simple pour ordonner les transactions.

Le planificateur sélectionne ensuite les transactions sans conflit et les distribue aux threads de travail. Chaque thread reçoit un lot et commence son traitement.

Orchestration par la Bank

La Bank représente l’état de tous les comptes à un slot donné. Il s’agit de la structure de données centrale qui gère les données des comptes, applique les règles de l’environnement d’exécution et orchestre l’exécution des transactions entre les workers de la Banking Stage et la VM sBPF. 

Cycle de vie

Chaque Bank passe par trois états :

  • Active : une Bank nouvellement créée et ouverte aux transactions. Les workers de la Banking Stage lui appliquent des transactions jusqu’à ce qu’elle atteigne son nombre cible de ticks ou que toutes les entrées du slot aient été traitées.
  • Frozen : lorsque le nombre de ticks est atteint ou que toutes les entrées ont été traitées, la Bank est figée. Aucune autre transaction ne peut lui être appliquée. À ce stade, les frais de transaction sont attribués au leader du bloc, les comptes sysvar sont mis à jour et le hachage final de la Bank est calculé.
  • Rooted : lorsque la Bank figée a reçu suffisamment de votes des validateurs, elle devient enracinée. Son état est alors finalisé et intégré au registre de la chaîne.

À l’exception de la Bank de genèse, chaque Bank pointe vers une Bank parente, formant une arborescence qui représente les différentes bifurcations du registre. 

Flux d’exécution

Les workers de la Banking Stage opèrent sur la Bank de travail actuelle (c’est-à-dire la Bank active et non figée construite pour le slot actuel). Lorsqu’un worker reçoit un lot de transactions du planificateur, la Bank orchestre l’exécution :

  • Verrouillage des comptes : les workers appellent la fonction prepare_sanitized_batch_with_results() pour verrouiller tous les comptes référencés dans les transactions afin d’empêcher les modifications simultanées.
  • Chargement des comptes : la Bank récupère dans AccountsDB les données de tous les comptes verrouillés.
  • Déduction des frais : la Bank déduit les frais de transaction du compte du payeur avant l’exécution.
  • Validation : la Bank vérifie que le blockhash est récent, contrôle l’état du compte de nonce et vérifie la propriété des comptes.
  • Transfert à la VM : la Bank appelle load_and_execute_transactions(), qui transmet les comptes chargés à la VM sBPF.
  • Exécution : la VM sBPF exécute le bytecode du programme de chaque instruction.
  • Résultats : la VM sBPF renvoie l’intégralité de la sortie d’exécution (c’est-à-dire LoadAndExecuteTransactionOutput).
  • Validation définitive : la Bank réécrit les états de compte mis à jour dans AccountsDB via bank.commit_transactions().

L’exécution entre maintenant dans la SVM proprement dite (c’est-à-dire la VM sBPF). Après l’appel de load_and_execute_transactions() par la Bank, les transactions traversent un pipeline en plusieurs étapes où chaque instruction est traitée dans une nouvelle instance isolée de la VM sBPF, provisionnée pour exécuter le bytecode du programme.

Les instructions d’une transaction sont exécutées de manière séquentielle. Le pipeline de chaque instruction est le suivant :

  • Localiser le programme : rechercher le compte du programme à partir de l’ID de programme de l’instruction.
  • Charger depuis le cache : vérifier si le programme concerné a déjà été compilé en JIT dans le cache des programmes. 
  • Provisionner la VM : créer une instance isolée de la VM sBPF avec des régions mémoire et un budget de calcul précis.
  • Exécuter le bytecode : exécuter le point d’entrée du programme avec les entrées de l’instruction.
  • Vérifier les invariants : s’assurer que l’exécution n’enfreint aucune règle de l’environnement d’exécution.
  • Collecter les résultats : regrouper les résultats de l’exécution.

Chargement des programmes

Avant de pouvoir être exécuté, un programme doit être chargé depuis son compte on-chain, vérifié et compilé en code machine natif. Cela passe par le cache des programmes et le pipeline de compilation JIT.

Le cache des programmes est une optimisation des performances qui évite de recharger et de recompiler les programmes à chaque invocation. Il est géré au niveau du lot de transactions et stocke des objets ProgramCacheEntry, qui contiennent :

  • Exécutable compilé en JIT : code machine natif adapté à l’architecture CPU du validateur.
  • Métadonnées de déploiement : slot auquel le programme a été déployé et est devenu effectif.
  • Environnement d’exécution : références au registre des syscalls et à la configuration d’exécution.

Lorsqu’une transaction référence un ID de programme, la séquence de recherche suivante est appliquée :

  • Vérifier le cache du lot de transactions : rechercher une version compilée en JIT et mise en cache.
  • Vérifier le cache global des programmes : si elle ne figure pas dans le cache du lot, consulter le cache global du validateur.
  • Charger depuis le compte : si aucun cache ne contient le programme, charger son compte depuis AccountsDB.
  • Analyser l’ELF : extraire le bytecode des données du compte du programme.
  • Vérifier le bytecode : exécuter le vérificateur statique pour garantir sa sûreté.
  • Compiler en JIT : traduire le bytecode sBPF en code machine natif.
  • Mettre l’entrée en cache : stocker le programme compilé pour les futures invocations.

Ainsi, la première invocation d’un programme nouvellement déployé supporte le coût total du chargement, de la vérification et de la compilation, tandis que les invocations suivantes exécutent directement le code natif mis en cache. 

Si le programme est absent du cache, il doit être chargé depuis son compte on-chain. Pour les programmes appartenant au BPF Loader Upgradeable, le compte du programme contient une référence au compte ProgramData, qui est chargé depuis AccountsDB. Avec le modèle à compte unique de Loader V4, le bytecode est chargé directement depuis le compte du programme et peut devoir être décompressé s’il est compressé avec zstd.

Les octets ELF extraits sont analysés afin de localiser le bytecode exécutable et les sections mentionnées précédemment (c’est-à-dire .text, .rodata, .data / .bss, .symtab / .strtab). Une fois l’ELF analysé avec succès, le RequisiteVerifier (c’est-à-dire l’analyseur statique de sBPF) vérifie tous les chemins d’exécution possibles sans réellement exécuter le programme, comme décrit dans les sections précédentes.

Compilation JIT

Une fois la vérification réussie, le bytecode est compilé à la volée (JIT) en code machine natif. Le compilateur JIT traduit chaque instruction sBPF en instructions CPU natives équivalentes pour l’architecture du validateur. 

La compilation JIT permet à la VM sBPF d’être suffisamment performante pour gérer le débit élevé de Solana. Sans JIT, la VM devrait interpréter le bytecode sBPF instruction par instruction, ce qui entraînerait une surcharge importante. 

Chaque instruction du bytecode doit être récupérée, décodée et transmise au code de traitement, ce qui ajoute une surcharge d’interprétation. En outre, le code interprété ne peut pas exploiter les optimisations au niveau du CPU, telles que le pipeline, la prédiction de branchement ou l’exécution dans le désordre. Chaque instruction sBPF devient donc un appel de fonction dans l’interpréteur.

La compilation JIT élimine entièrement ces coûts en produisant du code machine natif qui s’exécute directement sur le CPU. Elle offre ainsi des performances proches du natif, des contrôles de limites optimisés, une comptabilisation du calcul intégrée, des optimisations matérielles et une correspondance claire pour l’allocation des registres.

Le compilateur JIT effectue une traduction en une seule passe du bytecode sBPF vers le code machine natif. Pour chaque instruction sBPF :

  • L’instruction est décodée pour extraire l’opcode, les registres, le décalage et la valeur immédiate.
  • Le cadre de pile est configuré et les registres préservés par l’appelé sont sauvegardés.
  • L’opération sBPF est mise en correspondance avec l’instruction CPU équivalente afin que chaque opération soit compilée en instructions natives. Par exemple, dans la correspondance x86-64, rax correspond à R0 et rbp à R10.
  • Des contrôles de limites et une traduction logicielle des adresses sont émis pour les accès mémoire. La traduction des adresses invitées en adresses hôtes est l’une des opérations les plus lentes de la VM en raison de sa surcharge.
  • L’isolation de la pile est maintenue (c’est-à-dire que la pile invitée réside dans un tampon alloué sur le tas plutôt que dans la pile de l’hôte).
  • La déduction des CU et les contrôles du budget sont insérés pour comptabiliser le calcul.
  • Les registres sont restaurés et la pile est nettoyée.

Traduction des instructions

Le compilateur JIT traduit de manière spécifique les différents types d’instructions (c’est-à-dire les opérations arithmétiques, les accès mémoire, les opérations de stockage, les branchements conditionnels et la répartition des syscalls). 

Les opérations arithmétiques sont directement mises en correspondance avec des instructions CPU natives uniques, sans aucune surcharge, car les registres sBPF correspondent bien aux registres matériels du validateur. 

Les opérations d’accès mémoire nécessitent un contrôle des limites afin d’empêcher les lectures et écritures hors limites. Le compilateur JIT génère du code de validation qui compare les limites inférieure et supérieure de chaque accès mémoire aux limites de la région valide. 

Cela implique généralement 3 à 6 instructions natives qui calculent l’adresse effective, vérifient qu’elle respecte les limites attendues, puis effectuent le chargement proprement dit. La prédiction de branchement permet de gérer cela assez efficacement, car les violations de limites sont rares. 

Les opérations de stockage comprennent un contrôle des limites et une validation des autorisations d’écriture. Avant d’écrire en mémoire, le code compilé vérifie que l’adresse cible est dans les limites et que l’écriture est autorisée dans la région mémoire.

Les branchements conditionnels sont compilés en instructions natives de saut conditionnel. Le compilateur JIT résout toutes les cibles de saut pendant la compilation, de sorte que les décalages relatifs des instructions sBPF sont convertis en adresses absolues dans le code natif. 

La répartition d’un syscall exige de sauvegarder l’état de la VM — les 11 registres — et d’appeler le gestionnaire natif du syscall. L’état de la VM est ensuite restauré avec la valeur de retour. Cette surcharge de gestion de l’état explique pourquoi les syscalls ont des coûts fixes en CU, supérieurs à ceux des instructions ordinaires.

Comptabilisation des unités de calcul

Le compilateur JIT intègre directement le suivi des unités de calcul dans le code généré. Chaque instruction sBPF comprend un contrôle du budget pour s’assurer qu’il n’est pas épuisé à mesure que les instructions réduisent le nombre de CU restantes. 

Cette intégration évite la surcharge des appels de fonction. Elle peut être réalisée efficacement grâce à la prédiction de branchement, à l’exécution dans le désordre (c’est-à-dire que le contrôle des CU et l’opération peuvent s’exécuter en parallèle) et au parallélisme au niveau des instructions.

Pour les syscalls à coût variable (par exemple, le syscall sol_sha256, dont le coût évolue avec la longueur des données), le coût est calculé dans l’implémentation native du syscall avant le retour à la VM.

Mise en cache du code compilé

Une fois la compilation JIT terminée, l’exécutable natif est stocké dans un ProgramCacheEntry, qui contient :

  • Le code compilé en JIT
  • Les métadonnées du slot de déploiement et du slot effectif
  • Les références au registre des syscalls
  • La configuration de l’environnement d’exécution

L’entrée de cache est placée dans le cache du lot de transactions et dans le cache global des programmes. Le premier est accessible à toutes les instructions du lot actuel, tandis que le second est disponible pour toutes les transactions futures. 

L’entrée de cache peut être invalidée lorsque le programme est mis à niveau (c’est-à-dire lorsqu’un nouveau bytecode est déployé), lorsque le compte du programme est fermé, lorsque des feature gates modifient la disponibilité des syscalls ou lorsqu’un validateur décide de vider son cache.

Délai du slot effectif

Notez qu’un programme déployé dans le slot n ne peut pas être invoqué avant le slot n + 1. Ce délai garantit que tous les validateurs observent le déploiement, que les caches de programmes se synchronisent sur le réseau et que l’atomicité par slot est respectée. 

Provisionnement de la VM sBPF

Une fois le programme chargé et compilé en JIT, ou récupéré dans le cache, une nouvelle VM sBPF est provisionnée pour chaque exécution d’instruction. Ce provisionnement a lieu dans le BPF Loader et implique la configuration de cinq régions mémoire distinctes, l’initialisation du budget de calcul et l’enregistrement des syscalls.

Régions mémoire

Comme indiqué dans notre section consacrée à l’ISA de la SVM, la VM crée cinq régions mémoire distinctes. Ensemble, ces régions forment le bac à sable isolé dans lequel les programmes Solana s’exécutent. La région mémoire du programme commence généralement à l’adresse 0x100000000. Elle contient le code natif compilé en JIT qui doit être exécuté, c’est-à-dire le code machine proprement dit, qui dépend de l’architecture CPU du validateur. Si le JIT est désactivé à des fins de débogage, cette région contient à la place le bytecode sBPF interprété. Les autorisations de cette section sont limitées à la lecture et à l’exécution.

Les données en lecture seule sont également incluses à l’adresse 0x100000000. Elles contiennent les constantes et les chaînes statiques extraites de la section ELF .rodata lors du chargement du programme. Cette région mémoire permet d’accéder efficacement aux constantes définies à la compilation sans nécessiter d’allocation sur le tas. 

La pile commence à l’adresse 0x200000000 et contient les variables locales, les cadres d’appel de fonction et les adresses de retour. C’est là que les calculs temporaires ont lieu pendant l’exécution. Elle croît vers le bas à partir du haut de la région, le registre R10 (c’est-à-dire le pointeur de cadre) indiquant la limite du cadre actuel. Cette région autorise la lecture et l’écriture, et sa taille est fixée à 4 Ko par cadre d’appel. Le dépassement de cette limite génère une erreur StackAccessViolation, qui indique un débordement de pile. La petite taille de la pile encourage les développeurs à utiliser le tas ou, mieux encore, à stocker les données dans des comptes plutôt que de s’appuyer sur un stockage dans la pile.

Le tas commence à l’adresse 0x300000000 et contient la mémoire allouée dynamiquement aux structures de données de l’environnement d’exécution qui ne tiennent pas dans la pile ou dans un compte. Sa taille va de 32 Ko par défaut à un maximum de 256 Ko. Auparavant, les programmes pouvaient agrandir le tas au moyen du syscall sol_alloc_free. Toutefois, ce syscall est désormais obsolète et désactivé pour les nouveaux déploiements de programmes. Les programmes doivent indiquer la taille de tas requise au moment du déploiement au lieu de l’agrandir dynamiquement. 

Remarque : l’agrandissement du tas consomme des unités de calcul selon la formule (heap_size / 32KB) * 8,000 CUs, avec un coût par défaut du tas de 8 CU.

La région mémoire des données d’entrée commence à l’adresse 0x400000000. Elle contient les paramètres sérialisés du point d’entrée que le programme reçoit lors de son invocation. Il s’agit d’une région mémoire en lecture seule dont la taille réelle varie selon la transaction. Les trois composants sérialisés sont la clé publique de 32 octets du programme invoqué, un tableau de comptes et les données de l’instruction.

Contrôle des accès mémoire

Chaque instruction de chargement ou de stockage en mémoire fait l’objet d’un contrôle des limites par la VM. Avant chaque accès mémoire, la VM vérifie que l’adresse se trouve dans la plage valide de la région et que le type d’accès (c’est-à-dire lecture ou écriture) y est autorisé. 

L’exécution s’interrompt immédiatement avec une erreur AccessViolation si l’un de ces contrôles échoue. L’ensemble de la transaction est annulé et aucune modification d’état n’est validée. 

Ce contrôle entraîne un coût d’exécution presque nul, car le compilateur JIT compile ces vérifications en code natif efficace que le CPU peut exécuter directement. La prédiction de branchement des processeurs modernes les gère efficacement, puisque les violations sont rares. 

Initialisation du budget de calcul

Chaque instance de VM est initialisée avec un budget d’unités de calcul qui limite la quantité totale de travail qu’un programme donné peut effectuer. Ce modèle d’exécution borné garantit que les programmes ne peuvent pas s’exécuter indéfiniment et que tous les validateurs exécutent les transactions dans un délai prévisible.

Les paramètres actuels du budget sont les suivants :

  • Limite d’unités de calcul par défaut par instruction : 200 000 CU.
  • Limite maximale d’unités de calcul par transaction : 1 400 000 CU.
  • Limite d’une instruction intégrée : 3 000 CU.

Le nombre de CU par défaut par transaction est min(1_400_00, (200_000*non_reserve_instructions + 3_000*reserve_instructions)). Il s’agit essentiellement du minimum entre la limite maximale d’unités de calcul et les coûts par défaut de chaque type d’instruction, selon les instructions fournies.

Le budget de calcul suit le nombre d’unités restantes tout au long de l’exécution. À chaque instruction sBPF exécutée, son coût en CU est déduit du budget restant. Si le budget atteint zéro avant la fin du programme, l’exécution s’arrête immédiatement avec InstructionError::ComputationalBudgetExceeded. La transaction échoue et aucune modification d’état n’est validée, mais les frais de transaction sont tout de même facturés au payeur afin de rémunérer le validateur pour le traitement de sa transaction. 

Registre des appels système

Lors du provisionnement de la VM, une correspondance est également établie entre l’identifiant unique de hachage Murmur 32 bits de chaque appel système et son implémentation Rust native, afin d’enregistrer tous les appels système disponibles. 

Voici ce qui se produit lorsqu’un programme exécute une instruction CALL_IMM avec le hachage d’un appel système :

  • La VM suspend le flux d’instructions sBPF.
  • Le répartiteur d’appels système utilise le hachage pour trouver l’implémentation dans le registre.
  • L’appel système vérifie que l’appelant dispose des autorisations nécessaires (par exemple, pour effectuer un appel CPU ou accéder à un compte dont il est propriétaire).
  • L’implémentation Rust de l’appel système s’exécute avec un accès complet au runtime, en dehors de la sandbox.
  • Le coût fixe de l’appel système est déduit du budget de calcul restant.
  • Le résultat est placé dans le registre R0, puis l’exécution sBPF reprend. 

Exécution du programme

Une fois la VM sBPF provisionnée, l’exécution commence à la fonction de point d’entrée du programme. Pour les programmes compilés en JIT, la VM accède directement au code machine natif et laisse le CPU du validateur l’exécuter nativement. Comme indiqué dans la section précédente, le code compilé en JIT comprend toute l’instrumentation nécessaire — vérification des limites mémoire, mesure du calcul et validation du flux de contrôle — entièrement intégrée pour des performances maximales.

Le registre R1 contient un pointeur vers la région des données d’entrée où se trouvent les trois paramètres sérialisés (c’est-à-dire la clé publique du programme appelé, un tableau de comptes et les données de l’instruction). 

Remarque : les données des comptes sont accessibles via des pointeurs plutôt que copiées. Les pointeurs permettent aux programmes de lire et de modifier les données des comptes sur place, ce qui est essentiel pour les performances.

Chaque appel de fonction alloue une nouvelle frame de pile de 4 Ko, et le registre R10 est mis à jour pour pointer vers cette nouvelle frame. Le calcul est mesuré selon le budget de calcul mentionné précédemment.

Appels interprogrammes (CPI)

Pendant l’exécution, les programmes peuvent appeler d’autres programmes au moyen d’appels interprogrammes (CPI), qui constituent le socle de la composabilité de la SVM. 

Un CPI est lancé via l’appel système sol_invoke_signed, qui coûte 1 000 CU, auxquels s’ajoutent des coûts dépendant des données de compte sérialisées transmises. Les données de compte et la sérialisation des données d’instruction coûtent toutes deux 250 octets par CU.

Lorsqu’un programme effectue un CPI, un nouveau contexte d’exécution doté de sa propre frame de pile d’instructions est créé. À l’heure où nous écrivons ces lignes, la profondeur maximale de la pile d’instructions est de 5, ou de 9 avec SIMD-0268 activé. Un programme peut donc en appeler un autre, qui peut à son tour en appeler un autre, jusqu’à atteindre cette limite de profondeur. Chaque appel imbriqué conserve son propre ensemble de comptes modifiables et de privilèges de signataire. Un CPI peut compter au maximum 16 signataires et transmettre 128 structures AccountInfo. 

L’appelant sérialise l’ID du programme cible, les comptes et les données de l’instruction, puis lance l’appel système. L’exécution du programme actuel est suspendue et une nouvelle instance de VM sBPF est provisionnée pour le programme appelé, selon le même processus de provisionnement décrit précédemment. L’exécution du programme appelé commence alors. Celui-ci utilise son propre budget de calcul, prélevé sur le budget restant de l’appelant. Les appels CPI partagent donc le budget de calcul total de la transaction.

Les programmes peuvent signer au nom des comptes dont ils sont propriétaires grâce aux adresses dérivées de programme (PDA). Lors d’un appel avec sol_invoke_signed, l’appelant fournit des seeds qui prouvent qu’il est propriétaire de la PDA. La dérivation de la PDA est vérifiée avant d’accorder au programme appelé l’autorité de signature. 

Lorsque le programme appelé termine son exécution, le contrôle revient à l’appelant. Les modifications de comptes effectuées par le programme appelé sont visibles par l’appelant, ce qui permet à l’état de circuler tout au long de la chaîne d’appels. Si l’un des programmes de la chaîne CPI échoue, toute la transaction est interrompue et toutes les modifications d’état sont annulées.

Remarque : sol_invoke est une fonction auxiliaire qui appelle sol_invoke_signed sans seed.

Vérification après l’exécution

Une fois l’exécution d’un programme terminée, que ce soit directement ou dans le cadre d’une chaîne CPI, plusieurs vérifications sont effectuées afin de garantir la cohérence de l’état et le respect des invariants de sécurité. 

Par exemple, le runtime vérifie que tous les comptes marqués comme modifiables appartenaient bien au programme ou avaient été correctement signés. Les programmes ne peuvent pas modifier des comptes qui ne leur appartiennent pas, sauf si ces comptes ont été explicitement marqués comme modifiables et que leur propriétaire a accordé son autorisation. Cela empêche toute modification non autorisée de l’état.

Le runtime vérifie également que la somme totale des lamports de tous les comptes de la transaction reste identique, sauf si des lamports ont été explicitement transférés au moyen d’instructions du System Program. Ce contrôle de conservation empêche les programmes de créer ou de détruire des lamports, afin que l’offre totale de SOL reste constante.

Le runtime vérifie qu’aucun compte doté d’un indicateur exécutable (c’est-à-dire un programme) n’a été modifié. Les données d’un programme ne peuvent pas être modifiées pendant une exécution normale. Une mise à niveau n’est possible qu’au moyen du mécanisme d’autorité de mise à niveau du BPF Loader.

Résultats de l’exécution

Une fois la vérification après exécution terminée, le résultat est renvoyé au processeur de transactions du Bank. Il contient le statut de réussite ou d’erreur (respectivement un résultat nul ou non nul), le nombre d’unités de calcul consommées et toutes les modifications apportées à l’état des comptes. 

Pour les exécutions réussies, le Bank valide atomiquement toutes les modifications de comptes. Les données de compte mises à jour, les soldes en lamports et les métadonnées sont écrits dans AccountsDB et deviennent visibles pour les transactions suivantes. Les unités de calcul consommées sont journalisées pour calculer les frais de transaction et établir les métriques du réseau.

Aucune modification d’état n’est validée pour les transactions ayant échoué : il n’existe pas d’annulation partielle sur Solana, car la transaction est entièrement annulée. Les frais de transaction sont néanmoins débités du compte du payeur afin de rémunérer le validateur pour le travail de calcul effectué. Le code d’erreur et les unités de calcul consommées sont enregistrés dans les métadonnées de la transaction à des fins de débogage et d’analyse.

Le résultat de l’exécution remonte jusqu’au planificateur de la Banking Stage, qui met à jour ses métriques internes avant de passer à la transaction suivante. Les transactions réussies comme celles qui ont échoué sont enregistrées dans le flux de preuve d’historique et contribuent à la construction du bloc en cours. Les transactions ayant échoué sont incluses pour contribuer à empêcher les attaques par rejeu et préserver un historique complet des transactions.

Lorsque le slot se termine et atteint son nombre maximal de ticks, le Bank passe à l’état gelé. Le gel est une opération irréversible qui empêche la validation de nouvelles transactions et calcule le hachage du Bank. Notez que « gelé » ne signifie pas « finalisé » : le slot peut encore se trouver sur un fork qui sera abandonné. 

Le Bank devient enraciné lorsque le validateur appelle BankForks::set_root() pour le désigner comme faisant partie de la chaîne canonique. L’enracinement déclenche une opération de squash qui aplatit l’état des comptes du Bank enraciné dans AccountsDB, fusionne tous les états parents et le rend permanent du point de vue du validateur. Les forks non enracinés sont élagués et supprimés. Même les Banks enracinés ne sont pas encore finalisés du point de vue du cluster, en raison des différents niveaux d’engagement de Solana.

Perspectives

La machine virtuelle Solana repose sur une approche fondamentalement différente de l’exécution blockchain : une blockchain évolutive accessible au plus grand nombre, grâce au traitement parallèle, aux marchés de frais locaux et à un runtime performant et déterministe dérivé d’eBPF. 

Comprendre la SVM nécessite d’examiner l’ensemble du pipeline d’exécution, de la compilation du code source Rust vers LLVM et sBPF jusqu’au provisionnement d’instances de VM isolées. 

Il n’existe pas de « spécification » unique définissant la SVM. Celle-ci émerge plutôt de l’interaction entre le Bank, le planificateur, les BPF Loaders, la VM sBPF et l’ISA de la SVM.

L’avenir s’annonce prometteur à mesure que la SVM continue d’évoluer. 

La toolchain Solana fait l’objet d’une refonte complète afin d’éliminer l’infrastructure LLVM personnalisée qui complique l’intégration des développeurs depuis des années. 

L’approche actuelle oblige les développeurs à installer des toolchains personnalisées au moyen de scripts propres à chaque plateforme. La solution consiste à adopter la même toolchain qu’Aya, la bibliothèque eBPF pour Rust. Les développeurs pourront exécuter deux commandes simples pour compiler directement en bytecode eBPF :

Code
rustup toolchain install nightly
cargo build --target=bpfel-unknown-none

Aucun script. Aucun fork LLVM personnalisé. Uniquement les outils Rust standards qui compilent directement en bytecode eBPF avec la cible upstream bpfel-unknown-none, en tirant parti des innombrables années de développement du noyau Linux et d’amélioration de l’infrastructure LLVM.

L’ISA de la SVM doit également être mise à jour avec SIMD-0377, qui propose d’aligner l’implémentation eBPF de Solana (c’est-à-dire sBPF) sur les normes eBPF modernes. Cela comprend l’introduction de variantes de l’instruction JMP32, d’opérations de division signée et de modulo, de sauts indirects et de frames de pile dynamiques. Ces changements contribueront à réduire les coûts de calcul, à améliorer la compatibilité avec l’infrastructure LLVM upstream et à permettre une génération de code plus efficace. 

La SVM est fondamentalement un système mesuré au moyen de budgets de calcul. SIMD-0370 devrait changer ce fonctionnement en supprimant le plafond de calcul au niveau des blocs et, peut-être, celui des transactions. La suppression de ces plafonds permettrait aux producteurs de blocs de maximiser le débit selon les capacités de leur matériel plutôt que selon des limites artificielles. Associé au mécanisme de délai d’expiration d’Alpenglow, ce changement laisserait les forces du marché, plutôt que des contraintes imposées au niveau du protocole, déterminer la taille optimale des blocs. Bien entendu, cette perspective est très lointaine, car Anza souhaite d’abord porter la limite de CU à plus de 100 millions avant de supprimer ces plafonds.

Tous ces changements reposent sur une culture de bâtisseurs qui cherchent à repousser les limites de ce que les blockchains peuvent accomplir sans sacrifier la sécurité, le déterminisme ni la décentralisation. 

La SVM n’est pas un simple interpréteur de bytecode : c’est un pipeline d’exécution complet qui révolutionne les capacités des blockchains. Elle est l’aboutissement de décisions architecturales privilégiant le débit et une faible latence. À mesure que Solana gagnera en maturité, la SVM continuera d’évoluer pour prendre en charge des applications hautement performantes et efficaces en capital.

Le rêve des marchés de capitaux sur Internet nécessite une infrastructure capable de répondre aux exigences de débit, de latence et de coût des systèmes financiers mondiaux. La machine virtuelle Solana constitue une étape essentielle vers la concrétisation de cette vision.

Ressources supplémentaires

Abonnez-vous à Helius

Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication

Image agrandie