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:
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.
Berdasarkan posisi tetap desimal dari bilangan titik tetap, ada dua jenis bilangan titik tetap yaitu bilangan bulat murni ( dan bilangan desimal murni ).
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:
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,"");
}
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:
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:
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.
9 Suka
Hadiah
9
6
Bagikan
Komentar
0/400
WalletWhisperer
· 22jam yang lalu
menarik bagaimana titik mengapung rust bisa menjadi honeypot kerentanan kita berikutnya... mengawasi dengan seksama
Lihat AsliBalas0
OnlyOnMainnet
· 22jam yang lalu
Perhitungan float + on-chain Hehe, membuat saya terkejut
Lihat AsliBalas0
TopEscapeArtist
· 22jam yang lalu
Bro, masalah ketelitian ini sama tepatnya seperti aku menginjak puncak.
Lihat AsliBalas0
RamenDeFiSurvivor
· 22jam yang lalu
Ayo pergi, masalah presisi ini benar-benar mengganggu.
Lihat AsliBalas0
NFTArchaeologist
· 22jam yang lalu
Masalah akurasi yang paling mematikan... jika tidak hati-hati, bisa kehilangan semua uang.
Perhitungan nilai presisi dalam smart contract Rust: bilangan bulat vs bilangan pecahan
Rust smart contract pengembangan jurnal (7): akuntansi nilai
Tinjauan sebelumnya:
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:
Namun, untuk angka desimal lainnya yaitu 0.7, akan ada masalah sebagai berikut dalam proses konversinya menjadi angka floating point:
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.
Hasil keluaran dari pengujian kasus ini adalah sebagai berikut:
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.
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:
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:
Dengan ini dapat diperoleh hasil perhitungan nilai aktuaria: 0,7 NEAR / 10 = 0,07 NEAR
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:
.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:
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
.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:
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
) 3.2 meningkatkan order bilangan bulat
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.
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: