Rustスマートコントラクトにおける正確な数値計算:整数vs浮動小数点

Rustスマートコントラクト養成日記(7):数値精算

過去の振り返り:

  • Rustスマートコントラクト育成日記(1)契約状態データ定義とメソッド実装
  • Rustスマートコントラクト育成日記(2)Rustスマートコントラクト単体テストの作成
  • Rustスマートコントラクト育成日記(3)Rustスマートコントラクトデプロイ、関数呼び出し及びExplorerの使用
  • Rustスマートコントラクト育成日記(4)Rustスマートコントラクト整数オーバーフロー
  • Rustスマートコントラクト育成日記(5)再入攻撃
  • Rustスマートコントラクト養成日記(6)拒絶サービス攻撃

1. 浮動小数点演算の精度問題

一般的なスマートコントラクトプログラミング言語のSolidityとは異なり、Rust言語は浮動小数点演算をネイティブにサポートしています。しかし、浮動小数点演算には避けられない計算精度の問題があります。したがって、スマートコントラクトを作成する際には、特に重要な経済/金融の意思決定に関わる比率や金利を扱う場合には、浮動小数点演算の使用は推奨されません(。

現在、主流のプログラミング言語で浮動小数点数を表すものはほとんどがIEEE 754標準に従っており、Rust言語も例外ではありません。以下はRust言語における倍精度浮動小数点型f64の説明とコンピュータ内部の二進数データ保存形式です:

浮動小数点数は、基数2の科学的表記法を用いて表現されます。例えば、有限桁数の2進数0.1101を用いて小数0.8125を表すことができ、具体的な変換方法は以下の通りです:

0.8125 * 2 = 1 .625 // 0.1      1位の2進小数を得る
0.625  * 2 = 1 .25  // 0.11     2桁目の2進小数は1です 
0.25   * 2 = 0 .5   // 0.110    第3位の2進数小数は0です
0.5    * 2 = 1 .0   // 0.1101   第4位の二進小数は1です

つまり 0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1

しかし、もう一つの小数0.7については、浮動小数点数に変換される過程で次のような問題が存在します:

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

小数0.7は0.101100110011001100.....)無限循環(として表され、有限の桁数の浮動小数点数では正確に表すことができず、"切り捨て)Rounding("現象が存在します。

NEARブロックチェーン上で、10人のユーザーに0.7NEARトークンを分配する必要があると仮定します。具体的に各ユーザーが受け取るNEARトークンの数量はresult_0変数に計算して保存されます。

#)
fn precision_test_float[test]( {
    // 浮動小数点数は整数を正確に表現できません
    量を仮定します:f64 = 0.7;     二次変数の量は0.7 NEARトークンを表します
    除数をしましょう:f64 = 10.0;   除数を定義する
    result_0 = a / b;     浮動小数点数に対する除算演算の実行
    println!)"a の値: {:.20}", a(;
    assert_eq!)result_0, 0.07, ""(;
}

実行したテストケースの出力結果は以下の通りです:

1つのテストを実行中
aの値:0.69999999999999995559
スレッド "tests::p recision_test_float" が "assertion failed: )left == right( でパニックに陥りました。
 左:0.0699999999999999999999999999、右:0.07:"、src / lib.rs:185:9

上記の浮動小数点演算において、amountの値は0.7を正確に表しているわけではなく、非常に近似した値である0.69999999999999995559です。さらに、amount/divisorの単一の除算演算に対しても、その演算結果は期待される0.07ではなく、不正確な0.06999999999999999になります。このように、浮動小数点数演算の不確実性が明らかになります。

これについて、私たちはスマートコントラクト内で他のタイプの数値表現方法、例えば固定小数点数を使用することを考慮せざるを得ません。

  1. 定点数は小数点の固定位置によって、定点)純(整数と定点)純(小数の2種類があります。
  2. 小数点が数の最低位の後に固定されている場合、それは定点整数と呼ばれます。

実際のスマートコントラクトの作成においては、通常、特定の数値を表すために固定分母の分数を使用します。例えば、分数「x/N」であり、「N」は定数で、「x」は変化可能です。

"N"の値が"1,000,000,000,000,000,000"、つまり"10^18"の場合、小数は整数として表すことができます。このように:

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では、Nの一般的な値は"10^24"であり、つまり10^24のyoctoNEARは1つのNEARトークンに相当します。

これに基づいて、本節の単体テストを以下の方法で計算するように変更できます。

#)
fn precision_test_integer[test]( {
    // まず定数Nを定義し、精度を表します。
    N: u128 = 1_000_000_000_000_000_000_000_000_000_000_000;  つまり、1 NEAR = 10^24 yoctoNEARが定義されます
    金額を初期化すると、金額で表される値は700_000_000_000_000_000_000_000 / N = 0.7 NEARです。 
    量を仮定します: U128 = 700_000_000_000_000_000_000_000_000; yoctoニア
    除数を初期化します
    除数をしましょう:u128 = 10; 
    計算の結果: result_0 = 70_000_000_000_000_000_000_000_000_000 // yoctoNEAR
    実際の表現は 700_000_000_000_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_000, ""(;
}

これにより、数値計算の結果を得ることができます: 0.7 NEAR / 10 = 0.07 NEAR

1つのテストを実行中
テストテスト::精度テスト整数 ... ok
テスト結果: ok. 1 合格; 0 不合格; 0 無視; 0 測定; 8 フィルタリングされた; 0.00秒で終了

! [])https://img-cdn.gateio.im/webp-social/moments-7bdd27c1211e1cc345bf262666a993da.webp(

2. Rustの整数計算の精度に関する問題

上記の第1節の説明から、整数演算を使用することで、特定の演算シナリオでの浮動小数点演算の精度損失問題を解決できることがわかります。

しかし、これは整数計算の結果が完全に正確で信頼できることを意味するわけではありません。この小節では、整数計算の精度に影響を与えるいくつかの理由について紹介します。

) 2.1 操作の順序

同じ算数の優先順位の乗算と除算では、その前後の順序の変化が計算結果に直接影響を及ぼす可能性があり、整数計算の精度に問題を引き起こすことがあります。

例えば、以下の演算が存在します:

####
fn precision_test_div_before_mul[test]( {
    Aを仮定します:U128 = 1_0000;
    Bを仮定します:U128 = 10_0000;
    Cを仮定します:U128 = 20;
    result_0 = a * c / b
    result_0 = a とします
        .checked_mul)c(
        .expect)"ERR_MUL"(
        .checked_div)b(
        .expect)"ERR_DIV"(;
    result_0 = a / b * c
    result_1 = a とします
        .checked_div)b(
        .expect)"ERR_DIV"(
        .checked_mul)c(
        .expect)"ERR_MUL"(;
    assert_eq!)result_0,result_1,"(;
}

ユニットテストの結果は以下の通りです:

1つのテストを実行中
スレッド "tests::p recision_test_0" が "assertion failed: )left == right( でパニックに陥りました
 左:2、右: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 小さすぎる数量級

####
fn precision_test_decimals[test]( {
    Aを仮定します:U128 = 10;
    Bを仮定します:u128 = 3;
    C:u128 = 4とします。
    小数で仮定します:u128 = 100_0000;
    result_0 = )a / b( * c
    result_0 = a とします
        .checked_div)b(
        .expect)"ERR_DIV"(
        .checked_mul)c(
        .expect)"ERR_MUL"(;
    // result_1 = )a * デシマル / b( * c / デシマル;  
    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, ""(;
}

この単体テストの具体的な結果は以下の通りです:

1つのテストを実行中
12:13
スレッド "tests::p recision_test_decimals" は "assertion failed: )left == right( でパニックに陥りました。
 左:12、右:13:"、src / lib.rs:214:9

可視化された計算過程において等価なresult_0とresult_1の計算結果は異なり、かつresult_1 = 13は実際の期待される計算値:13.3333....にさらに近い。

! [])https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp(

3. 数値精算のRustスマートコントラクトの書き方

スマートコントラクトにおいて正しい精度を保証することは非常に重要です。Rust言語にも整数演算結果の精度喪失の問題が存在しますが、精度を向上させ、満足のいく結果を得るためにいくつかの防護手段を講じることができます。

) 3.1 操作の順序を調整する

  • 整数の乗法を整数の除法よりも優先させる。

3.2 整数の数量級を増加させる

  • 整数はより大きな数量を使用し、より大きな分子を創造します。

例えば、NEARトークンについて、上文で説明したN = 10と定義すると、5.123のNEAR価値を表す必要がある場合、実際の計算で使用される整数値は5.123 * 10^10 = 51_230_000_000として表されます。この値はその後の整数計算に引き続き参加し、計算精度を向上させることができます。

3.3 運用精度の累積損失

整数計算の精度問題が確実に避けられない場合、プロジェクトチームは累積した計算精度の損失を記録することを検討できます。

u128を使用してUSER_NUMのユーザーにトークンを配分します。

定数 USER_NUM: u128 = 3;
fn distribute###amount: u128, オフセット: u128( -> u128 {
    token_to_distribute = オフセット + 金額とします。
    per_user_share = token_to_distribute / USER_NUMとします。
    println!)"per_user_share {}",per_user_share(;
    recorded_offset = token_to_distribute - per_user_share * USER_NUM;
    recorded_offset
}
#)
fn record_offset_test() {
    mutオフセットをしましょう:u128 = 0;
    iを1から7まで繰り返す {
        println![test]"ラウンド {}",i(;
        オフセット = distribute)to_yocto("10"), offset(;
        println!("オフセット {}\n",offset);
    }
}

このテストケースでは、システムは毎回3人のユーザーに10個のトークンを配布します。しかし、整数計算の精度の問題により、最初のラウンドでper_user_shareを計算する際、得られた整数計算の結果は10 / 3 = 3となります。つまり、最初のラウンドでのdistributeユーザーは平均して3つのトークンを受け取ることになり、合計9つのトークンが配布されます。

この時、システムにはまだ1つのトークンがユーザーに配布されていないことがわかります。そのため、この残りのトークンをシステムのグローバル変数offsetに一時的に保存することを検討できます。次回システムが再びユーザーにトークンを配布するためにdistributeを呼び出す際に、その値は取り出され、今回の配布のトークンの金額と一緒にユーザーに配布されることを試みます。

以下はシミュレーションされたトークン配布プロセスです:

1つのテストを実行中
ラウンド1
per_user_share 3
オフセット 1
ラウンド2
per_user_share 3
オフセット 2
ラウンド3
per_user_share 4
オフセット 0
ラウンド4
per_user_share 3
オフセット 1
ラウンド5
per_user_share 3
TOKEN3.56%
原文表示
このページには第三者のコンテンツが含まれている場合があり、情報提供のみを目的としております(表明・保証をするものではありません)。Gateによる見解の支持や、金融・専門的な助言とみなされるべきものではありません。詳細については免責事項をご覧ください。
  • 報酬
  • 6
  • 共有
コメント
0/400
WalletWhisperervip
· 20時間前
興味深いことに、Rustの浮動小数点が私たちの次の脆弱性ハニーポットになる可能性がある... 注視している
原文表示返信0
OnlyOnMainnetvip
· 20時間前
浮動小数点計算+オンチェーン 笑
原文表示返信0
TopEscapeArtistvip
· 20時間前
鉄子たち、この精度の問題は私が頂上に登るのと同じくらい正確だね。
原文表示返信0
RamenDeFiSurvivorvip
· 21時間前
逃げた逃げた この精度の問題は本当にイライラする
原文表示返信0
NFTArchaeologistvip
· 21時間前
精度の問題が最も致命的です…うまくいかなければ全てを失うことになります
原文表示返信0
MaticHoleFillervip
· 21時間前
いつデバッグコレクションを書けるかな
原文表示返信0
いつでもどこでも暗号資産取引
qrCode
スキャンしてGateアプリをダウンロード
コミュニティ
日本語
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)