Cálculo numérico preciso em contratos inteligentes Rust: inteiros vs ponto flutuante

Diário de Desenvolvimento de Contratos Inteligentes Rust (7): Cálculo Numérico

Revisão de edições anteriores:

  • Diário de desenvolvimento de contratos inteligentes Rust (1) Definição de dados de estado do contrato e implementação de métodos
  • Diário de desenvolvimento de contratos inteligentes Rust (2) Escrevendo testes de unidade para contratos inteligentes Rust
  • Diário de desenvolvimento de contratos inteligentes Rust ( 3) Implementação de contratos inteligentes Rust, chamada de função e uso do Explorer
  • Diário de desenvolvimento de contratos inteligentes Rust (4) Overflow de inteiros em contratos inteligentes Rust
  • Diário de desenvolvimento de contratos inteligentes Rust (5) ataque de reentrada
  • Diário de desenvolvimento de contratos inteligentes Rust (6) ataque de negação de serviço

1. Problemas de precisão em cálculos de números de ponto flutuante

Diferente das linguagens de programação de contratos inteligentes comuns como Solidity, a linguagem Rust suporta nativamente operações com números de ponto flutuante. No entanto, as operações com números de ponto flutuante apresentam um problema de precisão de cálculo que não pode ser evitado. Portanto, 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 ).

Atualmente, as principais linguagens de programação que representam números de ponto flutuante seguem, em sua maioria, o padrão IEEE 754, e a linguagem Rust não é exceção. Abaixo estão as explicações sobre o tipo de ponto flutuante de dupla precisão f64 na linguagem Rust e a forma como os dados binários internos são armazenados no computador:

Os números de ponto flutuante são expressos na notação científica com base 2. Por exemplo, o número decimal 0.8125 pode ser representado pelo número binário de número finito de bits 0.1101, e a forma de conversão específica é a seguinte:

0.8125 * 2 = 1 .625 // 0.1      O primeiro dígito da fração binária é 1
0.625  * 2 = 1 .25  // 0.11     Obtém o 2º dígito binário decimal como 1  
0.25   * 2 = 0 .5   // 0.110    obteve o 3º dígito decimal binário como 0
0.5    * 2 = 1 .0   // 0.1101   obtém-se o 4º dígito binário decimal como 1

ou 0,8125 = 0,5 * 1 + 0,25 * 1 + 0,125 * 0 + 0,0625 * 1

No entanto, para outro decimal 0.7, haverá os seguintes problemas no processo de conversão para número de ponto flutuante:

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
....

Assim, o decimal 0.7 será representado como 0.101100110011001100.....( em ciclo infinito ), não podendo ser representado com precisão por um número de ponto flutuante de comprimento finito, e existe o fenômeno de "Rounding(".

Suponha que na blockchain NEAR, seja necessário distribuir 0,7 NEAR tokens para dez usuários, sendo que a quantidade de NEAR tokens recebida por cada usuário será calculada e salva na variável result_0.

#)
fn precision_test_float[test]( {
    // Números de ponto flutuante não podem representar inteiros com precisão
    let amount: f64 = 0.7;     // A variável amount representa 0.7 tokens NEAR
    let divisor: f64 = 10.0;   // Definir o divisor
    let result_0 = a / b;     // Executa a operação de divisão de ponto flutuante
    println!)"O valor de a: {:.20}", a(;
    assert_eq!)result_0, 0.07, ""(;
}

Os resultados da execução do caso de teste são os seguintes:

executando 1 teste
O valor de a: 0.69999999999999995559
thread "tests::precision_test_float" panicked at "assertion failed: )left == right(
 left: 0.06999999999999999, right: 0.07: ", src/lib.rs:185:9

É visível que na operação de ponto flutuante acima, o valor de amount não representa com precisão 0.7, mas sim um valor extremamente aproximado de 0.69999999999999995559. Além disso, para operações de divisão única como amount/divisor, o resultado da operação também se tornará impreciso, resultando em 0.06999999999999999, e não no esperado 0.07. Assim, podemos notar a incerteza das operações com números de ponto flutuante.

Para isso, temos de considerar o uso de outros tipos de representação numérica em contratos inteligentes, como números de ponto fixo.

  1. De acordo com a posição fixa do ponto decimal, os números fixos podem ser números inteiros fixos ) puros ( e números decimais fixos ) puros (.
  2. O ponto decimal fixado após a posição mais baixa do número é chamado de inteiro de ponto fixo.

Na prática da escrita de contratos inteligentes, geralmente é utilizado uma fração com um denominador fixo para representar um determinado valor, como a fração "x/N", onde "N" é uma constante e "x" pode variar.

Se o "N" tiver o valor de "1.000.000.000.000.000.000", ou seja, "10^18", neste caso, o decimal pode ser representado como um inteiro, assim:

1.0 ->  1_000_000_000_000_000_000
0.7 ->    700_000_000_000_000_000
3.14 -> 3_140_000_000_000_000_000

No NEAR Protocol, o valor comum do N é "10^24", ou seja, 10^24 yoctoNEAR equivalem a 1 token NEAR.

Com base nisso, podemos modificar os testes unitários desta seção para realizar o cálculo da seguinte forma:

#)
fn precision_test_integer[test]( {
    // Primeiro, defina a constante N, que representa a precisão.
    let N: u128 =    1_000_000_000_000_000_000_000_000;  // ou seja, define 1 NEAR = 10^24 yoctoNEAR
    // Inicializa amount, na verdade neste momento o valor representado por amount é 700_000_000_000_000_000 / N = 0.7 NEAR; 
    let amount: u128 = 700_000_000_000_000_000_000_000; // yoctoNEAR
    // Inicializar o divisor
    let divisor: u128 = 10; 
    // Cálculo pode resultar: result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
    // Representa efetivamente 700_000_000_000_000_000_000_000 / N = 0.07 NEAR; 
    let result_0 = amount / divisor;
    assert_eq!)result_0, 70_000_000_000_000_000_000_000, ""(;
}

Com isso, pode-se obter o resultado do cálculo atuarial: 0,7 NEAR / 10 = 0,07 NEAR

a executar 1 teste
teste testes::precision_test_integer ... ok
resultado do teste: ok. 1 passou; 0 falhou; 0 ignorado; 0 medido; 8 filtrados; terminado em 0.00s

![])https://img-cdn.gateio.im/webp-social/moments-7bdd27c1211e1cc345bf262666a993da.webp(

2. Problemas de precisão em cálculos inteiros em Rust

A partir da descrição do ponto 1 acima, pode-se perceber que o uso de operações inteiras pode resolver o problema da perda de precisão nas operações de ponto flutuante em certos cenários.

Mas isso não significa que os resultados dos cálculos inteiros sejam completamente precisos e confiáveis. Esta seção irá apresentar algumas das razões que afetam a precisão dos cálculos inteiros.

) 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.

Por exemplo, existe a seguinte operação:

####
fn precision_test_div_before_mul[test]( {
    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)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"( .checked_mul)c( .expect)"ERR_MUL"(; assert_eq!)result_0,result_1,""(; }

Os resultados dos testes unitários são os seguintes:

executando 1 teste
thread "tests::precision_test_0" panicked at "assertion failed: )left == right(
 left: 2, right: 0: ", src/lib.rs:175:9

Podemos observar que result_0 = a * c / b e result_1 = )a / b(* c, apesar de terem a mesma fórmula de cálculo, os resultados das operações são diferentes.

Analisando as razões específicas: no que diz respeito à divisão inteira, a precisão menor que o divisor será descartada. Assim, no cálculo de result_1, a primeira operação a ser calculada, )a / b(, perderá primeiro a precisão de cálculo, resultando em 0; enquanto no cálculo de result_0, o resultado de a * c será calculado primeiro como 20_0000, que será maior que o divisor b, evitando assim o problema da perda de precisão e permitindo obter o resultado correto.

) 2.2 magnitude demasiado pequeno

####
fn precision_test_decimals[test]( {
    let a: u128 = 10;
    let b: u128 = 3;
    let c: u128 = 4;
    let decimal: u128 = 100_0000;
    // result_0 = )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, ""(; }

Os resultados específicos deste teste unitário são os seguintes:

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

Os resultados das operações result_0 e result_1, que têm processos de cálculo equivalentes, não são os mesmos, e result_1 = 13 está mais próximo do valor de cálculo esperado: 13.3333....

![])https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp(

3. Como escrever contratos inteligentes Rust de avaliação numérica

Garantir a precisão correta nos contratos inteligentes é muito importante. Embora a linguagem Rust também apresente problemas de perda de precisão nos resultados de operações inteiras, podemos adotar algumas medidas de proteção a seguir para melhorar a precisão e alcançar resultados satisfatórios.

) 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 magnitude dos números inteiros

  • Números inteiros usam ordens de grandeza maiores, criando numeradores maiores.

Por exemplo, para um token NEAR, se definirmos N = 10 conforme descrito acima, isso significa que: se for necessário representar o valor NEAR de 5.123, o valor inteiro utilizado nos cálculos será representado como 5.123 * 10^10 = 51_230_000_000. Este valor continuará a participar em cálculos inteiros subsequentes, o que pode aumentar a precisão dos cálculos.

3.3 perda de precisão acumulada em cálculos

Para problemas de precisão em cálculos inteiros que não podem ser evitados, a equipe do projeto pode considerar registrar a perda acumulada de precisão nos cálculos.

u128 é usado para distribuir tokens entre USER_NUM usuários.

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 em 1..7 { println!("Round {}",i); offset = distribute[test]to_yocto("10"), offset(; println!)"Offset {}\n",offset(; } }

Neste caso de teste, o sistema distribuirá 10 Tokens a 3 usuários a cada vez. No entanto, devido a problemas de precisão nas operações inteiras, ao calcular per_user_share na primeira rodada, o resultado da operação inteira foi 10 / 3 = 3, ou seja, os usuários da primeira rodada distribuirão, em média, 3 tokens, totalizando 9 tokens distribuídos.

Neste momento, pode-se perceber que ainda resta 1 token no sistema que não foi distribuído aos usuários. Para isso, pode-se considerar guardar temporariamente esse token restante na variável global do sistema, offset. Quando o sistema chamar novamente a função distribute para distribuir tokens aos usuários, esse valor será retirado e tentará ser distribuído junto com o montante de tokens desta rodada.

Segue o processo simulado de distribuição de tokens:

executando 1 teste
Rodada 1
per_user_share 3
Offset1
Rodada 2
per_user_share 3
Offset 2
Rodada 3
per_user_share 4
Offset 0
Rodada 4
per_user_share 3
Offset 1
Rodada 5
per_user_share 3
TOKEN3.62%
Ver original
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.
  • Recompensa
  • 6
  • Partilhar
Comentar
0/400
WalletWhisperervip
· 11h atrás
fascinante como os pontos flutuantes do rust poderiam ser nosso próximo pote de mel de vulnerabilidade... observando de perto
Ver originalResponder0
OnlyOnMainnetvip
· 11h atrás
Cálculo de ponto flutuante + na cadeia hehe, me assustou
Ver originalResponder0
TopEscapeArtistvip
· 11h atrás
Irmãos, este problema de precisão é tão exato quanto eu atingir o pico.
Ver originalResponder0
RamenDeFiSurvivorvip
· 11h atrás
Fui embora, esse problema de precisão é realmente irritante.
Ver originalResponder0
NFTArchaeologistvip
· 11h atrás
O problema da precisão é o mais mortal... se não for bem feito, pode levar a perdas totais.
Ver originalResponder0
MaticHoleFillervip
· 11h atrás
Quando é que posso escrever uma coleção de debug?
Ver originalResponder0
  • Pino
Negocie cripto em qualquer lugar e a qualquer hora
qrCode
Digitalizar para transferir a aplicação Gate
Novidades
Português (Portugal)
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)