Diario de desarrollo de contratos inteligentes Rust (7): cálculo numérico
Revisión de ediciones anteriores:
Diario de desarrollo de contratos inteligentes en Rust (1) Definición de datos de estado del contrato y implementación de métodos
Diario de desarrollo de contratos inteligentes en Rust (2) Escribir pruebas unitarias para contratos inteligentes en Rust
Diario de desarrollo de contratos inteligentes Rust(3)Implementación de contratos inteligentes Rust, llamada a funciones y uso de Explorer
Diario de desarrollo de contratos inteligentes Rust ( 4) Desbordamiento de enteros en contratos inteligentes Rust
Diario de desarrollo de contratos inteligentes Rust (5) ataque de reentrada
Diario de desarrollo de contratos inteligentes Rust (6) ataque de denegación de servicio
1. Problema de precisión en cálculos de punto flotante
A diferencia de los lenguajes de programación de contratos inteligentes comunes como Solidity, el lenguaje Rust admite nativamente operaciones de punto flotante. Sin embargo, las operaciones de punto flotante presentan problemas de precisión de cálculo que son inevitables. Por lo tanto, al escribir contratos inteligentes, no se recomienda el uso de operaciones de punto flotante (, especialmente al tratar con tasas o intereses que involucran decisiones económicas/financieras importantes ).
Actualmente, la mayoría de los lenguajes de programación de uso común que representan números de punto flotante siguen el estándar IEEE 754, y el lenguaje Rust no es la excepción. A continuación se muestra una descripción del tipo de punto flotante de doble precisión f64 en Rust y la forma en que se almacenan los datos binarios internos en la computadora:
Los números de punto flotante utilizan la notación científica con base 2 para expresarse. Por ejemplo, el número binario de 4 bits 0.1101 puede representar el decimal 0.8125, y la forma específica de conversión es la siguiente:
0.8125 * 2 = 1 .625 // 0.1 Obtener el primer dígito decimal binario es 1
0.625 * 2 = 1 .25 // 0.11 Obtén el segundo dígito decimal binario como 1
0.25 * 2 = 0 .5 // 0.110 Obtiene el tercer dígito binario decimal como 0
0.5 * 2 = 1 .0 // 0.1101 obtener el cuarto decimal binario como 1
es 0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Sin embargo, para otro decimal 0.7, existirá el siguiente problema en el proceso de conversión a número de punto flotante:
0.7 x 2 = 1. 4 // 0.1
0.4 x 2 = 0. 8 // 0.10
0.8 x 2 = 1. 6 // 0.101
0.6 x 2 = 1. 2 // 0.1011
0.2 x 2 = 0. 4 // 0.10110
0.4 x 2 = 0. 8 // 0.101100
0.8 x 2 = 1. 6 // 0.1011001
....
Es decir, el decimal 0.7 se representará como 0.101100110011001100.....( en una repetición infinita ), y no se puede representar con un número de punto flotante de longitud finita de manera precisa, existiendo el fenómeno de "Rounding(".
Supongamos que en la cadena de bloques NEAR, se necesita distribuir 0.7 tokens NEAR a diez usuarios, y la cantidad de tokens NEAR que recibe cada usuario se calculará y se guardará en la variable result_0.
#)
fn precision_test_float[test]( {
// Los números de punto flotante no pueden representar enteros con precisión
let amount: f64 = 0.7; // La variable amount representa 0.7 tokens NEAR
let divisor: f64 = 10.0; // definir el divisor
let result_0 = a / b; // Ejecutar la operación de división de números en punto flotante
println!)"El valor de a: {:.20}", a(;
assert_eq!)result_0, 0.07, ""(;
}
El resultado de la ejecución de este caso de prueba es el siguiente:
ejecutando 1 prueba
El valor de a: 0.69999999999999995559
hilo "tests::precision_test_float" entró en pánico en "falló la afirmación: )left == right(
izquierda: 0.06999999999999999, derecha: 0.07: ", src/lib.rs:185:9
Como se puede ver en las operaciones de punto flotante anteriores, el valor de amount no representa con precisión 0.7, sino que es un valor extremadamente aproximado de 0.69999999999999995559. Además, para una operación de división única como amount/divisor, el resultado de la operación también se convertirá en un 0.06999999999999999 impreciso, y no en el 0.07 esperado. De esto se puede ver la incertidumbre en las operaciones con números de punto flotante.
A este respecto, debemos considerar el uso de otros tipos de representaciones numéricas en contratos inteligentes, como los números de punto fijo.
Según la posición fija del punto decimal, los números de punto fijo tienen dos tipos: números enteros de punto fijo ) y números decimales de punto fijo (.
Si el punto decimal está fijo después de la cifra más baja, se denomina entero de punto fijo.
En la redacción práctica de contratos inteligentes, generalmente se utiliza una fracción con un denominador fijo para representar un valor, como la fracción "x/N", donde "N" es una constante y "x" puede variar.
Si "N" toma el valor de "1,000,000,000,000,000,000", es decir, "10^18", en este caso el decimal puede ser representado como un entero, así:
En el protocolo NEAR, el valor común de N es "10^24", es decir, 10^24 yoctoNEAR equivalen a 1 token NEAR.
Basado en esto, podemos modificar las pruebas unitarias de esta sección para que se realicen los cálculos de la siguiente manera:
#)
fn precision_test_integer() {
// Primero definimos la constante N, que representa la precisión.
let N: u128 = 1_000_000_000_000_000_000_000_000; // es decir, define 1 NEAR = 10^24 yoctoNEAR
// Inicializar amount, en realidad el valor que representa amount en este momento es 700_000_000_000_000_000 / N = 0.7 NEAR;
let amount: u128 = 700_000_000_000_000_000_000_000; // yoctoNEAR
// Inicializar el divisor
let divisor: u128 = 10;
// Cálculo obtenido:result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
// Representa realmente 700_000_000_000_000_000_000_000 / N = 0.07 NEAR;
let result_0 = amount / divisor;
assert_eq![test]result_0, 70_000_000_000_000_000_000_000, ""(;
}
Con esto se puede obtener el resultado del cálculo actuarial: 0.7 NEAR / 10 = 0.07 NEAR
ejecutando 1 prueba
test tests::precision_test_integer ... ok
resultado de la prueba: ok. 1 aprobado; 0 fallidos; 0 ignorados; 0 medidos; 8 filtrados; terminado en 0.00s
2. El problema de la precisión de los cálculos enteros en Rust
A partir de la descripción en la sección 1 anterior, se puede observar que el uso de operaciones enteras puede resolver el problema de pérdida de precisión en las operaciones de punto flotante en ciertos escenarios.
Pero esto no significa que los resultados de los cálculos enteros sean completamente precisos y confiables. Esta sección introducirá algunas de las razones que afectan la precisión de los cálculos enteros.
) 2.1 orden de operaciones
El orden de las operaciones 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, existe la siguiente operación:
#(
fn precision_test_div_before_mul)### {
let a: u128 = 1_0000;
let b: u128 = 10_0000;
let c: u128 = 20;
// result_0 = a * c / b
let result_0 = a
.checked_mul[test]c(
.expect)"ERR_MUL"(
.checked_div)b(
.expect)"ERR_DIV"(;
// result_0 = a / b * c
let result_1 = a
.checked_div)b(
.expect)"ERR_DIV"(
Podemos encontrar que result_0 = a * c / b y result_1 = )a / b(* c, aunque sus fórmulas de cálculo son las mismas, los resultados de los cálculos son diferentes.
Analizando las razones específicas: en la división de enteros, la precisión menor que el divisor se descartará. Por lo tanto, en el proceso de cálculo de result_1, el cálculo inicial de )a / b( perderá primero precisión de cálculo, convirtiéndose en 0; mientras que al calcular result_0, primero se calculará el resultado de a * c que es 20_0000, este resultado será mayor que el divisor b, por lo que se evita el problema de la pérdida de precisión y se puede obtener el resultado de cálculo correcto.
) 2.2 cantidad demasiado pequeña
#(
fn precision_test_decimals)### {
let a: u128 = 10;
let b: u128 = 3;
let c: u128 = 4;
let decimal: u128 = 100_0000;
// result_0 = [test]a / b( * c
let result_0 = a
.checked_div)b(
.expect)"ERR_DIV"(
.checked_mul)c(
.expect)"ERR_MUL"(;
// result_1 = )a * decimal / b( * c / decimal;
let result_1 = a
.checked_mul)decimal( // mul decimal
.expect)"ERR_MUL"(
.checked_div)b(
.expect)"ERR_DIV"(
.checked_mul)c(
.expect)"ERR_MUL"(
.checked_div)decimal( // div decimal
.expect)"ERR_DIV"(;
println!)"{}:{}", result_0, result_1(;
assert_eq!)result_0, result_1, ""(;
}
Los resultados específicos de esta prueba unitaria son los siguientes:
ejecutando 1 prueba
12:13
hilo "tests::precision_test_decimals" entró en pánico en "la afirmación falló: )left == right(
izquierda: 12, derecha: 13: ", src/lib.rs:214:9
Se puede ver que los resultados de los cálculos equivalentes result_0 y result_1 no son los mismos, y result_1 = 13 se acerca más al valor calculado esperado: 13.3333....
3. Cómo escribir contratos inteligentes de cálculo numérico en Rust
Garantizar la precisión correcta en los contratos inteligentes es muy importante. Aunque también existe el problema de pérdida de precisión en los resultados de operaciones enteras en el lenguaje Rust, podemos tomar algunas medidas de protección para mejorar la precisión y lograr resultados satisfactorios.
) 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 órdenes de magnitud más grandes, creando numeradores más grandes.
Por ejemplo, para un token NEAR, si se define N = 10 como se describió anteriormente, significa que: si se necesita representar un valor NEAR de 5.123, el valor entero que se utilizará en el cálculo real se representará como 5.123 * 10^10 = 51_230_000_000. Este valor continuará participando en cálculos enteros posteriores, lo que puede mejorar la precisión de los cálculos.
) 3.3 Pérdida de precisión en los cálculos acumulativos
Para los problemas de precisión en cálculos enteros que realmente no se pueden evitar, el equipo del proyecto puede considerar registrar las pérdidas acumuladas de precisión en los cálculos.
u128 para distribuir tokens entre USER_NUM usuarios.
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() {
let mut offset: u128 = 0;
para i en 1..7 {
println!("Round {}",i);
offset = distribuir(to_yocto)"10"[test], offset(;
println!)"Offset {}\n",offset(;
}
}
En este caso de prueba, el sistema distribuirá 10 tokens a 3 usuarios en cada ocasión. Sin embargo, debido a problemas de precisión en las operaciones con enteros, al calcular per_user_share en la primera ronda, el resultado de la operación entera es 10 / 3 = 3, es decir, los usuarios distribuidos en la primera ronda recibirán un promedio de 3 tokens, con un total de 9 tokens distribuidos.
En este momento, se puede observar que aún queda 1 token en el sistema que no ha sido distribuido a los usuarios. Por lo tanto, se puede considerar almacenar temporalmente el token restante en la variable global del sistema llamada offset. Cuando el sistema vuelva a llamar a distribute para distribuir tokens a los usuarios, este valor será extraído y se intentará distribuir junto con la cantidad de tokens de esta ronda.
A continuación se presenta el proceso simulado de distribución de tokens:
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.
9 me gusta
Recompensa
9
6
Compartir
Comentar
0/400
WalletWhisperer
· hace5h
fascinante cómo los puntos flotantes de rust podrían ser nuestro próximo honeypot de vulnerabilidad... observando de cerca
Ver originalesResponder0
OnlyOnMainnet
· hace5h
Cálculo de números de punto flotante + on-chain jeje me asustó
Ver originalesResponder0
TopEscapeArtist
· hace5h
Chicos, este problema de precisión es tan preciso como yo pisando la cima.
Ver originalesResponder0
RamenDeFiSurvivor
· hace5h
Me voy, me voy. Este problema de precisión es realmente frustrante.
Ver originalesResponder0
NFTArchaeologist
· hace5h
El problema de la precisión es el más fatal... si no se hace bien, se perderá todo el capital.
Cálculo numérico preciso en contratos inteligentes de Rust: enteros vs flotantes
Diario de desarrollo de contratos inteligentes Rust (7): cálculo numérico
Revisión de ediciones anteriores:
1. Problema de precisión en cálculos de punto flotante
A diferencia de los lenguajes de programación de contratos inteligentes comunes como Solidity, el lenguaje Rust admite nativamente operaciones de punto flotante. Sin embargo, las operaciones de punto flotante presentan problemas de precisión de cálculo que son inevitables. Por lo tanto, al escribir contratos inteligentes, no se recomienda el uso de operaciones de punto flotante (, especialmente al tratar con tasas o intereses que involucran decisiones económicas/financieras importantes ).
Actualmente, la mayoría de los lenguajes de programación de uso común que representan números de punto flotante siguen el estándar IEEE 754, y el lenguaje Rust no es la excepción. A continuación se muestra una descripción del tipo de punto flotante de doble precisión f64 en Rust y la forma en que se almacenan los datos binarios internos en la computadora:
Los números de punto flotante utilizan la notación científica con base 2 para expresarse. Por ejemplo, el número binario de 4 bits 0.1101 puede representar el decimal 0.8125, y la forma específica de conversión es la siguiente:
Sin embargo, para otro decimal 0.7, existirá el siguiente problema en el proceso de conversión a número de punto flotante:
Es decir, el decimal 0.7 se representará como 0.101100110011001100.....( en una repetición infinita ), y no se puede representar con un número de punto flotante de longitud finita de manera precisa, existiendo el fenómeno de "Rounding(".
Supongamos que en la cadena de bloques NEAR, se necesita distribuir 0.7 tokens NEAR a diez usuarios, y la cantidad de tokens NEAR que recibe cada usuario se calculará y se guardará en la variable result_0.
El resultado de la ejecución de este caso de prueba es el siguiente:
Como se puede ver en las operaciones de punto flotante anteriores, el valor de amount no representa con precisión 0.7, sino que es un valor extremadamente aproximado de 0.69999999999999995559. Además, para una operación de división única como amount/divisor, el resultado de la operación también se convertirá en un 0.06999999999999999 impreciso, y no en el 0.07 esperado. De esto se puede ver la incertidumbre en las operaciones con números de punto flotante.
A este respecto, debemos considerar el uso de otros tipos de representaciones numéricas en contratos inteligentes, como los números de punto fijo.
En la redacción práctica de contratos inteligentes, generalmente se utiliza una fracción con un denominador fijo para representar un valor, como la fracción "x/N", donde "N" es una constante y "x" puede variar.
Si "N" toma el valor de "1,000,000,000,000,000,000", es decir, "10^18", en este caso el decimal puede ser representado como un entero, así:
En el protocolo NEAR, el valor común de N es "10^24", es decir, 10^24 yoctoNEAR equivalen a 1 token NEAR.
Basado en esto, podemos modificar las pruebas unitarias de esta sección para que se realicen los cálculos de la siguiente manera:
Con esto se puede obtener el resultado del cálculo actuarial: 0.7 NEAR / 10 = 0.07 NEAR
![])https://img-cdn.gateio.im/webp-social/moments-7bdd27c1211e1cc345bf262666a993da.webp(
2. El problema de la precisión de los cálculos enteros en Rust
A partir de la descripción en la sección 1 anterior, se puede observar que el uso de operaciones enteras puede resolver el problema de pérdida de precisión en las operaciones de punto flotante en ciertos escenarios.
Pero esto no significa que los resultados de los cálculos enteros sean completamente precisos y confiables. Esta sección introducirá algunas de las razones que afectan la precisión de los cálculos enteros.
) 2.1 orden de operaciones
El orden de las operaciones 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, existe la siguiente operación:
.checked_mul)c( .expect)"ERR_MUL"(; assert_eq!)result_0,result_1,""(; }
Los resultados de la ejecución de las pruebas unitarias son los siguientes:
Podemos encontrar que result_0 = a * c / b y result_1 = )a / b(* c, aunque sus fórmulas de cálculo son las mismas, los resultados de los cálculos son diferentes.
Analizando las razones específicas: en la división de enteros, la precisión menor que el divisor se descartará. Por lo tanto, en el proceso de cálculo de result_1, el cálculo inicial de )a / b( perderá primero precisión de cálculo, convirtiéndose en 0; mientras que al calcular result_0, primero se calculará el resultado de a * c que es 20_0000, este resultado será mayor que el divisor b, por lo que se evita el problema de la pérdida de precisión y se puede obtener el resultado de cálculo correcto.
) 2.2 cantidad demasiado pequeña
.checked_div)b( .expect)"ERR_DIV"( .checked_mul)c( .expect)"ERR_MUL"(; // result_1 = )a * decimal / b( * c / decimal;
let result_1 = a .checked_mul)decimal( // mul decimal .expect)"ERR_MUL"( .checked_div)b( .expect)"ERR_DIV"( .checked_mul)c( .expect)"ERR_MUL"( .checked_div)decimal( // div decimal .expect)"ERR_DIV"(; println!)"{}:{}", result_0, result_1(; assert_eq!)result_0, result_1, ""(; }
Los resultados específicos de esta prueba unitaria son los siguientes:
Se puede ver que los resultados de los cálculos equivalentes result_0 y result_1 no son los mismos, y result_1 = 13 se acerca más al valor calculado esperado: 13.3333....
![])https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp(
3. Cómo escribir contratos inteligentes de cálculo numérico en Rust
Garantizar la precisión correcta en los contratos inteligentes es muy importante. Aunque también existe el problema de pérdida de precisión en los resultados de operaciones enteras en el lenguaje Rust, podemos tomar algunas medidas de protección para mejorar la precisión y lograr resultados satisfactorios.
) 3.1 Ajustar el orden de las operaciones
( 3.2 aumentar el orden de magnitud de los enteros
Por ejemplo, para un token NEAR, si se define N = 10 como se describió anteriormente, significa que: si se necesita representar un valor NEAR de 5.123, el valor entero que se utilizará en el cálculo real se representará como 5.123 * 10^10 = 51_230_000_000. Este valor continuará participando en cálculos enteros posteriores, lo que puede mejorar la precisión de los cálculos.
) 3.3 Pérdida de precisión en los cálculos acumulativos
Para los problemas de precisión en cálculos enteros que realmente no se pueden evitar, el equipo del proyecto puede considerar registrar las pérdidas acumuladas de precisión en los cálculos.
u128 para distribuir tokens entre USER_NUM usuarios.
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() { let mut offset: u128 = 0; para i en 1..7 { println!("Round {}",i); offset = distribuir(to_yocto)"10"[test], offset(; println!)"Offset {}\n",offset(; } }
En este caso de prueba, el sistema distribuirá 10 tokens a 3 usuarios en cada ocasión. Sin embargo, debido a problemas de precisión en las operaciones con enteros, al calcular per_user_share en la primera ronda, el resultado de la operación entera es 10 / 3 = 3, es decir, los usuarios distribuidos en la primera ronda recibirán un promedio de 3 tokens, con un total de 9 tokens distribuidos.
En este momento, se puede observar que aún queda 1 token en el sistema que no ha sido distribuido a los usuarios. Por lo tanto, se puede considerar almacenar temporalmente el token restante en la variable global del sistema llamada offset. Cuando el sistema vuelva a llamar a distribute para distribuir tokens a los usuarios, este valor será extraído y se intentará distribuir junto con la cantidad de tokens de esta ronda.
A continuación se presenta el proceso simulado de distribución de tokens: