جداسازی لایه‌ی داده از لایه‌ی منطق در قراردادهای هوشمند اتریوم

اورکا!

حدود سه هفته بود که به عنوان یک مسئله‌ی جانبی درگیر حل جدا کردن کامل لایه‌ی Data از لایه‌ی Logic در زبان سالیدیتی بودم. امروز راه حل آن را یافتم و می‌خواهم آن را با شما به اشتراک بگذارم:

من قرارداد هوشمندی به نام Data می‌سازم. در این قرار داد هوشمند متغیرهای ذخیره‌ی داده‌ها را قرار می‌دهم، مثلا یک استراکت و یک مپینگ برای ذخیره‌ی داده از نوع آن استراکت:

    contract Data {
        struct User {
            string name;
            uint code;
        }
        mapping(uint => User) users;
        uint usersCount = 0;
    }

قرارداد هوشمند دیگری به نام Logic می‌سازم برای ذخیره سازی و بازیابی و گزارشگیری اطلاعات از قرارداد هوشمند Data. واضح است که اگر قرارداد هوشمند Logic بخواهد به داده‌های قرارداد هوشمند Data دسترسی داشته باشد، باید از آن ارث‌بری کند:

    contract Logic is Data {
        function regUser(string memory name, uint code) public {
            usersCount++;
            users[usersCount].name = name;
            users[usersCount].code = code;
        }
        function getUser(uint userId) public view returns(string memory, uint) {        
            return(users[userId].name, users[userId].code);
        }
    }

با توجه به این که کاربر به طور مستقیم باید با قرارداد Data در تماس باشد، حالا اگر بخواهیم قرارداد هوشمند Data بتواند توابع موجود در Logic را صدا کند باید آدرس آن را در خود ذخیره کند و بتوانیم آدرس آن را در صورت لزوم تغییر دهیم:

    contract Storage {
        struct User {
            string name;
            uint code;
        }
        mapping(uint => User) users;
        uint usersCount = 0;
        address logicAddress;
        Logic logic;
        function setLogicAddress(address _logicAddress) public {
            logicAddress = _logicAddress;
            logic = Logic(logicAddress);
        }
        function regUser(string memory name, uint code) public {
            logic.regUser(name, code);
        }
        function getUser(uint userId) public view returns(string memory, uint) {
            return(logic.getUser(userId));
        }
    }

در واقع وقتی در این جا مثلا تابع getUser از قرارداد Data را صدا می‌زنیم، در پشت صحنه همان تابع از قرارداد هوشمند Logic صدا زده می‌شود. اما مشکل اصلی این جا این است که اطلاعات نیز در همان قرارداد هوشمند Logic ذخیره می‌شود و با دپلوی کردن ورژن جدیدی از Logic داده‌های قبلی نیز از بین می‌روند. راهی که به نظر می‌رسد این است که داده‌های قرارداد Data از نوع public باشند، تا در قرارداد Logic در دسترس باشند و کار مستقیما روی آن انجام شود. اما این کار نیز با این که ظاهرا مشکل را حل می‌کند، همان سکیوریتی و امنیت ظاهری داده‌ها را نیز در معرض خطر قرار می‌دهد.

تا این جا در جریان صورت مسئله‌ای قرار گرفتید که در تلاش برای حل آن بودم. اما راه حلی که در یک مقاله در سایت مدیوم دیدم به کمک زبان اسمبلی حل شده و در زبان سالیدیتی من برای آن راه حلی نیافتم:

در قرارداد هوشمند Data لازم نیست همان توابع را مجدد تعریف کنیم، بلکه یک تابع fallback تعریف می‌کنیم که هر صدا زدن تابع غیر معمولی به آن ارجاع می‌شود و در آن تابع یک قطعه کد به زبان اسمبلی این ذخیره سازی را به کمک منطق موجود در قرارداد Logic ولی روی داده‌های قرارداد Data انجام دهد:

    contract Data {
        struct User {
            string name;
            uint code;
        }
        mapping(uint => User) users;
        uint usersCount = 0;
        address logicAddress;
        function setLogicAddress(address _logicAddress) public {
            logicAddress = _logicAddress;
        }
        function () payable external {
            address target = logicAddress;
            assembly {
                let ptr := mload(0x40)
                calldatacopy(ptr, 0, calldatasize)
                let result := delegatecall(gas, target, ptr, calldatasize, 0, 0)
                let size := returndatasize
                returndatacopy(ptr, 0, size)
                switch result
                case 0 { revert(ptr, size) }
                case 1 { return(ptr, size) }
            }
        }
    }

من با زبان اسمبلی اتریوم آشنا نیستم و اصلا نمی‌دانم که این برنامه چگونه کار می‌کند ولی این برنامه را با موفقیت تست کردم.

برای تغییر برنامه Logic می‌توانید تغییری در برنامه Logic ایجاد کنید و آن را دوباره دپلوی کنید و آدرس آن را با تابع setLogicAddress صدا کنید تا در قرارداد Data ثبت شود. مثلا این تغییر:

    contract Logic is Storage {
        function regUser(string memory name, uint code) public {
            usersCount++;
            users[usersCount].name = name;
            users[usersCount].code = code+2;
        }
        function getUser(uint userId) public view returns(string memory, uint) {
            return(users[userId].name, users[userId].code);
        }
    }

برای صدا زدن این تابع کافی است که آدرس قرارداد Data را مستقیما برای Logic به کار ببرید تا بتوانید توابع آن را اجرا کنید یا با متد Calldata آشنا باشید تا بتوانید توابع لازم را با استفاده از این متد صدا بزنید.