در این مقاله درباره ماشین حالت (State Machine) به عنوان روشی آسان و مناسب برای اعمال گردش کار در سالیدیتی (Solidity)، زبان برنامهنویسی قراردادهای هوشمند در بلاک چین اتریوم صحبت خواهیم کرد.
ماشین حالت (State Machine) همواره در یک حالت قرار دارد و با تغییر ورودی یا خروجی بین حالتهای مختلف انتقال مییابد؛ در خصوص قراردادهای هوشمند در سالیدیتی ، پیامی از حسابی دیگر به تابع قرارداد ارسال میشود تا تغییری در حالت ایجاد کند. اگر ورودی برای حالت کنونی معتبر باشد، ماشین حالت به حالت جدید انتقال خواهد یافت.
پیشزمینهای درباره زبان برنامه نویسی سالیدیتی
طی توسعه و آزمایش الگوهای قرارداد هوشمند دسترسی به اطلاعات (S-DAC) سالیدیتی ، اغلب از ماشینهای حالت برای در بر گرفتن گردش کار استفاده میکنیم.
در مثالی که در این مقاله بیان میشود، گردش کار بین دو طرف است که یک طرف سوالاتی میپرسد و طرف دیگر پاسخ میدهد.
نمودار ماشین حالت UML
نمودار ماشین حالت UML در مثال ما، به شرح زیر است:
مستطیلها بیانگر حالتها، فلشها بیانگر انتقالها و نوشته روی فلشها نیز رویدادهای فعالکننده (از جانب منابع مشخص) است که انتقال را امکانپذیر میسازند.
مدل ماشین حالت سالیدیتی
ماشین حالت با توابع انتقال بین حالتهای تعریف شده در قرارداد جابجا میشود. بخشی از قرارداد توسعهیافته سالیدیتی به شرح زیر است:
میتوان به راحتی مشاهده کرد که حالت کنونی (که توسط اصلاح کننده تابع onlyState بررسی شده است تا اطمینان حاصل شود که انتقال حالت فقط در حالت کنونی انجام میشود)، حالتهای جدید و توابع انتقال مستقیما به نمودار ماشین حالت وارد میشوند.
به طرفی که اطلاعات و دادهها را در اختیار دارد “دارنده اطلاعات (Data Owner)” میگوییم. دارنده اطلاعات میتواند با دارنده قرارداد متفاوت باشد.
به طرفی که علاقهمند به استفاده از اطلاعات است “درخواستکننده اطلاعات (Data Requester)” میگوییم.
قراردادهای دارای پشتیبانی
از آنجایی که این نقشها در قراردادها رایج هستند، طی توسعه قرارداد میتوانیم از کلاسهای پایه برای این نقشها و سایر نقشهای رایج در دامنه (Domain) خود استفاده کنیم. این روش میتواند برای آزمایش کد قابل قبول باشد، اما برای تولید کد، آن را توصیه نمیکنیم.
کلاس پایه DataOwner شامل اقدامات کلی دارنده اطلاعات نظیر سازنده (Constructor) و اصلاح کننده تابع (onlyDataOwner) به صورت زیر است:
سایر توابع نظیر changeDataOwner نیز ممکن است به منظور کمک به توسعه سریعتر قراردادهای آزمایشی ارائه شوند، اما در این مثال به هیچ کدام از آنها نیازی نیست.
کلاس پایه DataRequester و سایر کلاسهای پایه مشابه هستند.
میتوانیم اصلاح کنندههای تابع را از کلاسهای پایه به قرارداد اضافه کنیم:
کلاسهای پایه DataOwner و DataRequester باید در ابتدای سازنده قرارداد قرار بگیرند. حساب (یا همان آدرس) هر کدام از طرفین یا هر دو طرف میتوانند به عنوان مولفه قرارداد وارد شوند. اگر حساب شامل مولفهای نباشد (مقدار آن صفر باشد)، حساب msg.sender (یعنی همان دارنده قرارداد) مورد استفاده قرار میگیرد. ما از تابع getAccount برای کمک به خوانا کردن سازنده فوق استفاده کرده ایم.
منطقی نیست که DataOwner و DataRequester یک حساب باشند، بنابراین باید این شرایط را نیز در کد سازنده بررسی کنیم:
ذخیرهسازی اطلاعات در بلاک چین
ما باید سوالات درخواستکننده اطلاعات را ذخیره کنیم، در حالی که منتظر دارنده اطلاعات باشیم تا سوالات را دریافت کند. همچنین باید پاسخ دارنده اطلاعات را ذخیره کنیم، در حالی که منتظر درخواستکننده اطلاعات باشیم تا پاسخ را دریافت کند. در این مثال، ما هم سوال و هم پاسخ را در یک رشته اطلاعات مشترک در قرارداد ذخیره میکنیم:
میتوانیم از یک رشته اطلاعات مشترک استفاده کنیم؛ زیرا ماشین حالت اطمینان حاصل میکند که در هر لحظه فقط به یک استفاده از رشته (سوال یا پاسخ) نیاز است. همچنین به استفاده حداقلی از فضای ذخیرهسازی ترغیب میشویم زیرا فضای ذخیرهسازی گران است.
ماشین حالت سلسله مراتبی
یکی از مسائل موجود در راهکار کنونی این است که به هیچوجه نمیتوان قرارداد را فسخ کرد و سرمایهها را به دارنده قرارداد برگرداند.
با فرض بر این که این مورد را در هر زمانی میتوان انجام داد، این راهکار را میتوان پس از طراحی ماشین حالت سلسله مراتبی پیاده سازی کرد:
این مورد با وراثت (inheritance) از کلاس پایه ContractOwner به آسانی در قرارداد پیادهسازی میشود و به صورت خودکار دارنده قرارداد را ثبت میکند و اصلاح کننده onlyContractOwner را ارائه میدهد:
تابع ()selfdestruct سالیدیتی ، اتر را از طریق فسخ تابع به حساب مورد نظر برمیگرداند.
آزمایش قرارداد
به منظور آزمایش قرارداد، میتوانیم آن را در بلاک چین اجرا کنیم. در این مورد، اجرا در شبکه آزمایشی ایده خوبی است. هرچند، سازنده قرارداد دارای ۲ پارامتر است: حساب DataOwner و حساب DataRequester.
برای تسهیل آزمایش به صورت خودکار، میتوانیم حسابهای پروکسی (proxy یا همان واسطه) ایجاد کنیم که اقدامات DataOwner و DataRequester را انجام میدهند. مثالی از حساب پروکسی DataOwner به شرح زیر است:
قرارداد پروکسی DataRequester نیز مشابه است. این قرارداد، توابع setQuestion و getAnswer را ارائه میدهد.
خود قرارداد آزمایش، حسابهای پروکسی و سپس قرارداد اصلی را ایجاد میکند:
مثال فوق، تابع عمومی TestQnA را ارائه میدهد که سوال مورد نظر را از طریق proxyDataRequester به قرارداد اصلی ارسال میکند. سپس سوال را از proxyDataOwner دریافت و تایید میکند که این سوال معتبر است. اقدامات مشابهی نیز در سمت دیگر رخ میدهد، به طوری که پاسخ مورد نظر از طریق proxyDataOwner ارسال میشود، سپس از proxyDataRequester دریافت و بررسی میشود.
سایر آزمایشها برای اطمینان از رفتار مورد انتظار ماشین حالت توصیه میشود.
مصرف گس
مصرف گس برای عمل مستمر پرسیدن سوال ۲۰ کاراکتری و بلافاصله دریافت پاسخ ۲۰ کاراکتری تقریبا ۸۵,۰۰۰ گس در هر دوره است:
مصرف گس در بار اول بیشتر است؛ زیرا فضای اختصاص یافته برای رشته اطلاعات در فضای ذخیرهسازی قرار دارد. پاسخهای بعدی صرفا رشته اطلاعات قبلی را بهروزرسانی میکند.
مصرف گس همچنین به طول رشته نیز بستگی دارد.
یک رشتهای که بدون اطلاعات نیست، گس بیشتری نسبت به رشته بدون اطلاعات مصرف میکند، زیرا آدرس رشته نیز همانند کاراکترهای رشته باید ذخیره شود.
راهکار جایگزین استفاده کننده از رویدادها
اگر تصمیم بگیرید که قرارداد اصلی شما هرگاه که سوال یا پاسخی دریافت میکند به حذف رویدادها بپردازد، ماشین حالت قابل سادهسازی است. برای مثال:
هرچند، رویدادها فقط توسط کد برنامه غیرمتمرکز قابل مدیریت و کنترل هستند، نه توسط یک قرارداد دیگر. قرارداد اصلی نیز حداقل به اصلاحات زیر نیاز پیدا خواهد کرد:
مشکلات ماشین حالت
بزرگترین مشکل ماشینهای حالت را میتوان هم بزرگترین قوت و هم بزرگترین نقطه ضعف آنها خواند که در این مثال نشان داده شده است. گردش کار باید همانند طراحی باشد. هیچ انحرافی امکانپذیر نیست.
در این مورد، اگر درخواستکننده اطلاعات بخواهد سوال دوم را بپرسد، تا زمانی که سوال اول پاسخ داده نشود نمیتواند ادامه دهد. این موضوع در عمل میتواند بسیار دست و پاگیر باشد و قرارداد را تا آستانه بلااستفاده بودن پیش ببرد.
روشهای مختلفی برای حل این مشکل وجود دارد. برای مثال اجرای چندین قرارداد با اجرای صف سوالات و پاسخها. اما نقص اصلی در طراحی اساسی این ماشین حالت خاص نهفته است. طراحی بهتر در این مورد میتواند حالت تعاملی دوطرفه باشد که طی آن، سوالات و پاسخها آزادانه از یک نقش به نقش دیگر در جریان هستند.
نتیجهگیری
ماشینهای حالت یک روش ایدهآل برای کنترل گردش کار در قراردادهای سالیدیتی هستند.
آزمایش ماشینهای حالت آسان است و این موضوع برای ماشینهای حالت سالیدیتی نیز صادق است.
ماشین حالت سلسله مراتبی باید طوری طراحی شود که از کنترل مناسب توابع مختلف نظیر متوقف ساختن ماشین حالت اطمینان حاصل شود.
اگر قراردادها مستقیما توسط افراد مورد استفاده قرار بگیرند، ماشین حالت باید به دقت طراحی شود.
سالیدیتی ویژگی اصلاح کننده تابع مفیدی ارائه میدهد که برای روشی ایدهآل برای پیادهسازی مناسب بررسی حالت کنونی قبل از تغییر حالت ماشین حالت است.
مصرف گس ماشین حالت در مقایسه با هزینه ذخیرهسازی اطلاعات بسیار کم است. شاید ذخیرهسازی برون زنجیرهای اطلاعات هزینه کمتری داشته باشد.