முக்கிய உள்ளடக்கத்திற்குச் செல்லவும்

உங்களால் முடிந்த அனைத்தையும் தேக்ககப்படுத்துங்கள்

அடுக்கு 2
தேக்ககப்படுத்துதல்
சேமிப்பகம்
அளவிடுதல்
இடைநிலை
ஓரி பொமரன்ட்ஸ்
15 செப்டம்பர், 2022
19 நிமிட வாசிப்பு

ரோலப்களைப் பயன்படுத்தும் போது, பரிவர்த்தனையில் உள்ள ஒரு பைட்டின் விலை, சேமிப்பக நேரப்பகுதியின் விலையை விட மிகவும் அதிகம். எனவே, முடிந்தவரை அதிக தகவல்களைச் சங்கிலிசார் தேக்ககப்படுத்துவது அர்த்தமுள்ளதாக இருக்கும்.

இந்தக் கட்டுரையில், பல முறை பயன்படுத்தப்பட வாய்ப்புள்ள எந்தவொரு அளவுரு மதிப்பும் தேக்ககப்படுத்தப்பட்டு, (முதல் முறைக்குப் பிறகு) மிகக் குறைந்த பைட்டுகளுடன் பயன்படுத்தக் கிடைக்கும் வகையில் ஒரு தேக்கக ஒப்பந்தத்தை எவ்வாறு உருவாக்குவது மற்றும் பயன்படுத்துவது என்பதையும், இந்தத் தேக்ககத்தைப் பயன்படுத்தும் புறச்சங்கிலிக் குறியீட்டை எவ்வாறு எழுதுவது என்பதையும் நீங்கள் கற்றுக் கொள்வீர்கள்.

நீங்கள் கட்டுரையைத் தவிர்த்துவிட்டு மூலக் குறியீட்டை மட்டும் பார்க்க விரும்பினால், அது இங்கே உள்ளது (opens in a new tab). மேம்பாட்டுத் தொகுப்பு Foundry (opens in a new tab) ஆகும்.

ஒட்டுமொத்த வடிவமைப்பு

எளிமைக்காக, அனைத்துப் பரிவர்த்தனை அளவுருக்களும் uint256 ஆக, 32 பைட்டுகள் நீளம் கொண்டவை என்று கருதுவோம். ஒரு பரிவர்த்தனையைப் பெறும்போது, ஒவ்வொரு அளவுருவையும் பின்வருமாறு பாகுபடுத்துவோம்:

  1. முதல் பைட் 0xFF ஆக இருந்தால், அடுத்த 32 பைட்டுகளை அளவுரு மதிப்பாக எடுத்து, அதைத் தேக்ககத்தில் எழுதவும்.

  2. முதல் பைட் 0xFE ஆக இருந்தால், அடுத்த 32 பைட்டுகளை அளவுரு மதிப்பாக எடுக்கவும், ஆனால் அதைத் தேக்ககத்தில் எழுத வேண்டாம்.

  3. வேறு எந்த மதிப்பிற்கும், மேல் நான்கு பிட்களைக் கூடுதல் பைட்டுகளின் எண்ணிக்கையாகவும், கீழ் நான்கு பிட்களைத் தேக்ககத் திறவுகோலின் மிக முக்கியமான பிட்களாகவும் எடுத்துக்கொள்ளவும். இதோ சில எடுத்துக்காட்டுகள்:

    அழைப்புத் தரவில் உள்ள பைட்டுகள்தேக்ககத் திறவுகோல்
    0x0F0x0F
    0x10,0x100x10
    0x12,0xAC0x02AC
    0x2D,0xEA, 0xD60x0DEAD6

தேக்ககக் கையாளுதல்

தேக்ககம் Cache.sol (opens in a new tab) இல் செயல்படுத்தப்பட்டுள்ளது. இதை வரியாகப் பார்ப்போம்.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;


contract Cache {

    bytes1 public constant INTO_CACHE = 0xFF;
    bytes1 public constant DONT_CACHE = 0xFE;

அனைத்துத் தகவல்களையும் வழங்கி, அதைத் தேக்ககத்தில் எழுத வேண்டுமா வேண்டாமா என்பதைத் தீர்மானிக்கும் சிறப்பு நிகழ்வுகளை விளக்குவதற்கு இந்த மாறிலிகள் பயன்படுத்தப்படுகின்றன. தேக்ககத்தில் எழுதுவதற்கு, முன்பு பயன்படுத்தப்படாத சேமிப்பக நேரப்பகுதிகளில் இரண்டு SSTORE (opens in a new tab) செயல்பாடுகள் தேவைப்படுகின்றன, ஒவ்வொன்றிற்கும் 22100 எரிவாயு செலவாகும், எனவே இதை நாம் விருப்பத் தேர்வாக ஆக்குகிறோம்.


    mapping(uint => uint) public val2key;

மதிப்புகளுக்கும் அவற்றின் திறவுகோல்களுக்கும் இடையிலான ஒரு மேப்பிங் (opens in a new tab). பரிவர்த்தனையை அனுப்புவதற்கு முன் மதிப்புகளைக் குறியாக்கம் செய்ய இந்தத் தகவல் அவசியம்.

    // இடம் n ஆனது திறவுகோல் n+1 க்கான மதிப்பைக் கொண்டுள்ளது, ஏனெனில் நாம் பாதுகாக்க வேண்டும்
    // பூஜ்ஜியத்தை "தற்காலிக சேமிப்பில் இல்லை" என.
    uint[] public key2val;

திறவுகோல்களிலிருந்து மதிப்புகளுக்கு மேப்பிங் செய்ய நாம் ஒரு வரிசையைப் பயன்படுத்தலாம், ஏனெனில் நாம் திறவுகோல்களை ஒதுக்குகிறோம், மேலும் எளிமைக்காக அதை வரிசையாகச் செய்கிறோம்.

    function cacheRead(uint _key) public view returns (uint) {
        require(_key <= key2val.length, "Reading uninitialize cache entry");
        return key2val[_key-1];
    }  // cacheRead

தேக்ககத்திலிருந்து ஒரு மதிப்பைப் படிக்கவும்.

    // ஒரு மதிப்பு ஏற்கனவே தற்காலிக சேமிப்பில் இல்லையென்றால் அதை எழுதவும்
    // சோதனை செயல்படுவதை சாத்தியமாக்க பொதுவானதாக (public) மட்டுமே உள்ளது
    function cacheWrite(uint _value) public returns (uint) {
        // மதிப்பு ஏற்கனவே தற்காலிக சேமிப்பில் இருந்தால், தற்போதைய திறவுகோலை வழங்கவும்
        if (val2key[_value] != 0) {
            return val2key[_value];
        }

ஒரே மதிப்பை ஒன்றுக்கு மேற்பட்ட முறை தேக்ககத்தில் வைப்பதில் எந்த அர்த்தமும் இல்லை. மதிப்பு ஏற்கனவே இருந்தால், இருக்கும் திறவுகோலை மட்டும் திருப்பி அனுப்பவும்.

        // 0xFE ஒரு சிறப்பு நிலை என்பதால், தற்காலிக சேமிப்பு வைத்திருக்கக்கூடிய மிகப்பெரிய திறவுகோல்
        // 0x0D மற்றும் அதைத் தொடர்ந்து 15 0xFF-கள் ஆகும். தற்காலிக சேமிப்பின் நீளம் ஏற்கனவே அவ்வளவு
        // பெரிதாக இருந்தால், தோல்வியடையும்.
        //                              1 2 3 4 5 6 7 8 9 A B C D E F
        require(key2val.length+1 < 0x0DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
            "cache overflow");

அவ்வளவு பெரிய தேக்ககம் நமக்கு எப்போதாவது கிடைக்கும் என்று நான் நினைக்கவில்லை (தோராயமாக 1.8*1037 உள்ளீடுகள், இதைச் சேமிக்க சுமார் 1027 TB தேவைப்படும்). இருப்பினும், "640kB எப்போதும் போதுமானதாக இருக்கும்" (opens in a new tab) என்பதை நினைவில் கொள்ளும் அளவுக்கு எனக்கு வயதாகிவிட்டது. இந்தச் சோதனை மிகவும் மலிவானது.

        // அடுத்த திறவுகோலைப் பயன்படுத்தி மதிப்பை எழுதவும்
        val2key[_value] = key2val.length+1;

பின்னோக்கிய தேடலைச் சேர்க்கவும் (மதிப்பிலிருந்து திறவுகோலுக்கு).

        key2val.push(_value);

முன்னோக்கிய தேடலைச் சேர்க்கவும் (திறவுகோலிலிருந்து மதிப்புக்கு). நாம் மதிப்புகளை வரிசையாக ஒதுக்குவதால், கடைசி வரிசை மதிப்புக்குப் பிறகு அதைச் சேர்க்கலாம்.

        return key2val.length;
    }  // cacheWrite

புதிய மதிப்பு சேமிக்கப்பட்டுள்ள கலமான key2val இன் புதிய நீளத்தைத் திருப்பி அனுப்பவும்.

    function _calldataVal(uint startByte, uint length)
        private pure returns (uint)

இந்தச் சார்பு தன்னிச்சையான நீளமுள்ள (32 பைட்டுகள் வரை, சொல் அளவு) அழைப்புத் தரவிலிருந்து ஒரு மதிப்பைப் படிக்கிறது.

    {
        uint _retVal;

        require(length < 0x21,
            "_calldataVal length limit is 32 bytes");
        require(length + startByte <= msg.data.length,
            "_calldataVal trying to read beyond calldatasize");

இந்தச் சார்பு உள்ளகமானது, எனவே மீதமுள்ள குறியீடு சரியாக எழுதப்பட்டிருந்தால் இந்தச் சோதனைகள் தேவையில்லை. இருப்பினும், அவற்றுக்கு அதிகச் செலவாகாது என்பதால் அவற்றை நாம் வைத்திருக்கலாம்.

        assembly {
            _retVal := calldataload(startByte)
        }

இந்தக் குறியீடு Yul (opens in a new tab) இல் உள்ளது. இது அழைப்புத் தரவிலிருந்து 32 பைட் மதிப்பைப் படிக்கிறது. EVM இல் துவக்கப்படாத இடம் பூஜ்ஜியமாகக் கருதப்படுவதால், அழைப்புத் தரவு startByte+32 க்கு முன் நின்றாலும் இது வேலை செய்யும்.

        _retVal = _retVal >> (256-length*8);

நமக்கு 32 பைட் மதிப்பு கட்டாயம் தேவையில்லை. இது அதிகப்படியான பைட்டுகளை நீக்குகிறது.

        return _retVal;
    } // _calldataVal


    // _fromByte இலிருந்து தொடங்கி, அழைப்புத் தரவிலிருந்து ஒரு அளவுருவைப் படிக்கவும்
    function _readParam(uint _fromByte) internal
        returns (uint _nextByte, uint _parameterValue)
    {

அழைப்புத் தரவிலிருந்து ஒரு அளவுருவைப் படிக்கவும். அளவுருக்கள் 1 பைட் முதல் 33 பைட்டுகள் வரை நீளமாக இருக்கலாம் என்பதால், நாம் படித்த மதிப்பை மட்டுமல்லாமல், அடுத்த பைட்டின் இருப்பிடத்தையும் திருப்பி அனுப்ப வேண்டும் என்பதை நினைவில் கொள்ளவும்.

        // மீதமுள்ளவற்றை எவ்வாறு விளக்குவது என்பதை முதல் பைட் நமக்குக் கூறுகிறது
        uint8 _firstByte;

        _firstByte = uint8(_calldataVal(_fromByte, 1));

ஆபத்தான உள்ளுறை வகை மாற்றங்களைத் (opens in a new tab) தடை செய்வதன் மூலம் பிழைகளின் எண்ணிக்கையைக் குறைக்க Solidity முயற்சிக்கிறது. எடுத்துக்காட்டாக, 256 பிட்களிலிருந்து 8 பிட்களுக்குக் குறைப்பது வெளிப்படையாக இருக்க வேண்டும்.

கீழ் நிப்பிளை (nibble) (opens in a new tab) எடுத்து, தேக்ககத்திலிருந்து மதிப்பைப் படிக்க மற்ற பைட்டுகளுடன் அதை இணைக்கவும்.

அழைப்புத் தரவிலிருந்தே நம்மிடம் உள்ள அளவுருக்களின் எண்ணிக்கையைப் பெறலாம், ஆனால் நம்மை அழைக்கும் சார்புகளுக்கு அவை எத்தனை அளவுருக்களை எதிர்பார்க்கின்றன என்பது தெரியும். அவர்கள் நம்மிடம் சொல்வதே எளிதானது.

உங்களுக்குத் தேவையான எண்ணிக்கை கிடைக்கும் வரை அளவுருக்களைப் படிக்கவும். அழைப்புத் தரவின் முடிவைத் தாண்டிச் சென்றால், _readParams அழைப்பை மீளமைக்கும்.

Foundry இன் ஒரு பெரிய நன்மை என்னவென்றால், இது Solidity இல் சோதனைகளை எழுத அனுமதிக்கிறது (கீழே உள்ள தேக்ககத்தைச் சோதிப்பதைப் பார்க்கவும்). இது அலகுச் சோதனைகளை மிகவும் எளிதாக்குகிறது. இது நான்கு அளவுருக்களைப் படித்து அவற்றைத் திருப்பி அனுப்பும் ஒரு சார்பாகும், எனவே அவை சரியானவை என்பதைச் சோதனையால் சரிபார்க்க முடியும்.

    // ஒரு மதிப்பைப் பெறவும், அதை குறியாக்கம் செய்யும் பைட்டுகளை வழங்கவும் (முடிந்தால் தற்காலிக சேமிப்பைப் பயன்படுத்தி)
    function encodeVal(uint _val) public view returns(bytes memory) {

encodeVal என்பது தேக்ககத்தைப் பயன்படுத்தும் அழைப்புத் தரவை உருவாக்க உதவும் புறச்சங்கிலிக் குறியீடு அழைக்கும் ஒரு சார்பாகும். இது ஒரு மதிப்பைப் பெற்று, அதைக் குறியாக்கம் செய்யும் பைட்டுகளைத் திருப்பி அனுப்புகிறது. இந்தச் சார்பு ஒரு view ஆகும், எனவே இதற்குப் பரிவர்த்தனை தேவையில்லை, மேலும் வெளிப்புறமாக அழைக்கப்படும்போது எந்த எரிவாயுவும் செலவாகாது.

        uint _key = val2key[_val];

        // மதிப்பு இன்னும் தற்காலிக சேமிப்பில் இல்லை, அதைச் சேர்க்கவும்
        if (_key == 0)
            return bytes.concat(INTO_CACHE, bytes32(_val));

EVM இல் துவக்கப்படாத அனைத்துச் சேமிப்பகமும் பூஜ்ஜியங்களாகக் கருதப்படுகின்றன. எனவே, இல்லாத ஒரு மதிப்புக்கான திறவுகோலைத் தேடினால், நமக்கு ஒரு பூஜ்ஜியம் கிடைக்கும். அந்த நிலையில், அதைக் குறியாக்கம் செய்யும் பைட்டுகள் INTO_CACHE ஆக இருக்கும் (எனவே இது அடுத்த முறை தேக்ககப்படுத்தப்படும்), அதைத் தொடர்ந்து உண்மையான மதிப்பு இருக்கும்.

        // திறவுகோல் <0x10 ஆக இருந்தால், அதை ஒற்றை பைட்டாக வழங்கவும்
        if (_key < 0x10)
            return bytes.concat(bytes1(uint8(_key)));

ஒற்றை பைட்டுகள் மிகவும் எளிதானவை. எந்த நீளத்திலும் இருக்கக்கூடிய ஒரு பைட் வரிசையாக bytes<n> வகையை மாற்ற நாம் bytes.concat (opens in a new tab) ஐப் பயன்படுத்துகிறோம். பெயர் அப்படி இருந்தாலும், ஒரே ஒரு வாதம் வழங்கப்படும்போது இது நன்றாக வேலை செய்கிறது.

        // இரண்டு பைட் மதிப்பு, 0x1vvv என குறியாக்கம் செய்யப்பட்டுள்ளது
        if (_key < 0x1000)
            return bytes.concat(bytes2(uint16(_key) | 0x1000));

நம்மிடம் 163 க்கும் குறைவான திறவுகோல் இருக்கும்போது, அதை இரண்டு பைட்டுகளில் வெளிப்படுத்தலாம். நாம் முதலில் 256 பிட் மதிப்பான _key ஐ 16 பிட் மதிப்பாக மாற்றி, முதல் பைட்டுடன் கூடுதல் பைட்டுகளின் எண்ணிக்கையைச் சேர்க்க தர்க்கரீதியான OR ஐப் பயன்படுத்துகிறோம். பின்னர் அதை ஒரு bytes2 மதிப்பாக மாற்றுகிறோம், அதை bytes ஆக மாற்றலாம்.

மற்ற மதிப்புகள் (3 பைட்டுகள், 4 பைட்டுகள் போன்றவை) அதே வழியில் கையாளப்படுகின்றன, வெவ்வேறு புல அளவுகளுடன் மட்டுமே.

        // நாம் இங்கு வந்தால், ஏதோ தவறு உள்ளது.
        revert("Error in encodeVal, should not happen");

நாம் இங்கு வந்தால், 16*25615 க்கும் குறையாத ஒரு திறவுகோல் நமக்குக் கிடைத்துள்ளது என்று அர்த்தம். ஆனால் cacheWrite திறவுகோல்களைக் கட்டுப்படுத்துகிறது, எனவே நாம் 14*25616 வரை கூடப் பெற முடியாது (இது 0xFE இன் முதல் பைட்டைக் கொண்டிருக்கும், எனவே இது DONT_CACHE போல இருக்கும்). ஆனால் எதிர்கால நிரலாளர் ஒரு பிழையை அறிமுகப்படுத்தினால், ஒரு சோதனையைச் சேர்ப்பதற்கு அதிகச் செலவாகாது.

    } // encodeVal

}  // Cache

தேக்ககத்தைச் சோதித்தல்

Foundry இன் நன்மைகளில் ஒன்று, இது Solidity இல் சோதனைகளை எழுத உங்களை அனுமதிக்கிறது (opens in a new tab), இது அலகுச் சோதனைகளை எழுதுவதை எளிதாக்குகிறது. Cache வகுப்புக்கான சோதனைகள் இங்கே (opens in a new tab) உள்ளன. சோதனைகள் பொதுவாக இருப்பதைப் போலவே, சோதனைக் குறியீடும் திரும்பத் திரும்ப வருவதால், இந்தக் கட்டுரை சுவாரஸ்யமான பகுதிகளை மட்டுமே விளக்குகிறது.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";


// கன்சோலுக்கு `forge test -vv` ஐ இயக்க வேண்டும்.
import "forge-std/console.sol";

இது சோதனைத் தொகுப்பு மற்றும் console.log ஐப் பயன்படுத்தத் தேவையான பாய்லர்பிளேட் (boilerplate) மட்டுமே.

import "src/Cache.sol";

நாம் சோதிக்கும் ஒப்பந்தத்தை நாம் தெரிந்து கொள்ள வேண்டும்.

contract CacheTest is Test {
    Cache cache;

    function setUp() public {
        cache = new Cache();
    }

ஒவ்வொரு சோதனைக்கும் முன் setUp சார்பு அழைக்கப்படுகிறது. இந்த நிலையில் நாம் ஒரு புதிய தேக்ககத்தை உருவாக்குகிறோம், இதனால் நமது சோதனைகள் ஒன்றையொன்று பாதிக்காது.

    function testCaching() public {

சோதனைகள் என்பவை test என்று தொடங்கும் பெயர்களைக் கொண்ட சார்புகளாகும். இந்தச் சார்பு அடிப்படைத் தேக்ககச் செயல்பாட்டைச் சரிபார்க்கிறது, மதிப்புகளை எழுதுகிறது மற்றும் அவற்றை மீண்டும் படிக்கிறது.

        for(uint i=1; i<5000; i++) {
            cache.cacheWrite(i*i);
        }

        for(uint i=1; i<5000; i++) {
            assertEq(cache.cacheRead(i), i*i);

assert... சார்புகளைப் (opens in a new tab) பயன்படுத்தி உண்மையான சோதனையை நீங்கள் இப்படித்தான் செய்கிறீர்கள். இந்த நிலையில், நாம் எழுதிய மதிப்புதான் நாம் படித்ததா என்பதைச் சரிபார்க்கிறோம். தேக்ககத் திறவுகோல்கள் நேரியல் முறையில் ஒதுக்கப்படுகின்றன என்பது நமக்குத் தெரியும் என்பதால், cache.cacheWrite இன் முடிவை நாம் நிராகரிக்கலாம்.

முதலில் ஒவ்வொரு மதிப்பையும் இரண்டு முறை தேக்ககத்தில் எழுதி, திறவுகோல்கள் ஒரே மாதிரியாக இருப்பதை உறுதிசெய்கிறோம் (அதாவது இரண்டாவது எழுதும் செயல் உண்மையில் நடக்கவில்லை).

        for(uint i=1; i<100; i+=3) {
            uint _key = cache.cacheWrite(i);
            assertEq(_key, i);
        }
    }    // testRepeatCaching

கோட்பாட்டளவில், தொடர்ச்சியான தேக்கக எழுதுதல்களைப் பாதிக்காத ஒரு பிழை இருக்கலாம். எனவே இங்கே நாம் தொடர்ச்சியாக இல்லாத சில எழுதுதல்களைச் செய்கிறோம், மேலும் மதிப்புகள் இன்னும் மீண்டும் எழுதப்படவில்லை என்பதைப் பார்க்கிறோம்.

    // நினைவக இடையகத்திலிருந்து ஒரு uint ஐப் படிக்கவும் (நாம் அனுப்பிய அளவுருக்களை
    // திரும்பப் பெறுவதை உறுதிசெய்ய)
    function toUint256(bytes memory _bytes, uint256 _start) internal pure
        returns (uint256)

ஒரு bytes memory இடையகத்திலிருந்து 256 பிட் சொல்லைப் படிக்கவும். தேக்ககத்தைப் பயன்படுத்தும் சார்பு அழைப்பை இயக்கும்போது சரியான முடிவுகளைப் பெறுகிறோம் என்பதைச் சரிபார்க்க இந்தப் பயன்பாட்டுச் சார்பு நம்மை அனுமதிக்கிறது.

    {
        require(_bytes.length >= _start + 32, "toUint256_outOfBounds");
        uint256 tempUint;

        assembly {
            tempUint := mload(add(add(_bytes, 0x20), _start))
        }

Yul uint256 க்கு அப்பாற்பட்ட தரவுக் கட்டமைப்புகளை ஆதரிக்காது, எனவே நினைவக இடையகமான _bytes போன்ற அதிநவீன தரவுக் கட்டமைப்பைக் குறிப்பிடும்போது, அந்தக் கட்டமைப்பின் முகவரியைப் பெறுவீர்கள். Solidity bytes memory மதிப்புகளை நீளத்தைக் கொண்ட 32 பைட் சொல்லாகச் சேமிக்கிறது, அதைத் தொடர்ந்து உண்மையான பைட்டுகள் இருக்கும், எனவே பைட் எண் _start ஐப் பெற நாம் _bytes+32+_start ஐக் கணக்கிட வேண்டும்.

சோதனைக்கு நமக்குத் தேவையான சில மாறிலிகள்.

    function testReadParam() public {

அளவுருக்களைச் சரியாகப் படிக்க முடிகிறதா என்பதைச் சோதிக்க, readParams ஐப் பயன்படுத்தும் சார்பான fourParams() ஐ அழைக்கவும்.

        address _cacheAddr = address(cache);
        bool _success;
        bytes memory _callInput;
        bytes memory _callOutput;

தேக்ககத்தைப் பயன்படுத்தி ஒரு சார்பை அழைக்க நாம் சாதாரண ABI பொறிமுறையைப் பயன்படுத்த முடியாது, எனவே நாம் கீழ்நிலை <address>.call() (opens in a new tab) பொறிமுறையைப் பயன்படுத்த வேண்டும். அந்தப் பொறிமுறையானது ஒரு bytes memory ஐ உள்ளீடாக எடுத்து, அதை (அத்துடன் ஒரு பூலியன் மதிப்பையும்) வெளியீடாகத் திருப்பி அனுப்புகிறது.

        // முதல் அழைப்பு, தற்காலிக சேமிப்பு காலியாக உள்ளது
        _callInput = bytes.concat(
            FOUR_PARAMS,

ஒரே ஒப்பந்தம் தேக்ககப்படுத்தப்பட்ட சார்புகள் (பரிவர்த்தனைகளிலிருந்து நேரடியாக அழைப்புகளுக்கு) மற்றும் தேக்ககப்படுத்தப்படாத சார்புகள் (பிற ஸ்மார்ட் ஒப்பந்தங்களிலிருந்து அழைப்புகளுக்கு) இரண்டையும் ஆதரிப்பது பயனுள்ளதாக இருக்கும். அதைச் செய்ய, எல்லாவற்றையும் ஒரு fallback சார்பில் (opens in a new tab) வைப்பதற்குப் பதிலாக, சரியான சார்பை அழைக்க நாம் தொடர்ந்து Solidity பொறிமுறையை நம்பியிருக்க வேண்டும். இதைச் செய்வது ஒருங்கிணைக்கக்கூடிய தன்மையை மிகவும் எளிதாக்குகிறது. பெரும்பாலான சந்தர்ப்பங்களில் சார்பை அடையாளம் காண ஒரு பைட் போதுமானதாக இருக்கும், எனவே நாம் மூன்று பைட்டுகளை வீணடிக்கிறோம் (16*3=48 எரிவாயு). இருப்பினும், நான் இதை எழுதும்போது அந்த 48 எரிவாயுவின் விலை 0.07 சென்ட்கள் ஆகும், இது எளிமையான, குறைவான பிழைகள் ஏற்படக்கூடிய குறியீட்டிற்கான நியாயமான செலவாகும்.

            // முதல் மதிப்பு, அதை தற்காலிக சேமிப்பில் சேர்க்கவும்
            cache.INTO_CACHE(),
            bytes32(VAL_A),

முதல் மதிப்பு: இது தேக்ககத்தில் எழுதப்பட வேண்டிய முழு மதிப்பு என்று கூறும் ஒரு கொடி, அதைத் தொடர்ந்து மதிப்பின் 32 பைட்டுகள். VAL_B தேக்ககத்தில் எழுதப்படவில்லை என்பதையும், VAL_C மூன்றாவது மற்றும் நான்காவது அளவுருவாக இருப்பதையும் தவிர, மற்ற மூன்று மதிப்புகளும் ஒத்தவை.

             .
             .
             .
        );
        (_success, _callOutput) = _cacheAddr.call(_callInput);

இங்குதான் நாம் உண்மையில் Cache ஒப்பந்தத்தை அழைக்கிறோம்.

        assertEq(_success, true);

அழைப்பு வெற்றிகரமாக இருக்கும் என்று எதிர்பார்க்கிறோம்.

        assertEq(cache.cacheRead(1), VAL_A);
        assertEq(cache.cacheRead(2), VAL_C);

நாம் ஒரு வெற்றுத் தேக்ககத்துடன் தொடங்கி, பின்னர் VAL_A ஐயும் அதைத் தொடர்ந்து VAL_C ஐயும் சேர்க்கிறோம். முதலாவது திறவுகோல் 1 ஐயும், இரண்டாவது 2 ஐயும் கொண்டிருக்கும் என்று எதிர்பார்க்கிறோம்.

assertEq(toUint256(_callOutput,0), VAL_A);
        assertEq(toUint256(_callOutput,32), VAL_B);
        assertEq(toUint256(_callOutput,64), VAL_C);
        assertEq(toUint256(_callOutput,96), VAL_C);

வெளியீடு நான்கு அளவுருக்கள் ஆகும். இங்கே அது சரியானது என்பதைச் சரிபார்க்கிறோம்.

        // இரண்டாவது அழைப்பு, நாம் தற்காலிக சேமிப்பைப் பயன்படுத்தலாம்
        _callInput = bytes.concat(
            FOUR_PARAMS,

            // தற்காலிக சேமிப்பில் உள்ள முதல் மதிப்பு
            bytes1(0x01),

16 க்கும் குறைவான தேக்ககத் திறவுகோல்கள் ஒரு பைட் மட்டுமே.

அழைப்பிற்குப் பிந்தைய சோதனைகள் முதல் அழைப்பிற்குப் பிந்தைய சோதனைகளைப் போலவே இருக்கும்.

    function testEncodeVal() public {

இந்தச் சார்பு testReadParam ஐப் போன்றது, ஆனால் அளவுருக்களை வெளிப்படையாக எழுதுவதற்குப் பதிலாக நாம் encodeVal() ஐப் பயன்படுத்துகிறோம்.

testEncodeVal() இல் உள்ள ஒரே கூடுதல் சோதனை _callInput இன் நீளம் சரியானது என்பதைச் சரிபார்ப்பதாகும். முதல் அழைப்பிற்கு இது 4+33*4 ஆகும். ஒவ்வொரு மதிப்பும் ஏற்கனவே தேக்ககத்தில் உள்ள இரண்டாவது அழைப்பிற்கு, இது 4+1*4 ஆகும்.

மேலே உள்ள testEncodeVal சார்பு நான்கு மதிப்புகளை மட்டுமே தேக்ககத்தில் எழுதுகிறது, எனவே பல பைட் மதிப்புகளைக் கையாளும் சார்பின் பகுதி (opens in a new tab) சரிபார்க்கப்படவில்லை. ஆனால் அந்தக் குறியீடு சிக்கலானது மற்றும் பிழைகள் ஏற்பட வாய்ப்புள்ளது.

இந்தச் சார்பின் முதல் பகுதி 1 முதல் 0x1FFF வரையிலான அனைத்து மதிப்புகளையும் வரிசையாகத் தேக்ககத்தில் எழுதும் ஒரு சுழற்சியாகும், எனவே நாம் அந்த மதிப்புகளைக் குறியாக்கம் செய்து அவை எங்கு செல்கின்றன என்பதை அறிய முடியும்.

ஒரு பைட், இரண்டு பைட் மற்றும் மூன்று பைட் மதிப்புகளைச் சோதிக்கவும். போதுமான அடுக்கு உள்ளீடுகளை (குறைந்தது 0x10000000, தோராயமாக கால் பில்லியன்) எழுத அதிக நேரம் எடுக்கும் என்பதால், அதற்கு அப்பால் நாம் சோதிக்க மாட்டோம்.

போதுமான அளவுருக்கள் இல்லாத அசாதாரண நிலையில் என்ன நடக்கிறது என்பதைச் சோதிக்கவும்.

        .
        .
        .
        (_success, _callOutput) = _cacheAddr.call(_callInput);
        assertEq(_success, false);
    }   // testShortCalldata

இது மீளமைக்கப்படுவதால், நாம் பெற வேண்டிய முடிவு false ஆகும்.

இந்தச் சார்பு நான்கு முற்றிலும் சரியான அளவுருக்களைப் பெறுகிறது, ஆனால் தேக்ககம் காலியாக இருப்பதால் படிக்க அங்கு எந்த மதிப்புகளும் இல்லை.

இந்தச் சார்பு ஐந்து மதிப்புகளை அனுப்புகிறது. ஐந்தாவது மதிப்பு புறக்கணிக்கப்படுகிறது என்பது நமக்குத் தெரியும், ஏனெனில் இது சரியான தேக்கக உள்ளீடு அல்ல, இது சேர்க்கப்படாவிட்டால் மீளமைப்பை ஏற்படுத்தியிருக்கும்.

ஒரு மாதிரிச் செயலி

Solidity இல் சோதனைகளை எழுதுவது மிகவும் நல்லது, ஆனால் இறுதியில் ஒரு பரவலாக்கப்பட்ட செயலி (dapp) பயனுள்ளதாக இருக்கச் சங்கிலிக்கு வெளியேயிருந்து வரும் கோரிக்கைகளைச் செயலாக்கக்கூடியதாக இருக்க வேண்டும். "ஒரு முறை எழுது, பல முறை படி" (Write Once, Read Many) என்பதைக் குறிக்கும் WORM உடன் ஒரு பரவலாக்கப்பட்ட செயலியில் (dapp) தேக்ககத்தை எவ்வாறு பயன்படுத்துவது என்பதை இந்தக் கட்டுரை விளக்குகிறது. ஒரு திறவுகோல் இன்னும் எழுதப்படவில்லை என்றால், நீங்கள் அதில் ஒரு மதிப்பை எழுதலாம். திறவுகோல் ஏற்கனவே எழுதப்பட்டிருந்தால், உங்களுக்கு ஒரு மீளமைப்பு கிடைக்கும்.

ஒப்பந்தம்

இதுதான் ஒப்பந்தம் (opens in a new tab). இது பெரும்பாலும் Cache மற்றும் CacheTest உடன் நாம் ஏற்கனவே செய்ததையே மீண்டும் செய்கிறது, எனவே சுவாரஸ்யமான பகுதிகளை மட்டுமே நாங்கள் உள்ளடக்குகிறோம்.

import "./Cache.sol";

contract WORM is Cache {

Cache ஐப் பயன்படுத்துவதற்கான எளிதான வழி, அதை நமது சொந்த ஒப்பந்தத்தில் மரபுரிமையாகப் பெறுவதாகும்.

    function writeEntryCached() external {
        uint[] memory params = _readParams(2);
        writeEntry(params[0], params[1]);
    }    // writeEntryCached

இந்தச் சார்பு மேலே உள்ள CacheTest இல் உள்ள fourParam ஐப் போன்றது. நாம் ABI விவரக்குறிப்புகளைப் பின்பற்றாததால், சார்பில் எந்த அளவுருக்களையும் அறிவிக்காமல் இருப்பது நல்லது.

    // எங்களை அழைப்பதை எளிதாக்குங்கள்
    // writeEntryCached() க்கான செயல்பாட்டு கையொப்பம், வழங்கியவர்கள்
    // https://www.4byte.directory/signatures/?bytes4_signature=0xe4e4f2d3
    bytes4 constant public WRITE_ENTRY_CACHED = 0xe4e4f2d3;

writeEntryCached ஐ அழைக்கும் வெளிப்புறக் குறியீடு, worm.writeEntryCached ஐப் பயன்படுத்துவதற்குப் பதிலாக, அழைப்புத் தரவை கைமுறையாக உருவாக்க வேண்டும், ஏனெனில் நாம் ABI விவரக்குறிப்புகளைப் பின்பற்றுவதில்லை. இந்த மாறிலி மதிப்பைக் கொண்டிருப்பது அதை எழுதுவதை எளிதாக்குகிறது.

நாம் WRITE_ENTRY_CACHED ஐ ஒரு நிலை மாறியாக வரையறுத்தாலும், அதை வெளிப்புறமாகப் படிக்க அதற்கான கெட்டர் (getter) சார்பான worm.WRITE_ENTRY_CACHED() ஐப் பயன்படுத்துவது அவசியம் என்பதை நினைவில் கொள்ளவும்.

    function readEntry(uint key) public view
        returns (uint _value, address _writtenBy, uint _writtenAtBlock)

படிக்கும் சார்பு ஒரு view ஆகும், எனவே இதற்குப் பரிவர்த்தனை தேவையில்லை மற்றும் எரிவாயு செலவாகாது. இதன் விளைவாக, அளவுருவுக்குத் தேக்ககத்தைப் பயன்படுத்துவதில் எந்த நன்மையும் இல்லை. காட்சிச் சார்புகளுடன் (view functions) எளிமையான நிலையான பொறிமுறையைப் பயன்படுத்துவதே சிறந்தது.

சோதனைக் குறியீடு

இது ஒப்பந்தத்திற்கான சோதனைக் குறியீடு (opens in a new tab). மீண்டும், சுவாரஸ்யமானவற்றை மட்டும் பார்ப்போம்.

    function testWReadWrite() public {
        worm.writeEntry(0xDEAD, 0x60A7);

        vm.expectRevert(bytes("entry already written"));
        worm.writeEntry(0xDEAD, 0xBEEF);

அடுத்த அழைப்பு தோல்வியடைய வேண்டும் என்பதையும், தோல்விக்கான காரணத்தையும் Foundry சோதனையில் இப்படித்தான் (vm.expectRevert) (opens in a new tab) குறிப்பிடுகிறோம். அழைப்புத் தரவை உருவாக்கி, கீழ்நிலை இடைமுகத்தைப் (<contract>.call() போன்றவை) பயன்படுத்தி ஒப்பந்தத்தை அழைப்பதற்குப் பதிலாக, <contract>.<function name>() தொடரியலைப் பயன்படுத்தும்போது இது பொருந்தும்.

    function testReadWriteCached() public {
        uint cacheGoat = worm.cacheWrite(0x60A7);

இங்கே நாம் cacheWrite தேக்ககத் திறவுகோலைத் திருப்பி அனுப்புகிறது என்ற உண்மையைப் பயன்படுத்துகிறோம். இது உற்பத்தியில் நாம் பயன்படுத்த எதிர்பார்க்கும் ஒன்றல்ல, ஏனெனில் cacheWrite நிலையை மாற்றுகிறது, எனவே ஒரு பரிவர்த்தனையின் போது மட்டுமே அழைக்க முடியும். பரிவர்த்தனைகளுக்குத் திருப்பியனுப்பும் மதிப்புகள் இல்லை, அவற்றுக்கு முடிவுகள் இருந்தால் அந்த முடிவுகள் நிகழ்வுகளாக வெளியிடப்பட வேண்டும். எனவே cacheWrite திருப்பியனுப்பும் மதிப்பைச் சங்கிலிசார் குறியீட்டிலிருந்து மட்டுமே அணுக முடியும், மேலும் சங்கிலிசார் குறியீட்டிற்கு அளவுருத் தேக்ககம் தேவையில்லை.

        (_success,) = address(worm).call(_callInput);

<contract address>.call() இரண்டு திருப்பியனுப்பும் மதிப்புகளைக் கொண்டிருந்தாலும், நாம் முதலாவதைப் பற்றி மட்டுமே கவலைப்படுகிறோம் என்பதை Solidity இடம் இப்படித்தான் சொல்கிறோம்.

        (_success,) = address(worm).call(_callInput);
        assertEq(_success, false);

நாம் கீழ்நிலை <address>.call() சார்பைப் பயன்படுத்துவதால், நாம் vm.expectRevert() ஐப் பயன்படுத்த முடியாது, மேலும் அழைப்பிலிருந்து நாம் பெறும் பூலியன் வெற்றி மதிப்பைப் பார்க்க வேண்டும்.

Foundry இல் குறியீடு ஒரு நிகழ்வைச் சரியாக வெளியிடுகிறதா (opens in a new tab) என்பதை நாம் சரிபார்க்கும் வழி இதுதான்.

கிளையண்ட்

Solidity சோதனைகளில் உங்களுக்குக் கிடைக்காத ஒன்று, உங்கள் சொந்தச் செயலியில் வெட்டி ஒட்டக்கூடிய JavaScript குறியீடு ஆகும். அந்தக் குறியீட்டை எழுத, நான் WORM ஐ ஆப்டிமிசமின் (opens in a new tab) புதிய சோதனை வலையமைப்பான ஆப்டிமிசம் கோர்லிக்கு (opens in a new tab) பயன்படுத்தினேன். இது 0xd34335b1d818cee54e3323d3246bd31d94e6a78a (opens in a new tab) என்ற முகவரியில் உள்ளது.

கிளையண்டிற்கான JavaScript குறியீட்டை நீங்கள் இங்கே பார்க்கலாம் (opens in a new tab). இதைப் பயன்படுத்த:

  1. git களஞ்சியத்தை நகலெடுக்கவும் (Clone):

    git clone https://github.com/qbzzt/20220915-all-you-can-cache.git
    
  2. தேவையான தொகுப்புகளை நிறுவவும்:

    cd javascript
    yarn
    
  3. உள்ளமைவுக் கோப்பை நகலெடுக்கவும்:

    cp .env.example .env
    
  4. உங்கள் உள்ளமைவுக்கு .env ஐத் திருத்தவும்:

    அளவுருமதிப்பு
    MNEMONICஒரு பரிவர்த்தனைக்குச் செலுத்தப் போதுமான ETH உள்ள கணக்கிற்கான நினைவூட்டி (mnemonic). ஆப்டிமிசம் கோர்லி பிணையத்திற்கான இலவச ETH ஐ நீங்கள் இங்கே பெறலாம் (opens in a new tab).
    OPTIMISM_GOERLI_URLஆப்டிமிசம் கோர்லிக்கான URL. பொது இறுதிப்புள்ளியான https://goerli.optimism.io, விகித வரம்புக்குட்பட்டது, ஆனால் இங்கு நமக்குத் தேவையானதற்குப் போதுமானது
  5. index.js ஐ இயக்கவும்.

    node index.js
    

    இந்த மாதிரிச் செயலி முதலில் WORM இல் ஒரு உள்ளீட்டை எழுதுகிறது, அழைப்புத் தரவையும் Etherscan இல் உள்ள பரிவர்த்தனைக்கான இணைப்பையும் காட்டுகிறது. பின்னர் அது அந்த உள்ளீட்டை மீண்டும் படிக்கிறது, மேலும் அது பயன்படுத்தும் திறவுகோலையும் உள்ளீட்டில் உள்ள மதிப்புகளையும் (மதிப்பு, தொகுதி எண் மற்றும் ஆசிரியர்) காட்டுகிறது.

கிளையண்டின் பெரும்பகுதி சாதாரண Dapp JavaScript ஆகும். எனவே மீண்டும் நாம் சுவாரஸ்யமான பகுதிகளை மட்டுமே பார்ப்போம்.

.
.
.
const main = async () => {
    const func = await worm.WRITE_ENTRY_CACHED()

    // ஒவ்வொரு முறையும் புதிய திறவுகோல் தேவை
    const key = await worm.encodeVal(Number(new Date()))

கொடுக்கப்பட்ட ஒரு நேரப்பகுதியில் ஒரு முறை மட்டுமே எழுத முடியும், எனவே நேரப்பகுதிகளை மீண்டும் பயன்படுத்தவில்லை என்பதை உறுதிப்படுத்த நேரமுத்திரையைப் பயன்படுத்துகிறோம்.

const val = await worm.encodeVal("0x600D")

// ஒரு உள்ளீட்டை எழுதவும்
const calldata = func + key.slice(2) + val.slice(2)

அழைப்புத் தரவு ஒரு ஹெக்ஸ் சரமாக இருக்க வேண்டும் என்று Ethers எதிர்பார்க்கிறது, 0x ஐத் தொடர்ந்து இரட்டைப்படை எண்ணிக்கையிலான ஹெக்ஸாடெசிமல் இலக்கங்கள் இருக்க வேண்டும். key மற்றும் val இரண்டும் 0x உடன் தொடங்குவதால், நாம் அந்தத் தலைப்புகளை அகற்ற வேண்டும்.

const tx = await worm.populateTransaction.writeEntryCached()
tx.data = calldata

sentTx = await wallet.sendTransaction(tx)

Solidity சோதனைக் குறியீட்டைப் போலவே, தேக்ககப்படுத்தப்பட்ட சார்பை நாம் சாதாரணமாக அழைக்க முடியாது. அதற்குப் பதிலாக, நாம் ஒரு கீழ்நிலை பொறிமுறையைப் பயன்படுத்த வேண்டும்.

உள்ளீடுகளைப் படிக்க நாம் சாதாரண பொறிமுறையைப் பயன்படுத்தலாம். view சார்புகளுடன் அளவுருத் தேக்ககத்தைப் பயன்படுத்த வேண்டிய அவசியமில்லை.

முடிவுரை

இந்தக் கட்டுரையில் உள்ள குறியீடு ஒரு கருத்தின் நிரூபணம் (proof of concept) ஆகும், இதன் நோக்கம் யோசனையை எளிதாகப் புரிந்துகொள்ளச் செய்வதாகும். உற்பத்திக்குத் தயாரான அமைப்புக்கு நீங்கள் சில கூடுதல் செயல்பாடுகளைச் செயல்படுத்த விரும்பலாம்:

  • uint256 ஆக இல்லாத மதிப்புகளைக் கையாளவும். எடுத்துக்காட்டாக, சரங்கள் (strings).

  • உலகளாவிய தேக்ககத்திற்குப் பதிலாக, பயனர்களுக்கும் தேக்ககங்களுக்கும் இடையே ஒரு மேப்பிங் இருக்கலாம். வெவ்வேறு பயனர்கள் வெவ்வேறு மதிப்புகளைப் பயன்படுத்துகிறார்கள்.

  • முகவரிகளுக்குப் பயன்படுத்தப்படும் மதிப்புகள் பிற நோக்கங்களுக்காகப் பயன்படுத்தப்படும் மதிப்புகளிலிருந்து வேறுபட்டவை. முகவரிகளுக்கு மட்டும் தனித் தேக்ககத்தை வைத்திருப்பது அர்த்தமுள்ளதாக இருக்கலாம்.

  • தற்போது, தேக்ககத் திறவுகோல்கள் "முதலில் வருபவருக்கு, சிறிய திறவுகோல்" என்ற அல்காரிதத்தில் உள்ளன. முதல் பதினாறு மதிப்புகளை ஒற்றை பைட்டாக அனுப்பலாம். அடுத்த 4080 மதிப்புகளை இரண்டு பைட்டுகளாக அனுப்பலாம். அடுத்த சுமார் மில்லியன் மதிப்புகள் மூன்று பைட்டுகள் போன்றவை. ஒரு உற்பத்தி அமைப்பு தேக்கக உள்ளீடுகளில் பயன்பாட்டு கவுண்டர்களை வைத்திருக்க வேண்டும் மற்றும் அவற்றை மறுசீரமைக்க வேண்டும், இதனால் பதினாறு மிகவும் பொதுவான மதிப்புகள் ஒரு பைட்டாகவும், அடுத்த 4080 மிகவும் பொதுவான மதிப்புகள் இரண்டு பைட்டுகளாகவும் இருக்கும்.

    இருப்பினும், இது ஆபத்தான செயல்பாடாக இருக்கலாம். பின்வரும் நிகழ்வுகளின் வரிசையை கற்பனை செய்து பாருங்கள்:

    1. நோம் நேவ் (Noam Naive) தான் டோக்கன்களை அனுப்ப விரும்பும் முகவரியைக் குறியாக்கம் செய்ய encodeVal ஐ அழைக்கிறார். அந்த முகவரி செயலியில் முதலில் பயன்படுத்தப்பட்டவற்றில் ஒன்றாகும், எனவே குறியாக்கம் செய்யப்பட்ட மதிப்பு 0x06 ஆகும். இது ஒரு view சார்பு, பரிவர்த்தனை அல்ல, எனவே இது நோமுக்கும் அவர் பயன்படுத்தும் கணுவுக்கும் இடையில் உள்ளது, வேறு யாருக்கும் இதைப் பற்றித் தெரியாது

    2. ஓவன் ஓனர் (Owen Owner) தேக்கக மறுவரிசைப்படுத்தும் செயல்பாட்டை இயக்குகிறார். மிகச் சிலரே அந்த முகவரியைப் பயன்படுத்துகிறார்கள், எனவே அது இப்போது 0x201122 ஆகக் குறியாக்கம் செய்யப்பட்டுள்ளது. வேறுபட்ட மதிப்பான 1018 க்கு 0x06 ஒதுக்கப்பட்டுள்ளது.

    3. நோம் நேவ் தனது டோக்கன்களை 0x06 க்கு அனுப்புகிறார். அவை 0x0000000000000000000000000de0b6b3a7640000 என்ற முகவரிக்குச் செல்கின்றன, மேலும் அந்த முகவிக்கான தனிப்பட்ட திறவுகோல் யாருக்கும் தெரியாது என்பதால், அவை அங்கேயே சிக்கிக்கொள்கின்றன. நோம் மகிழ்ச்சியாக இல்லை.

    இந்தச் சிக்கலையும், தேக்கக மறுவரிசைப்படுத்தலின் போது மெம்பூலில் உள்ள பரிவர்த்தனைகளின் தொடர்புடைய சிக்கலையும் தீர்க்க வழிகள் உள்ளன, ஆனால் நீங்கள் அதைப் பற்றி அறிந்திருக்க வேண்டும்.

நான் ஆப்டிமிசம் ஊழியராக இருப்பதாலும், எனக்குச் சிறப்பாகத் தெரிந்த ரோலப் இது என்பதாலும், நான் இங்கே ஆப்டிமிசம் மூலம் தேக்ககப்படுத்துதலை விளக்கினேன். ஆனால் உள்ளகச் செயலாக்கத்திற்குக் குறைந்தபட்சச் செலவை வசூலிக்கும் எந்தவொரு ரோலப்புடனும் இது வேலை செய்ய வேண்டும், இதனால் ஒப்பிடுகையில் பரிவர்த்தனைத் தரவை அடுக்கு 1 (l1) இல் எழுதுவது முக்கியச் செலவாகும்.

எனது மேலும் பல பணிகளை இங்கே காணவும் (opens in a new tab).