L'importance du "local first" dans la recherche du "fork social"

Quand j’ai commencé à travailler sur mon “datapod v2”, le “local first” ne faisait pas partie de mes contraintes, je voulais d’abord explorer les problématiques liées à la modération décentralisée et savoir s’il était faisable de construire un index décentralisé sans consensus.

Dans ma première tentative sur les datapods (Hugo Trentesaux / Duniter Datapod · GitLab), je n’utilisais IPFS que pour collectionner les “faits” (c’est-à-dire les données soumises au système) dans un corpus. L’index était reconstruit de zéro pour chaque nœud dans une base Postgres exposant un endpoint Graphql. Cela venait avec plusieurs inconvénients :

  • synchronisation entre le corpus de faits et l’index délicate (notamment en cas de bugs cassant la transactionnalité)
  • solution monolithique inadaptée à un fonctionnement p2p (difficile d’embarquer postgres dans une app)
  • nécessité de découverte réseau et problématique de confiance dans un nœud distant

Ma première “trouvaille” en travaillant sur mon “datapod v2” était qu’il était assez facile de construire un index en utilisant les briques de la donnée décentralisée (ici Iroh plutôt que IPFS mais le principe est le même), et donc de requêter directement la couche de données sans dépendre d’un index secondaire. De plus, la couche d’interprétation SQL implémentée par Claude sur base GlueSQL s’est révélée assez performante. Concrètement, il s’agit simplement d’un Prolly Tree combiné à un choix adapté des clés de stockage clé/valeur.

L’index étant simplement dérivé des faits selon un set de règles communes, deux acteurs qui collectent les mêmes faits peuvent voir leur index converger vers une valeur commune pour peu que ces règles soient indépendantes de l’ordre de réception. Cette contrainte peut paraître anodine, mais elle ne l’est pas. C’est elle qui fait apparaître le plus clairement la différence entre un système avec consensus et sans consensus. La solution “simple” est d’introduire un consensus sous une forme ou une autre, mais j’étais guidé par l’intuition qu’il ne fallait pas introduire de consensus sous peine de réduire la décentralisation du système, j’ai donc continué à réfléchir.

Le consensus au sens algorithmique est ce qui nous permet de trancher entre deux versions des faits. C’est pratique de pouvoir trancher les cas liés à une divergence technique, mais quand cette divergence est humaine, mon avis est qu’il faut laisser les humains décider plutôt que l’algorithme. En l’absence de consensus on se retrouve avec des forks, et il faut savoir si ce fork est d’origine technique ou humaine. L’aspect technique peut être résolu avec des CRDT par exemple, mais les désaccords humains doivent pouvoir vivre leur vie. Je parle alors de “fork social”.

Dans les systèmes cryptographiques, le premier “fork social” est celui de l’identité. En effet, chaque acteur du réseau a un identité déterminée par sa clé cryptographique, chaque donnée du corpus doit être signé par une clé, et on doit décider si l’on accepte cette donnée dans notre corpus (clé de confiance) ou si on la refuse (bot, spammeur…). Accepter toutes les clés n’est pas envisageable, il y en a plus de 2252 pour ed25519, soit de quoi dépasser les capacités de n’importe quel système informatique. Un acteur qui génère une clé localement (donc tire un nombre aléatoire) crée donc nécessairement un “fork social” entre :

  • sa version du monde dans laquelle sa clé est légitime
  • la version du monde d’un autre qui ne reconnaît pas encore sa clé comme légitime

Ce cas d’usage m’a guidé vers le “local first” comme solution naturelle pour exprimer le fork social. Il me fallait construire un système qui permette de fonctionner complètement même avant que cette clé ne soit reconnue comme légitime par le réseau. Dans mon prototype “Ğeopod” (Ğeopod : mon approche p2p pour les datapods et gchange), l’appli est donc entièrement utilisable hors ligne avec une clé créée localement. Vous pouvez publier des annonces, rechercher parmi ces annonces, liker, commenter… Évidemment ce n’est pas très intéressant en local uniquement, ça le devient plus quand vous vous connectez en réseau local à quelqu’un d’autre qui a créé sa clé et que vous reconnaissez mutuellement vos clés. Vous pouvez alors commencer à explorer les annonces l’un de l’autre. Et ainsi de suite de proche en proche.

Voilà ma conclusion : le “local first” qui me paraissait être une propriété chouette mais non directement liée à la modération décentralisée se trouve en fait être une solution très élégante au problème du fork social. Voilà pourquoi j’y tiens tant, au moins d’un point de vue théorique. C’est la raison principale pour laquelle je m’y intéresse autant, même au vu de la difficulté additionnelle que cela apporte.


Au delà de cet intérêt théorique, une approche “local first” a d’autres avantages :

  • Il existe une grande communauté qui s’intéresse à ça, dont https://lofi.so/ (LOcal FIrst SOftware).
  • L’expérience utilisateur hors ligne est forcément meilleure qu’un système dépendant de la connexion, c’est le niveau ultime du “cache”.
  • Les performances sont généralement meilleures, car une fois la donnée en local, la requête est plus rapide (d’où les “cache”)
  • Le coût d’exécution des requêtes est porté localement plutôt que par un acteur distant qui doit se protéger contre les abus (quotas…).
  • La sauvegarde des données est native, pas de 404, une donnée supprimée sur le réseau reste disponible et exploitable là où elle a été lue.

et des inconvénients :

  • Des applis plus lourdes que si elles déportaient la charge sur un serveur (stockage, CPU…).
  • Moins de mutualisation des calculs les plus lourds sur des machines spécialisées.

Mais je préfère quand même avoir une approche “local first” qui est améliorée quand une ressource externe est disponible qu’une approche “server first” qui souffre de fallback en cas d’indisponibilité.


PS : Il faut que je relise sur Nextgraph mentionné ici

Je te remercie de prendre finalement le temps de développer pourquoi tu tiens tant au local-first. Cela m’aide à mieux comprendre pourquoi, mais je ne suis toujours pas convaincu par tes arguments. La plupart des avantages que tu vois au local-first peuvent déjà être obtenus sans, et certains avantages n’en sont pas vraiment en situation réelle.

Je partage l’objectif du « fork social », mais Dunipod résout déjà ce problème avec une séparation plus simple entre les faits, leur réplication et leur interprétation.

Dans Dunipod, le réseau n’essaie pas d’obtenir un consensus global sur une vue unique du monde. Il fait converger les pods vers un corpus commun de documents signés et immuables. Chaque document possède un identifiant dérivé du hash de sa représentation canonique.

La couche P2P réconcilie ensuite les ensembles de documents entre pods. Elle utilise RIBLT pour découvrir et transférer les différences, jusqu’à ce que les pods convergent vers l’union de leurs documents. Il n’existe pas de suppression au niveau de cet ensemble : lorsqu’un document devient obsolète, est effacé ou rejeté par l’indexation, son identifiant reste connu, éventuellement sous forme de tombstone accompagné de sa preuve.

Chaque pod applique ensuite ses propres règles d’admission, d’indexation et de modération à ce corpus. Avec le même corpus et les mêmes règles, deux pods construisent la même vue grâce à des règles déterministes et indépendantes de l’ordre de réception. Par exemple, les versions concurrentes d’un même auteur sont départagées selon un ordre canonique fondé sur l’epoch et le doc_id, tandis que les décisions qui nécessitent une référence objective utilisent l’état on-chain finalisé. En revanche, deux pods peuvent choisir des modérateurs, des règles ou des packages différents et produire volontairement des vues différentes.

C’est précisément là que vit le fork social dans Dunipod : non pas dans l’obligation de maintenir un index complet sur chaque appareil, mais dans la liberté de choisir le pod et les règles qui interprètent les faits. Une personne peut créer une clé, signer et soumettre immédiatement ses documents à son propre pod. Elle n’a pas besoin d’être préalablement reconnue par une autorité globale. Les autres pods peuvent répliquer ces documents tout en décidant localement de les indexer, de les rendre visibles ou de les classer différemment.

Autrement dit, le fork social est essentiellement une propriété du modèle de confiance et de projection, pas nécessairement du lieu où s’exécute la base de données. Le rendre possible ne nécessite pas d’embarquer toute la couche de stockage et de requête dans chaque application.

D’ailleurs, le problème posé par la création libre de clés n’est pas réellement la taille mathématique de l’espace Ed25519. Le problème concret est celui des attaques Sybil : un acteur peut créer à faible coût un grand nombre de clés et publier un grand volume de données. Ce problème existe aussi bien dans un système local-first que dans un réseau de pods et doit être traité par des quotas, des limites de ressources et des politiques d’admission.

Je nuancerais également certains autres avantages que tu attribue au local-first.

L’expérience hors ligne peut effectivement être excellente pour les données déjà présentes localement, les brouillons ou un petit corpus. Mais ce modèle ne passe pas à l’échelle pour une recherche exhaustive dans un corpus important : il faudrait d’abord télécharger, stocker et indexer une quantité potentiellement considérable de données sur chaque appareil. À partir d’un certain volume, l’utilisateur n’a plus réellement toutes les données en local ; il doit sélectionner un sous-ensemble, et on retrouve alors les limites ordinaires d’un cache.

De même, une requête locale n’est pas automatiquement plus performante. Elle évite la latence réseau, mais transfère le stockage, l’indexation et le calcul vers la machine de l’utilisateur, qui sera souvent moins puissante et moins bien optimisée qu’un serveur spécialisé. Un index serveur correctement conçu répond normalement en moins d’une seconde, hors latence réseau.

Le déplacement du coût des requêtes vers le client ne supprime pas non plus le problème des abus. La couche P2P doit malgré tout se protéger contre les identités Sybil, les volumes excessifs, les documents ou blobs surdimensionnés, les connexions lentes, les demandes coûteuses et l’épuisement du disque. J’ai précisément travaillé sur ces protections dans Dunipod, et elles sont loin d’être triviales. Le local-first déplace une partie du coût, mais ne dispense pas de quotas ni de mécanismes d’admission.

Enfin, il existe un coût de complexité applicative important. Notre écosystème utilise plusieurs stacks : JavaScript/TypeScript, Flutter/Dart, Python, etc. Pour que Ğeopod soit réellement accessible à toutes les app, il faudrait fournir, maintenir, sécuriser et tester un client complet (stockage, réplication, indexation et moteur de requêtes) pour chacune de ces stacks, ou imposer une technologie particulière aux applications. Cela augmente fortement le coût d’intégration et limite les applications capables d’utiliser la couche off-chain.

Dunipod place cette complexité dans des services interopérables exposant des API HTTP et GraphQL ordinaires. N’importe quelle application peut ainsi l’utiliser avec son langage et son architecture actuels, tout en conservant la possibilité de choisir son pod, d’en exploiter plusieurs ou d’héberger le sien.

En conclusion, je comprends que le modèle local-first puisse paraître plus élégant et plus décentralisé sur le papier, mais je maintiens que ce n’est pas le cas en pratique. Le modèle de confiance reste le même, et cela apporte d’autres problèmes qui sont évitables avec d’autres approches, sans avoir besoin de perdre les avantages qui comptent vraiment.

Une question, même si je suis loin de tout comprendre.

Si côté client il y avait une approche hybride?

C’est à dire que la personne qui a des dizaines de Go pour indexer peut le faire, en choisissant le volume maxi souhaité, la personne qui a le téléphone vieux plein et pas mis à jour a le réglage zéro données (ça sera le minimum c’est à dire ses données?) et son appli cherchera d’abord à se connecter en bluetooth ou wifi façon briar chez les copains d’à-côté, puis sur internet (ça facilite le partage de connexion et privilégie le local) chez la personne ayant de l’indexation…?

Je fais une sorte de soupe entre tox briar et yacy, je sais

Oui, c’est proche de ce que propose déjà Dunipod. Une personne disposant d’espace disque peut héberger un pod et définir le volume maximal qu’elle accepte de consacrer aux données. Dunipod fait respecter ce quota en privilégiant les données récentes. À l’inverse, un téléphone limité peut ne conserver localement que le strict nécessaire et interroger d’abord un pod accessible sur le réseau local, puis un pod distant.

Il faut toutefois préciser que Dunipod est agnostique des applications : il expose des API HTTP et GraphQL, mais chaque application reste responsable du stockage et de la consultation de ses propres données locales.

Sur un Wi-Fi local ou un partage de connexion, l’accès aux API d’un Dunipod est assez simple. Le Wi-Fi Direct pourrait aussi transporter HTTP, mais nécessiterait une intégration spécifique. En Bluetooth, les API mobiles fournissent surtout des flux de données et non une connexion TCP/IP standard : il faudrait donc développer une passerelle ou un protocole dédié plutôt que simplement exposer les API existantes.

Ces contraintes s’appliquent également à Ğeopod. Elles sont même plus problématiques pour lui : iOS suspend les applications en arrière-plan et ne permet pas de faire fonctionner durablement un un nœud P2P. Ğeopod n’est donc pas réellement utilisable sur iPhone, ce qui exclut une partie des utilisateurs. Dunipod contourne cette limite en faisant tourner le pod sur une machine adaptée, tandis que l’application iOS reste un client léger pouvant malgré tout conserver certaines données localement.

J’ai l’impression que tu as beaucoup utilisé l’IA pour rédiger ta réponse. Je trouve que ça nuit à la compréhension de tes idées. D’autre part, c’est proscrit sur ce forum : un message rédigé par IA même partiellement doit être signalé comme tel.

J’ai quand même fait abstraction de la forme pour m’intéresser au fond. Merci de détailler ta vision du local first, ça me permet de me mieux comprendre ta vision pour Dunipod.

Local first or local later maybe?

Première remarque tout d’abord : le local first est une expérimentation qu’il me plaît de mener, et il me semble plus facile de concevoir un système local first d’abord puis de l’exposer sous forme de passerelle graphql ou autre (le serveur est alors simplement un pod du réseau avec ses propres quotas et règles) que l’inverse. Même si tu affirmes que c’est théoriquement possible de faire dans ce sens :

ça me paraît plus complexe et moins élégant. J’ai peur qu’on finisse par ne pas le faire (comme des clients avec un duniter light node), ou qu’on se retrouve avec un système pas tout à fait adapté au local first. Ma philosophie est plutôt “local first, server then” que “light client first, full p2p eventually”.

Corpus commun ou scission possible ?

Existe-t-il un mécanisme de filtre par clé publique par exemple ? Comment Dunipod se protège-t-il contre quelqu’un qui créerait d’immenses quantités de documents à partir d’une immense quantité de clés différentes ? À partir du moment où il est possible d’exclure une clé, je ne vois pas comment on peut garantir la convergence. Par exemple si Fred publie des milliards de messages sur des milliards de clés différentes avec Astroport, comment Dunipod peut-il garder une référence sur tous ces documents ?

Un ensemble qui ne fait que grandir c’est justement ce que je souhaite éviter. Je veux qu’il soit possible de supprimer en masse des documents signés du corpus sur un critère simple comme la clé publique par exemple. Je me doute que tu as pensé à ça, mais je n’arrive pas à concevoir comment si le corpus synchronise le diff des documents signés sans critère sur la clé.

La seule chose que j’arrive à imaginer, c’est si on n’accepte la connexion p2p que des pairs dont on sait qu’ils pratiquent un quota au niveau de leur API HTTP de soumission. Si un pair forge un corpus gigantesque et qu’on se connecte à lui, le protocole forcera la synchronisation de ses documents signés et saturera notre quota disque en attendant que l’epoch soit passée. Donc il faut que le mécanisme d’appariement soit sur whitelist et pas ouvert. Dans l’idée ça me va.

Fonctionnement avec connexion intermittente

Je ne comprends pas pourquoi tu dis ça. Les connexions intermittentes, qu’elles soient liées au réseau ou à l’OS sont justement la cible. Quand l’appli retrouve une connexion réseau, elle synchronise l’index qui lui manque. La synchro peut être revalidante ou faire confiance au pair, ce qu’on préférera sur smartphone.