1. Проблема точности вычислений с плавающей запятой
В отличие от распространенного языка программирования смарт-контрактов Solidity, язык Rust нативно поддерживает операции с плавающей точкой. Однако операции с плавающей точкой имеют неотъемлемые проблемы с точностью вычислений. Поэтому при написании смарт-контрактов не рекомендуется использовать операции с плавающей точкой (, особенно при работе с коэффициентами или процентными ставками, связанными с важными экономическими/финансовыми решениями ).
В настоящее время основные языки программирования, представляющие числа с плавающей запятой, в основном следуют стандарту IEEE 754, и язык Rust не является исключением. Ниже приведено описание типа двойной точности с плавающей запятой f64 в языке Rust и форма хранения двоичных данных в компьютере:
Действительные числа выражаются в научной нотации с основанием 2. Например, десятичное число 0.8125 можно представить с помощью конечного двоичного числа 0.1101, конкретный способ преобразования следующий:
0.8125 * 2 = 1 .625 // 0.1 Получаем первую двоичную запятую равной 1
0.625 * 2 = 1 .25 // 0.11 Получаем 2-й двоичный десятичный знак 1
0.25 * 2 = 0 .5 // 0.110 Получаем 3-й двоичный знак после запятой равным 0
0.5 * 2 = 1 .0 // 0.1101 получаем 4-й двоичный знак после запятой равным 1
То есть 0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Однако для другого дробного числа 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.0699999999999999, справа: 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;
let amount: u128 = 700_000_000_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_000_000, ""(;
}
С помощью этого можно получить числовой расчет: 0,7 NEAR / 10 = 0,07 NEAR
запускается 1 тест
тест тесты::precision_test_integer ... ок
результат теста: хорошо. 1 пройден; 0 провален; 0 проигнорировано; 0 измерено; 8 отфильтровано; завершено за 0.00 с
2. Проблема точности вычислений с целыми числами в Rust
Из описания в первом пункте выше можно заметить, что использование целочисленных операций может решить проблему потери точности операций с плавающей запятой в некоторых сценариях.
Но это не означает, что результаты, полученные с использованием целочисленных вычислений, полностью точны и надежны. В этом разделе будут рассмотрены некоторые причины, влияющие на точность целочисленных вычислений.
) 2.1 Порядок операций
Изменение порядка выполнения операций умножения и деления с одинаковым приоритетом может напрямую повлиять на результат вычислений, что приведет к проблемам с точностью целочисленных расчетов.
Например, существует следующая операция:
####
fn precision_test_div_before_mul[test]( {
пусть a: u128 = 1_0000;
пусть b: u128 = 10_0000;
пусть c: u128 = 20;
result_0 = а * c / b
Пусть result_0 = a
.checked_mul)c(
.expect)"ERR_MUL"(
.checked_div)b(
.expect)"ERR_DIV"(;
result_0 = a / b * c
Пусть 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
поток "tests::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 токенов среди 3 пользователей. Однако из-за проблемы с точностью целочисленных вычислений, при расчете per_user_share в первом раунде, полученный результат целочисленных вычислений составил 10 / 3 = 3, то есть пользователи в первом раунде распределения в среднем получат по 3 токена, всего будет распределено 9 токенов.
В данный момент можно заметить, что в системе еще остался 1 токен, который не был распределен пользователю. В связи с этим можно рассмотреть возможность временного сохранения оставшегося токена в глобальной переменной системы offset. При следующем вызове системы для распределения токенов пользователю это значение будет извлечено и попытается быть распределено вместе с количеством токенов, распределяемых в этом раунде.
Ниже представлен смоделированный процесс распределения токенов:
На этой странице может содержаться сторонний контент, который предоставляется исключительно в информационных целях (не в качестве заявлений/гарантий) и не должен рассматриваться как поддержка взглядов компании Gate или как финансовый или профессиональный совет. Подробности смотрите в разделе «Отказ от ответственности» .
9 Лайков
Награда
9
6
Поделиться
комментарий
0/400
WalletWhisperer
· 20ч назад
удивительно, как плавающая запятая rust может стать нашей следующей уязвимостью приманкой... внимательно следим
Посмотреть ОригиналОтветить0
OnlyOnMainnet
· 20ч назад
Вычисление с плавающей запятой + в блокчейне Хе-хе, напугал меня
Посмотреть ОригиналОтветить0
TopEscapeArtist
· 20ч назад
Ребята, эта проблема с точностью такая же точная, как и то, что я наступаю на вершину.
Посмотреть ОригиналОтветить0
RamenDeFiSurvivor
· 21ч назад
Убегаю, убегаю. Эта проблема с точностью действительно беспокоит.
Посмотреть ОригиналОтветить0
NFTArchaeologist
· 21ч назад
Проблемы с точностью являются наиболее смертельными... если не повезет, можно потерять все.
Точное численное вычисление в смарт-контрактах 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
Из описания в первом пункте выше можно заметить, что использование целочисленных операций может решить проблему потери точности операций с плавающей запятой в некоторых сценариях.
Но это не означает, что результаты, полученные с использованием целочисленных вычислений, полностью точны и надежны. В этом разделе будут рассмотрены некоторые причины, влияющие на точность целочисленных вычислений.
) 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 токенов среди 3 пользователей. Однако из-за проблемы с точностью целочисленных вычислений, при расчете per_user_share в первом раунде, полученный результат целочисленных вычислений составил 10 / 3 = 3, то есть пользователи в первом раунде распределения в среднем получат по 3 токена, всего будет распределено 9 токенов.
В данный момент можно заметить, что в системе еще остался 1 токен, который не был распределен пользователю. В связи с этим можно рассмотреть возможность временного сохранения оставшегося токена в глобальной переменной системы offset. При следующем вызове системы для распределения токенов пользователю это значение будет извлечено и попытается быть распределено вместе с количеством токенов, распределяемых в этом раунде.
Ниже представлен смоделированный процесс распределения токенов: