La dernière mise à jour de Starknet (v0.13.2) appelée Bolt apporte deux changements majeurs : l'exécution parallèle et le block-packing . Bien qu'indépendantes l'une de l'autre, ces deux fonctionnalités soutiennent l'objectif d'un espace de blocs rapide et bon marché, sécurisé cryptographiquement par Ethereum.
L'exécution parallèle permet aux transactions non litigieuses (c'est-à-dire aux transactions qui ne touchent pas au même état) de s'exécuter simultanément. En implémentant l'exécution parallèle, les L2 comme Starknet peuvent réduire le temps d'exécution sans augmenter l'utilisation des ressources. Cela signifie des frais de transaction inférieurs pour les utilisateurs et des délais de confirmation des transactions considérablement améliorés.
Le block packaging optimise l'utilisation de l'espace blob par Starknet sur Ethereum L1 : avec le block packaging, les séquenceurs peuvent générer une preuve pour vérifier simultanément plusieurs blocs Starknet L2. Cela dissocie l'utilisation de l'espace blob de la fréquence de production des blocs L2 et amortit les coûts de vérification des preuves. Ces deux éléments réduisent les coûts d'exploitation du séquenceur Starknet, ce qui signifie que les utilisateurs paient moins par transaction.
Comme nous l'avons dit, Bolt rend Starknet moins cher et plus rapide ! Ce rapport fournira une analyse détaillée de la mise à niveau de Bolt, en se concentrant sur l'exécution parallèle et le regroupement de blocs, et explorera les implications pour les performances de Starknet.
Les rollups sont des solutions de mise à l'échelle de couche 2 (L2) qui visent à faire évoluer la blockchain de couche 1 (L1) sous-jacente en déplaçant le calcul hors chaîne. En déplaçant l'exécution hors chaîne, les rollups peuvent optimiser l'évolutivité (transactions bon marché et rapides), tandis que la couche 1 assure la sécurité des transactions de couche 2.
On dit souvent que les rollups « héritent de la sécurité du L1 ». Cela signifie essentiellement qu’ils héritent des garanties de consensus et de disponibilité des données fournies par le L1. En outre, le L1 fournit également une garantie de sécurité sous la forme d’un pont sécurisé entre lui et le rollup.
Lorsque les séquenceurs publient des blocs L2 vers le L1, ce dernier fournit des garanties de disponibilité ainsi que de classement de ces informations. À partir de là, les nœuds L2 peuvent calculer en toute confiance la chaîne L2 canonique avec ces informations ainsi que les règles de cumul autour de la dérivation de la chaîne et de la transition d'état décrites par l'implémentation du nœud.
Afin de faciliter la mise en place d'un pont sécurisé entre les niveaux L1 et L2, le niveau L1 requiert la preuve que la chaîne L2 qu'il suit actuellement est correcte et n'inclut pas de changements d'état illégaux (par exemple, une double dépense). Ce besoin de cumuls pour prouver la validité des changements d'état garantit que le niveau L1 n'autorise pas les retraits du cumul sur la base d'un état illégal.
Les cumuls diffèrent selon la manière dont ils prouvent la validité des modifications d'état apportées au L1 :
Les cumuls fournissent également à la couche de base suffisamment de données pour que les parties intéressées puissent reconstruire l'état L2. Alors que les cumuls optimistes doivent publier des données de transaction complètes pour permettre aux challengers de calculer les preuves de fraude, les cumuls de validité n'ont pas de telles exigences (les preuves de validité garantissent une exécution correcte). Mais la publication de données de transaction complètes sur L1 est toujours utile du point de vue de la minimisation de la confiance (reconstruction sans confiance de l'état et retraits sans autorisation).
Starknet est un cumul de validité qui utilise des arguments de connaissances scalables et transparents (STARK) pour prouver la validité des changements d'état. La dernière mise à niveau de Starknet, nommée Bolt, ajoute l'exécution parallèle et le regroupement de blocs. Dans les sections suivantes, nous expliquerons comment fonctionnent les deux fonctionnalités et quelles améliorations elles apportent aux utilisateurs de Starknet.
À un niveau élevé, la mise à niveau de Bolt a modifié les mécanismes d'exécution, de preuve et de disponibilité des données de Starknet.
Avant la mise à niveau de Bolt, les transactions Starknet étaient exécutées séquentiellement (l'une après l'autre) par le séquenceur. L'exécution séquentielle est simple mais également très inefficace. Elle est inefficace car elle ne tire pas parti des multiples unités de traitement indépendantes qu'offrent les ordinateurs modernes et de la parallélisabilité d'un ensemble de transactions.
La parallélisabilité est une mesure du degré d'indépendance des transactions dans un ensemble donné. Par exemple, considérons l'ensemble de trois transactions ci-dessous :
Transaction 1 : Alice veut envoyer 1 STRK à Bob
Transaction 2 : Caitlyn veut envoyer 100 ETH à Danny
Transaction 3 : Caitlyn veut envoyer 100 ETH à Ella
La transaction 1 est complètement indépendante des transactions 2 et 3, car elle accède à une partie différente de l'état (le solde d'Alice) et peut être exécutée simultanément. Cependant, les transactions 2 et 3 sont en conflit car elles veulent accéder au même état, le solde ETH de Caitlyn. Ces transactions ne peuvent pas être exécutées simultanément, sinon nous obtiendrons des résultats contradictoires.
Pour illustrer :
C'est pour éviter ce type de conflits (et la nature complexe des mécanismes d'atténuation) qu'Ethereum a choisi l'exécution séquentielle. Cependant, si l'exécution séquentielle réduit la complexité et améliore la sécurité, elle entraîne une utilisation inefficace du matériel. Pire encore, la tendance de la conception du matériel suggère que l'exécution séquentielle deviendra de plus en plus inefficace dans les années à venir.
La figure 4 montre l'évolution de la conception matérielle au cours des 50 dernières années. Le point important à retenir ? Les performances monothread (cercles violets) stagnent depuis le milieu des années 2000, alors que le nombre de cœurs logiques a augmenté à peu près au même moment. Nous pouvons tirer deux conclusions sur la base de ces données :
Les concepteurs de matériel informatique font évoluer les puces informatiques en ajoutant des unités de traitement indépendantes plutôt qu’en améliorant les performances d’une seule unité.
Tout système qui continue à s’appuyer sur les performances accrues d’une seule unité de traitement verra ses gains de performances stagner, même sur du matériel plus récent.
Ces dernières années, des algorithmes sophistiqués permettant de gérer les conflits de transactions et de garantir l'exactitude de l'exécution parallèle sont apparus. Block-STM (basé sur un article de Fikunmi et al*) est l'un de ces algorithmes et constitue la partie centrale du nouveau moteur d'exécution parallèle de Starknet. Nous analysons l'algorithme Block-STM dans les sections suivantes.
Le SHARP (abréviation de Shared Prover) de Starknet a toujours tiré parti des preuves récursives pour réduire autant que possible les coûts de vérification. Une preuve récursive est essentiellement une « preuve de preuve » où une preuve vérifie qu'une ou plusieurs preuves sont correctes. Vous trouverez ci-dessous un schéma de la manière dont SHARP génère une preuve récursive :
Le système SHARP prend en entrée un ensemble de programmes à exécuter (un « job ») et génère une preuve d'exécution pour le job. Ces « programmes » sont des blocs L2 et la preuve atteste de l'exactitude des transactions.
La preuve est envoyée à un autre programme qui vérifie la preuve et convertit le programme de vérification de preuve en tâche. SHARP prend la nouvelle tâche en entrée et génère une autre preuve (cette preuve confirme la validité de la preuve précédente).
Le processus (preuve → tâche → preuve) redémarre et continue jusqu'à ce qu'une cible soit atteinte, moment auquel la preuve finale (qui est maintenant une version hautement compressée de la preuve originale) est publiée sur le L1
Cette conception amortit considérablement les coûts pour deux raisons principales :
Bien que le système de validation soit efficace, des opportunités de réduction des coûts supplémentaires ont été manquées. Par exemple, chaque tâche était un seul bloc Starknet et chacun de ces blocs était conçu pour occuper un blob sur le L1. Cela a entraîné certaines inefficacités, comme décrit ci-dessous :
Le block-packing s'attaque à ces problèmes en utilisant un arbre binaire de preuves récursives. Nous aborderons le block-packing dans une section ultérieure de l'article.
Comme nous l'avons vu précédemment, l'exécution séquentielle est inefficace (et le sera encore plus avec le temps) et l'exécution parallèle naïve produit des résultats non valides. Les moteurs d'exécution parallèle de production veillent toutefois à éviter les résultats incohérents.
Il existe deux approches pour gérer l'exécution parallèle : le contrôle de concurrence pessimiste (PCC) et le contrôle de concurrence optimiste (OCC) . Le PCC et l'OCC sont des unités de traitement de transaction (TPU). Vous trouverez ci-dessous une définition d'une unité de traitement de transaction de Block-STM vs. SVM : Une comparaison des moteurs d'exécution parallèle :
Le TPU est généralement couplé à la machine virtuelle (VM), mais en est distinct. Les machines virtuelles blockchain comme l'EVM, la SVM et la MoveVM sont des machines virtuelles à langage de haut niveau… Le TPU, qui fait généralement l'objet d'intérêt, englobe la VM. Il est chargé de la gestion de l'ensemble du pipeline d'exécution des transactions, y compris la création et la gestion des instances de la VM.
Le contrôle de concurrence pessimiste est conçu sur la base de l'hypothèse selon laquelle de nombreuses transactions au sein de l'ensemble de transactions à exécuter seront en conflit, c'est-à-dire qu'elles toucheront le même état. Le contrôle de concurrence pessimiste empêche ces conflits.
Pour éviter les conflits, PCC exige qu'une transaction déclare à l'avance à quelles parties de l'état elle accédera pendant les opérations de lecture/écriture. L'unité de traitement des transactions peut utiliser ces informations pour planifier les transactions de manière à garantir que les transactions en conflit sont exécutées de manière séquentielle (au lieu de simultanément). Certains TPU utilisent également des verrous pour appliquer ce comportement (un verrou (également appelé mutex) est un mécanisme utilisé pour empêcher l'accès simultané à un emplacement de mémoire).
Cela dit, l’exécution basée sur PCC implique certains compromis. Tout d’abord, l’obligation de fournir des listes d’accès (qui identifient l’emplacement mémoire touché par une transaction) dégrade l’expérience du développeur et réduit la gamme d’applications possibles. Ensuite, la planification des transactions peut entraîner des frais généraux inutiles, en particulier lorsqu’il n’y a pas de conflits.
Le contrôle de concurrence optimiste est conçu en supposant que de nombreuses transactions au sein de l'ensemble donné ne seront pas en conflit, c'est-à-dire qu'elles n'écriront pas dans le même état. Ainsi, les TPU OCC exécutent l'ensemble des transactions avec toutes les ressources disponibles et tentent uniquement de détecter les conflits. Si un conflit est détecté, les transactions en conflit sont exécutées et revérifiées jusqu'à ce que l'ensemble soit validé.
Les unités de traitement de transactions OCC n'entraînent pas de surcharge liée à la planification, elles ont donc tendance à être plus performantes lorsqu'il y a peu de conflits. Les unités de traitement de transactions basées sur OCC offrent également une meilleure expérience de développement et une plus large gamme de cas d'utilisation, car les dépendances d'état n'ont pas besoin d'être connues à l'avance.
Cependant, lorsque l'ensemble des transactions est très controversé, l'OCC est moins performant que le PCC. Nous abordons les conceptions TPU (de manière beaucoup plus détaillée) et comparons les approches OCC et PCC dans notre article sur l'exécution parallèle.
Le nouveau TPU de Starknet utilise l'approche OCC. Plus précisément, il s'agit d'une implémentation de l'algorithme Block-STM. Block-STM exécute les transactions de manière optimiste avec toutes les ressources disponibles en supposant qu'aucune d'entre elles n'entrera en conflit et vérifie après l'exécution qu'aucune transaction conflictuelle n'est exécutée simultanément. Avant de nous plonger dans la nouvelle architecture de Starknet, il est important de passer en revue certaines définitions clés :
txj
est dite dépendante (ou dépendante) d'une transaction txi
si et seulement si les deux transactions écrivent dans le même emplacement mémoire et que txj
vient après txi
dans l'ordre séquentiel. Si txi
venait après txj
, txi
serait dépendant de txj
.Maintenant que nous avons défini ces définitions, nous pouvons passer à la description du fonctionnement de Block-STM.
L'entrée de Block-STM est une file d'attente (une liste ordonnée) de transactions, cette liste est souvent appelée BLOC. Cette liste peut être ordonnée de n'importe quelle manière ; la seule exigence est qu'il y ait un ordre clairement défini. Ainsi, étant donné un ensemble de transactions T contenant des transactions {t0…tn}
, les transactions sont triées de telle sorte que {t0 > t1 > t2 … > tn}
(lire comme si t0
était de priorité supérieure à t1
, t1
était de priorité supérieure à t2
etc.)
Au début du processus d'exécution, deux ensembles sont créés : un ensemble d'exécution E et un ensemble de validation V. E suit les transactions qui doivent encore être exécutées tandis que V suit les transactions qui ont été exécutées mais qui doivent encore être validées. Chaque transaction est également associée à un numéro d'incarnation n pour suivre le nombre de fois qu'elle a été exécutée (et réexécutée). L'état initial des ensembles est que E contient toutes les transactions et V est vide, c'est-à-dire que E = {t0,1 > t1,1 > t2,1 > … > tn,1}
et V = {}
.
Avec ces ensembles ordonnés de transactions, chaque thread utilisé pour l'exécution parcourt une boucle en trois étapes :
Au cours de cette étape, le thread vérifie à la fois V et E. Si les deux sont vides et qu'aucune transaction n'est en cours d'exécution, le lot actuel de transactions a été entièrement exécuté et les résultats peuvent être enregistrés dans le stockage.
Si V ou E contient des transactions, Block-STM sélectionne la transaction avec l'index le plus bas (et non le numéro d'incarnation) parmi les deux ensembles de transactions, c'est-à-dire que si E contient {t1,3 , t3,1 and t5,2}
et V contient {t0,1, t2,4, t4,3}
, la tâche de validation pour la transaction t0
sera sélectionnée comme tâche suivante.
Une fois la tâche suivante identifiée et sélectionnée, elle est exécutée. À la fin de cette étape, l'algorithme revient à Check Done. Ce processus se poursuit jusqu'à ce que les deux ensembles de transactions soient vides.
Regardons ce qui se passe pendant l’exécution et la validation :
Lors de l'exécution d'une transaction, l'algorithme Block-STM remplit deux ensembles (par transaction) : un ensemble de lecture ( Ri,n
) et un ensemble d'écriture ( Wn,i
). L'ensemble de lecture contient tous les emplacements mémoire à partir desquels une transaction a lu pendant son exécution tandis que l'ensemble d'écriture contient tous les emplacements mémoire dans lesquels elle a écrit. Pendant l'exécution, les transactions appliquent leurs écritures à la structure de données multiversion, mais la lecture est un peu nuancée.
Dans Block-STM, lorsqu'une transaction souhaite lire la structure de données, elle recherche la valeur écrite par la transaction de priorité la plus basse qui a la priorité la plus élevée. Par exemple, si tx1
, tx2
et tx7
ont tous écrit dans un emplacement mémoire et tx5
souhaite lire à partir de cet emplacement, il lit la version de la structure de données correspondant à tx2
.
Ceci est fait pour renforcer la sérialisation ; comme tx5
doit être exécuté après tx2
et avant tx7
, il doit utiliser les valeurs écrites par tx2
et non par tx7
. Dans cet exemple, tx7
devra être réexécuté car il aurait dû lire les valeurs écrites par tx5
, et non par tx2
ou toute autre transaction de priorité supérieure. Si une structure de données à version unique était utilisée, la valeur écrite par tx2
ne serait pas disponible et un conflit se produirait certainement.
Pour une tâche de validation, l'ensemble de lecture de la transaction est comparé aux valeurs actuelles des emplacements de mémoire à partir desquels elle a lu pendant l'exécution. Par exemple, si tx2
lit le compte B pendant l'exécution, pendant la validation, l'emplacement de mémoire du compte B est lu (en gardant à l'esprit la définition de lecture que nous avons établie précédemment). Si les deux valeurs sont identiques, cela signifie qu'aucune transaction de priorité supérieure (par exemple tx0
ou tx1
) n'a écrit à cet emplacement pendant l'exécution de tx2
. Cela entraîne que tx2
est marqué comme validé mais qu'il n'est pas sûr de le valider.
La transaction n'est pas considérée comme sûre à valider car une transaction de priorité inférieure pourrait, pour un certain nombre de raisons, être exécutée après la validation de la transaction. Dans notre exemple, si tx1
touche le compte B et ne le touche qu'après, tx2
passe la validation, alors tx2
doit être réexécuté.
Pour ce faire, à chaque fois qu'une transaction termine son exécution, toutes les transactions de priorité inférieure qui ont passé la validation sont revalidées pour garantir qu'elles n'entrent pas en conflit avec la transaction. Par exemple, si tx1
, tx3
et tx4
ont été validées et que tx2
termine son exécution, tx3
et tx4
doivent être revalidés pour garantir qu'ils n'entrent pas en conflit avec tx2
(et ne sont donc pas des dépendances de celui-ci).
Si une transaction échoue à la validation, c'est-à-dire si une transaction de priorité supérieure qui écrit dans le même état a été exécutée en même temps qu'elle, alors les écritures effectuées par la transaction sont sales (car les valeurs sont erronées). Mais au lieu de supprimer complètement ces valeurs de la base de données, elles sont marquées ESTIMATE.
L'indicateur ESTIMATE indique à toute transaction lisant cet emplacement mémoire que les valeurs qui y sont présentes ne sont pas correctes et les transactions suspendent leur exécution. Cette opération remplace la suppression, car la réexécution de la transaction dont la validation a échoué entraînerait probablement l'écriture dans les mêmes emplacements mémoire que l'exécution précédente.
En marquant l'emplacement mémoire comme une estimation au lieu de le supprimer, les dépendances (de la transaction dont la validation a échoué) peuvent être détectées avant même la réexécution, évitant ainsi les réexécutions inutiles. Cette heuristique réduit considérablement le travail inutile.
Un aperçu complet de la manière dont Block-STM aborde la parallélisation peut être résumé comme suit :
BLOCK
de transactions commence par une liste ordonnée avec un ordre sériel clairement défini. Ces transactions sont exécutées sur toutes les ressources disponibles par ordre de priorité.
Un exemple est présenté ci-dessous :
Voici un aperçu du fonctionnement de Block-STM. Plus de détails peuvent être trouvés dans notre rapport ici et dans le document original de Block-STM ici .
Pour quantifier l’importance de l’ajout de Block-STM, nous avons effectué quelques tests pour évaluer les améliorations de performances qu’il apporte par rapport à l’exécution séquentielle et les résultats sont présentés ci-dessous.
Les résultats montrent que lorsque le nombre de threads (analogues à des unités de traitement indépendantes) utilisés augmente, les performances augmentent également. Les améliorations sont plus prononcées avec des blocs plus grands, ce qui donne des améliorations de performances jusqu'à 9X par rapport à une exécution séquentielle avec seulement 16 threads. Nous avons constaté que les résultats sont plus prononcés avec des blocs plus grands.
Nos tests montrent que les performances de Block-STM se dégradent considérablement sous des charges très conflictuelles, mais la pratique courante dans le secteur consiste à revenir à l'exécution séquentielle pendant de telles périodes. Nous recommandons le même mécanisme à Starknet pour préserver le débit sous des charges de travail très conflictuelles. Mais, dans l'ensemble, l'ajout de Block-STM améliorera considérablement et pérennisera Starknet.
Le deuxième changement majeur intégré dans la mise à niveau v0.13.2 est le regroupement de blocs et nous en parlerons plus en détail ci-dessous.
Comme nous l'avons vu précédemment, avant Bolt, chaque bloc Starknet était une tâche distincte, ce qui entraînait un coût fixe par bloc pour chaque bloc. De plus, le système a été conçu de telle sorte que chaque bloc nécessitait son propre blob, quelle que soit la quantité de données réellement consommée par le bloc.
Dans un monde où la demande était toujours élevée, cela ne poserait pas de problème, mais Starknet offre actuellement plus d'espace de bloc que la demande et il y a donc beaucoup de ressources gaspillées qui pourraient entraîner la perte de centaines d'ETH au cours d'un mois. Pour mettre davantage en contexte la gravité de la situation avant Bolt, voici les coûts associés à la publication d'un bloc sur L1 :
Cela représente un total de 215 000 gaz par bloc et ce coût est fixe, c'est-à-dire qu'il est le même quelle que soit la quantité de données contenues dans chaque bloc et qu'il est lié au nombre de blocs par $Cost = num blocks * 215000$. La solution idéale à ce problème serait que les coûts soient liés à la quantité de données publiées plutôt qu'à la quantité de blocs. Et c'est exactement ce que le regroupement de blocs permet d'accomplir via les arbres SNAR.
Les arbres Starknet Applicative Recursive (SNAR) sont un nouveau type d'arbre binaire introduit dans Bolt pour résoudre les problèmes mis en évidence ci-dessus. Un arbre SNAR a la structure suivante : chaque feuille est un bloc Starknet et les nœuds de tous les autres niveaux sont des preuves récursives de leurs enfants. Le nœud racine qui est une preuve récursive de toutes les autres preuves est le travail final qui est envoyé au SHARed Prover (SHARP).
Le principal avantage de l'arbre SNAR est que plutôt que de publier un bloc par preuve, de nombreux blocs Starknet peuvent être amortis dans la même mise à jour L1. Les racines de l'arbre SNAR sont désormais publiées sur le L1 uniquement lorsque l'une des deux limites configurables est atteinte : soit la limite DA (6 blobs de données), soit après qu'un certain nombre de feuilles ont été ajoutées à l'arbre (où une feuille est un bloc).
Cette conception dissocie le coût des transactions du nombre de blocs. Il existe toujours un coût fixe pour chaque bloc résultant de l'exécution du système d'exploitation StarkNet (SNOS) dans chaque bloc, mais dans l'ensemble, les coûts sont dissociés. Voici les chiffres actuels :
Le graphique de la figure 11 ci-dessous montre comment les coûts du gaz varient en fonction du numéro de bloc dans la conception précédente et maintenant (sous Bolt) :
Il est assez évident que le regroupement de blocs réduit considérablement les coûts de vérification sur le L1, ce qui entraînera sans aucun doute une baisse des prix du gaz pour les utilisateurs de Starknet.
L'effet des changements apportés à Bolt : exécution parallèle optimiste via Block-STM et packaging de blocs via le SNAR propriétaire peut être résumé comme suit : plus rapide et moins cher.
L'exécution parallèle réduit le temps d'exécution et, par extension, la congestion, ce qui réduira les frais de gaz pendant les périodes de trafic élevé, tandis que les arbres SNAR s'attaquent aux coûts associés de DA et de preuve. Il est intéressant de noter que cette mise à niveau fait de Starknet le premier L2 avec exécution parallèle et le prépare à devenir un concurrent majeur dans l'espace L2. Il est important de noter qu'il est peu probable que l'effet de ces changements soit immédiatement évident, en particulier celui de l'exécution parallèle, mais ils sont essentiels pour assurer la pérennité de Starknet et de l'ensemble de l'écosystème Ethereum dans son ensemble.
Note de l'auteur : Une version de cet article a déjà été publiée ici .