Les utilisateurs ont des données: nom, commentaire de transactions, profil, messages privées, annonces, commentaires d’annonces, etc
L’architecture actuelle stocke une partie en blockhain, une partie sur des pod Cs+, une autre encore sur des pod gchange, etc
Cette architecture a le mérite d’exister, et mon propos ici n’est pas de critiquer ceux qui ont consacré énormément de temps à la mettre en place, ils ont fait ce qu’il pouvait pour répondre aux besoins présents.
Outre les problèmes de synchronisation des pod Cs+ ou gchange entre eux, le fonctionnement actuel est basé sur la bonne volonté de certains d’héberger un pod qui va stocker les données de tout les utilisateurs. Ce qui fonctionne, car on a encore peu de données, mais si on veut que la Ğ1 puisse monter en charge, je pense que l’on doit repenser entièrement la manière de stocker les données des utilisateurs.
Dès qu’il y a beaucoup d’utilisateurs (disons plusieurs millions), le stockage sécurisé des données (donc avec suffisamment de backup pour éviter les pertes) deviens nécessairement coûteux.
Et donc seuls les gros acteurs ont les moyens de stocker les données, ce qui induit une centralisation de fait.
Depuis quelque temps je réfléchis à un système de stockage qui :
- Sois générique: permette de stocker tout type de donnée pour tout usage.
- Soit décentralisé: participer au stockage des données doit être accessible avec des moyens raisonnables, même avec une quantité astronomique de données.
- Soit bien synchronisé: tous les acteurs doivent se mettre d’accord sur la version des données
- Sois vérifiable: l’authenticité d’une donnée fournie par un serveur doit être rapidement vérifiable.
- Sois résistant aux acteurs malveillants: il ne faut pas qu’un acteur propose d’héberger des données des utilisateurs mais ne les héberge en fait pas, où ne fournissent jamais les données quand on lui demande.
J’ai une proposition qui me semble satisfaire tous ces points, je n’ai pas tous les détails, mais j’ai déjà une idée relativement précise, voici le concept général:
Il s’agit d’un système de stockage clé/valeur.
Chaque acteur qui souhaite stocker une partie des données des utilisateurs doit s’inscrire en blockchain, un hash aléatoire lui est attribué, qui déterminera le milieu de la plage des hash de clés qu’il devra stocker.
Format de la clé
La clé doit être d’une longueur comprise entre 4 et 223 octets, et elle doit être préfixée par l’identifiant d’un protocole, l’identifiant du protocole peut être toute suite d’octet d’au moins 4 octets, y compris un nom encodé en ascii.
Ce n’est pas la clé en elle-même qui permettra de retrouver la valeur mais son hash. Avant de hasher la clé, il faut la préfixer avec la clé publique du propriétaire (en représentation binaire). Ce qui donne:
key_hash = blake2b-256( <protocol_id> || <protocol_key_data> || <owner_pubkey>)
Le format de protocol_key_data
doit être défini séparément pour chaque protocole de donnée. Duniter-v2s n’interprète pas ces données, il se contente de les hasher et de les publier tel quel dans un évènement, la seule contrainte est que le nombre d’octets totaux occupés par <protocol_id> || <protocol_key_data>
soit inférieur ou égal à 223.
Ajout/Modification/Supression d’une entrée (=paire clé/valeur)
Lorsqu’un utilisateur souhaite ajouter/modification/supprimer une entrée, il fournit à un data-pod (via une mutation GraphQL exposée par l’instance hasura associée) deux éléments:
- Les octets de la nouvelle valeur ou null si c’est une supression
- Une payload signée SCALE encodée qui contient dans l’ordre:
- le hash du genesis
- le data nonce
- la clé (min 4 octets, max 223 octets)
- le hash Sha256d de la valeur ou le hash zeroed si c’est une suppression (32 octets)
- la taille de la valeur (en octet)
Le data-pod va alors vérifier si l’utilisateur a le droit de stocker cette donnée (s’il a des quotas suffisant ou de quoi payer des frais), et si tout va bien, il va ajouter cette “écriture d’entrée” dans sa pool.
Dès qu’il peut, le data-pod va soumettre à la blockchain un extrinsic duniterData.bloc()
qui contiendra l’entièreté des payload signées dans sa pool, ainsi que la merkle proof contenant tout les hash intermédiaires permettant de calculer le merkle root actuel et le nouveau.
La blockchain va alors vérifier, pour chaque payload signée, l’utilisateur a le droit de stocker cette donnée (s’il a des quotas suffisant ou de quoi payer des frais), et si tout va bien, elle va émettre un évènement EntryUpserted
ou EntryRemoved
si la nouvelle valeur est None.
Tout les data-pod actifs suive la blockchain bloc après bloc, ils vont donc voir passer cet extrinsic et savoir quels sont les changements de donnée, et par qui ils ont été soumis, ils savent donc quels data-pod contacter pour récupérer les données qu’ils doivent stocker. Les endpoint de tout les data-pod actifs étant en blockchain, ainsi que leur «position» dans la plage des clés, chaque data-pod sait qui contacter quand, donc pas besoin de découverte par le réseau, on profite ici à fond du consensus glabal apporté par la blockchain.
Les indexeurs voient également passer tout les extrinsics et évènements, ils peuvent donc savoir selon le protocole id (qui est dans le préfixe de la clé) s’il s’agit d’une donnée qu’ils doivent indexer pour de la recherche (y compris recherche full text). Et s’ils doivent l’indexer, ils vont requeter les data-pod qui sont censés stocker cette donnée pour la récupérée et l’indexée.
Recherche d’une entrée
Un utilisateur souhaite par exemple faire une recherche full text dans les profils des utilisateurs.
Il va requeter une API hasura, qui va lui fournir les résultats de la recherche.
Si l’application de l’utilisateur souhaite s’assurer de l’authenticité des profils, il lui suffit de:
- Demander la merkle proof de chaque profil dans le résultat de la recherche (un champ graphQL optionel)
- Demander via l’API RPC (à son light node local ou à un full node considéré de confiance) le merkle root
- Vérifier chaque merkle proof
- Pour chaque profil dont la merkle proof est valide, l’utilisateur à la garantie que le profil est authentique, et qu’il s’agit bien de la dernière version du profil, il n’a pas besoin de truster l’instance hasura requetée. Si certain profil ont une merkle proof invalide (root hash qui ne correspond pas), afficher une alerte dans L’UX du style «Attention: l’authenticité de ce profil n’a pas pu être vérifiée !».
Obtention directe d’une entrée dont la clé est connue
C’est comme la recherche d’une entrée, sauf que la requete GraphQl sera un peu différente, et que Hasura n’ira pas chercher l’indexeur derrière mais uniquement le data-pod auquel elle est liée.
La valeur peut être vérifiée de la même façon.
Frais
Ici il y a 4 choses différentes qui ont un «coût», et chacune peut être gérée différemment si besoin.
1. le coût l’exécution des extrinsics
Ce coût n’a rien de spécifique au système proposé ici, ces extrinsics doivent être étalonnés comme les autres, c’est un problème plus global à toute la blockchain traitée dans un autre sujet: Comment partager équitablement cette ressource commune qu'est la blockchain Ğ1?
2. Le coût du storage onchain
Le onchain storage stockera:
- le merkle root d’un arbre dont les feuilles sont les hashs des entrées (32 octets)
- Pour chaque utilisateur: son data nonce (4octets), la taille totale de ses données(4 octets), et son dépôt total (8 octets).
L’idée est d’avoir un dépôt minimal par utilisateur pour qu’il paye les 64 octets nécessaires dans le storage on-chain (48 octets de préfixe + 16 octets de données).
3. Le coût du stockage par les full nodes inscrits
La blockchain sait exactement à tout bloc quelle quantité précise de donnée (en nombre d’octets) chaque acteur inscrit est censé stocker.
Il est donc possible de mettre en place un mécanisme de rémunération en fonction de la quantité de données stockées et de la durée de stockage.
Cette rémunération peut être financée par une trésorerie commune, ou par des frais payés directement par l’utilisateur, ou tout autre mécanisme de financement à imaginer.
4. Le coût du stockage par les indexeurs
Les indexeurs ne devrait stocker que les données nécessaires et suffisantes pour réaliser des recherches. Par exemple, les indexeur ne devrait pas stocker des avatars, mais seulement la clé, la valeur pouvant être demandées au data-pod.
Pour les indexeur il est également possible d’envisager un système d’inscription en blockchain, et donc une rémunération de ces derniers.
Refus de fournir une donnée
Si un data-pod refuse de fournir une donnée qu’il est censé fournir, il nous faut un mécanisme de rapport en blockchain, a minima pour stopper sa rémunération, et attribuer sa plage de hashs à d’autre.
Il y a juste à définir le seuil de déclenchement (en nombre ou proportion de membre certifiés), et la durée de vie d’un rapport (on ne va peut-être pas comptabiliser un rapport qui date de 3 ans).
Où peut également envisager un mécanisme de rapport (ou de vote), pour les indexeurs, mais il est plus délicat de montrer qu’un indexeur ne nous fournit pas les résultats qu’il est censé nous fournir, sauf à comparer avec un autre indexeur identique.
Pour ces questions de rapport ou de vote on a plus de temps pour y réfléchir et choisir les mécanismes, tant que les acteurs devant stocker les données sont inscrits en blockchain, ça permet de mettre en place plus tard des mécanismes de rémunération et de sanction.
Preuve de stockage d’une donnée
J’ai fait des recherches sur les preuves zk-SNARK, et j’ai expérimenté avec une implémentation en Rust.
Il est possible de demander au data-pod inscrits de publier régulièrement une preuve en blockchain qu’il stocke bien tel ou tel entrée (choisi aléatoirement pas la blockchain).
Ça forcerait les data-pod à bien stocker ce qu’ils sont censé stocker, mais ça ne les force pas à fournir ces données aux utilisateurs, donc il faut quand même un système de rapport ou de vote pour les full nodes inscrits.
Bref, juste pour signaler que c’est un mécanisme possible si on en a besoin un jour, mais on en aura peut-être jamais besoin.
Liste potentielle de protocoles
- username
- profile
- avatar
- private_message
- advert
- advert_comment
Autant qu’on veut en fait.
Exemple pour un message privé
Le protocole private_message
peut spécifier:
Format de clé: twox128("private_message") || blake2-128(receiver_pubkey) || timestamp || issuer_pubkey
.
Ainsi, lorsque l’application finale de l’utilisateur veut récupérer ces nouveaux messages reçus, elle peut le faire via un indexeur, qui aura pu indexer les messages graçe aux metadata darns la clé publiée en blockchain.
Exemple pour un profil une annonce de vente d’un bien ou service
Le protocole advert
peut spécifier:
Format de clé: twox128("advert") || blake2-128(issuer_pubkey) || twox64(category) || timestamp)
.
Je fais une recherche full text auprès d’une instance hasura, qui me retourne alors un highlight
pour chaque annonce qui match ma recherche.
Si j’ai demandé également le champ merkle_proof
, je peux vérifier en tache de fond chaque preuve.
En termes d’UX ça permet d’afficher quand même tous les résultats de suite, mais avec une icône indiquant que l’annonce est en cours de vérification.
Si l’utilisateur clique sur l’une des annonces pour en voir le détail, alors là je peux recalculer le hash de l’annonce et vérifier définitivement son authenticité. Ce qui permet d’ajouter une icône check vert.
Si jamais l’authenticité de l’annonce n’est pas vérifiée, il faut afficher un warning à l’utilisateur du genre “Erreur lors de la vérification de l’annonce , le serveur distant est désynchronisé ou malveillant”.
À noter que le champ merkle_proof
, peut valoir null
, ce sera le cas lorsqu’une modification récente n’a pas encore pu être enregistrée définitivement en blockchain. L’application pourra préciser dans sa recherche si elle veut uniquement les données définitivement validées, ou si elle veut les données les plus récentes (avec l’inconvénient de ne pas pouvoir les vérifier).
Conclusion
Il s’agit d’un système de stockage ambitieux mais dont l’effort de développement me semble valoir le coût, car il permet de potentiellement tout vérifier (mais ce n’est pas obligatoire), et de traiter avec le même code tout type de donnée.