एथेरियम (Ethereum) में, किसी ट्रांज़ैक्शन की कीमत की गणना के रूप में की जाती है। यह हमें बताता है कि ब्लॉकचेन पर ट्रांज़ैक्शन को शामिल करने के लिए कितना Ether खर्च किया जाएगा। ट्रांज़ैक्शन भेजे जाने से पहले, एक gasLimit निर्दिष्ट किया जाता है और उसका अग्रिम भुगतान किया जाता है। यदि ट्रांज़ैक्शन का गैस खत्म हो जाता है, तो यह revert हो जाता है।
EVM चेन्स के विपरीत, सोलाना opcodes/instructions गैस के बजाय “compute units” (शायद एक बेहतर नाम) की खपत करते हैं, और प्रत्येक ट्रांज़ैक्शन 200,000 compute units पर सॉफ्ट-कैप्ड (soft-capped) होता है। यदि ट्रांज़ैक्शन की लागत 200,000 compute units से अधिक है, तो यह revert हो जाता है।
एथेरियम में, कंप्यूटिंग के लिए गैस की लागत को स्टोरेज से जुड़ी गैस की लागत के समान ही माना जाता है। सोलाना में, स्टोरेज को अलग तरह से हैंडल किया जाता है, इसलिए सोलाना में परसिस्टेंट डेटा (persistent data) की कीमत निर्धारण चर्चा का एक अलग विषय है।
हालाँकि, रनिंग opcodes की कीमत तय करने के दृष्टिकोण से, एथेरियम और सोलाना समान रूप से व्यवहार करते हैं।
दोनों चेन संकलित (compiled) बायटकोड (bytecode) निष्पादित करते हैं और निष्पादित प्रत्येक निर्देश (instruction) के लिए शुल्क लेते हैं। एथेरियम EVM बायटकोड का उपयोग करता है, लेकिन सोलाना berkeley packet filter का एक संशोधित संस्करण चलाता है जिसे Solana packet filter कहा जाता है।
एथेरियम अलग-अलग opcodes के लिए अलग-अलग कीमतें वसूलता है जो इस बात पर निर्भर करता है कि उन्हें निष्पादित होने में कितना समय लगता है, जो कि एक गैस से लेकर हजारों गैस तक हो सकता है। सोलाना में, प्रत्येक opcode की कीमत एक compute unit होती है।
जब आपके पास पर्याप्त compute units न हों तो क्या करें
जब भारी कम्प्यूटेशनल संचालन (computational operations) किए जा रहे हों जिन्हें सीमा के नीचे नहीं किया जा सकता है, तो पारंपरिक रणनीति “अपने काम को सेव करने (save your work)” और इसे कई ट्रांज़ैक्शन में करने की होती है।
“save your work” वाले हिस्से को स्थायी स्टोरेज (permanent storage) में रखने की आवश्यकता होती है, जिसे हमने अभी तक कवर नहीं किया है। यह एथेरियम में एक विशाल लूप (massive loop) पर इटरेट (iterate) करने की कोशिश करने जैसा ही है; आपके पास उस इंडेक्स के लिए एक स्टोरेज वेरिएबल होगा जहां आपने छोड़ा था, और उस बिंदु तक किए गए कंप्यूटेशन को सेव करने वाला एक स्टोरेज वेरिएबल होगा।
Compute unit ऑप्टिमाइज़ेशन
जैसा कि हम पहले से ही जानते हैं, सोलाना हॉल्टिंग प्रॉब्लम (halting problem) को रोकने और हमेशा चलने वाले कोड को रोकने के लिए compute units का उपयोग करता है। इसमें प्रति ट्रांज़ैक्शन 200,000 CU (कुछ अतिरिक्त लागत पर इसे 1.4m CU तक बढ़ाया जा सकता है) की compute unit सीमा होती है। यदि यह (चुनी गई सीमा) पार हो जाती है, तो प्रोग्राम समाप्त (terminate) हो जाता है, सभी परिवर्तित स्टेट्स revert हो जाती हैं और कॉलर (caller) को शुल्क वापस नहीं किया जाता है। यह उन हमलावरों (attackers) को रोकता है जो नोड्स को धीमा करने या चेन को रोकने के लिए कभी खत्म न होने वाला या कम्प्यूटेशनल रूप से गहन प्रोग्राम चलाने का इरादा रखते हैं।
हालाँकि, EVM चेन्स के विपरीत, किसी ट्रांज़ैक्शन में उपयोग किए जाने वाले कम्प्यूटेशनल रिसोर्सेज उस ट्रांज़ैक्शन के लिए भुगतान किए गए शुल्क को प्रभावित नहीं करते हैं। आपसे वैसे ही शुल्क लिया जाएगा जैसे कि आपने अपनी पूरी सीमा का उपयोग किया हो या उसका बहुत कम उपयोग किया हो। उदाहरण के लिए, एक 400 compute unit ट्रांज़ैक्शन की लागत 200,000 compute unit ट्रांज़ैक्शन के समान ही होती है।
compute units के अलावा, signers for the Solana transaction की संख्या compute unit की लागत को प्रभावित करती है। सोलाना docs से:
“तो अभी, ट्रांज़ैक्शन शुल्क पूरी तरह से उन हस्ताक्षरों (signatures) की संख्या से निर्धारित होते हैं जिन्हें किसी ट्रांज़ैक्शन में सत्यापित (verified) करने की आवश्यकता होती है। किसी ट्रांज़ैक्शन में हस्ताक्षरों की संख्या की एकमात्र सीमा स्वयं ट्रांज़ैक्शन का अधिकतम आकार (max size) है। एक ट्रांज़ैक्शन (अधिकतम 1232 bytes) में प्रत्येक हस्ताक्षर (64 bytes) को एक अद्वितीय सार्वजनिक कुंजी (unique public key) (32 bytes) का संदर्भ देना चाहिए, इसलिए एक ही ट्रांज़ैक्शन में 12 हस्ताक्षर हो सकते हैं (पता नहीं आप ऐसा क्यों करेंगे)।”
हम इस छोटे से उदाहरण के साथ इसे कार्य करते हुए देख सकते हैं। एक खाली सोलाना प्रोग्राम के साथ इस प्रकार शुरुआत करें:
use anchor_lang::prelude::*;
declare_id!("6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC");
#[program]
pub mod compute_unit {
use super::*;
pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
टेस्ट फ़ाइल को अपडेट करें:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ComputeUnit } from "../target/types/compute_unit";
describe("compute_unit", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ComputeUnit as Program<ComputeUnit>;
const defaultKeyPair = new anchor.web3.PublicKey(
// replace this with your default provider keypair, you can get it by running `solana address` in your terminal
"EXJupeVMqDbHk7xY4XP4TVXq22L3ZJxJ9Gm68hJccpLp"
);
it("Is initialized!", async () => {
// log the keypair's initial balance
let bal_before = await program.provider.connection.getBalance(
defaultKeyPair
);
console.log("before:", bal_before);
// call the initialize function of our program
const tx = await program.methods.initialize().rpc();
// log the keypair's balance after
let bal_after = await program.provider.connection.getBalance(
defaultKeyPair
);
console.log("after:", bal_after);
// log the difference
console.log(
"diff:",
BigInt(bal_before.toString()) - BigInt(bal_after.toString())
);
});
});
नोट: JavaScript में, किसी संख्या के अंत में “n” का अर्थ है कि यह एक BigInt है।
रन करें: solana logs यदि आपने इसे पहले से चालू नहीं किया है।
जब हम anchor test --skip-local-validator चलाते हैं तो हमें टेस्ट लॉग और सोलाना वैलिडेटर लॉग के रूप में यह आउटपुट मिलता है:
# test logs
compute_unit
before: 15538436120
after: 15538431120
diff: 5000n
# solana logs
Status: Ok
Log Messages:
Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC invoke [1]
Program log: Instruction: Initialize
Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC consumed 320 of 200000 compute units
Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC success
5000 lamports का बैलेंस अंतर इसलिए है क्योंकि इस ट्रांज़ैक्शन को भेजते समय हमें केवल 1 हस्ताक्षर (हमारे डिफ़ॉल्ट प्रोवाइडर एड्रेस का) की आवश्यकता/उपयोग होता है। यह उस चीज़ के अनुरूप है जिसे हमने ऊपर स्थापित किया है, अर्थात् 1 * 5000 = 5000। यह भी ध्यान दें कि इसमें 320 compute units लगते हैं लेकिन यह राशि हमारे ट्रांज़ैक्शन शुल्क को प्रभावित नहीं करती है।
अब, चलिए अपने प्रोग्राम में कुछ जटिलता जोड़ते हैं और देखते हैं कि क्या होता है:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let mut a = Vec::new();
a.push(1);
a.push(2);
a.push(3);
a.push(4);
a.push(5);
Ok(())
}
निश्चित रूप से, इससे हमारे ट्रांज़ैक्शन शुल्क में कुछ अंतर आना चाहिए, है ना?
जब हम anchor test --skip-local-validator चलाते हैं तो हमें टेस्ट लॉग और सोलाना वैलिडेटर लॉग के रूप में यह आउटपुट मिलता है:
# test logs
compute_unit
before: 15538436120
after: 15538431120
diff: 5000n
# solana logs
Status: Ok
Log Messages:
Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC invoke [1]
Program log: Instruction: Initialize
Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC consumed 593 of 200000 compute units
Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC success
हम देख सकते हैं कि इसमें अधिक compute units लगते हैं, जो हमारे पहले उदाहरण से लगभग दोगुने हैं। लेकिन यह हमारे ट्रांज़ैक्शन शुल्क को प्रभावित नहीं करता है। यह अपेक्षित है और दर्शाता है कि वास्तव में, compute units उपयोगकर्ताओं द्वारा भुगतान किए गए ट्रांज़ैक्शन शुल्क को प्रभावित नहीं करते हैं।
खपत किए गए compute units की परवाह किए बिना, ट्रांज़ैक्शन ने 5000 lamports या 0.000005 SOL का शुल्क लिया।
वापस compute unit पर आते हैं। तो, हम compute units को ऑप्टिमाइज़ (optimize) क्यों करना चाहेंगे जबकि यह ट्रांज़ैक्शन के लिए भुगतान किए गए शुल्क को प्रभावित नहीं करता है?
- पहला, यह केवल अभी के लिए सच है, भविष्य में सोलाना सीमा को उच्च करने का निर्णय ले सकता है और नोड्स को इन जटिल ट्रांज़ैक्शन को साधारण ट्रांज़ैक्शन से अलग न मानने के लिए प्रोत्साहित (incentivize) करना होगा। इसका मतलब होगा कि ट्रांज़ैक्शन शुल्क की गणना करते समय खपत किए गए compute unit पर विचार करना।
- दूसरा, यदि ब्लॉकस्पेस (blockspace) के लिए प्रतिस्पर्धा करने वाली महत्वपूर्ण नेटवर्क गतिविधि है, तो एक छोटे ट्रांज़ैक्शन के ब्लॉक में शामिल होने की अधिक संभावना है।
- तीसरा, यह आपके प्रोग्राम को अन्य प्रोग्राम्स के साथ अधिक कम्पोज़ेबल (composable) बना देगा। यदि कोई अन्य प्रोग्राम आपके प्रोग्राम को कॉल करता है, तो ट्रांज़ैक्शन को अतिरिक्त compute limit नहीं मिलती है। अन्य प्रोग्राम शायद आपके साथ इंटीग्रेट (integrate) नहीं होना चाहेंगे यदि आपका ट्रांज़ैक्शन बहुत अधिक कंप्यूट का उपयोग करता है, जिससे मूल प्रोग्राम के लिए बहुत कम बचता है।
छोटे पूर्णांक (Smaller integers) compute units बचाते हैं
जितने बड़े वैल्यू टाइप (value types) का उपयोग किया जाएगा, उतने ही अधिक compute unit की खपत होगी। जहां लागू हो वहां छोटे प्रकारों (smaller types) का उपयोग करना सबसे अच्छा है। आइए कोड उदाहरण और टिप्पणियों (comments) को देखें:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// this costs 600 CU (type defaults to Vec<i32>)
let mut a = Vec::new();
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
// this costs 618 CU
let mut a: Vec<u64> = Vec::new();
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
// this costs 600 CU (same as the first one but the type was explicitly denoted)
let mut a: Vec<i32> = Vec::new();
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
// this costs 618 CU (takes the same space as u64)
let mut a: Vec<i64> = Vec::new();
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
// this costs 459 CU
let mut a: Vec<u8> = Vec::new();
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
a.push(1);
Ok(())
}
ध्यान दें कि जैसे-जैसे इंटीजर टाइप (integer type) कम होता गया, compute unit की लागत में कमी आती गई। यह अपेक्षित है क्योंकि बड़े प्रकार (larger types) दर्शाए गए मान (value) की परवाह किए बिना छोटे प्रकारों की तुलना में मेमोरी में अधिक जगह लेते हैं।
find_program_address का उपयोग करके ऑन-चेन एक प्रोग्राम डिराइव्ड अकाउंट (PDA) जनरेट करने में अधिक compute units का उपयोग हो सकता है क्योंकि यह विधि create_program_address की कॉल्स पर तब तक इटरेट करती है जब तक कि उसे एक ऐसा PDA न मिल जाए जो ed25519 कर्व (curve) पर न हो। कंप्यूट लागत को कम करने के लिए, ऑफ-चेन find_program_address() का उपयोग करें और जब संभव हो तो परिणामी bump seed को प्रोग्राम में पास करें। इसके बारे में बाद के अनुभाग में अधिक चर्चा की गई है क्योंकि यह इस अनुभाग के दायरे (scope) से बाहर है।
यह कोई विस्तृत सूची नहीं है, बल्कि कुछ बिंदु हैं जो यह विचार देने के लिए हैं कि क्या चीज़ एक प्रोग्राम को दूसरे की तुलना में अधिक कम्प्यूटेशनल रूप से गहन (computationally intensive) बनाती है।
eBPF क्या है?
सोलाना का बायटकोड भारी मात्रा में BPF से लिया गया है। “eBPF” का सीधा सा अर्थ है “extended BPF.” यह अनुभाग Linux के संदर्भ में BPF की व्याख्या करता है।
जैसा कि आप उम्मीद करेंगे, Solana VM रस्ट (Rust) या सी © को नहीं समझता है। इन भाषाओं में लिखे गए प्रोग्राम eBPF (extended Berkeley Packet Filter) में संकलित (compiled) होते हैं।
संक्षेप में, eBPF कर्नेल (kernel) (एक सैंडबॉक्स वातावरण में) के भीतर मनमाने ढंग से eBPF बायटकोड के निष्पादन (execution) की अनुमति देता है जब कर्नेल एक इवेंट (event) उत्सर्जित करता है जिसे eBPF बायटकोड सब्सक्राइब करता है, उदाहरण:
- नेटवर्क (network): सॉकेट खोलना/बंद करना
- डिस्क (disk): राइट/रीड (write/read)
- किसी प्रोसेस (process) का निर्माण
- किसी थ्रेड (thread) का निर्माण
- CPU इंस्ट्रक्शन इनवोकेशन (instruction invocation)
- 64 बिट्स तक सपोर्ट करता है (यही कारण है कि सोलाना में अधिकतम uint प्रकार u64 है)
आप इसे कर्नेल के लिए JavaScript के रूप में सोच सकते हैं। JavaScript ब्राउज़र पर तब क्रियाएं करता है जब कोई इवेंट उत्सर्जित होता है, eBPF कुछ ऐसा ही तब करता है जब कर्नेल के भीतर इवेंट उत्सर्जित होते हैं, उदाहरण के लिए जब कोई syscall निष्पादित होता है।
यह हमें विभिन्न उपयोग के मामलों के लिए प्रोग्राम बनाने की अनुमति देता है उदाहरण के लिए (ऊपर सूचीबद्ध इवेंट्स के आधार पर):
- नेटवर्क: रूट्स (routes) और अन्य का विश्लेषण करने के लिए
- सुरक्षा (security): कुछ नियमों के आधार पर ट्रैफ़िक को फ़िल्टर करना और किसी भी खराब/ब्लॉक किए गए ट्रैफ़िक की रिपोर्ट करना
- ट्रेसिंग और प्रोफाइलिंग (tracing and profiling): यूजरस्पेस (userspace) प्रोग्राम से कर्नेल निर्देशों तक विस्तृत निष्पादन प्रवाह (execution flow) एकत्र करना
- ऑब्जर्वेबिलिटी (observability): कर्नेल गतिविधियों की रिपोर्ट करना और विश्लेषण करना
प्रोग्राम केवल तभी निष्पादित होता है जब हमें इसकी आवश्यकता होती है (अर्थात जब कर्नेल में कोई इवेंट उत्सर्जित होता है)। उदाहरण के लिए, मान लीजिए कि जब किसी फ़ाइल में कुछ लिखा जाता है, तो आप उसका नाम और उसमें लिखा गया डेटा प्राप्त करना चाहते हैं, हम vfs_write() syscall इवेंट को सुनते/पंजीकृत/सब्सक्राइब करते हैं। अब, जब भी उस फ़ाइल में कुछ लिखा जाता है, तो हमारे पास वह डेटा उपलब्ध होता है।
सोलाना बायटकोड फॉर्मेट (SBF)
सोलाना बायटकोड फॉर्मेट (Solana Bytecode Format) कुछ बदलावों के साथ eBPF का एक प्रकार है और जो सबसे अलग है वह है बायटकोड वेरिफायर (bytecode verifier) को हटाना। eBPF में बायटकोड वेरिफायर मौजूद होता है ताकि यह सुनिश्चित किया जा सके कि सभी संभावित निष्पादन पथ (execution paths) परिमित (finite) हैं और निष्पादित करने के लिए सुरक्षित हैं।
सोलाना इसे compute unit limit का उपयोग करके हैंडल करता है। एक कंप्यूट मीटर (compute meter) होने से जो खर्च किए गए कम्प्यूटेशनल रिसोर्सेज को एक कैप (cap) के साथ सीमित करता है, सुरक्षा जाँच (safety checks) को रनटाइम (runtime) में स्थानांतरित कर देता है और आर्बिट्रेरी (arbitrary) मेमोरी एक्सेस, अप्रत्यक्ष जंप (indirect jumps), लूप (loops) और अन्य दिलचस्प व्यवहारों की अनुमति देता है।
बाद के ट्यूटोरियल में, हम एक साधारण प्रोग्राम और उसके बायटकोड में गहराई से उतरेंगे, इसे बदलेंगे (tweak), विभिन्न compute unit लागतों को समझेंगे और ठीक से सीखेंगे कि सोलाना बायटकोड कैसे काम करता है और इसका विश्लेषण कैसे किया जाए।
RareSkills के साथ और जानें
यह ट्यूटोरियल हमारे सोलाना कोर्स (Solana course) का हिस्सा है।
मूल रूप से 23 फरवरी, 2024 को प्रकाशित