Ceci est la troisième partie d’une série d’articles sur la création d’une application de chat avec Rust sur la blockchain Near. Vous pouvez retrouver les articles précédents de la série ici et ici.
Dans cet article, nous nous concentrerons sur les parties hors chaîne du code. Nous discuterons du besoin d’ “indexeurs” et passerons en revue certaines parties de l’implémentation de l’indexeur dans cet exemple. Vous pouvez trouver le référentiel complet avec tout le code dont nous parlerons aujourd’hui sur mon GitHub.
Les indexeurs, ce qu’ils sont et pourquoi nous en avons besoin
Dans l’espace blockchain, un indexeur est un service qui consomme des données brutes à partir d’une source (généralement une instance de nœud complet colocalisée pour cette blockchain) et les analyse dans un format plus utile pour une application spécifique. Par exemple, dans le cas de notre application de chat, l’indexeur consomme un flux de blocs Near et produit un flux d’événements (par exemple, des messages reçus et des demandes de contact).
Les indexeurs sont importants car les bases de données utilisées pour faire fonctionner la blockchain elle-même ne sont généralement pas optimisées pour effectuer les types de requêtes dont les applications se soucient. Par exemple, l’obtention du solde d’un utilisateur pour un jeton ERC-20 sur Ethereum se fait généralement en exécutant la requête via l’EVM, car c’est la seule façon dont les informations sont disponibles à partir d’un nœud Ethereum typique. Il s’agit d’une opération extrêmement coûteuse par rapport à la recherche d’une entrée dans une base de données relationnelle traditionnelle. Par conséquent, une optimisation simple pour toute application nécessitant un accès rapide aux soldes ERC-20 consisterait à exécuter un indexeur sur les données brutes d’Ethereum qui remplit une base de données traditionnelle avec les soldes qui l’intéressent. Ensuite, l’application utiliserait cette base de données comme source pour les soldes au lieu d’un nœud Ethereum directement. C’est ainsi que l’explorateur de blocs Etherscan fonctionne sous le capot ; Etherscan exécute un indexeur pour remplir une base de données qui est ensuite utilisée pour remplir les champs des pages Web desservies par Etherscan.
Les indexeurs ne sont pas seulement importants pour Ethereum, tout dapp haute performance sur n’importe quelle blockchain devra inclure un indexeur quelque part dans son architecture. L’exemple d’application de chat dont nous avons discuté sur Near ne fait pas exception, alors plongeons dans la façon dont l’indexeur est implémenté.
Obtenir les données brutes
Les indexeurs traitent uniquement les données brutes de la blockchain dans un format que l’application associée peut utiliser ; ils ne génèrent pas les données en premier lieu. Par conséquent, la première question à laquelle nous devons répondre lors de la création d’un indexeur est : d’où proviennent les données de la blockchain ?
Near fournit quelques sources de données différentes, comme décrit ci-dessous.
Exécution d’un nœud proche du cœur
La meilleure source de données (en termes de décentralisation et de sécurité) pour toute blockchain est le réseau peer-to-peer de la blockchain elle-même. Pour accéder à cette source de données, vous devez exécuter un nœud qui comprend le protocole de la blockchain. Dans le cas de Near, l’implémentation du nœud est appelée nearcore. Son code source est ouvert sur GitHub. Une documentation est disponible sur la façon d’exécuter votre propre nœud nearcore. Le principal obstacle à l’entrée ici est la quantité d’espace disque nécessaire pour cela ; il est recommandé d’avoir 1 To de stockage dédié pour votre nœud et il faut un certain temps pour qu’il se synchronise avec la chaîne car il faut télécharger toutes ces données.
Une fois que vous avez une configuration de nœud nearcore, Near fournit un cadre d’indexation pratique dans Rust qui peut être utilisé pour créer des indexeurs avec nearcore comme source de données. Pour un projet réel, ce serait la meilleure façon de créer un indexeur. Cependant, notre exemple n’est qu’une démonstration, nous ne voulons donc pas passer des heures à télécharger des données de chaîne sur un serveur dédié de 1 To. Heureusement, il existe d’autres options.
Lac de données NEAR
Pour faciliter le démarrage de leurs projets par les développeurs, Near a créé le framework de lac de données comme source alternative de données à utiliser par les indexeurs. Le cadre du lac de données est construit au-dessus du cadre de l’indexeur mentionné ci-dessus, en utilisant un nœud Near du cœur comme source de données. L’indexeur alimentant le lac de données est trivial dans le sens où il ne traite pas les données pour une application spécifique, il transmet simplement les données longtemps pour être stockées dans le stockage AWS S3. Cependant, cela permet aux développeurs d’accéder à ces données à l’aide de leur propre compte AWS, puis de créer leurs propres indexeurs (non triviaux) en utilisant ce stockage S3 comme source de données.
L’avantage pour les développeurs est que cette méthode est beaucoup plus rapide à faire fonctionner. L’inconvénient, cependant, est que les données proviennent d’une source centralisée et sont donc théoriquement plus faciles à corrompre que d’utiliser directement le réseau peer-to-peer.
Pour accéder au lac de données, vous devez payer les ressources AWS que vous utilisez pour vous fournir ces données. Encore une fois, pour les besoins de l’exemple d’application de chat, je ne voulais pas que les gens s’inscrivent à AWS et dépensent de l’argent pour exécuter l’indexeur. Par conséquent, j’ai choisi l’option de source de données finale.
Nœuds RPC publics
Le dernier moyen d’accéder aux données de la blockchain si vous n’exécutez pas votre propre nœud ou si vous n’accédez pas au magasin de données prédéfini de quelqu’un d’autre consiste à utiliser les nœuds de quelqu’un d’autre. Les nœuds RPC sont des nœuds du réseau blockchain qui sont destinés à répondre aux demandes des utilisateurs. Chaque blockchain a des fournisseurs de nœuds RPC (certains gratuits, d’autres payants). Une liste des fournisseurs RPC pour Near est disponible ici.
C’est le moyen le moins efficace d’accéder aux données de la blockchain car il faut plusieurs requêtes RPC pour obtenir les données utilisées par les indexeurs typiques. Chaque demande RPC entraîne une latence du réseau, ce qui rend l’indexeur lent pour répondre aux événements se produisant sur la chaîne. Le seul avantage de cette approche est qu’elle est libre de configurer une démo tant qu’il existe un fournisseur RPC gratuit pour la chaîne (ce qui est le cas avec Near). Par conséquent, il s’agit de la source de données utilisée par l’indexeur dans notre exemple.
Cela dit, l’indexeur lui-même ne se soucie pas de la provenance de ses données. Par conséquent, même si notre exemple utilise la pire source de données, il vaut la peine d’explorer son implémentation car les concepts utilisés par cet indexeur sont les mêmes que ceux d’un indexeur construit à l’aide du lac de données de Near ou des frameworks d’indexation basés sur des nœuds.
Implémentation de l’indexeur
Notre indexeur est conçu comme une application tokio dans Rust. Tokio est un framework Rust pour écrire des applications hautes performances où les opérations d’E/S sont le principal goulot d’étranglement. Notre indexeur est une telle application car le calcul réel qu’il effectue est extrêmement rapide par rapport au temps qu’il faut pour demander des données aux nœuds RPC. Les principales caractéristiques de tokio sont qu’il utilise des pimitifs asynchrones non bloquants et qu’il intègre le multithreading pour permettre une exécution parallèle. Cela s’ajoute au fait qu’il se trouve dans Rust, il possède donc naturellement les garanties de sécurité de la concurrence et de sécurité de la mémoire fournies par Rust.
Si tokio est la scène sur laquelle se déroule notre application, alors ce qui suit sont les acteurs de la pièce (jeu de mots ; cette application suit le modèle d’acteur, mais j’ai choisi de le faire directement dans tokio au lieu d’utiliser une bibliothèque comme actix parce que je pense que les canaux de tokio fournissent un typage plus fort que les messages génériques utilisés dans la plupart des frameworks d’acteurs).
L’indexeur a quatre rôles principaux : le gestionnaire, le téléchargeur de blocs, le téléchargeur de blocs et le gestionnaire de réception.
Le manager
Le processus manager supervise l’ensemble de l’indexeur. Il est chargé de déléguer le travail aux autres processus et de leur dire de s’arrêter lorsque le programme est fermé (par exemple en cas d’erreur rencontrée). Par exemple, le manager gère l’équilibrage de charge des téléchargeurs de blocs en les parcourant lors de l’attribution d’un bloc à télécharger.
Le téléchargeur de blocs
Comme son nom l’indique, le but du processus de téléchargement de blocs est de télécharger des blocs. Il interroge périodiquement le Near RPC pour vérifier s’il y a de nouveaux blocs et s’il y en a, les télécharge et les envoie au manager. Si nous n’utilisions pas le RPC comme source de données, ce processus serait remplacé par une connexion à un nœud near ou à un lac de données.
Le(s) téléchargeur(s) de chunk
Sur Near, les blocs ne contiennent pas les données sur les transactions ; les morceaux font. Les blocs ne donnent que des informations sur les nouveaux morceaux disponibles. La raison en est le partage de Near (vous pouvez en savoir plus à ce sujet ici). Par conséquent, nous avons besoin de processus distincts pour télécharger les données de bloc pour chaque bloc. Les téléchargeurs de chunk remplissent ce rôle. Notre indexeur dispose de plusieurs instances de téléchargement de chunk pour permettre le téléchargement des morceaux en parallèle.
Si nous n’utilisions pas le RPC comme source de données, selon la façon dont les données sont factorisées dans la source de données que nous utilisions, ces processus n’auraient peut-être pas besoin d’exister (par exemple, le cadre de near-indexeur inclut toutes les données de bloc et de bloc dans un message unique). Mais pour notre cas, puisque nous utilisons le RPC, ces processus sont nécessaires.
Le gestionnaire de réception
Les blocs contiennent des “reçus” qui sont créés lorsqu’une transaction est traitée. Lorsque le gestionnaire reçoit un nouveau morceau d’un téléchargeur de morceaux, il envoie tous les reçus au processus de gestionnaire de reçus (nous pourrions avoir plusieurs instances de gestionnaire de reçus pour traiter les reçus en parallèle, tout comme nous avons plusieurs téléchargeurs de morceaux, mais le traitement des reçus est rapide assez pour que je ne pense pas que cela ait ajouté beaucoup d’amélioration des performances). Ce processus filtre les reçus uniquement pour ceux qui nous intéressent, puis télécharge le résultat d’exécution pour les reçus et traite enfin les événements à partir de ces résultats. Dans le cas de cet exemple, nous écrivons simplement les événements dans un fichier (pour une démonstration en direct, vous pouvez regarder le fichier avec quelque chose comme la commande tail -f Unix pour voir les événements arriver), mais vous pouvez imaginer qu’une implémentation de production pourrait transférer ces événements sous forme de notifications push vers une version mobile de l’application.
Remarques
Vous remarquerez peut-être tout au long du code de l’indexeur qu’il y a une certaine complexité autour de l’envoi de morceaux/reçus avec le hachage de bloc après le bloc qui comprenait ces morceaux. C’est une bizarrerie du Near RPC où il veut savoir que vous êtes au courant des blocs ultérieurs pour servir le résultat de l’exécution. Encore une fois, cela serait géré beaucoup plus facilement si vous utilisiez une meilleure source de données.
Il est intentionnel qu’il n’y ait pas de panique dans aucune des fonctions d’acteur. Lorsqu’ils rencontrent une erreur, ils l’enregistrent et envoient un message d’arrêt au gestionnaire (et le gestionnaire l’envoie à tous les autres acteurs). Ceci est important car paniquer dans une application multithread peut provoquer un comportement inattendu (en général, tokio est assez bon pour faire tomber toute l’application avec élégance, mais il est toujours préférable de coder de manière défensive contre elle).
Conclusion
Dans cet article, nous avons expliqué pourquoi les indexeurs sont importants pour les dapps du monde réel et examiné certains des détails de l’exemple d’indexeur mis en œuvre pour le chat dapp. Comme pour le post précédent, il y a des exercices dans le code de l’indexeur inclus dans les commentaires marqués comme EXERCICE. Je vous encourage à essayer ces exercices si vous voulez une expérience pratique avec la base de code.
Ceci est le dernier article de cette série. Dans la partie 1, nous avons examiné les principes généraux du développement de contrats intelligents et comment ils s’appliquent à un exemple de contrat pour une application de chat. Dans la partie 2, nous avons approfondi la manière d’utiliser le quasi-sdk pour rédiger des contrats intelligents pour Near dans Rust. Enfin, cet article a expliqué comment les indexeurs sont nécessaires pour intégrer les données de la blockchain aux composants hors chaîne de notre application.
Un dernier élément du code que je n’ai pas couvert est le test d’intégration. Le test d’intégration utilise la bibliothèque Near-workspaces pour simuler la blockchain localement et utilise le même style Rust asynchrone que l’indexeur.. Même si les tests d’intégration ne sont pas particulièrement flashy ou intéressants, ils sont importants pour garantir le bon fonctionnement de votre contrat. Je vous encourage à jeter un coup d’œil aux tests d’intégration pour le contrat de messagerie et à essayer l’exercice là-bas pour acquérir une expérience pratique dans ce domaine également.
Si vous avez apprécié cette série d’articles de blog, veuillez nous contacter à Type-Driven consulting. Nous sommes heureux de fournir des services de développement de logiciels pour les dapps, ainsi que du matériel de formation pour vos propres ingénieurs.