Ce sujet liste du vocabulaire de base pour comprendre les concepts de Duniter-v2s, sa lecture est vivement recommandée, cela vous aidera beaucoup à comprendre les discussions autour de Duniter-v2s (la réimplémentation de Duniter basée sur Substrate).
Rédaction inachevée, les termes vont être ajoutés au fur et à mesure.
Vous pouvez lire dans n’importe-quel ordre, tous les termes se font référence les uns aux autres, il n’y a pas d’ordre plus facile où plus logique.
La description de chaque terme commence par une définition courte et une explication superficielle, puis est suivie par des explications plus détaillées et plus techniques, il est recommandé de tout lire si ce n’est pas inconfortable pour vous, vous apprendrez beaucoup, comprendrez mieux, et cela devrait améliorer sensiblement la qualité de vos interventions et contributions
Cliquez sur un terme pour dérouler sa description :
AccountId
AccountId
Quelque chose qui identifie un compte, dans la plupart des runtimes substrate il s’agit de 32 octets libres qui encodent une clé publique. Et c’est le cas également dans Duniter-v2s. Notez bien que cet identifiant de compte n’indique pas le type de cryptographie de la clé publique qu’il contient, cette information ne servant que pour vérifier une signature, elle n’est associée qu’à cette dernière.
Archive node / nœud Archive
Archive node / nœud Archive
Nœud lancé avec l’option --archive
et qui a la particularité de stocker l’état de la blockchain pour tous les blocs canonicalisés depuis le genesis sans jamais les élaguer.
Autrement dit, si la blockchain comporte 15,000,000 de blocs, un nœud conservera 15,000,000 d’états en base de données et sera capable d’en répondre : par exemple, pour connaître le nombre de membres à chaque bloc.
Un nœud archive consomme donc beaucoup plus d’espace disque qu’un Full node.
Authority / Autorité
Authority / Autorité
Une autorité est un compte qui est inscrit dans la “Session” actuelle, ce qui signifie qu’il participe actuellement à la co-création des blocs, le nœud “Validateur” associé doit impérativement être en ligne et correctement synchronisé au réseau, sinon quoi des sanctions automatiques s’appliquent, dont éventuellement l’exclusion des “Sessions” suivantes.
Call
Call
Une fonction d’une pallet (voir section “pallet”) qui est “appelable” ("dispatchable"
in english) et ses paramètres d’entrées.
Par exemple, un simple virement de Ğ1 correspond au call balances.transfer(dest, value)
où dest
est le destinataire et value
le montant en centimes de G1.
Un call est toujours exécuté par une “Origine” (voir section “Origine”).
L’exécution d’un call peut être déclenchée par :
- Un Extrinsic signé, l’origine sera alors nécessairement
Signed(account_id)
- Un Extrinsic non signé, l’origine sera alors
None
- Un Hook (voir section hook)
- Un autre call
Généralement, les extrinsics non-signés sont interdits, ou limités à des cas bien particuliers, comme quelque chose qui est soumis par l’auteur du bloc lui-même, voir section “Inhérent”.
Une grande partie de la puissance et de la simplicité de susbtrate viens du fait qu’un call peut en exécuter un autre, et qu’un call peut contenir d’autres call en paramètre !
Par exemple, le call utility.batch_all(calls)
prend en paramètre un tableau de calls, et permet de les exécuter tous dans l’ordre fourni de manière atomique, si l’un des call échoue, ils sont tous annulés comme si rien ne s’était produit.
Par exemple, dans Duniter-v2s, le call identity.create_identity
exécute le call cert.add_cert
, ce qui permet d’implémenter facilement le fait que “Le créateur de l’identité est également sont premiers certificateur”.
Canonicalization
Canonicalization
Fait pour le Client de considérer qu’un bloc fait partie de la chaîne principale de façon irréversible (sortie de la fenêtre de fork).
La canonicalisation peut intervenir de deux façon :
- finalisation d’un bloc (via Grandpa par ex., canonicalisation par le Runtime)
- sélection forcée par dépassement de la taille de la fenêtre de fork (canonicalisation par le Client)
Un bloc canonicalisé voit ses modifications au Trie impactées sur le backend, et donc persistées sur disque.
La canonicalisation supprime tous les forks qui ne sont pas basés sur le bloc canonicalisé (et leurs modifications sur le Trie).
Source : documentation Substrate
ChainSpec
Les chain specification sont un ensemble d’informations décrivant un réseau précis. Elles sont composées de :
- des spécifications du client (nom/id de la monnaie, bootnodes, code substitute…)
- un état genesis (bloc zéro) incluant le runtime initial
Certaines spécifications du client peuvent être modifiées après le lancement du réseau (par ex les bootnodes), alors que le genesis est fixé une fois pour toutes (d’où l’utilisation du hash du genesis comme identifiant de blockchain).
Client
Client
Le binaire livré et installé par les gestionnaires de nœuds. Le Client est aussi parfois nommé “Binary” ou “Node”.
Le client désigne tout ce qui ne se trouve pas dans le Runtime.
Le logiciel duniter-v2s contient à la fois un Client substrate ET des runtimes (un par monnaie).
Consumers
Consumers
Compteur par compte, indiquant combien de “choses” ont actuellement besoin que le compte existe.
Le compte ne peut être détruit qui si consumers == 0
.
Les “consommateurs” du compte ont également besoin qu’au moins une “chose” leur fournisse l’existence du compte, ce sont les Providers (voir section Providers).
Par exemple, l’attribution du RandomId a besoin que le compte concerné existe, consumers est donc incrémenté lors de la demande d’attribution du RandomId, puis décrémenté quand le RandomId est finalement assigné (deux epoch plus tard, le temps d’obtenir ure graine aléatoire suffisamment robuste et non-manipulable).
Le compteur consumers
ne peut être incrémenté que si le compteur providers
du même compte est strictement supérieur à zéro. Dit autrement, une “chose” ne peut consommer un compte qui si au moins une autre “chose” fourni l’existence du compte.
Pour le moment dans Duniter-v2S, deux “choses” consomment le compte :
- L’attribution du randomId
- Le stockage des sessions keys
Dispatch class
Dispatch class
Chaque call appartient à une et une seule “dispatch class”, qui indique en quelque sorte “l’importance” du Call, à quel point il est important de l’inclure dans le prochain bloc.
Il y a trois dispatch call : Normal, Operational et Mandatory.
- Normal : C’est la classe par défaut, utilisée pour la plupart des Calls. Un extrinsic contenant un call Normal ne peut pas faire dépasser le “poids” maximal d’un bloc, il sera reporté au bloc d’après à la place.
En outre, le “poids” total de l’ensemble des extrinsic contenant un call Normal ne peut pas dépasser une certaine proportion du «poids» maximal d’un bloc. Cette proportion, nomméeNORMAL_DISPATCH_RATIO
, est configurable par runtime, la valeur recommandée est75%
. - Operational : Les calls Operational sont des call important, ils sont prioritaires sur les calls Normal et bénéficient d’un espace dédié dans le block, qui correspond à l’espace que les call normal ne peuvent pas remplir, soit la proportion
1 - NORMAL_DISPATCH_RATIO
du “poids” maximal d’un bloc. Les call Operational sont généralement liés à la gouvernance on-chain. Par exemple, tout les call de la pallet collective sont Operational. - Mandatory : Les calls Mandatory doivent obligatoirement être inclus dans le prochain bloc, quelles qu’en soit les conséquences. Ces calls sont donc très dangereux, cette classe est réservée uniquement à certain inherent (calls soumis par l’auteur du bloc lui-même) essentiels. Par exemple, l’inherent
timestamp.set
, qui permet à l’auteur du bloc d’indiquer le timestamp de sa machine, est Mandatory.
Aucun Extrinsic ne peut contenir un call mandatory.
Epoch
Epoch
Durée pendant laquelle est définie une graine aléatoire collective qui correspond à la concaténation des outputs VRF de chaque bloc dans l’epoch.
Cette graine aléatoire collective est elle-même utilisée dans les inputs VRF de l’epoch N+2.
Cette graine aléatoire collective peut être utilisée pour tout autre usage nécessitant une graine aléatoire vraiment robuste et non manipulable, elle est par exemple utilisée pour assigner un random id à chaque nouveau compte.
Une Epoch à généralement une durée fixe de l’ordre de quelques heures.
Extrinsic
Extrinsic
Un Extrinsic est l’équivalent de ce que l’on nommait un “document” dans Duniter v1.
Un Extrinsic contient les cinq éléments suivants, tous sérialisés au format SCALE :
- La clé publique de l’émetteur (32 octets libres)
- Des
SignedExtra
: métadonnées configurables - Un Call
- Le type de cryptographie sur un octet (0x00: Ed25519, 0x01=sr25519)
- La signature des quatre éléments ci-dessus (64 octets)
Les SignedExtra
sont détaillés dans ce sujet : [lien à venir]
Extensions
Extensions
Parmi les Externalities, on peut ajouter des Extensions.
Définition officielle : sp_externalities - Rust :
Extensions are for example the keystore or the offchain externalities. These externalities are used to access the node from the runtime via the runtime interfaces.
Autrement dit, les extensions font partie, avec le Storage, des éléments extérieurs auxquels le Runtime a accès.
Externalities / Externalités
Externalities / Externalités
Ensemble des “host fonction” que le Client (=host) met à disposition du runtime.
Par exemple, les fonctionnalités de cryptographie sont fournies via des externalités.
Le runtime déclare la liste des externalités dont il a besoin, et elles sont toutes checkées lors du chargement du runtime, si le client ne les définit pas toutes, le runtime ne peut pas être utilisé.
En fonction du contexte d’exécution du runtime, certaines externalités ne sont pas activées, mais elles doivent tout de même être toutes définies et déclarées coté Client pour que le runtime puisse être lancé.
Par exemple, les offchain workers ont accès à des externalités permettant d’obtenir des données du monde extérieur (nombre aléatoire, requête http, etc), ces externalités sont bien évidemment désactivées dans le contexte de l’exécution d’un bloc (on parle de “contexte onchain”).
Finality / Finalisation
Finality
C’est une canonicalisation par le Runtime.
Autrement dit, c’est le fait de considérer qu’un bloc fait partie de la blockchain principale de façon définitive sur instruction d’un gadget de finalisation tel que GRANDPA.
Full node / Full client
Full node / Full client
Un “full node” (où “full client”) est un nœud duniter-v2s qui exécute tous les blocs (ce qui revient à les vérifier intégralement), et qui stocke l’entièreté du storage onchain.
Certains “full node” peuvent en plus créer des blocs, on dit alors que ce sont des nœuds ayant le rôle “Authority”.
Un “full node” peut exposer ou non une API RPC.
Un “full node” doit toujours exposer une API libp2p, afin de communiquer avec les autres fulls node et de fournir des informations aux light nodes (Voir “Light node”).
Hook
Hook
Un hook est une fonction exécutée automatiquement par la blockchain à chaque bloc (sauf pour certain hooks).
Tout ce qui est exécuté dans un bloc provient soit d’un hook soit d’un extrinsic.
On parle d’exécution automatique pour tout ce qui est exécuté dans le contexte d’un hook, les exécutions automatiques ne sont payés par personne,
On parle d’exécution manuelle pour tout ce qui est exécuté dans le contexte d’un extrinsic.
C’est une distinction très importante, car toute exécution manuelle est payée par des frais ou quotas d’un utilisateur, et peut être reportée ou annulée si le bloc est plein.
À contrario, les exécutions automatiques ne sont payés par personne, et ne peuvent généralement pas être reportées ou annulées, il y a donc un risque de dépasser le temps d’exécution maximal d’un bloc.
Pour passer à l’échelle, il faut minimiser autant que possible les exécutions automatiques, et maximiser les exécutions manuelles.
Il y a principalement trois hooks :
on_initialize(block_number)
: exécuter au début de chaque blocon_finalize(block_number)
: exécuter à la fin de chaque bloc.on_idle(block_number, remaining_weight)
: exécuter à la fin de chaque bloc (juste avanton_finalize
), mais uniquement s’il reste de la place dans le bloc.
Et quelques autres hooks exotiques dont je ne vais pas parler pour ne pas encombrer, surtout qu’on ne s’en sert pas, c’est juste pour dire que la liste n’est pas exhaustive.
Chaque pallet peut implémenter un ou plusieurs de ces hooks. Lorsqu’un type de hook est exécuté, il est exécuté pour toutes les instances de pallet qui l’implémente, dans l’ordre de déclaration des instances de pallet dans la macro construct_runtime!
.
Light Node / Light client
Light Node / Light client
Un “light node” est un nœud qui récupère uniquement les headers des blocs. Les headers contiennent le numéro du bloc, son hash, la signature de l’auteur et les merkle root hash du storage et des extrinsics.
Généralement, un “light node” est installé localement sur la machine de l’utilisateur final, et expose une API RPC locale consommée directement par les applications et wallets.
Lorsqu’une application (un wallet par exemple), demande au light node une donnée du onchain storage, le light node va récupérer cette donnée auprès d’un full node, avec une merkle proof, et comme le light node connaît le merkle root, il peut vérifier l’authenticité de la donnée.
Avoir un light node en local garanti à l’utilisateur un niveau maximal de décentralisation, car il n’a alors pas besoin de faire confiance à un nœud installé sur un autre ordinateur auquel il n’a pas accès.
Origin
Origin
À comprendre comme “L’origine d’un call”. Cela désigne en quelque sorte “qui” est l’émetteur du call, ça permet de gérer les droits.
La liste des origines possibles est définie par le runtime (voir section “Runtime”). Par défaut un runtime substrate contient au moins trois origines :
Signed(account_id)
Root
None
Mais on peut en définir d’autres, dans duniter-v2s on aura par exemple l’origine SmithsMembers(a, b)
qui désigne une proportion des membres forgerons (a
membres forgerons à l’origine du call sur b
membres forgerons au total).
Généralement, le code d’un “Call” commence par vérifier que l’“Origin” est autorisée à exécuter ce “Call”, et si ce n’est pas le cas, l’exécution du call échoue avec l’erreur BadOrigin
.
Overlay
Overlay
Cache intermédiaire entre le Runtime et le Storage, stocké en mémoire vive.
Ce cache est important à la fois pour :
- économiser des lectures et écritures sur le backend Storage (disque), qui sont très coûteuses
- éviter de solliciter le backend Storage pour des blocs qui seront jetés (forks élagués)
- permettre un rollback très rapide des modifications en cas d’échec de la transaction (extrinsic KO)
Ainsi, au traitement d’une transaction, le résultat est stocké dans l’Overlay et non pas directement dans le storage. Les données ainsi mises en cache dans l’Overlay ne seront mergées dans le Storage qu’à la canonicalisation du bloc.
Pallet
Pallet
Sorte de “module” que l’on peut intégrer dans son runtime, à condition d’implémenter intégralement son trait (sorte d’interface) Config
.
Toutes les interactions entre une pallet et l’extérieur (le runtime où d’autres pallets) sont définies dans le trait Config
de cette pallet, il est donc nécessaire et suffisant d’implémenter ce trait.
Par exemple, la pallet universal_dividend
a besoin de connaitre le nombre de membres (pour réévaluer le DU), son trait config déclare un type associé MembersCount: Get<u32>
, qui peut être n’importe quelle “chose” retournant un nombre entier, qui sera interprété comme le nombré de membres.
C’est le code de l’implémentation du trait Config
dans le runtime qui ira chercher cette information, en utilisant une fonction publique autre pallet.
Outre le trait Config
, chaque Pallet peut également définir des Call, Event, Error, Hook, Origin et StorageEntry. Chacun de ces éléments sont alors automatiquement ajoutés à tout runtime qui utilise cette pallet, et certains d’entre eux (Call, Event, Error et StorageEntry) sont accessibles dans les métadonnées du runtime.
Certaines pallet sont dites instantiables, on peut alors utiliser plusieurs instances différentes d’une même pallet dans un même runtime. Chaque instance à sa propre configuration (sa propre implémentation du trait Config
de cette Pallet).
Providers
Providers
Compteur par compte indiquant combien de “choses” fournissent l’existence du compte.
Dans duniter-v2s, nous avons pour le moment un seul provider : le solde monétaire.
Si de la monnaie est versée sur un compte dons le solde était à zéro, alors providers
est incrémenté.
Si, pour toute raison, le solde d’un compte descend à zéro, providers
est décrémenté.
providers
ne peut être décrémenté que si consumers == 0 || providers > 1
. Dit autrement, une “chose” qui fournissait l’existence d’un compte peut disparaître uniquement si plus rien ne consomme le compte o si au moins une autre “chose” en fourni l’existence.
C’est pourquoi dans duniterv2-s, il n’est pas possible de vider un compte pour lequel consumers > 0
.
RPC
RPC
RPC est un acronyme anglais qui signifie littéralement “Remote Procedure Call”, que l’on pourrait traduire en français par “Appel de Méthode Distante”.
Dans le contexte de Substrate (et de Duniter-v2s), le terme “RPC” désigne plus précisément l’API JSON-RPC exposée par le “Client” Substrate.
Cette API expose un unique endpoint HTTP et un unique endpoint WS. On lui indique la “méthode” RPC que l’on souhaite appeler, avec ses paramètres d’entrée. Le client substrate va alors exécuter cette méthode puis renvoyer le résultat via l’API.
C’est cette API qui est utilisée par les “Wallets” pour soumettre des “Transactions”.
Cette API RPC permet également de lire le contenu du “onchain storage”, et bien d’autres choses encore.
Il existe deux modes différents pour l’API RPC du substrate, le mode Safe (qui peut être exposé publiquement), et le mode Unsafe (qui ne doit pas être exposé publiquement).
Le node Unsafe contient des méthodes permettant d’administrer un nœud “Validateur”, ainsi que des méthodes coûteuses en calcul qui ne doivent être exposés qu’à des partenaires privés identifiés.
Runtime
Runtime
Partie de code qui contient les “Pallet” et leurs “Call”, elle est compilée en webassembly et le code webassembly du runtime est inclus dans le storage onchain, ce qui permet de mettre à jour le code du runtime via… des règles onchain définies par le runtime.
Plus précisément c’est le call system.set_code(code)
qui met à jour le Runtime, il doit être exécuté avec l’Origine Root
.
Cela permet de faire des mises à jour dites “forkless”, c’est dire que lorsqu’un bloc contient le call set_code
, le prochain bloc va automatiquement utiliser le nouveau runtime, sans que les gestionnaires de nœud n’aient besoin de mettre à jour leur nœud.
SCALE
SCALE
Format d’encodage des données propre à Substrate et utilisé notamment pour encoder les extrinsics, les blocs, les évènements, les entrées dans le storage onchain… à peu près tout quoi !
Retrouvez sur de site de Substrate la définition de ce format ainsi qu’une liste de bibliothèques permettant de le gérer : https://docs.substrate.io/reference/scale-codec/
Session
Session
Durée pendant laquelle la liste des autorités ne peut pas changer. La liste des autorités pour la session N est déterminée au 1er bloc de la session N-1.
La durée d’une session est nécessairement la même que celle d’une Epoch. Le changement de session se fait également nécessairement en même temps que le changement d’Epoch (pas de décalage).
Slot
Slot
Un slot est une notion qui ne s’applique qu’aux mécanismes de consensus dit “slot-based”, ce qui est le cas de BABE, mais il y en a d’autres.
Un slot est un intervalle de temps pour lequel le mécanisme de consensus défini qui est éligible pour produire un bloc. Certains mécanismes de consensus peuvent définir plusieurs types d’éligibilité, par exemple BABE défini deux éligibilités dites primaires et secondaires.
Lorsqu’un nouveau bloc est importé, on détermine d’abord le temps blockchain de ce bloc, puis de ce temps blockchain on en déduit le numéro de slot, ce qui permet ensuite de vérifier si l’auteur du bloc est bien éligible pour ce numéro de slot.
La notion de slot sert uniquement à définir qui est éligible quand, elle n’implique rien d’autre. En particulier, elle ne dit pas qu’un bloc doit être proposé, mais seulement quels auteurs peuvent en proposer un.
Storage onchain
Storage onchain
L’ensemble des informations permettant de décrire l’état courant du système.
La partie du runtime qui exécute un bloc peut être théorisée comme une Fonction de Changement d’État (State Transition Function in English), qui prend en entrée l’état actuel (=le “onchain storage” du dernier bloc) ainsi que le prochain bloc. Cette fonction produit en sortie un nouvel état (un nouveau “onchain storage”).
Toute exécution d’un « Call » ne peut in fine produire que deux choses : des changements dans le onchain storage et des « Évènements ».
On peut également définir le onchain storage comme l’ensemble des informations nécessaires et suffisantes pour vérifier la validité d’un bloc, dans subtrate c’est strictement équivalent.
Cela correspond à ce que l’on nommait les “index” dans Duniter v1.
L’intégralité du contenu de ce onchain storage est récupérable via l’API RPC.
De plus, ce onchain storage est stocké par bloc. Par défaut, un nœud conserve en base de donnée les onchain storage des 256 derniers blocs. Mais certains nœuds, dits nœuds d’archive, conservent tout les onchain storage de tous les blocs, il est donc possible de connaître l’état passé à n’importe-quel bloc, et ceux directement via l’API RPC.
Transaction
Transaction
Aucun rapport avec une transaction au sens d’un “transfert de monnaie”, c’est beaucoup plus large que ça !
Nom que l’on donne à un “Extrinsic” lorsque l’on se situe côté “Client”.
Ce qui est une “Transaction” côté “Client” devient un “Extrinsic” côté “Runtime”. C’est la même chose, mais donner un nom différent permet de savoir si on parle côté Runtime où côté Client.
Validateur
Validateur
Un validateur est un nœud Substrate qui a vocation à participer à la co-création des blocs, il ne doit pas exposer d’API RPC publique, et doit impérativement être déployé sur une machine dont les ressources sont supérieures où égale à la machine de référence utilisée pour le “Weight Benchmarking” étalonnage des poids.
Concrètement c’est le même logiciel, mais lancé avec l’option --validator
.
Wallet
Wallet
Logiciel permettant aux utilisateurs de générer des transactions, de les signer et de les soumettre au réseau (généralement via l’API RPC).
Généralement, les wallets se chargent également de récupérer des informations dans le storage onchain (via l’API RPC) pour les afficher aux utilisateurs, par exemple le solde de leur compte, le nombre de certifications qu’ils ont reçues, etc
De plus, certains wallets se chargent également de récupérer des données passées auprès d’indexeurs afin de fournir des informations utiles aux utilisateurs mais qui ne se trouve plus dans le storage onchain, par exemple l’historique des transferts de monnaie passés dont le compte est émetteur ou récepteur.