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 поток 'tests::precision_test_float' паниковал с сообщением 'проверка не удалась: (left == right) Слева: 0.0699999999999999, справа: 0.07: ', src/lib.rs:185:9

Можно увидеть, что значение amount не точно 0.7, а является приближенным значением 0.69999999999999995559. Результат дальнейшего деления также становится неточным 0.06999999999999999.

Чтобы решить эту проблему, можно рассмотреть использование фиксированной запятой. В протоколе NEAR обычно используется представление 10^24 йоктоNEAR, эквивалентного 1 токену NEAR.

Измененный тестовый код:

ржавчина #[test] fn precision_test_integer() { пусть N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000; пусть делитель: u128 = 10;
пусть result_0 = сумма / делитель; assert_eq!(result_0, 70_000_000_000_000_000_000_000_000, ""); }

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

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

2. Проблема точности вычислений целых чисел в Rust

2.1 порядок операций

Изменение порядка операций между умножением и делением с одинаковым приоритетом может непосредственно повлиять на результат вычисления, что приводит к проблемам с точностью целочисленных вычислений.

ржавчина #[test] fn precision_test_div_before_mul() { пусть a: u128 = 1_0000; пусть b: u128 = 10_0000; пусть c: 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() { пусть a: u128 = 10; пусть b: u128 = 3; пусть c: u128 = 4; пусть десятичная: u128 = 100_0000;

Пусть result_0 = a
    .checked_div(b)
    .expect("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 Потеря точности накопления вычислений

Для неизбежных проблем с точностью вычислений целых чисел можно рассмотреть возможность записи накопленных потерь точности вычислений.

ржавчина константа 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; recorded_offset }

#[test] FN record_offset_test() { let mut offset: u128 = 0; для i в 1..7 { println!("Round {}", i); смещение = distribute(10_000_000_000_000_000_000_000_000, offset); println!("Смещение {}\n", offset); } }

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

проведение 1 теста Раунд 1 per_user_share 3333333333333333333333333 Смещение 1

Раунд 2 per_user_share 3333333333333333333333333 Смещение 2

Раунд 3 per_user_share 400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Смещение 0

Раунд 4 per_user_share 3333333333333333333333333 Смещение 1

Раунд 5 per_user_share 3333333333333333333333333 Смещение 2

Раунд 6 per_user_share 400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Смещение 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
  • Закрепить