Rust akıllı sözleşmeler yetiştirme günlüğü (6) hizmet reddi saldırısı
1. Ondalık sayı işlemlerinin hassasiyet sorunu
Yaygın akıllı sözleşmeler programlama dili Solidity'den farklı olarak, Rust dili yerel olarak ondalık sayı işlemlerini destekler. Ancak, ondalık sayı işlemleri kaçınılmaz hesaplama hassasiyeti sorunları taşır. Bu nedenle, akıllı sözleşme yazarken, özellikle önemli ekonomik/finansal kararlarla ilgili oran veya faiz oranlarını işlerken ondalık sayı işlemlerinin kullanılmasını tavsiye etmiyoruz (.
Günümüzdeki ana akım programlama dilleri, kayan noktalı sayıları genellikle IEEE 754 standardına göre temsil etmektedir; Rust dili de bu kuralın dışındadır. Aşağıda Rust dilinde çift hassasiyetli kayan nokta tipi f64 ile ilgili açıklama ve bilgisayarın içindeki ikili veri saklama biçimi bulunmaktadır:
Kayan nokta sayıları, tabanı 2 olan bilimsel notasyon ile ifade edilir. Örneğin, sınırlı haneli ikili sayı 0.1101 kullanılarak 0.8125 ondalık sayısı temsil edilebilir, dönüşüm yöntemi aşağıdaki gibidir:
0.8125 * 2 = 1 .625 // 0.1 1. binary ondalık kesirini aldık.
0.625 * 2 = 1 .25 // 0.11 2. bit ikili ondalık 1 olarak elde edildi
0.25 * 2 = 0 .5 // 0.110 3. bit ikili ondalık 0 olarak elde edildi
0.5 * 2 = 1 .0 // 0.1101 4. basamak ikili ondalık 1 olarak elde edilir.
Yani 0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Ancak diğer bir ondalık olan 0.7 için, gerçek sayıya dönüştürme sürecinde aşağıdaki sorunlar ortaya çıkacaktır:
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
....
Yani ondalık 0.7, 0.101100110011001100.....) sonsuz döngü olarak gösterilecektir, sınırlı uzunlukta bir kayan nokta sayısı ile doğru bir şekilde gösterilemez ve "yuvarlama (Rounding )" olayı vardır.
NEAR blok zincirinde, on kullanıcıya toplam 0.7 NEAR tokeni dağıtılması gerektiğini varsayalım, her bir kullanıcının alacağı NEAR tokeni miktarı result_0 değişkeninde hesaplanıp saklanacaktır.
#(
fn precision_test_float)[test] {
// Ondalık sayılar tam sayıları doğru bir şekilde temsil edemez
let amount: f64 = 0.7; // Bu amount değişkeni 0.7 NEAR tokenini temsil eder
let divisor: f64 = 10.0; // bölgenin tanımı
let result_0 = a / b; // Ondalık sayıların bölme işlemini gerçekleştir
println!("a'nın değeri: {:.20}", a);
assert_eq!(result_0, 0.07, "");
}
Bu test durumunun çıktı sonuçları aşağıdaki gibidir:
Yukarıdaki kayan nokta işlemlerinde, amount'un değeri tam olarak 0.7'yi temsil etmemektedir, aksine son derece yakın bir değer olan 0.69999999999999995559'dur. Ayrıca, amount/divisor gibi tek bir bölme işlemi için, işlem sonucu da beklenen 0.07 değil, belirsiz bir 0.06999999999999999 olacaktır. Bu da kayan nokta sayıların işlemlerindeki belirsizliği göstermektedir.
Buna karşın, akıllı sözleşmelerde diğer tür sayısal gösterim yöntemlerinin, örneğin sabit noktalı sayıların kullanılmasını düşünmek zorundayız.
Sabit noktaya dayalı sayılarda ondalık noktanın sabit konumuna göre, sabit noktalı sayılar sabit ( tam ) sayı ve sabit ( tam ) ondalık iki tür vardır.
Ondalık nokta sayının en düşük basamağının arkasında sabitlenirse, buna sabit nokta tam sayı denir.
Gerçek akıllı sözleşme yazımında, genellikle belirli bir değeri ifade etmek için sabit bir paydası olan bir kesir kullanılır, örneğin kesir "x/N", burada "N" bir sabittir ve "x" değişkenlik gösterebilir.
Eğer "N" değeri "1.000.000.000.000.000.000" yani "10^18" olarak alınırsa, bu durumda ondalık sayılar tam sayı olarak ifade edilebilir, şöyle:
NEAR Protocol'da, bu N'nin yaygın değeri "10^24" olup, 10^24 yoctoNEAR 1 NEAR token'e eşdeğerdir.
Buna dayanarak, bu bölümün birim testini hesaplamak için aşağıdaki şekilde değiştirebiliriz:
#(
fn precision_test_integer)[test] {
// Öncelikle sabit N'yi tanımlayın, hassasiyeti belirtir.
let N: u128 = 1_000_000_000_000_000_000_000_000; // Yani 1 NEAR = 10^24 yoctoNEAR olarak tanımlanır
// amount'ı başlat, aslında bu durumda amount'un temsil ettiği değer 700_000_000_000_000_000 / N = 0.7 NEAR;
let amount: u128 = 700_000_000_000_000_000_000_000; // yoctoNEAR
// Bölme işlemi için payda divisor'u başlat
let divisor: u128 = 10;
// Hesaplanan:result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
// Gerçekten 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, "");
}
Bununla birlikte, sayısal hesaplamanın sonucu elde edilebilir: 0.7 NEAR / 10 = 0.07 NEAR
1 test çalıştırılıyor
test testler::kesinlik_testi_tamsayı ... tamam
test sonucu: tamam. 1 geçti; 0 başarısız; 0 görmezden gelindi; 0 ölçüldü; 8 filtrelendi; 0.00s içinde tamamlandı
2. Rust tamsayı hesaplama hassasiyeti sorunu
Yukarıdaki 1. bölümdeki açıklamalardan, bazı hesaplama senaryolarında tam sayı işlemleri kullanmanın, kayan nokta işlemlerindeki hassasiyet kaybı sorununu çözebileceği anlaşılmaktadır.
Ancak bu, tam sayı hesaplamalarının sonuçlarının tamamen doğru ve güvenilir olduğu anlamına gelmez. Bu bölüm, tam sayı hesaplama doğruluğunu etkileyen bazı nedenleri tanıtacaktır.
( 2.1 İşlem Sırası
Aynı aritmetik önceliğe sahip çarpma ve bölme işlemlerinin sırasındaki değişiklik, hesaplama sonucunu doğrudan etkileyebilir ve tam sayı hesaplama doğruluğu sorunlarına yol açabilir.
Örneğin aşağıdaki işlem vardır:
#)
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,"");
}
Birim testlerinin sonuçları aşağıdaki gibidir:
1 test çalıştırılıyor
thread "tests::precision_test_0" panicked at "assertion failed: (left == right)
sol: 2, sağ: 0: ", src/lib.rs:175:9
result_0 = a * c / b ve result_1 = (a / b)* c formülleri aynı olmasına rağmen, hesaplama sonuçları farklıdır.
Analizin spesifik nedeni şudur: Tam sayı bölmesi açısından, bölenin altında olan hassasiyet atılacaktır. Bu nedenle result_1'in hesaplanması sürecinde, öncelikle hesaplanan (a / b) hesaplama hassasiyetini kaybedecek ve 0'a dönüşecektir; result_0 hesaplanırken, önce a * c'nin sonucu 20_0000 hesaplanacak, bu sonuç bölen b'den büyük olacağından, hassasiyet kaybı problemi önlenmiş olacak ve doğru hesaplama sonucu elde edilecektir.
( 2.2 çok küçük bir büyüklük
#)
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) // çarpma decimal
.expect("ERR_MUL")
.checked_div(b)
.expect("ERR_DIV")
.checked_mul(c)
.expect("ERR_MUL")
.checked_div(ondalık) // div ondalık
.expect("ERR_DIV");
println!("{}:{}", result_0, result_1);
assert_eq!(result_0, result_1, "");
}
Bu birim testinin spesifik sonuçları aşağıdaki gibidir:
1 test çalıştırılıyor
12:13
thread "tests::precision_test_decimals" panicked at "assertion failed: (left == right)
sol: 12, sağ: 13: ", src/lib.rs:214:9
Görülebilir işlem süreci eşdeğer olan result_0 ve result_1'in işlem sonuçları aynı değildir ve result_1 = 13, gerçek beklenen hesap değerine: 13.3333.... daha yakındır.
3. Sayısal Aktüerya için Rust akıllı sözleşmeler nasıl yazılır
Akıllı sözleşmelerde doğru hassasiyetin sağlanması son derece önemlidir. Rust dilinde de tam sayı işlemlerinin sonuçlarında hassasiyet kaybı sorunu olsa da, hassasiyeti artırmak ve tatmin edici bir sonuç elde etmek için aşağıdaki bazı koruma önlemlerini alabiliriz.
( 3.1 İşlem sırasını ayarlama
Tam sayı çarpımını tam sayı bölümünden önce yapın.
) 3.2 Tam sayıların büyüklüğünü artırmak
Tam sayılar daha büyük bir ölçekte kullanılır, daha büyük paylar yaratır.
Örneğin, bir NEAR token için, yukarıda tanımlanan N = 10 olarak belirlenirse, bu, 5.123 değerindeki NEAR değerini ifade etmek gerektiğinde, gerçek hesaplamada kullanılan tam sayı değeri 5.123 * 10^10 = 51_230_000_000 olarak ifade edileceği anlamına gelir. Bu değer, sonraki tam sayı hesaplamalarına devam eder ve hesaplama hassasiyetini artırabilir.
3.3 Hesaplama hassasiyetinin kaybı
Kesinlikle kaçınılmaz olan tam sayı hesaplama hassasiyeti sorunları için, proje ekibi biriken hesaplama hassasiyeti kaybını kaydetmeyi düşünebilir.
u128 ile USER_NUM kadar kullanıcıya token dağıtma senaryosu varsayılmaktadır.
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;
kayıtlı_ofset
}
#(
fn record_offset_test)( {
let mut offset: u128 = 0;
for i in 1..7 {
println!)"Round {}",i(;
offset = distribute)to_yocto[test]"10"(, offset);
println!("Offset {}\n",offset);
}
}
Bu test senaryosunda, sistem her seferinde 3 kullanıcıya 10 Token dağıtacak. Ancak, tam sayı işlemleri hassasiyeti nedeniyle, ilk turda per_user_share hesaplanırken elde edilen tam sayı işlemi sonucu 10 / 3 = 3'tür, yani ilk turda dağıtılan kullanıcılar ortalama 3 token alacak, toplamda 9 token dağıtılmış olacak.
Bu aşamada, sistemde kullanıcıya dağıtılmamış 1 adet token kaldığı görülebilir. Bu nedenle, bu kalan token'in sistemin genel değişkeni olan offset'te geçici olarak saklanması düşünülebilir. Sistem bir sonraki kez kullanıcıya token dağıtmak için distribute'yi tekrar çağırdığında, bu değer alınacak ve bu turda dağıtılan token miktarıyla birlikte kullanıcıya dağıtılmaya çalışılacaktır.
Aşağıda simüle edilmiş token dağıtım süreci yer almaktadır:
1 test çalıştırılıyor
1. Tur
kullanıcı_başına_pay 3
Offset1
2. Tur
per_user_share 3
Offset 2
3. Tur
per_user_share 4
Ofset 0
Dönem 4
her_kullanıcı_payı 3
Ofset 1
5. Tur
kullanıcı_başına_pay 3
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
9 Likes
Reward
9
6
Share
Comment
0/400
WalletWhisperer
· 7h ago
rust'ın kayan noktalarının bizim sonraki güvenlik açığı honeypot'umuz olabileceği büyüleyici... dikkatle izliyorum
View OriginalReply0
OnlyOnMainnet
· 7h ago
Kayan nokta hesaplama + on-chain Hehe beni korkuttun
View OriginalReply0
TopEscapeArtist
· 7h ago
Kardeşlerim, bu hassasiyet sorunu benim zirveye çıkmam kadar doğru.
View OriginalReply0
RamenDeFiSurvivor
· 7h ago
Kaçtım kaçtım bu hassasiyet sorunu gerçekten can sıkıcı.
View OriginalReply0
NFTArchaeologist
· 7h ago
Hassasiyet sorunu en ölümcül olanıdır... İyi yönetilmezse, tüm sermaye kaybolur.
Rust akıllı sözleşmelerindeki hassas sayı hesaplaması: tam sayılar vs kayan noktalar
Rust akıllı sözleşmeler yetiştirme günlüğü (7): Sayısal hesaplama
Geçmiş Dönem İncelemesi:
1. Ondalık sayı işlemlerinin hassasiyet sorunu
Yaygın akıllı sözleşmeler programlama dili Solidity'den farklı olarak, Rust dili yerel olarak ondalık sayı işlemlerini destekler. Ancak, ondalık sayı işlemleri kaçınılmaz hesaplama hassasiyeti sorunları taşır. Bu nedenle, akıllı sözleşme yazarken, özellikle önemli ekonomik/finansal kararlarla ilgili oran veya faiz oranlarını işlerken ondalık sayı işlemlerinin kullanılmasını tavsiye etmiyoruz (.
Günümüzdeki ana akım programlama dilleri, kayan noktalı sayıları genellikle IEEE 754 standardına göre temsil etmektedir; Rust dili de bu kuralın dışındadır. Aşağıda Rust dilinde çift hassasiyetli kayan nokta tipi f64 ile ilgili açıklama ve bilgisayarın içindeki ikili veri saklama biçimi bulunmaktadır:
Kayan nokta sayıları, tabanı 2 olan bilimsel notasyon ile ifade edilir. Örneğin, sınırlı haneli ikili sayı 0.1101 kullanılarak 0.8125 ondalık sayısı temsil edilebilir, dönüşüm yöntemi aşağıdaki gibidir:
Ancak diğer bir ondalık olan 0.7 için, gerçek sayıya dönüştürme sürecinde aşağıdaki sorunlar ortaya çıkacaktır:
Yani ondalık 0.7, 0.101100110011001100.....) sonsuz döngü olarak gösterilecektir, sınırlı uzunlukta bir kayan nokta sayısı ile doğru bir şekilde gösterilemez ve "yuvarlama (Rounding )" olayı vardır.
NEAR blok zincirinde, on kullanıcıya toplam 0.7 NEAR tokeni dağıtılması gerektiğini varsayalım, her bir kullanıcının alacağı NEAR tokeni miktarı result_0 değişkeninde hesaplanıp saklanacaktır.
Bu test durumunun çıktı sonuçları aşağıdaki gibidir:
Yukarıdaki kayan nokta işlemlerinde, amount'un değeri tam olarak 0.7'yi temsil etmemektedir, aksine son derece yakın bir değer olan 0.69999999999999995559'dur. Ayrıca, amount/divisor gibi tek bir bölme işlemi için, işlem sonucu da beklenen 0.07 değil, belirsiz bir 0.06999999999999999 olacaktır. Bu da kayan nokta sayıların işlemlerindeki belirsizliği göstermektedir.
Buna karşın, akıllı sözleşmelerde diğer tür sayısal gösterim yöntemlerinin, örneğin sabit noktalı sayıların kullanılmasını düşünmek zorundayız.
Gerçek akıllı sözleşme yazımında, genellikle belirli bir değeri ifade etmek için sabit bir paydası olan bir kesir kullanılır, örneğin kesir "x/N", burada "N" bir sabittir ve "x" değişkenlik gösterebilir.
Eğer "N" değeri "1.000.000.000.000.000.000" yani "10^18" olarak alınırsa, bu durumda ondalık sayılar tam sayı olarak ifade edilebilir, şöyle:
NEAR Protocol'da, bu N'nin yaygın değeri "10^24" olup, 10^24 yoctoNEAR 1 NEAR token'e eşdeğerdir.
Buna dayanarak, bu bölümün birim testini hesaplamak için aşağıdaki şekilde değiştirebiliriz:
Bununla birlikte, sayısal hesaplamanın sonucu elde edilebilir: 0.7 NEAR / 10 = 0.07 NEAR
2. Rust tamsayı hesaplama hassasiyeti sorunu
Yukarıdaki 1. bölümdeki açıklamalardan, bazı hesaplama senaryolarında tam sayı işlemleri kullanmanın, kayan nokta işlemlerindeki hassasiyet kaybı sorununu çözebileceği anlaşılmaktadır.
Ancak bu, tam sayı hesaplamalarının sonuçlarının tamamen doğru ve güvenilir olduğu anlamına gelmez. Bu bölüm, tam sayı hesaplama doğruluğunu etkileyen bazı nedenleri tanıtacaktır.
( 2.1 İşlem Sırası
Aynı aritmetik önceliğe sahip çarpma ve bölme işlemlerinin sırasındaki değişiklik, hesaplama sonucunu doğrudan etkileyebilir ve tam sayı hesaplama doğruluğu sorunlarına yol açabilir.
Örneğin aşağıdaki işlem vardır:
.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,""); }
Birim testlerinin sonuçları aşağıdaki gibidir:
result_0 = a * c / b ve result_1 = (a / b)* c formülleri aynı olmasına rağmen, hesaplama sonuçları farklıdır.
Analizin spesifik nedeni şudur: Tam sayı bölmesi açısından, bölenin altında olan hassasiyet atılacaktır. Bu nedenle result_1'in hesaplanması sürecinde, öncelikle hesaplanan (a / b) hesaplama hassasiyetini kaybedecek ve 0'a dönüşecektir; result_0 hesaplanırken, önce a * c'nin sonucu 20_0000 hesaplanacak, bu sonuç bölen b'den büyük olacağından, hassasiyet kaybı problemi önlenmiş olacak ve doğru hesaplama sonucu elde edilecektir.
( 2.2 çok küçük bir büyüklük
.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) // çarpma decimal .expect("ERR_MUL") .checked_div(b) .expect("ERR_DIV") .checked_mul(c) .expect("ERR_MUL") .checked_div(ondalık) // div ondalık .expect("ERR_DIV"); println!("{}:{}", result_0, result_1); assert_eq!(result_0, result_1, ""); }
Bu birim testinin spesifik sonuçları aşağıdaki gibidir:
Görülebilir işlem süreci eşdeğer olan result_0 ve result_1'in işlem sonuçları aynı değildir ve result_1 = 13, gerçek beklenen hesap değerine: 13.3333.... daha yakındır.
3. Sayısal Aktüerya için Rust akıllı sözleşmeler nasıl yazılır
Akıllı sözleşmelerde doğru hassasiyetin sağlanması son derece önemlidir. Rust dilinde de tam sayı işlemlerinin sonuçlarında hassasiyet kaybı sorunu olsa da, hassasiyeti artırmak ve tatmin edici bir sonuç elde etmek için aşağıdaki bazı koruma önlemlerini alabiliriz.
( 3.1 İşlem sırasını ayarlama
) 3.2 Tam sayıların büyüklüğünü artırmak
Örneğin, bir NEAR token için, yukarıda tanımlanan N = 10 olarak belirlenirse, bu, 5.123 değerindeki NEAR değerini ifade etmek gerektiğinde, gerçek hesaplamada kullanılan tam sayı değeri 5.123 * 10^10 = 51_230_000_000 olarak ifade edileceği anlamına gelir. Bu değer, sonraki tam sayı hesaplamalarına devam eder ve hesaplama hassasiyetini artırabilir.
3.3 Hesaplama hassasiyetinin kaybı
Kesinlikle kaçınılmaz olan tam sayı hesaplama hassasiyeti sorunları için, proje ekibi biriken hesaplama hassasiyeti kaybını kaydetmeyi düşünebilir.
u128 ile USER_NUM kadar kullanıcıya token dağıtma senaryosu varsayılmaktadır.
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; kayıtlı_ofset } #( fn record_offset_test)( { let mut offset: u128 = 0; for i in 1..7 { println!)"Round {}",i(; offset = distribute)to_yocto[test]"10"(, offset); println!("Offset {}\n",offset); } }
Bu test senaryosunda, sistem her seferinde 3 kullanıcıya 10 Token dağıtacak. Ancak, tam sayı işlemleri hassasiyeti nedeniyle, ilk turda per_user_share hesaplanırken elde edilen tam sayı işlemi sonucu 10 / 3 = 3'tür, yani ilk turda dağıtılan kullanıcılar ortalama 3 token alacak, toplamda 9 token dağıtılmış olacak.
Bu aşamada, sistemde kullanıcıya dağıtılmamış 1 adet token kaldığı görülebilir. Bu nedenle, bu kalan token'in sistemin genel değişkeni olan offset'te geçici olarak saklanması düşünülebilir. Sistem bir sonraki kez kullanıcıya token dağıtmak için distribute'yi tekrar çağırdığında, bu değer alınacak ve bu turda dağıtılan token miktarıyla birlikte kullanıcıya dağıtılmaya çalışılacaktır.
Aşağıda simüle edilmiş token dağıtım süreci yer almaktadır: