تناولت المشاركة السابقة التنفيذ الشامل من البداية إلى النهاية: عقد توكن بسيط، وإعادة بناء الحالة خارج السلسلة، وواجهة أمامية React — من `()mint` إلى MetaMask. تبدأ هذه المشاركة من حيث توقفت تلك: كيف تضمن جودة شيء مثل هذا؟
لست مهندس بلوكتشين (بعد)، ولكن أنماط ضمان الجودة تنتقل بشكل جيد عبر المجالات، واستعارة ما ينجح بالفعل في مكان آخر هو الطريقة التي أتعلم بها بشكل أسرع.
العقد يقوم بثلاثة أشياء فقط: `mint`، `transfer`، و `burn`، لكن حتى هذا يكفي لممارسة سلسلة أدوات ضمان الجودة الكاملة: التحليل الثابت، اختبار الطفرات، تحليل استهلاك الغاز، التحقق الرسمي.
الكود موجود في `egpivo/ethereum-account-state`.
هرم ضمان جودة البلوكتشين: من التحليل الثابت في القاعدة إلى التحقق الرسمي في القمةقبل إضافة أي شيء جديد، كان لدى المشروع بالفعل:
نجحت جميع الاختبارات. بدا التغطية جيدة. فلماذا نهتم بالمزيد؟
لأن "جميع الاختبارات تمر" لا يعني "تم اكتشاف جميع الأخطاء". تغطية 100٪ من السطور لا يزال بإمكانها تفويت خطأ حقيقي إذا لم يتحقق أي تأكيد من الشيء الصحيح.
Slither (Trail of Bits) يكتشف المشكلات غير المرئية للاختبارات: إعادة الدخول، القيم المرجعة غير المحققة، عدم تطابق الواجهة.
./scripts/run-qa.sh slither
النتيجة: اكتشاف متوسط واحد: `erc20-interface`: `()transfer` لا ترجع `bool`.
هذا متوقع. العقد ليس ERC20 كاملاً عمداً: إنه آلة حالة تعليمية. لكن الاكتشاف ليس أكاديمياً:
إذا قام شخص ما لاحقاً باستيراد هذا التوكن إلى بروتوكول يتوقع ERC20، فإن عدم تطابق الواجهة سيفشل بصمت. يشير Slither إليه الآن حتى يكون القرار واعياً.
./scripts/run-qa.sh coverage نتيجة التغطية.
دالة واحدة غير مغطاة: `()BalanceLib.gt`. سنعود إلى هذا.
مخرجات تغطية forge: نجح 24 اختبار، جدول تغطية Token.sol./scripts/run-qa.sh gas
تكاليف الغاز الأساسية للعمليات الثلاث:
الغاز من حيث العملياتفي التشغيلات اللاحقة، `forge snapshot — diff` يقارن مع الخط الأساسي. تراجع بنسبة 20٪ في الغاز في `()transfer` هو تكلفة حقيقية على كل مستخدم — اكتشافه قبل الدمج رخيص.
هنا أصبحت الأمور مثيرة للاهتمام. Gambit (Certora) يولد المتحولين: نسخ من `Token.sol` بأخطاء صغيرة متعمدة (`+=` إلى `-=`، `>=` إلى `>`، شروط منفية). يشغل خط الأنابيب مجموعة الاختبارات الكاملة ضد كل متحول. إذا نجا متحول (لا تزال جميع الاختبارات تمر)، فهذه فجوة اختبار ملموسة.
./scripts/run-qa.sh mutation
النتيجة: درجة طفرة 97.0٪ — 32 قتيل، 1 نجا من 33 متحول.
سجل مخرجات Gambit يعرض كل متحول وما تغير. بعض الأمثلة:
تم توليد المتحول #7: BinaryOpMutation — Token.sol:168
totalSupply = totalSupply.add(amountBalance) → totalSupply = totalSupply.sub(amountBalance)
قُتل بواسطة test_Mint_Success
تم توليد المتحول #19: RelationalOpMutation — Token.sol:196
if (!fromBalance.gte(amountBalance)) → if (fromBalance.gte(amountBalance))
قُتل بواسطة test_Transfer_Success
تم توليد المتحول #28: SwapArgumentsMutation — Token.sol:81
return Balance.unwrap(a) > Balance.unwrap(b) → return Balance.unwrap(b) > Balance.unwrap(a)
نجا ← لم يكتشفه أي اختبار
اختبار طفرة Gambit: 32 قتيل، 1 نجا، درجة طفرة 97.0٪
المتحول الناجي بدّل `a > b` إلى `b > a` في `()BalanceLib.gt`. لم يكتشفه أي اختبار لأن `()gt` هو كود ميت. لا يتم استدعاؤه أبداً في أي مكان في `Token.sol`.
أشارت التغطية إلى 91.67٪ من الدوال لكنها لم تستطع تفسير الفجوة. اختبار الطفرات فعل: `()gt` هو كود ميت، لا شيء يستدعيه، ولن يلاحظ أحد إذا كان خاطئاً.
الكود الميت أو غير المحمي في العقود الذكية له سابقة حقيقية.
لم تكن الدالة مقصودة لتكون قابلة للاستدعاء، لكن لم يختبر أحد هذا الافتراض. `()gt` الخاص بنا غير ضار بالمقارنة، لكن النمط هو نفسه: الكود الموجود لكن لم يتم تنفيذه أبداً هو كود لا أحد يراقبه.
Halmos (a16z) يستدل حول جميع المدخلات الممكنة بشكل رمزي. حيث تأخذ اختبارات fuzz عينات من قيم عشوائية وتأمل في الوصول إلى حالات حافة، يثبت Halmos الخصائص بشكل شامل.
./scripts/run-qa.sh halmos
النتيجة: نجح 9/9 اختبارات رمزية — تم إثبات جميع الخصائص لجميع المدخلات.
الخصائص التي تم التحقق منها:
الخصائص التي تم التحقق منهاملاحظة عملية واحدة: Halmos 0.3.3 لا يدعم `()vm.expectRevert`، لذا لم أتمكن من كتابة اختبارات الارتداد بطريقة Foundry العادية. الحل البديل هو نمط try/catch — إذا نجح الاستدعاء عندما يجب أن يرتد، `assert(false)` يفشل الإثبات:
function check_mint_reverts_on_zero_address(uint256 amount) public {
vm.assume(amount > 0);
try token.mint(address(0), amount) {
assert(false); // لا ينبغي الوصول إلى هنا
} catch {
// ارتداد متوقع - Halmos يثبت أن هذا المسار يتم اتخاذه دائماً
}
}
ليس الأجمل، لكنه يعمل — Halmos لا يزال يثبت الخاصية لجميع المدخلات. هذا هو نوع الأشياء التي تكتشفها فقط بتشغيل الأداة فعلياً.
للسياق حول سبب أهمية التحقق الرسمي:
كانت الثغرة في الكود، قابلة للمراجعة من قبل أي شخص، لكن لم تكتشفها أي أداة أو اختبار قبل النشر. المثبتون الرمزيون مثل Halmos موجودون على وجه التحديد لسد تلك الفجوة — إنهم لا يأخذون عينات؛ إنهم يستنفدون مساحة الإدخال.
مخرجات Halmos: نجح 9 اختبارات، 0 فشل، نتائج الاختبار الرمزيملف الاختبار هو `contracts/test/Token.halmos.t.sol`.
تحتوي بنية المشاركة الأولى على طبقة نطاق TypeScript تعكس آلة الحالة على السلسلة. تختبر هذه المرحلة ما إذا كانت الاثنتان تتفقان فعلاً.
أضفت اختبارات خاصية fast-check لطبقة نطاق TypeScript، تعكس ما يفعله fuzzer الخاص بـ Foundry لـ Solidity:
npm test - tests/unit/property.test.ts
النتيجة: نجح 9/9 اختبارات خاصية بعد إصلاح خطأ حقيقي.
الخصائص التي تم اختبارها:
وجد fast-check خطأ اتساق حقيقي عبر الطبقات في `()Token.ts` `transfer`. كان المثال المضاد المنكمش واضحاً على الفور:
فشلت الخاصية بعد 3 اختبارات
انكمشت مرتين
مثال مضاد: transfer(from=0xaaa…, to=0xaaa…, amount=1n)
→ from == to (تحويل ذاتي)
→ ()verifyInvariant أرجع false
التحويل الذاتي (`from == to`) كسر ثابت `sum(balances) == totalSupply`. تمت قراءة `toBalance` قبل تحديث `fromBalance`، لذلك عندما `from == to`، القيمة القديمة استبدلت الخصم:
// قبل (به خطأ)
const fromBalance = this.getBalance(from);
const toBalance = this.getBalance(to); // ← قديمة عندما from == to
this.accounts.set(from.getValue(), fromBalance.subtract(amount));
this.accounts.set(to.getValue(), toBalance.add(amount)); // ← يستبدل الطرح
الإصلاح: اقرأ `toBalance` بعد كتابة `fromBalance`، مطابقاً دلالات تخزين Solidity:
// بعد (تم إصلاحه)
const fromBalance = this.getBalance(from);
this.accounts.set(from.getValue(), fromBalance.subtract(amount));
const toBalance = this.getBalance(to); // ← الآن يقرأ القيمة المحدثة
this.accounts.set(to.getValue(), toBalance.add(amount));
العقد Solidity لم يتأثر: يعيد قراءة التخزين بعد كل كتابة. لكن مرآة TypeScript كان لها اعتماد ترتيب دقيق لم يغطيه أي اختبار وحدة موجود.
عدم تطابق عبر الطبقات على نطاق أكبر كان كارثياً.
خطأ التحويل الذاتي لدينا لم يكن ليخسر أي شخص أموالاً، لكن وضع الفشل هو نفسه هيكلياً: طبقتان من المفترض أن تتفقا، لا تتفقان.
تشغيل أدوات ضمان الجودة على مشروع موجود ليس مجرد "تثبيت وتشغيل". انكسرت بعض الأشياء قبل أن تعمل:
كل شيء يعمل من خلال نصين برمجيين:
./scripts/run-qa.sh slither gas # فقط التحليل الثابت + الغاز
./scripts/run-qa.sh mutation # فقط اختبار الطفرات
./scripts/run-qa.sh all # كل شيء
ليست كل فحص سريعاً. يعمل Slither والتغطية على كل التزام. اختبار الطفرات وHalmos أبطأ — أكثر ملاءمة للتشغيلات الأسبوعية أو قبل الإصدار.
خمس طبقات لضمان الجودة، كل منها يكتشف فئة مختلفة من المشاكل.
شرح الطبقاتأعطى Gambit وfast-check أكثر النتائج قابلية للتنفيذ في هذه الجولة.
تم توصيل فحوصات ضمان الجودة الآن في GitHub Actions كخط أنابيب من ست مراحل:
خط أنابيب CI: Build & Lint يتفرع إلى مراحل Test وCoverage وGas وSlither وAuditخط أنابيب GitHub Actions: Build & Lint يتحكم في جميع المراحل اللاحقة.
شرح المراحلتم نشر Ethereum Account State: QA Pipeline for a Minimal Token في الأصل في Coinmonks على Medium، حيث يواصل الناس المحادثة من خلال تسليط الضوء والرد على هذه القصة.


