Rust смартконтракти чисельного розрахунку: пастки з плаваючою крапкою та оптимізація точності цілого числа

Rust смартконтракти养成日记(7)数值精算

1. Проблема точності обчислень з плаваючою комою

Мова Rust нативно підтримує операції з плаваючою комою, але операції з плаваючою комою мають невідворотні проблеми з точністю обчислень. При написанні смартконтрактів не рекомендується використовувати операції з плаваючою комою, особливо при обробці співвідношень або відсоткових ставок, які стосуються важливих економічних/фінансових рішень.

У мові Rust числові значення з плаваючою комою відповідають стандарту IEEE 754. Наприклад, для типу з подвійною точністю f64 його внутрішнє двійкове подання виглядає так:

!

Числа з плаваючою комою виражаються у науковій нотації з основою 2. Наприклад, 0.8125 можна представити обмеженою кількістю двійкових цифр 0.1101:

0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1

Однак для таких малих дробів, як 0.7, може виникнути ситуація безкінечного повторення:

0.7 = 0.1011001100110011...

Це призводить до того, що неможливо точно представити з обмеженою кількістю знаків з плаваючою комою, існує "округлення".

Наприклад, розподілити 0.7 NEAR токенів десятьом користувачам на блокчейні NEAR:

іржа #[test] fn precision_test_float() { Нехай кількість: f64 = 0,7;
Нехай дільник: f64 = 10,0;
нехай result_0 = сума / дільник;
println!("Значення суми: {:.20}", amount); assert_eq!(result_0, 0,07, "); }

Результат виконання:

проводиться 1 тест Значення суми: 0.69999999999999995559 потік 'тести::precision_test_float' вийшов з ладу з повідомленням 'перевірка не вдалася: (left == right) ліворуч: 0.06999999999999999, праворуч: 0.07: ', src/lib.rs:185:9

Як видно, значення amount не є точним 0.7, а є приблизним значенням 0.69999999999999995559. Подальші результати ділення також стають неточними 0.06999999999999999.

Щоб вирішити цю проблему, можна розглянути використання фіксованих чисел. У NEAR Protocol зазвичай використовують спосіб представлення, де 10^24 йоктоNEAR дорівнює 1 токену NEAR.

Виправлений тестовий код:

іржава #[test] fn precision_test_integer() { нехай N: u128 = 1_000_000_000_000_000_000_000;
Нехай кількість: U128 = 700_000_000_000_000_000; Нехай дільник: u128 = 10;
нехай result_0 = сума / дільник; assert_eq!(result_0, 70_000_000_000_000_000_000, ""); }

Результат виконання:

запуск 1 тесту тест тестує::точність_тест_ціле_число ... ок результат тесту: ок. 1 пройшов; 0 не пройшов; 0 проігноровано; 0 виміряно; 8 відфільтровано; завершено за 0.00с

2. Проблема точності обчислень цілих чисел у Rust

2.1 порядок виконання

Зміна порядку виконання множення та ділення з однаковим пріоритетом може безпосередньо вплинути на результат обчислення, що призводить до проблеми точності обчислень з цілими числами.

ржавчина #[test] fn precision_test_div_before_mul() { Нехай a: u128 = 1_0000; нехай b: u128 = 10_0000; Нехай С: U128 = 20;

Нехай result_0 = a
    .checked_mul(c)
    .expect("ERR_MUL")
    .checked_div(b)
    .expect("ERR_DIV");

Нехай 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, що запобігає втраті точності.

2.2 занадто малий масштаб

іржа #[test] fn precision_test_decimals() { Нехай А: U128 = 10; нехай b: u128 = 3; Нехай c: u128 = 4; Нехай десятковий дріб: u128 = 100_0000;

Нехай result_0 = a
    .checked_div(b)
    .очікувати("ERR_DIV")
    .checked_mul(c)
    .expect("ERR_MUL");

Нехай result_1 = a
    .checked_mul(decimal)  
    .expect("ERR_MUL")
    .checked_div(b)
    .expect("ERR_DIV")
    .checked_mul(c)
    .expect("ERR_MUL")
    .checked_div(decimal)  
    .expect("ERR_DIV");

println!("{}:{}", result_0, result_1);
assert_eq!(result_0, result_1, ");

}

Результат виконання:

виконання 1 тесту 12:13 потік 'tests::precision_test_decimals' зазнав збою внаслідок 'помилка твердження: (left == right) ліворуч: 12, праворуч: 13: ', src/lib.rs:214:9

Очевидно, що результати result_0 і result_1, які є еквівалентними за процесом обчислення, різні, і result_1 = 13 наближається до фактичного очікування 13.3333....

3. Як написати Rust смартконтракт для числового актуарію

3.1 Зміна порядку виконання операцій

  • Нехай множення цілих чисел має пріоритет над діленням цілих чисел.

3.2 збільшити кількість порядків

  • Цілі числа використовують більші порядки величини, створюючи більші чисельники.

3.3 втрата точності накопичувальних обчислень

Щодо невідворотних проблем з точністю обчислень цілих чисел, можна розглянути можливість фіксації накопичених втрат точності обчислень.

іржава const USER_NUM: u128 = 3;

FN distribute(amount: U128, зміщення: u128) -> u128 { Нехай token_to_distribute = зсув + сума; Нехай per_user_share = token_to_distribute / USER_NUM; println!("per_user_share {}", per_user_share); нехай recorded_offset = token_to_distribute - per_user_share * USER_NUM; записаний_зсув }

#[test] fn record_offset_test() { нехай mut зміщення: u128 = 0; для i в 1..7 { println!("Round {}", i); зсув = distribute(10_000_000_000_000_000_000_000_000, offset); println!("Offset {}\n", offset); } }

Результат виконання:

проводиться 1 тест Раунд 1 per_user_share 3333333333333333333333333 Зсув 1

Раунд 2 per_user_share 3333333333333333333333333 Зміщення 2

Раунд 3 per_user_share 400000000000000000 Зсув 0

Раунд 4 per_user_share 3333333333333333333333333 Зміщення 1

Раунд 5 per_user_share 3333333333333333333333333 Зсув 2

Раунд 6 per_user_share 400000000000000000 Зсув 0

тест тести::record_offset_test ... ок результат тесту: ок. 1 пройдено; 0 не вдалося; 0 проігноровано; 0 виміряно; 9 відфільтровано; завершено за 0.00с

3.4 Використання бібліотеки Rust Crate rust-decimal

Ця бібліотека підходить для фінансових обчислень з малими числами, які потребують точної обчислювальної точності та не мають помилок округлення.

3.5 Розглянути механізм округлення

При проектуванні смартконтрактів проблема округлення зазвичай ґрунтується на принципі "я хочу виграти, інші не повинні забирати моє". Згідно з цим принципом, якщо округлення вниз вигідне для мене, то округляємо вниз; якщо округлення вгору вигідне для мене, то округляємо вгору; округлення до найближчого цілого не може бути визначено, кому воно вигідне, тому рідко застосовується.

!

!

Переглянути оригінал
Ця сторінка може містити контент третіх осіб, який надається виключно в інформаційних цілях (не в якості запевнень/гарантій) і не повинен розглядатися як схвалення його поглядів компанією Gate, а також як фінансова або професійна консультація. Див. Застереження для отримання детальної інформації.
  • Нагородити
  • 5
  • Поділіться
Прокоментувати
0/400
LiquidationKingvip
· 07-16 06:01
Те, що ти кажеш про 0.7, не є великою справою. Великий крах був би цікавішим.
Переглянути оригіналвідповісти на0
BankruptcyArtistvip
· 07-16 05:57
Ай, писати смартконтракти дійсно було жахливо через плаваючі числа.
Переглянути оригіналвідповісти на0
GmGmNoGnvip
· 07-16 05:55
Цей баг може зіпсувати мою точність.
Переглянути оригіналвідповісти на0
NotSatoshivip
· 07-16 05:55
У смартконтрактах грати з плаваючою комою, голова розірветься.
Переглянути оригіналвідповісти на0
SandwichVictimvip
· 07-16 05:40
Проблема точності Шредінгера
Переглянути оригіналвідповісти на0
  • Закріпити