Diário de Desenvolvimento de Contratos Inteligentes Rust (7): Cálculo de Valores
Este artigo irá explorar os problemas de cálculo numérico na programação de contratos inteligentes em Rust, incluindo principalmente a questão da precisão das operações de ponto flutuante, a questão da precisão dos cálculos inteiros, e como escrever contratos inteligentes Rust para cálculos numéricos.
1. Problemas de precisão em operações com números 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 são inevitáveis. Ao escrever contratos inteligentes, não é recomendado usar operações com números de ponto flutuante, especialmente ao lidar com razões ou taxas de 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 para a sua representação. No entanto, alguns números decimais (, como 0.7), não podem ser representados com precisão usando um número finito de bits, resultando em fenômenos de "arredondamento".
Usando como exemplo a distribuição de 0,7 tokens NEAR para dez usuários na blockchain NEAR, os resultados dos cálculos podem apresentar imprecisões.
ferrugem
#[test]
fn precision_test_float() {
let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("O valor da quantia: {:.20}", amount);
assert_eq!(result_0, 0.07, "");
}
O resultado da execução mostra que o valor de amount não é exatamente 0.7, mas sim um valor extremamente próximo de 0.69999999999999995559. O resultado de uma operação de divisão posterior também se torna impreciso, resultando em 0.06999999999999999, em vez do esperado 0.07.
Para resolver este problema, pode-se considerar o uso de números fixos. No NEAR Protocol, normalmente utiliza-se 10^24 como denominador, ou seja, 10^24 yoctoNEAR equivalem a 1 token NEAR.
O código de teste modificado é o seguinte:
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, "");
}
Desta forma, pode-se obter resultados de cálculo precisos: 0,7 NEAR / 10 = 0,07 NEAR.
2. Problema de precisão no cálculo de inteiros em Rust
Embora a utilização de operações inteiras possa resolver o problema da perda de precisão de números de ponto flutuante em certos cenários, os resultados dos cálculos inteiros também não são completamente precisos e confiáveis. Algumas das razões que afetam a precisão dos cálculos inteiros incluem:
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. 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,"");
}
Os resultados dos testes mostram que os resultados de cálculo de result_0 e result_1 são diferentes. Isso ocorre porque, para a divisão inteira, a precisão abaixo do divisor será descartada. Ao calcular result_1, (a / b) perderá primeiro a precisão do cálculo, tornando-se 0; enquanto ao calcular result_0, calcular a * c primeiro pode evitar a perda de precisão.
2.2 quantidade muito pequena
Quando se trata de cálculos em ordens de grandeza menores, também podem surgir problemas 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 dos testes mostram que os resultados das operações result_0 e result_1 são diferentes, e que result_1 = 13 está mais próximo do valor de cálculo esperado de 13.3333...
3. Como escrever contratos inteligentes Rust para avaliação numérica
Para aumentar a precisão, podem ser adotadas as seguintes medidas de proteção:
3.1 Ajustar a ordem das operações
Fazer a multiplicação de inteiros ter prioridade sobre a divisão de inteiros.
3.2 aumentar a ordem de grandeza dos inteiros
Usar uma maior magnitude para criar moléculas maiores. Por exemplo, 5.123 NEAR pode ser representado como 5.123 * 10^10 = 51_230_000_000.
3.3 Perda de precisão de cálculo acumulada
Para problemas de precisão de cálculo de inteiros que não podem ser evitados, pode-se considerar a registar a perda acumulada de precisão nas operações. 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 e redistribuir os tokens não distribuídos devido à perda de precisão.
( 3.4 Usando a biblioteca Rust Crate rust-decimal
Esta biblioteca é adequada para cálculos financeiros decimais que requerem precisão efetiva e não apresentam erro de arredondamento.
) 3.5 Considere o mecanismo de arredondamento
Ao projetar contratos inteligentes, o problema de arredondamento geralmente segue o princípio "quero lucrar, os outros não devem tirar vantagem de mim". De acordo com esse princípio, se o arredondamento para baixo for favorável ao contrato, então é para baixo; se o arredondamento para cima for favorável ao contrato, então é para cima; o arredondamento convencional é raramente adotado, pois não se pode determinar a quem é favorável.
Ao adotar esses métodos, é possível implementar cálculos numéricos mais precisos em contratos inteligentes Rust, aumentando a fiabilidade e a equidade do contrato.
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.
14 gostos
Recompensa
14
8
Partilhar
Comentar
0/400
ZeroRushCaptain
· 07-17 09:17
Ah, já estou a sofrer com isto outra vez. Da última vez, foi um problema de precisão que resultou em Posição de bloqueio e perdi tudo.
Ver originalResponder0
BlockchainFries
· 07-16 10:52
Este negócio de ponto flutuante é muito problemático. Quem usa, se dá mal.
Ver originalResponder0
MoonBoi42
· 07-15 08:48
A precisão dos números de ponto flutuante não é tão agradável quanto a dos números fixos~
Ver originalResponder0
OnchainDetective
· 07-14 09:54
É evidente que o erro de precisão é a brecha que alguns pontes de cadeia cruzada foram exploradas por hackers, é de arrepiar.
Ver originalResponder0
HashRateHermit
· 07-14 09:54
O problema de precisão realmente me matou.
Ver originalResponder0
GateUser-e87b21ee
· 07-14 09:47
Outra vez com a questão dos pontos flutuantes, estou confuso.
Ver originalResponder0
BearMarketSurvivor
· 07-14 09:45
A precisão é uma armadilha da qual ninguém pode escapar, esta pessoa entende.
Ver originalResponder0
AirdropLicker
· 07-14 09:35
O arredondamento de números de ponto flutuante é muito complicado. Já caí nessa armadilha antes.
Técnicas e precauções de precisão numérica em contratos inteligentes Rust
Diário de Desenvolvimento de Contratos Inteligentes Rust (7): Cálculo de Valores
Este artigo irá explorar os problemas de cálculo numérico na programação de contratos inteligentes em Rust, incluindo principalmente a questão da precisão das operações de ponto flutuante, a questão da precisão dos cálculos inteiros, e como escrever contratos inteligentes Rust para cálculos numéricos.
1. Problemas de precisão em operações com números 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 são inevitáveis. Ao escrever contratos inteligentes, não é recomendado usar operações com números de ponto flutuante, especialmente ao lidar com razões ou taxas de 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 para a sua representação. No entanto, alguns números decimais (, como 0.7), não podem ser representados com precisão usando um número finito de bits, resultando em fenômenos de "arredondamento".
Usando como exemplo a distribuição de 0,7 tokens NEAR para dez usuários na blockchain NEAR, os resultados dos cálculos podem apresentar imprecisões.
ferrugem #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("O valor da quantia: {:.20}", amount); assert_eq!(result_0, 0.07, ""); }
O resultado da execução mostra que o valor de amount não é exatamente 0.7, mas sim um valor extremamente próximo de 0.69999999999999995559. O resultado de uma operação de divisão posterior também se torna impreciso, resultando em 0.06999999999999999, em vez do esperado 0.07.
Para resolver este problema, pode-se considerar o uso de números fixos. No NEAR Protocol, normalmente utiliza-se 10^24 como denominador, ou seja, 10^24 yoctoNEAR equivalem a 1 token NEAR.
O código de teste modificado é o seguinte:
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, ""); }
Desta forma, pode-se obter resultados de cálculo precisos: 0,7 NEAR / 10 = 0,07 NEAR.
2. Problema de precisão no cálculo de inteiros em Rust
Embora a utilização de operações inteiras possa resolver o problema da perda de precisão de números de ponto flutuante em certos cenários, os resultados dos cálculos inteiros também não são completamente precisos e confiáveis. Algumas das razões que afetam a precisão dos cálculos inteiros incluem:
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. Por exemplo:
ferrugem #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
}
Os resultados dos testes mostram que os resultados de cálculo de result_0 e result_1 são diferentes. Isso ocorre porque, para a divisão inteira, a precisão abaixo do divisor será descartada. Ao calcular result_1, (a / b) perderá primeiro a precisão do cálculo, tornando-se 0; enquanto ao calcular result_0, calcular a * c primeiro pode evitar a perda de precisão.
2.2 quantidade muito pequena
Quando se trata de cálculos em ordens de grandeza menores, também podem surgir problemas 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 dos testes mostram que os resultados das operações result_0 e result_1 são diferentes, e que result_1 = 13 está mais próximo do valor de cálculo esperado de 13.3333...
3. Como escrever contratos inteligentes Rust para avaliação numérica
Para aumentar a precisão, podem ser adotadas as seguintes medidas de proteção:
3.1 Ajustar a ordem das operações
Fazer a multiplicação de inteiros ter prioridade sobre a divisão de inteiros.
3.2 aumentar a ordem de grandeza dos inteiros
Usar uma maior magnitude para criar moléculas maiores. Por exemplo, 5.123 NEAR pode ser representado como 5.123 * 10^10 = 51_230_000_000.
3.3 Perda de precisão de cálculo acumulada
Para problemas de precisão de cálculo de inteiros que não podem ser evitados, pode-se considerar a registar a perda acumulada de precisão nas operações. 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 e redistribuir os tokens não distribuídos devido à perda de precisão.
( 3.4 Usando a biblioteca Rust Crate rust-decimal
Esta biblioteca é adequada para cálculos financeiros decimais que requerem precisão efetiva e não apresentam erro de arredondamento.
) 3.5 Considere o mecanismo de arredondamento
Ao projetar contratos inteligentes, o problema de arredondamento geralmente segue o princípio "quero lucrar, os outros não devem tirar vantagem de mim". De acordo com esse princípio, se o arredondamento para baixo for favorável ao contrato, então é para baixo; se o arredondamento para cima for favorável ao contrato, então é para cima; o arredondamento convencional é raramente adotado, pois não se pode determinar a quem é favorável.
Ao adotar esses métodos, é possível implementar cálculos numéricos mais precisos em contratos inteligentes Rust, aumentando a fiabilidade e a equidade do contrato.
![]###https://img-cdn.gateio.im/webp-social/moments-6e8b4081214a69423fc7ae022d05c728.webp###