Implémentation des commentaires de transaction

Suite du sujet Protocole pour les commentaires de transaction (Ğ1v2 protocol) mais cette fois dans la catégorie Duniter-v2s puisqu’on passe vraiment à l’implémentation.

Le ticket en question est #235, et je me lance dans une pallet “describe” qui est une sorte d’hybride entre utility::batch et remark des pallets du frame.

Dans ce fil, je vais faire comme pour Implémentation des quotas, écrire en langage humain les choix d’implémentation que je suis en train de faire pour pouvoir les discuter le plus en amont possible et changer de direction tôt si nécessaire.


Vidéos d’avancement :

Je découvre dans les primitives substrate les interfaces TransactionIndex “Interface that provides transaction indexing API.” et OffchainIndex “Interface that provides functions to access the Offchain DB.”. On dirait un angle mort de ma compréhension de substrate qui permettrait de demander des choses au client substrate non obligatoires car hors du storage on-chain. Peut-être qu’on pourrait les utiliser pour interfacer proprement les actions onchain et offchain. → c’est tout ce qui concerne les offchain workers (ocw), mais en terme d’architecture, si la blockchain n’utilise jamais ces données, ça n’est pas utile de passer par ça, et plus simple de développer une autre stack déconnectée comme les datapods.

Voici le call store de la pallet remark :

#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::store(remark.len() as u32))]
pub fn store(origin: OriginFor<T>, remark: Vec<u8>) -> DispatchResultWithPostInfo {
	ensure!(!remark.is_empty(), Error::<T>::Empty);
	let sender = ensure_signed(origin)?;
	let content_hash = sp_io::hashing::blake2_256(&remark);
	let extrinsic_index = <frame_system::Pallet<T>>::extrinsic_index()
		.ok_or_else(|| Error::<T>::BadContext)?;
	sp_io::transaction_index::index(extrinsic_index, remark.len() as u32, content_hash);
	Self::deposit_event(Event::Stored {
		sender,
		content_hash: content_hash.into(),
	});
	Ok(().into())
}

Si je comprends bien :

  1. extrinsic_index permet de référencer précisément l’extrinsic commenté avec remark (donc par exemple le batch qui contient remark et des transfers)
  2. sp_io::transaction_index::index() est utilisé pour stocker le content_hash dans le stockage offchain (par contre je ne vois pas les cas d’usage)

Ce serait plus souple de forker remark pour plus de comptabilité avec le CID IPFS, et peut-être plus simple d’utiliser un wrapper plutôt que de reposer sur batch dont le sens est moins précis.

Mais dans un premier temps on pourrait se contenter d’utiliser batch et remark pour éviter de développer une nouvelle pallet. Mais comme seul un hash blake2_256 est publié, il faudrait le convertir en CID hors chaîne (ajouter multibase et multicodec).


Si on se contente de ça, on a déjà ce qu’il nous faut dans le runtime :

cf le bloc 1639903 où je fais un batch avec un commentaire de transaction :

Voici mon extrinsic :

  • utility.batch
    • system.remarkWithEvent(“commentaire de transaction”)
    • balances.transferKeepAlive(Daigongen, 123)

Et je peux remplacer “commentaire de transaction” par un CID si je veux pouvoir demander la suppression de mon commentaire, ou juste utiliser moins de ressources blockchain pour un long message chiffré qui ne concerne de toute façon que moi ou mon destinataire.

L’événement publié est un hash blake2_256 : 0x911d0d1f204f5f8bced666c8619a686a3fc75940aa34b6f026a1e85aa2fe5ee0 qui correspond au hash de mon commentaire de transaction.


J’ai fait un deuxième commentaire pour faciliter l’ajout sur IPFS en utilisant un fichier qui finit par 0x0a : 1640537.

On voit que le hash est 0xfcbbe2e60bee2b1b0b7429997993d7e486df936c3b7d841f1ed5b932003cdc23. C’est un hash blake two 256.

On peut le convertir en CID comme ça :

import { CID } from 'multiformats/cid'
import * as multihash from 'multiformats/hashes/digest'
import { blake2b256 } from '@multiformats/blake2/blake2b'

const comment = 'commentaire de transaction' + String.fromCharCode(0x0a)
const commentBytes = Buffer.from(comment, 'ascii')
const commentHash = await blake2b256.encode(commentBytes)
const commentHashHex = Buffer.from(commentHash).toString('hex')

console.log(commentHashHex)

const hexHash = commentHashHex
const hashBytes = Buffer.from(hexHash, 'hex')
const mh = multihash.create(blake2b256.code, hashBytes)
const cid = CID.createV1(0x55, mh)

console.log(cid.toString())

On obtient bien le même hash, et le CID suivant : bafk2bzaced6lxyxgbpxcwgyloquzs6mt27sinx4tnq5x3ba7d3k3smqahtocg.

On peut donc retrouver le commentaire de transaction sur une passerelle IPFS :
:point_right: https://bafk2bzaced6lxyxgbpxcwgyloquzs6mt27sinx4tnq5x3ba7d3k3smqahtocg.ipfs.pagu.re/

En faisant ça, le message est inscrit en blockchain pour toujours. Mais si au lieu d’un commentaire de transaction on met un CID, on peut ensuite faire disparaître la donnée. Ce serait quand même plus confortable de :

  • préciser si le commentaire de transaction doit être interprété comme une donnée brute ou comme un CID (ou autre chose si on veut dans le futur). L’événement pourrait alors contenir soit le hash de la donnée brute, soit le CID directement.
  • wrapper le call à commenter dans le commentateur plutôt que d’avoir recours à un batch

Mais pour éviter de prendre du retard, je vais déjà tenter une implémentation complète sur ce schéma, c’est-à-dire :

  • dans duniter-squid pour collecter les hash annoncés et les convertir en CID pour graphql
  • dans ddd-ui pour soumettre un commentaire de transaction sur IPFS en même temps qu’un transfert sur Duniter

Petite vidéo point d’étape pour les commentaires de transaction :

00:00 intro
00:24 résumé sur les commentaires de transaction v1
01:20 ce qu’on aurait pu faire en v1 (et dont on a discuté)
02:34 ce qu’on envisage de faire en v2
04:44 parenthèse sur l’interopérabilité avec des systèmes automatisés
05:15 démonstration technique du début d’implémentation
06:12 batch call pour grouper la remarque avec les transactions
06:38 encodage hexadécimal du commentaire de transaction
08:28 examen du bloc incluant le batch avec commentaire
09:25 examen des données indexées par subsquid
10:13 au programme pour la suite
12:30 à bientôt


Dans un premier temps, je propose de ne rien implémenter côté Duniter et de partir sur :

  • si la “remark” est au format /ipfs/<cid> c’est une donnée “offchain”
  • sinon la “remark” est interprétée comme donnée “onchain”

Pour ce qui est des messages crowdfunding Ğchange par exemple, c’est au choix d’implémentation :

  • soit c’est onchain, dans ce cas pas de droit à l’oubli et c’est mieux si c’est plus petit que 256 bits
  • soit c’est offchain, dans ce cas liberté totale

Pour écouter un événement il suffit par exemple de produire une donnée unique et d’écouter les événements remark avec son hash.

Dans un second temps, j’aimerais bien implémenter côté Duniter une pallet “describe” similaire à “remark” mais :

  • qui limite le wrapping à un seul call et pas un array pour faciliter l’implémentation downstream (mais on peut prendre en charge le batch commentés si on veut)
  • qui permet de qualifier nativement le type de donnée, par exemple avec un enum :
    • en clair → événement avec un hash (blake two 256 par exemple)
    • hors chaîne IPFS → événement avec un CID

Mais j’ai d’abord besoin de faire une PoC complète et de plus d’échanges sur ce fil.

2 Likes

Si on part sur une solution qui n’implémente rien côté Duniter, et donc qu’on peut faire un batch qui inclut plusieurs remark et plusieurs transfers, comment l’interpréter dans un indexeur par exemple ? Ce serait plus simple de se limiter à une seule remark par batch, et donc d’avoir un wrapper custom.

Ensuite, on peut interpréter les bytes hors blockchain (par exemple avec un système de préfixe custom), mais ce serait plus simple d’avoir directement cette indication en blockchain. Voici les cas d’usage que j’identifie :

  • stockage “onchain” (pas de droit à l’oubli, utilisation probablement plus haute de la blockchain)
    • bytes à interpréter comme chaîne utf8 en clair
    • bytes à interpréter comme un identifiant standardisé (crowdfunding gchange, paiement pour un item x de référence donnée…)
    • bytes à interpréter comme message chiffré binaire
  • stockage “offchain” (“droit à l’oubli”, compatible avec une donnée plus volumineuse, mais nécessite une autre couche, par exemple via les datapods ipfs)
    • bytes à interpréter comme un CID IPFS (longueur connue d’avance)
    • bytes à interpréter comme un autre système de référencement hors chaîne (uuid, URL…)

Évidemment, tout ceci n’est qu’indicatif, puisque quelle que soit la catégorie qu’on définisse, on peut mettre les bytes qu’on veut, et inversement une plateforme peut décider d’encoder ses références comme un ascii suivant un format propre à elle. Mais définir ces catégories permet d’avoir plusieurs événements plutôt qu’un seul “remarkWithEvent” (un événement avec le hash d’un CID n’est pas très utile) :

  • bytes comme chaîne en clair → événement vide
  • bytes d’identifiant standardisé → événement avec une enum Rust
  • bytes comme CID → événement avec le CID

On pourrait aussi distinguer en fonction de ce qu’on commente. Par exemple, pour le call createIdentity on pourrait mettre un commentaire avec le pseudo que nous a donné la personne. Même si dans un premier temps on ne commente que les transferts (éventuellement tous les transferts d’un batch), c’est intéressant d’imaginer des évolutions pour ce système.

PS : si vous êtes intéressés par l’histoire de remarkWithEvent, c’est ici : emit event on remark by xlc · Pull Request #8120 · paritytech/substrate · GitHub

Pour ceux qui voudraient des nouvelles en avant-première, cf les branches hugo/tx-comments sur gcli et duniter-squid. Pour les autres, je ferai un retour détaillé plus tard.

Suite aux discussions sur Remboursement des frais de transaction en cas de bloc non-plein - #20 by tuxmain, il semble qu’il soit important d’ajouter une pallet (“describe” par exemple) permettant de commenter un call afin de pouvoir limiter la taille du commentaire (contrairement à system.remark()), ce qui apparaît nécessaire dans le cas de transactions gratuites.

Cela permettrait par la même occasion :

  • de commenter de manière plus claire que via un batch (un seul commentaire par call)
  • d’adapter l’événement aux cas d’usage (CID ou simple hash)

Je suis un peu à plat, donc c’est pas hyper clair, mais voici une deuxième vidéo sur les commentaires de transaction :

00:00 début
00:16 #commentaires sur le forum
01:26 Ǧcli version 0.2.14 commentaire onchain
02:30 commentaire offchain : soumission d’un CID
03:55 inspection des commentaires dans l’indexeur
06:00 stockage sur un noeud local ou demande d’indexation utilisant les datapods
09:17 ce qu’il y a à faire du point de vue des clients
11:00 l’idée : indiquer comment décoder les données mises en blockchain → nouvelle pallet Duniter
12:22 fin

Il faudra discuter de ce qu’on met en place pour la soumission de la donnée des commentaires de transaction offchain. Pour l’instant je suis parti sur le réseau datapod puisque qu’il était présent, mais on pourrait partir sur un autre système d’épinglage des données sur IPFS (pour l’instant je n’en connais aucun décentralisé).

3 Likes