Diário de Desenvolvimento de Contratos Inteligentes Rust (7) Cálculo de Valores
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 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.
Na linguagem Rust, os números de ponto flutuante seguem o padrão IEEE 754. Tomando como exemplo o tipo de ponto flutuante de dupla precisão f64, sua representação binária interna é a seguinte:
Os números de ponto flutuante são expressos em notação científica com base 2. Por exemplo, 0.8125 pode ser representado pelo número binário finito 0.1101:
No entanto, para decimais pequenos como 0.7, pode ocorrer uma situação de repetição infinita:
0.7 = 0.1011001100110011...
Isso leva à incapacidade de representar com precisão números de ponto flutuante de comprimento finito, existindo o fenômeno de "arredondamento".
Usando como exemplo a distribuição de 0,7 NEAR tokens a dez usuários na blockchain NEAR:
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, "");
}
Resultado da execução:
executando 1 teste
O valor da quantia: 0.69999999999999995559
thread 'tests::precision_test_float' panicked at 'assertion failed: (left == right)
left: 0.06999999999999999, right: 0.07: ', src/lib.rs:185:9
Como pode ser visto, o valor de amount não é exatamente 0.7, mas sim um valor aproximado de 0.69999999999999995559. O resultado de uma operação de divisão adicional também se torna impreciso, resultando em 0.06999999999999999.
Para resolver este problema, pode-se considerar o uso de números fixos. No NEAR Protocol, geralmente utiliza-se a representação de 10^24 yoctoNEAR equivalente a 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, "");
}
Resultado da execução:
executando 1 teste
test tests::precision_test_integer ... ok
resultado do teste: ok. 1 passado; 0 falhado; 0 ignorado; 0 medido; 8 filtrados; terminado em 0.00s
2. Problema de precisão na computação de inteiros em Rust
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 nos cálculos inteiros.
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
executando 1 teste
thread 'tests::precision_test_0' panicked at 'assertion failed: (left == right)
left: 2, right: 0: ', src/lib.rs:175:9
Pode-se observar que result_0 = a * c / b e result_1 = (a / b) * c, embora as fórmulas de cálculo sejam as mesmas, os resultados são diferentes. A razão é que a divisão inteira descartará a precisão menor que o divisor. Ao calcular result_1, primeiro calcular (a / b) resulta numa perda de precisão que se torna 0; enquanto ao calcular result_0, primeiro calcula-se a * c = 20_0000, que é maior que o divisor b, evitando assim a perda de precisão.
2.2 quantidade muito pequena
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")
executando 1 teste
12:13
thread 'tests::precision_test_decimals' panicked at 'assertion failed: (left == right)
left: 12, right: 13: ', src/lib.rs:214:9
É visível que os resultados result_0 e result_1, que são equivalentes no processo de cálculo, são diferentes, e result_1 = 13 está mais próximo da expectativa real de 13.3333....
3. Como escrever contratos inteligentes de avaliação numérica em Rust
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
Números inteiros usam ordens de magnitude maiores, criando numeradores maiores.
3.3 perda de precisão acumulada
Para os problemas de precisão de cálculo inteiro que não podem ser evitados, pode-se considerar registrar a perda acumulada de precisão dos cálculos.
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(10_000_000_000_000_000_000_000_000, offset);
println!("Offset {}\n", offset);
}
}
Resultado da execução:
executando 1 teste
Rodada 1
per_user_share 3333333333333333333333333
Offset 1
test tests::record_offset_test ... ok
resultado do teste: ok. 1 passado; 0 falhou; 0 ignorado; 0 medido; 9 filtrados; terminado em 0.00s
( 3.4 Utilizando a biblioteca Rust Crate rust-decimal
Esta biblioteca é adequada para cálculos financeiros de números decimais que exigem precisão efetiva e que não têm erro de arredondamento.
) 3.5 considerar o mecanismo de arredondamento
Ao projetar contratos inteligentes, o problema do arredondamento geralmente adota o princípio "Quero me beneficiar, os outros não devem me explorar". De acordo com este princípio, se arredondar para baixo for benéfico para mim, então arredondo para baixo; se arredondar para cima for benéfico para mim, então arredondo para cima; o arredondamento para o mais próximo não pode determinar a quem é benéfico, portanto, raramente é adotado.
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.
17 gostos
Recompensa
17
5
Partilhar
Comentar
0/400
LiquidationKing
· 07-16 06:01
O 0,7 que você mencionou não é nada de mais, um grande colapso é que seria emocionante.
Ver originalResponder0
BankruptcyArtist
· 07-16 05:57
Ai, escrever contratos inteligentes realmente me prejudicou muito com números de ponto flutuante.
Ver originalResponder0
GmGmNoGn
· 07-16 05:55
Este bug pode fazer-me perder a precisão.
Ver originalResponder0
NotSatoshi
· 07-16 05:55
Nos contratos inteligentes, brincar com ponto flutuante pode fazer a cabeça explodir.
Rust contratos inteligentes numéricos: armadilhas de ponto flutuante e otimização de precisão inteira
Diário de Desenvolvimento de Contratos Inteligentes Rust (7) Cálculo de Valores
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 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.
Na linguagem Rust, os números de ponto flutuante seguem o padrão IEEE 754. Tomando como exemplo o tipo de ponto flutuante de dupla precisão f64, sua representação binária interna é a seguinte:
Os números de ponto flutuante são expressos em notação científica com base 2. Por exemplo, 0.8125 pode ser representado pelo número binário finito 0.1101:
0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
No entanto, para decimais pequenos como 0.7, pode ocorrer uma situação de repetição infinita:
0.7 = 0.1011001100110011...
Isso leva à incapacidade de representar com precisão números de ponto flutuante de comprimento finito, existindo o fenômeno de "arredondamento".
Usando como exemplo a distribuição de 0,7 NEAR tokens a dez usuários na blockchain NEAR:
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, ""); }
Resultado da execução:
executando 1 teste O valor da quantia: 0.69999999999999995559 thread 'tests::precision_test_float' panicked at 'assertion failed: (left == right) left: 0.06999999999999999, right: 0.07: ', src/lib.rs:185:9
Como pode ser visto, o valor de amount não é exatamente 0.7, mas sim um valor aproximado de 0.69999999999999995559. O resultado de uma operação de divisão adicional também se torna impreciso, resultando em 0.06999999999999999.
Para resolver este problema, pode-se considerar o uso de números fixos. No NEAR Protocol, geralmente utiliza-se a representação de 10^24 yoctoNEAR equivalente a 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, ""); }
Resultado da execução:
executando 1 teste test tests::precision_test_integer ... ok resultado do teste: ok. 1 passado; 0 falhado; 0 ignorado; 0 medido; 8 filtrados; terminado em 0.00s
2. Problema de precisão na computação de inteiros em Rust
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 nos cálculos inteiros.
ferrugem #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
.checked_div(b) .expect("ERR_DIV") .checked_mul(c) .expect("ERR_MUL");
}
Resultado da execução:
executando 1 teste thread 'tests::precision_test_0' panicked at 'assertion failed: (left == right) left: 2, right: 0: ', src/lib.rs:175:9
Pode-se observar que result_0 = a * c / b e result_1 = (a / b) * c, embora as fórmulas de cálculo sejam as mesmas, os resultados são diferentes. A razão é que a divisão inteira descartará a precisão menor que o divisor. Ao calcular result_1, primeiro calcular (a / b) resulta numa perda de precisão que se torna 0; enquanto ao calcular result_0, primeiro calcula-se a * c = 20_0000, que é maior que o divisor b, evitando assim a perda de precisão.
2.2 quantidade muito pequena
ferrugem #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;
.checked_mul(c) .expect("ERR_MUL");
.checked_div(b) .expect("ERR_DIV") .checked_mul(c) .expect("ERR_MUL") .checked_div(decimal)
.expect("ERR_DIV");
}
Resultado da execução:
executando 1 teste 12:13 thread 'tests::precision_test_decimals' panicked at 'assertion failed: (left == right) left: 12, right: 13: ', src/lib.rs:214:9
É visível que os resultados result_0 e result_1, que são equivalentes no processo de cálculo, são diferentes, e result_1 = 13 está mais próximo da expectativa real de 13.3333....
3. Como escrever contratos inteligentes de avaliação numérica em Rust
3.1 Ajustar a ordem das operações
3.2 aumentar a ordem de grandeza dos inteiros
3.3 perda de precisão acumulada
Para os problemas de precisão de cálculo inteiro que não podem ser evitados, pode-se considerar registrar a perda acumulada de precisão dos cálculos.
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(10_000_000_000_000_000_000_000_000, offset); println!("Offset {}\n", offset); } }
Resultado da execução:
executando 1 teste Rodada 1 per_user_share 3333333333333333333333333 Offset 1
Rodada 2 per_user_share 3333333333333333333333333 Offset 2
Rodada 3 per_user_share 4000000000000000000000000 Offset 0
Rodada 4 per_user_share 3333333333333333333333333 Offset 1
Rodada 5 per_user_share 3333333333333333333333333 Offset 2
Rodada 6 per_user_share 4000000000000000000000000 Offset 0
test tests::record_offset_test ... ok resultado do teste: ok. 1 passado; 0 falhou; 0 ignorado; 0 medido; 9 filtrados; terminado em 0.00s
( 3.4 Utilizando a biblioteca Rust Crate rust-decimal
Esta biblioteca é adequada para cálculos financeiros de números decimais que exigem precisão efetiva e que não têm erro de arredondamento.
) 3.5 considerar o mecanismo de arredondamento
Ao projetar contratos inteligentes, o problema do arredondamento geralmente adota o princípio "Quero me beneficiar, os outros não devem me explorar". De acordo com este princípio, se arredondar para baixo for benéfico para mim, então arredondo para baixo; se arredondar para cima for benéfico para mim, então arredondo para cima; o arredondamento para o mais próximo não pode determinar a quem é benéfico, portanto, raramente é adotado.
![]###https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp###