Journal de développement des smart contracts Rust(1) Définition des données d'état du contrat et mise en œuvre des méthodes
Journal de développement des smart contracts Rust (2) Écriture de tests unitaires pour les smart contracts Rust
Journal de développement des smart contracts Rust ( 3) Déploiement des smart contracts Rust, appel de fonctions et utilisation de l'Explorer
Journal de développement des smart contracts Rust (4) Débordement d'entier des smart contracts Rust
Journal de développement des smart contracts Rust (5) attaque de réentrance
Journal de développement de smart contracts Rust (6) attaque par déni de service
1. Problèmes de précision dans les opérations sur les nombres à virgule flottante
Contrairement au langage de programmation de contrats intelligents courant, Solidity, le langage Rust prend en charge nativement les opérations sur les nombres à virgule flottante. Cependant, les opérations sur les nombres à virgule flottante présentent des problèmes de précision de calcul inévitables. Par conséquent, lors de la rédaction de contrats intelligents, il n'est pas recommandé d'utiliser des opérations sur les nombres à virgule flottante (, en particulier lors du traitement de ratios ou de taux d'intérêt impliquant des décisions économiques/financières importantes ).
Actuellement, la plupart des langages de programmation populaires qui représentent des nombres à virgule flottante suivent la norme IEEE 754, et le langage Rust ne fait pas exception. Voici une explication concernant le type à virgule flottante double précision f64 dans le langage Rust et la façon dont les données binaires sont stockées en interne dans l'ordinateur :
Les nombres à virgule flottante utilisent la notation scientifique en base 2 pour être exprimés. Par exemple, le nombre décimal 0.8125 peut être représenté par le nombre binaire à un nombre limité de chiffres 0.1101, la méthode de conversion spécifique est la suivante :
0.8125 * 2 = 1 .625 // 0.1 Obtention du 1er chiffre binaire décimal égal à 1
0.625 * 2 = 1 .25 // 0.11 obtenir le 2ème chiffre binaire décimal comme 1
0.25 * 2 = 0 .5 // 0.110 obtient le 3ème chiffre binaire décimal comme 0
0.5 * 2 = 1 .0 // 0.1101 Obtention du 4ème chiffre binaire décimal égal à 1
donc 0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Cependant, pour un autre nombre décimal 0.7, il y aura les problèmes suivants lors de sa conversion réelle en nombre à virgule flottante :
0.7 x 2 = 1. 4 // 0.1
0,4 x 2 = 0,8 // 0,10
0.8 x 2 = 1. 6 // 0.101
0,6 x 2 = 1,2 // 0,1011
0.2 x 2 = 0. 4 // 0.10110
0.4 x 2 = 0. 8 // 0.101100
0.8 x 2 = 1. 6 // 0.1011001
....
Ainsi, le nombre décimal 0.7 sera représenté par 0.101100110011001100.....( en boucle infinie ), et ne peut pas être représenté avec précision par un nombre à virgule flottante de longueur finie, ce qui entraîne le phénomène de "Rounding(".
Supposons qu'il soit nécessaire de distribuer 0,7 NEAR token à dix utilisateurs sur la blockchain NEAR, la quantité de NEAR tokens que chaque utilisateur recevra sera calculée et enregistrée dans la variable result_0.
#)
fn precision_test_float[test]( {
// Les nombres à virgule flottante ne peuvent pas représenter avec précision les entiers
let amount: f64 = 0.7; // La variable amount représente 0.7 jetons NEAR
let divisor: f64 = 10.0; // définir le diviseur
let result_0 = a / b; // Exécuter l'opération de division à virgule flottante
println!)"La valeur de a : {:.20}", a(;
assert_eq!)result_0, 0.07, ""(;
}
Le résultat de l'exécution de ce cas de test est comme suit :
exécution d'un test
La valeur de a : 0.69999999999999995559
thread "tests::precision_test_float" a paniqué à "assertion échouée : )left == right(
gauche: 0.06999999999999999, droite: 0.07: ", src/lib.rs:185:9
Il est évident que dans les opérations à virgule flottante ci-dessus, la valeur de amount ne représente pas exactement 0.7, mais plutôt une valeur très proche de 0.69999999999999995559. De plus, pour une opération de division unique comme amount/divisor, le résultat de l'opération devient également imprécis à 0.06999999999999999, et non à 0.07 comme prévu. Cela illustre l'incertitude des opérations sur les nombres à virgule flottante.
À cet égard, nous devons envisager d'utiliser d'autres types de représentations numériques dans les smart contracts, comme les nombres à virgule fixe.
Selon la position fixe de la virgule décimale, les nombres à virgule fixe se divisent en deux types : les entiers à virgule fixe ) purs et les décimales à virgule fixe ( pures.
Un point décimal fixé après le chiffre le plus bas est appelé un entier à virgule fixe.
Dans la rédaction réelle des smart contracts, on utilise généralement une fraction avec un dénominateur fixe pour représenter une certaine valeur, par exemple la fraction "x/N", où "N" est une constante et "x" peut varier.
Si "N" prend la valeur de "1,000,000,000,000,000,000", c'est-à-dire "10^18", à ce moment-là, la décimale peut être représentée comme un entier, comme ceci :
Dans le protocole NEAR, la valeur courante de N est "10^24", c'est-à-dire que 10^24 yoctoNEAR équivaut à 1 jeton NEAR.
Sur cette base, nous pouvons modifier les tests unitaires de cette section pour effectuer les calculs de la manière suivante :
#)
fn precision_test_integer() {
// D'abord, définissez la constante N, qui représente la précision.
let N: u128 = 1_000_000_000_000_000_000_000_000; // c'est-à-dire définir 1 NEAR = 10^24 yoctoNEAR
// Initialiser le montant, en réalité à ce moment le montant représente la valeur de 700_000_000_000_000_000 / N = 0,7 NEAR;
let amount: u128 = 700_000_000_000_000_000_000_000; // yoctoNEAR
// Initialiser le diviseur divisor
let divisor: u128 = 10;
// Calculer le résultat: result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
// Représente réellement 700_000_000_000_000_000_000_000 / N = 0.07 NEAR;
let result_0 = amount / divisor;
assert_eq![test]result_0, 70_000_000_000_000_000_000_000, ""(;
}
Cela permet d'obtenir le résultat de calcul actuariel suivant : 0,7 NEAR / 10 = 0,07 NEAR
exécution de 1 test
test tests::precision_test_integer ... ok
résultat du test : ok. 1 réussi; 0 échoué; 0 ignoré; 0 mesuré; 8 filtrés; terminé en 0.00s
2. Problème de précision des calculs d'entiers en Rust
D'après la description du point 1 ci-dessus, on peut constater que l'utilisation d'opérations entières peut résoudre le problème de perte de précision des opérations à virgule flottante dans certains scénarios.
Mais cela ne signifie pas que les résultats calculés avec des entiers sont complètement précis et fiables. Cette section présentera certaines raisons qui influencent la précision des calculs entiers.
) 2.1 Ordre des opérations
Pour la multiplication et la division ayant la même priorité arithmétique, le changement de leur ordre peut directement influencer le résultat du calcul, entraînant des problèmes de précision dans les calculs entiers.
Par exemple, il existe les opérations suivantes :
#(
fn precision_test_div_before_mul)### {
let a: u128 = 1_0000;
let b: u128 = 10_0000;
let c: u128 = 20;
// result_0 = a * c / b
let result_0 = a
.checked_mul[test]c(
.expect)"ERR_MUL"(
.checked_div)b(
.expect)"ERR_DIV"(;
// result_0 = a / b * c
let result_1 = a
.checked_div)b(
.expect)"ERR_DIV"(
.checked_mul)c(
.expect)"ERR_MUL"(;
assert_eq!)result_0,result_1,""(;
}
Les résultats des tests unitaires sont les suivants :
exécution d'un test
thread "tests::precision_test_0" a paniqué à "assertion échouée : )left == right(
gauche: 2, droite: 0: ", src/lib.rs:175:9
Nous pouvons constater que result_0 = a * c / b et result_1 = )a / b(* c bien qu'ils aient la même formule de calcul, les résultats des opérations sont différents.
Analyser les raisons spécifiques : pour la division entière, la précision inférieure au diviseur sera abandonnée. Ainsi, lors du calcul de result_1, le calcul )a / b( perdra d'abord la précision, devenant 0 ; tandis que pour le calcul de result_0, le résultat a * c sera d'abord calculé comme 20_0000, ce résultat étant supérieur au diviseur b, évitant ainsi le problème de perte de précision, ce qui permet d'obtenir le bon résultat.
) 2.2 trop petit ordre de grandeur
#(
fn precision_test_decimals)### {
let a: u128 = 10;
let b: u128 = 3;
let c: u128 = 4;
let decimal: u128 = 100_0000;
// result_0 = [test]a / b( * c
let result_0 = a
.checked_div)b(
.expect)"ERR_DIV"(
.checked_mul)c(
.expect)"ERR_MUL"(;
// result_1 = )a * decimal / b( * c / decimal;
let result_1 = a
.checked_mul)decimal( // mul decimal
.expect)"ERR_MUL"(
.checked_div)b(
.expect)"ERR_DIV"(
.checked_mul)c(
.expect)"ERR_MUL"(
.checked_div)decimal( // div decimal
.expect)"ERR_DIV"(;
println!)"{}:{}", result_0, result_1(;
assert_eq!)result_0, result_1, ""(;
}
Les résultats spécifiques de ce test unitaire sont les suivants :
exécution d'un test
12:13
thread "tests::precision_test_decimals" a paniqué à "assertion échouée : )left == right(
gauche: 12, droite: 13: ", src/lib.rs:214:9
Il est évident que les résultats des opérations result_0 et result_1, qui sont équivalents dans le processus de calcul, ne sont pas les mêmes, et que result_1 = 13 est beaucoup plus proche de la valeur de calcul réelle attendue : 13.3333....
3. Comment écrire des smart contracts Rust pour l'évaluation actuarielle des valeurs numériques
Il est très important de garantir la bonne précision dans les smart contracts. Bien qu'il existe également des problèmes de perte de précision dans les résultats des opérations entières en Rust, nous pouvons prendre certaines mesures de protection pour améliorer la précision et obtenir des résultats satisfaisants.
) 3.1 Ajuster l'ordre des opérations de calcul
Faire en sorte que la multiplication d'entiers ait la priorité sur la division d'entiers.
( 3.2 augmenter l'ordre de grandeur des entiers
Utiliser des puissances de dix plus grandes pour créer des numérateurs plus grands.
Par exemple, pour un jeton NEAR, si l'on définit N = 10 comme décrit ci-dessus, cela signifie : si l'on doit représenter la valeur NEAR de 5.123, la valeur entière utilisée pour le calcul sera représentée par 5.123 * 10^10 = 51_230_000_000. Cette valeur continuera à participer aux calculs entiers ultérieurs, ce qui peut améliorer la précision des calculs.
) 3.3 Perte de précision due à l'accumulation des opérations
En ce qui concerne les problèmes de précision des calculs entiers qui ne peuvent vraiment pas être évités, les équipes de projet peuvent envisager d'enregistrer la perte de précision cumulée.
u128 pour distribuer des jetons à USER_NUM utilisateurs.
const USER_NUM: u128 = 3;
u128 {
let token_to_distribute = offset + amount;
let per_user_share = token_to_distribute / USER_NUM;
println!###"per_user_share {}",per_user_share###;
let recorded_offset = token_to_distribute - per_user_share * USER_NUM;
recorded_offset
}
####
fn record_offset_test() {
let mut offset: u128 = 0;
pour i dans 1..7 {
println!("Round {}",i);
offset = distribute(to_yocto)"10"[test], offset(;
println!)"Offset {}\n",offset(;
}
}
Dans ce cas de test, le système distribue 10 tokens à 3 utilisateurs à chaque fois. Cependant, en raison des problèmes de précision des calculs entiers, lors du calcul de per_user_share au premier tour, le résultat du calcul entier obtenu est 10 / 3 = 3, c'est-à-dire que les utilisateurs distribués au premier tour recevront en moyenne 3 tokens, pour un total de 9 tokens distribués.
À ce moment, on peut constater qu'il reste 1 token non distribué aux utilisateurs dans le système. Pour cela, on peut envisager de conserver ce token restant temporairement dans la variable globale offset du système. Lors de la prochaine fois que le système appellera à nouveau distribute pour distribuer des tokens aux utilisateurs, cette valeur sera récupérée et tentera d'être distribuée avec le montant des tokens de cette ronde.
Voici le processus de distribution de tokens simulé :
exécution d'un test
Tour 1
part par utilisateur 3
Offset1
Tour 2
per_user_share 3
Décalage 2
Round 3
part par utilisateur 4
Décalage 0
Round 4
part par utilisateur 3
Décalage 1
Tour 5
per_user_share 3
Cette page peut inclure du contenu de tiers fourni à des fins d'information uniquement. Gate ne garantit ni l'exactitude ni la validité de ces contenus, n’endosse pas les opinions exprimées, et ne fournit aucun conseil financier ou professionnel à travers ces informations. Voir la section Avertissement pour plus de détails.
9 J'aime
Récompense
9
6
Partager
Commentaire
0/400
WalletWhisperer
· Il y a 5h
fascinant comment les points flottants de Rust pourraient être notre prochain pot de miel de vulnérabilité... à surveiller de près
Voir l'originalRépondre0
OnlyOnMainnet
· Il y a 5h
calcul de nombres à virgule flottante + off-chain 呵呵吓死我
Voir l'originalRépondre0
TopEscapeArtist
· Il y a 5h
Les gars, ce problème de précision est aussi précis que quand je suis au sommet.
Voir l'originalRépondre0
RamenDeFiSurvivor
· Il y a 6h
Je me tire, ce problème de précision est vraiment ennuyeux.
Voir l'originalRépondre0
NFTArchaeologist
· Il y a 6h
Le problème de précision est le plus mortel... Si mal géré, on peut perdre tout son capital.
Voir l'originalRépondre0
MaticHoleFiller
· Il y a 6h
Quand pourrons-nous écrire une collection de débogage ?
Calcul des valeurs numériques précises dans les smart contracts Rust : entiers vs flottants
Rust smart contracts养成日记(7):数值精算
Rétrospective :
1. Problèmes de précision dans les opérations sur les nombres à virgule flottante
Contrairement au langage de programmation de contrats intelligents courant, Solidity, le langage Rust prend en charge nativement les opérations sur les nombres à virgule flottante. Cependant, les opérations sur les nombres à virgule flottante présentent des problèmes de précision de calcul inévitables. Par conséquent, lors de la rédaction de contrats intelligents, il n'est pas recommandé d'utiliser des opérations sur les nombres à virgule flottante (, en particulier lors du traitement de ratios ou de taux d'intérêt impliquant des décisions économiques/financières importantes ).
Actuellement, la plupart des langages de programmation populaires qui représentent des nombres à virgule flottante suivent la norme IEEE 754, et le langage Rust ne fait pas exception. Voici une explication concernant le type à virgule flottante double précision f64 dans le langage Rust et la façon dont les données binaires sont stockées en interne dans l'ordinateur :
Les nombres à virgule flottante utilisent la notation scientifique en base 2 pour être exprimés. Par exemple, le nombre décimal 0.8125 peut être représenté par le nombre binaire à un nombre limité de chiffres 0.1101, la méthode de conversion spécifique est la suivante :
Cependant, pour un autre nombre décimal 0.7, il y aura les problèmes suivants lors de sa conversion réelle en nombre à virgule flottante :
Ainsi, le nombre décimal 0.7 sera représenté par 0.101100110011001100.....( en boucle infinie ), et ne peut pas être représenté avec précision par un nombre à virgule flottante de longueur finie, ce qui entraîne le phénomène de "Rounding(".
Supposons qu'il soit nécessaire de distribuer 0,7 NEAR token à dix utilisateurs sur la blockchain NEAR, la quantité de NEAR tokens que chaque utilisateur recevra sera calculée et enregistrée dans la variable result_0.
Le résultat de l'exécution de ce cas de test est comme suit :
Il est évident que dans les opérations à virgule flottante ci-dessus, la valeur de amount ne représente pas exactement 0.7, mais plutôt une valeur très proche de 0.69999999999999995559. De plus, pour une opération de division unique comme amount/divisor, le résultat de l'opération devient également imprécis à 0.06999999999999999, et non à 0.07 comme prévu. Cela illustre l'incertitude des opérations sur les nombres à virgule flottante.
À cet égard, nous devons envisager d'utiliser d'autres types de représentations numériques dans les smart contracts, comme les nombres à virgule fixe.
Dans la rédaction réelle des smart contracts, on utilise généralement une fraction avec un dénominateur fixe pour représenter une certaine valeur, par exemple la fraction "x/N", où "N" est une constante et "x" peut varier.
Si "N" prend la valeur de "1,000,000,000,000,000,000", c'est-à-dire "10^18", à ce moment-là, la décimale peut être représentée comme un entier, comme ceci :
Dans le protocole NEAR, la valeur courante de N est "10^24", c'est-à-dire que 10^24 yoctoNEAR équivaut à 1 jeton NEAR.
Sur cette base, nous pouvons modifier les tests unitaires de cette section pour effectuer les calculs de la manière suivante :
Cela permet d'obtenir le résultat de calcul actuariel suivant : 0,7 NEAR / 10 = 0,07 NEAR
![])https://img-cdn.gateio.im/webp-social/moments-7bdd27c1211e1cc345bf262666a993da.webp(
2. Problème de précision des calculs d'entiers en Rust
D'après la description du point 1 ci-dessus, on peut constater que l'utilisation d'opérations entières peut résoudre le problème de perte de précision des opérations à virgule flottante dans certains scénarios.
Mais cela ne signifie pas que les résultats calculés avec des entiers sont complètement précis et fiables. Cette section présentera certaines raisons qui influencent la précision des calculs entiers.
) 2.1 Ordre des opérations
Pour la multiplication et la division ayant la même priorité arithmétique, le changement de leur ordre peut directement influencer le résultat du calcul, entraînant des problèmes de précision dans les calculs entiers.
Par exemple, il existe les opérations suivantes :
Les résultats des tests unitaires sont les suivants :
Nous pouvons constater que result_0 = a * c / b et result_1 = )a / b(* c bien qu'ils aient la même formule de calcul, les résultats des opérations sont différents.
Analyser les raisons spécifiques : pour la division entière, la précision inférieure au diviseur sera abandonnée. Ainsi, lors du calcul de result_1, le calcul )a / b( perdra d'abord la précision, devenant 0 ; tandis que pour le calcul de result_0, le résultat a * c sera d'abord calculé comme 20_0000, ce résultat étant supérieur au diviseur b, évitant ainsi le problème de perte de précision, ce qui permet d'obtenir le bon résultat.
) 2.2 trop petit ordre de grandeur
.checked_div)b( .expect)"ERR_DIV"( .checked_mul)c( .expect)"ERR_MUL"(; // result_1 = )a * decimal / b( * c / decimal;
let result_1 = a .checked_mul)decimal( // mul decimal .expect)"ERR_MUL"( .checked_div)b( .expect)"ERR_DIV"( .checked_mul)c( .expect)"ERR_MUL"( .checked_div)decimal( // div decimal .expect)"ERR_DIV"(; println!)"{}:{}", result_0, result_1(; assert_eq!)result_0, result_1, ""(; }
Les résultats spécifiques de ce test unitaire sont les suivants :
Il est évident que les résultats des opérations result_0 et result_1, qui sont équivalents dans le processus de calcul, ne sont pas les mêmes, et que result_1 = 13 est beaucoup plus proche de la valeur de calcul réelle attendue : 13.3333....
![])https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp(
3. Comment écrire des smart contracts Rust pour l'évaluation actuarielle des valeurs numériques
Il est très important de garantir la bonne précision dans les smart contracts. Bien qu'il existe également des problèmes de perte de précision dans les résultats des opérations entières en Rust, nous pouvons prendre certaines mesures de protection pour améliorer la précision et obtenir des résultats satisfaisants.
) 3.1 Ajuster l'ordre des opérations de calcul
( 3.2 augmenter l'ordre de grandeur des entiers
Par exemple, pour un jeton NEAR, si l'on définit N = 10 comme décrit ci-dessus, cela signifie : si l'on doit représenter la valeur NEAR de 5.123, la valeur entière utilisée pour le calcul sera représentée par 5.123 * 10^10 = 51_230_000_000. Cette valeur continuera à participer aux calculs entiers ultérieurs, ce qui peut améliorer la précision des calculs.
) 3.3 Perte de précision due à l'accumulation des opérations
En ce qui concerne les problèmes de précision des calculs entiers qui ne peuvent vraiment pas être évités, les équipes de projet peuvent envisager d'enregistrer la perte de précision cumulée.
u128 pour distribuer des jetons à USER_NUM utilisateurs.
u128 { let token_to_distribute = offset + amount; let per_user_share = token_to_distribute / USER_NUM; println!###"per_user_share {}",per_user_share###; let recorded_offset = token_to_distribute - per_user_share * USER_NUM; recorded_offset } #### fn record_offset_test() { let mut offset: u128 = 0; pour i dans 1..7 { println!("Round {}",i); offset = distribute(to_yocto)"10"[test], offset(; println!)"Offset {}\n",offset(; } }
Dans ce cas de test, le système distribue 10 tokens à 3 utilisateurs à chaque fois. Cependant, en raison des problèmes de précision des calculs entiers, lors du calcul de per_user_share au premier tour, le résultat du calcul entier obtenu est 10 / 3 = 3, c'est-à-dire que les utilisateurs distribués au premier tour recevront en moyenne 3 tokens, pour un total de 9 tokens distribués.
À ce moment, on peut constater qu'il reste 1 token non distribué aux utilisateurs dans le système. Pour cela, on peut envisager de conserver ce token restant temporairement dans la variable globale offset du système. Lors de la prochaine fois que le système appellera à nouveau distribute pour distribuer des tokens aux utilisateurs, cette valeur sera récupérée et tentera d'être distribuée avec le montant des tokens de cette ronde.
Voici le processus de distribution de tokens simulé :