पिछली पोस्ट में एंड-टू-एंड कार्यान्वयन के बारे में बताया गया था: एक न्यूनतम टोकन कॉन्ट्रैक्ट, ऑफ-चेन स्थिति पुनर्निर्माण, और एक React फ्रंटएंड — `mint()` से लेकर MetaMask तक। यह पोस्ट वहीं से शुरू होती है जहां वह छूटी थी: आप इस तरह की चीज़ का QA कैसे करते हैं?
मैं एक ब्लॉकचेन इंजीनियर नहीं हूं (अभी तक), लेकिन QA पैटर्न डोमेन में अच्छी तरह से काम करते हैं, और जो पहले से कहीं और काम करता है उसे उधार लेना मेरे लिए सबसे तेज़ सीखने का तरीका है।
कॉन्ट्रैक्ट केवल तीन काम करता है: `mint`, `transfer`, और `burn`, लेकिन यह पूर्ण QA टूलचेन का अभ्यास करने के लिए पर्याप्त है: स्टैटिक एनालिसिस, म्यूटेशन टेस्टिंग, गैस प्रोफाइलिंग, फॉर्मल वेरिफिकेशन।
कोड `egpivo/ethereum-account-state` में है।
ब्लॉकचेन QA पिरामिड: आधार पर स्टैटिक एनालिसिस से लेकर शीर्ष पर फॉर्मल वेरिफिकेशन तककुछ नया जोड़ने से पहले, प्रोजेक्ट में पहले से ही था:
सभी टेस्ट पास हुए। कवरेज ठीक लग रही थी। तो फिर और अधिक की परेशानी क्यों?
क्योंकि "सभी टेस्ट पास" का मतलब यह नहीं है कि "सभी बग पकड़े गए।" 100% लाइन कवरेज अभी भी एक वास्तविक बग को मिस कर सकती है अगर कोई एसर्शन सही चीज़ की जांच नहीं करता है।
Slither(Trail of Bits) उन समस्याओं को पकड़ता है जो टेस्ट के लिए अदृश्य हैं: रीएंट्रेंसी, अनचेक्ड रिटर्न वैल्यू, इंटरफ़ेस मिसमैच।
./scripts/run-qa.sh slither
परिणाम: 1 मध्यम खोज: `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` बेसलाइन के साथ तुलना करता है। `transfer()` में 20% गैस रिग्रेशन हर यूज़र के लिए एक वास्तविक लागत है — इसे मर्ज से पहले पकड़ना सस्ता है।
यहाँ चीजें दिलचस्प हो गईं। Gambit(Certora) म्यूटेंट: `Token.sol` की कॉपियाँ छोटे जानबूझकर बग के साथ जनरेट करता है (`+=` से `-=`, `>=` से `>`, शर्तें नेगेट की गईं)। पाइपलाइन प्रत्येक म्यूटेंट के खिलाफ पूर्ण टेस्ट सूट चलाती है। अगर कोई म्यूटेंट जीवित रहता है (सभी टेस्ट अभी भी पास होते हैं), तो वह एक ठोस टेस्ट गैप है।
./scripts/run-qa.sh mutation
परिणाम: 97.0% म्यूटेशन स्कोर — 33 म्यूटेंट में से 32 मारे गए, 1 जीवित रहा।
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%
जीवित रहने वाले म्यूटेंट ने `BalanceLib.gt()` में `a > b` को `b > a` से स्वैप कर दिया। किसी टेस्ट ने इसे नहीं पकड़ा क्योंकि `gt()` डेड कोड है। यह `Token.sol` में कहीं भी कॉल नहीं किया जाता है।
कवरेज ने 91.67% फंक्शंस को फ्लैग किया लेकिन गैप को समझा नहीं सका। म्यूटेशन टेस्टिंग ने किया: `gt()` डेड कोड है, कुछ भी इसे कॉल नहीं करता है, और कोई भी नोटिस नहीं करेगा अगर यह गलत था।
स्मार्ट कॉन्ट्रैक्ट्स में डेड या अनप्रोटेक्टेड कोड का वास्तविक उदाहरण है।
फंक्शन कॉल करने योग्य नहीं था, लेकिन किसी ने भी उस धारणा का परीक्षण नहीं किया। हमारा `gt()` तुलनात्मक रूप से हानिरहित है, लेकिन पैटर्न समान है: कोड जो मौजूद है लेकिन कभी एक्सरसाइज नहीं किया जाता है वह कोड है जिसे कोई नहीं देख रहा है।
Halmos(a16z) सभी संभावित इनपुट के बारे में प्रतीकात्मक रूप से तर्क करता है। जहाँ फज़ टेस्ट रैंडम वैल्यू सैंपल करते हैं और एज केस हिट करने की उम्मीद करते हैं, 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 डोमेन लेयर है जो ऑन-चेन स्टेट मशीन को मिरर करती है। यह चरण परीक्षण करता है कि क्या दोनों वास्तव में सहमत हैं।
मैंने TypeScript डोमेन लेयर के लिए fast-check प्रॉपर्टी टेस्ट जोड़े, जो Foundry का फज़र Solidity के लिए करता है उसे मिरर करते हुए:
npm test - tests/unit/property.test.ts
परिणाम: एक वास्तविक बग को ठीक करने के बाद 9/9 प्रॉपर्टी टेस्ट पास।
परीक्षित प्रॉपर्टीज:
fast-check ने `Token.ts` `transfer()` में एक वास्तविक क्रॉस-लेयर स्थिरता बग पाया। सिकुड़ा हुआ काउंटरएग्जाम्पल तुरंत स्पष्ट था:
3 टेस्ट के बाद प्रॉपर्टी विफल
2 बार सिकुड़ा
काउंटरएग्जाम्पल: 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)); // ← सबट्रैक्शन को ओवरराइट करता है
ठीक करें: `fromBalance` लिखने के बाद `toBalance` पढ़ें, 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 मिरर में एक सूक्ष्म ऑर्डरिंग निर्भरता थी जिसे कोई मौजूदा यूनिट टेस्ट कवर नहीं करता था।
बड़े पैमाने पर क्रॉस-लेयर मिसमैच विनाशकारी रहे हैं।
हमारा सेल्फ-ट्रांसफर बग किसी का भी पैसा नहीं खोता, लेकिन विफलता मोड संरचनात्मक रूप से समान है: दो लेयर जो सहमत होने वाली थीं, नहीं हैं।
मौजूदा प्रोजेक्ट पर QA टूल चलाना कभी भी केवल "इंस्टॉल और रन" नहीं है। काम करने से पहले कुछ चीज़ें टूटीं:
सब कुछ दो स्क्रिप्ट के माध्यम से चलता है:
./scripts/run-qa.sh slither gas # बस स्टैटिक एनालिसिस + गैस
./scripts/run-qa.sh mutation # बस म्यूटेशन टेस्टिंग
./scripts/run-qa.sh all # सब कुछ
हर चेक तेज़ नहीं है। Slither और कवरेज हर कमिट पर चलते हैं। म्यूटेशन टेस्टिंग और Halmos धीमे हैं — साप्ताहिक या प्री-रिलीज़ रन के लिए बेहतर अनुकूल।
पाँच QA लेयर, प्रत्येक समस्या के एक अलग वर्ग को पकड़ती है।
लेयर स्पष्टीकरणGambit और fast-check ने इस राउंड में सबसे अधिक कार्रवाई योग्य परिणाम दिए।
QA चेक अब GitHub Actions में एक छह-चरण पाइपलाइन के रूप में वायर्ड हैं:
CI पाइपलाइन: Build & Lint Test, Coverage, Gas, Slither, और Audit चरणों में फैलता हैGitHub Actions पाइपलाइन: Build & Lint सभी डाउनस्ट्रीम चरणों को गेट करता है।
चरण स्पष्टीकरणEthereum Account State: QA Pipeline for a Minimal Token मूल रूप से Medium पर Coinmonks में प्रकाशित हुआ था, जहाँ लोग इस कहानी को हाइलाइट करके और प्रतिक्रिया देकर बातचीत जारी रख रहे हैं।

