يوميات تطوير العقود الذكية Rust(2) كتابة اختبارات الوحدة لعقود Rust الذكية
يوميات تطوير العقود الذكية Rust(3) نشر العقود الذكية Rust واستخدام استدعاء الدوال وExplorer
Rust العقود الذكية养成日记(4)Rust العقود الذكية整数溢出
Rust العقود الذكية养成日记(5)重入攻击
Rustالعقود الذكية养成日记(6)رفض هجوم الخدمة
1. مشكلة دقة العمليات العشرية
على عكس لغة البرمجة الشائعة للعقود الذكية Solidity، تدعم لغة Rust العمليات الحسابية العشرية بشكل أصلي. ومع ذلك، فإن العمليات الحسابية العشرية تواجه مشكلة دقة الحساب التي لا مفر منها. لذلك، عند كتابة العقود الذكية، لا يُنصح باستخدام العمليات الحسابية العشرية (، خصوصًا عند التعامل مع النسب أو أسعار الفائدة التي تتعلق بقرارات اقتصادية/مالية مهمة ).
تتبع معظم لغات البرمجة السائدة لتمثيل الأعداد العشرية العائمة معيار IEEE 754، وليس لغة Rust استثناءً. فيما يلي وصف لنوع العائمة مزدوجة الدقة f64 في لغة Rust وشكل البيانات الثنائية المخزنة داخليًا في الكمبيوتر:
يتم استخدام التدوين العلمي برقم أساسي 2 للتعبير عن الأعداد العشرية. على سبيل المثال، يمكن استخدام عدد ثنائي محدود 0.1101 لتمثيل العدد العشري 0.8125، وطريقة التحويل المحددة هي كما يلي:
0.8125 * 2 = 1 .625 // 0.1 الحصول على الرقم الثنائي العشري الأول هو 1
0.625 * 2 = 1 .25 // 0.11 الحصول على الرقم الثنائي العشري الثاني هو 1
0.25 * 2 = 0 .5 // 0.110 الحصول على الرقم الثنائي العشري الثالث هو 0
0.5 * 2 = 1 .0 // 0.1101 الحصول على الرقم الثنائي العشري الرابع يكون 1
أي 0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
ومع ذلك، بالنسبة للعدد العشري الآخر 0.7، ستظهر المشكلة التالية خلال عملية تحويله إلى عدد عائم:
سيتم تمثيل الكسر العشري 0.7 كـ 0.101100110011001100.....( حلقة لانهائية )، ولا يمكن تمثيله بدقة باستخدام عدد محدود من الأرقام العشرية، ويوجد ظاهرة "التقريب ( Rounding )".
افترض أنه في شبكة NEAR العامة، تحتاج إلى توزيع 0.7 من رموز NEAR على عشرة مستخدمين، وسيتم حساب كمية رموز NEAR التي يحصل عليها كل مستخدم وحفظها في المتغير result_0.
#[test]
الجبهة precision_test_float() {
// لا يمكن تمثيل الأعداد الصحيحة بدقة باستخدام الأعداد العشرية
let amount: f64 = 0.7; // هذه المتغير amount يمثل 0.7 من توكن NEAR
let divisor: f64 = 10.0; // تعريف القاسم
let result_0 = a / b; // تنفيذ عملية القسمة على الأعداد العشرية
println!("قيمة a: {:.20}", a);
assert_eq!(result_0 ، 0.07 ، "") ؛
}
نتيجة تنفيذ حالة الاختبار هي كما يلي:
تشغيل 1 اختبار
قيمة a: 0.69999999999999995559
الخيط "tests::precision_test_float" انهار عند "فشل التأكيد: (left == right)
اليسار: 0.06999999999999999, اليمين: 0.07: ", src/lib.rs:185:9
من الواضح أنه في العمليات الحسابية العائمة المذكورة أعلاه، فإن قيمة amount لا تعبر بدقة عن 0.7، بل هي قيمة قريبة للغاية تبلغ 0.69999999999999995559. وعلاوة على ذلك، بالنسبة لعمليات القسمة الفردية مثل amount/divisor، فإن نتيجة العملية ستصبح غير دقيقة أيضًا 0.06999999999999999، وليست 0.07 المتوقعة. ومن هنا تظهر عدم اليقين في العمليات الحسابية العائمة.
لذلك، يجب علينا أن نفكر في استخدام أنواع أخرى من طرق التمثيل العددي في العقود الذكية، مثل الأعداد العشرية.
بناءً على الموقع الثابت للفاصلة العشرية، يوجد نوعان من الأعداد الثابتة: الأعداد الصحيحة الثابتة ( والأعداد العشرية الثابتة ).
يتم تثبيت الفاصلة العشرية بعد أقل رقم في العدد، ويطلق عليه اسم عدد صحيح ثابت النقطة.
في كتابة العقود الذكية الفعلية، غالبًا ما يتم استخدام كسر له مقام ثابت لتمثيل قيمة معينة، على سبيل المثال الكسر "x/N"، حيث "N" هو ثابت، و"x" يمكن أن يتغير.
إذا كانت قيمة "N" هي "1,000,000,000,000,000,000"، أي "10^18"، فيمكن تمثيل الأعداد العشرية كأعداد صحيحة، مثل هذا:
في بروتوكول NEAR، القيمة الشائعة لـ N هي "10^24"، أي أن 10^24 من yoctoNEAR تعادل 1 رمز NEAR.
استنادًا إلى ذلك، يمكننا تعديل اختبار الوحدة في هذا القسم ليتم الحساب بالطريقة التالية:
#(
الجبهة الوطنية precision_test_integer)[test] {
// أولاً، تعريف الثابت N، الذي يمثل الدقة.
دع N: u128 = 1_000_000_000_000_000_000_000_000_000 ؛ أي أن 1 NEAR = 10 ^ 24 yoctoNEAR محدد
// تهيئة المبلغ، في الواقع القيمة الممثلة هنا هي 700_000_000_000_000_000 / N = 0.7 NEAR;
المبلغ اليدخ: U128 = 700_000_000_000_000_000_000_000 ؛ يوكتو نير
// تهيئة المقام divisor
دع المقسوم: U128 = 10 ؛
// حساب الناتج:result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
// يمثل فعليًا 700_000_000_000_000_000_000_000 / N = 0.07 NEAR;
دع result_0 = الكمية / القاسم ؛
assert_eq!(result_0, 70_000_000_000_000_000_000_000_000, "");
}
يمكن الحصول على نتيجة الحسابات الدقيقة للقيم على النحو التالي: 0.7 NEAR / 10 = 0.07 NEAR
تشغيل 1 اختبار
اختبارات الاختبار: :p recision_test_integer ... موافق
نتيجة الاختبار: جيد. 1 ناجح؛ 0 فاشل؛ 0 متجاهل؛ 0 مقاس؛ 8 تم تصفيتهم؛ انتهى في 0.00ث.
!
2. مشكلة دقة حسابات الأعداد الصحيحة في Rust
من الوصف في الفقرة 1 أعلاه، يمكننا أن نجد أن استخدام العمليات الصحيحة يمكن أن يحل مشكلة فقدان الدقة في العمليات العائمة في بعض سيناريوهات العمليات.
لكن هذا لا يعني أن نتائج الحسابات الصحيحة دقيقة وموثوقة تمامًا. ستتناول هذه الفقرة بعض الأسباب التي تؤثر على دقة الحسابات الصحيحة.
( 2.1 ترتيب العمليات
قد يؤثر تغيير الترتيب بين الضرب والقسمة، اللذين لهما نفس أولوية العمليات الحسابية، بشكل مباشر على نتيجة الحساب، مما يؤدي إلى مشكلة دقة الحسابات الصحيحة.
جاري تشغيل 1 اختبار
الخيط "tests::precision_test_0" انهار عند "فشل التحقق: (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 حجم صغير جدا
#)
الجبهة precision_test_decimals###[test] {
دع أ: u128 = 10 ؛
دع ب: u128 = 3 ؛
دع C: U128 = 4 ؛
دع الرقم العشري: U128 = 100_0000 ؛
result_0 = (a / b) * ج
دع result_0 = أ
.checked_div(b)
.expect("ERR_DIV")
.checked_mul(c)
.expect( "ERR_MUL" ) ؛
result_1 = (a * عشري / b) * ج / عشري ؛
دع result_1 = أ
.checked_mul(decimal) // مولية عشرية
.expect("ERR_MUL")
.checked_div(b)
.expect("ERR_DIV")
.checked_mul(c)
.expect("ERR_MUL")
.checked_div(decimal) // div عشري
.expect("ERR_DIV");
println!("{}:{}", result_0, result_1);
assert_eq!(result_0 ، result_1 ، "") ؛
}
نتائج اختبار الوحدة المحددة هي كما يلي:
جاري اختبار واحد
12:13
الخيط "tests::precision_test_decimals" انهار عند "فشل التأكيد: (left == right)
اليسار: 12، اليمين: 13: ", src/lib.rs:214:9
يمكن ملاحظة أن نتائج العمليات result_0 و result_1 المتكافئة لا تتطابق، وأن result_1 = 13 أقرب بكثير إلى القيمة المحسوبة المتوقعة: 13.3333....
!
3. كيفية كتابة العقود الذكية Rust للتقييم العددي
من المهم ضمان الدقة الصحيحة في العقود الذكية. على الرغم من أن هناك أيضًا مشكلة فقدان دقة نتائج العمليات العددية في لغة Rust، إلا أننا يمكننا اتخاذ بعض التدابير الوقائية التالية لتحسين الدقة، وتحقيق نتائج مرضية.
( 3.1 تعديل ترتيب عمليات الحساب
اجعل ضرب الأعداد الصحيحة أولوية على قسمة الأعداد الصحيحة.
) 3.2 زيادة مرتبة العدد الصحيح
الأعداد الصحيحة تستخدم كميات أكبر، لإنشاء أعداد أكبر.
على سبيل المثال، بالنسبة لرمز NEAR، إذا تم تعريف N كما هو موصوف أعلاه على أنه 10، فهذا يعني: إذا كان من الضروري تمثيل قيمة NEAR البالغة 5.123، فإن القيمة العددية الصحيحة المستخدمة في العمليات الفعلية ستُعبر عنها كالتالي 5.123 * 10^10 = 51_230_000_000. ستستمر هذه القيمة في المشاركة في العمليات العددية اللاحقة، مما يمكن أن يزيد من دقة العمليات.
3.3 خسارة دقة العمليات التراكمية
بالنسبة لمشكلة دقة الحسابات الصحيحة التي لا يمكن تجنبها حقًا، يمكن لفريق المشروع أن يأخذ في اعتباره تسجيل الخسائر المتراكمة في دقة العمليات.
u128 لتوزيع الرموز على عدد USER_NUM من المستخدمين.
في هذه الحالة الاختبارية، يقوم النظام بتوزيع 10 رموز على 3 مستخدمين في كل مرة. ومع ذلك، بسبب مشكلة دقة العمليات الحسابية الصحيحة، عند حساب per_user_share في الجولة الأولى، كانت نتيجة العملية الحسابية الصحيحة هي 10 / 3 = 3، مما يعني أن المستخدمين الذين تم توزيعهم في الجولة الأولى سيحصلون في المتوسط على 3 رموز، بإجمالي 9 رموز تم توزيعها.
في هذه المرحلة، يمكن ملاحظة أن هناك 1 توكن متبقي لم يتم توزيعه على المستخدمين في النظام. لذلك، يمكن التفكير في الاحتفاظ بالتوكن المتبقي مؤقتًا في متغير النظام العالمي offset. وعند استدعاء النظام مرة أخرى لتوزيع التوكن على المستخدمين، سيتم استخراج هذه القيمة، وسيتم محاولة توزيعها مع مبلغ التوكن الموزع في هذه الجولة.
قد تحتوي هذه الصفحة على محتوى من جهات خارجية، يتم تقديمه لأغراض إعلامية فقط (وليس كإقرارات/ضمانات)، ولا ينبغي اعتباره موافقة على آرائه من قبل Gate، ولا بمثابة نصيحة مالية أو مهنية. انظر إلى إخلاء المسؤولية للحصول على التفاصيل.
تسجيلات الإعجاب 8
أعجبني
8
6
مشاركة
تعليق
0/400
WalletWhisperer
· منذ 2 س
من الرائع كيف يمكن أن تكون نقاط العائمة في Rust وعاء العسل الضعف التالي لدينا... أراقب عن كثب
شاهد النسخة الأصليةرد0
OnlyOnMainnet
· منذ 2 س
حساب الأعداد العشرية + داخل السلسلة ههه أخافني
شاهد النسخة الأصليةرد0
TopEscapeArtist
· منذ 2 س
يا أصدقائي، هذه المشكلة في الدقة دقيقة تمامًا كما لو كنت قد ضغطت على القمة.
شاهد النسخة الأصليةرد0
RamenDeFiSurvivor
· منذ 2 س
هربت هربت، هذه مشكلة الدقة مزعجة حقًا
شاهد النسخة الأصليةرد0
NFTArchaeologist
· منذ 2 س
مشكلة الدقة هي الأكثر فتكًا... إذا لم تكن حذرًا، ستخسر كل شيء.
حسابات القيم الدقيقة في العقود الذكية بلغة راست: الأعداد الصحيحة مقابل الأعداد العشرية
Rust العقود الذكية养成日记(7):数值精算
نظرة على الماضي:
1. مشكلة دقة العمليات العشرية
على عكس لغة البرمجة الشائعة للعقود الذكية Solidity، تدعم لغة Rust العمليات الحسابية العشرية بشكل أصلي. ومع ذلك، فإن العمليات الحسابية العشرية تواجه مشكلة دقة الحساب التي لا مفر منها. لذلك، عند كتابة العقود الذكية، لا يُنصح باستخدام العمليات الحسابية العشرية (، خصوصًا عند التعامل مع النسب أو أسعار الفائدة التي تتعلق بقرارات اقتصادية/مالية مهمة ).
تتبع معظم لغات البرمجة السائدة لتمثيل الأعداد العشرية العائمة معيار IEEE 754، وليس لغة Rust استثناءً. فيما يلي وصف لنوع العائمة مزدوجة الدقة f64 في لغة Rust وشكل البيانات الثنائية المخزنة داخليًا في الكمبيوتر:
يتم استخدام التدوين العلمي برقم أساسي 2 للتعبير عن الأعداد العشرية. على سبيل المثال، يمكن استخدام عدد ثنائي محدود 0.1101 لتمثيل العدد العشري 0.8125، وطريقة التحويل المحددة هي كما يلي:
ومع ذلك، بالنسبة للعدد العشري الآخر 0.7، ستظهر المشكلة التالية خلال عملية تحويله إلى عدد عائم:
سيتم تمثيل الكسر العشري 0.7 كـ 0.101100110011001100.....( حلقة لانهائية )، ولا يمكن تمثيله بدقة باستخدام عدد محدود من الأرقام العشرية، ويوجد ظاهرة "التقريب ( Rounding )".
افترض أنه في شبكة NEAR العامة، تحتاج إلى توزيع 0.7 من رموز NEAR على عشرة مستخدمين، وسيتم حساب كمية رموز NEAR التي يحصل عليها كل مستخدم وحفظها في المتغير result_0.
نتيجة تنفيذ حالة الاختبار هي كما يلي:
من الواضح أنه في العمليات الحسابية العائمة المذكورة أعلاه، فإن قيمة amount لا تعبر بدقة عن 0.7، بل هي قيمة قريبة للغاية تبلغ 0.69999999999999995559. وعلاوة على ذلك، بالنسبة لعمليات القسمة الفردية مثل amount/divisor، فإن نتيجة العملية ستصبح غير دقيقة أيضًا 0.06999999999999999، وليست 0.07 المتوقعة. ومن هنا تظهر عدم اليقين في العمليات الحسابية العائمة.
لذلك، يجب علينا أن نفكر في استخدام أنواع أخرى من طرق التمثيل العددي في العقود الذكية، مثل الأعداد العشرية.
في كتابة العقود الذكية الفعلية، غالبًا ما يتم استخدام كسر له مقام ثابت لتمثيل قيمة معينة، على سبيل المثال الكسر "x/N"، حيث "N" هو ثابت، و"x" يمكن أن يتغير.
إذا كانت قيمة "N" هي "1,000,000,000,000,000,000"، أي "10^18"، فيمكن تمثيل الأعداد العشرية كأعداد صحيحة، مثل هذا:
في بروتوكول NEAR، القيمة الشائعة لـ N هي "10^24"، أي أن 10^24 من yoctoNEAR تعادل 1 رمز NEAR.
استنادًا إلى ذلك، يمكننا تعديل اختبار الوحدة في هذا القسم ليتم الحساب بالطريقة التالية:
يمكن الحصول على نتيجة الحسابات الدقيقة للقيم على النحو التالي: 0.7 NEAR / 10 = 0.07 NEAR
!
2. مشكلة دقة حسابات الأعداد الصحيحة في Rust
من الوصف في الفقرة 1 أعلاه، يمكننا أن نجد أن استخدام العمليات الصحيحة يمكن أن يحل مشكلة فقدان الدقة في العمليات العائمة في بعض سيناريوهات العمليات.
لكن هذا لا يعني أن نتائج الحسابات الصحيحة دقيقة وموثوقة تمامًا. ستتناول هذه الفقرة بعض الأسباب التي تؤثر على دقة الحسابات الصحيحة.
( 2.1 ترتيب العمليات
قد يؤثر تغيير الترتيب بين الضرب والقسمة، اللذين لهما نفس أولوية العمليات الحسابية، بشكل مباشر على نتيجة الحساب، مما يؤدي إلى مشكلة دقة الحسابات الصحيحة.
على سبيل المثال، هناك العمليات التالية:
نتائج اختبار الوحدة كما يلي:
يمكننا أن نلاحظ أن result_0 = a * c / b و result_1 = (a / b)* c على الرغم من أن صيغتهما الحسابية متطابقة، إلا أن نتائج العمليات مختلفة.
تحليل الأسباب المحددة هو: بالنسبة لقسمة الأعداد الصحيحة، ستفقد الدقة التي تقل عن المقام. لذلك، أثناء حساب result_1، فإن العملية الأولى (a / b) ستفقد الدقة الحسابية أولاً، مما يجعلها 0؛ بينما في حساب result_0، سيتم حساب نتيجة a * c أولاً 20_0000، وستكون هذه النتيجة أكبر من المقام b، وبالتالي تجنب فقدان الدقة، مما يتيح الحصول على نتيجة حساب صحيحة.
( 2.2 حجم صغير جدا
نتائج اختبار الوحدة المحددة هي كما يلي:
يمكن ملاحظة أن نتائج العمليات result_0 و result_1 المتكافئة لا تتطابق، وأن result_1 = 13 أقرب بكثير إلى القيمة المحسوبة المتوقعة: 13.3333....
!
3. كيفية كتابة العقود الذكية Rust للتقييم العددي
من المهم ضمان الدقة الصحيحة في العقود الذكية. على الرغم من أن هناك أيضًا مشكلة فقدان دقة نتائج العمليات العددية في لغة Rust، إلا أننا يمكننا اتخاذ بعض التدابير الوقائية التالية لتحسين الدقة، وتحقيق نتائج مرضية.
( 3.1 تعديل ترتيب عمليات الحساب
) 3.2 زيادة مرتبة العدد الصحيح
على سبيل المثال، بالنسبة لرمز NEAR، إذا تم تعريف N كما هو موصوف أعلاه على أنه 10، فهذا يعني: إذا كان من الضروري تمثيل قيمة NEAR البالغة 5.123، فإن القيمة العددية الصحيحة المستخدمة في العمليات الفعلية ستُعبر عنها كالتالي 5.123 * 10^10 = 51_230_000_000. ستستمر هذه القيمة في المشاركة في العمليات العددية اللاحقة، مما يمكن أن يزيد من دقة العمليات.
3.3 خسارة دقة العمليات التراكمية
بالنسبة لمشكلة دقة الحسابات الصحيحة التي لا يمكن تجنبها حقًا، يمكن لفريق المشروع أن يأخذ في اعتباره تسجيل الخسائر المتراكمة في دقة العمليات.
u128 لتوزيع الرموز على عدد USER_NUM من المستخدمين.
في هذه الحالة الاختبارية، يقوم النظام بتوزيع 10 رموز على 3 مستخدمين في كل مرة. ومع ذلك، بسبب مشكلة دقة العمليات الحسابية الصحيحة، عند حساب per_user_share في الجولة الأولى، كانت نتيجة العملية الحسابية الصحيحة هي 10 / 3 = 3، مما يعني أن المستخدمين الذين تم توزيعهم في الجولة الأولى سيحصلون في المتوسط على 3 رموز، بإجمالي 9 رموز تم توزيعها.
في هذه المرحلة، يمكن ملاحظة أن هناك 1 توكن متبقي لم يتم توزيعه على المستخدمين في النظام. لذلك، يمكن التفكير في الاحتفاظ بالتوكن المتبقي مؤقتًا في متغير النظام العالمي offset. وعند استدعاء النظام مرة أخرى لتوزيع التوكن على المستخدمين، سيتم استخراج هذه القيمة، وسيتم محاولة توزيعها مع مبلغ التوكن الموزع في هذه الجولة.
التالي هو عملية توزيع الرموز المحاكاة:
الجولة 1 حصة لكل مستخدم 3 الإزاحة1 الجولة 2 حصة_لكل_مستخدم 3 الإزاحة 2 الجولة 3 حصة لكل مستخدم 4 الإزاحة 0 الجولة 4 حصة_كل_مستخدم 3 الإزاحة 1 الجولة 5 نصيب كل مستخدم 3