মূল কন্টেন্টে যান

স্মার্ট কন্ট্রাক্ট টেস্ট করতে কীভাবে Echidna ব্যবহার করবেন

Solidity
স্মার্ট কন্ট্রাক্ট
নিরাপত্তা
টেস্টিং
ফাজিং
অ্যাডভান্সড
ট্রেইলঅফবিটস
10 এপ্রিল, 2020
13 মিনিট পড়া

ইনস্টলেশন

Echidna ডকার (docker) এর মাধ্যমে বা প্রি-কম্পাইল করা বাইনারি ব্যবহার করে ইনস্টল করা যেতে পারে।

ডকারের মাধ্যমে Echidna

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox

শেষ কমান্ডটি eth-security-toolbox-কে এমন একটি ডকারে রান করে যার আপনার বর্তমান ডিরেক্টরিতে অ্যাক্সেস রয়েছে। আপনি আপনার হোস্ট থেকে ফাইলগুলো পরিবর্তন করতে পারেন এবং ডকার থেকে ফাইলগুলোর ওপর টুলগুলো রান করতে পারেন।

ডকারের ভেতরে রান করুন:

solc-select 0.5.11
cd /home/training

বাইনারি

https://github.com/crytic/echidna/releases/tag/v1.4.0.0 (opens in a new tab)

প্রপার্টি-ভিত্তিক ফাজিং (fuzzing) পরিচিতি

Echidna হলো একটি প্রপার্টি-ভিত্তিক ফাজার, যা আমরা আমাদের আগের ব্লগপোস্টগুলোতে বর্ণনা করেছি (1 (opens in a new tab), 2 (opens in a new tab), 3 (opens in a new tab))।

ফাজিং

ফাজিং (opens in a new tab) হলো নিরাপত্তা কমিউনিটিতে একটি সুপরিচিত কৌশল। প্রোগ্রামে বাগ (bug) খুঁজে বের করার জন্য কমবেশি র‍্যান্ডম ইনপুট তৈরি করা এর কাজ। প্রথাগত সফটওয়্যারের জন্য ফাজারগুলো (যেমন AFL (opens in a new tab) বা LibFuzzer (opens in a new tab)) বাগ খুঁজে বের করার কার্যকর টুল হিসেবে পরিচিত।

সম্পূর্ণ র‍্যান্ডম ইনপুট তৈরির বাইরেও, ভালো ইনপুট তৈরি করার জন্য অনেক কৌশল এবং পদ্ধতি রয়েছে, যার মধ্যে অন্তর্ভুক্ত:

  • প্রতিটি এক্সিকিউশন থেকে ফিডব্যাক নেওয়া এবং এটি ব্যবহার করে জেনারেশন গাইড করা। উদাহরণস্বরূপ, যদি একটি নতুন তৈরি করা ইনপুট নতুন কোনো পাথ (path) আবিষ্কার করতে সাহায্য করে, তবে এর কাছাকাছি নতুন ইনপুট তৈরি করা যৌক্তিক হতে পারে।
  • কাঠামোগত সীমাবদ্ধতা মেনে ইনপুট তৈরি করা। উদাহরণস্বরূপ, যদি আপনার ইনপুটে চেকসাম (checksum) সহ একটি হেডার থাকে, তবে ফাজারকে চেকসাম যাচাই করার মতো ইনপুট তৈরি করতে দেওয়া যৌক্তিক হবে।
  • নতুন ইনপুট তৈরি করতে পরিচিত ইনপুট ব্যবহার করা: যদি আপনার কাছে বৈধ ইনপুটের একটি বড় ডেটাসেট থাকে, তবে আপনার ফাজার শূন্য থেকে শুরু করার পরিবর্তে সেগুলো থেকে নতুন ইনপুট তৈরি করতে পারে। এগুলোকে সাধারণত সীড (seeds) বলা হয়।

প্রপার্টি-ভিত্তিক ফাজিং

Echidna ফাজারের একটি নির্দিষ্ট পরিবারের অন্তর্গত: প্রপার্টি-ভিত্তিক ফাজিং যা QuickCheck (opens in a new tab) দ্বারা ব্যাপকভাবে অনুপ্রাণিত। ক্লাসিক ফাজারের বিপরীতে যা ক্র্যাশ খুঁজে বের করার চেষ্টা করে, Echidna ব্যবহারকারী-নির্ধারিত ইনভ্যারিয়েন্টগুলো (invariants) ভাঙার চেষ্টা করবে।

স্মার্ট কন্ট্রাক্ট-এ, ইনভ্যারিয়েন্টগুলো হলো Solidity ফাংশন, যা কন্ট্রাক্ট পৌঁছাতে পারে এমন যেকোনো ভুল বা অবৈধ স্টেট (state) উপস্থাপন করতে পারে, যার মধ্যে রয়েছে:

  • ভুল অ্যাক্সেস কন্ট্রোল: আক্রমণকারী কন্ট্রাক্টের মালিক হয়ে গেছে।
  • ভুল স্টেট মেশিন: কন্ট্রাক্ট পজ (pause) থাকা অবস্থায় টোকেন ট্রান্সফার করা যেতে পারে।
  • ভুল পাটিগণিত: ব্যবহারকারী তার ব্যালেন্স আন্ডারফ্লো (underflow) করতে পারে এবং আনলিমিটেড ফ্রি টোকেন পেতে পারে।

Echidna দিয়ে একটি প্রপার্টি টেস্ট করা

আমরা দেখব কীভাবে Echidna দিয়ে একটি স্মার্ট কন্ট্রাক্ট টেস্ট করতে হয়। লক্ষ্য হলো নিচের স্মার্ট কন্ট্রাক্টটি token.sol (opens in a new tab):

আমরা ধরে নেব যে এই টোকেনের নিচের প্রপার্টিগুলো থাকতে হবে:

  • যেকোনো ব্যক্তির কাছে সর্বোচ্চ 1000 টোকেন থাকতে পারে
  • টোকেনটি ট্রান্সফার করা যাবে না (এটি কোনো ERC-20 টোকেন নয়)

একটি প্রপার্টি লেখা

Echidna প্রপার্টিগুলো হলো Solidity ফাংশন। একটি প্রপার্টিতে অবশ্যই:

  • কোনো আর্গুমেন্ট থাকা যাবে না
  • সফল হলে true রিটার্ন করতে হবে
  • এর নাম echidna দিয়ে শুরু হতে হবে

Echidna যা করবে:

  • প্রপার্টি টেস্ট করার জন্য স্বয়ংক্রিয়ভাবে আরবিট্রারি লেনদেন তৈরি করবে।
  • কোনো লেনদেন যদি প্রপার্টিকে false রিটার্ন করায় বা এরর (error) থ্রো করে, তবে তা রিপোর্ট করবে।
  • প্রপার্টি কল করার সময় সাইড-ইফেক্ট বাতিল করবে (অর্থাৎ, যদি প্রপার্টি কোনো স্টেট ভেরিয়েবল পরিবর্তন করে, তবে টেস্টের পরে তা বাতিল করা হয়)

নিচের প্রপার্টিটি চেক করে যে কলারের কাছে 1000-এর বেশি টোকেন নেই:

function echidna_balance_under_1000() public view returns(bool){
      return balances[msg.sender] <= 1000;
}

আপনার কন্ট্রাক্টকে প্রপার্টিগুলো থেকে আলাদা করতে ইনহেরিটেন্স (inheritance) ব্যবহার করুন:

contract TestToken is Token{
      function echidna_balance_under_1000() public view returns(bool){
            return balances[msg.sender] <= 1000;
      }
  }

token.sol (opens in a new tab) প্রপার্টিটি ইমপ্লিমেন্ট করে এবং টোকেন থেকে ইনহেরিট করে।

একটি কন্ট্রাক্ট ইনিশিয়েট করা

Echidna-এর জন্য আর্গুমেন্ট ছাড়া একটি কনস্ট্রাক্টর প্রয়োজন। যদি আপনার কন্ট্রাক্টে নির্দিষ্ট কোনো ইনিশিয়ালাইজেশনের প্রয়োজন হয়, তবে আপনাকে তা কনস্ট্রাক্টরে করতে হবে।

Echidna-তে কিছু নির্দিষ্ট এডড্রেস রয়েছে:

  • 0x00a329c0648769A73afAc7F9381E08FB43dBEA72 যা কনস্ট্রাক্টরকে কল করে।
  • 0x10000, 0x20000, এবং 0x00a329C0648769a73afAC7F9381e08fb43DBEA70 যা র‍্যান্ডমভাবে অন্যান্য ফাংশনগুলোকে কল করে।

আমাদের বর্তমান উদাহরণে কোনো নির্দিষ্ট ইনিশিয়ালাইজেশনের প্রয়োজন নেই, যার ফলে আমাদের কনস্ট্রাক্টরটি ফাঁকা।

Echidna রান করা

Echidna চালু করা হয় এভাবে:

echidna-test contract.sol

যদি contract.sol-এ একাধিক কন্ট্রাক্ট থাকে, তবে আপনি টার্গেট নির্দিষ্ট করতে পারেন:

echidna-test contract.sol --contract MyContract

সারসংক্ষেপ: একটি প্রপার্টি টেস্ট করা

নিচে আমাদের উদাহরণে echidna রান করার সারসংক্ষেপ দেওয়া হলো:

contract TestToken is Token{
    constructor() public {}
        function echidna_balance_under_1000() public view returns(bool){
          return balances[msg.sender] <= 1000;
        }
  }

Echidna দেখতে পেয়েছে যে backdoor কল করা হলে প্রপার্টিটি লঙ্ঘিত হয়।

ফাজিং ক্যাম্পেইনের সময় কল করার জন্য ফাংশন ফিল্টার করা

আমরা দেখব কীভাবে ফাজ (fuzz) করার জন্য ফাংশনগুলো ফিল্টার করতে হয়। লক্ষ্য হলো নিচের স্মার্ট কন্ট্রাক্টটি:

এই ছোট উদাহরণটি Echidna-কে একটি স্টেট ভেরিয়েবল পরিবর্তন করার জন্য লেনদেনের একটি নির্দিষ্ট সিকোয়েন্স খুঁজে বের করতে বাধ্য করে। এটি একটি ফাজারের জন্য কঠিন (এর জন্য Manticore (opens in a new tab)-এর মতো একটি সিম্বলিক এক্সিকিউশন টুল ব্যবহার করার পরামর্শ দেওয়া হয়)। এটি যাচাই করার জন্য আমরা Echidna রান করতে পারি:

echidna-test multi.sol
...
echidna_state4: passed! 🎉
Seed: -3684648582249875403

ফাংশন ফিল্টার করা

এই কন্ট্রাক্টটি টেস্ট করার জন্য সঠিক সিকোয়েন্স খুঁজে পেতে Echidna-র সমস্যা হয় কারণ দুটি রিসেট ফাংশন (reset1 এবং reset2) সমস্ত স্টেট ভেরিয়েবলকে false-এ সেট করে দেবে। যাইহোক, আমরা রিসেট ফাংশনটিকে ব্ল্যাকলিস্ট করতে বা শুধুমাত্র f, g, h এবং i ফাংশনগুলোকে হোয়াইটলিস্ট করতে একটি বিশেষ Echidna ফিচার ব্যবহার করতে পারি।

ফাংশন ব্ল্যাকলিস্ট করতে, আমরা এই কনফিগারেশন ফাইলটি ব্যবহার করতে পারি:

filterBlacklist: true
filterFunctions: ["reset1", "reset2"]

ফাংশন ফিল্টার করার আরেকটি পদ্ধতি হলো হোয়াইটলিস্ট করা ফাংশনগুলোর তালিকা করা। এটি করার জন্য, আমরা এই কনফিগারেশন ফাইলটি ব্যবহার করতে পারি:

filterBlacklist: false
filterFunctions: ["f", "g", "h", "i"]
  • filterBlacklist ডিফল্টভাবে true থাকে।
  • ফিল্টারিং শুধুমাত্র নাম দিয়ে করা হবে (প্যারামিটার ছাড়া)। যদি আপনার f() এবং f(uint256) থাকে, তবে "f" ফিল্টারটি উভয় ফাংশনের সাথেই মিলবে।

Echidna রান করা

একটি কনফিগারেশন ফাইল blacklist.yaml দিয়ে Echidna রান করতে:

echidna-test multi.sol --config blacklist.yaml
...
echidna_state4: failed!💥
  Call sequence:
    f(12)
    g(8)
    h(42)
    i()

Echidna প্রায় সাথে সাথেই প্রপার্টিটিকে ভুল প্রমাণ করার জন্য লেনদেনের সিকোয়েন্স খুঁজে পাবে।

সারসংক্ষেপ: ফাংশন ফিল্টার করা

Echidna ফাজিং ক্যাম্পেইনের সময় কল করার জন্য ফাংশনগুলোকে ব্ল্যাকলিস্ট বা হোয়াইটলিস্ট করতে পারে, যা ব্যবহার করে:

filterBlacklist: true
filterFunctions: ["f1", "f2", "f3"]
echidna-test contract.sol --config config.yaml
...

Echidna filterBlacklist বুলিয়ানের মানের ওপর ভিত্তি করে f1, f2 এবং f3 ব্ল্যাকলিস্ট করে অথবা শুধুমাত্র এগুলোকে কল করে একটি ফাজিং ক্যাম্পেইন শুরু করে।

Echidna দিয়ে Solidity-এর assert কীভাবে টেস্ট করবেন

এই সংক্ষিপ্ত টিউটোরিয়ালে, আমরা দেখাব কীভাবে কন্ট্রাক্টে অ্যাসারশন (assertion) চেকিং টেস্ট করতে Echidna ব্যবহার করতে হয়। ধরুন আমাদের কাছে এরকম একটি কন্ট্রাক্ট আছে:

একটি অ্যাসারশন লেখা

আমরা নিশ্চিত করতে চাই যে এর পার্থক্য রিটার্ন করার পরে tmp যেন counter-এর চেয়ে কম বা সমান হয়। আমরা একটি Echidna প্রপার্টি লিখতে পারতাম, কিন্তু আমাদের tmp মানটি কোথাও স্টোর করতে হবে। এর পরিবর্তে, আমরা এরকম একটি অ্যাসারশন ব্যবহার করতে পারি:

Echidna রান করা

অ্যাসারশন ফেইলিওর টেস্টিং চালু করতে, একটি Echidna কনফিগারেশন ফাইল (opens in a new tab) config.yaml তৈরি করুন:

checkAsserts: true

যখন আমরা Echidna-তে এই কন্ট্রাক্টটি রান করি, তখন আমরা প্রত্যাশিত ফলাফল পাই:

যেমনটি আপনি দেখতে পাচ্ছেন, Echidna inc ফাংশনে কিছু অ্যাসারশন ফেইলিওর রিপোর্ট করে। প্রতি ফাংশনে একাধিক অ্যাসারশন যোগ করা সম্ভব, তবে কোন অ্যাসারশনটি ব্যর্থ হয়েছে তা Echidna বলতে পারে না।

কখন এবং কীভাবে অ্যাসারশন ব্যবহার করবেন

অ্যাসারশনগুলো এক্সপ্লিসিট প্রপার্টির বিকল্প হিসেবে ব্যবহার করা যেতে পারে, বিশেষ করে যদি চেক করার শর্তগুলো সরাসরি কোনো অপারেশন f-এর সঠিক ব্যবহারের সাথে সম্পর্কিত হয়। কিছু কোডের পরে অ্যাসারশন যোগ করলে তা নিশ্চিত করবে যে এটি এক্সিকিউট হওয়ার পরপরই চেক করা হবে:

function f(..) public {
    // কিছু জটিল কোড
    ...
    assert (condition);
    ...
}

বিপরীতভাবে, একটি এক্সপ্লিসিট echidna প্রপার্টি ব্যবহার করলে তা র‍্যান্ডমভাবে লেনদেন এক্সিকিউট করবে এবং এটি ঠিক কখন চেক করা হবে তা নিশ্চিত করার কোনো সহজ উপায় নেই। তবে এই ওয়ার্কঅ্যারাউন্ডটি করা এখনও সম্ভব:

function echidna_assert_after_f() public returns (bool) {
    f(..);
    return(condition);
}

যাইহোক, কিছু সমস্যা রয়েছে:

  • যদি f-কে internal বা external হিসেবে ডিক্লেয়ার করা হয় তবে এটি ব্যর্থ হয়।
  • f কল করার জন্য কোন আর্গুমেন্টগুলো ব্যবহার করা উচিত তা অস্পষ্ট।
  • যদি f রিভার্ট (revert) করে, তবে প্রপার্টিটি ব্যর্থ হবে।

সাধারণভাবে, আমরা অ্যাসারশন কীভাবে ব্যবহার করতে হয় সে সম্পর্কে জন রেগেহরের সুপারিশ (opens in a new tab) অনুসরণ করার পরামর্শ দিই:

  • অ্যাসারশন চেকিংয়ের সময় কোনো সাইড ইফেক্ট জোর করে প্রয়োগ করবেন না। উদাহরণস্বরূপ: assert(ChangeStateAndReturn() == 1)
  • সুস্পষ্ট স্টেটমেন্ট অ্যাসার্ট করবেন না। উদাহরণস্বরূপ assert(var >= 0) যেখানে var-কে uint হিসেবে ডিক্লেয়ার করা হয়েছে।

পরিশেষে, অনুগ্রহ করে assert-এর পরিবর্তে require ব্যবহার করবেন না, কারণ Echidna এটি শনাক্ত করতে পারবে না (তবে কন্ট্রাক্টটি যেভাবেই হোক রিভার্ট করবে)।

সারসংক্ষেপ: অ্যাসারশন চেকিং

নিচে আমাদের উদাহরণে echidna রান করার সারসংক্ষেপ দেওয়া হলো:

Echidna দেখতে পেয়েছে যে inc-এর অ্যাসারশনটি ব্যর্থ হতে পারে যদি এই ফাংশনটি বড় আর্গুমেন্ট দিয়ে একাধিকবার কল করা হয়।

একটি Echidna কর্পাস (corpus) সংগ্রহ এবং পরিবর্তন করা

আমরা দেখব কীভাবে Echidna দিয়ে লেনদেনের একটি কর্পাস সংগ্রহ এবং ব্যবহার করতে হয়। লক্ষ্য হলো নিচের স্মার্ট কন্ট্রাক্টটি magic.sol (opens in a new tab):

এই ছোট উদাহরণটি Echidna-কে একটি স্টেট ভেরিয়েবল পরিবর্তন করার জন্য নির্দিষ্ট মান খুঁজে বের করতে বাধ্য করে। এটি একটি ফাজারের জন্য কঠিন (এর জন্য Manticore (opens in a new tab)-এর মতো একটি সিম্বলিক এক্সিকিউশন টুল ব্যবহার করার পরামর্শ দেওয়া হয়)। এটি যাচাই করার জন্য আমরা Echidna রান করতে পারি:

echidna-test magic.sol
...

echidna_magic_values: passed! 🎉

Seed: 2221503356319272685

যাইহোক, এই ফাজিং ক্যাম্পেইন রান করার সময় আমরা এখনও কর্পাস সংগ্রহ করতে Echidna ব্যবহার করতে পারি।

একটি কর্পাস সংগ্রহ করা

কর্পাস সংগ্রহ চালু করতে, একটি কর্পাস ডিরেক্টরি তৈরি করুন:

mkdir corpus-magic

এবং একটি Echidna কনফিগারেশন ফাইল (opens in a new tab) config.yaml:

coverage: true
corpusDir: "corpus-magic"

এখন আমরা আমাদের টুলটি রান করতে পারি এবং সংগৃহীত কর্পাস চেক করতে পারি:

echidna-test magic.sol --config config.yaml

Echidna এখনও সঠিক ম্যাজিক মানগুলো খুঁজে পাচ্ছে না, তবে আমরা এর সংগৃহীত কর্পাসটি দেখতে পারি। উদাহরণস্বরূপ, এই ফাইলগুলোর মধ্যে একটি ছিল:

স্পষ্টতই, এই ইনপুটটি আমাদের প্রপার্টিতে ফেইলিওর ট্রিগার করবে না। যাইহোক, পরবর্তী ধাপে, আমরা দেখব কীভাবে এর জন্য এটিকে পরিবর্তন করতে হয়।

একটি কর্পাস সীডিং (seeding) করা

magic ফাংশনটি নিয়ে কাজ করার জন্য Echidna-র কিছু সাহায্য প্রয়োজন। আমরা এর জন্য উপযুক্ত প্যারামিটার ব্যবহার করতে ইনপুটটি কপি এবং পরিবর্তন করতে যাচ্ছি:

cp corpus/2712688662897926208.txt corpus/new.txt

আমরা magic(42,129,333,0) কল করার জন্য new.txt পরিবর্তন করব। এখন, আমরা Echidna পুনরায় রান করতে পারি:

এবার, এটি দেখতে পেয়েছে যে প্রপার্টিটি সাথে সাথেই লঙ্ঘিত হয়েছে।

উচ্চ গ্যাস (gas) খরচ সহ লেনদেন খুঁজে বের করা

আমরা দেখব কীভাবে Echidna দিয়ে উচ্চ গ্যাস খরচ সহ লেনদেন খুঁজে বের করতে হয়। লক্ষ্য হলো নিচের স্মার্ট কন্ট্রাক্টটি:

এখানে expensive-এর একটি বড় গ্যাস খরচ থাকতে পারে।

বর্তমানে, Echidna-র টেস্ট করার জন্য সবসময় একটি প্রপার্টি প্রয়োজন হয়: এখানে echidna_test সবসময় true রিটার্ন করে। এটি যাচাই করার জন্য আমরা Echidna রান করতে পারি:

echidna-test gas.sol
...
echidna_test: passed! 🎉

Seed: 2320549945714142710

গ্যাস খরচ পরিমাপ করা

Echidna দিয়ে গ্যাস খরচ পরিমাপ চালু করতে, একটি কনফিগারেশন ফাইল config.yaml তৈরি করুন:

estimateGas: true

এই উদাহরণে, ফলাফলগুলো সহজে বোঝার জন্য আমরা লেনদেনের সিকোয়েন্সের আকারও কমিয়ে দেব:

seqLen: 2
estimateGas: true

Echidna রান করা

একবার কনফিগারেশন ফাইল তৈরি হয়ে গেলে, আমরা এভাবে Echidna রান করতে পারি:

  • দেখানো গ্যাস হলো HEVM (opens in a new tab) দ্বারা প্রদত্ত একটি অনুমান।

গ্যাস-হ্রাসকারী কলগুলো ফিল্টার করা

ওপরের ফাজিং ক্যাম্পেইনের সময় কল করার জন্য ফাংশন ফিল্টার করা টিউটোরিয়ালটি দেখায় কীভাবে আপনার টেস্টিং থেকে কিছু ফাংশন সরিয়ে ফেলতে হয়।
একটি সঠিক গ্যাস অনুমান পাওয়ার জন্য এটি অত্যন্ত গুরুত্বপূর্ণ হতে পারে। নিচের উদাহরণটি বিবেচনা করুন:

যদি Echidna সমস্ত ফাংশন কল করতে পারে, তবে এটি সহজে উচ্চ গ্যাস খরচ সহ লেনদেন খুঁজে পাবে না:

এর কারণ হলো খরচটি addrs-এর আকারের ওপর নির্ভর করে এবং র‍্যান্ডম কলগুলো অ্যারেকে (array) প্রায় ফাঁকা রাখার প্রবণতা দেখায়। যাইহোক, pop এবং clear ব্ল্যাকলিস্ট করা আমাদের অনেক ভালো ফলাফল দেয়:

filterBlacklist: true
filterFunctions: ["pop", "clear"]
echidna-test pushpop.sol --config config.yaml
...
push used a maximum of 40839 gas
...
check used a maximum of 1484472 gas

সারসংক্ষেপ: উচ্চ গ্যাস খরচ সহ লেনদেন খুঁজে বের করা

Echidna estimateGas কনফিগারেশন অপশন ব্যবহার করে উচ্চ গ্যাস খরচ সহ লেনদেন খুঁজে পেতে পারে:

estimateGas: true
echidna-test contract.sol --config config.yaml
...

ফাজিং ক্যাম্পেইন শেষ হওয়ার পরে, Echidna প্রতিটি ফাংশনের জন্য সর্বোচ্চ গ্যাস খরচ সহ একটি সিকোয়েন্স রিপোর্ট করবে।

পেজ সর্বশেষ আপডেট করা হয়েছে: 3 মার্চ, 2026

এই টিউটোরিয়ালটি কি সহায়ক ছিল?