التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟


التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟

مرحباً أيها المبرمجون! في هذا الدرس الاحترافي، سنتعمق في فهم تكاليف الغاز (Gas Fees) في البلوكتشين. سنتعلم كيف يتم حساب هذه التكاليف عملياً عند تنفيذ العقود الذكية وسنرى ذلك بأمثلة برمجية حية.

1. فهم مفهوم الغاز (Gas) في البلوكتشين

الغاز هو وحدة قياس للجهد الحسابي المطلوب لتنفيذ عملية معينة على شبكة البلوكتشين (مثل الإيثيريوم). كل عملية، سواء كانت نقل عملات بسيطة أو تنفيذ دالة معقدة في عقد ذكي، تستهلك كمية معينة من الغاز. هذه الكمية ثابتة لنوع معين من العمليات.

ملاحظة تقنية: التكلفة الإجمالية للمعاملة تُحسب بالصيغة التالية: التكلفة الإجمالية = كمية الغاز المستخدمة (Gas Used) * سعر الغاز (Gas Price). يُدفع سعر الغاز بوحدة Gwei (جزء من الإيثر)، بينما تُدفع التكلفة الإجمالية بالإيثر.

يحدد المستخدم أيضاً حد الغاز (Gas Limit)، وهو أقصى كمية غاز مستعد لدفعها للمعاملة. إذا تجاوزت المعاملة هذا الحد، يتم إلغاؤها وتفقد رسوم الغاز المدفوعة حتى تلك النقطة.

2. تصميم عقد ذكي بسيط في Solidity لحساب الغاز

لنقم بإنشاء عقد ذكي بسيط في Solidity يسمح لنا بتخزين قيمة وزيادة عداد. هذا سيوضح كيف تختلف تكاليف الغاز بناءً على نوع العملية (كتابة على التخزين، تحديث حالة).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GasCalculator {
    uint256 public storedValue; // متغير لتخزين قيمة
    uint256 public counter;     // متغير لزيادة عداد

    event ValueStored(uint256 _value, address indexed _sender);
    event CounterIncremented(uint256 newCounter, address indexed _sender);

    constructor() {
        storedValue = 0;
        counter = 0;
    }

    // دالة لتخزين قيمة جديدة في متغير الحالة
    // هذه العملية تتطلب غازاً لتغيير حالة البلوكتشين
    function storeValue(uint255 _newValue) public {
        storedValue = _newValue;
        emit ValueStored(_newValue, msg.sender);
    }

    // دالة لزيادة العداد
    // هذه العملية أيضاً تتطلب غازاً لتغيير حالة البلوكتشين
    function incrementCounter() public {
        counter++;
        emit CounterIncremented(counter, msg.sender);
    }

    // دالة للقراءة فقط (view)
    // هذه العمليات لا تغير حالة البلوكتشين وبالتالي تستهلك غازاً قليلاً جداً (أو مجاناً عند استدعائها من خارج العقد)
    function getStoredValue() public view returns (uint256) {
        return storedValue;
    }

    // دالة للقراءة فقط (pure)
    // هذه العمليات لا تقرأ ولا تكتب على حالة البلوكتشين، وبالتالي لا تستهلك غازاً عند استدعائها من خارج العقد
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }
}
ملاحظة تقنية: دالات view و pure لا تستهلك غازاً عند استدعائها من حساب خارجي (مثل محفظة أو سكريبت Ethers.js) لأنها لا تتطلب إجماع الشبكة لتغيير الحالة. ومع ذلك، إذا تم استدعاؤها من داخل عقد آخر، فإنها تستهلك غازاً.

3. التفاعل مع العقد وحساب التكاليف باستخدام Ethers.js

الآن، سنستخدم Ethers.js للتفاعل مع العقد الذكي الذي أنشأناه. سنقوم بنشره على شبكة تطوير محلية (مثل Hardhat Network) ثم نستدعي دواله ونراقب تكاليف الغاز. تأكد من تثبيت Hardhat و Ethers.js في مشروعك (npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers).

أولاً: نشر العقد

// script.js
const { ethers } = require("hardhat");

async function deployContract() {
    const GasCalculatorFactory = await ethers.getContractFactory("GasCalculator");
    const gasCalculator = await GasCalculatorFactory.deploy(); // نشر العقد
    await gasCalculator.deployed(); // انتظار تأكيد النشر

    console.log("Contract deployed to:", gasCalculator.address);
    return gasCalculator;
}

// deployContract(); // يمكن استدعائها مباشرة للنشر

ثانياً: استدعاء الدوال وتقدير الغاز

سنقوم الآن بكتابة دالة تستدعي دوال العقد وتظهر لنا كيف يمكن تقدير الغاز المستخدم.

// script.js (تابع)
async function interactWithContract(gasCalculator) {
    const [deployer] = await ethers.getSigners(); // الحصول على الحساب الناشر

    console.log("\n--- استدعاء دالة 'storeValue' ---");
    // تقدير الغاز قبل إرسال المعاملة
    const estimateStoreGas = await gasCalculator.estimateGas.storeValue(123);
    console.log("تقدير الغاز لتخزين قيمة (estimateGas):", estimateStoreGas.toString());

    // إرسال المعاملة
    const txStore = await gasCalculator.storeValue(123);
    const receiptStore = await txStore.wait(); // انتظار تأكيد المعاملة

    console.log("تم تخزين القيمة 123 بنجاح.");
    console.log("الغاز الفعلي المستخدم (gasUsed):", receiptStore.gasUsed.toString());
    console.log("سعر الغاز (gasPrice):", receiptStore.effectiveGasPrice.toString(), "wei");
    console.log("التكلفة الإجمالية (Ether):", ethers.utils.formatEther(receiptStore.gasUsed.mul(receiptStore.effectiveGasPrice)));

    console.log("\n--- استدعاء دالة 'incrementCounter' ---");
    // تقدير الغاز قبل إرسال المعاملة
    const estimateIncrementGas = await gasCalculator.estimateGas.incrementCounter();
    console.log("تقدير الغاز لزيادة العداد (estimateGas):", estimateIncrementGas.toString());

    // إرسال المعاملة
    const txIncrement = await gasCalculator.incrementCounter();
    const receiptIncrement = await txIncrement.wait(); // انتظار تأكيد المعاملة

    console.log("تم زيادة العداد بنجاح.");
    console.log("الغاز الفعلي المستخدم (gasUsed):", receiptIncrement.gasUsed.toString());
    console.log("سعر الغاز (gasPrice):", receiptIncrement.effectiveGasPrice.toString(), "wei");
    console.log("التكلفة الإجمالية (Ether):", ethers.utils.formatEther(receiptIncrement.gasUsed.mul(receiptIncrement.effectiveGasPrice)));

    console.log("\n--- استدعاء دالة 'getStoredValue' (View Function) ---");
    // دالات View لا تكلف غازاً عند استدعائها مباشرة لأنها لا تغير الحالة
    const stored = await gasCalculator.getStoredValue();
    console.log("القيمة المخزنة (getStoredValue):", stored.toString());
    // لا يوجد gasUsed أو تكلفة لأنها استدعاء محلي
    console.log("ملاحظة: دالات 'view' لا تستهلك غازاً عند استدعائها من خارج العقد.");

    console.log("\n--- استدعاء دالة 'add' (Pure Function) ---");
    const sum = await gasCalculator.add(5, 7);
    console.log("نتيجة دالة 'add':", sum.toString());
    console.log("ملاحظة: دالات 'pure' لا تستهلك غازاً عند استدعائها من خارج العقد.");
}

// دالة رئيسية لتشغيل السيناريو
async function main() {
    const gasCalculator = await deployContract();
    await interactWithContract(gasCalculator);
}

// تشغيل الدالة الرئيسية
main().catch((error) => {
    console.error(error);
    process.exit(1);
});

4. تحليل تكاليف الغاز وتأثيرها

من خلال المخرجات، سنلاحظ أن:

  • estimateGas يعطي تقديراً لكمية الغاز المطلوبة لتنفيذ المعاملة بنجاح. هذا التقدير مفيد للمستخدمين لتحديد gasLimit.
  • gasUsed هو الكمية الفعلية للغاز التي استهلكتها المعاملة بعد تنفيذها. يمكن أن تكون أقل من gasLimit إذا كانت المعاملة تتطلب غازاً أقل من الحد الأقصى المسموح به.
  • effectiveGasPrice هو سعر الغاز الفعلي الذي تم دفعه. قد يختلف عن gasPrice الذي حدده المستخدم إذا كانت الشبكة مزدحمة أو إذا تم استخدام EIP-1559.
  • العمليات التي تغير حالة البلوكتشين (مثل storeValue و incrementCounter) تستهلك غازاً، بينما العمليات التي تقرأ الحالة فقط (view) أو لا تتفاعل معها إطلاقاً (pure) لا تستهلك غازاً عند استدعائها من خارج العقد.
ملاحظة تقنية: عمليات تخزين البيانات الجديدة (SSTORE من صفر إلى قيمة) تكلف غازاً أكثر بكثير من تحديث البيانات الموجودة (SSTORE من قيمة إلى قيمة أخرى). كذلك، حذف البيانات (SSTORE من قيمة إلى صفر) يمكن أن يعيد جزءاً من الغاز كحافز.

الكود النهائي الكامل

هذا هو الكود الكامل للعقد الذكي وسكريبت التفاعل.

عقد Solidity (contracts/GasCalculator.sol)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GasCalculator {
    uint256 public storedValue; // متغير لتخزين قيمة
    uint256 public counter;     // متغير لزيادة عداد

    event ValueStored(uint256 _value, address indexed _sender);
    event CounterIncremented(uint256 newCounter, address indexed _sender);

    constructor() {
        storedValue = 0;
        counter = 0;
    }

    // دالة لتخزين قيمة جديدة في متغير الحالة
    // هذه العملية تتطلب غازاً لتغيير حالة البلوكتشين
    function storeValue(uint256 _newValue) public {
        storedValue = _newValue;
        emit ValueStored(_newValue, msg.sender);
    }

    // دالة لزيادة العداد
    // هذه العملية أيضاً تتطلب غازاً لتغيير حالة البلوكتشين
    function incrementCounter() public {
        counter++;
        emit CounterIncremented(counter, msg.sender);
    }

    // دالة للقراءة فقط (view)
    // هذه العمليات لا تغير حالة البلوكتشين وبالتالي تستهلك غازاً قليلاً جداً (أو مجاناً عند استدعائها من خارج العقد)
    function getStoredValue() public view returns (uint256) {
        return storedValue;
    }

    // دالة للقراءة فقط (pure)
    // هذه العمليات لا تقرأ ولا تكتب على حالة البلوكتشين، وبالتالي لا تستهلك غازاً عند استدعائها من خارج العقد
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }
}

سكريبت JavaScript (scripts/deploy-and-interact.js)

const { ethers } = require("hardhat");

async function deployContract() {
    const GasCalculatorFactory = await ethers.getContractFactory("GasCalculator");
    const gasCalculator = await GasCalculatorFactory.deploy();
    await gasCalculator.deployed();

    console.log("Contract deployed to:", gasCalculator.address);
    return gasCalculator;
}

async function interactWithContract(gasCalculator) {
    const [deployer] = await ethers.getSigners();

    console.log("\n--- استدعاء دالة 'storeValue' ---");
    const estimateStoreGas = await gasCalculator.estimateGas.storeValue(123);
    console.log("تقدير الغاز لتخزين قيمة (estimateGas):", estimateStoreGas.toString());

    const txStore = await gasCalculator.storeValue(123);
    const receiptStore = await txStore.wait();

    console.log("تم تخزين القيمة 123 بنجاح.");
    console.log("الغاز الفعلي المستخدم (gasUsed):", receiptStore.gasUsed.toString());
    console.log("سعر الغاز (gasPrice):", receiptStore.effectiveGasPrice.toString(), "wei");
    console.log("التكلفة الإجمالية (Ether):", ethers.utils.formatEther(receiptStore.gasUsed.mul(receiptStore.effectiveGasPrice)));

    console.log("\n--- استدعاء دالة 'incrementCounter' ---");
    const estimateIncrementGas = await gasCalculator.estimateGas.incrementCounter();
    console.log("تقدير الغاز لزيادة العداد (estimateGas):", estimateIncrementGas.toString());

    const txIncrement = await gasCalculator.incrementCounter();
    const receiptIncrement = await txIncrement.wait();

    console.log("تم زيادة العداد بنجاح.");
    console.log("الغاز الفعلي المستخدم (gasUsed):", receiptIncrement.gasUsed.toString());
    console.log("سعر الغاز (gasPrice):", receiptIncrement.effectiveGasPrice.toString(), "wei");
    console.log("التكلفة الإجمالية (Ether):", ethers.utils.formatEther(receiptIncrement.gasUsed.mul(receiptIncrement.effectiveGasPrice)));

    console.log("\n--- استدعاء دالة 'getStoredValue' (View Function) ---");
    const stored = await gasCalculator.getStoredValue();
    console.log("القيمة المخزنة (getStoredValue):", stored.toString());
    console.log("ملاحظة: دالات 'view' لا تستهلك غازاً عند استدعائها من خارج العقد.");

    console.log("\n--- استدعاء دالة 'add' (Pure Function) ---");
    const sum = await gasCalculator.add(5, 7);
    console.log("نتيجة دالة 'add':", sum.toString());
    console.log("ملاحظة: دالات 'pure' لا تستهلك غازاً عند استدعائها من خارج العقد.");
}

async function main() {
    const gasCalculator = await deployContract();
    await interactWithContract(gasCalculator);
}

main().catch((error) => {
    console.error(error);
    process.exit(1);
});

النتيجة المتوقعة

عند تشغيل السكريبت (مثلاً باستخدام npx hardhat run scripts/deploy-and-interact.js --network localhost بعد تشغيل npx hardhat node في طرفية أخرى)، ستشاهد مخرجات مشابهة لما يلي في الطرفية:

Contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

--- استدعاء دالة 'storeValue' ---
تقدير الغاز لتخزين قيمة (estimateGas): 42095
تم تخزين القيمة 123 بنجاح.
الغاز الفعلي المستخدم (gasUsed): 42095
سعر الغاز (gasPrice): 875000000 wei
التكلفة الإجمالية (Ether): 0.000036833125

--- استدعاء دالة 'incrementCounter' ---
تقدير الغاز لزيادة العداد (estimateGas): 21544
تم زيادة العداد بنجاح.
الغاز الفعلي المستخدم (gasUsed): 21544
سعر الغاز (gasPrice): 875000000 wei
التكلفة الإجمالية (Ether): 0.000018856

--- استدعاء دالة 'getStoredValue' (View Function) ---
القيمة المخزنة (getStoredValue): 123
ملاحظة: دالات 'view' لا تستهلك غازاً عند استدعائها من خارج العقد.

--- استدعاء دالة 'add' (Pure Function) ---
نتيجة دالة 'add': 12
ملاحظة: دالات 'pure' لا تستهلك غازاً عند استدعائها من خارج العقد.

لاحظ أن قيم estimateGas و gasUsed و gasPrice قد تختلف قليلاً بناءً على إصدار Hardhat أو Ethers.js أو حتى حالة الشبكة المحلية. الأهم هو فهم كيفية حساب هذه القيم وتأثيرها على التكلفة الإجمالية للمعاملة.

بهذا نكون قد غطينا الجوانب الأساسية لتكاليف الغاز وكيفية التفاعل معها برمجياً. نأمل أن يكون هذا الدرس قد أضاف لكم الكثير!