سالیدیتی از ایده تا عمل در یک هفته

سلام. در این نوشته می‌خواهم تجربه خودم در چگونگی پیاده‌سازی یک ایده حول قراردادهای هوشمند به زبان سالیدیتی (Solidity) را با شما به اشتراک بگذارم. در این مسیر از OpenZeppelin و Truffle Suite استفاده می‌کنیم و در نهایت قرارداد را روی شبکه آزمایشی اتریوم (Ropsten) می‌بریم تا مقدار گس مصرفی را اندازه بگیریم. یک بلاکچین نسبتاً جدید شبیه اتریوم را هم به نام RSK آزمایش می‌کنیم که کارمزد دلاری تراکنش‌ها درش خیلی مقرون به صرفه‌تر است. نتیجه اینکه این کار، برای کسی که تجربه کافی در برنامه‌نویسی داشته باشد، خیلی سخت نیست و در کمتر از یک هفته جمع می‌شود!

اگر می‌خواهید به صورت گام به گام و عملی با مقدمات اتریوم و قراردادهای هوشمند آشنا شوید، پیشنهاد می‌کنم نوشته دیگر با عنوان آشنایی گام به گام با اتریوم و قراردادهای هوشمند را بخوانید.

لوگوی چیزهایی که استفاده می‌کنیم!
لوگوی چیزهایی که استفاده می‌کنیم!

پیشگفتار

ماجرا از اینجا شروع شد که وقتی نوشتن یکی از مقالاتمان در حوزه کاربردهای بلاکچین و قراردادهای هوشمند به آخرهایش رسید، گفتیم یک برآوردی از هزینه‌هایش داشته باشیم. چون کدی نداشتیم، گفتیم حداقل یک برآورد سرانگشتی بکنیم. برآورد اینطوری شد که متوسط زمان تأیید تراکنش‌ها روی شبکه اتریوم حدود ۱۳.۱۹ ثانیه و متوسط یک ماهه کارمزد تراکنش‌ها طی ۸ ماهه اول ۲۰۲۱ از ۳.۴۸ تا ۲۳.۶۴ دلار بوده است! یعنی باید می‌گفتیم برای یک تراکنش ساده (مثلا به روز رسانی یک متغیر عددی از قرارداد هوشمند)، اینهمه دلار باید خرج کرد! با خودمان گفتیم حتماً اینجا داریم یک اشتباهی می‌کنیم و به احتمال خیلی زیاد ما جزو این میانگینی‌ها نیستیم! این قلقلک اساسی باعث شد دل به دریا بزنیم و اقدام به پیاده‌سازی یک اثبات مفهومی یا proof-of-concept بکنیم (البته مفهومی بودنش را قبول ندارم چون ما کد زدیم!) کل ماجرا چند روزی طول کشید و اگر می‌خواهید بفهمید که ارتباط این تصور ما با واقعیت چطوری بود، در بخش «یک لحظه تأمل» به آن هم می‌رسم!

ما به واسطه مطالبی که من در درس امنیت تجارت الکترونیکی می‌گویم، از لحاظ تئوریک با یکسری مسائل در سالیدیتی آشنا بودیم. یعنی صفرِ صفر نبودیم. مثلا می‌دانستیم که تا جای ممکن باید از نوشتن حلقه و کارهایی که نیاز به پیمایش آرایه و لیست دارد پرهیز کرد. یا بهتر است به جای string در جاهایی که رشته خیلی طولانی نیست از bytes32 (طول ثابت) استفاده کرد. یا اینکه مثلا یک دستور مخصوص ecrecover برای چک کردن امضای دیجیتالی در سالیدیتی هست. اما نمی‌دانستیم دقیقا چطوری یک برنامه کامل داشته باشیم که به قرارداد هوشمند وصل شود و آزمایشش کنیم. در این نوشته سعی می‌کنم ترکیبی از آموزش و انتقال تجربه را برای شما ارائه دهم.

اگر می‌خواهید با خود زبان سالیدیتی بیشتر آشنا شوید، پیشنهاد می‌کنم به صفحه سالیدیتی با مثال از سایت رسمی این زبان مراجعه کنید.

مرحله اول: شروع از راهنمای OpenZeppelin

ما باید در بخشی از کارمان یک توکن ERC-20 و یک توکن ERC-721 درست می‌کردیم. کتابخانه OpenZeppelin کد آماده درست و حسابی برای این کار دارد. لذا پیشنهاد می‌کنم مثل ما از راهنمای کتابخانه OpenZeppelin شروع کنید. دست کم سه تا عنوان اولش را بخوانید و قدم به قدم انجام دهید. در این مرحله یاد می‌گیرید که:

۱. چطور یک پروژه NodeJS بسازید و Truffle Suite را به آن اضافه کنید (برای کار کردن با قراردادهای هوشمند از داخل کد جاوا اسکریپت)

۲. چطور قراردادهای خودتان به زبان سالیدیتی را به پروژه اضافه کنید، آن‌ها را کامپایل کنید، و از کتابخانه‌های OpenZeppelin در آن‌ها استفاده کنید.

۳. چطور یک گره (ماینر) بلاکچین (ganache-cli) را به صورت محلی برای تست سریع بالا بیاورید، قراردادهای کامپایل شده را رویش مستقر کنید، و چطور از داخل کد جاوا اسکریپت تراکنش بزنید و قراردادها را فراخوانی کنید.

اجرا کردن دستور العمل‌های آموزش در حد چند ساعت زمان می‌بَرَد.

مرحله دوم: نوشتن قرارداد هوشمند خودمان به زبان سالیدیتی

با این چیزهایی که یاد گرفتید، باید بروید سراغ نوشتن کد خودتان. داشتن چیزی شبیه شبه کد که جزئیات فرایندها درش مشخص است، می‌تواند کارتان را خیلی جلو بیاندازد! در مورد پروژه ما، برای توکن ERC-20 که فقط چند خط کد constructor باید زده می‌شد و بقیه‌اش را OpenZeppelin انجام داده بود. از اینجا ببینید. برای توکن ERC-721 هم علاوه بر constructor باید یک تابع ضرب توکن نوشته می‌شد که توکن‌های جدید را به تناسب ایجاد کند. کلّیّت روشِ کار اینجا بود ولی ما میخواستیم یک transfer برای توکن ERC-20 درش داشته باشیم و هزینه ضرب توکن را بگیریم. برای آن لازم بود فراخواننده قبل از درخواست ضرب توکن، مقدار مناسب از توکن ERC-20 را برای نقل و انتقال توسط قرارداد ما approve کند؛ همین!

اما اصل کار در یک قرارداد دیگر بود که یکسری اطلاعات را باید نگه می‌داشت و یکسری چیزها را چک می‌کرد و خلاصه منطق قراردادْ-هوشمندیِ ما در این قرارداد بود. یک نکته‌ای که اینجا یاد گرفتیم این بود که برای صدا زدن یک قرارداد دیگر که قبلاً روی شبکه رفته است، راحت‌ترین کار این است که یک interface تعریف کنیم و تعریف دقیق توابعی که می‌خواهیم ازش صدا کنیم را درش بگذاریم. همه این توابع با قید external تعریف می‌شوند و بدنه ندارند. بعد، یک متغیر از نوع همان interfaceی که تعریف کرده‌ایم را در قرارداد خودمان می‌گذاریم و مساوی آدرس شبکهٔ قرارداد مستقر شده می‌گذاریم (یک cast لازم است!) البته در محیط آزمایشی باید هر دو قرارداد را خودمان مستقر کنیم و یک نسخه آزمایشی از قراردادی که اول بهش اشاره کردم داشته باشیم!

مرحله سوم: صدا زدن قراردادها از جاوا اسکریپت

بعد از اینکه کد سالیدیتی را نوشتید، طبق همان آموزش‌های مرحله اول، با جاوا اسکریپت و گره محلی Ganache آزمایشش کنید. ما اینجا چندتا نکته یاد گرفتیم که ذکر می‌کنم:

۱- هر فراخوانی تابعی باید با ذکر await باشد و گرنه بدون اینکه صبر کند جواب معلوم شود می‌رود خط بعدی!مگر اینکه بخواهید به صورت async برنامه بنویسید. اگر اینطور است هم پیشنهاد می‌کنم اول مطمئن شوید که sync کار می‌کند بعد بروید و asyncش کنید.

۲- آدرس‌ها و bytes32ها به صورت مبنای شانزده در یک متغیر رشته‌ای در جاوا اسکریپت ذخیره می‌شوند.

۳- مقدار بازگشتی فراخوانی‌های قراردادهای هوشمند یک مقدار ساده (مثلا عدد صحیح یا رشته) نیست. بلکه کلی اطلاعات همراهش هست (جزئیات تراکنش، خطا در صورت وقوع، logهایی که تولید شده، و صد البته مقدار خروجی تابع فراخوانده شده سالیدیتی). اگر چیزهای اضافه را نمی‌خواهید می‌توانید از toString استفاده کنید یا به نوع مورد نظر cast کنید. در این میان، مقدار بازگشتی، دو تا attribute خوب برای مستندسازی آزمایش‌هایی که انجام می‌دهید دارد: receipt.gasUsed و tx که اولی گس مصرف شده و دومی آدرس تراکنشی را می‌دهد که متعاقب فراخوانی صادر شده است. پیشنهاد می‌کنم این اطلاعات را console.log کنید. در خصوص این‌ها به این مستندات Truffle مراجعه کنید.

۴- برای فراخوانی‌های توابعی که در سالیدیتی pure یا view نیستند، باید تراکنش صادر شود و گس پرداخته شود. Truffle به صورت پیش فرض برآورد خوبی برای مقدار گس مصرفی ندارد و شما به اشتباه فکر می‌کنید باید کلی پولدار باشید تا بتوانید حتی یک تراکنش ساده را بزنید! برای مشخص کردن سقف گس مصرفی و اینکه از چه حسابی کم شود، این آکولاد را به عنوان آرگومان آخر به انتهای آرگومان‌ها اضافه کنید: {from:THEADDRESS, gas:AMOUNTGAS} و به جای عبارت‌های حروف بزرگ، مقدار مناسب را بگذارید.

۵- اگر می‌خواهید توابع pure یا view را صدا بزنید، نیازی به زدن تراکنش و خرج کردن نیست. این توابع را با استفاده از call باید صدا بزنید. ما تا وقتی به آزمایش روی شبکه Ropsten نرسیدیم متوجه نشدیم که داریم این فراخوانی‌ها را اشتباه می‌زنیم و الکی بابتشان اتر می‌پردازیم. مثلاً به جای اینکه mycontract.balanceOf() را استفاده کنید که سعی می‌کند یک تراکنش صادر کند، mycontract.balanceOf.call() را بزنید. راهنمای تکمیلی این دو نکته اخیر در مستندات ذکر شده در مورد (۳) آمده است.

اینجای کار بستگی به خودتان دارد. ممکن است یکی دو روز یا بیشتر طول بکشد!

مرحله چهارم: بردن قراردادها روی شبکه آزمایشی

آزمایش کردن قراردادها با گره محلی خیلی سریع است و یکسری مشکلات اصلی را برایتان مشخص می‌کند. اما برای ارزیابی مقدار گس مصرفی در واقعیت، چندان مناسب نیست. یک سوتی‌هایی(!) هم که مربوط به کدهای جاوا اسکریپت است خودش را روی گره محلی نشان نمی‌دهد. وقتی دیگر از همه چیز مطمئن شدید، باید قرارداد را ببرید روی شبکه آزمایشی. در این مرحله هم راهنمای OpenZeppelin کارگشا است. باید یک حساب آزمایشی هم روی Infura درست کنید که درگاه اتصال برنامه شما به Ropsten است. در این خصوص چند تا نکته هست که ممکن است به دردتان بخورد:

۱- اگر مقدار mnemonic را به کیف پول MetaMask بدهید یا اصلا از mnemonic ساخته شده توسط MetaMask استفاده کنید، می‌توانید راحت‌تر با حساب‌هایتان روی شبکه آزمایشی کار کنید. می‌توانید آدرس قرارداد توکن‌هایی را که ساخته‌اید را هم بهش بدهید تا امکان نمایش موجودی و نقل و انتقال را به شما بدهد.

۲- می‌توانید از سایت Etherscan تراکنش‌ها را ببینید و با قراردادها کار کنید. حواستان باشد که روی شبکه آزمایشی مورد نظرتان تنظیمش کنید و گرنه تراکنش‌های یک شبکه دیگر را می‌بینید! این سایت هم توکن‌های ERC-20 و ERC-721 را تشخیص می‌دهد و نحوه نمایش متفاوتی برای آن‌ها دارد.

۳. ما هر کاری کردیم نتوانستیم از Faucetهایی که اتر روی شبکه Ropsten می‌دهند چیزی بگیریم! قبلاً اینطوری نبود! مجبور شدیم از اکانت‌هایی که قبلاً برای پروژه‌های درس شارژ کرده بودیم استفاده کنیم. لذا بعد که کارتان تمام شد، اتر مانده ته حساب‌هایتان را برای روز مبادا به یک حساب تجمیعی منتقل کنید.

یک لحظه تأمل!

چیزی که ما در اینجا فهمیدیم این بود که استقرار یک قرارداد هوشمند کلّی گس مصرف می‌کند (چندین میلیون!) کارهای دیگر هم همینطور. مثلا یک ERC20.approve حدود ۴۷۰۰۰ گس، یک ERC20.transferFrom حدود ۵۹۰۰۰ گس، و کارهای یک کمی پیچیده‌تر دیگر دست کم ۲۰۰۰۰۰ گس را لازم دارند. روی شبکه آزمایشی همه چیز رایگان است اما هدف ما این بود که هزینه‌ها را روی شبکه اصلی برآورد کنیم. برای فهمیدن اینکه این مقدار گس معادل چند دلار می‌شود، کافی است به سایت Ethgasstation مراجعه کنید. قیمت گس را بسته به اینکه بخواهید تراکنش‌تان چقدر سریع تأیید شود به صورت لحظه‌ای (بر حسب giga Wei یا Gwei) نشان می‌دهد. از این اطلاعات می‌توانید استفاده کنید و مقدار اتری که برای کارمزد تراکنش لازم است را برآورد کنید:

fee = gasUsed * gasPrice * 10^-9

مقدار gasPrice را بر حسب همان Gwei در این فرمول بگذارید. اگر عدد حاصل را ضربدر قیمت اتر بکنید، کارمزد دلاری (یا ریالی) تراکنش معلوم می‌شود. با این اوصاف، این روزها یک انتقال ساده توکن روی شبکه اصلی اتریوم باید حدود ۳۰ دلار آب بخورد! یا قرارداد ما به ۳.۶۴۶ میلیون گس و به عبارت دیگر ۱۸۶۸.۴۰ دلار فقط برای استقرار احتیاج دارد! کارهای دیگر هم از حدود ۸۷ دلار به بالا در می‌آید! اینجا بود که فهمیدیم ما نه تنها جزو میانگینی‌های مورد اشاره در «پیشگفتار» نوشته نیستیم، بلکه از آن‌ها لاکچری‌تر هم هستیم!

دستمان را از پوست گردو در بیاوریم!

ما اگر از اول می‌دانستیم که خرج زندگی در دنیای اتریوم اینقدر گران است، اصلا به یک جای دیگر مهاجرت می‌کردیم! فرهنگ آن جاهای دیگر مثل Cardano یا Tezos یا EOS با اتریوم فرق می‌کند و قراردادهایشان هم با زبان‌های دیگری هم به غیر سالیدیتی نوشته می‌شوند! الان ما بومی اتریوم شده‌ایم و آنجا برایمان دیار غربت است! شبی که به این نتایج رسیدیم، من به سختی خوابیدم از بس که با موبایل در رختخواب جستجو می‌زدم :-(

صبح با ذهن آزاد شروع کردم و اتفاقا به یک شبکه‌ای به نام RSK برخوردم که ظاهرا یک زمانی مخفف Rootstock بوده است. خیلی در جزئیاتش وارد نشدم ولی همین بس که یک بلاکچینی است با پشتیبانی از قراردادهای هوشمند به زبان سالیدیتی و تا حد زیادی هم با اتریوم سازگار است. اما یک جورهایی خودش را به بلاکچین بیتکوین چسبانده و ادعا می‌کند امنیت بیتکوین و قراردادهای هوشمند اتریوم را در کنار هم دارد. یک مزیت مهمش برای ما این است که (فعلاً؟) کارمزد دلاری تراکنش‌ها روی RSK خیلی ارزان‌تر است. ما که می‌خواستیم مقاله‌مان را بنویسیم و دستمان هم در پوست گردو مانده بود. لذا بلافاصله اقدام به مهاجرت به RSK کردیم!

مهاجرت به RSK

این کار خیلی ساده است! یک پروژه نمونه در اینجا با توضیحات روی سایت Truffle Suite هست. با تجربه‌ای که تا اینجا داشته‌اید، خیلی سریع می‌توانید پروژه را آزمایش کنید و مطمئن شوید که همه چیز کار می‌کند. بعد با تغییر در truffle-config مهاجرت انجام می‌شود. همین! تجربه ما روی RSK اینطوری بود:

۱- گرفتن اتر آزمایشی و اجرا کردن تراکنش‌ها سریع و راحت است و مستقیما با اتصال به آدرسی که RSK مشخص کرده است انجام می‌شود.

۲- یک کیف پول به نام Nifty دارند که برای کار با شبکه ‌RSK تنظیم شده است.

۳- نحوه ساختن آدرس‌ها در RSK با اتریوم متفاوت است. همچنین ظاهراً اینکه آدرس مربوط به چه شبکه‌ای هست هم درش ذکر می‌شود. لذا شما نمی‌توانید با Nifty لزوماً به آدرس‌هایی که طبق استاندارد اتریوم ساخته شده‌اند اتر RSK منتقل کنید (جلوی یکسری اشتباه‌ها گرفته می‌شود.)

دستاورد مهاجرت!

ما دقیقاً همان قراردادها را روی RSK بردیم و چیزی که فهمیدیم این بود که در این شبکه گس لازم برای استقرار قراردادمان حدود ۲۲ درصد بیشتر از Ropsten بود. اما همه تراکنش‌ها گس کمتر یا مساوی مصرف می‌کردند. خبر خوش اینکه قیمت گس در این شبکه خیلی ارزان‌تر از اتریوم بود. به طور دقیق، این روزها قیمت گس RSK حدوداً ۰.۰۰۶۳ برابر یا ۰.۶۳ درصد گس اتریوم است. لذا علی رغم بیشتر بودن گس لازم برای استقرار، قرارداد ما روی RSK با کمی بیش از ۱۴ دلار مستقر می‌شد؛ انتقال توکن فقط ۱۷ سنت هزینه داشت؛ و کارهای دیگر هم زیر یک دلار ترتیب داده می‌شد. البته اشکال این شبکه این است که طبق آمار لحظه‌ای رسمی RSK، فاصله زمانی تأیید بلوک‌ها ۳۱.۵ ثانیه است و تعداد گره‌های شبکه‌شان هم خیلی کم، یعنی ۲۶ عدد است. این را با ۱۳.۲ ثانیه اتریوم و ۳,۶۸۵ گره عضو آن مقایسه کنید.

اینجا یک نفس راحت کشیدیم. نتایج را بردیم در مقاله، و ارسالش کردیم! هورا!

جمع‌بندی

ما دیگر از اینجا جلوتر نرفتیم که خدمتتان عرض کنم. کلی کارهای دیگر از جمله آزمایش‌های خودکار، آزمون امنیتی و غیره هست که در مورد یک پروژه واقعی روی بلاکچین باید انجام شود که به آن‌ها نپرداختم. Truffle و OpenZeppelin هم کلی مطلب برای خودشان دارند که ما به آن‌ها وارد نشدیم. اما تجربه این بود که در فقط چند روز می‌شود از ایده به عمل رسید! اگر قصد مهاجرت به جاهای خیلی دور مثل Cardano یا امثالهم را هم ندارید، همین همسایگی هم جاهای خوش آب و هوا و ارزان هست! خوش بگذرد!

خوشحال می‌شوم نظراتتان را در مورد این نوشته با من هم در میان بگذارید!