गैस शुल्क प्रायोजित करना: अपने उपयोगकर्ताओं के लिए लेन-देन की लागत को कैसे कवर करें
परिचय
यदि हम चाहते हैं कि इथेरियम एक अरब और लोगों (opens in a new tab) की सेवा करे, तो हमें बाधाओं को दूर करने और इसे उपयोग करने में यथासंभव आसान बनाने की आवश्यकता है। इस बाधा का एक स्रोत गैस शुल्क का भुगतान करने के लिए ETH की आवश्यकता है।
यदि आपके पास एक विकेंद्रीकृत एप्लिकेशन (dapp) है जो उपयोगकर्ताओं से पैसा कमाता है, तो उपयोगकर्ताओं को आपके सर्वर के माध्यम से लेन-देन सबमिट करने देना और लेन-देन शुल्क का भुगतान स्वयं करना समझदारी हो सकती है। क्योंकि उपयोगकर्ता अभी भी अपने वॉलेट में एक EIP-712 प्राधिकरण संदेश (opens in a new tab) पर हस्ताक्षर करते हैं, वे इथेरियम की अखंडता की गारंटी बनाए रखते हैं। उपलब्धता उस सर्वर पर निर्भर करती है जो लेन-देन को रिले करता है, इसलिए यह अधिक सीमित है। हालाँकि, आप चीजों को इस तरह सेट कर सकते हैं कि उपयोगकर्ता सीधे स्मार्ट अनुबंध तक भी पहुँच सकें (यदि उन्हें ETH मिलता है), और यदि अन्य लोग लेन-देन को प्रायोजित करना चाहते हैं तो उन्हें अपने स्वयं के सर्वर स्थापित करने दें।
इस ट्यूटोरियल की तकनीक केवल तभी काम करती है जब आप स्मार्ट अनुबंध को नियंत्रित करते हैं। अन्य तकनीकें भी हैं, जिनमें खाता अमूर्तन (opens in a new tab) शामिल है जो आपको अन्य स्मार्ट अनुबंधों के लिए लेन-देन प्रायोजित करने देती हैं, जिन्हें मैं भविष्य के ट्यूटोरियल में कवर करने की उम्मीद करता हूँ।
नोट: यह उत्पादन-स्तर (production-level) का कोड नहीं है। यह महत्वपूर्ण हमलों के प्रति संवेदनशील है और इसमें प्रमुख विशेषताओं का अभाव है। इस गाइड के भेद्यता (vulnerabilities) अनुभाग में और जानें।
पूर्वापेक्षाएँ
इस ट्यूटोरियल को समझने के लिए आपको पहले से ही निम्नलिखित से परिचित होना चाहिए:
- Solidity
- JavaScript
- React और WAGMI। यदि आप इन यूजर इंटरफेस टूल्स से परिचित नहीं हैं, तो हमारे पास उसके लिए एक ट्यूटोरियल है।
नमूना एप्लिकेशन
यहाँ नमूना एप्लिकेशन Hardhat के Greeter अनुबंध का एक प्रकार है। आप इसे GitHub पर (opens in a new tab) देख सकते हैं। स्मार्ट अनुबंध पहले से ही Sepolia (opens in a new tab) पर, 0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA (opens in a new tab) पते पर डिप्लॉय किया गया है।
इसे काम करते हुए देखने के लिए, इन चरणों का पालन करें।
-
रिपॉजिटरी को क्लोन करें और आवश्यक सॉफ़्टवेयर इंस्टॉल करें।
1git clone https://github.com/qbzzt/260301-gasless.git2cd 260301-gasless/server3npm install -
PRIVATE_KEYको ऐसे वॉलेट पर सेट करने के लिए.envको संपादित करें जिसमें Sepolia पर ETH हो। यदि आपको Sepolia ETH की आवश्यकता है, तो फॉसेट का उपयोग करें। आदर्श रूप से, यह निजी कुंजी आपके ब्राउज़र वॉलेट में मौजूद कुंजी से अलग होनी चाहिए। -
सर्वर प्रारंभ करें।
1npm run dev -
URL
http://localhost:5173(opens in a new tab) पर एप्लिकेशन ब्राउज़ करें। -
वॉलेट से कनेक्ट करने के लिए Connect with Injected पर क्लिक करें। वॉलेट में स्वीकृति दें, और यदि आवश्यक हो तो Sepolia में बदलाव को स्वीकृति दें।
-
एक नया अभिवादन (greeting) लिखें और Update greeting via sponsor पर क्लिक करें।
-
संदेश पर हस्ताक्षर करें।
-
लगभग 12 सेकंड (Sepolia पर ब्लॉक समय) प्रतीक्षा करें। प्रतीक्षा करते समय आप लेन-देन देखने के लिए सर्वर के कंसोल में URL देख सकते हैं।
-
देखें कि अभिवादन बदल गया है, और अंतिम अपडेट किए गए पते का मान अब आपके ब्राउज़र वॉलेट का पता है।
यह कैसे काम करता है, यह समझने के लिए, हमें यह देखना होगा कि यूजर इंटरफेस में संदेश कैसे बनाया जाता है, इसे सर्वर द्वारा कैसे रिले किया जाता है, और स्मार्ट अनुबंध इसे कैसे प्रोसेस करता है।
यूजर इंटरफेस
यूजर इंटरफेस WAGMI (opens in a new tab) पर आधारित है; आप इसके बारे में इस ट्यूटोरियल में पढ़ सकते हैं।
यहाँ बताया गया है कि हम संदेश पर हस्ताक्षर कैसे करते हैं:
1const signGreeting = useCallback(React हुक useCallback (opens in a new tab) हमें घटक (component) के फिर से ड्रा होने पर उसी फ़ंक्शन का पुन: उपयोग करके प्रदर्शन में सुधार करने देता है।
1 async (greeting) => {2 if (!account) throw new Error("Wallet not connected")यदि कोई खाता नहीं है, तो एक त्रुटि (error) उत्पन्न करें। ऐसा कभी नहीं होना चाहिए क्योंकि UI बटन जो उस प्रक्रिया को शुरू करता है जो signGreeting को कॉल करती है, उस स्थिति में अक्षम (disabled) होता है। हालाँकि, भविष्य के प्रोग्रामर उस सुरक्षा उपाय को हटा सकते हैं, इसलिए इस स्थिति की यहाँ भी जाँच करना एक अच्छा विचार है।
1 const domain = {2 name: "Greeter",3 version: "1",4 chainId,5 verifyingContract: contractAddr,6 }डोमेन सेपरेटर (opens in a new tab) के लिए पैरामीटर। यह मान स्थिर (constant) है, इसलिए बेहतर-अनुकूलित कार्यान्वयन में, हम इसे हर बार फ़ंक्शन कॉल किए जाने पर पुनर्गणना करने के बजाय एक बार गणना कर सकते हैं।
nameएक उपयोगकर्ता-पठनीय नाम है, जैसे कि उस dapp का नाम जिसके लिए हम हस्ताक्षर तैयार कर रहे हैं।versionसंस्करण (version) है। विभिन्न संस्करण संगत (compatible) नहीं हैं।chainIdवह चेन है जिसका हम उपयोग कर रहे हैं, जैसा कि WAGMI द्वारा (opens in a new tab) प्रदान किया गया है।verifyingContractवह अनुबंध पता है जो इस हस्ताक्षर को सत्यापित करेगा। हम नहीं चाहते कि एक ही हस्ताक्षर कई अनुबंधों पर लागू हो, यदि कईGreeterअनुबंध हैं और हम चाहते हैं कि उनके अलग-अलग अभिवादन हों।
1
2 const types = {3 GreetingRequest: [4 { name: "greeting", type: "string" },5 ],6 }डेटा प्रकार जिस पर हम हस्ताक्षर करते हैं। यहाँ, हमारे पास एक ही पैरामीटर है, greeting, लेकिन वास्तविक जीवन के सिस्टम में आमतौर पर अधिक होते हैं।
1 const message = { greeting }वास्तविक संदेश जिस पर हम हस्ताक्षर करना और भेजना चाहते हैं। greeting फ़ील्ड का नाम और इसे भरने वाले चर (variable) का नाम दोनों है।
1 const signature = await signTypedDataAsync({2 domain,3 types,4 primaryType: "GreetingRequest",5 message,6 })वास्तव में हस्ताक्षर प्राप्त करें। यह फ़ंक्शन एसिंक्रोनस है क्योंकि उपयोगकर्ताओं को डेटा पर हस्ताक्षर करने में (कंप्यूटर के दृष्टिकोण से) लंबा समय लगता है।
1 const r = `0x${signature.slice(2, 66)}`2 const s = `0x${signature.slice(66, 130)}`3 const v = parseInt(signature.slice(130, 132), 16)4
5 return {6 req: { greeting },7 v,8 r,9 s,10 }11 },फ़ंक्शन एक एकल हेक्साडेसिमल मान लौटाता है। यहाँ हम इसे फ़ील्ड्स में विभाजित करते हैं।
1 [account, chainId, contractAddr, signTypedDataAsync],2)यदि इनमें से कोई भी चर बदलता है, तो फ़ंक्शन का एक नया उदाहरण (instance) बनाएँ। account और chainId पैरामीटर उपयोगकर्ता द्वारा वॉलेट में बदले जा सकते हैं। contractAddr चेन Id का एक फ़ंक्शन है। signTypedDataAsync नहीं बदलना चाहिए, लेकिन हम इसे एक हुक (opens in a new tab) से आयात करते हैं, इसलिए हम निश्चित नहीं हो सकते, और इसे यहाँ जोड़ना सबसे अच्छा है।
अब जब नए अभिवादन पर हस्ताक्षर हो गए हैं, तो हमें इसे सर्वर पर भेजना होगा।
1 const sponsoredGreeting = async () => {2 try {यह फ़ंक्शन एक हस्ताक्षर लेता है और इसे सर्वर पर भेजता है।
1 const signedMessage = await signGreeting(newGreeting)2 const response = await fetch("/server/sponsor", {हम जिस सर्वर से आए हैं, उसमें /server/sponsor पथ (path) पर भेजें।
1 method: "POST",2 headers: { "Content-Type": "application/json" },3 body: JSON.stringify(signedMessage),4 })जानकारी को JSON-एन्कोडेड भेजने के लिए POST का उपयोग करें।
1 const data = await response.json()2 console.log("Server response:", data)3 } catch (err) {4 console.error("Error:", err)5 }6 }प्रतिक्रिया (response) आउटपुट करें। उत्पादन सिस्टम पर हम उपयोगकर्ता को प्रतिक्रिया भी दिखाएंगे।
सर्वर
मुझे अपने फ्रंट-एंड के रूप में Vite (opens in a new tab) का उपयोग करना पसंद है। यह स्वचालित रूप से React लाइब्रेरीज़ को सर्व करता है और फ्रंट-एंड कोड बदलने पर ब्राउज़र को अपडेट करता है। हालाँकि, Vite में बैकएंड टूलिंग शामिल नहीं है।
समाधान index.js (opens in a new tab) में है।
1 app.post("/server/sponsor", async (req, res) => {2 ...3 })4
5 // बाकी सब कुछ Vite को संभालने दें6 const vite = await createViteServer({7 server: { middlewareMode: true }8 })9
10 app.use(vite.middlewares)पहले हम उन अनुरोधों के लिए एक हैंडलर पंजीकृत करते हैं जिन्हें हम स्वयं संभालते हैं (/server/sponsor पर POST)। फिर हम अन्य सभी URL को संभालने के लिए एक Vite सर्वर बनाते हैं और उसका उपयोग करते हैं।
1 app.post("/server/sponsor", async (req, res) => {2 try {3 const signed = req.body4
5 const txHash = await sepoliaClient.writeContract({6 address: greeterAddr,7 abi: greeterABI,8 functionName: 'sponsoredSetGreeting',9 args: [signed.req, signed.v, signed.r, signed.s],10 })11 } ...12 })यह सिर्फ एक मानक viem (opens in a new tab) ब्लॉकचेन कॉल है।
स्मार्ट अनुबंध
अंत में, Greeter.sol (opens in a new tab) को हस्ताक्षर सत्यापित करने की आवश्यकता है।
1 constructor(string memory _greeting) {2 greeting = _greeting;3
4 DOMAIN_SEPARATOR = keccak256(5 abi.encode(6 keccak256(7 "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"8 ),9 keccak256(bytes("Greeter")),10 keccak256(bytes("1")),11 block.chainid,12 address(this)13 )14 );15 }कंस्ट्रक्टर डोमेन सेपरेटर (opens in a new tab) बनाता है, जो ऊपर दिए गए यूजर इंटरफेस कोड के समान है। ब्लॉकचेन निष्पादन बहुत अधिक महंगा है, इसलिए हम इसकी गणना केवल एक बार करते हैं।
1 struct GreetingRequest {2 string greeting;3 }यह वह स्ट्रक्चर है जिस पर हस्ताक्षर किए जाते हैं। यहाँ हमारे पास केवल एक फ़ील्ड है।
1 bytes32 private constant GREETING_TYPEHASH =2 keccak256("GreetingRequest(string greeting)");यह स्ट्रक्चर आइडेंटिफ़ायर (opens in a new tab) है। इसकी गणना हर बार यूजर इंटरफेस में की जाती है।
1 function sponsoredSetGreeting(2 GreetingRequest calldata req,3 uint8 v,4 bytes32 r,5 bytes32 s6 ) external {यह फ़ंक्शन एक हस्ताक्षरित अनुरोध प्राप्त करता है और अभिवादन को अपडेट करता है।
1 // EIP-712 डाइजेस्ट की गणना करें2 bytes32 digest = keccak256(3 abi.encodePacked(4 "\x19\x01",5 DOMAIN_SEPARATOR,6 keccak256(7 abi.encode(8 GREETING_TYPEHASH,9 keccak256(bytes(req.greeting))10 )11 )12 )13 );EIP 712 (opens in a new tab) के अनुसार डाइजेस्ट (digest) बनाएँ।
1 // हस्ताक्षरकर्ता को पुनर्प्राप्त करें2 address signer = ecrecover(digest, v, r, s);3 require(signer != address(0), "Invalid signature");हस्ताक्षरकर्ता का पता प्राप्त करने के लिए ecrecover (opens in a new tab) का उपयोग करें। ध्यान दें कि एक खराब हस्ताक्षर के परिणामस्वरूप अभी भी एक वैध पता मिल सकता है, बस वह एक यादृच्छिक (random) पता होगा।
1 // अभिवादन ऐसे लागू करें जैसे कि हस्ताक्षरकर्ता ने इसे कॉल किया हो2 greeting = req.greeting;3 emit SetGreeting(signer, req.greeting);4 }अभिवादन अपडेट करें।
भेद्यताएँ (Vulnerabilities)
यह उत्पादन-स्तर का कोड नहीं है। यह महत्वपूर्ण हमलों के प्रति संवेदनशील है और इसमें प्रमुख विशेषताओं का अभाव है। यहाँ कुछ दिए गए हैं, साथ ही उन्हें हल करने के तरीके भी हैं।
इनमें से कुछ हमलों को देखने के लिए, Attacks शीर्षक के अंतर्गत बटन पर क्लिक करें और देखें कि क्या होता है। Invalid signature बटन के लिए, लेन-देन प्रतिक्रिया देखने के लिए सर्वर कंसोल की जाँच करें।
सर्वर पर डिनायल ऑफ सर्विस (Denial of service)
सबसे आसान हमला सर्वर पर डिनायल-ऑफ-सर्विस (denial-of-service) (opens in a new tab) हमला है। सर्वर इंटरनेट पर कहीं से भी अनुरोध प्राप्त करता है और उन अनुरोधों के आधार पर लेन-देन भेजता है। हमलावर को वैध या अमान्य हस्ताक्षरों का एक गुच्छा जारी करने से रोकने के लिए बिल्कुल कुछ भी नहीं है। प्रत्येक एक लेन-देन का कारण बनेगा। अंततः सर्वर के पास गैस का भुगतान करने के लिए ETH खत्म हो जाएगा।
इस समस्या का एक समाधान दर को प्रति ब्लॉक एक लेन-देन तक सीमित करना है। यदि उद्देश्य बाहरी रूप से स्वामित्व वाले खातों (externally owned accounts) को अभिवादन दिखाना है, तो इससे कोई फर्क नहीं पड़ता कि ब्लॉक के बीच में अभिवादन क्या है।
एक अन्य समाधान पतों का ट्रैक रखना और केवल वैध ग्राहकों के हस्ताक्षरों की अनुमति देना है।
गलत अभिवादन हस्ताक्षर
जब आप Signature for wrong greeting पर क्लिक करते हैं, तो आप एक विशिष्ट पते (0xaA92c5d426430D4769c9E878C1333BDe3d689b3e) और अभिवादन (Hello) के लिए एक वैध हस्ताक्षर सबमिट करते हैं। लेकिन यह इसे एक अलग अभिवादन के साथ सबमिट करता है। यह ecrecover को भ्रमित करता है, जो अभिवादन को बदल देता है लेकिन इसमें गलत पता होता है।
इस समस्या को हल करने के लिए, पते को हस्ताक्षरित स्ट्रक्चर (opens in a new tab) में जोड़ें। इस तरह, ecrecover यादृच्छिक पता हस्ताक्षर में पते से मेल नहीं खाएगा, और स्मार्ट अनुबंध संदेश को अस्वीकार कर देगा।
रिप्ले हमले (Replay attacks)
जब आप Replay attack पर क्लिक करते हैं, तो आप वही "मैं 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e हूँ, और मैं चाहूंगा कि अभिवादन Hello हो" हस्ताक्षर सबमिट करते हैं, लेकिन सही अभिवादन के साथ। परिणामस्वरूप, स्मार्ट अनुबंध मानता है कि पते (जो आपका नहीं है) ने अभिवादन को वापस Hello में बदल दिया है। ऐसा करने की जानकारी लेन-देन की जानकारी (opens in a new tab) में सार्वजनिक रूप से उपलब्ध है।
यदि यह एक समस्या है, तो एक समाधान नॉन्स (opens in a new tab) जोड़ना है। पतों और संख्याओं के बीच एक मैपिंग (opens in a new tab) रखें, और हस्ताक्षर में एक नॉन्स फ़ील्ड जोड़ें। यदि नॉन्स फ़ील्ड पते के लिए मैपिंग से मेल खाता है, तो हस्ताक्षर स्वीकार करें और अगली बार के लिए मैपिंग बढ़ाएँ। यदि ऐसा नहीं होता है, तो लेन-देन को अस्वीकार कर दें।
एक अन्य समाधान हस्ताक्षरित डेटा में एक टाइमस्टैम्प जोड़ना है और उस टाइमस्टैम्प के कुछ सेकंड बाद ही हस्ताक्षर को वैध के रूप में स्वीकार करना है। यह सरल और सस्ता है, लेकिन हम समय सीमा के भीतर रिप्ले हमलों का जोखिम उठाते हैं, और यदि समय सीमा पार हो जाती है तो वैध लेन-देन की विफलता का जोखिम होता है।
अन्य अनुपलब्ध विशेषताएँ
उत्पादन सेटिंग में हम अतिरिक्त विशेषताएँ जोड़ेंगे।
अन्य सर्वरों से पहुँच
वर्तमान में, हम किसी भी पते को sponsorSetGreeting सबमिट करने की अनुमति देते हैं। विकेंद्रीकरण के हित में, यह बिल्कुल वही हो सकता है जो हम चाहते हैं। या हो सकता है कि हम यह सुनिश्चित करना चाहें कि प्रायोजित लेन-देन हमारे सर्वर के माध्यम से हों, जिस स्थिति में हम स्मार्ट अनुबंध में msg.sender की जाँच करेंगे।
किसी भी तरह से, यह एक सचेत डिज़ाइन निर्णय होना चाहिए, न कि केवल इस मुद्दे के बारे में न सोचने का परिणाम।
त्रुटि प्रबंधन (Error handling)
एक उपयोगकर्ता एक अभिवादन सबमिट करता है। हो सकता है कि यह अगले ब्लॉक में अपडेट हो जाए। हो सकता है कि ऐसा न हो। त्रुटियाँ अदृश्य होती हैं। उत्पादन सिस्टम पर, उपयोगकर्ता को इन मामलों के बीच अंतर करने में सक्षम होना चाहिए:
- नया अभिवादन अभी तक सबमिट नहीं किया गया है
- नया अभिवादन सबमिट कर दिया गया है, और यह प्रक्रिया में है
- नया अभिवादन अस्वीकार कर दिया गया है
निष्कर्ष
इस बिंदु पर, आपको कुछ केंद्रीकरण की कीमत पर, अपने dapp उपयोगकर्ताओं के लिए एक गैसलेस अनुभव बनाने में सक्षम होना चाहिए।
हालाँकि, यह केवल उन स्मार्ट अनुबंधों के साथ काम करता है जो ERC-712 का समर्थन करते हैं। उदाहरण के लिए, ERC-20 टोकन ट्रांसफर करने के लिए, केवल एक संदेश के बजाय मालिक द्वारा लेन-देन पर हस्ताक्षर करवाना आवश्यक है। इसका समाधान खाता अमूर्तन (ERC-4337) (opens in a new tab) है। मुझे इसके बारे में भविष्य में एक ट्यूटोरियल लिखने की उम्मीद है।
मेरे और काम के लिए यहाँ देखें (opens in a new tab)।
पेज का अंतिम अपडेट: 3 मार्च 2026