Options pour gérer les commentaires de transaction côté serveur

C’est à peu près ce que j’essaye d’exprimer oui, je vais le reformuler :

Je pars du principe (possiblement erroné, ça reste à confirmer) que, bien que toute écriture de donnée impacte le Storage (la BDD, en gros) et notamment le Merkle Root du bloc courant, toute écriture ne va pas forcément impacter le Merkle Root des blocs suivants.

Autrement dit : que quand un bloc est traité et accepté, il va construire son propre arbre de Merkle dont certaines branches vont “reprendre” tout ou partie des données de l’état au bloc précédent, mais que certaines autres branches seront purement et simplement écrasées.

Je prétends que les évènements font partie de ces données systématiquement écrasées en intégralité par chaque bloc, et que donc par définition ce type de donnée n’impacte pas les blocs suivants et que forcément, à l’élagage du bloc, la donnée disparaît et ne vient jamais “gonfler” la BDD contrairement à des données “métier” (les comptes, les transactions, etc.).

Voilà pourquoi les évènements me semblent une très bonne façon d’indexer les hash de commentaires.

Et c’est également pour cela que je dis que stocker ces hash comme une donnée “métier” viendrait au contraire créer une donnée stockée par tout nœud (archive ou pas) et créerait même une donnée à stocker de façon historisée pour un nœud archive (en gros = stocker tous les évènements de tous les blocs).

edit : tiens bah je suis allé vérifier sur la documentation Substrate, c’est bien comme ça que ça se passe :

This function places the event in the System pallet’s runtime storage for that block. At the beginning of a new block, the System pallet automatically removes all events that were stored from the previous block.

Source

2 Likes

Si on veut stocker un hash en blockchain en émettant un événement, on peut faire un wrapper en s’inspirant de system.remarkWithEvent.

1 Like

Tu as raison, je me suis trompé, les événements sont purgés au delà du nombre de bloc configuré par l’option purge (256 par défaut je crois), mais les opérations qui les déclenchent sont conservés (extrinsics) même si pas accessibles dans le storage vide des nœuds purgés.

Si l’on en crois la doc de Substrate, un nœud Full, contient tout ce qu’il faut pour reconstruire un nœud archive, ie. pour reconstruire tous les états du storage pour historique.

Although older blocks are discarded, full nodes retain all of the block headers from the genesis block to the most recent block to validate that the state is correct. Because the full node has access to all of the block headers, it can be used to rebuild the state of the entire blockchain by executing all of the blocks from the genesis block. Thus it requires much more computation to retrieve information about some previous state, and an archive should generally be used instead.

Source

Donc un extrinsic de création d’identité avec l’info du “pseudo”, est forcément stocké dans la BDD RocksDB du full node, ad vitam, pour pouvoir être rejoué par un nœud archive. Le pseudo est stocké aussi dans un événement qui est déclenché par l’extrinsic, mais purgé. Attention, ce n’est pas parce qu’on ne peut pas obtenir les extrinsics des blocs purgés via l’API qu’il ne sont plus en BDD ! (je crois que cela fait partie de la confusion).

A partir du moment où une info comme les “pseudos” et les futures “hash de commentaire” ou “commentaire” peut “réapparaître” dans un nœud archive qui execute tous les blocs d’un Full node depuis le genesis, alors ces infos sont forcément dans la BDD rocksDB de tous les nodes, dans les extrinsics à mon avis (puisque les événements sont purgés complètements).

Amha.

Oui, je suppose que le nœud full conserve les blocs avec body (donc avec les extrinsics dedans) tandis qu’un light node par exemple ne synchronise que le header de chaque bloc. Ce qui fait que ce dernier type de nœud est le seul à ne pas pouvoir reconstruire l’intégralité des données de la blockchain, tous les autres le peuvent.

Oui, dans les extrinsics.

Mais on ne peut pas les exploiter directement via un indexeur (d’après ce que j’ai entendu de @poka et Hugo) contrairement aux évènements qui sont d’ailleurs sa principale source de données.

1 Like

Techniquement rien ne l’empêche (sur un noeud archive), il suffit de parser les extrinsics en plus des événements bloc par bloc, mais on se retrouve avec tous les extrinsics échoués, ainsi que ceux provoquant des actions en blockchain hors extrinsic par effet domino (actions automatiques, même si on les évite au maximum), là où les événements sont conçus pour délivrer la bonne information à exploiter pour un client ou un indexer, une fois cette action réalisée avec succès.

Par exemple en regardant le bloc de confirmation d’identité de ChristCosmic via polkadotjs:

On voit que sont nom est présent dans l’événement de l’extrinsic, mais également en tant que idtyName dans l’extrinsic lui même, donc parsable par un indexer.

En voici la preuve ultime via un script écrit en 10 secondes par claude 3 (j’avais un doute):

script polkadot.js et son résultat

prompt:

écris moi un script nodejs qui utilise polkadot.js lib pour récupérer le contenu des extrinsics présents dans le bloc 69302 du noeud wss://gdev.p2p.legal/ws. Je veux idtyName.
avec screenshot polkadotjs fourni au dessus pour qu’il comprenne que je veux idtyName

const { ApiPromise, WsProvider } = require('@polkadot/api');
const { u8aToHex } = require('@polkadot/util');

async function main() {
  const wsProvider = new WsProvider('wss://gdev.p2p.legal/ws');
  const api = await ApiPromise.create({ provider: wsProvider });

  const blockHash = await api.rpc.chain.getBlockHash(69302);
  const signedBlock = await api.rpc.chain.getBlock(blockHash);

  signedBlock.block.extrinsics.forEach((extrinsic, index) => {
    console.log(`Extrinsic ${index}:`);

    const { method, section } = extrinsic.registry.findMetaCall(extrinsic.callIndex);
    console.log(`Pallete: ${section}`);
    console.log(`Fonction: ${method}`);

    if (section === 'identity' && method === 'confirmIdentity') {
      const idtyName = extrinsic.args[0].toUtf8();
      console.log(`idtyName: ${idtyName}`);

      const extrinsicHash = u8aToHex(extrinsic.data);
      console.log(`Hash de l'extrinsique: ${extrinsicHash}`);
    } else {
      console.log('Arguments:', extrinsic.args.map(arg => arg.toString()).join(', '));
    }

    console.log('------------------------');
  });

  await api.disconnect();
}

main().catch(console.error);

Résultat:

$ node main.js

Extrinsic 0:
Pallete: timestamp
Fonction: set
Arguments: 1707483204001


Extrinsic 1:
Pallete: identity
Fonction: confirmIdentity
idtyName: ChristCosmic
Hash de l’extrinsique: 0x30436872697374436f736d6963


Non d’après la source fourni, justement les noeuds full conserve les blocs avec body pour les 256 derniers blocs, mais ne conservent que les header pour l’intégralité des autres blocs depuis le genesis.

Pour reconstruire un noeud archive, il faut forcément récupérer le body des blocs depuis le début, pas juste les header, donc je ne voit pas comment ce serait possible à partir d’un noeud full (non archive), même en partant du genesis. C’est sur ce point que je flanche, mais on sort progressivement du sujet de ce topic.

Un light node ne garde que les header des quelques dernier blocs.

Mais tous les événements sont stockés en blockchain !
On ne va pas s’en sortir avec des terminologies aussi ambiguës :sweat_smile:

1 Like

Je ne comprends pas quel est le débat. Pour récapituler :

  • La blockchain est une suite de blocs.
  • Les blocs contiennent des extrinsics.
  • Les nœuds d’archive ont tous les blocs.
  • Les nœuds normaux ont seulement les derniers blocs.
  • Le runtime exécute du code pour chaque extrinsic, qui peut générer des événements et modifier l’état, de manière déterministe (fonction du bloc et de l’état).
  • Les événements ne sont pas stockés.

Donc :

  • Les extrinsics sont publics.
  • Les événements peuvent être retrouvés facilement à partir d’une archive (on prend un ancien état soit à partir d’une archive des états, soit en enchaînant les anciens blocs, et on exécute le bloc suivant).

Les événements ne sont là que pour aider les indexeurs et clients. Ils sont totalement décorrélés du droit à l’oubli.

Mettre les hashes de commentaires en extrinsics cause :

  • gestion peut-être un peu plus facile pour les indexeurs
  • gestion presque inchangée pour les clients, si les indexeurs font le lien tx->commentaire eux-mêmes
  • extrinsics un peu plus lourds (d’un facteur <100%)
  • aucun droit à l’oubli du hash du commentaire
  • garantie cryptographique qu’un commentaire est bien antérieur à l’extrinsic (sans besoin de se fier au filtrage de l’indexeur qui pourrait être corrompu et accepter des commentaires des jours après la transaction)

La garantie d’intégrité du commentaire (càd qu’il a bien été publié à la date de la transaction, et non modifié depuis) semble intéressante pour augmenter la sécurité de protocoles tels que ĞMixer ou la confiance de commerçants. Son (faible) impact sur la vie privée et sur le déni plausible peut être amoindri grâce aux méthodes cryptographiques que j’ai décrites ici.

Aucun de ces arguments pour ou contre n’étant très fort, le dernier point me semble le plus significatif et faire pencher la balance du côté “hash du commentaire en blockchain”.

1 Like

A mon avis non, (si le body est un état du Storage) seulement les headers et données nécessaires à l’exécution des blocs (qu’on peut appeler Body si ce n’est pas un état du Storage). :wink:

Pas exactement, il sont créer lors de l’exécution d’un bloc de Full node et stockés dans les états du storage des blocs des nœuds Archive (pas simple hein :rofl:)

Essayez de me suivre (je ne dis pas que j’ai raison, mais mon petit doigt me dit…) :

J’identifie 3 types de données :

  • États du Storage - Données modifiées par la (STF ?) qui donne un État du storage au bloc N (tous les états en Archive node, les quelques derniers blocs en Full node).
  • Immuables - Données immuables blockchain Full node/Archive node, nécessaires à l’exécution d’un bloc (exemple : les extrinsics).
  • Volatiles - Données volatiles issues de l’exécution d’un bloc, uniquement dans node Archive (on peut dire incluses dans les États du storage ?, exemple : les événements).

Dans un Full node, on a :

  • Les Headers des blocs qui comme le dis la doc servent à un nœud full node pour exécuter à nouveau les blocs pour reconstruire un node archive.
  • Les données immuables nécessaires à l’exécution des blocs (les headers ne suffisent pas à l’exécution des blocs même s’ils y participent)
    Malheureusement, même si les données immuables, comme les extrinsics existent en DB, l’API RPC n’y donnent pas accès sur un Full node.

Dans un Node Archive :

  • Les Headers

  • Les données immuables (extrinsics).

  • Les États du storage.

  • Les données volatiles (évènements).

    Ce qui est perturbant c’est que certaines données immuables comme les extrinsics et les données volatiles se retrouve dans les États du Storage du point de vue des requêtes de l’API.

C’est ce que je crois comprendre du peu que je connais du système.

On déplacera cette discussion ailleurs plus tard, mais c’est intéressant donc je me permet de répondre sur ce sujet.

Moi je ne suis pas en débat j’essai juste de comprendre un peu mieux comment fonctionne le storage substrate.

Mais au delà de ça, il semble y avoir divergence d’opinion entre toi qui préfère stocker les hash de commentaire en blockchain, et cgeek qui préfère simplement les voir figurer en événement. Chaque proposition est argumenté, je laisse les personnes intelligentes trancher sur cette conception. Moi je retourne jouer avec mes prouts.

Je pense qu’il y a confusion ici, Substrate n’identifie pas de donnée “volatiles”. Il y a simplement des données mutables (le runtime storage) et des données imutables (inscrits en bloc).

Là où je pense avoir fait une erreur précédemment, c’est que les événements ne sont pas présents en bloc, mais présent dans le runtime storage. Ce que je comprends mieux en relisant la doc, c’est que les noeuds archives ne contiennent pas juste le body de chaque bloc, mais également l’état du storage de chaque bloc. D’où la présence des événements.

En fait ce que je trouve perturbant c’est que dans ma conception du réseau duniter, les noeuds archives sont des éléments indispensables au bon fonctionnement de ce réseau, car nécessaires au fonctionnement des indexer.

Hors toutes les explications de @cgeek concernant les branches de merkle tree effacés à chaque bloc ne sont pas vrai pour les nœuds archives, qui eux gardent tout, même l’état du storage pour chaque bloc (reprenez moi si je me trompe svp sinon on va pas s’en sortir ^^).
Je trouve la doc Substrate très pauvre sur ces sujets, j’ai beaucoup de mal à trouver des informations détaillés sur ces sujets.

Je ne comprends toujours pas comment un nœud archive peut être reconstitué à partir d’un full node, sachant que ce dernier de contient que les header de blocs, ni le body, ni les états de storage intermédiaires.

La source cité dans le message de vit stipule que les headers permettent de reconstruire l’état de l’ensemble de la blockchain.

Hors :

In Substrate, a block consists of a header and an array of transactions. The header contains the following properties:

  • Block height
  • Parent hash
  • Transaction root
  • State root
  • Digest

Source

Ca fait longtemps que je pose cette question et que je n’ai pas de réponse, je pense que ce serait bien qu’on comprenne ce point.
Si on élimine tous les nœuds archives du réseau, peut on réellement reconstituer un nouveau noeud archive ? Comment ?

Non il n’y a pas que le header, il y a bien le body avec (les transactions). Le body n’est pas un état du storage, c’est juste le contenu brut du bloc.

C’est pour ça que l’on n’a pas absolument besoin des noeuds archive.

1 Like

Pour générer un événement contenant un hash il faut bien donner ce hash au forgeron. Les deux moyens sont les extrinsics et les intrinsics, qui sont forcément stockés dans le bloc. Si le hash n’est pas dans un bloc alors on passe par un mécanisme offchain et on perd la garantie forte d’antériorité.

Chaque nœud recevant le bloc forgé doit pouvoir l’exécuter et obtenir exactement les mêmes état du storage et événements(*). Si le hash est dans le bloc c’est simple. Sinon il faudrait le propager de nœud en nœud avec un autre protocole… et ça s’appelle IPFS.

(*) Quand je dis que c’est déterministe, c’est qu’un couple (bloc, état) est associé de manière unique, déterministe, reproductible, à un état qui est l’état suivant. Ça ne dépend de rien d’autre, c’est en partie pour ça que le runtime est isolé et ne peut faire d’appels système : c’est une fonction au sens mathématique.

1 Like

Pour essayer de se mettre d’accord sur le vocabulaire, on peut reprendre cette définition donnée par Eloïs : Storage onchain.

Donc une donnée “onchain” ou comme je l’ai appelée dans ce fil “stockage blockchain” est une donnée réutilisée par la STF. Les évènements n’en font pas partie. Et je ne vois aucun intérêt à y faire figurer les hash de transaction non plus, dans la mesure où les évènements suffisent à porter l’information jusqu’aux indexeurs.

edit : et pour la complétude :

C’est ce qui s’appelle du stockage onblock. Voir ce message très complet d’Eloïs qui distingue stockage onchain, onblock, offchain.

3 Likes

Je pense que c’est la meilleure solution. Un peu comme un batch qui peut contenir plusieurs calls, on aurait un “commentedTx” qui wrappe un call en l’accompagnant d’un commentaire libre. Un peu comme la pallet “remark”, mais qui permet de grouper la remarque et le call.

[edit]
Voici un code produit par chatGPT qui montre à quoi cela pourrait ressembler :

#[pallet::call]
impl<T: Config> Pallet<T> {
    #[pallet::weight(10_000)]
    pub fn add_comment(
        origin: OriginFor<T>,
        call: Box<<T as Config>::Call>,
        comment: Vec<u8>,
    ) -> DispatchResult {
        let who = ensure_signed(origin)?;

        // Wrap the call
        let call_hash = T::Hashing::hash_of(&call);
        
        // Store the comment
        <Comments<T>>::insert(call_hash, comment.clone());

        // Dispatch the call
        call.dispatch(origin)?;

        // Emit an event
        Self::deposit_event(Event::CommentStored(who, call_hash, comment));

        Ok(())
    }
}

Pour ce qui est du call_hash et du storage, c’est clairement en trop, il est un peu bête là dessus ><

Donc plusieurs questions :

  • est-ce qu’on fixe une limite de taille sur ce qui peut être mis dans comment ou est-ce qu’on laisse le mécanisme de frais de la blockchain “réguler” le stockage
  • est-ce qu’on émet l’événement quel que soit le résultat du dispatch, ou est-ce qu’on n’émet l’événement qu’en cas de succès ? ça permettrait par exemple de distinguer un client qui n’a pas fait le virement promis pour payer et un client qui a fait le virement mais qui est tombé en échec

C’est amusant comme je prenais pour acquis que les commentaires de tx n’avaient pas leur place en blockchain et que les voilà en train d’y retourner ><

Je me demande comment, en écoutant les événements seulement, on pourra faire le lien entre le commentaire et l’ensemble des événements créés par l’extrinsic. Par exemple un batch permet de relier les extrinsics, mais pas leurs événements.

Un service de vérification de transfert devra donc écouter les événements de crédit sur son compte, puis s’il en trouve un, chercher l’extrinsic correspondant et le commentaire parent.

1 Like

Je croyais que justement les événements étaient groupés : http://dotapps.io.ipns.localhost:8080/?rpc=wss%3A%2F%2Fgdev.coinduf.eu%2Fws#/explorer/query/0x9a43b41df4da35f0ef023a1ea8979fdc192765d716f97c18225ec5e0e77566fa :face_with_peeking_eye:

Un batch permet de relier des calls, mais il n’y a qu’un seul extrinsic.

Dans duniter-squid, quand on a un événement, c’est facile de remonter au call associé et à l’extrinsic associé. Donc si on fait :

  • batch
    • remark
    • transfer
    • transfer

Quand on a un événement remark, il faut faire remark -> batch -> transfer* et on peut se retrouver avec plusieurs calls. Alors que si on fait

  • wrapper(comment)
    • transfer

Il suffit de faire comment -> wrapper -> transfer et on sait qu’il ne peut y en avoir qu’un parce que wrapper est conçu comme ça. Et on peut même avoir (ou pas, si on souhaite filtrer) :

  • wrapper(comment)
    • batch
      • transfer
      • transfer

Qui reste à gérer côté indexeur.

2 Likes