IPFS pour les datapods v2

J’ai passé deux jours à me plonger dans IPFS et ça m’a ensuite occupé l’esprit tout le weekend.

Il m’est apparu évident qu’il fallait utiliser IPFS comme couche de stockage de données hors chaîne, ça me paraît même stupide de faire autrement.

Je m’y suis intéressé dans le cadre de
Étendre la toile de confiance sans consensus global
et du financement NGI, mais ça s’applique également aux datapods v2
Thinking about datapods v2
Attention, ça ne veut pas dire qu’on n’aurait pas de endpoint graphql “centralisé” comme elasticsearch ou
V2s-datapod: Hasura with Deno middleware to store profiles
mais que ce serait uniquement une couche d’indexation construite sur un stockage de données IPFS.

J’ai quand même identifié quelques points à creuser :

  • comment gérer la demande d’indexation d’une nouvelle donnée → gossip libp2p / ipfs pubsub
  • comment gérer l’indexation du contenu à épingler et indexer → orbitdb ou autre structure de donnée dans ipfs avec publication ipns

Je détaille :

Lors de l’émission d’une nouvelle donnée, par un téléphone, plusieurs options sont possibles :

  • inscription auprès d’un “pinning service” centralisé, courant dans IPFS mais ne nous intéresse pas (centralisé)
  • inscription en blockchain centralisée, également fréquent mais ne nous intéresse pas (centralisé)
  • publication sur le réseau ipfs décentralisé via la fonction p2p pubsub (décentralisé)

A priori c’est vers cette dernière option qu’on aimerait se tourner. Mais cela pose une question :

“Comment un nouveau nœud ou un nœud qui était hors ligne un moment peut-il récupérer les données émises par le passé ?”

La réponse est une première couche d’indexation sur ipfs. Il faut développer un programme à brancher sur son noeud ipfs qui écoute les messages pubsub sur le réseau et les traite :

  • le nouveau message (cid) est examiné
    • s’il dépasse une certaine taille (par exemple > à l’espace libre sur disque dur), il l’ignore
    • sinon il rapatrie le contenu et continue
  • si le contenu est un message signé (ipld) il vérifie si la signature est valide et continue
  • il peut ici optionnellement appliquer un filtre sur le contenu avant de continuer
  • si la clé émettrice correspond à son critère de confiance, il ajoute le cid du message dans le set des messages émis par cette clé

Ce set de clés publique avec une set de messages émis par chaque clé publique constitue un index. Cet index est un objet ipfs publié via ipns. On ne recherche pas le consensus sur cet index mais tant que certains nœuds partagent des critères en commun, ils ont le même index.

Quand un nouveau nœud se connecte ou souhaite rattraper son retard après une déconnexion, il peut copier l’index de quelqu’un d’autre, et éventuellement le fusionner avec son index existant.

Cet index n’est finalement qu’une liste de cid. Chaque nœud ipfs peut appliquer ses propres critères pour épingler ou pas la donnée derrière ce cid. Par exemple, mon critère serait : j’épingle toute donnée <10 Mo de tous les membres de la Ğ1 sauf si un membre dépasse un quota de 1Go ou que ces données dépassent 100Go sur mon disque dur, ou si un cid fait partie de ma liste de blocage formulée selon mes critères et partagée sur ipfs.

D’autre part, à chaque fois que j’épingle un nouvelle donnée, je préviens mon indexeur (postgresql + hasura) pour qu’il l’indexe et la rende facilement explorable via une api graphql.


Voilà pour la théorie, mon programme de la semaine est de passer à la pratique en faisant une poc.

[edit] vidéos de poc :

10 Likes

Vidéo sur le prototypage des datapods IPFS :

Ça donne une idée de comment ça va marcher côté client, mais il me reste à prototyper le plus dur, c’est-à-dire la structure de données de l’index. J’espère avoir du nouveau en fin de semaine :slight_smile:

00:00 vidéo démo des datapods ipfs pas du tout expliqué
00:16 démarrage de ipfs (kubo)
00:53 ddd-ui pour “Duniter Datapods Demo UI”
01:39 UI qui fait pas grand chose Home/Network/Login/Upload/Download/Pubsub
02:02 exemple d’upload d’un fichier texte dans IPFS
02:17 exemple d’upload d’image
02:34 exemple d’upload d’une information structurée et demande d’indexation signée
03:07 inspection du DAG de demande d’indexation dans IPFS
04:14 résumé : on vient de montrer la partie client des datapods
05:00 deuxième logiciel : ddd-indexer
05:40 mise en place des abonnements pubsub et démonstration
06:34 documentation de libp2p pubsub
09:00 j’ai fait ça en quatre jours : deux jours de lecture de doc, deux jours de prototypage
09:50 lecture de code ddd-ui
10:16 parenthèse sur notre sous-réseau d’IPFS public
11:00 lecture du code des view
11:36 logique de la soumission de donnée depuis un client (smartphone par exemple)
12:58 message de demande d’indexation
15:00 publication du CID de la demande d’indexation sur pubsub
15:44 plus d’info sur le fonctionnement du ddd-indexer (qui se branche en RPC sur kubo par exemple)
17:20 construction de l’index : set des clés publiques et set des demandes d’indexation de cette clé publique
18:10 interaction avec le noeud ipfs en ligne de commande
18:40 re-démo de la diffusion pubsub du CID de la demande d’indexation
19:55 ipfs dag stats du DAG de la demande d’indexation en ligne de commande
21:02 ipfs ls du DAG
22:05 exemple du découpage IPFS d’un gros fichier (vidéo)
23:37 fin de la démo et conclusion !

4 Likes

Sympa, hâte de voir ce que ça donne en condition réelle.
La partie indexation n’est pas encore claire pour moi.

Si on a encore besoin d’indexer ES ou gql, j’ai du mal à percevoir l’intérêt de ce swarm ipfs du coup.
Si les clients ne font in fine que confiance aux indexer, le endpoint ES ou gql tombe et le client est HS. La manière dont la donnée est stocké et répliqué derrière ne changera rien de ce point de vue là.

Enfin j’en suis là dans ma compréhension du système.

Le choix de la structure de donnée est primordial, il faut prendre le temps de bien la définir, car la modifier c’est incrémenter une version de format de donnée et implémenter des migrations ou des prises en charge versionnés côté client.

Oui, j’ai pas encore prototypé ni détaillé la partie indexation. Mais en gros il y aura un index sur IPFS qui sera une sorte de “liste de tous les messages” et qui servira de base pour savoir quelle donnée épingler. Et les indexeurs ES ou GQL pourront être plusieurs pour la redondance comme pour duniter-squid au détail près qu’ils auront peut-être des sources de données légèrement différentes par exemple si désaccord sur la modération. Il serait possible de partir sur une stack full ipfs, mais plutôt sur les clients bureau. Pour mobile c’est nécessaire de limiter au max le flux de données et l’espace de stockage (les gens sont souvent bloqués par ça) et donc d’avoir ES ou GQL.

La partie importante est de choisir la bonne structure de donnée d’index pour que ce soit rapide de récupérer le diff si on a été hors ligne un moment. Et effectivement, il vaut mieux se mettre d’accord sur ce qu’on signe pour pouvoir faire la vérification facilement.

1 Like

Vidéo intéressante mais je ne comprends pas tout :slight_smile:

Possible d’avoir un déniaisage sur les principes de base d’IPFS, et en quoi ça répond à quelles problématiques des indexeurs ?

Comme je le comprends jusque là (mais pas certain de ne pas avoir compris de travers) IPFS serait un outil de stockage objet distribué avec fonctionnement en P2P. Si j’ai bon là dessus ce sera déjà pas mal :slight_smile:

2 Likes

Tu peux regarder la vidéo de Fred où il inclut quelques explications : 🌐 UPlanet. Ouverture des inscriptions

Et sinon en quelques mots, c’est

  • un système d’adressage du contenu par son hash (CID) de sa découpe en DAG, avec immutabilité et vérifiabilité des données
  • une couche réseau bien pensée qui permet de récupérer une donnée bouts par bouts depuis un ensemble de serveurs connectés en p2p

J’ai l’intention de détailler mais ça m’a pris deux jours complets d’assimiler tous les concepts (IPLD, IPFS, IPNS…) et avant ça c’était toujours flou. Donc je suppose qu’il faut juste se plonger un peu dedans pour en saisir la teneur.

1 Like

Deuxième vidéo sur le prototypage des datapods IPFS :

00:00 référence à la précédente vidéo
00:26 présentation interface de l’indexeur ipfs
02:30 présentation de l’index Map(timestamp → cid)
03:16 merkle tree base-16
04:38 historique de l’index : blockchain type “git”
06:50 publication du root cid courant et du head de l’historique avec ipns
07:32 facilité pour récupérer les données hors chaîne
08:15 l’idée dernière l’utilisation du timestamp comme clé
09:44 il reste à brancher un indexeur Hasura
10:58 possibilités d’index spécialisés
11:16 possibilités d’interopérabilité Nostr / NIPS
12:05 conclusion


Je continue mon prototype pour avoir une “tranche verticale”, c’est-à-dire qu’il me reste à ajouter :

  • la couche d’indexation Hasura de poka
  • les données C+ actuelles à titre d’exemple
  • une mise en prod en dev avec une démo de comment bootstrap son indexeur sur un index existant

Mais je vais aussi vous faire des explications par écrit avec des schémas pour prendre les retours assez tôt pour pas m’enfoncer dans une mauvaise voie (même si je suis assez confiant sur les choix que j’ai fait).

2 Likes

Par rapport à mon message précédent :

Une nouvelle vidéo sur le sujet :

00:00 début
00:08 noeud IPFS en tâche de fond (kubo)
00:15 interface démo de soumission de données
00:42 détection du nouveau root CID et de la nouvelle donnée
00:57 visualisation dans la base postgres / Hasura
01:36 informations présentes en db
02:00 visualisation des données dans l’exploreur IPFS
02:09 demande d’indexation (pubk, time, kind, data)
03:03 bug d’effet démo
03:43 visualisation d’une donnée C+ importée et indexée
05:00 conclusion : ce qu’il reste à faire
05:22 démarrage de l’indexeur avec un CID en argument

Pas trop le temps de vous en dire plus avant la semaine prochaine, mais je m’approche de la fin de cette période de prototypage intense qui a duré trois semaines.

6 Likes

Les Datapods IPFS ont leur propre dépôt maintenant :

Je vais fermer ce sujet prototypage et ouvrir une catégorie dédiée dans NodesDatapods

1 Like

J’ai régardé les vidéos, très beau travail ! Ca promet.

Tu me diras quand tu aura un noeud qui tourne ? :slight_smile:

Au niveau de la structure des données, voici quelques suggestions :

  • Ne pas confondre pubkey et address (et éviter les trucs pas clair comme pubk)
  • Regrouper les éléments qui concernent ipfs dans un object ipfs (plutot que data_cid)
  • Ajouter aux documents (demande d’indexation, profiles, etc.) l’attribut version (number).
    • Pour le profil on passerait simplement en version 3
  • Conserver les documents JSON issue de la V1 dans un sous-objet dag (avec son propre cid) par exempleraw (ou alors previous pour pointer vers l’ancien document en version 2)

Question :

  • Si on migre les profils Cs+ d’un coup, il ne seront pas signés dans le DAG (sig à null) et donc non vérifiable. Il faut donc conserver le contenu intact. Comment faire ?
1 Like

Merci pour tes retours, c’est le bon moment avant que je m’enfonce trop profond dans des choix problématiques ! En plus ça m’aide à détailler des choix, ce que je remettais sans cesse à plus tard.


noeud qui tourne

Je mets tout en oeuvre pour y arriver le plus vite possible, d’ici la fin de la semaine j’espère, pour pouvoir avoir des retours sur cas concrets.

pubkey et address

J’étais parti pour pubkey parce c’est agnostique du réseau et que ça évite de changer les données à la migration (donc les données de phase de test avec le préfixe d’adresse “42” sont compatibles avec le réseau de prod sans avoir à adapter). Mais en fait, tu as raison, je pourrais partir sur address, ça change pas grand chose.

éviter les trucs pas clair comme pubk

C’était plus clair pour moi en phase de test, mais c’est le genre de trucs sur lesquels on peut se mettre d’accord à un moment donné, c’est assez facile à changer. J’utilise un modèle dag-cbor (similaire à dag-json) et à terme on pourrait plutôt se tourner vers un format plus “fortement typé” et compact comme dag-pb ou même dag-scale si on est fous ><

Ajouter aux documents (demande d’indexation, profiles, etc.) l’attribut version (number).

J’ai beaucoup réfléchi à ça et j’aimerais bien en discuter plus avec toi. Actuellement j’ai un argument “kind” qui peut justement servir à ça : cesium profile v1, cesium profile v2, cesium profile delete, transaction comment… On peut tout mettre dedans comme c’est un CID.
Le seul intérêt que je vois à mettre un champ “version” qui est un entier numéroté est que c’est plus compact (on peut aller jusqu’à 8 bits si on prévoit de pas dépasser 256 numéros, contre 256 bits pour un CID). Mais :

  • l’utilisation d’un CID permet d’éliminer le besoin de se coordonner pour ajouter des formats, n’importe qui peut ajouter un format sans consulter personne et le collecteur fonctionnera toujours, on peut même s’en servir pour des références (pointeurs) plutôt qu’une simple énumération, à la manière du NIP-27 de Nostr.
  • alors qu’un numéro de version est implicite, le CID peut être explicite, donner des informations sur le format qu’il décrit, et pointer vers une documentation. Tout est contenu dans ce “kind”.

Donc en gros, c’est plus extensible de se baser sur un kind. On pourra se mettre d’accord entre nous sur des kinds communs, mais si des gens développent un plugin wordpress qui a besoin de données offchain, ils pourront développer leur propre kind sans nous consulter, mais en utilisant le même système de données. Pareil si des gens développent un bot qui bridge telegram et la Ğ1 pour afficher le profil telegram directement dans une appli Ğ1, pas besoin de passer par nous et “réserver” un numéro de version.

Conserver les documents JSON issue de la V1 dans un sous-objet dag (avec son propre cid) par exempleraw (ou alors previous pour pointer vers l’ancien document en version 2)

Il y a un truc qui me gène à faire ça : on stocke donc deux fois l’avatar :

  • une fois comme un fichier unixfs ipfs
  • une fois comme un base64

Ça m’aurait pas posé problème de stocker uniquement le json brut s’il n’y avait que la partie données, mais avec l’image c’est un peu lourd, et d’après ce que j’ai compris la signature porte sur l’image en entier et pas uniquement son hash, donc pas moyen de s’en passer et de conserver la signature valide. Pour récupérer les données, j’ai utilisé le script de poka.

Si le but et est de faire un indexeur elasticsearch qui ré-indexe des documents bruts, la bonne manière de faire à mon avis est de reconstruire des documents json à partir du dag du document archivé.

En fait j’ai deux “kind” différents pour les profils C+ : /src/consts.ts#L19-L22. L’un est pour les profils importés pour lesquels la demande d’indexation (IndexRequest) contient une signature vide (""), et le profil entier est stocké en tant que donnée, au détail près de la photo de profil qui est stockée dans un objet IPFS à part. La raison de ce choix est que pour moi, les images et documents “lourds” et non structurés devraient être servis via IPFS, et non via un indexeur. Pour moi, une base de données (Postgres ou autre) n’est pas faite pour stocker des byte array dans lesquels aucune recherche ne sera faite.

J’aimerais que tu détailles le besoin de conserver les profils “legacy” au format raw parce que ça me semble être juste un effet de bord du “genesis” de la blockchain des Datapods IPFS. Donc perdre la vérifiabilité individuelle des données n’a pas de conséquences : une fois qu’on a vérifié que le hash du genesis provenait effectivement de l’ensemble des données C+ authentiques, plus besoin de vérifier chaque donné individuellement.

oui et non :slight_smile: Dans les profiles Cs+ en version 1, oui la signature portait sur tout, mais depuis la version 2, elle porte uniquement sur le hash (qui lui porte sur tout, y compris l’image, depuis le départ).

il faut y réfléchir, car je vois une solution possible :

  • on ne stocke initialement que la version actuelle des profiles, sans altération. L’avatar reste en base64.
  • Les indexeurs Hasura peuvent alors resoumettre l’avatar avec un cid pour l’avoir sous forme de fichier. Charge alors à l’indexeur de faire le lien, en conservant le CID de l’avatar quelque part dans la BDD. A priori si c’est le même hash IPFS, c’est le même CID, non ?
  • que les clients gère la migration des profiles (disont en version 1 et 2) vers la version DAG. AInsi tous les profils DAG auront bien une signature (et non un sig vide), et peu à peu les anciens profiles disparaitront (en passant unpinned dès qu’une nouvelle version du profil apparait)

C’est pour la transparence. Il me parait simple de stocker dans IPFS les fichiers d’origine. Et de faire le ménage au fur et à mesure.

Comme évoqué au dessus, l’indexeur peut très bien indexer le reste du profile dans la BDD, et conserver uniquement un CID de l’avatar (qu’il resoumet quand il veut). Il indexe ce qu’il veut et comme il veut, par définition. Pour autant, le document d’origine n’est pas altéré.

Un avantage à cela est que nous n’avons pas besoin de figer l’état d’un genesys des profils : on pourrait continuer d’indexer les profils G1v1 en permanence, pour faciliter la bascule des utilisateurs “refractaires” mais qui change d’avis.

Bref, je pneses qu’il faut qu’on en discute de vive voix :slight_smile:
Des dispos ?

1 Like

En effet ça me semble assez élégant de faire comme ça : stocker uniquement le JSON brut et soumettre les images à part au moment de l’ajout en base de donnée.

Oui, tout à fait, un CID (v1), c’est juste

  • un préfixe pour déterminer la base d’encodage (base32, base58…)
  • un numéro de version du CID (v0 ou v1)
  • un identifiant multicodec (dag-json, dag-cbor, dag-pb… mais aussi ip4, dns…)
  • un identifiant multihash pour savoir avec quel algo la donnée a été hashée (sha-256…)
  • le hash de la donnée hashée avec l’algo précédemment défini

Donc on peut se contenter de calculer le CID d’un fichier de manière déterministe sans nécessairement le re-soumettre et ça fonctionnera.

Ça dépend des clients. Dans ma tête, les clients mobile se contenteront par défaut de récupérer les données en GraphQL et n’effectueront pas de vérification. Mais un client bureau offrira la possibilité de demander la donnée source signer et pourra faire la vérification de manière automatique par défaut.

Je n’ai jamais d’absence de signature, c’est juste que pour les documents importés, la signature est dans le document alors que pour les nouveaux documents, la signature est déplacée dans la demande d’indexation.

La manière dont j’ai fait permet quand même de vérifier la signature des profils “v1” en version 1 et 2 individuellement, il faut juste faire un peu plus de travail :

  1. récupérer l’avatar et le convertir en base64
  2. récupérer le dag du document v1, remplacer le CID de l’avatar par l’avatar en base64, retirer la signature
  3. vérifier que la signature correspond bien au hash du document json précédement reconstitué en priant pour que le document original n’ait pas été sérialisé de manière exotique, par exemple en ajoutant des espaces

En fait ma plus grosse critique sur les documents C+ est que le payload de signature n’est pas défini précisément (cf Données Cesium plus, à quoi correspond exactement le hash?). Il dépend du format sous lequel l’utilisateur a soumis son message et il y a beaucoup de liberté sur ce format. De plus, il n’y pas d’encapsulation de la donnée, les champs hash et signature sont ajoutés et retirés au json avec des regexp. Mais peu importe, ça complexifie juste la vérification de la signature, pas l’import des données.

Cette semaine je ne suis pas à Toulouse mais du 23 au 26 avril en journée avec plaisir !

1 Like

Je vois pas trop l’intérêt de faire une table en plus en base de données, mais je pourrais avoir des noms de champs préfixés par ipfs plutôt que postfixés par cid. Pour l’instant il n’y a que :

  • ir_cid le CID de la demande d’indexation
  • data_cid dont on pourrait se passer en fait
  • avatar le CID de la photo de profil

Peux-tu préciser ton point en s’appuyant sur ces exemples ?

Ok, je vais peut-être implémenter cette solution.

Est-ce que tu confirmes que c’est une manière de récupérer le document intact ?