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