Access control यह निर्धारित करता है कि कौन विशिष्ट functions को कॉल कर सकता है या contract के व्यवहार को संशोधित कर सकता है। यह लेख समझाता है कि Cairo assert macro का उपयोग करके access control को कैसे लागू करता है।
Solidity में Access Control की एक समीक्षा
Solidity में, modifiers किसी function के चारों ओर व्यवहार (behavior) को रैप (wrap) करने का एक संक्षिप्त तरीका हैं। इनका उपयोग आमतौर पर access control के लिए किया जाता है। विचार करें कि निम्नलिखित contract एक onlyOwner modifier को परिभाषित करता है, जो यह सुनिश्चित करता है कि केवल contract का owner ही callMe function को कॉल कर सकता है:
// SPDX-License-Identifier: MIT
pragma solidity =0.8.30;
contract SomeContract {
address owner;
constructor() {
owner = msg.sender;
}
// THE `ONLYOWNER` MODIFIER
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
function callMe() public onlyOwner {
// callMe logic
}
}
Modifiers आपको preconditions को कहीं और ले जाकर अपने मुख्य function लॉजिक को साफ रखने की अनुमति देते हैं, जैसे कि हमने ऊपर onlyOwner modifier में किया है।
Cairo में कोई modifiers नहीं हैं — Cairo access control कैसे करता है
Cairo में, कोई modifier कीवर्ड नहीं है। इसके बजाय, हम अपने चेक लागू करने के लिए एक नियमित function को परिभाषित करते हैं, मान लीजिए only_owner और इसे call_me function के अंदर invoke करते हैं।
नीचे दिया गया कोड इसका एक उदाहरण दिखाता है कि यह कैसा दिख सकता है:
Constructor कॉलर (caller) के एड्रेस को (get_caller_address(), Solidity के msg.sender के समान है) owner वेरिएबल को असाइन करता है।
#[starknet::contract]
mod SomeContract {
// import the required functions from the starknet core library
use starknet::ContractAddress;
use starknet::get_caller_address;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
owner: ContractAddress,
}
#[constructor]
fn constructor(ref self: ContractState) {
self.owner.write(get_caller_address());
}
#[generate_trait]
impl Internal of InternalTrait {
fn only_owner(self: @ContractState) {
let caller = get_caller_address();
let stored_owner = self.owner.read();
// ENSURES THE CALLER IS THE OWNER OR REVERT
assert(caller == stored_owner, 'Not owner');
}
}
#[abi(embed_v0)]
impl SomeContractImpl of super::ISomeContract<ContractState> {
// CALL_ME FUNCTION
fn call_me(ref self: ContractState) {
self.only_owner();
// callMe logic
}
}
}
यह Cairo संस्करण call_me function तक पहुंच को प्रतिबंधित करके Solidity पैटर्न को दर्शाता है। यह assert करके सुनिश्चित करता है कि केवल owner ही इसे कॉल कर सकता है कि कॉलर का एड्रेस contract state में स्टोर किए गए owner से मेल खाता है।
assert(caller == stored_owner, 'Not owner');
assert function Solidity के require के समान व्यवहार करता है जो निष्पादन (execution) को रोकता है और यदि शर्त (condition) विफल हो जाती है तो ट्रांजेक्शन को revert कर देता है। इसे और भी बेहतर बनाने के लिए, Cairo एक और function प्रदान करता है जिसे assert! कहा जाता है, जो फॉर्मेटेड एरर मैसेजेस (formatted error messages) का समर्थन करता है, जिससे यह अधिक एक्सप्रेसिव (expressive) बन जाता है।
assert Vs assert!
जबकि assert function और assert! macro (! मैक्रोज़ (macros) को functions से अलग करता है) एक ही उद्देश्य की पूर्ति करते हैं, यह सुनिश्चित करना कि एक condition सही (true) है, वे एरर रिपोर्ट करने के तरीके में भिन्न होते हैं।
assert:
पहला आर्गुमेंट, condition, एक boolean एक्सप्रेशन है। यदि यह false है, तो प्रोग्राम सिंगल कोट्स (single quotes) में फिक्स्ड एरर मैसेज के साथ पैनिक (panic) हो जाता है।
assert(condition, 'static error message');
assert!:
- पहला आर्गुमेंट,
condition, एक boolean एक्सप्रेशन है। यदि यहfalseहै, तो प्रोग्राम दूसरे आर्गुमेंट के साथ पैनिक (panic) हो जाता है। - दूसरा आर्गुमेंट डबल कोट्स (double quotes) में एक फॉर्मेटेड स्ट्रिंग (formatted string) है।
assert!(condition, "Formatted error: {}", variable);
फॉर्मेटेड स्ट्रिंग में {} का क्या अर्थ है
एक फॉर्मेटेड स्ट्रिंग में, {} एक प्लेसहोल्डर (placeholder) है। जब कोड चलता है, तो variable के मान को एक स्ट्रिंग में बदल दिया जाता है और वहां डाला जाता है जहां {} दिखाई देता है।
इसे एक रिक्त स्थान भरने (fill-in-the-blank) की तरह समझें:
let name = "Alice";
println!("Hello, {}", name);
// Prints: Hello, Alice
हमारे पास कई प्लेसहोल्डर्स हो सकते हैं:
println!("x = {}, y = {}", x, y);
क्रम मायने रखता है: प्रत्येक {} स्ट्रिंग के बाद संबंधित आर्गुमेंट द्वारा भरा जाता है।
यह डेवलपर्स को डिबगिंग या एरर हैंडलिंग करते समय अधिक लचीलापन (flexibility) देता है। एक स्टैटिक स्ट्रिंग के बजाय, आप मैसेज में रनटाइम वैल्यूज को शामिल कर सकते हैं, ऐसा कुछ जिसे Solidity का require सीधे तौर पर सपोर्ट नहीं करता है।
अनुशंसित तरीका
assert!का उपयोग करना है, यहां तक कि प्रोडक्शन (production) में भी।
assert! में समर्थित प्रकार (Supported Types)
सभी प्रकार (types) को assert! मैसेज के अंदर उपयोग नहीं किया जा सकता है। केवल वे types जो core::fmt::Display trait को लागू (implement) करते हैं, उनका उपयोग assert! मैसेज फॉर्मेटिंग में किया जा सकता है। Display trait यह परिभाषित करता है कि {} फॉर्मेट स्पेसिफायर (format specifier) का उपयोग करते समय कोई type स्ट्रिंग प्रतिनिधित्व (string representation) में कैसे परिवर्तित होता है। ये हैं:
ByteArrayboolNonZero<T>(किसी भीTके लिए जो स्वयंDisplayको लागू करता है)- सभी इंटीजर प्रिमिटिव्स (integer primitives) (
felt252,u8,u16,u32,u64,u128,u256, और हस्ताक्षरित वेरिएंट (signed variants) यदि मौजूद हैं) @T(ऊपर दिए गए किसी भीDisplaytype का संदर्भ (reference))
उदाहरण के लिए, felt252 जैसा type ठीक है, लेकिन कस्टम structs या ContractAddress जैसे types एरर उत्पन्न करेंगे क्योंकि वे Display trait को लागू नहीं करते हैं।
यदि आप यह करने का प्रयास करते हैं:
let caller: ContractAddress = get_caller_address();
// ❌ This will fail to compile
assert!(caller == owner, "Caller was: {}", caller);
आपको इस तरह की एक एरर दिखाई देगी:
Trait has no implementation in context: core::fmt::Display::<core::starknet::contract_address::ContractAddress>
इससे बचने के लिए, यदि आपको केवल संख्यात्मक प्रतिनिधित्व (numeric representation) की आवश्यकता है, तो आप एड्रेस को felt252 में बदल सकते हैं:
let caller: ContractAddress = get_caller_address();
let caller_felt: felt252 = caller.into();
// ✅ This works, assuming the `owner` variable is of type felt252 too
assert!(caller_felt == owner, "Caller was: {}", caller_felt);
इसलिए जबकि assert! आपको एक्सप्रेसिव एरर हैंडलिंग (expressive error handling) देता है, अपने मैसेजेस को फॉर्मेट करते समय type आवश्यकताओं को ध्यान में रखें।
अभ्यास (Exercise): एक Cairo function लिखें जो दो संख्याएं, n और d लेता है, और उनका विभाजन (division) लौटाता है। यदि d शून्य है, तो function को इस मैसेज के साथ revert करना चाहिए: “n is not divisible by d” (एरर में n और d के वास्तविक मूल्यों को शामिल करते हुए)। संकेत (Hint): assert! function का उपयोग करें। safe_divide अभ्यास को हल करने के लिए, इस repo को क्लोन करें।
यह लेख Cairo Programming on Starknet पर एक ट्यूटोरियल श्रृंखला का हिस्सा है