পূর্ববর্তী পোস্টে একটি সম্পূর্ণ বাস্তবায়ন তুলে ধরা হয়েছিল: একটি ন্যূনতম টোকেন কন্ট্রাক্ট, অফ-চেইন স্থিতি পুনর্গঠন, এবং একটি React ফ্রন্টএন্ড — `mint()` থেকে MetaMask পর্যন্ত সম্পূর্ণ পথ। এই পোস্টটি সেখান থেকে এগিয়ে যায়: এই ধরনের কিছু কীভাবে QA করবেন?
আমি (এখনও) একজন ব্লকচেইন ইঞ্জিনিয়ার নই, কিন্তু QA প্যাটার্নগুলি বিভিন্ন ডোমেইন জুড়ে ভালভাবে স্থানান্তরিত হয়, এবং যা অন্যত্র ইতিমধ্যে কাজ করে তা ধার করা হল আমার দ্রুততম শেখার উপায়।
কন্ট্রাক্টটি শুধুমাত্র তিনটি কাজ করে: `mint`, `transfer`, এবং `burn`, কিন্তু এটিই সম্পূর্ণ QA টুলচেইন অনুশীলনের জন্য যথেষ্ট: স্ট্যাটিক বিশ্লেষণ, মিউটেশন টেস্টিং, গ্যাস প্রোফাইলিং, ফর্মাল ভেরিফিকেশন।
কোডটি `egpivo/ethereum-account-state`-এ রয়েছে।
ব্লকচেইন QA পিরামিড: ভিত্তিতে স্ট্যাটিক বিশ্লেষণ থেকে শীর্ষে ফর্মাল ভেরিফিকেশননতুন কিছু যোগ করার আগে, প্রকল্পে ইতিমধ্যে ছিল:
সমস্ত টেস্ট পাস হয়েছে। কভারেজ ভালো লাগছিল। তাহলে আরও কেন বিরক্ত করবেন?
কারণ "সমস্ত টেস্ট পাস" মানে "সমস্ত বাগ ধরা পড়েছে" নয়। ১০০% লাইন কভারেজ এখনও একটি প্রকৃত বাগ মিস করতে পারে যদি কোনো অ্যাসার্শন সঠিক জিনিস চেক না করে।
Slither(Trail of Bits) এমন সমস্যাগুলি ধরে যা টেস্টে অদৃশ্য: রিএন্ট্র্যান্সি, আনচেকড রিটার্ন ভ্যালু, ইন্টারফেস মিসম্যাচ।
./scripts/run-qa.sh slither
ফলাফল: ১টি মাঝারি খুঁজে পাওয়া: `erc20-interface`: `transfer()` `bool` রিটার্ন করে না।
এটি প্রত্যাশিত। কন্ট্রাক্টটি ইচ্ছাকৃতভাবে সম্পূর্ণ ERC20 নয়: এটি একটি শিক্ষামূলক স্টেট মেশিন। কিন্তু খুঁজে পাওয়াটি একাডেমিক নয়:
যদি কেউ পরে এই টোকেনটি ERC20 প্রত্যাশা করে এমন প্রোটোকলে আমদানি করে, ইন্টারফেস মিসম্যাচ নীরবে ব্যর্থ হবে। Slither এখনই এটি ফ্ল্যাগ করে যাতে সিদ্ধান্তটি সচেতন হয়।
./scripts/run-qa.sh coverageকভারেজ ফলাফল।
একটি আনকভার্ড ফাংশন: `BalanceLib.gt()`। আমরা এটিতে ফিরে আসব।
forge কভারেজ আউটপুট: ২৪টি টেস্ট পাস হয়েছে, Token.sol কভারেজ টেবিল./scripts/run-qa.sh gas
তিনটি অপারেশনের বেসলাইন গ্যাস খরচ:
অপারেশনের ভিত্তিতে গ্যাসপরবর্তী রানে, `forge snapshot — diff` বেসলাইনের সাথে তুলনা করে। `transfer()`-এ ২০% গ্যাস রিগ্রেশন প্রতিটি ব্যবহারকারীর জন্য প্রকৃত খরচ — মার্জের আগে এটি ধরা সস্তা।
এখানেই বিষয়গুলি আকর্ষণীয় হয়ে উঠেছিল। Gambit(Certora) মিউট্যান্ট তৈরি করে: `Token.sol`-এর কপি ছোট ইচ্ছাকৃত বাগ সহ (`+=` থেকে `-=`, `>=` থেকে `>`, শর্ত নেগেট করা)। পাইপলাইন প্রতিটি মিউট্যান্টের বিরুদ্ধে সম্পূর্ণ টেস্ট স্যুট চালায়। যদি একটি মিউট্যান্ট বেঁচে থাকে (সমস্ত টেস্ট এখনও পাস হয়), তা একটি সুনির্দিষ্ট টেস্ট ফাঁক।
./scripts/run-qa.sh mutation
ফলাফল: ৯৭.০% মিউটেশন স্কোর — ৩৩টি মিউট্যান্টের মধ্যে ৩২টি নিহত, ১টি বেঁচে গেছে।
Gambit-এর আউটপুট লগ প্রতিটি মিউট্যান্ট এবং এটি কী পরিবর্তন করেছে তা দেখায়। কয়েকটি উদাহরণ:
Generated mutant #7: BinaryOpMutation — Token.sol:168
totalSupply = totalSupply.add(amountBalance) → totalSupply = totalSupply.sub(amountBalance)
KILLED by test_Mint_Success
Generated mutant #19: RelationalOpMutation — Token.sol:196
if (!fromBalance.gte(amountBalance)) → if (fromBalance.gte(amountBalance))
KILLED by test_Transfer_Success
Generated mutant #28: SwapArgumentsMutation — Token.sol:81
return Balance.unwrap(a) > Balance.unwrap(b) → return Balance.unwrap(b) > Balance.unwrap(a)
SURVIVED ← কোনো টেস্ট এটি ধরেনিGambit মিউটেশন টেস্টিং: ৩২টি নিহত, ১টি বেঁচে গেছে, মিউটেশন স্কোর ৯৭.০%
বেঁচে যাওয়া মিউট্যান্ট `BalanceLib.gt()`-এ `a > b` থেকে `b > a` স্ব্যাপ করেছে। কোনো টেস্ট এটি ধরেনি কারণ `gt()` হল ডেড কোড। এটি `Token.sol`-এর কোথাও কল করা হয় না।
কভারেজ ৯১.৬৭% ফাংশন ফ্ল্যাগ করেছে কিন্তু ফাঁক ব্যাখ্যা করতে পারেনি। মিউটেশন টেস্টিং করেছে: `gt()` হল ডেড কোড, কিছুই এটি কল করে না, এবং কেউ লক্ষ্য করবে না যদি এটি ভুল হয়।
স্মার্ট কন্ট্রাক্টে মৃত বা অরক্ষিত কোডের প্রকৃত নজির রয়েছে।
ফাংশনটি কলযোগ্য হওয়ার উদ্দেশ্য ছিল না, কিন্তু কেউ সেই অনুমান টেস্ট করেনি। আমাদের `gt()` তুলনায় ক্ষতিকর নয়, কিন্তু প্যাটার্নটি একই: যে কোড বিদ্যমান কিন্তু কখনও ব্যবহার করা হয় না তা এমন কোড যা কেউ দেখছে না।
Halmos(a16z) সমস্ত সম্ভাব্য ইনপুট সম্পর্কে প্রতীকীভাবে যুক্তি করে। যেখানে ফাজ টেস্ট র্যান্ডম মান নমুনা করে এবং এজ কেস হিট করার আশা করে, Halmos সম্পত্তি সম্পূর্ণভাবে প্রমাণ করে।
./scripts/run-qa.sh halmos
ফলাফল: ৯/৯ প্রতীকী টেস্ট পাস — সমস্ত ইনপুটের জন্য সমস্ত সম্পত্তি প্রমাণিত।
যাচাইকৃত সম্পত্তি:
যাচাইকৃত সম্পত্তিএকটি ব্যবহারিক নোট: 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 আউটপুট: ৯টি টেস্ট পাস হয়েছে, ০ ব্যর্থ, প্রতীকী টেস্ট ফলাফলটেস্ট ফাইল হল `contracts/test/Token.halmos.t.sol`।
প্রথম পোস্টের আর্কিটেকচারে একটি TypeScript ডোমেইন লেয়ার রয়েছে যা অন-চেইন স্টেট মেশিনকে প্রতিফলিত করে। এই ফেজ টেস্ট করে যে দুটি আসলে একমত কিনা।
আমি TypeScript ডোমেইন লেয়ারের জন্য fast-check প্রপার্টি টেস্ট যোগ করেছি, যা Foundry-এর ফাজার Solidity-এর জন্য যা করে তা প্রতিফলিত করে:
npm test - tests/unit/property.test.ts
ফলাফল: প্রকৃত বাগ ঠিক করার পরে ৯/৯ প্রপার্টি টেস্ট পাস।
টেস্ট করা সম্পত্তি:
fast-check `Token.ts` `transfer()`-এ একটি প্রকৃত ক্রস-লেয়ার সামঞ্জস্যতা বাগ খুঁজে পেয়েছে। সঙ্কুচিত কাউন্টারএক্সাম্পল অবিলম্বে স্পষ্ট ছিল:
Property failed after 3 tests
Shrunk 2 time(s)
Counterexample: transfer(from=0xaaa…, to=0xaaa…, amount=1n)
→ from == to (self-transfer)
→ verifyInvariant() returned 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 মূলত Coinmonks-এ Medium-এ প্রকাশিত হয়েছিল, যেখানে লোকেরা এই গল্পটি হাইলাইট এবং প্রতিক্রিয়া জানিয়ে কথোপকথন চালিয়ে যাচ্ছে।


