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

গ্যাস ফি স্পনসর করা: কীভাবে আপনার ব্যবহারকারীদের জন্য ট্রানজ্যাকশন খরচ কভার করবেন

গ্যাসবিহীন
Solidity
eip-712
মেটা-ট্রানজ্যাকশন
মধ্যবর্তী
ওরি পোমেরান্টজ
27 ফেব্রুয়ারী, 2026
10 মিনিট পড়ার সময়

ভূমিকা

আমরা যদি চাই ইথেরিয়াম আরও এক বিলিয়ন মানুষকে (একটি নতুন ট্যাবে খোলে) পরিষেবা দিক, তবে আমাদের বাধাগুলো দূর করতে হবে এবং এটিকে যতটা সম্ভব সহজে ব্যবহারযোগ্য করে তুলতে হবে। এই বাধার একটি উৎস হলো গ্যাস ফি প্রদানের জন্য ETH-এর প্রয়োজনীয়তা।

আপনার যদি এমন একটি বিকেন্দ্রীকৃত অ্যাপ্লিকেশন (dapp) থাকে যা ব্যবহারকারীদের থেকে অর্থ উপার্জন করে, তবে ব্যবহারকারীদের আপনার সার্ভারের মাধ্যমে ট্রানজ্যাকশন জমা দিতে দেওয়া এবং ট্রানজ্যাকশন ফি নিজে প্রদান করা যৌক্তিক হতে পারে। যেহেতু ব্যবহারকারীরা এখনও তাদের ওয়ালেটে একটি EIP-712 অনুমোদন বার্তা (একটি নতুন ট্যাবে খোলে) স্বাক্ষর করে, তাই তারা ইথেরিয়ামের অখণ্ডতার নিশ্চয়তা বজায় রাখে। প্রাপ্যতা নির্ভর করে সেই সার্ভারের ওপর যা ট্রানজ্যাকশন রিলে করে, তাই এটি তুলনামূলকভাবে সীমিত। তবে, আপনি এমনভাবে সবকিছু সেট আপ করতে পারেন যাতে ব্যবহারকারীরা সরাসরি স্মার্ট কন্ট্রাক্ট অ্যাক্সেস করতে পারে (যদি তারা ETH পায়), এবং অন্যরা যদি ট্রানজ্যাকশন স্পনসর করতে চায় তবে তাদের নিজস্ব সার্ভার সেট আপ করতে দিতে পারেন।

এই টিউটোরিয়ালের কৌশলটি কেবল তখনই কাজ করে যখন আপনি স্মার্ট কন্ট্রাক্ট নিয়ন্ত্রণ করেন। অন্যান্য কৌশলও রয়েছে, যার মধ্যে অ্যাকাউন্ট বিমূর্তকরণ (একটি নতুন ট্যাবে খোলে) অন্তর্ভুক্ত, যা আপনাকে অন্যান্য স্মার্ট কন্ট্রাক্টে ট্রানজ্যাকশন স্পনসর করতে দেয়, যা আমি ভবিষ্যতের একটি টিউটোরিয়ালে কভার করার আশা করি।

দ্রষ্টব্য: এটি প্রোডাকশন-স্তরের কোড নয়। এটি উল্লেখযোগ্য আক্রমণের জন্য ঝুঁকিপূর্ণ এবং এতে প্রধান বৈশিষ্ট্যগুলোর অভাব রয়েছে। এই গাইডের দুর্বলতা বিভাগে আরও জানুন।

পূর্বশর্ত

এই টিউটোরিয়ালটি বোঝার জন্য আপনাকে আগে থেকেই নিম্নলিখিত বিষয়গুলোর সাথে পরিচিত হতে হবে:

নমুনা অ্যাপ্লিকেশন

এখানকার নমুনা অ্যাপ্লিকেশনটি Hardhat-এর Greeter কন্ট্রাক্টের একটি রূপ। আপনি এটি GitHub-এ (একটি নতুন ট্যাবে খোলে) দেখতে পারেন। স্মার্ট কন্ট্রাক্টটি ইতিমধ্যেই Sepolia (একটি নতুন ট্যাবে খোলে)-তে, 0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA (একটি নতুন ট্যাবে খোলে) ঠিকানায় ডিপ্লয় করা হয়েছে।

এটি কার্যকর দেখতে, এই ধাপগুলো অনুসরণ করুন।

  1. রিপোজিটরিটি ক্লোন করুন এবং প্রয়োজনীয় সফ্টওয়্যার ইনস্টল করুন।

    git clone https://github.com/qbzzt/260301-gasless.git
    cd 260301-gasless/server
    npm install
    
  2. .env সম্পাদনা করে PRIVATE_KEY-কে এমন একটি ওয়ালেটে সেট করুন যার Sepolia-তে ETH আছে। আপনার যদি Sepolia ETH-এর প্রয়োজন হয়, তবে একটি ফসেট ব্যবহার করুন। আদর্শভাবে, এই প্রাইভেট কী আপনার ব্রাউজার ওয়ালেটে থাকা প্রাইভেট কী থেকে আলাদা হওয়া উচিত।

  3. সার্ভার চালু করুন।

    npm run dev
    
  4. http://localhost:5173 (একটি নতুন ট্যাবে খোলে) URL-এ অ্যাপ্লিকেশনটি ব্রাউজ করুন।

  5. একটি ওয়ালেটের সাথে সংযোগ করতে Connect with Injected-এ ক্লিক করুন। ওয়ালেটে অনুমোদন করুন এবং প্রয়োজন হলে Sepolia-তে পরিবর্তনটি অনুমোদন করুন।

  6. একটি নতুন অভিবাদন (greeting) লিখুন এবং Update greeting via sponsor-এ ক্লিক করুন।

  7. বার্তাটিতে স্বাক্ষর করুন।

  8. প্রায় 12 সেকেন্ড অপেক্ষা করুন (Sepolia-তে ব্লক টাইম)। অপেক্ষা করার সময় আপনি ট্রানজ্যাকশনটি দেখতে সার্ভারের কনসোলে URL-টি দেখতে পারেন।

  9. দেখুন যে অভিবাদনটি পরিবর্তিত হয়েছে, এবং সর্বশেষ আপডেট করা ঠিকানার মানটি এখন আপনার ব্রাউজার ওয়ালেটের ঠিকানা।

এটি কীভাবে কাজ করে তা বোঝার জন্য, আমাদের দেখতে হবে কীভাবে ইউজার ইন্টারফেসে বার্তাটি তৈরি হয়, কীভাবে এটি সার্ভার দ্বারা রিলে করা হয় এবং কীভাবে স্মার্ট কন্ট্রাক্ট এটি প্রক্রিয়া করে।

ইউজার ইন্টারফেস

ইউজার ইন্টারফেসটি WAGMI (একটি নতুন ট্যাবে খোলে)-এর ওপর ভিত্তি করে তৈরি; আপনি এই টিউটোরিয়ালে এটি সম্পর্কে পড়তে পারেন।

আমরা কীভাবে বার্তাটিতে স্বাক্ষর করি তা এখানে দেওয়া হলো:

const signGreeting = useCallback(

React হুক useCallback (একটি নতুন ট্যাবে খোলে) কম্পোনেন্টটি পুনরায় আঁকা হলে একই ফাংশন পুনরায় ব্যবহার করে আমাদের পারফরম্যান্স উন্নত করতে দেয়।

    async (greeting) => {
        if (!account) throw new Error("Wallet not connected")

যদি কোনো অ্যাকাউন্ট না থাকে, তবে একটি ত্রুটি (error) দেখান। এটি কখনই হওয়া উচিত নয় কারণ যে UI বোতামটি signGreeting কল করার প্রক্রিয়া শুরু করে তা সেই ক্ষেত্রে নিষ্ক্রিয় থাকে। তবে, ভবিষ্যতের প্রোগ্রামাররা সেই সুরক্ষাটি সরিয়ে ফেলতে পারে, তাই এখানেও এই শর্তটি পরীক্ষা করা একটি ভালো ধারণা।

        const domain = {
            name: "Greeter",
            version: "1",
            chainId,
            verifyingContract: contractAddr,
        }

ডোমেইন বিভাজকের (domain separator) (একটি নতুন ট্যাবে খোলে) জন্য প্যারামিটার। এই মানটি ধ্রুবক, তাই একটি আরও ভালোভাবে অপ্টিমাইজ করা বাস্তবায়নে, আমরা ফাংশনটি প্রতিবার কল করার সময় এটি পুনরায় গণনা করার পরিবর্তে একবার গণনা করতে পারি।

  • name হলো একটি ব্যবহারকারী-পঠনযোগ্য নাম, যেমন সেই dapp-এর নাম যার জন্য আমরা স্বাক্ষর তৈরি করছি।
  • version হলো সংস্করণ। বিভিন্ন সংস্করণ সামঞ্জস্যপূর্ণ নয়।
  • chainId হলো সেই চেইন যা আমরা ব্যবহার করছি, যা WAGMI দ্বারা (একটি নতুন ট্যাবে খোলে) প্রদান করা হয়েছে।
  • verifyingContract হলো সেই কন্ট্রাক্ট ঠিকানা যা এই স্বাক্ষরটি যাচাই করবে। আমরা চাই না যে একই স্বাক্ষর একাধিক কন্ট্রাক্টে প্রযোজ্য হোক, যদি একাধিক Greeter কন্ট্রাক্ট থাকে এবং আমরা চাই যে সেগুলোতে আলাদা অভিবাদন থাকুক।

        const types = {
            GreetingRequest: [
                { name: "greeting", type: "string" },
            ],
        }

যে ডেটা টাইপে আমরা স্বাক্ষর করি। এখানে, আমাদের একটি মাত্র প্যারামিটার আছে, greeting, তবে বাস্তব জীবনের সিস্টেমে সাধারণত আরও বেশি থাকে।

        const message = { greeting }

আসল বার্তা যা আমরা স্বাক্ষর করে পাঠাতে চাই। greeting হলো ফিল্ডের নাম এবং সেই ভেরিয়েবলের নাম যা এটি পূরণ করে।

        const signature = await signTypedDataAsync({
            domain,
            types,
            primaryType: "GreetingRequest",
            message,
        })

প্রকৃতপক্ষে স্বাক্ষরটি পান। এই ফাংশনটি অ্যাসিনক্রোনাস কারণ ব্যবহারকারীরা ডেটা স্বাক্ষর করতে (কম্পিউটারের দৃষ্টিকোণ থেকে) দীর্ঘ সময় নেয়।

ফাংশনটি একটি একক হেক্সাডেসিমাল মান প্রদান করে। এখানে আমরা এটিকে ফিল্ডে ভাগ করি।

    [account, chainId, contractAddr, signTypedDataAsync],
)

যদি এই ভেরিয়েবলগুলোর কোনোটি পরিবর্তিত হয়, তবে ফাংশনটির একটি নতুন ইনস্ট্যান্স তৈরি করুন। account এবং chainId প্যারামিটারগুলো ব্যবহারকারী ওয়ালেটে পরিবর্তন করতে পারেন। contractAddr হলো চেইন আইডির একটি ফাংশন। signTypedDataAsync পরিবর্তন হওয়া উচিত নয়, তবে আমরা এটি একটি হুক (একটি নতুন ট্যাবে খোলে) থেকে ইমপোর্ট করি, তাই আমরা নিশ্চিত হতে পারি না, এবং এটি এখানে যোগ করা সবচেয়ে ভালো।

এখন যেহেতু নতুন অভিবাদনটি স্বাক্ষরিত হয়েছে, আমাদের এটি সার্ভারে পাঠাতে হবে।

  const sponsoredGreeting = async () => {
    try {

এই ফাংশনটি একটি স্বাক্ষর নেয় এবং এটি সার্ভারে পাঠায়।

      const signedMessage = await signGreeting(newGreeting)
      const response = await fetch("/server/sponsor", {

আমরা যে সার্ভার থেকে এসেছি তার /server/sponsor পাথে পাঠান।

        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(signedMessage),
      })

তথ্য JSON-এনকোড করে পাঠাতে POST ব্যবহার করুন।

      const data = await response.json()
      console.log("Server response:", data)
    } catch (err) {
      console.error("Error:", err)
    }
  }

রেসপন্স আউটপুট করুন। একটি প্রোডাকশন সিস্টেমে আমরা ব্যবহারকারীকেও রেসপন্সটি দেখাব।

সার্ভার

আমি আমার ফ্রন্ট-এন্ড হিসেবে Vite (একটি নতুন ট্যাবে খোলে) ব্যবহার করতে পছন্দ করি। এটি স্বয়ংক্রিয়ভাবে React লাইব্রেরিগুলো পরিবেশন করে এবং ফ্রন্ট-এন্ড কোড পরিবর্তিত হলে ব্রাউজার আপডেট করে। তবে, Vite-এ ব্যাকএন্ড টুলিং অন্তর্ভুক্ত নেই।

এর সমাধান রয়েছে index.js (একটি নতুন ট্যাবে খোলে)-এ।

প্রথমে আমরা সেই রিকোয়েস্টগুলোর জন্য একটি হ্যান্ডলার নিবন্ধন করি যা আমরা নিজেরাই পরিচালনা করি (/server/sponsor-এ POST)। তারপর আমরা অন্যান্য সমস্ত URL পরিচালনা করার জন্য একটি Vite সার্ভার তৈরি এবং ব্যবহার করি।

এটি কেবল একটি স্ট্যান্ডার্ড viem (একটি নতুন ট্যাবে খোলে) ব্লকচেইন কল।

স্মার্ট কন্ট্রাক্ট

অবশেষে, Greeter.sol (একটি নতুন ট্যাবে খোলে)-কে স্বাক্ষরটি যাচাই করতে হবে।

কনস্ট্রাক্টর ডোমেইন বিভাজক (একটি নতুন ট্যাবে খোলে) তৈরি করে, যা ওপরের ইউজার ইন্টারফেস কোডের মতোই। ব্লকচেইন এক্সিকিউশন অনেক বেশি ব্যয়বহুল, তাই আমরা এটি কেবল একবার গণনা করি।

    struct GreetingRequest {
        string greeting;
    }

এটি হলো সেই স্ট্রাকচার যা স্বাক্ষরিত হয়। এখানে আমাদের কেবল একটি ফিল্ড আছে।

    bytes32 private constant GREETING_TYPEHASH =
        keccak256("GreetingRequest(string greeting)");

এটি হলো স্ট্রাকচার আইডেন্টিফায়ার (একটি নতুন ট্যাবে খোলে)। এটি ইউজার ইন্টারফেসে প্রতিবার গণনা করা হয়।

    function sponsoredSetGreeting(
        GreetingRequest calldata req,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {

এই ফাংশনটি একটি স্বাক্ষরিত রিকোয়েস্ট গ্রহণ করে এবং অভিবাদন আপডেট করে।

EIP 712 (একটি নতুন ট্যাবে খোলে) অনুসারে ডাইজেস্ট তৈরি করুন।

        // স্বাক্ষরকারী পুনরুদ্ধার করুন
        address signer = ecrecover(digest, v, r, s);
        require(signer != address(0), "Invalid signature");

স্বাক্ষরকারীর ঠিকানা পেতে ecrecover (একটি নতুন ট্যাবে খোলে) ব্যবহার করুন। মনে রাখবেন যে একটি ভুল স্বাক্ষরও একটি বৈধ ঠিকানা প্রদান করতে পারে, তবে সেটি কেবল একটি র্যান্ডম ঠিকানা হবে।

        // অভিবাদন এমনভাবে প্রয়োগ করুন যেন স্বাক্ষরকারী এটি কল করেছে
        greeting = req.greeting;
        emit SetGreeting(signer, req.greeting);
    }

অভিবাদন আপডেট করুন।

দুর্বলতা

এটি প্রোডাকশন-স্তরের কোড নয়। এটি উল্লেখযোগ্য আক্রমণের জন্য ঝুঁকিপূর্ণ এবং এতে প্রধান বৈশিষ্ট্যগুলোর অভাব রয়েছে। এখানে কিছু দুর্বলতা এবং সেগুলো কীভাবে সমাধান করা যায় তা দেওয়া হলো।

এই আক্রমণগুলোর কয়েকটি দেখতে, Attacks শিরোনামের নিচের বোতামগুলোতে ক্লিক করুন এবং দেখুন কী ঘটে। Invalid signature বোতামের জন্য, ট্রানজ্যাকশন রেসপন্স দেখতে সার্ভার কনসোল চেক করুন।

সার্ভারে ডিনায়াল অফ সার্ভিস

সবচেয়ে সহজ আক্রমণ হলো সার্ভারে একটি ডিনায়াল-অফ-সার্ভিস (একটি নতুন ট্যাবে খোলে) আক্রমণ। সার্ভার ইন্টারনেটের যেকোনো জায়গা থেকে রিকোয়েস্ট গ্রহণ করে এবং সেই রিকোয়েস্টগুলোর ওপর ভিত্তি করে ট্রানজ্যাকশন পাঠায়। একজন আক্রমণকারীকে একগুচ্ছ স্বাক্ষর (বৈধ বা অবৈধ) ইস্যু করা থেকে বাধা দেওয়ার মতো কিছুই নেই। প্রতিটি স্বাক্ষর একটি ট্রানজ্যাকশন ঘটাবে। শেষ পর্যন্ত সার্ভারের গ্যাস ফি দেওয়ার জন্য ETH শেষ হয়ে যাবে।

এই সমস্যার একটি সমাধান হলো প্রতি ব্লকে একটি ট্রানজ্যাকশনের হার সীমিত করা। যদি উদ্দেশ্য হয় এক্সটার্নালি ওনড অ্যাকাউন্টগুলোকে অভিবাদন দেখানো, তবে ব্লকের মাঝখানে অভিবাদনটি কী তা কোনো ব্যাপার নয়।

আরেকটি সমাধান হলো ঠিকানাগুলোর ট্র্যাক রাখা এবং কেবল বৈধ গ্রাহকদের থেকে স্বাক্ষর অনুমোদন করা।

ভুল অভিবাদন স্বাক্ষর

আপনি যখন Signature for wrong greeting-এ ক্লিক করেন, তখন আপনি একটি নির্দিষ্ট ঠিকানা (0xaA92c5d426430D4769c9E878C1333BDe3d689b3e) এবং অভিবাদনের (Hello) জন্য একটি বৈধ স্বাক্ষর জমা দেন। কিন্তু এটি একটি ভিন্ন অভিবাদনের সাথে জমা দেয়। এটি ecrecover-কে বিভ্রান্ত করে, যা অভিবাদন পরিবর্তন করে কিন্তু ভুল ঠিকানা পায়।

এই সমস্যা সমাধানের জন্য, স্বাক্ষরিত স্ট্রাকচারে (একটি নতুন ট্যাবে খোলে) ঠিকানাটি যোগ করুন। এইভাবে, ecrecover র্যান্ডম ঠিকানাটি স্বাক্ষরের ঠিকানার সাথে মিলবে না এবং স্মার্ট কন্ট্রাক্ট বার্তাটি প্রত্যাখ্যান করবে।

রিপ্লে আক্রমণ

আপনি যখন Replay attack-এ ক্লিক করেন, তখন আপনি একই "আমি 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, এবং আমি চাই অভিবাদনটি Hello হোক" স্বাক্ষর জমা দেন, কিন্তু সঠিক অভিবাদনের সাথে। ফলস্বরূপ, স্মার্ট কন্ট্রাক্ট বিশ্বাস করে যে ঠিকানাটি (যা আপনার নয়) অভিবাদনটিকে আবার Hello-এ পরিবর্তন করেছে। এটি করার তথ্য ট্রানজ্যাকশন তথ্যে (একটি নতুন ট্যাবে খোলে) সর্বজনীনভাবে উপলব্ধ।

যদি এটি একটি সমস্যা হয়, তবে একটি সমাধান হলো একটি নন্স (একটি নতুন ট্যাবে খোলে) যোগ করা। ঠিকানা এবং সংখ্যাগুলোর মধ্যে একটি ম্যাপিং (একটি নতুন ট্যাবে খোলে) রাখুন এবং স্বাক্ষরে একটি নন্স ফিল্ড যোগ করুন। যদি নন্স ফিল্ডটি ঠিকানার ম্যাপিংয়ের সাথে মিলে যায়, তবে স্বাক্ষরটি গ্রহণ করুন এবং পরের বারের জন্য ম্যাপিংটি বৃদ্ধি করুন। যদি এটি না মেলে, তবে ট্রানজ্যাকশনটি প্রত্যাখ্যান করুন।

আরেকটি সমাধান হলো স্বাক্ষরিত ডেটাতে একটি টাইমস্ট্যাম্প যোগ করা এবং সেই টাইমস্ট্যাম্পের পর কেবল কয়েক সেকেন্ডের জন্য স্বাক্ষরটিকে বৈধ হিসেবে গ্রহণ করা। এটি সহজ এবং সস্তা, তবে আমরা সময়সীমার মধ্যে রিপ্লে আক্রমণের ঝুঁকি নিই, এবং সময়সীমা অতিক্রম করলে বৈধ ট্রানজ্যাকশনগুলো ব্যর্থ হওয়ার ঝুঁকি থাকে।

অন্যান্য অনুপস্থিত বৈশিষ্ট্য

একটি প্রোডাকশন সেটিংয়ে আমরা আরও কিছু অতিরিক্ত বৈশিষ্ট্য যোগ করব।

অন্যান্য সার্ভার থেকে অ্যাক্সেস

বর্তমানে, আমরা যেকোনো ঠিকানাকে একটি sponsorSetGreeting জমা দেওয়ার অনুমতি দিই। বিকেন্দ্রীকরণের স্বার্থে এটি ঠিক আমরা যা চাই তা হতে পারে। অথবা হয়তো আমরা নিশ্চিত করতে চাই যে স্পনসর করা ট্রানজ্যাকশনগুলো আমাদের সার্ভারের মাধ্যমে যায়, সেক্ষেত্রে আমরা স্মার্ট কন্ট্রাক্টে msg.sender চেক করব।

যাই হোক না কেন, এটি একটি সচেতন ডিজাইনের সিদ্ধান্ত হওয়া উচিত, কেবল বিষয়টি নিয়ে চিন্তা না করার ফলাফল নয়।

ত্রুটি পরিচালনা (Error handling)

একজন ব্যবহারকারী একটি অভিবাদন জমা দেন। হয়তো এটি পরবর্তী ব্লকে আপডেট হবে। হয়তো হবে না। ত্রুটিগুলো অদৃশ্য। একটি প্রোডাকশন সিস্টেমে, ব্যবহারকারীর এই ক্ষেত্রগুলোর মধ্যে পার্থক্য করতে সক্ষম হওয়া উচিত:

  • নতুন অভিবাদনটি এখনও জমা দেওয়া হয়নি
  • নতুন অভিবাদনটি জমা দেওয়া হয়েছে, এবং এটি প্রক্রিয়াধীন রয়েছে
  • নতুন অভিবাদনটি প্রত্যাখ্যান করা হয়েছে

উপসংহার

এই পর্যায়ে, কিছুটা কেন্দ্রীকরণের বিনিময়ে আপনি আপনার বিকেন্দ্রীকৃত অ্যাপ্লিকেশন (dapp) ব্যবহারকারীদের জন্য একটি গ্যাসহীন অভিজ্ঞতা তৈরি করতে সক্ষম হবেন।

তবে, এটি কেবল সেই স্মার্ট কন্ট্রাক্টগুলোর সাথেই কাজ করে যা ERC-712 সমর্থন করে। উদাহরণস্বরূপ, একটি ERC-20 টোকেন হস্তান্তর করার জন্য, কেবল একটি বার্তার পরিবর্তে মালিকের দ্বারা ট্রানজ্যাকশনটি স্বাক্ষরিত হওয়া প্রয়োজন। এর সবচেয়ে সহজ সমাধান হলো সম্পদগুলো EOA ঠিকানার মালিকানায় না রেখে একটি পৃথক কন্ট্রাক্টের (যা অ্যাকাউন্ট বিমূর্তকরণ-এর একটি সহজ রূপ) মালিকানায় রাখা। আপনি পরবর্তী টিউটোরিয়ালে এটি সম্পর্কে আরও পড়তে পারেন।

আমার আরও কাজের জন্য এখানে দেখুন (একটি নতুন ট্যাবে খোলে)