Signature verification एक public key का उपयोग करके गणितीय रूप से यह साबित करने की प्रक्रिया है कि किसी संदेश या लेनदेन (transaction) पर corresponding private key का उपयोग करके हस्ताक्षर (sign) किया गया था।
Ethereum बनाम Starknet पर Signature verification
Ethereum पर, signature verification या तो प्रोटोकॉल में या contract कोड में रहता है, जो इस बात पर निर्भर करता है कि अकाउंट EOA है या smart contract वॉलेट।
- EOA नेटिव ECDSA (secp256k1) का उपयोग करते हैं: प्रोटोकॉल transaction प्रोसेसिंग के हिस्से के रूप में signature verification को संभालता है। यह हस्ताक्षर से हस्ताक्षरकर्ता (signer) का पता (address) रिकवर करता है, और यदि रिकवर किया गया पता अपेक्षित पते (expected address) से मेल खाता है, तो हस्ताक्षर वैध (valid) होता है। Verification नियम प्रोटोकॉल में अंतर्निहित (built-in) होते हैं और उन्हें बदला नहीं जा सकता।
- Smart contract वॉलेट contract-defined validation का उपयोग करते हैं: वॉलेट contract अपना स्वयं का verification लॉजिक (सबसे आम तौर पर EIP-1271 के माध्यम से) परिभाषित करता है और यह लौटाता है कि हस्ताक्षर वैध है या नहीं। इसका अधिकार प्रोटोकॉल से हटकर contract कोड में चला जाता है।
Starknet पर, कोई EOA नहीं हैं। प्रत्येक अकाउंट एक smart contract है, इसलिए EOA और smart वॉलेट के बीच का अंतर मौजूद नहीं है। Signature verification हमेशा अकाउंट contract द्वारा ही संभाला जाता है। यह account abstraction का एक उदाहरण है, जहां validation के नियम प्रोटोकॉल द्वारा तय होने के बजाय प्रोग्राम करने योग्य (programmable) होते हैं।
प्रोटोकॉल के दृष्टिकोण से, signature verification प्रक्रिया कभी नहीं बदलती है: एक message hash और एक signature प्रदान किया जाता है, अकाउंट contract को कॉल किया जाता है, और यह तय करता है कि हस्ताक्षर वैध है या नहीं। उस निर्णय के आधार पर transaction को फिर निष्पादित (executed) या अस्वीकार (rejected) किया जाता है।
अकाउंट contract उस निर्णय तक कैसे पहुंचता है, यह पूरी तरह से उसके implementation पर निर्भर करता है। व्यवहार में, अधिकांश implementations मुख्य रूप से verification के लिए उपयोग की जाने वाली signature scheme में भिन्न होते हैं।
इस लेख में, हम Starknet पर उपयोग होने वाली सामान्य signature schemes को कवर करेंगे और दिखाएंगे कि व्यवहार में प्रत्येक को कैसे सत्यापित (verify) किया जाता है।
Verification के लिए उपयोग की जाने वाली सामान्य signature schemes
आज Starknet पर, सबसे आम signature schemes हैं:
- Stark curve ECDSA, और
- secp256k1 ECDSA.
Stark curve ECDSA (नेटिव Starknet स्कीम)
Starknet की नेटिव signature scheme Stark-friendly elliptic curve (“Stark curve”) पर ECDSA का उपयोग करती है। Cairo के built-in ECDSA फ़ंक्शन्स का उपयोग करके संदेश के हैश और signer की public key के विरुद्ध हस्ताक्षरों (signatures) को सत्यापित किया जाता है।
Stark curve ECDSA का उपयोग करके signature को कैसे सत्यापित किया जाता है
Cairo की कोर लाइब्रेरी Stark curve signatures को सत्यापित करने के लिए एक built-in फ़ंक्शन, check_ecdsa_signature, प्रदान करती है। यह चार arguments लेता है: message hash, signer की public key, और signature values r और s, फिर यह लौटाता है कि signature वैध है या नहीं:
fn check_ecdsa_signature(
message_hash: felt252,
public_key: felt252,
signature_r: felt252,
signature_s: felt252
) -> bool;
कोई भी verification होने से पहले, फ़ंक्शन के भीतर इनपुट्स की sanity-check की जाती है। Signature values शून्य (non-zero) नहीं होनी चाहिए और stark curve order के बराबर नहीं होनी चाहिए। हालाँकि, यह फ़ंक्शन यह जांच नहीं करता है कि r और s सख्ती से curve order से कम हैं, जो कि महत्वपूर्ण है क्योंकि दोनों मान पूर्णांक मॉड्यूलो n (curve order) में काम करते हैं, और न ही यह signature malleability के लिए जांच करता है। इसलिए कॉलर्स (Callers) से फ़ंक्शन को लागू करने (invoking) से पहले दो चीजों को सुनिश्चित (assert) करने की अपेक्षा की जाती है:
- कि
rcurve order से कम है, - और malleable वेरिएंट्स को खत्म करने के लिए कि
s <= ORDER / 2है।
दोनों चेक्स को बाद के उप-खंड (sub-section) में कवर किया जाएगा।
सभी चेक्स हो जाने के बाद, यह सत्यापित करने के लिए कि signature (r, s) दिए गए message hash और public key से जुड़ा है, check_ecdsa_signature फ़ंक्शन को कॉल किया जाता है। यह इन इनपुट्स पर elliptic curve संचालन (operations) की एक श्रृंखला (series) करता है और जांचता है कि क्या परिणाम सुसंगत (consistent) हैं। यदि वे हैं, तो signature वैध है और फ़ंक्शन true लौटाता है; अन्यथा, यह false लौटाता है।
वॉलेट प्रदाता (उदा., Ready, Braavos) आमतौर पर इस scheme पर भरोसा करते हैं ताकि यह पुष्टि की जा सके कि किसी transaction को ऑन-चेन (on-chain) निष्पादित होने की अनुमति देने से पहले सही अकाउंट द्वारा अधिकृत (authorized) किया गया था।
Stark curve signature verification को व्यवहार में लाना: एक टोकन airdrop का उदाहरण
इसे और अधिक ठोस (concrete) बनाने के लिए, एक सरल टोकन airdrop उदाहरण पर विचार करें जो दिखाता है कि यह पुष्टि करने के लिए कि कोई उपयोगकर्ता टोकन प्राप्त करने के योग्य है, व्यवहार में Stark-curve ECDSA verification का उपयोग कैसे किया जाता है।
इस फ्लो (flow) में, हम:
- एक अधिकृत signer से उसकी private key का उपयोग करके योग्य प्राप्तकर्ता (eligible recipient) के पते और दावा राशि (claim amount) पर एक Stark-curve signature उत्पन्न करवाएंगे
- एक airdrop contract डिप्लॉय करेंगे जो कोई भी टोकन ट्रांसफर करने से पहले ऑन-चेन signature को सत्यापित करता है
- जनरेट किए गए signature को सत्यापित करके और कुछ टोकनों का दावा (claim) करके airdrop contract का परीक्षण (test) करेंगे।
starknet.js का उपयोग करके एक Stark-curve signature जनरेट करना
इससे पहले कि हम हस्ताक्षर स्वीकार करने वाला airdrop contract बनाएं, हमें ऑफ-चेन (off-chain) एक Stark-curve signature उत्पन्न करने के तरीके की आवश्यकता है। एक वास्तविक एप्लिकेशन में, यह आमतौर पर वॉलेट के साथ किया जाता है। इस उदाहरण के लिए, हम चीजों को सरल रखेंगे और starknet.js का उपयोग करके signature जनरेट करेंगे।
हम निम्न कार्य करने के लिए एक छोटा Node.js प्रोजेक्ट सेट अप करके शुरुआत करेंगे:
- हस्ताक्षर किए जाने वाले संदेश (message) का निर्माण करना
- उसे हैश (hash) करना
- Stark-curve private key का उपयोग करके उस पर हस्ताक्षर करना
प्रोजेक्ट सेटअप (Project setup)
सबसे पहले, एक नया प्रोजेक्ट फ़ोल्डर बनाएं और इसे इनिशियलाइज़ करें:
mkdir my_signature
cd my_signature
npm init -y
इसके बाद, dependencies इंस्टॉल करें:
npm install starknet dotenv
यहाँ बताया गया है कि हमें उनकी आवश्यकता क्यों है:
starknet: messages को हैश करने और Stark-curve ECDSA signatures जनरेट करने के लिए फ़ंक्शन्स प्रदान करता हैdotenv: हमें private keys और कॉन्फ़िगरेशन को कोडबेस से बाहर रखने और उन्हें environment variables से सुरक्षित रूप से लोड करने देता है
अब, चलिए src डायरेक्टरी और एंट्री फ़ाइल बनाते हैं:
mkdir src
touch src/index.js
इस बिंदु पर, प्रोजेक्ट संरचना (structure) इस तरह दिखनी चाहिए:
my_signature/
├── src/
│ └── index.js
├── package.json
└── node_modules/
package.json फ़ाइल पर नेविगेट करें, यह नीचे दी गई छवि जैसा दिखना चाहिए:

लाल रंग में हाइलाइट किए गए भागों को निम्नलिखित से बदलें:
{
...
scripts: {
"start": "node src/index.js"
},
...
"type": "module",
...
}
कॉन्फ़िगरेशन वेरिएबल्स के लिए एक .env फ़ाइल बनाएं:
touch .env
.env फ़ाइल में निम्नलिखित जोड़ें:
AIRDROP_SIGNER_PK=0x...
RECIPIENT=0x...
प्लेसहोल्डर (placeholder) मानों को बदलें:
AIRDROP_SIGNER_PK: उस अकाउंट की private key जो संदेश पर हस्ताक्षर करेगा। उपलब्ध अकाउंट्स और उनकी संबंधित private keys देखने के लिएsncast account list -pचलाकर अपने मौजूदाsncastअकाउंट्स में से किसी एक का उपयोग करेंRECIPIENT: योग्य उपयोगकर्ता (Eligible user) का Starknet अकाउंट पता (address)
प्रोजेक्ट सेट अप हो जाने के बाद, हम अगले भाग पर जा सकते हैं, जो वह स्क्रिप्ट लिखना है जो किसी संदेश पर हस्ताक्षर करती है।
ऐसा करने के लिए, हम:
- हस्ताक्षर किए जाने वाले संदेश को परिभाषित करेंगे। हम यह परिभाषित करेंगे कि कौन टोकन का दावा (claim) कर सकता है और वे कितना दावा कर सकते हैं
- उस संदेश को ठीक उसी तरह हैश करेंगे जैसा कि contract अपेक्षा करता है
- Stark-curve ECDSA का उपयोग करके हैश पर हस्ताक्षर करेंगे
Airdrop संदेश पर हस्ताक्षर करना (Signing the airdrop message)
नीचे पूरी स्क्रिप्ट दी गई है। इसे हमारे द्वारा पहले बनाई गई index.js फ़ाइल में पेस्ट करें। यह पहली नज़र में थोड़ा भारी लग सकता है, लेकिन चिंता न करें, हम इसके ठीक बाद इसे एक-एक करके तोड़कर समझाएंगे।
import { ec, hash } from "starknet";
import * as dotenv from "dotenv";
dotenv.config();
const privateKey = process.env.AIRDROP_SIGNER_PK;
if (!privateKey) throw new Error("Set AIRDROP_SIGNER_PK=0x... in .env file");
const recipient = process.env.RECIPIENT;
if (!recipient) throw new Error("Set RECIPIENT=0x... in .env file");
// Amount (scaled 18 decimals)
const amount = 200 * 10**18;
// Message layout MUST match Cairo hashing exactly
// [recipient, amount]
const message = [recipient, amount];
// Hash + sign (Stark curve ECDSA)
const msgHash = hash.computePoseidonHashOnElements(message);
const signature = ec.starkCurve.sign(msgHash, privateKey);
// Log the signature
console.log(signature);
// Log `r` and `s` values in hex
console.log("\n\n\t\t==== `r` and `s` values in HEX ====");
console.log("r: 0x" + signature.r.toString(16));
console.log("s: 0x" + signature.s.toString(16) + "\n");
यहाँ ऊपर दिए गए कोड का पूरा ब्रेकडाउन (break down) है।
Imports और कॉन्फ़िगरेशन:
import { ec, hash } from "starknet";
import * as dotenv from "dotenv";
dotenv.config();
यहाँ, हम starknet.js से दो चीज़ें इम्पोर्ट करते हैं:
hash: एक मॉड्यूल जो Starknet की हैशिंग स्कीम का उपयोग करके संदेशों को हैश करने के लिए हेल्पर फ़ंक्शन्स प्रदान करता हैec: elliptic curve ऑपरेशन्स के लिए एक मॉड्यूल, जिसका उपयोग Stark curve पर हैश किए गए संदेश पर हस्ताक्षर करने के लिए किया जाता है
हम dotenv का उपयोग करके environment variables भी लोड करते हैं, जो निजी जानकारी को सोर्स कोड से बाहर रखता है।
Environment variables से इनपुट्स पढ़ना और उन्हें वैलिडेट (validate) करना:
कोड के इस हिस्से में, हम private key (AIRDROP_SIGNER_PK) और उपयोगकर्ता का पता (RECIPIENT) पढ़ रहे हैं, फिर यह वैलिडेट कर रहे हैं कि वे मौजूद हैं, अन्यथा, एक error थ्रो (throw) करते हैं।
const privateKey = process.env.AIRDROP_SIGNER_PK;
if (!privateKey) throw new Error("Set AIRDROP_SIGNER_PK=0x...");
const recipient = process.env.RECIPIENT;
if (!recipient) throw new Error("Set RECIPIENT=0x...");
हस्ताक्षर किए जाने वाले संदेश को परिभाषित करना:
const amount = 200 * 10**18;
const message = [recipient, amount];
यह ठीक वही संदेश है जिस पर हम हस्ताक्षर कर रहे हैं। स्पष्ट शब्दों में, इसका मतलब है “इस ‘recipient’ को इतने ‘amount’ टोकनों का दावा करने की अनुमति है।”
ध्यान दें कि message लेआउट ऑन-चेन contract द्वारा हैश किए जाने वाले लेआउट से बिल्कुल मेल खाना चाहिए। कोई भी बेमेल (mismatch), चाहे वह एक अलग क्रम (order) हो, कोई छूटा हुआ मान (missing value) हो, या यहां तक कि एक अलग हैश फ़ंक्शन का उपयोग करना हो, signature verification को विफल कर देगा।
संदेश को हैश करना (Hashing the message):
const msgHash = hash.computePoseidonHashOnElements(message);
computePoseidonHashOnElements मानों का एक ऐरे (array) लेता है (हमारे मामले में, airdrop संदेश) और उन्हें Poseidon हैश फ़ंक्शन का उपयोग करके एक एकल फ़ील्ड तत्व (single field element) में हैश करता है। परिणाम वह message hash (msgHash) है जिस पर हस्ताक्षर किए जाएंगे।
हम यहाँ Poseidon का उपयोग कर रहे हैं क्योंकि अन्य hash functions की तुलना में इसे ऑन-चेन सत्यापित करना तेज़ और सस्ता है।
Stark-curve ECDSA के साथ हस्ताक्षर करना:
const signature = ec.starkCurve.sign(msgHash, privateKey);
अंत में, हम Stark-curve ECDSA का उपयोग करके message hash पर हस्ताक्षर करते हैं। परिणाम एक signature ऑब्जेक्ट है जिसमें दो बड़ी संख्याएँ, r और s होती हैं, जो मिलकर signature बनाती हैं, साथ ही एक recovery मान होता है जिसका उपयोग verification के दौरान आंतरिक रूप से किया जाता है।
यह signature डेटा ही वह है जिसे airdrop contract ऑन-चेन सत्यापित करेगा।
स्क्रिप्ट चलाने के लिए निम्नलिखित कमांड का उपयोग करें:
npm start
यदि सब कुछ सही ढंग से निष्पादित (executed) होता है, तो टर्मिनल आउटपुट निम्नलिखित के समान होना चाहिए (ध्यान दें कि उपयोग किए गए संदेश और private key के आधार पर मान भिन्न होंगे):

व्यवहार में, केवल Signature स्ट्रक्चर से (लाल बॉक्स में) r और s मानों की आवश्यकता होती है। ये वे signature components हैं जिन्हें योग्य प्राप्तकर्ता (eligible recipient) अपने टोकन का दावा करते समय contract को पास करेगा।
recovery मान का उपयोग हस्ताक्षर (signature) से signer की public key के पुनर्निर्माण (reconstruct) के लिए किया जाता है। हालाँकि, Stark-curve signature verification के लिए इसकी आवश्यकता नहीं है, क्योंकि public key को verification फ़ंक्शन के तर्क (argument) के रूप में प्रदान किया जाता है, इसलिए verification के दौरान इसे रिकवर करने की कोई आवश्यकता नहीं है।
नीला बॉक्स हेक्साडेसिमल (hexadecimal) प्रारूप में r और s मान दिखाता है। इन मानों को सेव (save) करें, क्योंकि टोकन क्लेम के दौरान बाद में इनकी आवश्यकता होगी।
Airdrop contract
अब जब हम ऑफ़-चेन एक वैध (valid) Stark-curve signature जनरेट कर सकते हैं, तो अगला कदम airdrop contract को देखना है और यह देखना है कि यह प्राप्तकर्ता (recipient) को टोकन जारी करने से पहले इस signature को ऑन-चेन कैसे सत्यापित करता है।
एक Scarb प्रोजेक्ट बनाएं
रूट फ़ोल्डर में, my_contract नामक एक Scarb प्रोजेक्ट बनाने के लिए निम्नलिखित कमांड चलाएँ और फिर उसमें cd करें:
scarb new my_contract
cd my_contract
नई प्रोजेक्ट संरचना इस तरह दिखनी चाहिए:
my_signature/
├── my_contract/
│ ├── src/
│ │ └── lib.cairo
│ └ ...
└── ...
इसके बाद, lib.cairo फ़ाइल में ऑटो-जनरेटेड contract को नीचे दिए गए airdrop contract से बदलें। उच्च स्तर पर, contract तीन काम करता है:
- ठीक उसी message hash का पुनर्निर्माण (reconstruct) करता है जिस पर ऑफ-चेन हस्ताक्षर किए गए थे
- Stark-curve ECDSA signature को सत्यापित करता है
- सफल verification पर टोकन ट्रांसफर करता है
हम कोड ब्लॉक के बाद contract को एक-एक करके समझेंगे।
// WARNING: This code is for demonstration purposes only. Do not use in production.
use starknet::ContractAddress;
#[starknet::interface]
trait IERC20<TContractState> {
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
#[starknet::interface]
pub trait ISignatureAirdrop<TContractState> {
fn claim(ref self: TContractState, amount: felt252, r: felt252, s: felt252);
}
#[starknet::contract]
mod SignatureAirdrop {
use core::ecdsa::check_ecdsa_signature;
use core::ec::stark_curve::ORDER;
use core::poseidon::poseidon_hash_span;
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
// Bring the generated dispatcher types/traits into scope (from IERC20 interface).
use super::{IERC20Dispatcher, IERC20DispatcherTrait};
#[storage]
struct Storage {
// Use ContractAddress as the mapping key (instead of felt252).
claimed: Map<ContractAddress, bool>,
// Store the signer "Stark key" as felt252 (what check_ecdsa_signature expects).
signer: felt252,
// ERC20 token to airdrop
token: ContractAddress,
}
#[constructor]
fn constructor(ref self: ContractState, signer_stark_key: felt252, token: ContractAddress) {
self.signer.write(signer_stark_key);
self.token.write(token);
}
#[abi(embed_v0)]
impl SignatureAirdropImpl of super::ISignatureAirdrop<ContractState> {
fn claim(ref self: ContractState, amount: felt252, r: felt252, s: felt252) {
// 1) Cache caller's address
let recipient = get_caller_address();
// 2) One-time claim
let already_claimed = self.claimed.entry(recipient).read();
assert!(!already_claimed, "Already claimed");
// 3) Reconstruct message hash exactly like starknet.js:
// msgHash = computePoseidonHashOnElements([recipient, amount])
let msg: Array<felt252> = array![recipient.into(), amount];
let msg_hash: felt252 = poseidon_hash_span(msg.span());
// 4) Sanity-check on r and s value.
let order_u256: u256 = ORDER.into();
let r_u256: u256 = r.into();
let s_u256: u256 = s.into();
assert!(r_u256 < order_u256, "r >= curve order");
assert!(s_u256 <= order_u256 / 2, "s > curve order / 2");
// 5) Verify signature
let signer_pk = self.signer.read();
let valid = check_ecdsa_signature(msg_hash, signer_pk, r, s);
assert!(valid, "Invalid signature");
// 6) Mark claimed
self.claimed.entry(recipient).write(true);
// 7) Transfer tokens
let token_addr = self.token.read();
let token = IERC20Dispatcher { contract_address: token_addr };
let ok = token.transfer(recipient, amount.into());
assert!(ok, "Transfer failed");
}
}
}
Contract Interfaces
उपरोक्त कोड में, हमने दो interfaces का उपयोग किया:
- टोकन ट्रांसफर करने के लिए एक न्यूनतम (minimal) ERC-20 इंटरफ़ेस
- airdrop के
claimफ़ंक्शन के लिए एक इंटरफ़ेस
use starknet::ContractAddress;
#[starknet::interface]
trait IERC20<TContractState> {
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
#[starknet::interface]
pub trait ISignatureAirdrop<TContractState> {
fn claim(
ref self: TContractState,
amount: felt252,
r: felt252,
s: felt252,
);
}
Airdrop contract के लिए दो interfaces की आवश्यकता होती है: एक ERC-20 contract के साथ उसके transfer फ़ंक्शन के माध्यम से इंटरैक्ट (interact) करने के लिए, और दूसरा उपयोगकर्ताओं को अपने टोकन का दावा करने के लिए एक claim एंट्रीपॉइंट को एक्सपोज़ करने के लिए।
प्रासंगिक Dependencies इम्पोर्ट करना (Importing Relevant Dependencies)
यहाँ अधिकांश dependencies पहले के लेखों से पहले ही परिचित लगनी चाहिए। जिन एकमात्र imports के बारे में हमने अभी तक बात नहीं की है, वे कमेंट NEWLY ADDED IMPORTS के नीचे दिए गए हैं:
use core::poseidon::poseidon_hash_span;
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
use super::{IERC20Dispatcher, IERC20DispatcherTrait};
// *** NEWLY ADDED IMPORTS *** //
use core::ecdsa::check_ecdsa_signature;
use core::ec::stark_curve::ORDER;
check_ecdsa_signature वह फ़ंक्शन है जो verification करता है।
ORDER स्थिरांक (constant) Cairo के कोर Stark-curve मॉड्यूल से इम्पोर्ट किया गया है, जिसका उपयोग यह सुनिश्चित करने के लिए किया जाएगा कि r और s मान curve की वैध सीमा (valid range) के भीतर हों।
स्टोरेज लेआउट (Storage Layout)
प्रत्येक फ़ील्ड की एक बहुत ही विशिष्ट भूमिका होती है:
claimedट्रैक करता है कि क्या प्राप्तकर्ता ने पहले ही दावा कर लिया है (प्रति पता एक दावा)signerStark-curve public key है जो airdrop संदेशों पर हस्ताक्षर करने के लिए अधिकृत private key के अनुरूप (corresponding) हैtokenवितरित (distributed) किए जा रहे ERC-20 टोकन का पता है
#[storage]
struct Storage {
claimed: Map<ContractAddress, bool>,
signer: felt252,
token: ContractAddress,
}
Airdrop contract constructor
Constructor बस ये निर्दिष्ट (assign) करता है:
- अधिकृत (authorized) signer की public key को
signerस्टोरेज वेरिएबल में - airdrop टोकन पते (address) को
tokenस्टोरेज वेरिएबल में
#[constructor]
fn constructor(ref self: ContractState, signer_stark_key: felt252, token: ContractAddress) {
self.signer.write(signer_stark_key);
self.token.write(token);
}
Claim फ़ंक्शन
claim फ़ंक्शन एक योग्य उपयोगकर्ता को अपने airdrop आवंटन (allocation) का दावा करने की अनुमति देता है। यह दावा की गई amount और एक signature (r, s) को तर्कों (arguments) के रूप में लेता है:
fn claim(ref self: ContractState, amount: felt252, r: felt252, s: felt252) {
// 1) Cache caller's address
let recipient = get_caller_address();
// 2) One-time claim
let already_claimed = self.claimed.entry(recipient).read();
assert!(!already_claimed, "Already claimed");
// 3) Reconstruct message hash exactly like starknet.js:
// msgHash = computePoseidonHashOnElements([recipient, amount])
let msg: Array<felt252> = array![recipient.into(), amount];
let msg_hash: felt252 = poseidon_hash_span(msg.span());
// 4) Sanity-check on r and s value.
let order_u256: u256 = ORDER.into();
let r_u256: u256 = r.into();
let s_u256: u256 = s.into();
assert!(r_u256 < order_u256, "r >= curve order");
assert!(s_u256 <= order_u256 / 2, "s > curve order / 2");
// 5) Verify signature
let signer_pk = self.signer.read();
let valid = check_ecdsa_signature(msg_hash, signer_pk, r, s);
assert!(valid, "Invalid signature");
// 6) Mark claimed
self.claimed.entry(recipient).write(true);
// 7) Transfer tokens
let token_addr = self.token.read();
let token = IERC20Dispatcher { contract_address: token_addr };
let ok = token.transfer(recipient, amount.into());
assert!(ok, "Transfer failed");
}
आइए इसे स्टेप बाय स्टेप समझते हैं।
-
कॉलर के पते को कैश (Cache) करें:
// 1) Cache caller's address let recipient = get_caller_address();हमें संदेश का पुनर्निर्माण करने और यह सुनिश्चित करने के लिए इसकी आवश्यकता है कि कॉलर कुछ टोकन का दावा करने के योग्य है।
-
एक बार के दावों (one-time claims) को लागू करें:
let already_claimed = self.claimed.entry(recipient).read(); assert!(!already_claimed, "Already claimed");कोई भी क्रिप्टोग्राफ़ी (cryptography) करने से पहले, हम सुनिश्चित करते हैं कि प्राप्तकर्ता ने पहले ही दावा नहीं किया है। यह airdrop को खाली (drain) करने के लिए समान signature को दोबारा उपयोग (replay) करने से रोकता है।
-
Message hash का पुनर्निर्माण करें:
let msg: Array<felt252> = array![recipient.into(), amount]; let msg_hash: felt252 = poseidon_hash_span(msg.span());यहाँ, contract समान हैश फ़ंक्शन का उपयोग करके ठीक उसी message hash का पुनर्निर्माण करता है जिस पर ऑफ-चेन हस्ताक्षर किए गए थे। ऑन-चेन और ऑफ-चेन हैशिंग के बीच कोई भी बेमेल (mismatch) signature verification को विफल कर देगा।
-
Signature sanity-check:
let order_u256: u256 = ORDER.into(); let r_u256: u256 = r.into(); let s_u256: u256 = s.into(); assert!(r_u256 < order_u256, "r >= curve order"); assert!(s_u256 <= order_u256 / 2, "s > curve order / 2");यह जांचता है कि
rसख्ती से Stark curve order से कम है, औरscurve order के निचले आधे हिस्से में है (s <= ORDER / 2)। दोनों चेक्स यह सुनिश्चित करते हैं कि मान वैध ECDSA स्केलर्स हैं।sचेक अतिरिक्त रूप से signature malleability को समाप्त करता है।sको निचले आधे हिस्से तक सीमित करने से यह सुनिश्चित होता है कि दो रूपों में से केवल एक ही स्वीकार किया जाता है, प्रति संदेश एक अद्वितीय (unique) signature की गारंटी देता है। -
Stark-curve signature को सत्यापित करें:
let signer_pk = self.signer.read(); let valid = check_ecdsa_signature(msg_hash, signer_pk, r, s); assert!(valid, "Invalid signature");यहाँ, Cairo का built-in
check_ecdsa_signatureफ़ंक्शन सत्यापित करता है:- message hash
- signer की public key
(r, s)signature जोड़ा (pair)
यदि यह चेक पास हो जाता है, तो हम जानते हैं कि signer ने “recipient” को इतने “amount” के टोकनों का दावा करने के लिए अधिकृत किया है। यदि ऐसा नहीं होता है, तो पूरा transaction “Invalid signature” error के साथ रिवर्ट (revert) हो जाता है।
-
Claimed के रूप में मार्क करें:
self.claimed.entry(recipient).write(true);Signature को सत्यापित करने के बाद, टोकन ट्रांसफर करने से पहले प्राप्तकर्ता को claimed के रूप में मार्क किया जाता है।
-
टोकन ट्रांसफर करें:
let token_addr = self.token.read(); let token = IERC20Dispatcher { contract_address: token_addr }; let ok = token.transfer(recipient, amount.into()); assert!(ok, "Transfer failed");अंत में, contract ERC-20 contract को कॉल करता है और टोकन ट्रांसफर करता है। यदि ट्रांसफर विफल हो जाता है, तो पूरा transaction रिवर्ट हो जाता है।
Contract का परीक्षण करें (Test the contract)
इस अनुभाग (section) को हम वास्तव में जो परीक्षण कर रहे हैं (signature verification और claim लॉजिक) उस पर केंद्रित रखने के लिए, हम Starknet sepolia पर डिप्लॉय किए गए मौजूदा ERC-20 का पुन: उपयोग करेंगे और टोकनों को सीधे airdrop contract में मिंट (mint) करेंगे।
Airdrop contract को डिप्लॉय करें
सबसे पहले, हम निम्नलिखित कमांड का उपयोग करके इसका क्लास हैश (class hash) प्राप्त करने के लिए contract को डिक्लेयर (declare) करते हैं:
sncast --account <ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name SignatureAirdrop
बदलें (Replace):
<ACCOUNT_NAME>को sncast से अपने अकाउंट के नाम से<YOUR_API_KEY>को Alchemy से अपनी API कुंजी (key) से।
Contract को डिक्लेयर करने से इसकी क्लास StarkNet पर रजिस्टर हो जाती है। एक बार जब हमारे पास क्लास हैश हो जाता है, तो हम contract का एक इंस्टेंस डिप्लॉय कर सकते हैं।
Airdrop contract को डिप्लॉय करने के लिए, हमें निम्नलिखित की आवश्यकता है:
-
Signer public key: यह वह Stark-curve public key है जिसकी संबंधित private key का उपयोग ऑफ-चेन airdrop संदेशों पर हस्ताक्षर करने के लिए किया जाता है। क्लेम (claim) के दौरान, contract यह सत्यापित करता है कि कोई भी टोकन जारी करने से पहले प्रस्तुत किया गया हस्ताक्षर (signature) इस कुंजी (key) द्वारा तैयार किया गया था।
उनकी संबंधित private keys से जुड़ी public keys की सूची प्राप्त करने के लिए यह कमांड चलाएँ:
sncast account list # OR sncast account list -p # To show private keys tooफिर उस public key को कॉपी करें जो signature जनरेट करने में उपयोग की गई signer की private key से मेल खाती हो:

-
Token contract address: StarkNet Sepolia पर पहले से डिप्लॉय किए गए ERC-20 टोकन का पता।
0x03ec6283d9c7c8936991fad6523e01b60ad2bb092aa489087a3376c2ade7c09b
अब हम airdrop contract को डिप्लॉय करने के लिए आगे बढ़ सकते हैं:
sncast --account <ACCOUNT_NAME> \
deploy \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--class-hash <CLASS_HASH> \
--arguments '<SIGNER_PUBKEY>, <TOKEN_CONTRACT_ADDRESS>'
बदलें (Replace):
<ACCOUNT_NAME>को अपने अकाउंट के नाम से<YOUR_API_KEY>को अपनी Alchemy API कुंजी से<CLASS_HASH>को डिक्लेरेशन से प्राप्त क्लास हैश से<SIGNER_PUBKEY>को signer की public key से<TOKEN_CONTRACT_ADDRESS>को डिप्लॉय किए गए टोकन contract के पते से (0x03ec6283d9c7c8936991fad6523e01b60ad2bb092aa489087a3376c2ade7c09b)
एक बार डिप्लॉयमेंट सफल हो जाने पर, airdrop contract के पते (address) को सेव करें, हमें इसमें कुछ टोकन मिंट (mint) करने होंगे और बाद के सभी टेस्ट कॉल्स (test calls) के लिए इसका उपयोग करना होगा।
Airdrop contract में टोकन मिंट करें
इससे पहले कि कोई भी क्लेम (claim) सफल हो सके, airdrop contract के पास वितरित (distribute) करने के लिए पर्याप्त टोकन होने चाहिए।
Airdrop contract में 1,000 टोकन (18 दशमलव (decimals) तक स्केल किए गए) मिंट करने के लिए कमांड चलाएँ:
sncast --account <ACCOUNT_NAME> \
invoke \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-address <TOKEN_CONTRACT_ADDRESS> \
--function <FUNCTION_NAME> \
--arguments '<RECIPIENT>, 1_000_000_000_000_000_000_000'
बदलें (Replace):
<ACCOUNT_NAME>को अपने अकाउंट के नाम से<YOUR_API_KEY>को अपनी Alchemy API कुंजी से<TOKEN_CONTRACT_ADDRESS>को डिप्लॉय किए गए टोकन contract के पते से (0x03ec6283d9c7c8936991fad6523e01b60ad2bb092aa489087a3376c2ade7c09b)<FUNCTION_NAME>को इन्वोक (invoke) किए जाने वाले फ़ंक्शन के नाम से (mint)<RECIPIENT>को airdrop contract के पते से
इस बिंदु पर, सेटअप पूरा हो गया है। Airdrop contract डिप्लॉय हो गया है और टोकनों से फंड (fund) कर दिया गया है। अब हम claim फ़ंक्शन के परीक्षण (testing) पर आगे बढ़ सकते हैं।
claim फ़ंक्शन को इन्वोक करना
क्लेम करने के लिए, हम टोकन amount के साथ r और s signature components की आपूर्ति (supply) करेंगे जो पिछले अनुभाग में ऑफ-चेन जनरेट किए गए थे।
जो अकाउंट पता (address)
claimफ़ंक्शन को कॉल करता है वह वही प्राप्तकर्ता का पता (recipient address) होना चाहिए जिसे संदेश में साइन किया गया था। यदि कोई भिन्न अकाउंट उस हस्ताक्षर का उपयोग करके दावा करने का प्रयास करता है, तो transaction रिवर्ट हो जाएगा।
sncast का उपयोग करके, प्राप्तकर्ता आवश्यक तर्कों (arguments) को पास करते हुए डिप्लॉय किए गए airdrop contract पर claim फ़ंक्शन को कॉल करता है:
sncast --account <RECIPIENT_ACCOUNT> \
invoke \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-address <AIRDROP_CONTRACT_ADDRESS> \
--function claim \
--arguments '<AMOUNT>, <R>, <S>'
बदलें (Replace):
<RECIPIENT_ACCOUNT>को हस्ताक्षर किए गए प्राप्तकर्ता (signed recipient) के अकाउंट के नाम से संबंधित अकाउंट से<YOUR_API_KEY>को अपनी Alchemy API कुंजी से<AIRDROP_CONTRACT_ADDRESS>को डिप्लॉय किए गए airdrop contract के पते से<AMOUNT>को हस्ताक्षर किए गए संदेश में शामिल सटीक राशि (exact amount) से, हमारे मामले में,0xad78ebc5ac6200000(200 * 1e18)<R>और<S>को क्रमशःrऔरsमानों से (ऑफ-चेन जनरेट किए गए हस्ताक्षर से प्राप्त मान)
यदि सब कुछ सही है, तो transaction सफल होना चाहिए। यह पुष्टि करने के लिए कि क्लेम काम कर गया, हम प्राप्तकर्ता के लिए ERC-20 टोकन के balance_of फ़ंक्शन को क्वेरी करते हैं:
sncast call \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-address <TOKEN_CONTRACT_ADDRESS> \
--function balance_of \
--arguments '<RECIPIENT_ADDRESS>'
बदलें (Replace):
<YOUR_API_KEY>को अपनी Alchemy API कुंजी से<TOKEN_CONTRACT_ADDRESS>को टोकन contract के पते से<RECIPIENT_ADDRESS>को प्राप्तकर्ता के पते से
यदि क्लेम सफल रहा, तो लौटाया गया बैलेंस क्लेम किए गए <AMOUNT> को दर्शाना चाहिए।
अब जब हमने सफलतापूर्वक Stark-curve signature को सत्यापित कर लिया है और एक क्लेम पूरा कर लिया है, तो आइए देखें कि Cairo contract के भीतर Ethereum signatures को कैसे सत्यापित किया जाता है।
Secp256k1 ECDSA (Ethereum-शैली के हस्ताक्षर)
secp256k1 वह elliptic curve है जिसका उपयोग Ethereum की ECDSA signature scheme द्वारा किया जाता है। व्यवहार में, एक Ethereum वॉलेट अपनी private key के साथ किसी संदेश या transaction पर हस्ताक्षर करके यह साबित करता है कि “मैं इस पते को नियंत्रित करता हूं”। Verifier signature और संदेश या transaction हैश से signer का Ethereum पता रिकवर कर सकता है, फिर जांच सकता है कि यह अपेक्षित पते (expected address) से मेल खाता है या नहीं।
यहीं पर फ्लो पहले चर्चा की गई Stark-curve स्कीम से भिन्न होता है। check_ecdsa_signature फ़ंक्शन जिस पर हमने पहले चर्चा की थी, उसके साथ public key को इनपुट के रूप में पास किया जाता है और फ़ंक्शन एक स्पष्ट बूलियन (boolean) परिणाम लौटाता है। Ethereum-शैली के हस्ताक्षरों के साथ, ecrecover केवल एक पता रिकवर करता है, तब तक कोई verification नहीं होता जब तक कि रिकवर किए गए पते की स्पष्ट रूप से अपेक्षित signer के विरुद्ध तुलना नहीं की जाती।
Starknet पर, Cairo की कोर लाइब्रेरी Ethereum हस्ताक्षरों को सत्यापित करने के लिए हेल्पर फ़ंक्शन्स प्रदान करती है, अर्थात्, verify_eth_signature और is_eth_signature_valid। इन फ़ंक्शन्स का उपयोग Starknet contracts में एक message hash और एक अपेक्षित Ethereum पते के विरुद्ध Ethereum signature को सत्यापित करने के लिए किया जाता है। उनके बीच मुख्य अंतर यह है कि verify_eth_signature अमान्य इनपुट पर assert और panic करता है, जबकि is_eth_signature_valid एक Result लौटाता है, जो errors को ग्रेसफुल (graceful) तरीके से संभालने की अनुमति देता है।
इसके अतिरिक्त, दोनों फ़ंक्शन्स में signature malleability फ़िक्स built-in है, वे हुड के नीचे (under the hood) is_signature_s_valid के माध्यम से s <= N/2 (sanity check के रूप में s != 0 के साथ) लागू करते हैं, जिसका अर्थ है कि आपको स्वयं उस चेक को जोड़ने की आवश्यकता नहीं है।
यह प्रदर्शित करने के लिए कि Starknet contract के अंदर Ethereum signature verification कैसे काम करता है, हम Stark-curve ECDSA अनुभाग से उसी airdrop-आधारित उदाहरण विचार का पुन: उपयोग कर सकते हैं।
टोकन airdrop उदाहरण
मान लीजिए कि प्रोजेक्ट उन उपयोगकर्ताओं को Starknet पर टोकन वितरित करना चाहता है जो उनके Ethereum पते के आधार पर योग्य (eligible) हैं। यह सुनिश्चित करने के लिए कि केवल योग्य Ethereum पते का सही मालिक ही दावा कर सकता है, हस्ताक्षरित संदेश (signed message) में वे सभी मान शामिल होने चाहिए जो दावे (claim) को परिभाषित करते हैं, जैसे:
- योग्य Ethereum पता,
- Starknet पता जो टोकन प्राप्त करेगा, और
- टोकन की राशि (amount)।
यह क्लेम डेटा को एक साथ बांधता है। यदि इनमें से किसी भी मान को बदला जाता है, तो message hash भी बदल जाएगा, रिकवर किया गया signer अब विश्वसनीय (trusted) signer से मेल नहीं खाएगा, और verification विफल हो जाएगा।
कोड में, फ्लो यह होगा:
ethers.jsके साथ एक Ethereum private key (अधिकृत signer) का उपयोग करके ऑफ-चेन एक हस्ताक्षर (signature) जनरेट करना- हस्ताक्षर सत्यापित करने और टोकन का दावा करने के लिए एक Starknet airdrop contract बनाना
Ethers.js का उपयोग करके signature जनरेट करना
ऑफ-चेन Ethereum signature जनरेट करने के लिए, हम Ethers.js का उपयोग कर सकते हैं, जो Ethereum के साथ इंटरैक्ट करने के लिए एक JavaScript लाइब्रेरी है। यह हैश की गणना करने, हस्ताक्षर बनाने और स्मार्ट contracts के साथ इंटरैक्ट करने के लिए उपकरण प्रदान करता है, लेकिन Ethereum पारिस्थितिकी तंत्र (ecosystem) के लिए।
उच्च स्तर पर, signature जनरेट करने वाला कोड निम्नलिखित चरण (steps) निष्पादित करता है:
- ethers लाइब्रेरी से इम्पोर्ट करना।
- private key का उपयोग करके Ethereum वॉलेट लोड करना।
- क्लेम संदेश (claim message) का निर्माण करना (जिसमें Ethereum पता, Starknet पता और टोकन राशि शामिल है)।
- keccak256 का उपयोग करके संदेश को हैश करना।
- secp256k1 का उपयोग करके हैश पर हस्ताक्षर करना
- हस्ताक्षर को सत्यापित करने के लिए आवश्यक
r,s, औरvमानों को निकालना (Extract)।
// 1. Imports
import { solidityPackedKeccak256, Signature, SigningKey } from "ethers";
import * as dotenv from "dotenv";
dotenv.config();
// 2. Authorized signer (distributor)
const privateKey = process.env.ETH_SIGNER_PK; // SET `privateKey` IN .env FILE
const key = new SigningKey(privateKey);
// 3. Claim data (eligible user info)
const ethAddress = "0x1234567890123456789012345678901234567890";
const starknetAddress =
"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd";
const amount = 1000;
// 4. Create message hash
const messageHash = solidityPackedKeccak256(
["uint256", "uint256", "uint256"],
[ethAddress, starknetAddress, amount],
);
// 5. Sign the hash
const signature = key.sign(messageHash);
// 6. Split signature
const { r, s, v } = Signature.from(signature);
console.log({ messageHash, r, s, v });
यहाँ प्रत्येक भाग का विस्तृत ब्रेकडाउन दिया गया है:
-
Imports
import { solidityPackedKeccak256, Signature, SigningKey } from "ethers";solidityPackedKeccak256: मानों को एक साथ पैक करता है और Keccak256 का उपयोग करके उन्हें हैश करता है।SigningKey: निम्न-स्तरीय (low-level) secp256k1 हस्ताक्षर करने की कार्यक्षमता प्रदान करता है। इसका उपयोग सीधे 32-बाइट message hash पर हस्ताक्षर करने के लिए किया जाता है।Signature: signature ऑब्जेक्ट सेr,s, औरvको प्रारूपित (format) करने और निकालने के लिए एक उपयोगिता (utility)।
-
Signing Key बनाना
const key = new SigningKey(privateKey);यह हमें secp256k1 हस्ताक्षर कार्यक्षमता तक पहुंच प्रदान करता है, जिसकी हमें ठीक-ठीक आवश्यकता है: 32-बाइट हैश पर हस्ताक्षर करना और
(r, s, v)प्राप्त करना। -
क्लेम डेटा (Claim data)
const ethAddress = "0x1234567890123456789012345678901234567890"; const starknetAddress = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd"; const amount = 1000;ethAddressयोग्य Ethereum पता है।starknetAddressवह पता है जो Starknet पर टोकन प्राप्त करेगा।amountदावा किए जा रहे टोकनों की संख्या है।
-
क्लेम डेटा के साथ message hash बनाना
const messageHash = solidityPackedKeccak256( ["uint256", "uint256", "uint256"], [ethAddress, starknetAddress, amount], );क्लेम डेटा (Ethereum पता, Starknet पता, राशि) को पैक किया और Keccak-256 का उपयोग करके इसे हैश किया, वही हैश फ़ंक्शन जिसका उपयोग Ethereum द्वारा किया जाता है।
ध्यान रखें कि ऑफ-चेन हैशिंग लॉजिक Starknet contract के अंदर हैशिंग लॉजिक से बिल्कुल मेल खाना चाहिए। यदि पैकिंग क्रम (packing order), या हैशिंग फ़ंक्शन भिन्न होता है, तो रिकवर किया गया signer मेल नहीं खाएगा और क्लेम विफल हो जाएगा।
-
हैश पर हस्ताक्षर करना (Signing the Hash)
const signature = key.sign(messageHash);यह secp256k1 पर एक मानक (standard) Ethereum ECDSA signature उत्पन्न करता है। परिणाम में तीन components
r,s, औरv(recovery bit) शामिल हैं। ये वे मान हैं जिनका उपयोग airdrop contract बाद में signer की public key को रिकवर करने और Ethereum पते को प्राप्त (derive) करने के लिए करेगा। -
r,s, औरvनिकालना (Extracting)const { r, s, v } = Signature.from(signature);ये तीन मान ही वे हैं जिन्हें उपयोगकर्ता Starknet पर
claimफ़ंक्शन को पास करेगा।
इसके बाद airdrop contract का implementation है, फिर हम इसे स्थानीय रूप से (locally) टेस्ट करेंगे।
Airdrop contract
Contract में, claim फ़ंक्शन निम्न कार्य करता है:
- Message hash का पुनर्निर्माण करना, बिल्कुल ऑफ-चेन स्क्रिप्ट के समान
- keccak आउटपुट को little-endian से big-endian में कनवर्ट करना
- Cairo के secp256k1 हेल्पर्स का उपयोग करके Ethereum signature को सत्यापित करना
- Starknet प्राप्तकर्ता को टोकन ट्रांसफर करना
lib.cairo की सामग्री को निम्नलिखित कोड से बदलें:
// WARNING: This code is for demonstration purposes only. Do not use in production.
use starknet::ContractAddress;
use starknet::eth_address::EthAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
#[starknet::interface]
pub trait ISignatureAirdrop<TContractState> {
fn claim(
ref self: TContractState,
eth_address: EthAddress,
recipient: ContractAddress,
amount: u256,
r: u256,
s: u256,
v: u256,
);
}
#[starknet::contract]
mod SignatureAirdrop {
// Imports
use core::integer::u128_byte_reverse;
use core::keccak::keccak_u256s_be_inputs;
use starknet::eth_address::EthAddress;
use starknet::eth_signature::verify_eth_signature;
use starknet::secp256_trait::Signature;
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::ContractAddress;
use super::{IERC20Dispatcher, IERC20DispatcherTrait, ISignatureAirdrop};
#[storage]
struct Storage {
authorized_signer: EthAddress,
token: ContractAddress,
claimed: Map<ContractAddress, bool>,
}
#[constructor]
fn constructor(ref self: ContractState, authorized_signer: EthAddress, token: ContractAddress) {
self.authorized_signer.write(authorized_signer);
self.token.write(token);
}
#[abi(embed_v0)]
impl SignatureAirdropImpl of ISignatureAirdrop<ContractState> {
fn claim(
ref self: ContractState,
eth_address: EthAddress,
recipient: ContractAddress,
amount: u256,
r: u256,
s: u256,
v: u256,
) {
let already_claimed = self.claimed.entry(recipient).read();
assert(!already_claimed, 'ALREADY_CLAIMED');
// 1) Rebuild the exact same hash as ethers.js:
// solidityPackedKeccak256(
// ["uint256", "uint256", "uint256"],
// [ethAddress, starknetAddress, amount]
// )
let eth_felt: felt252 = eth_address.into();
let eth_u256: u256 = eth_felt.into();
let recipient_felt: felt252 = recipient.into();
let recipient_u256: u256 = recipient_felt.into();
let msg_hash_le = keccak_u256s_be_inputs(
array![eth_u256, recipient_u256, amount].span(),
);
// 2) Convert from little-endian to big-endian
let msg_hash_be = u256 {
low: u128_byte_reverse(msg_hash_le.high),
high: u128_byte_reverse(msg_hash_le.low),
};
// 3) Verify Ethereum signature from the stored authorized signer
let signer = self.authorized_signer.read();
let sig = Signature {
r,
s,
y_parity: v == 28
};
verify_eth_signature(msg_hash_be, sig, signer);
self.claimed.entry(recipient).write(true);
// 4) Transfer tokens
let token = IERC20Dispatcher { contract_address: self.token.read() };
let ok = token.transfer(recipient, amount);
assert(ok, 'TRANSFER_FAILED');
}
}
}
यहाँ कमेंट किए गए भागों का विस्तृत ब्रेकडाउन दिया गया है:
Imports
ऑन-चेन Ethereum-शैली के signature को सत्यापित करने से पहले, हमें कुछ हेल्पर प्रकारों (types) और फ़ंक्शन्स की आवश्यकता होती है। नीचे दिए गए पहले पांच imports सभी Starknet contract के अंदर secp256k1 (Ethereum) signature को सत्यापित करने के लिए आवश्यक हैं।
use core::integer::u128_byte_reverse;
use core::keccak::keccak_u256s_be_inputs;
use starknet::eth_address::EthAddress;
use starknet::eth_signature::verify_eth_signature;
use starknet::secp256_trait::Signature;
u128_byte_reverse128-बिट पूर्णांक (integer) के बाइट क्रम को उलट देता है (reverses)।keccak_u256s_be_inputsbig-endian प्रारूप मेंu256इनपुट पर Keccak हैश की गणना करता है, जो Ethereum के हैशिंग मानक (standard) से मेल खाता है।EthAddressCairo के अंदर एक मानक 20-बाइट Ethereum पते को दर्शाता है।verify_eth_signatureऑन-चेन secp256k1 verification करता है। यह एक message hash और एक signature लेता है, signature से Ethereum पते को रिकवर करता है, और अपेक्षित पते के विरुद्ध इसकी तुलना करता है। यदि वे मेल खाते हैं, तो signature वैध है।Signatureएक Ethereum signature की संरचना को परिभाषित करता है, जिसमें तीन componentsr,sऔरvहोते हैं।
claim फ़ंक्शन
फ़ंक्शन को प्राप्त होता है:
eth_address: airdrop के लिए योग्य Ethereum पताrecipient: Starknet पता जो airdrop टोकन प्राप्त करेगाamount: दावा किए जा रहे टोकनों की संख्याr,s,v: Ethereum ECDSA signature के components
fn claim(
ref self: ContractState,
eth_address: EthAddress,
recipient: ContractAddress,
amount: u256,
r: u256,
s: u256,
v: u256,
) {
// Claim logic
}
क्लेम लॉजिक (The claim logic)
-
Message Hash का पुनर्निर्माण
let eth_felt: felt252 = eth_address.into(); let eth_u256: u256 = eth_felt.into(); let recipient_felt: felt252 = recipient.into(); let recipient_u256: u256 = recipient_felt.into(); let msg_hash_le = keccak_u256s_be_inputs( array![eth_u256, recipient_u256, amount].span(), );यह ठीक उसी message hash को फिर से बनाता है जिसे ऑफ-चेन निम्नलिखित का उपयोग करके बनाया गया था:
solidityPackedKeccak256( ["uint256","uint256","uint256"], [ethAddress, starknetAddress, amount] );हम:
EthAddressऔरContractAddressकोu256में कनवर्ट करते हैं- समान क्रम रखते हैं:
[ethAddress, starknetAddress, amount] - Keccak-256 का उपयोग करके उन्हें हैश करते हैं
क्रम, प्रकार (types) और हैशिंग फ़ंक्शन बिल्कुल ऑफ-चेन लॉजिक से मेल खाने चाहिए। कोई भी अंतर एक अलग हैश उत्पन्न करेगा, और signature verification विफल हो जाएगा।
-
Message Hash को Big-Endian प्रारूप में बदलना
let msg_hash_be = u256 { low: u128_byte_reverse(msg_hash_le.high), high: u128_byte_reverse(msg_hash_le.low), };इस चरण का कारण यह है कि Cairo का
keccak_u256s_be_inputslittle-endian प्रारूप में हैश लौटाता है, जबकि Ethereum signatures big-endian 32-बाइट हैश पर उत्पन्न होते हैं।इस अंतर के कारण, Starknet contract के अंदर निर्मित message hash
ethersjsका उपयोग करके ऑफ-चेन निर्मित हैश से मेल नहीं खाएगा। यदि हम little-endian मान का सीधे उपयोग करके signature को सत्यापित करने का प्रयास करते हैं, तो रिकवर किया गया signer गलत होगा और verification विफल हो जाएगा।इसे ठीक करने के लिए, हम बाइट क्रम को उलट कर हैश को big-endian प्रारूप में बदलते हैं। चूँकि एक
u256को आंतरिक रूप से दोu128हिस्सों (lowऔरhigh) के रूप में दर्शाया जाता है, हम:- प्रत्येक
u128के बाइट्स को उलटते हैं - उनकी स्थितियों को स्वैप (swap) करते हैं
यह वह message hash उत्पन्न करता है जो ऑफ-चेन हस्ताक्षर किए गए हैश से मेल खाता है, जिससे signature verification चरण सफल हो जाता है।
- प्रत्येक
-
Ethereum Signature को सत्यापित करना
let signer = self.authorized_signer.read(); let sig = Signature { r, s, y_parity: v == 28, }; verify_eth_signature(msg_hash_be, sig, signer);यह क्रिप्टोग्राफ़िक verification चरण है:
-
संग्रहीत (stored)
authorized_signerको लोड करें -
(r, s, y_parity)का उपयोग करके एकSignatureस्ट्रक्चर का निर्माण करें। हालांकिclaimफ़ंक्शन signature पैरामीटरv(आमतौर पर Ethereum में 27 या 28) प्राप्त करता है, StarknetSignatureप्रकार इसके बजायboolप्रकार कीy_parityकी अपेक्षा करता है। यदिv28के बराबर है, तो paritytrueहै, अन्यथा यहfalseहै।y_parity: v == 28 -
verify_eth_signatureफ़ंक्शन को कॉल करें
आंतरिक रूप से, यह फ़ंक्शन:
(r, s, y_parity)से public key रिकवर करता है- उस कुंजी से Ethereum पते को प्राप्त (derive) करता है
- इसकी तुलना
authorized_signerसे करता है
यदि वे मेल नहीं खाते हैं, तो execution रिवर्ट हो जाता है।
-
-
टोकन ट्रांसफर करना
self.claimed.entry(recipient).write(true); let token = IERC20Dispatcher { contract_address:self.token.read() }; let ok = token.transfer(recipient,amount); assert(ok,'TRANSFER_FAILED');सफल signature verification के बाद ही हम:
- उपयोगकर्ता को claimed के रूप में मार्क करते हैं (डबल-क्लेम को रोकने के लिए)।
- ERC20 contract के
transferफ़ंक्शन को कॉल करते हैं। - Assert करते हैं कि ट्रांसफर सफल रहा।
यदि ट्रांसफर विफल हो जाता है, तो पूरा transaction रिवर्ट हो जाता है।
Airdrop contract का स्थानीय रूप से (locally) परीक्षण करें
चूँकि यह बिना ERC-20 एकीकरण (integration) वाला एक स्थानीय (local) परीक्षण है, इसलिए contract के claim फ़ंक्शन में चरण 4 (टोकन ट्रांसफर) को कमेंट आउट करें, क्योंकि ध्यान पूरी तरह से signature लॉजिक को सत्यापित करने पर है।
निम्नलिखित कोड को test_contract.cairo फ़ाइल में पेस्ट करें:
use /** <PROJECT_NAME> **/::{ISignatureAirdropDispatcher, ISignatureAirdropDispatcherTrait};
use snforge_std::{ContractClassTrait, DeclareResultTrait, declare};
use starknet::{ContractAddress, EthAddress};
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let signerAddress: felt252 = /** <SIGNER_ADDRESS> **/;
let tokenAddress: felt252 = 0x123;
let mut args = ArrayTrait::new();
args.append(signerAddress);
args.append(tokenAddress);
let (contract_address, _) = contract.deploy(@args).unwrap();
contract_address
}
#[test]
fn test_increase_balance() {
let contract_address = deploy_contract("SignatureAirdrop");
let dispatcher = ISignatureAirdropDispatcher { contract_address };
// The same message we signed off-chain
let eth_address: EthAddress = 0x1234567890123456789012345678901234567890.try_into().unwrap();
let starknet_address: ContractAddress =
0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd
.try_into()
.unwrap();
let amount: u256 = 1000;
dispatcher
.claim(
eth_address,
starknet_address,
amount,
/** <R> **/,
/** <S> **/,
/** <V> **/,
);
}
बदलें (Replace):
<PROJECT_NAME>को प्रोजेक्ट (फ़ोल्डर) के नाम से<SIGNER_ADDRESS>को signer की private key से जुड़े Ethereum पते से<R>,<S>, और<V>को क्रमशःr,s, औरvमानों से (ऑफ-चेन जनरेट किए गए हस्ताक्षर से प्राप्त मान)
फिर चलाएँ:
scarb test
एक पास होने वाला परीक्षण इस बात की पुष्टि करता है कि contract सही ढंग से message hash का पुनर्निर्माण करता है, signer की public key के विरुद्ध signature को सत्यापित करता है, और क्लेम को उपयोग किए गए (used) के रूप में मार्क करता है। यदि परीक्षण "Invalid signature" के साथ विफल हो जाता है, तो सबसे संभावित कारण ये हैं:
- Message hash को ऑन-चेन और ऑफ-चेन समान रूप से नहीं बनाया गया था (फ़ील्ड क्रम, एन्कोडिंग बेमेल)
- Constructor को गलत signer पता पास किया गया था
r,s, याvमानों को ऑफ-चेन स्क्रिप्ट आउटपुट से गलत तरीके से कॉपी किया गया था
अभ्यास (Exercise): टेस्ट फ़ाइल में केवल amount को एक अलग मान में बदलकर विफलता (failure) के मामले का परीक्षण करें, फिर से परीक्षण चलाएँ।