Remboursement des frais de transaction en cas de bloc non-plein

Idée à creuser pour réduire l’impact des frais de transaction la plupart du temps (voire quasiment tout le temps) : remboursement en cas de bloc rempli en-dessous d’un certain seuil (par exemple 50%), pour tous les comptes, sans user les quotas.

En cas de dépassement du seuil de remplissage du bloc, les règles actuelles s’appliquent (frais et remboursement des membres par quota).

Il faut tout de même faire attention à ce qu’on ne puisse spammer en augmentant le temps de calcul du code chargé du remboursement. C’est pourquoi le seuil doit être strictement inférieur à 100%, voire significativement plus petit.

  • souhaitable ?
  • effets secondaires inattendus ?
  • facile à implémenter ?

Inconvénients potentiels :

  • peut augmenter la charge des indexeurs et le remplissage de l’archivage car le spam est moins cher.
  • frais impossibles à prédire à coup sûr par les clients
  • surcoût sur toutes les transactions (comparable à celui induit par les quotas)
7 Likes

Ça a le très grand mérite d’être simple comme solution. Si ça fait le travail c’est probablement la meilleure solution à ce stade.

On pourrait définir un quota global dans la pallet Quota réinitialisé à chaque bloc. Ce serait un quota disponible pour tous et associé à aucune identité. Pour chaque transaction anonyme (compte non lié à une identité), un remboursement serait planifié en prélevant sur ce quota global. Quand le quota global est excédé ou la queue de remboursements anonymes est dépassée, les remboursements pour comptes anonymes ne sont plus déclenchés. L’intérêt de ce fonctionnement est qu’on bénéficie des traitements “on_idle” de la pallet quota et qu’on peut prioriser les remboursements de membres par rapport aux remboursements anonymes.

Oui, je le souhaite :slight_smile:

“Inattendus” on n’y a par définition pas pensé, mais le traitement “on_idle” peut apaiser les craintes. Par contre, ça double le nombre d’événements “withdraw” et “deposit” associés aux frais. Il faudrait en estimer le coût.

Avec ma proposition, oui, c’est assez peu de lignes puisqu’il suffit de réutiliser le mécanisme des quotas et de l’appliquer également aux comptes non liés avec une deuxième queue de remboursements moins prioritaire.

Oui, le spam est remboursé :slight_smile:. Un système de sécurité sociale à l’épreuve des placebo chers ^^

Ah oui, ça je n’y avais pas pensé. Mais on peut avoir une heuristique basée sur le bloc précédent qui regarde :

  • si le quota global a été consommé en grande partie
  • si la queue de remboursements globale a été presque remplie

et prévient l’utilisateur anonyme que les frais de transaction pourraient ne pas lui être remboursés.

Oui. C’est le ×2 qu’il faut apparemment accepter avec l’implémentation actuelle. Mais on pourrait aussi toucher directement à la pallet transaction_payment. J’ai hésité à le faire par peur de casses les protections qu’elle offre contre le spam, et c’est pour ça que je me suis tourné vers le système de remboursements, cf historique sur Implémentation des quotas.

1 Like

Je voyais plutôt un remboursement soit pour tout le monde, soit pour personne.

Si un spammeur a la chance d’avoir les premiers extrinsics (tiens, comment est déterminé l’ordre d’exécution des extrinsics ?) il profite du quota, et laisse les utilisateurs légitimes payer les frais.

Avec un remboursement égalitaire, tout le monde paierait les frais y compris le spammeur, dès qu’on dépasse le seuil.

On peut aussi faire un remboursement progressif pour réduire les effets de seuil : par exemple une courbe allant de 100% de remboursement à 25% de remplissage du bloc, jusqu’à 0% de remboursement à 75% de remplissage du bloc. Ainsi les prédictions des clients seront moins souvent justes mais moins souvent très éloignées de la réalité. Mais ok c’est pas prioritaire.

3 Likes

Si j’avais pu j’aurais mis plusieurs étoiles à cette idée. C’est vraiment top ! Il y a du CPU de dispo ? Allez la blockchain est gratuite d’utilisation !

Certes mais c’est binaire : soit les frais calculés sont les bons, soient les frais sont finalement de zéro. C’est plutôt acceptable comme “erreur du client”.

Autre proposition : les transactions sans frais

L’idée peut paraître absurde parce qu’en cas d’attaque (spam) il est impossible de ponctionner l’attaquant. Mais je rappelle que tout nœud Duniter (v2) dispose d’une transaction pool dans laquelle celui-ci “pioche” les transactions pour les mettre dans un bloc, et qu’à cette étape le nœud fait déjà quelques contrôles.

Nous pourrions ainsi, je suppose, ajouter des transactions sans frais que le nœud choisirait à loisir d’inclure ou non dans le bloc, et qu’en cas d’échec de transaction le montant des frais soit prélevé sur le compte du forgeron (c’est déjà le cas me semble-t-il pour les inhérents).

L’avantage : les utilisateurs peuvent explicitement choisir de ne pas payer pour l’utilisation de la blockchain, et qu’en cas de congestion leur transaction passe après les autres à l’instar de ce qui se fait sur Bitcoin.

Inconvénient : possible surcoût caché d’utilisation CPU pour piocher dans la transaction pool. Mais ce pourrait être une option à activer pour chaque nœud forgeron.

3 Likes

On peut faire ça aussi : si le quota disponible atteint zéro, on vide la liste de remboursement et saute cette étape.

Le forgeron peut écrire un algorithme de priorisation, je crois que par défaut ça trie en fonction des tips (= pourboire). @flodef m’avait expliqué que chez Solana, les frais étant très bas et l’utilisation (légitime) très grande, il était fréquent que les clients déterminent le tip de manière algorithmique en fonction de la latence d’exécution qu’ils visaient.

Pourquoi pas, ça me semble lourd en R&D, mais ce serait propre.

Est-ce que ça vous va si pour l’instant on part sur l’option “quota global” avec remboursement pour tous si non atteint et pour personne si atteint ?

Ça permettrait à @bgallois de s’en charger sans partir dans des directions trop incertaines. Et ça me semble assez intéressant à implémenter dans le cadre de l’objectif “migration sans fork”.

4 Likes

On pourrait aussi se servir du FeeMultiplier (Transaction Fees · Polkadot Wiki) pour avoir des frais nuls (à tester) ou quasi nuls pour des blocs peu saturés et qui augmentent proportionnellement à la saturation pour éviter le spam.
Cela permettrait de réduire les frais pour tout le monde en temps normal et de se servir des quotas, comme implémenté actuellement, en cas de saturation.

4 Likes

Oui :+1:

4 Likes

Il me semble que ce n’était pas possible techniquement, cf [runtime-104] Bug critique ĞDev: substrate ne peut pas fonctionner correctement sans frais.

Mais ça a peut-être changé avec les mises à jour du framework ? Je pense que trouver la solution pour implémenter l’absence de frais en cas de bloc non plein va être ta prochaine grosse mission >< → Exonerate all transaction fees when blockchain use is low (#232) · Issues · nodes / rust / Duniter v2S · GitLab
Et il était temps qu’on y arrive puisque tu avais déjà presque tout fini !! Issues · nodes / rust / Duniter v2S · GitLab

Bonjour,

J’ai vu dernièrement la vidéo de Hugo sur le sujet, et je me pose une question peut être bête : pourquoi prélever des frais de transaction pour ensuite les rembourser ? Ne serait-il pas mieux de ne pas prélever de frais de transaction du tout. Cela éviterait deux transactions en blobkchain (le prélèvement et le remboursement) et cela éviterait de polluer les lignes de compte par des transactions opposées identiques.

1 Like

Les frais ne sont remboursés que s’il n’y a pas d’attaque par saturation de la blockchain.
Il est moins couteux en temps de calcul de rembourser tous les frais s’il n’y a pas eu d’attaque que de se demander à chaque fois s’il y a une attaque en cours.

1 Like

Si, mais comment faire techniquement ? Jusque là on ne voyait pas trop comment faire donc on a développé ce système. En ce moment @bgallois est en train de se creuser la tête pour utiliser astucieusement le fee_multiplier qui serait égal à zéro au début du remplissage du bloc et monterait ensuite quand le remplissage dépasse un seuil au choix. Si ce seuil est atteint, tout le monde paye des frais, et il faut donc les rembourser aux membres… à moins de trouver une autre solution évidemment.

Avec un remboursement, comment on fait quand la transaction a supprimé le compte ? (par exemple transferAll) Il faudrait verser le remboursement au compte destinataire, ce qui demanderait une logique spécifique compliquée…

1 Like

En cas de suppression de compte, les frais retournent dans le compte treasury. pallets/quota/src/lib.rs · master · nodes / rust / Duniter v2S · GitLab

// perform refund
let res = CurrencyOf::<T>::resolve_into_existing(&queued_refund.account, imbalance);
match res {
    // take money from refund account OK + refund account OK → event
    Ok(_) => {
        Self::deposit_event(Event::Refunded {
            who: queued_refund.account,
            identity: queued_refund.identity,
            amount,
        });
    }
    Err(imbalance) => {
        // refund failed (for example account stopped existing) → handle dust
        // give back to refund account (should not happen)
        CurrencyOf::<T>::resolve_creating(&T::RefundAccount::get(), imbalance);
        // if this event is observed, block should be examined carefully
        Self::deposit_event(Event::RefundFailed(queued_refund.account));
    }
}

Effectivement, il faudrait ajouter une condition pour tester ce cas avant d’initier le remboursement, sinon on va avoir l’événement RefundFailed à chaque suppression de compte. On pourrait à la place faire un RefundDropped.

2 Likes

Voilà la MR !268 en réponse au ticket #232.

Super documentation dans la MR. Un angle mort que je vois pour l’instant est que si le seuil est uniquement calé sur le poids (weight), on peut attaquer la blockchain en soumettant des transactions lourdes en octets (length) et légères en poids. Par exemple, plein de system.remark avec des remark de 100 MB. Comme il n’y a quasiment aucun calcul dans ce call, on peut en soumettre beaucoup avant de devoir payer, et donc potentiellement uploader des Giga en blockchain, ce qui permet de charger les disques dur inutilement.

D’où l’intérêt de définir un quota en Ğ1 (donc après intégration des weight2fee et des length2fee), ce qui par ailleurs est plus parlant pour la communauté, dans le genre :

  • 1 Mo en blockchain coûte 200 Ğ1 (exemple non représentatif)
  • la capacité de calcul d’un raspberry pi (machine de référence) pendant un bloc est valorisée à 100 Ğ1 (exemple non représentatif)
  • tant que les frais totaux d’un bloc sont inférieur à 30 Ğ1 (exemple non représentatif), quelle que soit la répartition weight/length, les frais ne sont pas dus

Et on pourra décider de rendre les frais de length plus chers, ce qui changera la vitesse à laquelle on atteint le quota.

Par ailleurs, cette remarque me paraissait intéressante :

Avec l’implémentation actuelle, ce n’est pas le cas, on peut spammer juste avant la limite et espérer ne pas payer frais si les transactions sont intégrées dans un ordre à notre avantage, mais faire payer les frais à d’autres. À mon avis, soit on complète ça par un algorithme de priorisation, soit il faut des remboursements pour tous ou pour personne.

D’ailleurs je me demande si pendant que les frais sont nuls, un compte vide peut exécuter une transaction (par exemple remark). Vu que d’habitude l’erreur est “inability to pay some fees” mais que là ils seront nuls. C’est toutes ces questions auxquelles je n’avais pas envie de répondre qui m’ont dirigé vers l’implémentation “quota” avec remboursement après la transaction. Comme ça le comportement par défaut reste robuste, et si les conditions sont remplies pour l’exonération elle a lieu, sinon non.

1 Like

Le seuil est calé sur le poids, qui contient une composante calcul (ref_time) et une composante longueur (proof_size weight - What is proof size? - Substrate and Polkadot Stack Exchange), et la comparaison est faite sur la plus limitante, ce qui devrait couvrir le cas d’un spam de Remark. Cela dit, il serait peut-être plus judicieux de faire une comparaison explicite entre ref_time et max_ref_time ainsi qu’entre block_length et max_block_length pour optimiser les ressources (Include extrinsics len in PoV size · Issue #193 · paritytech/polkadot-sdk · GitHub), vu que pour une chaîne seule, le proof_size est non pertinent et très limitant par rapport au block_length.

Dans le cas actuel, il serait possible de faire du spam jusqu’à la limite pour forcer les transactions suivantes à payer, ce qui ne fonctionnerait que pour le premier bloc de l’attaque, dans le suivant, tout le monde paiera. Dans la pratique, cela revient à effectuer environ une centaine de transactions dans un bloc isolé afin de potentiellement gêner les utilisateurs non-membres qui souhaiteraient effectuer une transaction à ce moment-là.

Dans le cas d’un compte vide, la même erreur de fonds insuffisants sera renvoyée, car le compte doit quand même avoir le dépôt existentiel pour effectuer des transactions.

2 Likes

Après réflexion et quelques recherches, j’ai remarqué que notre chaîne est sous-utilisée, car le paramètre proof_size est limité à 5 * 1024 * 1024 (runtime/gdev/src/parameters.rs · master · nodes / rust / Duniter v2S · GitLab). Pour une solochaine, cette limite peut être ignorée (polkadot-sdk/templates/solochain/runtime/src/lib.rs at cbe45121c9a7bb956101bf28e6bb23f0efd3cbbf · paritytech/polkadot-sdk · GitHub), ce qui signifie que nous sous-utilisons les ressources de la chaîne parce que le proof_size est vite limitant sur le ref_time. Il serait préférable d’ajouter une vérification explicite de la longueur du bloc pour la limite des frais et de modifier la valeur du proof_size maximale et de laisser cette composante aux parachaines.

Voir polkadot-sdk/substrate/frame/system/src/extensions/check_weight.rs at cbe45121c9a7bb956101bf28e6bb23f0efd3cbbf · paritytech/polkadot-sdk · GitHub pour plus de détails sur la manière dont Substrate calcule les poids et les longueurs avant exécution.

3 Likes

Pour que tout le monde puisse suivre (enfin surtout les devs intéressés par le cœur), il faudrait un peu de vocabulaire en plus de

J’ai donc créé ce sujet en mode wiki à compléter

Donc si on augmente drastiquement la proof_size limite, on risque également d’augmenter beaucoup le pouvoir de spam de remark. En gros, ce que je crains c’est que des applications utilisent l’aspect pratique et gratuit du consensus en blockchain pour y soumettre des documents directement (pdf, images…) parce que c’est plus simple que de passer par ipfs et soumettre juste un CID. Si ces applications ont du succès, on pourrait se retrouver avec des Go de données uploadés tous les jours sur la blockchain, ce qui aurait comme conséquence :

  • d’alourdir la blockchain et de prendre beaucoup de place sur les disques
  • d’augmenter le trafic réseau et le temps de synchronisation

Comme c’est une blockchain, même si on limite ce cas d’usage a posteriori, on gardera quand même ce poids tout au long de sa vie.

Donc je veux bien merger !268 si tu fais le calcul en ordre de grandeur du flux de données qui pourra être uploadé en blockchain en passant sous les radars (donc en restant gratuit).

Et je veux bien aussi des retours des autres notamment @cgeek et @tuxmain parce que c’est à la fois une super fonctionnalité et à la fois politique et un peu sensible, donc je veux pas fusionner “à la légère” :slight_smile:. Et bien sûr, on pourra vulgariser ça pour avoir des retours de la communauté qui arrive souvent à pointer des aspects intéressants ^^ (j’en parlerai sûrement à Batzan avec les dev espagnols).

1 Like

Pour décourager l’utilisation abusive du stockage en blockchain on pourrait simplement limiter la taille de remark, voire le supprimer et créer un équivalent spécial pour les commentaires (il me semble qu’il était prévu que remark ne soit autorisé que sur les chaînes de dev). Une taille de 128 octets devrait suffire, pour stocker un hash de 512 bits plus quelques métadonnées.

Je suis un attaquant. Je crée des centaines de comptes gratuitement sans dépasser la limite. Je peux alors faire des transactions gratuites en masse depuis des comptes différents. L’algo de priorisation ne peut donc pas déprioriser spécifiquement mes transactions.

Les deux algos (compatibles entre eux) que je vois pour réduire l’efficacité de l’attaque sont :

  • prendre les transactions dans un ordre aléatoire
  • prioriser les identités (ou les membres) et leurs comptes liés

L’attaque devant être assez bien conçue pour être efficace et peu chère, la solution actuelle me convient pour l’instant, et on aura le temps de faire mieux plus tard.

3 Likes