Щоденник розвитку смартконтрактів Rust (1) Визначення даних стану контракту та реалізація методів
Щоденник розвитку смартконтрактів на Rust (2) Написання юніт-тестів смартконтрактів на Rust
Щоденник розвитку смартконтрактів Rust(3)Розгортання смартконтрактів Rust, виклик функцій та використання Explorer
Rust смартконтракти виховання щоденник ( 4) Rust смартконтракти переповнення цілих чисел
Rust смартконтракти养成日记(5)重入攻击
Щоденник розвитку смартконтрактів Rust (6) атака відмови в обслуговуванні
1. Проблема точності обчислень з плаваючою комою
На відміну від звичайних смартконтрактів програмування мовою Solidity, мова Rust нативно підтримує операції з плаваючою комою. Однак, операції з плаваючою комою мають невідворотні проблеми з точністю обчислень. Тому, при написанні смартконтрактів, не рекомендується використовувати операції з плаваючою комою (, особливо при обробці співвідношень або відсоткових ставок, що стосуються важливих економічних/фінансових рішень ).
Наразі основні мови програмування для представлення чисел з плаваючою комою в основному дотримуються стандарту IEEE 754, мова Rust не є винятком. Нижче наведено опис типу з плаваючою комою подвійної точності f64 в мові Rust та форма зберігання двійкових даних в комп'ютері:
Числа з плаваючою комою використовують наукову нотацію з основою 2 для вираження. Наприклад, десяткове число 0.8125 можна представити за допомогою обмеженої кількості двійкових знаків 0.1101, конкретний спосіб перетворення наведено нижче:
Однак для іншого дробу 0.7 під час його фактичного перетворення на число з плаваючою комою виникнуть такі проблеми:
0,7 х 2 = 1. 4 // 0.1
0,4 х 2 = 0. 8 // 0.10
0,8 х 2 = 1. 6 // 0.101
0,6 х 2 = 1. 2 // 0.1011
0,2 х 2 = 0. 4 // 0.10110
0,4 х 2 = 0. 8 // 0.101100
0,8 х 2 = 1. 6 // 0.1011001
....
Отже, десяткове 0.7 буде представлене як 0.101100110011001100.....( безкінечно циклічно ), його не можна точно представити за допомогою кінцевої довжини чисел з плаваючою комою, і існує явище "округлення (Rounding)".
Припустимо, що на блокчейні NEAR потрібно розподілити 0.7 токена NEAR між десятьма користувачами, конкретна кількість токенів NEAR, яку отримає кожен користувач, буде обчислена та збережена у змінній result_0.
#[test]
fn precision_test_float() {
// Дійсні числа не можуть точно представляти цілі числа
let amount: f64 = 0.7; // Ця змінна amount представляє 0.7 токенів NEAR
let divisor: f64 = 10.0; // визначити дільник
let result_0 = a / b; // Виконання операції ділення з плаваючою комою
println!("Значення a: {:.20}", a);
assert_eq!(result_0, 0,07, ");
}
Результати виконання цього тестового випадку наведені нижче:
виконання 1 тесту
Значення a: 0.69999999999999995559
потік "tests::precision_test_float" вийшов з ладу з повідомленням "перевірка не вдалася: (left == right)
ліворуч: 0.06999999999999999, праворуч: 0.07: ", src/lib.rs:185:9
Як видно з наведених вище операцій з плаваючою комою, значення amount не точно представляє 0.7, а є надзвичайно близьким значенням 0.69999999999999995559. Далі, для таких одиничних операцій ділення, як amount/divisor, результат також стане неточним 0.06999999999999999, а не очікуваним 0.07. Це показує невизначеність операцій з плаваючими числами.
У зв'язку з цим, ми змушені розглянути використання інших типів числових представлень у смартконтрактах, таких як фіксована точка.
Залежно від фіксованого розташування десяткової крапки, фіксовані числа поділяються на фіксовані ( цілі ) та фіксовані ( дробові два види.
Фіксуючи десяткову крапку після найнижчого розряду числа, його називають фіксованим цілим числом.
У реальному написанні смартконтрактів зазвичай використовують дроби з фіксованим знаменником для представлення певного значення, наприклад, дроб "x/N", де "N" є константою, а "x" може змінюватися.
Якщо "N" дорівнює "1,000,000,000,000,000,000", тобто "10^18", у цьому випадку дробове число може бути представлене як ціле число, ось так:
У NEAR Protocol звичайне значення N становить "10^24", тобто 10^24 yoctoNEAR еквівалентно 1 токену NEAR.
На основі цього ми можемо змінити модульні тести цього підрозділу для обчислення наступним чином:
#)
fn precision_test_integer[test]( {
// По-перше, визначимо константу N, що позначає точність.
let N: u128 = 1_000_000_000_000_000_000_000_000; // тобто визначення 1 NEAR = 10^24 yoctoNEAR
// Ініціалізація amount, насправді в даний момент значення, яке представляє amount, дорівнює 700_000_000_000_000_000 / N = 0.7 NEAR;
Нехай кількість: U128 = 700_000_000_000_000_000; yoctoNEAR
// Ініціалізація дільника divisor
Нехай дільник: u128 = 10;
// Розрахунок: result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
// Фактично представляє 700_000_000_000_000_000_000_000 / N = 0.07 NEAR;
нехай result_0 = сума / дільник;
assert_eq!)result_0, 70_000_000_000_000_000_000, ""(;
}
З цього можна отримати результат чисельного обчислення: 0.7 NEAR / 10 = 0.07 NEAR
запуск 1 тесту
тести тестують::прецизійний_тест_ціле ... ок
тестовий результат: ок. 1 пройдено; 0 провалено; 0 ігноровано; 0 виміряно; 8 відфільтровано; закінчено за 0.00с
2. Проблема з точністю обчислень з цілими числами в Rust
З опису в пункті 1 вище можна помітити, що використання цілочисельних обчислень може вирішити проблему втрати точності обчислень з плаваючою комою в деяких сценаріях обчислень.
Але це не означає, що результати обчислень з цілими числами є абсолютно точними та надійними. У цьому підрозділі будуть розглянуті деякі причини, які впливають на точність обчислень з цілими числами.
) 2.1 порядок виконання операцій
Зміна порядку множення та ділення з однаковим пріоритетом арифметичних дій може безпосередньо вплинути на результат обчислення, що призводить до проблеми точності обчислень з цілими числами.
Наприклад, існує наступна операція:
####
fn precision_test_div_before_mul[test]( {
Нехай a: u128 = 1_0000;
нехай b: u128 = 10_0000;
Нехай С: U128 = 20;
result_0 = а * с / б
Нехай result_0 = a
.checked_mul)c(
.expect)"ERR_MUL"(
.checked_div)b(
.expect)"ERR_DIV"(;
result_0 = а / б * с
Нехай result_1 = a
.checked_div)b(
.expect)"ERR_DIV"(
.checked_mul)c(
.expect)"ERR_MUL"(;
assert_eq!)result_0,result_1,"(;
}
Результати виконання модульних тестів такі:
запуск 1 тесту
потік "tests::precision_test_0" панікував на "перевірка не пройшла: )left == right(
ліворуч: 2, праворуч: 0: ", src/lib.rs:175:9
Ми можемо помітити, що result_0 = a * c / b та result_1 = )a / b( * c, хоча їхні формули обчислення однакові, проте результати обчислень різні.
Аналіз конкретних причин: для цілочисельного ділення точність, менша за дільник, буде відкинута. Тому під час обчислення result_1 спочатку обчислюється )a / b(, яке спочатку втратить обчислювальну точність і стане 0; а під час обчислення result_0 спочатку буде отримано результат a * c, який дорівнює 20_0000, цей результат буде більшим за дільник b, тому проблема втрати точності буде уникнута, і можна отримати правильний обчислювальний результат.
запуск 1 тесту
12:13
потік "тести::precision_test_decimals" панікував з причини "помилка перевірки: )left == right(
ліворуч: 12, праворуч: 13: ", src/lib.rs:214:9
Як видно, результати обчислень result_0 і result_1, які є еквівалентними в обчислювальному процесі, не однакові, і result_1 = 13 набагато ближче до фактичного очікуваного значення обчислення: 13.3333....
3. Як написати Rust смартконтракти для числового актуарію
Гарантування правильної точності в смартконтракти є дуже важливим. Хоча в мові Rust також існує проблема втрати точності при цілочисельних операціях, ми можемо вжити такі захисні заходи для покращення точності та досягнення задовільного результату.
) 3.1 Налаштування порядку виконання операцій
Зробіть множення цілих чисел пріоритетом над діленням цілих чисел.
3.2 збільшити порядок числа
Цілі числа використовують більші порядки, створюючи більші чисельники.
Наприклад, для токена NEAR, якщо визначити N, як описано вище, рівним 10, це означає: якщо потрібно виразити вартість NEAR 5.123, то фактичне значення, яке використовується в обчисленнях, буде виражено як 5.123 * 10^10 = 51_230_000_000. Це значення продовжуватиме брати участь у наступних цілочисельних обчисленнях, що може підвищити точність розрахунків.
3.3 Втрата точності при накопленні обчислень
Щодо дійсно невідворотних проблем з точністю обчислень з цілими числами, команда проекту може розглянути можливість фіксації накопичених втрат точності обчислень.
u128 використовується для розподілу токенів серед USER_NUM користувачів.
У цьому тестовому випадку система кожного разу розподіляє 10 токенів трьом користувачам. Однак через проблеми з точністю цілочисельних обчислень, під час першого раунду при обчисленні per_user_share отриманий результат цілочисельних обчислень дорівнює 10 / 3 = 3, тобто у першому раунді користувачі отримають в середньому 3 токени, всього буде розподілено 9 токенів.
У цей момент можна помітити, що в системі залишився 1 токен, який не був розподілений користувачеві. Для цього можна розглянути можливість тимчасово зберегти цей залишковий токен у глобальній змінній системи offset. Чекаючи наступного виклику системи distribute для розподілу токенів користувачеві, це значення буде витягнуте та спробують розподілити його разом з сумою токенів, що розподіляються в цьому раунді.
Ця сторінка може містити контент третіх осіб, який надається виключно в інформаційних цілях (не в якості запевнень/гарантій) і не повинен розглядатися як схвалення його поглядів компанією Gate, а також як фінансова або професійна консультація. Див. Застереження для отримання детальної інформації.
9 лайків
Нагородити
9
6
Поділіться
Прокоментувати
0/400
WalletWhisperer
· 5год тому
цікаво, як плаваючі точки rust можуть стати нашою наступною вразливістю honeypot... уважно спостерігаю
Переглянути оригіналвідповісти на0
OnlyOnMainnet
· 5год тому
Вирахування з плаваючою комою+у блокчейні Хе-хе, налякав мене
Переглянути оригіналвідповісти на0
TopEscapeArtist
· 5год тому
Товариші, ця проблема з точністю така ж точна, як і те, що я стою на вершині.
Переглянути оригіналвідповісти на0
RamenDeFiSurvivor
· 5год тому
Пішов, пішов. Ця проблема з точністю справді дратує.
Переглянути оригіналвідповісти на0
NFTArchaeologist
· 5год тому
Проблема точності є найсмертельнішою... якщо не впораєшся, залишишся без грошей.
Точні числові обчислення в смартконтрактах Rust: цілі числа проти чисел з плаваючою комою
Rust смартконтракти养成日記(7): числовий розрахунок
Огляд попередніх випусків:
1. Проблема точності обчислень з плаваючою комою
На відміну від звичайних смартконтрактів програмування мовою Solidity, мова Rust нативно підтримує операції з плаваючою комою. Однак, операції з плаваючою комою мають невідворотні проблеми з точністю обчислень. Тому, при написанні смартконтрактів, не рекомендується використовувати операції з плаваючою комою (, особливо при обробці співвідношень або відсоткових ставок, що стосуються важливих економічних/фінансових рішень ).
Наразі основні мови програмування для представлення чисел з плаваючою комою в основному дотримуються стандарту IEEE 754, мова Rust не є винятком. Нижче наведено опис типу з плаваючою комою подвійної точності f64 в мові Rust та форма зберігання двійкових даних в комп'ютері:
Числа з плаваючою комою використовують наукову нотацію з основою 2 для вираження. Наприклад, десяткове число 0.8125 можна представити за допомогою обмеженої кількості двійкових знаків 0.1101, конкретний спосіб перетворення наведено нижче:
Однак для іншого дробу 0.7 під час його фактичного перетворення на число з плаваючою комою виникнуть такі проблеми:
Отже, десяткове 0.7 буде представлене як 0.101100110011001100.....( безкінечно циклічно ), його не можна точно представити за допомогою кінцевої довжини чисел з плаваючою комою, і існує явище "округлення (Rounding)".
Припустимо, що на блокчейні NEAR потрібно розподілити 0.7 токена NEAR між десятьма користувачами, конкретна кількість токенів NEAR, яку отримає кожен користувач, буде обчислена та збережена у змінній result_0.
Результати виконання цього тестового випадку наведені нижче:
Як видно з наведених вище операцій з плаваючою комою, значення amount не точно представляє 0.7, а є надзвичайно близьким значенням 0.69999999999999995559. Далі, для таких одиничних операцій ділення, як amount/divisor, результат також стане неточним 0.06999999999999999, а не очікуваним 0.07. Це показує невизначеність операцій з плаваючими числами.
У зв'язку з цим, ми змушені розглянути використання інших типів числових представлень у смартконтрактах, таких як фіксована точка.
У реальному написанні смартконтрактів зазвичай використовують дроби з фіксованим знаменником для представлення певного значення, наприклад, дроб "x/N", де "N" є константою, а "x" може змінюватися.
Якщо "N" дорівнює "1,000,000,000,000,000,000", тобто "10^18", у цьому випадку дробове число може бути представлене як ціле число, ось так:
У NEAR Protocol звичайне значення N становить "10^24", тобто 10^24 yoctoNEAR еквівалентно 1 токену NEAR.
На основі цього ми можемо змінити модульні тести цього підрозділу для обчислення наступним чином:
З цього можна отримати результат чисельного обчислення: 0.7 NEAR / 10 = 0.07 NEAR
! [])https://img-cdn.gateio.im/webp-social/moments-7bdd27c1211e1cc345bf262666a993da.webp(
2. Проблема з точністю обчислень з цілими числами в Rust
З опису в пункті 1 вище можна помітити, що використання цілочисельних обчислень може вирішити проблему втрати точності обчислень з плаваючою комою в деяких сценаріях обчислень.
Але це не означає, що результати обчислень з цілими числами є абсолютно точними та надійними. У цьому підрозділі будуть розглянуті деякі причини, які впливають на точність обчислень з цілими числами.
) 2.1 порядок виконання операцій
Зміна порядку множення та ділення з однаковим пріоритетом арифметичних дій може безпосередньо вплинути на результат обчислення, що призводить до проблеми точності обчислень з цілими числами.
Наприклад, існує наступна операція:
Результати виконання модульних тестів такі:
Ми можемо помітити, що result_0 = a * c / b та result_1 = )a / b( * c, хоча їхні формули обчислення однакові, проте результати обчислень різні.
Аналіз конкретних причин: для цілочисельного ділення точність, менша за дільник, буде відкинута. Тому під час обчислення result_1 спочатку обчислюється )a / b(, яке спочатку втратить обчислювальну точність і стане 0; а під час обчислення result_0 спочатку буде отримано результат a * c, який дорівнює 20_0000, цей результат буде більшим за дільник b, тому проблема втрати точності буде уникнута, і можна отримати правильний обчислювальний результат.
) 2.2 занадто малий порядок величини
Конкретні результати цього модульного тесту такі:
Як видно, результати обчислень result_0 і result_1, які є еквівалентними в обчислювальному процесі, не однакові, і result_1 = 13 набагато ближче до фактичного очікуваного значення обчислення: 13.3333....
! [])https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp(
3. Як написати Rust смартконтракти для числового актуарію
Гарантування правильної точності в смартконтракти є дуже важливим. Хоча в мові Rust також існує проблема втрати точності при цілочисельних операціях, ми можемо вжити такі захисні заходи для покращення точності та досягнення задовільного результату.
) 3.1 Налаштування порядку виконання операцій
3.2 збільшити порядок числа
Наприклад, для токена NEAR, якщо визначити N, як описано вище, рівним 10, це означає: якщо потрібно виразити вартість NEAR 5.123, то фактичне значення, яке використовується в обчисленнях, буде виражено як 5.123 * 10^10 = 51_230_000_000. Це значення продовжуватиме брати участь у наступних цілочисельних обчисленнях, що може підвищити точність розрахунків.
3.3 Втрата точності при накопленні обчислень
Щодо дійсно невідворотних проблем з точністю обчислень з цілими числами, команда проекту може розглянути можливість фіксації накопичених втрат точності обчислень.
u128 використовується для розподілу токенів серед USER_NUM користувачів.
У цьому тестовому випадку система кожного разу розподіляє 10 токенів трьом користувачам. Однак через проблеми з точністю цілочисельних обчислень, під час першого раунду при обчисленні per_user_share отриманий результат цілочисельних обчислень дорівнює 10 / 3 = 3, тобто у першому раунді користувачі отримають в середньому 3 токени, всього буде розподілено 9 токенів.
У цей момент можна помітити, що в системі залишився 1 токен, який не був розподілений користувачеві. Для цього можна розглянути можливість тимчасово зберегти цей залишковий токен у глобальній змінній системи offset. Чекаючи наступного виклику системи distribute для розподілу токенів користувачеві, це значення буде витягнуте та спробують розподілити його разом з сумою токенів, що розподіляються в цьому раунді.
Ось імітований процес розподілу токенів: