Perhitungan nilai presisi dalam smart contract Rust: bilangan bulat vs bilangan pecahan

Rust smart contract pengembangan jurnal (7): akuntansi nilai

Tinjauan sebelumnya:

  • Rust smart contract pengembangan jurnal (1) definisi data status kontrak dan implementasi metode
  • Buku Harian Pengembangan Kontrak Pintar Rust (2) Menulis Pengujian Unit Kontrak Pintar Rust
  • Rust smart contract pengembangan jurnal (3) Rust smart contract penerapan, pemanggilan fungsi dan penggunaan Explorer
  • Rust smart contract diary (4) Rust smart contract integer overflow
  • Buku Harian Pengembangan Smart Contract Rust (5) Serangan Reentrancy
  • Rust smart contract pengembangan jurnal (6) serangan penolakan layanan

1. Masalah Presisi dalam Operasi Bilangan Patah

Berbeda dengan bahasa pemrograman smart contract yang umum, Solidity, bahasa Rust mendukung perhitungan angka desimal secara native. Namun, perhitungan angka desimal memiliki masalah akurasi komputasi yang tidak dapat dihindari. Oleh karena itu, dalam penulisan smart contract, tidak disarankan untuk menggunakan perhitungan angka desimal ( terutama ketika menangani rasio atau suku bunga yang melibatkan keputusan ekonomi/keuangan yang penting ).

Saat ini, sebagian besar bahasa pemrograman utama yang merepresentasikan angka floating point mengikuti standar IEEE 754, dan bahasa Rust tidak terkecuali. Berikut adalah penjelasan tentang tipe floating point presisi ganda f64 dalam bahasa Rust dan bentuk penyimpanan data biner di dalam komputer:

Bilangan pecahan menggunakan notasi ilmiah dengan basis 2 untuk menyatakannya. Misalnya, bilangan biner 0.1101 dengan jumlah digit terbatas dapat digunakan untuk menyatakan desimal 0.8125, cara konversi secara rinci adalah sebagai berikut:

0.8125 * 2 = 1 .625 // 0.1      Mendapatkan digit pertama dari desimal biner adalah 1
0.625  * 2 = 1 .25  // 0.11     Mendapatkan digit kedua dari pecahan biner adalah 1  
0.25   * 2 = 0 .5   // 0.110    mendapatkan digit ketiga dari desimal biner adalah 0
0.5    * 2 = 1 .0   // 0.1101   mendapatkan digit desimal biner ke-4 adalah 1

yaitu 0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1

Namun, untuk angka desimal lainnya yaitu 0.7, akan ada masalah sebagai berikut dalam proses konversinya menjadi angka floating point:

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

Jadi, desimal 0.7 akan dinyatakan sebagai 0.101100110011001100.....( yang berulang tanpa batas ), dan tidak dapat diwakili secara akurat dengan angka floating point yang memiliki panjang terbatas, serta terdapat fenomena "pembulatan ( Rounding )".

Misalkan di blockchain NEAR, perlu mendistribusikan 0,7 NEAR token kepada sepuluh pengguna, jumlah NEAR token yang diterima masing-masing pengguna akan dihitung dan disimpan dalam variabel result_0.

#[test]
fn precision_test_float() {
    // Bilangan desimal tidak dapat merepresentasikan bilangan bulat dengan akurat
    let amount: f64 = 0.7;     // Variabel amount ini menunjukkan 0.7 token NEAR
    let divisor: f64 = 10.0;   // mendefinisikan pembagi
    let result_0 = a / b;     // Melakukan operasi pembagian float
    println!("Nilai a: {:.20}", a);
    assert_eq!(result_0, 0.07, "");
}

Hasil keluaran dari pengujian kasus ini adalah sebagai berikut:

menjalankan 1 tes
Nilai a: 0.69999999999999995559
thread "tests::precision_test_float" panicked at "assertion failed: (left == right)
 kiri: 0.06999999999999999, kanan: 0.07: ", src/lib.rs:185:9

Dapat dilihat dalam perhitungan floating point di atas, nilai amount tidak secara akurat mewakili 0.7, melainkan sebuah nilai yang sangat mendekati 0.69999999999999995559. Lebih lanjut, untuk operasi pembagian tunggal seperti amount/divisor, hasil operasinya juga akan menjadi tidak tepat 0.06999999999999999, bukan 0.07 yang diharapkan. Dengan demikian, terlihat ketidakpastian dalam perhitungan angka floating.

Oleh karena itu, kita harus mempertimbangkan untuk menggunakan metode representasi numerik lainnya dalam smart contract, seperti angka titik tetap.

  1. Berdasarkan posisi tetap desimal dari bilangan titik tetap, ada dua jenis bilangan titik tetap yaitu bilangan bulat murni ( dan bilangan desimal murni ).
  2. Titik desimal tetap di belakang angka terendah, maka disebut sebagai bilangan bulat tetap.

Dalam penulisan smart contract yang sebenarnya, biasanya akan digunakan sebuah pecahan dengan penyebut tetap untuk merepresentasikan suatu nilai, misalnya pecahan "x/N", di mana "N" adalah konstanta, dan "x" dapat bervariasi.

Jika "N" bernilai "1.000.000.000.000.000.000", yaitu "10^18", saat ini desimal dapat diwakili sebagai bilangan bulat, seperti ini:

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

Dalam NEAR Protocol, nilai umum N adalah "10^24", yaitu 10^24 yoctoNEAR setara dengan 1 token NEAR.

Berdasarkan ini, kami dapat memodifikasi pengujian unit di bagian ini untuk melakukan perhitungan sebagai berikut:

#(
fn precision_test_integer)[test] {
    // Pertama, tentukan konstanta N, yang menunjukkan presisi.
    let N: u128 =    1_000_000_000_000_000_000_000_000;  // yaitu mendefinisikan 1 NEAR = 10^24 yoctoNEAR
    // Inisialisasi amount, sebenarnya saat ini nilai yang diwakili oleh amount adalah 700_000_000_000_000_000 / N = 0.7 NEAR; 
    let amount: u128 = 700_000_000_000_000_000_000_000; // yoctoNEAR
    // Inisialisasi penyebut divisor
    let divisor: u128 = 10; 
    // Hitung yang didapat:result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
    // Menunjukkan sebenarnya 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, "");
}

Dengan ini dapat diperoleh hasil perhitungan nilai aktuaria: 0,7 NEAR / 10 = 0,07 NEAR

menjalankan 1 tes
test tests::precision_test_integer ... ok
hasil uji: ok. 1 lulus; 0 gagal; 0 diabaikan; 0 diukur; 8 difilter; selesai dalam 0.00s

2. Masalah Presisi Perhitungan Bilangan Bulat Rust

Dari deskripsi di bagian 1 di atas, dapat ditemukan bahwa penggunaan operasi bilangan bulat dapat menyelesaikan masalah kehilangan presisi operasi bilangan pecahan dalam beberapa skenario operasi.

Namun, ini tidak berarti bahwa hasil perhitungan menggunakan bilangan bulat sepenuhnya akurat dan dapat diandalkan. Subbagian ini akan memperkenalkan beberapa alasan yang mempengaruhi akurasi perhitungan bilangan bulat.

( 2.1 Urutan Operasi

Perubahan urutan antara perkalian dan pembagian dengan prioritas aritmatika yang sama dapat langsung mempengaruhi hasil perhitungan, menyebabkan masalah presisi dalam perhitungan bilangan bulat.

Misalnya terdapat operasi berikut:

#)
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,""); }

Hasil dari pengujian unit adalah sebagai berikut:

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

Kita dapat menemukan result_0 = a * c / b dan result_1 = (a / b)* c meskipun rumus perhitungannya sama, namun hasil perhitungannya berbeda.

Analisis penyebab spesifiknya adalah: untuk pembagian bilangan bulat, presisi yang lebih kecil dari divisor akan diabaikan. Oleh karena itu, dalam proses perhitungan result_1, perhitungan awal (a / b) akan kehilangan presisi perhitungan dan menjadi 0; sedangkan dalam perhitungan result_0, hasil dari a * c akan diperoleh terlebih dahulu dengan hasil 20_0000, hasil ini akan lebih besar dari divisor b, sehingga menghindari masalah kehilangan presisi dan menghasilkan hasil perhitungan yang benar.

( 2.2 ukuran yang terlalu kecil

#)
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, ""); }

Hasil spesifik dari pengujian unit ini adalah sebagai berikut:

menjalankan 1 tes
12:13
thread "tests::precision_test_decimals" panicked at "assertion failed: (left == right)
 kiri: 12, kanan: 13: ", src/lib.rs:214:9

Dapat dilihat bahwa hasil operasi result_0 dan result_1 yang setara tidak sama, dan result_1 = 13 lebih dekat dengan nilai perhitungan yang diharapkan: 13.3333....

3. Cara Menulis Kontrak Pintar Rust untuk Aktuaria Numerik

Menjaga akurasi yang benar dalam smart contract sangat penting. Meskipun ada juga masalah kehilangan akurasi hasil operasi integer dalam bahasa Rust, kita dapat mengambil beberapa langkah perlindungan berikut untuk meningkatkan akurasi dan mencapai hasil yang memuaskan.

( 3.1 Menyesuaikan Urutan Operasi Perhitungan

  • Memprioritaskan perkalian bilangan bulat dibandingkan dengan pembagian bilangan bulat.

) 3.2 meningkatkan order bilangan bulat

  • Bilangan bulat menggunakan skala yang lebih besar, menciptakan pembilang yang lebih besar.

Misalnya untuk token NEAR, jika didefinisikan N = 10 seperti yang dijelaskan di atas, itu berarti: jika perlu mewakili nilai NEAR 5.123, maka nilai integer yang digunakan dalam perhitungan sebenarnya akan dinyatakan sebagai 5.123 * 10^10 = 51_230_000_000. Nilai ini akan terus berpartisipasi dalam perhitungan integer selanjutnya, yang dapat meningkatkan akurasi perhitungan.

3.3 Kerugian presisi akumulasi operasi

Untuk masalah presisi perhitungan integer yang tidak dapat dihindari, pihak proyek dapat mempertimbangkan untuk mencatat akumulasi kehilangan presisi perhitungan.

u128 untuk mendistribusikan token kepada USER_NUM pengguna.

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

Dalam kasus uji ini, sistem akan mendistribusikan 10 Token kepada 3 pengguna setiap kali. Namun, karena masalah presisi dalam operasi integer, pada putaran pertama saat menghitung per_user_share, hasil operasi integer yang didapat adalah 10 / 3 = 3, yang berarti pengguna yang didistribusikan pada putaran pertama akan menerima rata-rata 3 token, total 9 token telah didistribusikan.

Saat ini dapat ditemukan bahwa masih ada 1 token yang belum didistribusikan kepada pengguna dalam sistem. Oleh karena itu, dapat dipertimbangkan untuk menyimpan token yang tersisa tersebut sementara di variabel global sistem yang disebut offset. Menunggu hingga sistem memanggil kembali distribute untuk mendistribusikan token kepada pengguna, nilai tersebut akan diambil dan dicoba untuk didistribusikan bersama dengan jumlah token yang didistribusikan pada putaran ini.

Berikut adalah proses distribusi token yang disimulasikan:

menjalankan 1 tes
Putaran 1
per_user_share 3
Offset1
Putaran 2
per_user_share 3
Offset 2
Putaran 3
per_user_share 4
Offset 0
Putaran 4
per_user_share 3
Offset 1
Putaran 5
per_user_share 3
TOKEN5.57%
Lihat Asli
Halaman ini mungkin berisi konten pihak ketiga, yang disediakan untuk tujuan informasi saja (bukan pernyataan/jaminan) dan tidak boleh dianggap sebagai dukungan terhadap pandangannya oleh Gate, atau sebagai nasihat keuangan atau profesional. Lihat Penafian untuk detailnya.
  • Hadiah
  • 6
  • Bagikan
Komentar
0/400
WalletWhisperervip
· 08-06 11:51
menarik bagaimana titik mengapung rust bisa menjadi honeypot kerentanan kita berikutnya... mengawasi dengan seksama
Lihat AsliBalas0
OnlyOnMainnetvip
· 08-06 11:50
Perhitungan float + on-chain Hehe, membuat saya terkejut
Lihat AsliBalas0
TopEscapeArtistvip
· 08-06 11:45
Bro, masalah ketelitian ini sama tepatnya seperti aku menginjak puncak.
Lihat AsliBalas0
RamenDeFiSurvivorvip
· 08-06 11:24
Ayo pergi, masalah presisi ini benar-benar mengganggu.
Lihat AsliBalas0
NFTArchaeologistvip
· 08-06 11:23
Masalah akurasi yang paling mematikan... jika tidak hati-hati, bisa kehilangan semua uang.
Lihat AsliBalas0
MaticHoleFillervip
· 08-06 11:21
Kapan bisa menulis kumpulan debug?
Lihat AsliBalas0
  • Sematkan
Perdagangkan Kripto Di Mana Saja Kapan Saja
qrCode
Pindai untuk mengunduh aplikasi Gate
Komunitas
Bahasa Indonesia
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)