Problemas de precisión en los cálculos numéricos en el desarrollo de contratos inteligentes en Rust
1. Problemas de precisión en operaciones de punto flotante
El lenguaje Rust admite de forma nativa operaciones de punto flotante, pero estas operaciones presentan problemas de precisión de cálculo inevitables. Al escribir contratos inteligentes, no se recomienda el uso de operaciones de punto flotante, especialmente al manejar tasas o intereses que impliquen decisiones económicas/financieras importantes.
El tipo de punto flotante de doble precisión f64 en el lenguaje Rust sigue el estándar IEEE 754 y utiliza la notación científica con una base de 2. Algunos decimales no se pueden representar con precisión utilizando un número finito de bits de punto flotante, lo que da lugar a fenómenos de "redondeo".
Por ejemplo, al distribuir 0.7 tokens NEAR a 10 usuarios en la cadena pública NEAR:
óxido
#[test]
fn precision_test_float() {
let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("El valor de la cantidad: {:.20}", amount);
assert_eq!(result_0, 0.07, "");
}
El resultado de salida muestra que el valor de amount es 0.69999999999999995559, no el exacto 0.7. El resultado de la operación de división también se convierte en 0.06999999999999999, en lugar del esperado 0.07.
Para resolver este problema, se puede considerar el uso de números de punto fijo. En el NEAR Protocol, se utiliza comúnmente 10^24 yoctoNEAR para representar 1 token NEAR. Código de prueba modificado:
óxido
#[test]
fn precision_test_integer() {
let N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000;
let divisor: u128 = 10;
let result_0 = amount / divisor;
assert_eq!(result_0, 70_000_000_000_000_000_000_000, "");
}
Así se puede obtener un resultado de cálculo preciso: 0.7 NEAR / 10 = 0.07 NEAR.
2. Problemas de precisión en los cálculos enteros de Rust
El uso de operaciones enteras puede resolver el problema de pérdida de precisión en operaciones de punto flotante en ciertos escenarios, pero los resultados de los cálculos enteros tampoco son completamente precisos y confiables.
2.1 Orden de operaciones
El cambio en el orden de multiplicación y división con la misma prioridad aritmética puede afectar directamente el resultado del cálculo, lo que lleva a problemas de precisión en los cálculos enteros. Por ejemplo:
óxido
#[test]
fn precision_test_div_before_mul() {
let a: u128 = 1_0000;
let b: u128 = 10_0000;
let c: u128 = 20;
let result_0 = a.checked_mul(c).expect("ERR_MUL").checked_div(b).expect("ERR_DIV");
let result_1 = a.checked_div(b).expect("ERR_DIV").checked_mul(c).expect("ERR_MUL");
assert_eq!(result_0,result_1,"");
}
El resultado de la ejecución muestra que result_0 y result_1 no son iguales. La razón es que la división entera descarta la precisión menor que el divisor. Al calcular result_1, (a / b) pierde primero precisión y se convierte en 0; mientras que al calcular result_0, primero se calcula a * c para evitar la pérdida de precisión.
2.2 cantidad demasiado pequeña
Cuando se trata de cálculos decimales, las operaciones enteras pueden provocar una pérdida de precisión:
óxido
#[test]
fn precision_test_decimals() {
let a: u128 = 10;
let b: u128 = 3;
let c: u128 = 4;
let decimal: u128 = 100_0000;
let result_0 = a.checked_div(b).expect("ERR_DIV").checked_mul(c).expect("ERR_MUL");
let 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, "");
}
Los resultados muestran result_0=12, result_1=13, mientras que el valor esperado real debería ser 13.3333....
3. Cómo escribir contratos inteligentes de cálculo actuarial numérico en Rust
Para mejorar la precisión, se pueden tomar las siguientes medidas de protección:
3.1 Ajustar el orden de las operaciones
Hacer que la multiplicación de enteros tenga prioridad sobre la división de enteros.
3.2 aumentar el orden de magnitud de los enteros
Usar un orden de magnitud mayor para crear un numerador más grande. Por ejemplo, expresar 5.123 NEAR como 5.123 * 10^10 = 51_230_000_000 para participar en cálculos posteriores.
3.3 Pérdida de precisión en los cálculos acumulativos
Para los problemas de precisión inevitables, se puede registrar la pérdida acumulada de precisión en los cálculos. Por ejemplo:
óxido
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)[test] {
let mut offset: u128 = 0;
for i in 1..7 {
println!("Round {}", i);
offset = distribute(to_yocto)"10"(, offset(;
println!)"Offset {}\n", offset);
}
}
Este método puede acumular los tokens restantes de cada distribución y entregarlos juntos en la próxima distribución, con el objetivo final de alcanzar la entrega completa.
( 3.4 Uso de la biblioteca Rust Crate rust-decimal
Esta biblioteca es adecuada para cálculos financieros decimales que requieren una precisión efectiva y no tienen errores de redondeo.
) 3.5 Considerar el mecanismo de redondeo
En el diseño de contratos inteligentes, el problema de redondeo generalmente se aborda con el principio de "quiero beneficiarme, los demás no deben aprovecharse de mí". Según la situación, se elige redondear hacia abajo o hacia arriba, raramente se utiliza el redondeo estándar.
Esta página puede contener contenido de terceros, que se proporciona únicamente con fines informativos (sin garantías ni declaraciones) y no debe considerarse como un respaldo por parte de Gate a las opiniones expresadas ni como asesoramiento financiero o profesional. Consulte el Descargo de responsabilidad para obtener más detalles.
10 me gusta
Recompensa
10
8
Compartir
Comentar
0/400
rugpull_survivor
· 07-14 15:34
El problema de redondeo ha afectado a muchas personas.
Ver originalesResponder0
WhaleWatcher
· 07-14 07:46
El control de precisión es clave.
Ver originalesResponder0
consensus_failure
· 07-13 12:52
Usar una precisión fija es más seguro
Ver originalesResponder0
Ser_Liquidated
· 07-12 19:41
Si hay otro problema de precisión, se obtendrá liquidación.
Ver originalesResponder0
DecentralizeMe
· 07-11 16:13
Usa números de punto fijo.
Ver originalesResponder0
BoredWatcher
· 07-11 15:58
El código es un poco confuso.
Ver originalesResponder0
BrokenDAO
· 07-11 15:53
La sobrecapacidad total de potencia computacional es un problema.
Desarrollo de contratos inteligentes en Rust: superando el problema de precisión en cálculos numéricos
Problemas de precisión en los cálculos numéricos en el desarrollo de contratos inteligentes en Rust
1. Problemas de precisión en operaciones de punto flotante
El lenguaje Rust admite de forma nativa operaciones de punto flotante, pero estas operaciones presentan problemas de precisión de cálculo inevitables. Al escribir contratos inteligentes, no se recomienda el uso de operaciones de punto flotante, especialmente al manejar tasas o intereses que impliquen decisiones económicas/financieras importantes.
El tipo de punto flotante de doble precisión f64 en el lenguaje Rust sigue el estándar IEEE 754 y utiliza la notación científica con una base de 2. Algunos decimales no se pueden representar con precisión utilizando un número finito de bits de punto flotante, lo que da lugar a fenómenos de "redondeo".
Por ejemplo, al distribuir 0.7 tokens NEAR a 10 usuarios en la cadena pública NEAR:
óxido #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("El valor de la cantidad: {:.20}", amount); assert_eq!(result_0, 0.07, ""); }
El resultado de salida muestra que el valor de amount es 0.69999999999999995559, no el exacto 0.7. El resultado de la operación de división también se convierte en 0.06999999999999999, en lugar del esperado 0.07.
Para resolver este problema, se puede considerar el uso de números de punto fijo. En el NEAR Protocol, se utiliza comúnmente 10^24 yoctoNEAR para representar 1 token NEAR. Código de prueba modificado:
óxido #[test] fn precision_test_integer() { let N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000; let divisor: u128 = 10;
let result_0 = amount / divisor; assert_eq!(result_0, 70_000_000_000_000_000_000_000, ""); }
Así se puede obtener un resultado de cálculo preciso: 0.7 NEAR / 10 = 0.07 NEAR.
2. Problemas de precisión en los cálculos enteros de Rust
El uso de operaciones enteras puede resolver el problema de pérdida de precisión en operaciones de punto flotante en ciertos escenarios, pero los resultados de los cálculos enteros tampoco son completamente precisos y confiables.
2.1 Orden de operaciones
El cambio en el orden de multiplicación y división con la misma prioridad aritmética puede afectar directamente el resultado del cálculo, lo que lleva a problemas de precisión en los cálculos enteros. Por ejemplo:
óxido #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
}
El resultado de la ejecución muestra que result_0 y result_1 no son iguales. La razón es que la división entera descarta la precisión menor que el divisor. Al calcular result_1, (a / b) pierde primero precisión y se convierte en 0; mientras que al calcular result_0, primero se calcula a * c para evitar la pérdida de precisión.
2.2 cantidad demasiado pequeña
Cuando se trata de cálculos decimales, las operaciones enteras pueden provocar una pérdida de precisión:
óxido #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;
}
Los resultados muestran result_0=12, result_1=13, mientras que el valor esperado real debería ser 13.3333....
3. Cómo escribir contratos inteligentes de cálculo actuarial numérico en Rust
Para mejorar la precisión, se pueden tomar las siguientes medidas de protección:
3.1 Ajustar el orden de las operaciones
Hacer que la multiplicación de enteros tenga prioridad sobre la división de enteros.
3.2 aumentar el orden de magnitud de los enteros
Usar un orden de magnitud mayor para crear un numerador más grande. Por ejemplo, expresar 5.123 NEAR como 5.123 * 10^10 = 51_230_000_000 para participar en cálculos posteriores.
3.3 Pérdida de precisión en los cálculos acumulativos
Para los problemas de precisión inevitables, se puede registrar la pérdida acumulada de precisión en los cálculos. Por ejemplo:
óxido 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)[test] { let mut offset: u128 = 0; for i in 1..7 { println!("Round {}", i); offset = distribute(to_yocto)"10"(, offset(; println!)"Offset {}\n", offset); } }
Este método puede acumular los tokens restantes de cada distribución y entregarlos juntos en la próxima distribución, con el objetivo final de alcanzar la entrega completa.
( 3.4 Uso de la biblioteca Rust Crate rust-decimal
Esta biblioteca es adecuada para cálculos financieros decimales que requieren una precisión efectiva y no tienen errores de redondeo.
) 3.5 Considerar el mecanismo de redondeo
En el diseño de contratos inteligentes, el problema de redondeo generalmente se aborda con el principio de "quiero beneficiarme, los demás no deben aprovecharse de mí". Según la situación, se elige redondear hacia abajo o hacia arriba, raramente se utiliza el redondeo estándar.
![]###https://img-cdn.gateio.im/webp-social/moments-6e8b4081214a69423fc7ae022d05c728.webp###