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

உங்களால் முடிந்த அனைத்தையும் கேச் (cache) செய்யுங்கள்

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

ரோலப்களைப் (rollups) பயன்படுத்தும் போது, பரிவர்த்தனையில் உள்ள ஒரு பைட்டின் (byte) விலை, சேமிப்பக ஸ்லாட்டின் (storage slot) விலையை விட மிகவும் அதிகம். எனவே, முடிந்தவரை அதிக தகவல்களை ஆன்செயினில் (onchain) கேச் (cache) செய்வது அர்த்தமுள்ளதாக இருக்கும்.

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

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

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

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

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

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

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

    கால்டேட்டாவில் உள்ள பைட்டுகள் (Bytes in calldata)கேச் விசை (Cache key)
    0x0F0x0F
    0x10,0x100x10
    0x12,0xAC0x02AC
    0x2D,0xEA, 0xD60x0DEAD6

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

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

1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.13;
3
4
5contract Cache {
6
7 bytes1 public constant INTO_CACHE = 0xFF;
8 bytes1 public constant DONT_CACHE = 0xFE;

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

1
2 mapping(uint => uint) public val2key;

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

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

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

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

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

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

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

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

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

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

ரிவர்ஸ் லுக்அப்பை (reverse lookup) சேர்க்கவும் (மதிப்பிலிருந்து விசைக்கு).

1 key2val.push(_value);

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

1 return key2val.length;
2 } // cacheWrite

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

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

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

1 {
2 uint _retVal;
3
4 require(length < 0x21,
5 "_calldataVal length limit is 32 bytes");
6 require(length + startByte <= msg.data.length,
7 "_calldataVal trying to read beyond calldatasize");

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

1 assembly {
2 _retVal := calldataload(startByte)
3 }

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

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

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

1 return _retVal;
2 } // _calldataVal
3
4
5 // _fromByte இலிருந்து தொடங்கி, கால்டேட்டாவிலிருந்து (calldata) ஒரு அளவுருவை (parameter) படிக்கவும்
6 function _readParam(uint _fromByte) internal
7 returns (uint _nextByte, uint _parameterValue)
8 {

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

1 // மீதமுள்ளவற்றை எவ்வாறு விளக்குவது என்பதை முதல் பைட் (byte) நமக்குக் கூறுகிறது
2 uint8 _firstByte;
3
4 _firstByte = uint8(_calldataVal(_fromByte, 1));

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

1
2 // மதிப்பைப் படிக்கவும், ஆனால் அதை கேச்சில் எழுத வேண்டாம்
3 if (_firstByte == uint8(DONT_CACHE))
4 return(_fromByte+33, _calldataVal(_fromByte+1, 32));
5
6 // மதிப்பைப் படித்து, அதை கேச்சில் எழுதவும்
7 if (_firstByte == uint8(INTO_CACHE)) {
8 uint _param = _calldataVal(_fromByte+1, 32);
9 cacheWrite(_param);
10 return(_fromByte+33, _param);
11 }
12
13 // நாம் இங்கு வந்துள்ளோம் என்றால், கேச்சிலிருந்து படிக்க வேண்டும் என்று அர்த்தம்
14
15 // படிக்க வேண்டிய கூடுதல் பைட்டுகளின் எண்ணிக்கை
16 uint8 _extraBytes = _firstByte / 16;

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

1 uint _key = (uint256(_firstByte & 0x0F) << (8*_extraBytes)) +
2 _calldataVal(_fromByte+1, _extraBytes);
3
4 return (_fromByte+_extraBytes+1, cacheRead(_key));
5
6 } // _readParam
7
8
9 // n அளவுருக்களைப் படிக்கவும் (செயல்பாடுகளுக்கு எத்தனை அளவுருக்கள் எதிர்பார்க்கப்படுகின்றன என்பது தெரியும்)
10 function _readParams(uint _paramNum) internal returns (uint[] memory) {

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

1 // நாம் படித்த அளவுருக்கள்
2 uint[] memory params = new uint[](_paramNum);
3
4 // அளவுருக்கள் பைட் 4 இல் தொடங்குகின்றன, அதற்கு முன் அது செயல்பாட்டின் கையொப்பம் (function signature) ஆகும்
5 uint _atByte = 4;
6
7 for(uint i=0; i<_paramNum; i++) {
8 (_atByte, params[i]) = _readParam(_atByte);
9 }

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

1
2 return(params);
3 } // readParams
4
5 // _readParams ஐ சோதிக்க, நான்கு அளவுருக்களைப் படிப்பதை சோதிக்கவும்
6 function fourParam() public
7 returns (uint256,uint256,uint256,uint256)
8 {
9 uint[] memory params;
10 params = _readParams(4);
11 return (params[0], params[1], params[2], params[3]);
12 } // fourParam

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

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

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

1 uint _key = val2key[_val];
2
3 // மதிப்பு இன்னும் கேச்சில் இல்லை, அதைச் சேர்க்கவும்
4 if (_key == 0)
5 return bytes.concat(INTO_CACHE, bytes32(_val));

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

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

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

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

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

1 // பின்வரும் வரிகளை ஒரு லூப்பாக (loop) செய்ய ஒரு புத்திசாலித்தனமான வழி இருக்கலாம்,
2 // ஆனால் இது ஒரு view செயல்பாடு, எனவே நான் நிரலாளரின் நேரம் மற்றும்
3 // எளிமைக்காக மேம்படுத்துகிறேன்.
4
5 if (_key < 16*256**2)
6 return bytes.concat(bytes3(uint24(_key) | (0x2 * 16 * 256**2)));
7 if (_key < 16*256**3)
8 return bytes.concat(bytes4(uint32(_key) | (0x3 * 16 * 256**3)));
9 .
10 .
11 .
12 if (_key < 16*256**14)
13 return bytes.concat(bytes15(uint120(_key) | (0xE * 16 * 256**14)));
14 if (_key < 16*256**15)
15 return bytes.concat(bytes16(uint128(_key) | (0xF * 16 * 256**15)));

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

1 // நாம் இங்கு வந்தால், ஏதோ தவறு என்று அர்த்தம்.
2 revert("Error in encodeVal, should not happen");

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

1 } // encodeVal
2
3} // Cache

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

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

1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.13;
3
4import "forge-std/Test.sol";
5
6
7// கன்சோலுக்கு `forge test -vv` ஐ இயக்க வேண்டும்.
8import "forge-std/console.sol";

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

1import "src/Cache.sol";

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

1contract CacheTest is Test {
2 Cache cache;
3
4 function setUp() public {
5 cache = new Cache();
6 }

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

1 function testCaching() public {

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

1 for(uint i=1; i<5000; i++) {
2 cache.cacheWrite(i*i);
3 }
4
5 for(uint i=1; i<5000; i++) {
6 assertEq(cache.cacheRead(i), i*i);

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

1 }
2 } // testCaching
3
4
5 // ஒரே மதிப்பை பல முறை கேச் செய்யவும், விசை அப்படியே
6 // இருப்பதை உறுதிசெய்யவும்
7 function testRepeatCaching() public {
8 for(uint i=1; i<100; i++) {
9 uint _key1 = cache.cacheWrite(i);
10 uint _key2 = cache.cacheWrite(i);
11 assertEq(_key1, _key2);
12 }

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

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

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

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

bytes memory பஃபரிலிருந்து (buffer) 256 பிட் சொல்லைப் படிக்கவும். கேச்சைப் பயன்படுத்தும் செயல்பாட்டு அழைப்பை (function call) இயக்கும்போது சரியான முடிவுகளைப் பெறுகிறோம் என்பதைச் சரிபார்க்க இந்தப் பயன்பாட்டுச் செயல்பாடு (utility function) அனுமதிக்கிறது.

1 {
2 require(_bytes.length >= _start + 32, "toUint256_outOfBounds");
3 uint256 tempUint;
4
5 assembly {
6 tempUint := mload(add(add(_bytes, 0x20), _start))
7 }

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

1
2 return tempUint;
3 } // toUint256
4
5 // fourParams() க்கான செயல்பாட்டு கையொப்பம், வழங்கியவர்கள்
6 // https://www.4byte.directory/signatures/?bytes4_signature=0x3edc1e6d
7 bytes4 constant FOUR_PARAMS = 0x3edc1e6d;
8
9 // சரியான மதிப்புகளைத் திரும்பப் பெறுகிறோமா என்பதைப் பார்க்க சில நிலையான மதிப்புகள்
10 uint256 constant VAL_A = 0xDEAD60A7;
11 uint256 constant VAL_B = 0xBEEF;
12 uint256 constant VAL_C = 0x600D;
13 uint256 constant VAL_D = 0x600D60A7;

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

1 function testReadParam() public {

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

1 address _cacheAddr = address(cache);
2 bool _success;
3 bytes memory _callInput;
4 bytes memory _callOutput;

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

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

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

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

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

1 .
2 .
3 .
4 );
5 (_success, _callOutput) = _cacheAddr.call(_callInput);

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

1 assertEq(_success, true);

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

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

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

1 assertEq(toUint256(_callOutput,0), VAL_A);
2 assertEq(toUint256(_callOutput,32), VAL_B);
3 assertEq(toUint256(_callOutput,64), VAL_C);
4 assertEq(toUint256(_callOutput,96), VAL_C);

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

1 // இரண்டாவது அழைப்பு, நாம் கேச்சைப் பயன்படுத்தலாம்
2 _callInput = bytes.concat(
3 FOUR_PARAMS,
4
5 // கேச்சில் உள்ள முதல் மதிப்பு
6 bytes1(0x01),

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

1 // இரண்டாவது மதிப்பு, அதை கேச்சில் சேர்க்க வேண்டாம்
2 cache.DONT_CACHE(),
3 bytes32(VAL_B),
4
5 // மூன்றாவது மற்றும் நான்காவது மதிப்புகள், அதே மதிப்பு
6 bytes1(0x02),
7 bytes1(0x02)
8 );
9 .
10 .
11 .
12 } // testReadParam

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

1 function testEncodeVal() public {

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

1 .
2 .
3 .
4 _callInput = bytes.concat(
5 FOUR_PARAMS,
6 cache.encodeVal(VAL_A),
7 cache.encodeVal(VAL_B),
8 cache.encodeVal(VAL_C),
9 cache.encodeVal(VAL_D)
10 );
11 .
12 .
13 .
14 assertEq(_callInput.length, 4+1*4);
15 } // testEncodeVal

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

1 // விசை ஒரு பைட்டை விட அதிகமாக இருக்கும்போது encodeVal ஐ சோதிக்கவும்
2 // அதிகபட்சம் மூன்று பைட்டுகள், ஏனெனில் கேச்சை நான்கு பைட்டுகளுக்கு நிரப்ப
3 // அதிக நேரம் எடுக்கும்.
4 function testEncodeValBig() public {
5 // கேச்சில் பல மதிப்புகளை வைக்கவும்.
6 // விஷயங்களை எளிமையாக வைத்திருக்க, மதிப்பு n க்கு விசை n ஐப் பயன்படுத்தவும்.
7 for(uint i=1; i<0x1FFF; i++) {
8 cache.cacheWrite(i);
9 }

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

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

1 .
2 .
3 .
4
5 _callInput = bytes.concat(
6 FOUR_PARAMS,
7 cache.encodeVal(0x000F), // ஒரு பைட் 0x0F
8 cache.encodeVal(0x0010), // இரண்டு பைட்டுகள் 0x1010
9 cache.encodeVal(0x0100), // இரண்டு பைட்டுகள் 0x1100
10 cache.encodeVal(0x1000) // மூன்று பைட்டுகள் 0x201000
11 );

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

1 .
2 .
3 .
4 .
5 } // testEncodeValBig
6
7
8 // மிகச் சிறிய இடையகத்துடன் (buffer) நாம் ஒரு ரிவர்ட்டை (revert) பெறுகிறோமா என்று சோதிக்கவும்
9 function testShortCalldata() public {

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

1 .
2 .
3 .
4 (_success, _callOutput) = _cacheAddr.call(_callInput);
5 assertEq(_success, false);
6 } // testShortCalldata

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

1 // Call with cache keys that aren't there
2 function testNoCacheKey() public {
3 .
4 .
5 .
6 _callInput = bytes.concat(
7 FOUR_PARAMS,
8
9 // First value, add it to the cache
10 cache.INTO_CACHE(),
11 bytes32(VAL_A),
12
13 // Second value
14 bytes1(0x0F),
15 bytes2(0x1234),
16 bytes11(0xA10102030405060708090A)
17 );

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

1 .
2 .
3 .
4 // மிக நீண்ட இடையகத்துடன் எல்லாம் சரியாக வேலை செய்கிறதா என்று சோதிக்கவும்
5 function testLongCalldata() public {
6 address _cacheAddr = address(cache);
7 bool _success;
8 bytes memory _callInput;
9 bytes memory _callOutput;
10
11 // முதல் அழைப்பு, கேச் காலியாக உள்ளது
12 _callInput = bytes.concat(
13 FOUR_PARAMS,
14
15 // முதல் மதிப்பு, அதை கேச்சில் சேர்க்கவும்
16 cache.INTO_CACHE(), bytes32(VAL_A),
17
18 // இரண்டாவது மதிப்பு, அதை கேச்சில் சேர்க்கவும்
19 cache.INTO_CACHE(), bytes32(VAL_B),
20
21 // மூன்றாவது மதிப்பு, அதை கேச்சில் சேர்க்கவும்
22 cache.INTO_CACHE(), bytes32(VAL_C),
23
24 // நான்காவது மதிப்பு, அதை கேச்சில் சேர்க்கவும்
25 cache.INTO_CACHE(), bytes32(VAL_D),
26
27 // மேலும் "நல்ல அதிர்ஷ்டத்திற்காக" மற்றொரு மதிப்பு
28 bytes4(0x31112233)
29 );

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

1 (_success, _callOutput) = _cacheAddr.call(_callInput);
2 assertEq(_success, true);
3 .
4 .
5 .
6 } // testLongCalldata
7
8} // CacheTest
9

ஒரு மாதிரிப் பயன்பாடு

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

ஒப்பந்தம்

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

1import "./Cache.sol";
2
3contract WORM is Cache {

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

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

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

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

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

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

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

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

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

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

1 function testWReadWrite() public {
2 worm.writeEntry(0xDEAD, 0x60A7);
3
4 vm.expectRevert(bytes("entry already written"));
5 worm.writeEntry(0xDEAD, 0xBEEF);

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

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

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

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

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

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

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

1 event EntryWritten(uint indexed key, uint indexed value);
2
3 .
4 .
5 .
6
7 _callInput = bytes.concat(
8 worm.WRITE_ENTRY_CACHED(), worm.encodeVal(a), worm.encodeVal(b));
9 vm.expectEmit(true, true, false, false);
10 emit EntryWritten(a, b);
11 (_success,) = address(worm).call(_callInput);

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

கிளையன்ட்

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

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

  1. git ரெபோசிட்டரியை (repository) குளோன் (clone) செய்யவும்:

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

    1cd javascript
    2yarn
  3. உள்ளமைவுக் கோப்பை (configuration file) நகலெடுக்கவும்:

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

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

    1node index.js

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

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

1.
2.
3.
4const main = async () => {
5 const func = await worm.WRITE_ENTRY_CACHED()
6
7 // ஒவ்வொரு முறையும் புதிய விசை தேவை
8 const key = await worm.encodeVal(Number(new Date()))

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

1const val = await worm.encodeVal("0x600D")
2
3// ஒரு உள்ளீட்டை எழுதவும்
4const calldata = func + key.slice(2) + val.slice(2)

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

1const tx = await worm.populateTransaction.writeEntryCached()
2tx.data = calldata
3
4sentTx = await wallet.sendTransaction(tx)

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

1 .
2 .
3 .
4 // சற்று முன் எழுதிய உள்ளீட்டைப் படிக்கவும்
5 const realKey = '0x' + key.slice(4) // FF கொடியை (flag) அகற்றவும்
6 const entryRead = await worm.readEntry(realKey)
7 .
8 .
9 .

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

முடிவுரை

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

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

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

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

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

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

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

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

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

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

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

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

பக்கம் கடைசியாகப் புதுப்பிக்கப்பட்டது: 3 மார்ச், 2026

இந்த வழிகாட்டி பயனுள்ளதாக இருந்ததா?