Rust智能合約數值精算:浮點數陷阱與整數精度優化

Rust智能合約養成日記(7)數值精算

1. 浮點數運算的精度問題

Rust語言原生支持浮點數運算,但浮點數運算存在着無法避免的計算精度問題。在編寫智能合約時,不推薦使用浮點數運算,尤其是處理涉及重要經濟/金融決策的比率或利率時。

Rust語言中浮點數遵循IEEE 754標準。以雙精度浮點類型f64爲例,其內部二進制表示如下:

浮點數採用底數爲2的科學計數法表達。例如0.8125可用有限位數的二進制數0.1101表示:

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

然而對於0.7這樣的小數,會出現無限循環的情況:

0.7 = 0.1011001100110011...

這就導致無法用有限位長的浮點數準確表示,存在"舍入"現象。

以在NEAR公鏈上分發0.7個NEAR代幣給十位用戶爲例:

rust #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("The value of amount: {:.20}", amount); assert_eq!(result_0, 0.07, ""); }

執行結果:

running 1 test The value of amount: 0.69999999999999995559 thread 'tests::precision_test_float' panicked at 'assertion failed: (left == right) left: 0.06999999999999999, right: 0.07: ', src/lib.rs:185:9

可見amount的值並非準確的0.7,而是一個近似值0.69999999999999995559。進一步的除法運算結果也變爲不精確的0.06999999999999999。

爲解決這個問題,可以考慮使用定點數。在NEAR Protocol中,通常採用10^24個yoctoNEAR等價於1個NEAR代幣的表示方法。

修改後的測試代碼:

rust #[test] fn precision_test_integer() { let N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000; let divisor: u128 = 10;
let result_0 = amount / divisor; assert_eq!(result_0, 70_000_000_000_000_000_000_000, ""); }

執行結果:

running 1 test test tests::precision_test_integer ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s

2. Rust整數計算精度的問題

2.1 運算順序

同一算數優先級的乘法與除法,其前後順序的變化可能直接影響到計算結果,導致整數計算精度的問題。

rust #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;

let result_0 = a
    .checked_mul(c)
    .expect("ERR_MUL")
    .checked_div(b)
    .expect("ERR_DIV");

let result_1 = a
    .checked_div(b)
    .expect("ERR_DIV")
    .checked_mul(c)
    .expect("ERR_MUL");

assert_eq!(result_0,result_1,"");

}

執行結果:

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

可以發現result_0 = a * c / b 和 result_1 = (a / b) * c 盡管計算公式相同,但結果不同。原因是整數除法會舍棄小於除數的精度。在計算result_1時,先計算 (a / b) 會導致精度丟失變爲0;而計算result_0時,先算得a * c = 20_0000大於除數b,避免了精度丟失。

2.2 過小的數量級

rust #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;

let result_0 = a
    .checked_div(b)
    .expect("ERR_DIV")
    .checked_mul(c)
    .expect("ERR_MUL");

let result_1 = a
    .checked_mul(decimal)  
    .expect("ERR_MUL")
    .checked_div(b)
    .expect("ERR_DIV")
    .checked_mul(c)
    .expect("ERR_MUL")
    .checked_div(decimal)  
    .expect("ERR_DIV");

println!("{}:{}", result_0, result_1);
assert_eq!(result_0, result_1, "");

}

執行結果:

running 1 test 12:13 thread 'tests::precision_test_decimals' panicked at 'assertion failed: (left == right) left: 12, right: 13: ', src/lib.rs:214:9

可見運算過程等價的result_0和result_1結果不同,且result_1 = 13更接近實際預期的13.3333....

3. 如何編寫數值精算的Rust智能合約

3.1 調整運算的操作順序

  • 令整數乘法優先於整數的除法。

3.2 增加整數的數量級

  • 整數使用更大的數量級,創造更大的分子。

3.3 積累運算精度的損失

對於無法避免的整數計算精度問題,可以考慮記錄累計的運算精度損失。

rust const USER_NUM: u128 = 3;

fn distribute(amount: u128, offset: u128) -> 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 }

#[test] fn record_offset_test() { let mut offset: u128 = 0; for i in 1..7 { println!("Round {}", i); offset = distribute(10_000_000_000_000_000_000_000_000, offset); println!("Offset {}\n", offset); } }

執行結果:

running 1 test Round 1 per_user_share 3333333333333333333333333 Offset 1

Round 2 per_user_share 3333333333333333333333333 Offset 2

Round 3 per_user_share 4000000000000000000000000 Offset 0

Round 4 per_user_share 3333333333333333333333333 Offset 1

Round 5 per_user_share 3333333333333333333333333 Offset 2

Round 6 per_user_share 4000000000000000000000000 Offset 0

test tests::record_offset_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 0.00s

3.4 使用Rust Crate庫rust-decimal

該庫適用於需要有效精度計算和沒有舍入誤差的小數金融計算。

3.5 考慮舍入機制

在設計智能合約時,舍入問題通常採用"我要佔便宜,他人不得薅我羊毛"的原則。根據這個原則,如果向下取整對我有利,則向下;如果向上取整對我有利,則向上;四舍五入不能確定是對誰有利,因此極少被採用。

查看原文
此頁面可能包含第三方內容,僅供參考(非陳述或保證),不應被視為 Gate 認可其觀點表述,也不得被視為財務或專業建議。詳見聲明
  • 讚賞
  • 5
  • 分享
留言
0/400
LiquidationKingvip
· 07-16 06:01
你说的0.7不是什么大事儿 来个大崩盘才带劲
回復0
破产艺术家vip
· 07-16 05:57
唉 写智能合约真的被浮点数坑惨过
回復0
GmGmNoGnvip
· 07-16 05:55
这bug能把我精度整丢了
回復0
NotSatoshivip
· 07-16 05:55
智能合约里玩浮点 脑袋都要炸咯
回復0
SandwichVictimvip
· 07-16 05:40
薛定谔的精度问题
回復0
交易,隨時隨地
qrCode
掃碼下載 Gate APP
社群列表
繁體中文
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)