Encore une réflexion sur les datapods -- vision serveur vs vision client

Il y a deux mois j’en étais à des réflexions théorique : Réflexion sur les datapods décentralisés, et maintenant ça rentre vraiment dans la pratique : Soumission via endpoint http (centralisé) et format de signature.

Et là où ça devient particulièrement intéressant, c’est de confronter la vision serveur avec la vision client, ça donne des discussions intéressantes qui à mon avis nous amènent à quelque chose qui est simple à la fois côté client et côté serveur.

C’est assez facile de faire une implémentation serveur simple qui rejette toute la complexité sur le côté client. C’est aussi facile de faire une implémentation simple côté client mais qui rejette toute la complexité côté serveur. J’ai essayé d’éviter ce piège en développant à la fois :

Mais suite aux retours de @kimamila et @poka, je me suis rendu compte que ce que j’avais conçu côté client ne convenait pas à tous les usages (ce qui est dommage pour un client), en particulier parce que :

  • Un système p2p n’est pas forcément adapté à un smartphone avec peu de ressources réseau, capacité de calcul, batterie… (même si le projet IPFS s’intéresse au sujet IPFS Mobile Design Guidelines | IPFS Blog & News)
  • Les implémentations client IPFS ne sont pour l’instant disponible que en Javascript et Go, ce qui limite les possibilités pour des applis Dart par exemple. L’alternative consistant à embarquer un nœud IPFS et le contrôler via son API RPC n’est pas non plus évidente à déployer.

J’ai tenté une première implémentation très simple de passerelle HTTP POST, mais on pourrait pousser un chouilla plus loin pour pouvoir faire des simples mutations graphql. Voici plusieurs contraintes que l’on peut chercher à respecter :

  • utiliser des API standard type Graphql car :
    • il existe des implémentations dans beaucoup de langages dédiés aux applications
    • le développeur de client peut l’intégrer à une gestion d’état existante avec subscription, cache, gestion d’erreur…
    • le typage peut être fourni par le serveur avec le schema.graphql
  • limiter le traffic réseau
    • si seule la description texte d’un profil est modifiée, on ne devrait pas avoir à re-transmettre la photo de profil (actuellement modifier une virgule dans une annonce ğchange entraîne la re-transmission de toutes les images de l’annonce)
  • pouvoir restituer le payload signé tel quel sans nécessiter de modification pour vérifier la signature
  • utiliser le pouvoir de décentralisation permis par IPFS : on peut récupérer une donnée volumineuse par son CID en contactant le réseau plutôt qu’un seul nœud / on peut se contenter de transmettre un CID si on sait que la donnée est déjà disponible quelque part dans le réseau IPFS

Ces contraintes font apparaître des conflits :

  1. le choix entre le typage IPLD et le typage GraphQL
  2. Si on signe juste une mutation, ce n’est pas forcément évident de récupérer le document entier qui est le résultat de plusieurs mutations. Si on signe le document entier sans le transmettre, la vérifiabilité n’est pas assurée
  3. une logique “pull”, le nœud demande au client ce dont il a besoin versus une logique “push”, le client donne au serveur ce dont il pense que le serveur aura besoin

On peut réconcilier les points 1 et 2 en introduisant un type CID dans GraphQL qui soit connecté à un backend IPFS (Hasura data connector). J’avais déjà noté cette idée sur un coin de papier mais n’avais pas poursuivi puisque je ne voyais pas encore l’intérêt.

Reste le conflit “push/pull” typique de l’opposition p2p/client-serveur. Un bon compromis consiste à pousser toute la donnée mais pas les liens s’ils ne sont pas modifier et à pousser également les liens s’ils sont modifiés.


Si on résume ce que ça donne concrètement, pour modifier une donnée existante l’application client :

  1. calcule le CID de la donnée avant modification (oldDataCID)
  2. calcule le CID de la donnée après modification (newDataCID)
  3. signe un payload de demande d’indexation de la nouvelle donnée (prefix, time, kind, newDataCID)
  4. soumet la mutation GraphQL avec
    • un champ "indexRequest" contenant les infos de la demande d’indexation (pubkey, time, kind, dataCID, signature)
    • un champ "previous" qui contient oldDataCID
    • un champ "blocks" qui contient une map (cid => block) des blocs IPFS encodés en base64 éventuellement modifiés (par exemple si on a modifié une image, elle sera fournie là, sinon il n’y a rien à modifier)

à la réception de la mutation, le datapod :

  1. récupère la donnée oldDataCID (normalement il l’a déjà)
  2. applique la mutation GraphQL dessus, il obtient newData
  3. vérifie que son newDataCID local correspond au newDataCID de la demande d’indexation (sinon ça veut dire que la mutation ne permet pas de retrouver la nouvelle donnée, il renvoie une erreur)
  4. ajoute la nouvelle donnée à son nœud IPFS
  5. crée une nouvelle demande d’indexation à partir des infos fournies et en publie le cid sur pubsub, la suite est gérée par le fonctionnement habituel

Avec ce fonctionnement, la mutation ne peut pas retourner la donnée modifiée (why-mutations-return-data), mais ça n’aurait pas tellement de sens puisque la nouvelle donnée doit impérativement être newData.

Ça me semble un peu trop complexe (en particulier l’étape “applique la mutation GraphQL”) par rapport à ce que ça apporte. Les mutations GraphQL sont conçues pour opérer sur de la donnée mutable, or dans le cas de IPFS il s’agit de donnée immutable, ce qui est par ailleurs nécessaire si on veut conserver l’intégrité de la signature.

Par rapport au point

utiliser des API standard type Graphql

ça n’est pas très utile d’utiliser un standard en dehors du cadre pour lequel il a été conçu. Il y a peut-être moyen de tordre davantage notre cas d’usage pour le faire rentrer dedans mais pour l’instant je ne vois pas.

Par contre, je vois un moyen de me rapprocher davantage de ce que faisaient les datapods cesium+. On peut abandonner IPLD et repartir sur des documents json bruts qui contiennent eux-même les champs de la demande d’indexation : pubkey, time, kind (version), dataCID (hash), signature, au détail près que le champ avatar serait un CID plutôt que l’image elle-même au format base64. Si on veut uploader une nouvelle image, il faut la rendre accessible sur IPFS (par exemple en la fournissant au datapod).
L’inconvénient de ce format est que retrouver le document original avant signature (_source) n’est pas trivial, il faut une regexp qui retire le hash et la signature. Alors que comme dag-json :

  • Sort object keys by their (UTF-8) encoded representation, i.e. with byte comparisons
  • Strip whitespace

Cette représentation est déterministe, et si on retire le champ signature, on retombe sur le document initial.


Bref, j’ai l’impression que je tourne un peu en rond dans la réflexion. Je vais continuer un peu plus loin pour y voir plus clair.