Diario de desarrollo de contratos inteligentes en Rust (7) Cálculo numérico
1. Problemas de precisión en las operaciones con números de punto flotante
El lenguaje Rust admite operaciones de punto flotante de forma nativa, pero estas operaciones presentan problemas de precisión de cálculo que son inevitables. Al escribir contratos inteligentes, no se recomienda utilizar operaciones de punto flotante, especialmente al tratar con tasas o intereses que implican decisiones económicas/financieras importantes.
En el lenguaje Rust, los números de punto flotante siguen el estándar IEEE 754. Tomando como ejemplo el tipo de punto flotante de doble precisión f64, su representación binaria interna es la siguiente:
Los números de punto flotante se expresan en notación científica de base 2. Por ejemplo, 0.8125 se puede representar con el número binario de un número finito de bits 0.1101:
Sin embargo, para decimales tan pequeños como 0.7, puede ocurrir una situación de ciclo infinito:
0.7 = 0.1011001100110011...
Esto lleva a que no se pueda representar con precisión utilizando números de punto flotante de longitud limitada, existiendo el fenómeno de "redondeo".
Tomando como ejemplo la distribución de 0.7 tokens NEAR a diez usuarios en la cadena de bloques 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, "");
}
Resultado de la ejecución:
ejecutando 1 prueba
El valor de la cantidad: 0.69999999999999995559
hilo 'tests::precision_test_float' entró en pánico en 'la afirmación falló: (left == right)
izquierda: 0.06999999999999999, derecha: 0.07: ', src/lib.rs:185:9
Se puede ver que el valor de amount no es exactamente 0.7, sino un valor aproximado de 0.69999999999999995559. El resultado de la operación de división también se convierte en un valor inexacto de 0.06999999999999999.
Para resolver este problema, se puede considerar el uso de números de punto fijo. En el Protocolo NEAR, generalmente se utiliza el método de representación donde 10^24 yoctoNEAR es equivalente a 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, "");
}
Resultado de la ejecución:
ejecutando 1 prueba
test tests::precision_test_integer ... ok
resultado de la prueba: ok. 1 aprobado; 0 fallido; 0 ignorado; 0 medido; 8 filtrados; terminado en 0.00s
2. Problema de precisión en los cálculos enteros de Rust
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 el cálculo de enteros.
óxido
#[test]
fn precision_test_div_before_mul() {
let a: u128 = 1_0000;
let b: u128 = 10_0000;
let c: u128 = 20;
let result_1 = a
.checked_div(b)
.expect("ERR_DIV")
.checked_mul(c)
.expect("ERR_MUL");
assert_eq!(result_0,result_1,"");
}
Resultado de la ejecución:
ejecutando 1 prueba
el hilo 'tests::precision_test_0' se ha bloqueado en 'falló la afirmación: (left == right)
izquierda: 2, derecha: 0: ', src/lib.rs:175:9
Se puede observar que result_0 = a * c / b y result_1 = (a / b) * c, aunque las fórmulas de cálculo son las mismas, los resultados son diferentes. La razón es que la división entera descartará la precisión que es menor que el divisor. En el cálculo de result_1, calcular primero (a / b) provoca que se pierda precisión y se convierta en 0; mientras que en el cálculo de result_0, primero se calcula a * c = 20_0000, que es mayor que el divisor b, evitando así la pérdida de precisión.
2.2 magnitud demasiado pequeña
ó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 result_0 y result_1, que son equivalentes en el proceso de cálculo, son diferentes, y result_1 = 13 está más cerca de la expectativa real de 13.3333....
3. Cómo escribir contratos inteligentes de evaluación actuarial numérica en Rust
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
Los enteros utilizan un mayor orden de magnitud, creando numeradores más grandes.
3.3 Pérdida de precisión en operaciones acumulativas
Para los problemas de precisión en cálculos enteros que no se pueden evitar, se puede considerar registrar la pérdida acumulada de precisión en los cálculos.
ó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 = distribuir(10_000_000_000_000_000_000_000_000, offset);
println!("Offset {}\n", offset);
}
}
test tests::record_offset_test ... ok
resultado de la prueba: ok. 1 aprobado; 0 fallido; 0 ignorado; 0 medido; 9 filtrados; terminado en 0.00s
( 3.4 Uso de la biblioteca Rust Crate rust-decimal
Esta biblioteca es adecuada para cálculos financieros en decimales que requieren una precisión efectiva y no tienen errores de redondeo.
) 3.5 Considerar el mecanismo de redondeo
Al diseñar contratos inteligentes, el problema de redondeo generalmente sigue el principio de "quiero aprovecharme, los demás no deben aprovecharse de mí". Según este principio, si redondear hacia abajo me beneficia, entonces se hace hacia abajo; si redondear hacia arriba me beneficia, entonces se hace hacia arriba; el redondeo al más cercano no puede determinar a quién le beneficia, por lo tanto, se utiliza muy poco.
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.
15 me gusta
Recompensa
15
5
Compartir
Comentar
0/400
LiquidationKing
· hace20h
Lo que dices de 0.7 no es nada importante, un gran colapso sería emocionante.
Ver originalesResponder0
BankruptcyArtist
· hace20h
Ay, realmente he sido estafado por los números de punto flotante al escribir contratos inteligentes.
Ver originalesResponder0
GmGmNoGn
· hace20h
Este bug puede hacer que pierda toda mi precisión.
Ver originalesResponder0
NotSatoshi
· hace20h
En contratos inteligentes jugar con puntos flotantes puede hacer que la cabeza explote.
Rust contratos inteligentes cálculo numérico: trampas de punto flotante y optimización de precisión entera
Diario de desarrollo de contratos inteligentes en Rust (7) Cálculo numérico
1. Problemas de precisión en las operaciones con números de punto flotante
El lenguaje Rust admite operaciones de punto flotante de forma nativa, pero estas operaciones presentan problemas de precisión de cálculo que son inevitables. Al escribir contratos inteligentes, no se recomienda utilizar operaciones de punto flotante, especialmente al tratar con tasas o intereses que implican decisiones económicas/financieras importantes.
En el lenguaje Rust, los números de punto flotante siguen el estándar IEEE 754. Tomando como ejemplo el tipo de punto flotante de doble precisión f64, su representación binaria interna es la siguiente:
Los números de punto flotante se expresan en notación científica de base 2. Por ejemplo, 0.8125 se puede representar con el número binario de un número finito de bits 0.1101:
0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Sin embargo, para decimales tan pequeños como 0.7, puede ocurrir una situación de ciclo infinito:
0.7 = 0.1011001100110011...
Esto lleva a que no se pueda representar con precisión utilizando números de punto flotante de longitud limitada, existiendo el fenómeno de "redondeo".
Tomando como ejemplo la distribución de 0.7 tokens NEAR a diez usuarios en la cadena de bloques 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, ""); }
Resultado de la ejecución:
ejecutando 1 prueba El valor de la cantidad: 0.69999999999999995559 hilo 'tests::precision_test_float' entró en pánico en 'la afirmación falló: (left == right) izquierda: 0.06999999999999999, derecha: 0.07: ', src/lib.rs:185:9
Se puede ver que el valor de amount no es exactamente 0.7, sino un valor aproximado de 0.69999999999999995559. El resultado de la operación de división también se convierte en un valor inexacto de 0.06999999999999999.
Para resolver este problema, se puede considerar el uso de números de punto fijo. En el Protocolo NEAR, generalmente se utiliza el método de representación donde 10^24 yoctoNEAR es equivalente a 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, ""); }
Resultado de la ejecución:
ejecutando 1 prueba test tests::precision_test_integer ... ok resultado de la prueba: ok. 1 aprobado; 0 fallido; 0 ignorado; 0 medido; 8 filtrados; terminado en 0.00s
2. Problema de precisión en los cálculos enteros de Rust
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 el cálculo de enteros.
óxido #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
.checked_mul(c) .expect("ERR_MUL") .checked_div(b) .expect("ERR_DIV");
}
Resultado de la ejecución:
ejecutando 1 prueba el hilo 'tests::precision_test_0' se ha bloqueado en 'falló la afirmación: (left == right) izquierda: 2, derecha: 0: ', src/lib.rs:175:9
Se puede observar que result_0 = a * c / b y result_1 = (a / b) * c, aunque las fórmulas de cálculo son las mismas, los resultados son diferentes. La razón es que la división entera descartará la precisión que es menor que el divisor. En el cálculo de result_1, calcular primero (a / b) provoca que se pierda precisión y se convierta en 0; mientras que en el cálculo de result_0, primero se calcula a * c = 20_0000, que es mayor que el divisor b, evitando así la pérdida de precisión.
2.2 magnitud demasiado pequeña
óxido #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;
.checked_div(b) .expect("ERR_DIV") .checked_mul(c) .expect("ERR_MUL");
}
Resultado de la ejecución:
ejecutando 1 prueba 12:13 thread 'tests::precision_test_decimals' panicked at 'assertion failed: (left == right) left: 12, right: 13: ', src/lib.rs:214:9
Los resultados result_0 y result_1, que son equivalentes en el proceso de cálculo, son diferentes, y result_1 = 13 está más cerca de la expectativa real de 13.3333....
3. Cómo escribir contratos inteligentes de evaluación actuarial numérica en Rust
3.1 Ajustar el orden de las operaciones
3.2 aumentar el orden de magnitud de los enteros
3.3 Pérdida de precisión en operaciones acumulativas
Para los problemas de precisión en cálculos enteros que no se pueden evitar, se puede considerar registrar la pérdida acumulada de precisión en los cálculos.
ó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 = distribuir(10_000_000_000_000_000_000_000_000, offset); println!("Offset {}\n", offset); } }
Resultado de la ejecución:
ejecutando 1 prueba Ronda 1 per_user_share 3333333333333333333333333 Desplazamiento 1
Ronda 2 per_user_share 3333333333333333333333333 Offset 2
Ronda 3 per_user_share 4000000000000000000000000 Offset 0
Ronda 4 per_user_share 3333333333333333333333333 Offset 1
Ronda 5 per_user_share 3333333333333333333333333 Offset 2
Ronda 6 per_user_share 4000000000000000000000000 Offset 0
test tests::record_offset_test ... ok resultado de la prueba: ok. 1 aprobado; 0 fallido; 0 ignorado; 0 medido; 9 filtrados; terminado en 0.00s
( 3.4 Uso de la biblioteca Rust Crate rust-decimal
Esta biblioteca es adecuada para cálculos financieros en decimales que requieren una precisión efectiva y no tienen errores de redondeo.
) 3.5 Considerar el mecanismo de redondeo
Al diseñar contratos inteligentes, el problema de redondeo generalmente sigue el principio de "quiero aprovecharme, los demás no deben aprovecharse de mí". Según este principio, si redondear hacia abajo me beneficia, entonces se hace hacia abajo; si redondear hacia arriba me beneficia, entonces se hace hacia arriba; el redondeo al más cercano no puede determinar a quién le beneficia, por lo tanto, se utiliza muy poco.
![]###https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp###