गॅस शुल्काचे प्रायोजकत्व: तुमच्या वापरकर्त्यांसाठी व्यवहार खर्च कसा कव्हर करावा
परिचय
जर आपल्याला इथेरियमने आणखी एक अब्ज लोकांना (नवीन टॅबमध्ये उघडते) सेवा द्यावी असे वाटत असेल, तर आपल्याला अडथळे दूर करणे आणि ते वापरण्यास शक्य तितके सोपे करणे आवश्यक आहे. या अडथळ्याचे एक कारण म्हणजे गॅस शुल्क भरण्यासाठी ETH ची आवश्यकता.
जर तुमच्याकडे एखादे विकेंद्रित ॲप्लिकेशन (dapp) असेल जे वापरकर्त्यांकडून पैसे कमवते, तर वापरकर्त्यांना तुमच्या सर्व्हरद्वारे व्यवहार सबमिट करू देणे आणि व्यवहार शुल्क स्वतः भरणे अर्थपूर्ण ठरू शकते. कारण वापरकर्ते अजूनही त्यांच्या वॉलेटमध्ये EIP-712 अधिकृतता संदेशावर (नवीन टॅबमध्ये उघडते) स्वाक्षरी करतात, ते इथेरियमच्या अखंडतेची हमी कायम ठेवतात. उपलब्धता व्यवहार रिले करणाऱ्या सर्व्हरवर अवलंबून असते, त्यामुळे ती अधिक मर्यादित असते. तथापि, तुम्ही गोष्टी अशा प्रकारे सेट करू शकता जेणेकरून वापरकर्ते थेट स्मार्ट कॉन्ट्रॅक्टमध्ये प्रवेश करू शकतील (जर त्यांना ETH मिळाले), आणि इतरांना व्यवहार प्रायोजित करायचे असल्यास त्यांचे स्वतःचे सर्व्हर सेट करू देऊ शकता.
या ट्युटोरिअलमधील तंत्र केवळ तेव्हाच कार्य करते जेव्हा तुम्ही स्मार्ट कॉन्ट्रॅक्ट नियंत्रित करता. इतर तंत्रे आहेत, ज्यामध्ये खाते अमूर्तीकरण (नवीन टॅबमध्ये उघडते) समाविष्ट आहे जे तुम्हाला इतर स्मार्ट कॉन्ट्रॅक्ट्ससाठी व्यवहार प्रायोजित करू देते, जे मी भविष्यातील ट्युटोरिअलमध्ये कव्हर करण्याची आशा करतो.
टीप: हा उत्पादन-स्तरावरील (production-level) कोड नाही. तो महत्त्वपूर्ण हल्ल्यांसाठी असुरक्षित आहे आणि त्यात प्रमुख वैशिष्ट्यांचा अभाव आहे. याबद्दल अधिक जाणून घेण्यासाठी या मार्गदर्शकाच्या असुरक्षा विभागात वाचा.
पूर्वअटी
हे ट्युटोरिअल समजून घेण्यासाठी तुम्हाला खालील गोष्टींची आधीच माहिती असणे आवश्यक आहे:
- Solidity
- JavaScript
- React आणि WAGMI. जर तुम्हाला या युजर इंटरफेस टूल्सची माहिती नसेल, तर आमच्याकडे त्यासाठी एक ट्युटोरिअल आहे.
नमुना ॲप्लिकेशन
येथील नमुना ॲप्लिकेशन हे Hardhat च्या Greeter कॉन्ट्रॅक्टचा एक प्रकार आहे. तुम्ही ते GitHub वर (नवीन टॅबमध्ये उघडते) पाहू शकता. स्मार्ट कॉन्ट्रॅक्ट आधीपासूनच Sepolia (नवीन टॅबमध्ये उघडते) वर, 0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA (नवीन टॅबमध्ये उघडते) या पत्त्यावर तैनात (deployed) केले आहे.
ते प्रत्यक्ष कृतीत पाहण्यासाठी, या पायऱ्या फॉलो करा.
-
रिपॉझिटरी क्लोन करा आणि आवश्यक सॉफ्टवेअर इन्स्टॉल करा.
git clone https://github.com/qbzzt/260301-gasless.git cd 260301-gasless/server npm install -
Sepolia वर ETH असलेल्या वॉलेटवर
PRIVATE_KEYसेट करण्यासाठी.envसंपादित करा. जर तुम्हाला Sepolia ETH ची आवश्यकता असेल, तर फॉसेट वापरा. आदर्शपणे, ही खाजगी की तुमच्या ब्राउझर वॉलेटमध्ये असलेल्या की पेक्षा वेगळी असावी. -
सर्व्हर सुरू करा.
npm run dev -
http://localhost:5173(नवीन टॅबमध्ये उघडते) या URL वर ॲप्लिकेशन ब्राउझ करा. -
वॉलेटशी कनेक्ट करण्यासाठी Connect with Injected वर क्लिक करा. वॉलेटमध्ये मंजूर करा, आणि आवश्यक असल्यास Sepolia मधील बदल मंजूर करा.
-
नवीन ग्रीटिंग लिहा आणि Update greeting via sponsor वर क्लिक करा.
-
संदेशावर स्वाक्षरी करा.
-
सुमारे 12 सेकंद प्रतीक्षा करा (Sepolia वरील ब्लॉक वेळ). प्रतीक्षा करत असताना तुम्ही व्यवहार पाहण्यासाठी सर्व्हरच्या कन्सोलमधील URL पाहू शकता.
-
ग्रीटिंग बदलले आहे हे पहा, आणि 'last updated by' पत्त्याचे मूल्य आता तुमच्या ब्राउझर वॉलेटचा पत्ता आहे.
हे कसे कार्य करते हे समजून घेण्यासाठी, युजर इंटरफेसमध्ये संदेश कसा तयार होतो, तो सर्व्हरद्वारे कसा रिले केला जातो आणि स्मार्ट कॉन्ट्रॅक्ट त्यावर कशी प्रक्रिया करते हे पाहणे आवश्यक आहे.
युजर इंटरफेस
युजर इंटरफेस WAGMI (नवीन टॅबमध्ये उघडते) वर आधारित आहे; तुम्ही त्याबद्दल या ट्युटोरिअलमध्ये वाचू शकता.
आम्ही संदेशावर कशी स्वाक्षरी करतो ते येथे आहे:
const signGreeting = useCallback(
React हूक useCallback (नवीन टॅबमध्ये उघडते) घटक पुन्हा काढला जातो तेव्हा तेच फंक्शन पुन्हा वापरून कार्यप्रदर्शन सुधारण्यास मदत करते.
async (greeting) => {
if (!account) throw new Error("Wallet not connected")
जर कोणतेही खाते नसेल, तर त्रुटी (error) निर्माण करा. असे कधीही होऊ नये कारण त्या बाबतीत signGreeting ला कॉल करणारी प्रक्रिया सुरू करणारे UI बटण अक्षम (disabled) केलेले असते. तथापि, भविष्यातील प्रोग्रामर ती सुरक्षा काढून टाकू शकतात, त्यामुळे ही अट येथेही तपासणे चांगली कल्पना आहे.
const domain = {
name: "Greeter",
version: "1",
chainId,
verifyingContract: contractAddr,
}
डोमेन सेपरेटरसाठी (नवीन टॅबमध्ये उघडते) पॅरामीटर्स. हे मूल्य स्थिर आहे, त्यामुळे अधिक चांगल्या-ऑप्टिमाइझ केलेल्या अंमलबजावणीमध्ये, प्रत्येक वेळी फंक्शन कॉल केल्यावर त्याची पुनर्गणना करण्याऐवजी आपण त्याची एकदाच गणना करू शकतो.
nameहे वापरकर्त्याला वाचता येण्याजोगे नाव आहे, जसे की dapp चे नाव ज्यासाठी आपण स्वाक्षऱ्या तयार करत आहोत.versionही आवृत्ती (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,
})
प्रत्यक्षात स्वाक्षरी मिळवा. हे फंक्शन असिंक्रोनस आहे कारण वापरकर्त्यांना डेटावर स्वाक्षरी करण्यासाठी (संगणकाच्या दृष्टिकोनातून) बराच वेळ लागतो.
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],
)
यापैकी कोणतेही व्हेरिएबल्स बदलल्यास, फंक्शनचा नवीन इन्स्टन्स तयार करा. account आणि chainId पॅरामीटर्स वापरकर्त्याद्वारे वॉलेटमध्ये बदलले जाऊ शकतात. contractAddr हे चेन Id चे फंक्शन आहे. 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)
}
}
प्रतिसाद (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)
प्रथम आपण स्वतः हाताळत असलेल्या विनंत्यांसाठी एक हँडलर नोंदवतो (POST ते /server/sponsor). नंतर आपण इतर सर्व 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 (नवीन टॅबमध्ये उघडते) नुसार डायजेस्ट तयार करा.
// स्वाक्षरीकर्ता पुनर्प्राप्त करा
address signer = ecrecover(digest, v, r, s);
require(signer != address(0), "Invalid signature");
स्वाक्षरीकर्त्याचा पत्ता मिळवण्यासाठी ecrecover (नवीन टॅबमध्ये उघडते) वापरा. लक्षात घ्या की चुकीच्या स्वाक्षरीमुळे तरीही वैध पत्ता मिळू शकतो, फक्त तो एक यादृच्छिक (random) पत्ता असेल.
// स्वाक्षरीकर्त्याने कॉल केल्याप्रमाणे ग्रीटिंग लागू करा
greeting = req.greeting;
emit SetGreeting(signer, req.greeting);
}
ग्रीटिंग अपडेट करा.
असुरक्षा
हा उत्पादन-स्तरावरील कोड नाही. तो महत्त्वपूर्ण हल्ल्यांसाठी असुरक्षित आहे आणि त्यात प्रमुख वैशिष्ट्यांचा अभाव आहे. येथे काही असुरक्षा आणि त्या कशा सोडवायच्या याबद्दल माहिती दिली आहे.
यापैकी काही हल्ले पाहण्यासाठी, Attacks शीर्षकाखालील बटणांवर क्लिक करा आणि काय होते ते पहा. Invalid signature बटणासाठी, व्यवहाराचा प्रतिसाद पाहण्यासाठी सर्व्हर कन्सोल तपासा.
सर्व्हरवरील डिनायल ऑफ सर्व्हिस (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 पत्त्याच्या मालकीची नसून एका वेगळ्या कॉन्ट्रॅक्टच्या मालकीची असणे (खाते अमूर्तीकरणचा एक सोपा प्रकार). तुम्ही याबद्दल पुढील ट्युटोरिअलमध्ये अधिक वाचू शकता.