Problemas de precisão nos cálculos numéricos no desenvolvimento de contratos inteligentes em Rust
1. Problemas de precisão em operações de ponto flutuante
A linguagem Rust suporta nativamente operações com números de ponto flutuante, mas essas operações apresentam problemas de precisão de cálculo que não podem ser evitados. Ao escrever contratos inteligentes, não se recomenda o uso de operações com números de ponto flutuante, especialmente ao lidar com taxas ou juros que envolvem decisões econômicas/financeiras importantes.
O tipo de ponto flutuante de dupla precisão f64 na linguagem Rust segue o padrão IEEE 754, utilizando a notação científica com base 2. Certos números decimais não podem ser representados com precisão por números de ponto flutuante de comprimento finito, resultando em um fenômeno de "arredondamento".
Por exemplo, ao distribuir 0.7 tokens NEAR na blockchain NEAR para 10 usuários:
ferrugem
#[test]
fn precision_test_float() {
let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("O valor de montante: {:.20}", montante);
assert_eq!(result_0, 0.07, "");
}
Os resultados mostram que o valor de amount é 0.69999999999999995559, não o exato 0.7. O resultado da operação de divisão também se tornou impreciso, sendo 0.06999999999999999, em vez do esperado 0.07.
Para resolver este problema, pode-se considerar o uso de números fixos. No NEAR Protocol, é comum usar 10^24 yoctoNEAR para representar 1 token NEAR. Código de teste modificado:
ferrugem
#[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, "");
}
Dessa forma, é possível obter um resultado de cálculo preciso: 0,7 NEAR / 10 = 0,07 NEAR.
2. Problema de precisão em cálculos inteiros com Rust
A utilização de operações inteiras pode resolver o problema de perda de precisão nas operações de ponto flutuante em certos cenários, mas os resultados dos cálculos inteiros também não são completamente precisos e fiáveis.
2.1 Ordem das operações
A mudança na ordem de multiplicação e divisão com a mesma prioridade aritmética pode afetar diretamente o resultado do cálculo, levando a problemas de precisão no cálculo de inteiros. Por exemplo:
ferrugem
#[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,"");
}
O resultado da execução mostra que result_0 e result_1 não são iguais. A razão é que a divisão inteira descarta a precisão abaixo do divisor. Ao calcular result_1, (a / b) perde primeiro a precisão e se torna 0; enquanto ao calcular result_0, primeiro se calcula a * c, evitando a perda de precisão.
2.2 quantidade muito pequena
Quando se trata de cálculos decimais, a operação com inteiros pode levar à perda de precisão:
ferrugem
#[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, "");
}
Os resultados mostram result_0=12, result_1=13, enquanto o valor esperado real deve ser 13.3333....
3. Como escrever contratos inteligentes de cálculo numérico em Rust
Para aumentar a precisão, podem ser adotadas as seguintes medidas de proteção:
3.1 Ajustar a ordem das operações
Deixe a multiplicação de inteiros ter prioridade sobre a divisão de inteiros.
3.2 aumentar a ordem de grandeza dos inteiros
Use uma magnitude maior para criar moléculas maiores. Por exemplo, representar 5.123 NEAR pode ser feito como 5.123 * 10^10 = 51_230_000_000 para participar de cálculos subsequentes.
3.3 perda de precisão em operações acumuladas
Para problemas de precisão inevitáveis, pode-se registar a perda acumulada de precisão nos cálculos. Por exemplo:
ferrugem
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;
para i em 1..7 {
println!("Round {}", i);
offset = distribute(to_yocto)"10"(, offset(;
println!)"Offset {}\n", offset);
}
}
Este método pode acumular os tokens restantes de cada distribuição e distribuí-los todos de uma vez na próxima distribuição, alcançando assim o objetivo de uma distribuição completa.
( 3.4 Usando a biblioteca Rust Crate rust-decimal
Esta biblioteca é adequada para cálculos financeiros decimais que requerem precisão eficaz e que não apresentam erro de arredondamento.
) 3.5 Considerar o mecanismo de arredondamento
Na concepção de contratos inteligentes, o problema de arredondamento geralmente adota o princípio de "quero me beneficiar, os outros não devem me explorar". Escolhe-se arredondar para baixo ou para cima, raramente se utiliza o arredondamento convencional.
Esta página pode conter conteúdos de terceiros, que são fornecidos apenas para fins informativos (sem representações/garantias) e não devem ser considerados como uma aprovação dos seus pontos de vista pela Gate, nem como aconselhamento financeiro ou profissional. Consulte a Declaração de exoneração de responsabilidade para obter mais informações.
10 gostos
Recompensa
10
8
Partilhar
Comentar
0/400
rugpull_survivor
· 07-14 15:34
Muitas pessoas caíram na armadilha de arredondamento.
Ver originalResponder0
WhaleWatcher
· 07-14 07:46
O controle de precisão é fundamental.
Ver originalResponder0
consensus_failure
· 07-13 12:52
Mais seguro com precisão fixa
Ver originalResponder0
Ser_Liquidated
· 07-12 19:41
Se houver outro problema de precisão, serei liquidado.
Ver originalResponder0
DecentralizeMe
· 07-11 16:13
Substitua por números de ponto fixo.
Ver originalResponder0
BoredWatcher
· 07-11 15:58
O código é um pouco complicado.
Ver originalResponder0
BrokenDAO
· 07-11 15:53
Poder de computação total excedente será problemático
Desenvolvimento de contratos inteligentes em Rust: Superando o desafio da precisão dos cálculos numéricos
Problemas de precisão nos cálculos numéricos no desenvolvimento de contratos inteligentes em Rust
1. Problemas de precisão em operações de ponto flutuante
A linguagem Rust suporta nativamente operações com números de ponto flutuante, mas essas operações apresentam problemas de precisão de cálculo que não podem ser evitados. Ao escrever contratos inteligentes, não se recomenda o uso de operações com números de ponto flutuante, especialmente ao lidar com taxas ou juros que envolvem decisões econômicas/financeiras importantes.
O tipo de ponto flutuante de dupla precisão f64 na linguagem Rust segue o padrão IEEE 754, utilizando a notação científica com base 2. Certos números decimais não podem ser representados com precisão por números de ponto flutuante de comprimento finito, resultando em um fenômeno de "arredondamento".
Por exemplo, ao distribuir 0.7 tokens NEAR na blockchain NEAR para 10 usuários:
ferrugem #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("O valor de montante: {:.20}", montante); assert_eq!(result_0, 0.07, ""); }
Os resultados mostram que o valor de amount é 0.69999999999999995559, não o exato 0.7. O resultado da operação de divisão também se tornou impreciso, sendo 0.06999999999999999, em vez do esperado 0.07.
Para resolver este problema, pode-se considerar o uso de números fixos. No NEAR Protocol, é comum usar 10^24 yoctoNEAR para representar 1 token NEAR. Código de teste modificado:
ferrugem #[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, ""); }
Dessa forma, é possível obter um resultado de cálculo preciso: 0,7 NEAR / 10 = 0,07 NEAR.
2. Problema de precisão em cálculos inteiros com Rust
A utilização de operações inteiras pode resolver o problema de perda de precisão nas operações de ponto flutuante em certos cenários, mas os resultados dos cálculos inteiros também não são completamente precisos e fiáveis.
2.1 Ordem das operações
A mudança na ordem de multiplicação e divisão com a mesma prioridade aritmética pode afetar diretamente o resultado do cálculo, levando a problemas de precisão no cálculo de inteiros. Por exemplo:
ferrugem #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
}
O resultado da execução mostra que result_0 e result_1 não são iguais. A razão é que a divisão inteira descarta a precisão abaixo do divisor. Ao calcular result_1, (a / b) perde primeiro a precisão e se torna 0; enquanto ao calcular result_0, primeiro se calcula a * c, evitando a perda de precisão.
2.2 quantidade muito pequena
Quando se trata de cálculos decimais, a operação com inteiros pode levar à perda de precisão:
ferrugem #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;
}
Os resultados mostram result_0=12, result_1=13, enquanto o valor esperado real deve ser 13.3333....
3. Como escrever contratos inteligentes de cálculo numérico em Rust
Para aumentar a precisão, podem ser adotadas as seguintes medidas de proteção:
3.1 Ajustar a ordem das operações
Deixe a multiplicação de inteiros ter prioridade sobre a divisão de inteiros.
3.2 aumentar a ordem de grandeza dos inteiros
Use uma magnitude maior para criar moléculas maiores. Por exemplo, representar 5.123 NEAR pode ser feito como 5.123 * 10^10 = 51_230_000_000 para participar de cálculos subsequentes.
3.3 perda de precisão em operações acumuladas
Para problemas de precisão inevitáveis, pode-se registar a perda acumulada de precisão nos cálculos. Por exemplo:
ferrugem 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; para i em 1..7 { println!("Round {}", i); offset = distribute(to_yocto)"10"(, offset(; println!)"Offset {}\n", offset); } }
Este método pode acumular os tokens restantes de cada distribuição e distribuí-los todos de uma vez na próxima distribuição, alcançando assim o objetivo de uma distribuição completa.
( 3.4 Usando a biblioteca Rust Crate rust-decimal
Esta biblioteca é adequada para cálculos financeiros decimais que requerem precisão eficaz e que não apresentam erro de arredondamento.
) 3.5 Considerar o mecanismo de arredondamento
Na concepção de contratos inteligentes, o problema de arredondamento geralmente adota o princípio de "quero me beneficiar, os outros não devem me explorar". Escolhe-se arredondar para baixo ou para cima, raramente se utiliza o arredondamento convencional.
![]###https://img-cdn.gateio.im/webp-social/moments-6e8b4081214a69423fc7ae022d05c728.webp###