مرکزی مواد پر جائیں

سب کچھ جو آپ کیش کر سکتے ہیں

لیئر 2
کیشنگ
اسٹوریج
اسکیلنگ
درمیانی
اوری پومرانٹز
15 ستمبر، 2022
27 منٹ کی پڑھائی

رول اپس (rollups) کا استعمال کرتے وقت ٹرانزیکشن میں ایک بائٹ کی قیمت اسٹوریج سلاٹ کی قیمت سے کہیں زیادہ مہنگی ہوتی ہے۔ اس لیے، زیادہ سے زیادہ معلومات کو آن چین (onchain) کیش (cache) کرنا سمجھداری کی بات ہے۔

اس مضمون میں آپ سیکھیں گے کہ کیشنگ کنٹریکٹ کو کس طرح بنایا اور استعمال کیا جائے تاکہ کسی بھی پیرامیٹر کی ویلیو جس کے متعدد بار استعمال ہونے کا امکان ہو، اسے کیش کیا جا سکے اور (پہلی بار کے بعد) بہت کم بائٹس کے ساتھ استعمال کے لیے دستیاب ہو، اور اس کیش کو استعمال کرنے والا آف چین (offchain) کوڈ کیسے لکھا جائے۔

اگر آپ مضمون کو چھوڑ کر صرف سورس کوڈ دیکھنا چاہتے ہیں، تو وہ یہاں ہے (opens in a new tab)۔ ڈیولپمنٹ اسٹیک Foundry (opens in a new tab) ہے۔

مجموعی ڈیزائن

سادگی کی خاطر ہم فرض کریں گے کہ تمام ٹرانزیکشن پیرامیٹرز uint256 ہیں، جو 32 بائٹس طویل ہیں۔ جب ہمیں کوئی ٹرانزیکشن موصول ہوتی ہے، تو ہم ہر پیرامیٹر کو اس طرح پارس (parse) کریں گے:

  1. اگر پہلی بائٹ 0xFF ہے، تو اگلی 32 بائٹس کو پیرامیٹر ویلیو کے طور پر لیں اور اسے کیش میں لکھیں۔

  2. اگر پہلی بائٹ 0xFE ہے، تو اگلی 32 بائٹس کو پیرامیٹر ویلیو کے طور پر لیں لیکن اسے کیش میں نہ لکھیں۔

  3. کسی بھی دوسری ویلیو کے لیے، اوپر والی چار بٹس کو اضافی بائٹس کی تعداد کے طور پر لیں، اور نیچے والی چار بٹس کو کیش کی (key) کی سب سے اہم بٹس (most significant bits) کے طور پر لیں۔ یہاں کچھ مثالیں ہیں:

    کال ڈیٹا میں بائٹسکیش کی (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 گیس ہوتی ہے، اس لیے ہم اسے اختیاری بناتے ہیں۔

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

ویلیوز اور ان کی کیز (keys) کے درمیان ایک میپنگ (mapping) (opens in a new tab)۔ ٹرانزیکشن بھیجنے سے پہلے ویلیوز کو انکوڈ کرنے کے لیے یہ معلومات ضروری ہے۔

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 // صرف پبلک ہے تاکہ ٹیسٹ کام کر سکے
3 function cacheWrite(uint _value) public returns (uint) {
4 // اگر قدر پہلے سے کیشے میں موجود ہے، تو موجودہ کلید واپس کریں
5 if (val2key[_value] != 0) {
6 return val2key[_value];
7 }

ایک ہی ویلیو کو ایک سے زیادہ بار کیش میں رکھنے کا کوئی فائدہ نہیں۔ اگر ویلیو پہلے سے موجود ہے، تو بس موجودہ کی (key) واپس کر دیں۔

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;

ریورس لک اپ (ویلیو سے کی تک) شامل کریں۔

1 key2val.push(_value);

فارورڈ لک اپ (کی سے ویلیو تک) شامل کریں۔ چونکہ ہم ویلیوز کو ترتیب وار تفویض کرتے ہیں اس لیے ہم اسے آخری ایرے ویلیو کے بعد شامل کر سکتے ہیں۔

1 return key2val.length;
2 } // cacheWrite

key2val کی نئی لمبائی واپس کریں، جو وہ سیل ہے جہاں نئی ویلیو اسٹور کی گئی ہے۔

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

یہ فنکشن صوابدیدی لمبائی (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 بائٹ کی ویلیو پڑھتا ہے۔ یہ تب بھی کام کرتا ہے اگر کال ڈیٹا startByte+32 سے پہلے رک جائے کیونکہ EVM میں غیر شروع شدہ (uninitialized) جگہ کو صفر سمجھا جاتا ہے۔

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

ہمیں لازمی طور پر 32 بائٹ کی ویلیو نہیں چاہیے۔ یہ اضافی بائٹس سے چھٹکارا دلاتا ہے۔

1 return _retVal;
2 } // _calldataVal
3
4
5 // کال ڈیٹا (calldata) سے ایک پیرامیٹر پڑھیں، جس کی شروعات _fromByte سے ہو
6 function _readParam(uint _fromByte) internal
7 returns (uint _nextByte, uint _parameterValue)
8 {

کال ڈیٹا سے ایک واحد پیرامیٹر پڑھیں۔ نوٹ کریں کہ ہمیں نہ صرف وہ ویلیو واپس کرنے کی ضرورت ہے جو ہم نے پڑھی ہے، بلکہ اگلی بائٹ کا مقام بھی کیونکہ پیرامیٹرز 1 بائٹ سے لے کر 33 بائٹس تک طویل ہو سکتے ہیں۔

1 // پہلا بائٹ ہمیں بتاتا ہے کہ باقی کو کیسے سمجھنا ہے
2 uint8 _firstByte;
3
4 _firstByte = uint8(_calldataVal(_fromByte, 1));

Solidity ممکنہ طور پر خطرناک امپلیسٹ ٹائپ کنورژنز (implicit type conversions) (opens in a new tab) کو منع کر کے بگز کی تعداد کو کم کرنے کی کوشش کرتی ہے۔ ایک ڈاؤن گریڈ، مثال کے طور پر 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 سے شروع ہوتے ہیں، اس سے پہلے یہ فنکشن کے دستخط (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 میں لکھنے کی اجازت دیتا ہے (نیچے کیش کی ٹیسٹنگ دیکھیں)۔ اس سے یونٹ ٹیسٹس بہت آسان ہو جاتے ہیں۔ یہ ایک فنکشن ہے جو چار پیرامیٹرز پڑھتا ہے اور انہیں واپس کرتا ہے تاکہ ٹیسٹ تصدیق کر سکے کہ وہ درست تھے۔

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 میں تمام غیر شروع شدہ (uninitialized) اسٹوریج کو صفر فرض کیا جاتا ہے۔ لہذا اگر ہم کسی ایسی ویلیو کی کی (key) تلاش کرتے ہیں جو وہاں نہیں ہے، تو ہمیں صفر ملتا ہے۔ اس صورت میں اسے انکوڈ کرنے والی بائٹس INTO_CACHE ہوتی ہیں (تاکہ اگلی بار اسے کیش کیا جا سکے)، جس کے بعد اصل ویلیو ہوتی ہے۔

1 // اگر کلید <0x10 ہے، تو اسے سنگل بائٹ کے طور پر واپس کریں
2 if (_key < 0x10)
3 return bytes.concat(bytes1(uint8(_key)));

سنگل بائٹس سب سے آسان ہیں۔ ہم صرف bytes.concat (opens in a new tab) کا استعمال کرتے ہیں تاکہ bytes<n> ٹائپ کو بائٹ ایرے میں تبدیل کیا جا سکے جو کسی بھی لمبائی کی ہو سکتی ہے۔ نام کے باوجود، یہ تب بھی ٹھیک کام کرتا ہے جب اسے صرف ایک آرگومنٹ فراہم کیا جائے۔

1 // دو بائٹ کی قدر، جسے 0x1vvv کے طور پر انکوڈ کیا گیا ہے
2 if (_key < 0x1000)
3 return bytes.concat(bytes2(uint16(_key) | 0x1000));

جب ہمارے پاس کوئی کی (key) ہوتی ہے جو 163 سے کم ہوتی ہے، تو ہم اسے دو بائٹس میں ظاہر کر سکتے ہیں۔ ہم پہلے _key کو، جو کہ 256 بٹ کی ویلیو ہے، 16 بٹ کی ویلیو میں تبدیل کرتے ہیں اور پہلی بائٹ میں اضافی بائٹس کی تعداد شامل کرنے کے لیے لاجیکل OR کا استعمال کرتے ہیں۔ پھر ہم اسے bytes2 ویلیو میں ڈال دیتے ہیں، جسے bytes میں تبدیل کیا جا سکتا ہے۔

1 // شاید مندرجہ ذیل لائنوں کو لوپ کے طور پر کرنے کا کوئی ہوشیار طریقہ موجود ہے،
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 بائٹس، وغیرہ) کو اسی طرح ہینڈل کیا جاتا ہے، بس مختلف فیلڈ سائز کے ساتھ۔

1 // اگر ہم یہاں پہنچتے ہیں، تو کچھ غلط ہے۔
2 revert("Error in encodeVal, should not happen");

اگر ہم یہاں پہنچتے ہیں تو اس کا مطلب ہے کہ ہمیں ایک ایسی کی (key) ملی ہے جو 16*25615 سے کم نہیں ہے۔ لیکن cacheWrite کیز کو محدود کرتا ہے اس لیے ہم 14*25616 تک بھی نہیں پہنچ سکتے (جس کی پہلی بائٹ 0xFE ہوگی، اس لیے یہ DONT_CACHE کی طرح نظر آئے گی)۔ لیکن اگر مستقبل کا کوئی پروگرامر کوئی بگ متعارف کراتا ہے تو اس صورت میں ٹیسٹ شامل کرنے پر ہمیں زیادہ لاگت نہیں آتی۔

کیش کی ٹیسٹنگ

Foundry کے فوائد میں سے ایک یہ ہے کہ یہ آپ کو Solidity میں ٹیسٹ لکھنے کی اجازت دیتا ہے (opens in a new tab)، جس سے یونٹ ٹیسٹ لکھنا آسان ہو جاتا ہے۔ Cache کلاس کے ٹیسٹ یہاں (opens in a new tab) ہیں۔ چونکہ ٹیسٹنگ کوڈ دہرایا جاتا ہے، جیسا کہ ٹیسٹس میں ہوتا ہے، یہ مضمون صرف دلچسپ حصوں کی وضاحت کرتا ہے۔

1 } // encodeVal
2
3} // کیشے
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";

یہ صرف بوائلرپلیٹ (boilerplate) ہے جو ٹیسٹ پیکج اور console.log استعمال کرنے کے لیے ضروری ہے۔

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) کا استعمال کرتے ہوئے۔ اس صورت میں، ہم چیک کرتے ہیں کہ جو ویلیو ہم نے لکھی ہے وہی ہم نے پڑھی ہے۔ ہم cache.cacheWrite کے نتیجے کو مسترد کر سکتے ہیں کیونکہ ہم جانتے ہیں کہ کیش کیز لینیئر (linearly) طور پر تفویض کی جاتی ہیں۔

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

نظریاتی طور پر کوئی ایسا بگ ہو سکتا ہے جو لگاتار کیش رائٹس (writes) کو متاثر نہ کرے۔ لہذا یہاں ہم کچھ ایسی رائٹس کرتے ہیں جو لگاتار نہیں ہیں اور دیکھتے ہیں کہ ویلیوز اب بھی دوبارہ نہیں لکھی گئیں۔

1 // میموری بفر سے ایک uint پڑھیں (یہ یقینی بنانے کے لیے کہ ہمیں وہ پیرامیٹرز واپس ملیں
2 // جو ہم نے بھیجے تھے)
3 function toUint256(bytes memory _bytes, uint256 _start) internal pure
4 returns (uint256)

bytes memory بفر سے 256 بٹ کا ورڈ پڑھیں۔ یہ یوٹیلیٹی فنکشن ہمیں اس بات کی تصدیق کرنے دیتا ہے کہ جب ہم کیش استعمال کرنے والی فنکشن کال چلاتے ہیں تو ہمیں درست نتائج موصول ہوتے ہیں۔

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 سے آگے ڈیٹا اسٹرکچرز کو سپورٹ نہیں کرتا، لہذا جب آپ کسی زیادہ نفیس ڈیٹا اسٹرکچر کا حوالہ دیتے ہیں، جیسے کہ میموری بفر _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 // یہ دیکھنے کے لیے کہ ہمیں درست قدریں واپس مل رہی ہیں، بس کچھ مستقل (constant) قدریں
10 uint256 constant VAL_A = 0xDEAD60A7;
11 uint256 constant VAL_B = 0xBEEF;
12 uint256 constant VAL_C = 0x600D;
13 uint256 constant VAL_D = 0x600D60A7;
سب دکھائیں

کچھ کنسٹنٹس (constants) جن کی ہمیں ٹیسٹنگ کے لیے ضرورت ہے۔

1 function testReadParam() public {

fourParams() کو کال کریں، جو ایک فنکشن ہے جو readParams استعمال کرتا ہے، تاکہ یہ ٹیسٹ کیا جا سکے کہ ہم پیرامیٹرز کو درست طریقے سے پڑھ سکتے ہیں۔

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,

ایک ہی کنٹریکٹ کے لیے کیشڈ فنکشنز (براہ راست ٹرانزیکشنز سے کالز کے لیے) اور نان کیشڈ فنکشنز (دوسرے اسمارٹ کنٹریکٹس سے کالز کے لیے) دونوں کو سپورٹ کرنا مفید ہے۔ ایسا کرنے کے لیے ہمیں درست فنکشن کو کال کرنے کے لیے Solidity میکانزم پر انحصار جاری رکھنے کی ضرورت ہے، بجائے اس کے کہ ہر چیز کو ایک fallback فنکشن (opens in a new tab) میں ڈال دیا جائے۔ ایسا کرنے سے کمپوزایبلٹی (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 شامل کرتے ہیں۔ ہمیں توقع ہوگی کہ پہلے کی کی (key) 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 تک کی تمام ویلیوز کو ترتیب سے کیش میں لکھتا ہے، تاکہ ہم ان ویلیوز کو انکوڈ کر سکیں اور جان سکیں کہ وہ کہاں جا رہی ہیں۔

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 );
سب دکھائیں

ایک بائٹ، دو بائٹ، اور تین بائٹ ویلیوز کا ٹیسٹ کریں۔ ہم اس سے آگے ٹیسٹ نہیں کرتے کیونکہ کافی اسٹیک اندراجات (کم از کم 0x10000000، تقریباً ایک چوتھائی ارب) لکھنے میں بہت زیادہ وقت لگے گا۔

1 .
2 .
3 .
4 .
5 } // testEncodeValBig
6
7
8 // ٹیسٹ کریں کہ انتہائی چھوٹے بفر کے ساتھ ہمیں ریورٹ (revert) ملتا ہے
9 function testShortCalldata() public {
سب دکھائیں

ٹیسٹ کریں کہ غیر معمولی صورت میں کیا ہوتا ہے جہاں کافی پیرامیٹرز نہیں ہوتے ہیں۔

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

چونکہ یہ ریورٹ (revert) ہوتا ہے، اس لیے ہمیں جو نتیجہ ملنا چاہیے وہ 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) کو مفید ہونے کے لیے چین کے باہر سے آنے والی درخواستوں پر کارروائی کرنے کے قابل ہونا چاہیے۔ یہ مضمون ظاہر کرتا ہے کہ WORM کے ساتھ ڈیپ میں کیشنگ کا استعمال کیسے کیا جائے، جس کا مطلب ہے "Write Once, Read Many" (ایک بار لکھیں، کئی بار پڑھیں)۔ اگر کوئی کی (key) ابھی تک نہیں لکھی گئی ہے، تو آپ اس پر ایک ویلیو لکھ سکتے ہیں۔ اگر کی پہلے سے لکھی ہوئی ہے، تو آپ کو ریورٹ ملتا ہے۔

کنٹریکٹ

یہ کنٹریکٹ ہے (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 تصریحات (specifications) کی پیروی نہیں کرتے ہیں، اس لیے بہتر ہے کہ فنکشن میں کسی بھی پیرامیٹر کا اعلان نہ کیا جائے۔

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) ویری ایبل کے طور پر بیان کرتے ہیں، اسے بیرونی طور پر پڑھنے کے لیے اس کے لیے گیٹر (getter) فنکشن، worm.WRITE_ENTRY_CACHED() استعمال کرنا ضروری ہے۔

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

ریڈ (read) فنکشن ایک 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>() استعمال کرتے ہیں۔

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

یہاں ہم اس حقیقت کا استعمال کرتے ہیں کہ cacheWrite کیش کی (key) واپس کرتا ہے۔ یہ کوئی ایسی چیز نہیں ہے جس کی ہم پروڈکشن میں استعمال کرنے کی توقع کریں گے، کیونکہ cacheWrite اسٹیٹ (state) کو تبدیل کرتا ہے، اور اس لیے اسے صرف ٹرانزیکشن کے دوران کال کیا جا سکتا ہے۔ ٹرانزیکشنز کی ریٹرن ویلیوز نہیں ہوتیں، اگر ان کے نتائج ہوں تو ان نتائج کو ایونٹس کے طور پر خارج (emit) کیا جانا چاہیے۔ لہذا cacheWrite کی ریٹرن ویلیو صرف آن چین کوڈ سے قابل رسائی ہے، اور آن چین کوڈ کو پیرامیٹر کیشنگ کی ضرورت نہیں ہے۔

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

اس طرح ہم Solidity کو بتاتے ہیں کہ اگرچہ <contract address>.call() کی دو ریٹرن ویلیوز ہیں، ہمیں صرف پہلی کی پرواہ ہے۔

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 میں ایونٹ کو درست طریقے سے خارج (emit) کرتا ہے (opens in a new tab)۔

کلائنٹ

ایک چیز جو آپ کو Solidity ٹیسٹس کے ساتھ نہیں ملتی وہ JavaScript کوڈ ہے جسے آپ کاٹ کر اپنی ایپلی کیشن میں پیسٹ کر سکتے ہیں۔ اس کوڈ کو لکھنے کے لیے میں نے WORM کو Optimism Goerli (opens in a new tab) پر ڈیپلائے کیا، جو Optimism کا (opens in a new tab) نیا ٹیسٹ نیٹ ہے۔ یہ ایڈریس 0xd34335b1d818cee54e3323d3246bd31d94e6a78a (opens in a new tab) پر ہے۔

آپ کلائنٹ کے لیے JavaScript کوڈ یہاں دیکھ سکتے ہیں (opens in a new tab)۔ اسے استعمال کرنے کے لیے:

  1. گٹ (git) ریپوزٹری کو کلون کریں:

    1git clone https://github.com/qbzzt/20220915-all-you-can-cache.git
  2. ضروری پیکجز انسٹال کریں:

    1cd javascript
    2yarn
  3. کنفیگریشن فائل کاپی کریں:

    1cp .env.example .env
  4. اپنی کنفیگریشن کے لیے .env میں ترمیم کریں:

    پیرامیٹرویلیو
    MNEMONICایک اکاؤنٹ کے لیے نیمونک (mnemonic) جس کے پاس ٹرانزیکشن کی ادائیگی کے لیے کافی ETH ہے۔ آپ Optimism Goerli نیٹ ورک کے لیے مفت ETH یہاں سے حاصل کر سکتے ہیں (opens in a new tab)۔
    OPTIMISM_GOERLI_URLOptimism Goerli کا URL۔ پبلک اینڈ پوائنٹ، https://goerli.optimism.io، ریٹ لمیٹڈ (rate limited) ہے لیکن ہماری ضرورت کے لیے کافی ہے۔
  5. index.js چلائیں۔

    1node index.js

    یہ نمونہ ایپلی کیشن پہلے WORM میں ایک انٹری لکھتی ہے، کال ڈیٹا اور Etherscan پر ٹرانزیکشن کا لنک دکھاتی ہے۔ پھر یہ اس انٹری کو واپس پڑھتی ہے، اور اس کی استعمال کردہ کی (key) اور انٹری میں موجود ویلیوز (ویلیو، بلاک نمبر، اور مصنف) دکھاتی ہے۔

کلائنٹ کا زیادہ تر حصہ عام 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()))

دیے گئے سلاٹ میں صرف ایک بار لکھا جا سکتا ہے، اس لیے ہم ٹائم اسٹیمپ کا استعمال کرتے ہیں تاکہ یہ یقینی بنایا جا سکے کہ ہم سلاٹس کو دوبارہ استعمال نہیں کرتے ہیں۔

1const val = await worm.encodeVal("0x600D")
2
3// ایک اندراج (entry) لکھیں
4const calldata = func + key.slice(2) + val.slice(2)

Ethers توقع کرتا ہے کہ کال ڈیٹا ایک ہیکس (hex) اسٹرنگ ہوگا، 0x جس کے بعد ہیکسا ڈیسیمل ہندسوں کی جفت تعداد ہوگی۔ چونکہ key اور val دونوں 0x سے شروع ہوتے ہیں، اس لیے ہمیں ان ہیڈرز کو ہٹانے کی ضرورت ہے۔

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 فلیگ کو ہٹا دیں
6 const entryRead = await worm.readEntry(realKey)
7 .
8 .
9 .
سب دکھائیں

انٹریز پڑھنے کے لیے ہم عام میکانزم استعمال کر سکتے ہیں۔ view فنکشنز کے ساتھ پیرامیٹر کیشنگ استعمال کرنے کی کوئی ضرورت نہیں ہے۔

نتیجہ

اس مضمون میں موجود کوڈ ایک پروف آف کانسیپٹ (proof of concept) ہے، جس کا مقصد خیال کو سمجھنے میں آسان بنانا ہے۔ پروڈکشن کے لیے تیار سسٹم کے لیے آپ کچھ اضافی فعالیت لاگو کرنا چاہیں گے:

  • ان ویلیوز کو ہینڈل کریں جو uint256 نہیں ہیں۔ مثال کے طور پر، اسٹرنگز (strings)۔

  • گلوبل کیش کے بجائے، شاید صارفین اور کیشز کے درمیان میپنگ ہو۔ مختلف صارفین مختلف ویلیوز استعمال کرتے ہیں۔

  • ایڈریسز کے لیے استعمال ہونے والی ویلیوز دیگر مقاصد کے لیے استعمال ہونے والی ویلیوز سے الگ ہوتی ہیں۔ صرف ایڈریسز کے لیے ایک الگ کیش رکھنا سمجھداری کی بات ہو سکتی ہے۔

  • فی الحال، کیش کیز "پہلے آئیں، سب سے چھوٹی کی" الگورتھم پر ہیں۔ پہلی سولہ ویلیوز کو ایک بائٹ کے طور پر بھیجا جا سکتا ہے۔ اگلی 4080 ویلیوز کو دو بائٹس کے طور پر بھیجا جا سکتا ہے۔ اگلی تقریباً دس لاکھ ویلیوز تین بائٹس کی ہیں، وغیرہ۔ ایک پروڈکشن سسٹم کو کیش انٹریز پر استعمال کے کاؤنٹرز رکھنے چاہئیں اور انہیں دوبارہ منظم کرنا چاہیے تاکہ سولہ سب سے عام ویلیوز ایک بائٹ کی ہوں، اگلی 4080 سب سے عام ویلیوز دو بائٹس کی ہوں، وغیرہ۔

    تاہم، یہ ممکنہ طور پر ایک خطرناک آپریشن ہے۔ واقعات کی درج ذیل ترتیب کا تصور کریں:

    1. Noam Naive اس ایڈریس کو انکوڈ کرنے کے لیے encodeVal کو کال کرتا ہے جس پر وہ ٹوکن بھیجنا چاہتا ہے۔ وہ ایڈریس ایپلی کیشن پر استعمال ہونے والے پہلے ایڈریسز میں سے ایک ہے، اس لیے انکوڈ شدہ ویلیو 0x06 ہے۔ یہ ایک view فنکشن ہے، ٹرانزیکشن نہیں، اس لیے یہ Noam اور اس کے استعمال کردہ نوڈ کے درمیان ہے، اور کسی اور کو اس کے بارے میں معلوم نہیں ہے۔

    2. Owen Owner کیش ری آرڈرنگ (reordering) آپریشن چلاتا ہے۔ بہت کم لوگ دراصل اس ایڈریس کو استعمال کرتے ہیں، اس لیے اب اسے 0x201122 کے طور پر انکوڈ کیا گیا ہے۔ ایک مختلف ویلیو، 1018، کو 0x06 تفویض کیا گیا ہے۔

    3. Noam Naive اپنے ٹوکن 0x06 پر بھیجتا ہے۔ وہ ایڈریس 0x0000000000000000000000000de0b6b3a7640000 پر جاتے ہیں، اور چونکہ کوئی بھی اس ایڈریس کی پرائیویٹ کی (private key) نہیں جانتا، اس لیے وہ وہیں پھنس جاتے ہیں۔ Noam خوش نہیں ہے۔

    اس مسئلے کو حل کرنے کے طریقے موجود ہیں، اور کیش ری آرڈر کے دوران میم پول (mempool) میں موجود ٹرانزیکشنز کے متعلقہ مسئلے کو بھی، لیکن آپ کو اس سے آگاہ ہونا چاہیے۔

میں نے یہاں Optimism کے ساتھ کیشنگ کا مظاہرہ کیا، کیونکہ میں Optimism کا ملازم ہوں اور یہ وہ رول اپ ہے جسے میں سب سے بہتر جانتا ہوں۔ لیکن اسے کسی بھی ایسے رول اپ کے ساتھ کام کرنا چاہیے جو اندرونی پروسیسنگ کے لیے کم سے کم لاگت وصول کرتا ہو، تاکہ اس کے مقابلے میں ٹرانزیکشن ڈیٹا کو L1 پر لکھنا بڑا خرچ ہو۔

میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔

صفحہ کی آخری اپ ڈیٹ: 3 مارچ، 2026

کیا یہ ٹیوٹوریل مددگار تھا؟