Optimiser pour NEAR Storage On-Chain (Réduction de la Taille du Contrat)

12 min read
To Share and +4 nLEARNs

Avis : Nous vous laissons quelques « questions » pour que vous puissiez expérimenter vous-mêmes les résultats finaux.

Le stockage sur chaîne coûte cher. Pour chaque 100 kB utilisés, 1 NEAR est verrouillé. Ces NEAR peuvent être retournés si vous supprimez le stockage utilisé. Il existe deux principales causes d’utilisation du stockage :

  • Transactions : chaque transaction est un reçu, qui est enregistré en chaîne, ce qui occupe un peu de stockage. Chaque reçu de transaction a une longueur différente (certains appellent plus de fonctions, donc une réception plus longue ; d’autres n’appellent qu’une seule fonction, donc une réception plus courte), d’où la quantité de stockage utilisée est différente. Tout comme vous avez besoin de plus de papier pour imprimer un long reçu, vous utilisez plus d’espace de stockage pour stocker un reçu plus long.
  • Déploiement de contrat intelligent : lorsque vous déployez un contrat intelligent, il prendra la taille du contrat intelligent (il peut différer légèrement de ce que vous voyez dans le fichier wasm local, mais dans l’ensemble, les différences sont minimes).

Pour les transactions, à moins de réduire les transactions que vous avez effectuées, vous ne pouvez pas vraiment réduire l’espace occupé en raison des transactions.

(Question : Pouvons-nous supprimer notre compte de niveau supérieur via near-cli ? Si nous le supprimons, cela libère-t-il des coûts de stockage ? (P. -S. vérifier l’explorateur après la suppression d’un sous-compte.) Certes, les transactions ne seront pas supprimées, d’où verrouillera-t-il toujours le coût de stockage, donc ne pourra jamais être libéré ?)

Ici, nous parlerons de l’optimisation de la taille de votre contrat contract (Smart Contrat)

Remarque : on dit que le développement en AssemblyScript conduit à une taille de wasm plus petite, pour le même code écrit, par rapport à Rust. On n’est pas un développeur AS (préfère utiliser Rust), donc on n’en parlera pas ; en fait, on n’a pas besoin d’en parler, c’est de toute façon le boulot du compilateur.

Optimiser la taille du contrat

Stockage Persistant

Near a une liste de collections, la liste est ici. Ces collections mettent en cache les données pour réduire les frais de gaz. Le compromis est le suivant : combien de frais de gaz souhaitez-vous réduire ? Combien de frais de stockage êtes-vous prêt à payer pour la réduction des frais de gaz ? De combien de stockage persistant unique avez-vous besoin ?

La différence est mentionnée dans le lien ci-dessus (les mots exacts sont comme ci-dessous):

Il est important de garder à l’esprit que lors de l’utilisation de std :: collections (c’est-à-dire HashMap, etc. Rust locals), chaque fois que l’état est chargé, toutes les entrées de la structure de données seront lues à partir du stockage et désérialisées. Cela aura un coût élevé pour toute quantité de données non négligeable, donc pour minimiser la quantité de gaz utilisée, les collections SDK doivent être utilisées dans la plupart des cas.

Considérez cet exemple : nous avons un contrat principal qui a un dictionnaire/hash/map pour lier un ArticleId à un Article. L’article est un objet (dans Rust, on l’appelle aussi un Struct) avec ses attributs.

use near_sdk::AccountId;
use near_sdk::collections::LookupMap;
use std::collections::HashMap;

pub struct Contract {
pub article_by_id: LookupMap<ArticleId, Article>,
}

pub struct Article {
pub owner_id: AccountId,
pub article_id: String,
pub royalty: HashMap<Account, u16>,
}
We see that Contract.article_by_id uses LookupMap and Article.royalty uses HashMap. We shall discuss why we don’t use other types.

Considérez article_by_id, lorsque nous créons un article, l’identifiant id sera unique et spécifique à cet article. Il sera stocké pour toujours. Une LookupMap cache les résultats en mémoire afin que nous n’ayons pas à « calculer » (en utilisant les frais de gaz) chaque fois que nous avons besoin des résultats.

 

Comme mentionné précédemment, étant donné que tout sera désérialisé s’il est lu lors de l’utilisation de HashMap, et que LookupMap<AccountId, Article> n’est pas une quantité insignifiante de données (lorsqu’il y a beaucoup d’articles en cours de création), il doit être mis en cache sur la chaîne.

 

Maintenant, pourquoi utilisons-nous LookupMap au lieu de UnorderedMap ? Le latter offre une itération sur la fonctionnalité de collection, dont nous n’avons pas besoin. Si vous en avez besoin, utilisez le latter.

 

Ensuite, pour la royalty, nous utilisons HashMap. Le fait est que nous avons beaucoup d’articles, chacun avec son propre Article.royalty unique. Pour chaque Article.royalty unique, nous devons créer une nouvelle clé pour enregistrer le stockage.

P.-S. Si vous ne le savez pas encore, vous avez besoin d’une clé unique pour chaque objet des collections NEAR SDK. Si deux collections NEAR SDK partagent la même clé, elles partagent les mêmes données (indépendamment du fait que cela fonctionnera ou échouera si vous partagez de la mémoire entre deux objets différents, comme Vector et LookupMap)

Illustrons le même scénario de clé stockage partagé. Supposons que nous créons deux articles, l’article A et l’article B. Ce sont leurs équivalents Article.royalty.

// Values are percentage, 100.00% == 10_000.

// Article A
{
"alice.near": 1_000,
"bob.near": 500,
}

// Article B
{
"alice.near": 1_500,
"charlie.near": 2_000,
}
For some reason, there’s a repeat royalty for alice.near. If you use the same storage key, it will lead to error, complaining that: you already have an alice.near stored in Article A, and you are repeatingly storage another value to that key, and this is not possible. We also want them to be independent of each other; and same storage key used by 2 different HashMap means they share the same values.

(Question : pouvez-vous récupérer les valeurs stockées dans l’article A si vous initialisez avec la même clé de stockage dans l’article B ?)

La solution consiste à créer une clé de stockage distincte pour chaque collection. Cependant, si nous avons 1 million d’articles, nous avons besoin de 1 million de clés de collecte différentes pour les stocker séparément. Cela semble stupide ! Par conséquent, il est logique de les stocker plutôt sous forme de HashMap. De plus, ils sont triviaux. Ces royalty ou redevances sont conçues à l’origine pour limiter la quantité de données qu’elles peuvent stocker, de sorte que la récupération des données est petite et que leur désérialisation est bon marché. Cela renforce notre choix d’utiliser HashMap plutôt que les collections SDK équivalentes, malgré une consommation de gaz (légèrement) plus élevée (ce qui est négligeable puisque la collection est si petite qu’elle est négligeable).

En conclusion, lors de la conception de votre contrat intelligent, choisissez d’utiliser les collections NEAR SDK ou les collections Rust en fonction de la trivialité et du nombre de répétitions dont vous avez besoin pour le même Map.

Réduction de codes

Les premiers codes que nous écrivons, ils sont mauvais. En fait, ce ne sont qu’un brouillon. Nous avons besoin d’une refactorisation pour supprimer le code inutile. Il existe un compromis entre un code plus facile à comprendre et l’optimisation du stockage.

Par exemple, on a peut-être un PayoutObject, et il n’est utilisé que dans une seule fonction.

use near_sdk::json_types::U128;
use near_sdk::AccountId;

pub struct Payout {
pub payout: HashMap<ArticleId, U128>,
}

impl Default for Payout {
fn default() -> Self {
Self [
payout: HashMap::new(),
]
}
}

Pourquoi ne pouvons-nous pas simplement définir un

HashMap::new()

dans la fonction spécifique qui l’utilise? Bien sûr, si vous faites le latter, il serait plus difficile de comprendre le code. Le former rend les choses plus faciles à comprendre, d’un point de vue d’Objet Orienté. Cependant, (significativement) plus de code conduit à plus de stockage utilisé après la compilation vers WASM. Alors, il est temps de faire quelques compromis.

À l’avis, la lisibilité est plus importante que l’optimisation de l’espace de stockage. Si nécessaire, clonez le composant lisible d’origine et effectuez une optimisation à chaque fois que vous apportez des modifications, afin que les gens puissent comprendre ce que vous faites en lisant votre code d’origine. Bien sûr, cela signifie plus de travail pour vous.

(Question : Combien d’espace est économisé si vous remplacez le former par le latter ? Si vous avez un scénario similaire dans votre programme, essayez de l’optimiser en écrivant moins de code et voyez combien d’espace il a compilé, y a-t-il des différences ? (Parfois il n’y en a pas, parfois il y en a. Pour ceux qui n’en ont pas, préférez garder le code lisible pour faciliter le débogage à l’avenir. ))

Wasm-Opt

Après avoir compilé la version optimisée, vous pouvez encore réduire davantage la taille du contrat en utilisant wasm-opt. Pour l’installer, téléchargez le binaire ici pour votre système d’exploitation OS et décompressez-le (unzip). À l’intérieur, il y a un dossier « bin », dont vous devez copier le chemin exact vers ce dossier et l’ajouter à votre chemin d’Environnement. Après quoi, essayez d’appeler wasm-opt depuis la ligne de command line/terminal, qu’il s’exécute ou non. Sinon, google en ligne comment le résoudre (peut-être que vous ne l’avez pas ajouté à la bonne variable d’environnement, peut-être que votre terminal est déjà ouvert et qu’il n’actualise pas le dernier chemin, sont les deux problèmes les plus courants).

L’exécution de ceux-ci réduirait la taille du fichier :

#!/bin/bash
set -e

export WASM_NAME=tipping.wasm
z
mkdir res

RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/$WASM_NAME ./res/
wasm-opt -Os -o res/output_s.wasm res/$WASM_NAME
ls res -lh

Ici, nous supposons que le contrat d’origine est compilé dans tipping.wasm (le nom Cargo.toml est tipping). Ensuite, le nom optimisé est output_s.wasm. Nous exécutons ensuite ls (sous Linux) pour vérifier leur différence de taille de fichier. Il devrait être plus petit.

Remarque : vous pouvez également utiliser -Oz pour les drapeaux, mais on a trouvé cela inutile, comme pour le projet sur lequel on travaille, cela ne conduit pas à une taille de fichier plus petite.

Remarque importante : Le RUSTFLAGS doit être « link-arg=-s », si vous le changez accidentellement en « -z », vous pourriez avoir un gros problème. Au moins pour un, il génère un fichier wasm beaucoup plus volumineux. Vous devrez l’expérimenter et vérifier votre propre projet.

Peut-être qu’à l’avenir, ils pourraient autoriser le fichier .wasm.gz afin que vous puissiez optimiser davantage la taille du fichier. Actuellement, on l’a essayé et il ne peut pas désérialiser un fichier gzippé, ne prenant en charge que le fichier .wasm en chaîne.

Cargo.toml

Ce sont les drapeaux habituels pour le cargo toml.

[profile.release]
codegen-units = 1
opt-level = "s"
lto = true
debug = false
panic = "abort"
overflow-checks = true

Vous pouvez également choisir opt-level = « z », cela peut ou non générer un binaire plus petit.

Quelques autres petites victoires

Éviter le Formatage de Chaîne

format! et to_string() peut apporter un gonflement du code ; utilisez donc une chaîne statique (&str) chaque fois que possible.

Suppression de rlib si non requis

Si vous n’avez pas besoin de faire des tests de simulation, supprimez rlib.

Utiliser la sérialisation Borsh

Préférez ne pas utiliser serde lorsque cela est possible. Voici une page sur la façon de remplacer le protocole de sérialisation.

Évitez les assertions standard de Rust et les macros de panique

Ceux-ci contiennent des informations sur l’erreur renvoyée, ce qui introduit un gonflement inutile. Essayez plutôt ces méthodes :

// Instead of this
assert_eq!(
contract_owner,
predecessor_account,
"ERR_NOT_OWNER"
);

// Try this (for all versions of near-sdk-rs)
if contract_owner != predecessor_account {
env::panic_str("ERR_NOT_OWNER")
}

// For near-sdk-rs v4.0 pre-release
use near_sdk::require;
require!(
contract_owner == predecessor_account,
"ERR_NOT_OWNER"
);

require! est une macro légère introduite dans near-sdk-rs v4 (et c’est aussi sa macro préférée) pour remplacer assert! macro. Cela fonctionne comme assert! la plupart du temps, à une petite différence près : il ne peut pas spécifier de format ! per se.

// assert! can specify format
assert!(
some_conditions,
"{}: {}"
error_type,
error_message
);

// require! cannot
require!(
some_conditions,
format!( // you need format! yourself
"{}: {}",
error_type,
error_message
)
);

Et comme nous l’avons mentionné précédemment pour éviter le formatage des chaînes, il est préférable de coder le message en dur. Bien sûr, si vous en avez vraiment besoin, sacrifiez simplement quelques octets pour utiliser le format ! est ok: il ne prend qu’un espace négligeable si vous ne l’utilisez pas de manière intensive.

N’utilisez pas .expect()

Utilisez plutôt unwrap_or_else. L’un a écrit la fonction d’assistance dans la caisse d’assistance NEAR helper crate que vous voudrez peut-être consulter.

Sinon, vous pouvez toujours mettre ceci dans internal.rs :

fn expect_lightweight(option: Option, message: &str) -> T {
option.unwrap_or_else(||
env::panic_str(message)
)
}

// instead of:
let owner_id = self.owner_by_id
.get(&token_id)
.expect("Token not found");

// use this:
let owner_id = expect_lightweight(
self.owner_by_id.get(&token_id),
"Token not found"
);

Évitez Panicking

Voici quelques erreurs courantes dans les paniques :

  • Indexation d’une tranche hors limites. my_slice[i]
  • Division de zéro : dividend / 0
  • unwrap() : préférez utiliser unwrap_or ou unwrap_or_else ou d’autres méthodes plus sûres pour ne pas paniquer. Dans le near-sdk, il y a aussi env::panic_str (env::panic est obsolète) pour panic, et ils ont mentionné ici que cela pourrait être préféré. Cependant, vous pouvez également utiliser la correspondance à l’ancienne pour gérer les choses et voir si cela fonctionne mieux que panic_str ; sinon, utilisez panic_str pour une compréhension plus facile du code. Sinon, vous pouvez passer à match si cela en vaut la peine.

Essayez d’implémenter une solution de contournement pour qu’elle renvoie None ou n’imposez aucune panicking lors du développement du contrat.

Approches de niveau inférieur

Consultez le lien dans la référence pour d’autres moyens de réduire la taille du contrat non mentionnés ici. (Les éléments mentionnés ici ne sont également pour la plupart pas mentionnés ici).

Gardez à l’esprit que cet exemple n’est plus mis à jour, il vous oblige donc à dériver manuellement la dernière mise à jour.

La liste est ici :

  • Tiny Contract (obsolète)
  • Contrat de fuzzing rs (vous pouvez visualiser la branche master, il s’agit d’une branche fixe pour éviter qu’elle ne soit supprimée à l’avenir). On ne sait pas ce que fait ce contrat, ni ce que signifie « fuzzing » ; vous auriez besoin de vous comprendre.
  • L’exemple d’Eugene pour un jeton fongible rapide, et vous pouvez regarder la vidéo youtube ici. Il l’implémente sans utiliser le near-sdk. Une expérience de programmation plus malheureuse, mais qui optimise la taille.

Aurora utilise rjson comme crate de sérialisation JSON légère. Il a une empreinte plus petite que serde actuellement fourni avec Rust SDK. Voir cet exemple et le lecteur a besoin de déduire comment il s’utilise lui-même. Ajouter à considérer est miniserde crate, exemple ici

L’outil Wasm-snip

Il peut être utile de remplacer les fonctions non utilisées par des instructions inaccessibles. Habituellement, vous n’avez pas besoin de le faire : seulement si vous avez vraiment vraiment besoin d’économiser cet espace, vous pouvez continuer.

Ils ont mentionné que l’outil est également utile pour supprimer l’infrastructure en panicking !

Vous pouvez également exécuter wasm-opt avec l’indicateur –dce après la capture, afin que ces fonctions coupées soient supprimées.

Conclusion

Il existe de nombreuses façons d’optimiser un contrat. Certaines optimisations se font facilement sans aucun changement, d’autres ont des compromis et des trade-offs que vous déciderez si cela en vaut la peine ou non. En général, à moins que votre contrat ne soit extrêmement volumineux, ce qui résulte généralement de l’écriture d’un trop grand nombre de lignes de code et pour lesquelles vous êtes encouragé à vérifier la nécessité d’écrire du code ; sinon, une utilisation simple comme wasm-opt et un choix de stockage persistant devrait suffire.

Les références

Near SDK documentation on reducing contract size
Reducing Wasm size for Rust

20
Retour haut de page