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:

  • Rust akıllı sözleşmeler yetiştirme günlüğü (1) sözleşme durum verisi tanımı ve yöntem gerçekleştirme
  • Rust akıllı sözleşmeler yetiştirme günlüğü (2) Rust akıllı sözleşmeler birim testleri yazma
  • Rust akıllı sözleşmeler yetiştirme günlüğü (3) Rust akıllı sözleşme dağıtımı, fonksiyon çağrısı ve Explorer'ın kullanımı
  • Rust akıllı sözleşmeler yetiştirme günlüğü ( 4) Rust akıllı sözleşmeler tam sayı taşması
  • Rust akıllı sözleşmeler yetiştirme günlüğü (5) reentrancy saldırısı
  • 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:

1 test çalıştırılıyor
a'nın değeri: 0.69999999999999995559
thread "tests::precision_test_float" panicked at "assertion failed: (left == right)
 left: 0.06999999999999999, right: 0.07: ", src/lib.rs:185:9

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.

  1. 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.
  2. 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:

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

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
TOKEN3.1%
View Original
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.
  • Reward
  • 6
  • Share
Comment
0/400
WalletWhisperervip
· 7h ago
rust'ın kayan noktalarının bizim sonraki güvenlik açığı honeypot'umuz olabileceği büyüleyici... dikkatle izliyorum
View OriginalReply0
OnlyOnMainnetvip
· 7h ago
Kayan nokta hesaplama + on-chain Hehe beni korkuttun
View OriginalReply0
TopEscapeArtistvip
· 7h ago
Kardeşlerim, bu hassasiyet sorunu benim zirveye çıkmam kadar doğru.
View OriginalReply0
RamenDeFiSurvivorvip
· 7h ago
Kaçtım kaçtım bu hassasiyet sorunu gerçekten can sıkıcı.
View OriginalReply0
NFTArchaeologistvip
· 7h ago
Hassasiyet sorunu en ölümcül olanıdır... İyi yönetilmezse, tüm sermaye kaybolur.
View OriginalReply0
MaticHoleFillervip
· 7h ago
Ne zaman bir debug derlemesi yazabilirim?
View OriginalReply0
Trade Crypto Anywhere Anytime
qrCode
Scan to download Gate app
Community
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)