Önbelleğe alabileceğiniz her şey
İşlemdeki bir baytın maliyeti, toplama kullanırken depolama yuvası kullanımına göre çok daha pahalıdır. Bu nedenle, zincirde mümkün olduğu kadar çok bilgiyi önbelleğe almak mantıklıdır.
Bu makalede, birden fazla kez kullanılması olası olan herhangi bir parametre değerinin nasıl önbelleğe alınacağını ve daha az bellek (ilk kez kullanıldıktan sonra) kullanacak şekilde nasıl kullanıma hazır hale getirileceğini öğrenecek ve ayrıca bu önbelleği kullanan zincir dışı kodu yazmayı da öğrenmiş olacaksınız.
Makaleyi atlayıp doğrudan kaynak kodunu görmek istiyorsanız buraya(opens in a new tab) tıklayabilirsiniz. Geliştirme yığını Foundry(opens in a new tab)'dir.
Genel tasarım
Kolay anlaşılması için tüm işlem parametrelerinin 32 bayt uzunluğunda ve uint256
tipinde olduğunu varsayacağız. Bir işlem aldığımızda parametreleri şu şekilde ayrıştıracağız:
İlk bayt
0xFF
ise, sonraki 32 baytı parametre değeri olarak alın ve önbelleğe yazın.İlk bayt
0xFE
ise, sonraki 32 baytı parametre değeri olarak alın ancak önbelleğe yazmayın.Başka herhangi bir değer için ilk dört biti ek bayt sayısı ve son dört biti önbellek anahtarının en önemli bitleri olarak alın. İşte bazı örnekler:
Calldata'daki baytlar Önbellek anahtarı 0x0F 0x0F 0x10,0x10 0x10 0x12,0xAC 0x02AC 0x2D,0xEA, 0xD6 0x0DEAD6
Önbellek manipülasyonu
Önbellek Cache.sol
(opens in a new tab) içinde uygulanır. Hadi satır satır inceleyelim.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.13;345contract Cache {67 bytes1 public constant INTO_CACHE = 0xFF;8 bytes1 public constant DONT_CACHE = 0xFE;Kopyala
Bu sabitler, tüm bilgileri sağladığımız ve önbelleğe yazılmasını isteyip istemediğimiz özel durumları yorumlamak için kullanılır. Önbelleğe yazdırmak için her birisine 22100 gaz ücreti ödeyerek daha önce kullanılmayan depolama yuvalarına iki SSTORE
(opens in a new tab) işlemi yapılması gerekir; bu nedenle isteğe bağlı hale getiririz.
12 mapping(uint => uint) public val2key;Kopyala
Değerler ile anahtarları arasında eşleme(opens in a new tab). Bu bilgi, işlemi göndermeden önce değerleri kodlayabilmek için gereklidir.
1 // Location n has the value for key n+1, because we need to preserve2 // zero as "not in the cache".3 uint[] public key2val;Kopyala
Anahtarları atadığımızdan anahtarlardan değerlere eşleme için bir dizi kullanabiliriz ve basitlik için bunu sırayla yaparız.
1 function cacheRead(uint _key) public view returns (uint) {2 require(_key <= key2val.length, "Reading uninitialize cache entry");3 return key2val[_key-1];4 } // cacheReadKopyala
Önbellekten değer okuma.
1 // Write a value to the cache if it's not there already2 // Only public to enable the test to work3 function cacheWrite(uint _value) public returns (uint) {4 // If the value is already in the cache, return the current key5 if (val2key[_value] != 0) {6 return val2key[_value];7 }Kopyala
Aynı değeri önbelleğe birden fazla kez koymanın hiçbir anlamı yoktur. Değer zaten oradaysa mevcut anahtarı döndürmeniz yeterli olur.
1 // Since 0xFE is a special case, the largest key the cache can2 // hold is 0x0D followed by 15 0xFF's. If the cache length is already that3 // large, fail.4 // 1 2 3 4 5 6 7 8 9 A B C D E F5 require(key2val.length+1 < 0x0DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,6 "cache overflow");Kopyala
Hiçbir zaman bu kadar büyük bir önbelleğe sahip olacağımızı sanmıyorum (yaklaşık 1,8*1037 giriş, yani depolamak için 1027TB gerektirir). Aynı zamanda da "640 kB her zaman yeterli olacaktır"(opens in a new tab)lafını da hatırlayacak kadar yaşlıyım. Bu test oldukça ucuz.
1 // Write the value using the next key2 val2key[_value] = key2val.length+1;Kopyala
Geriye doğru aramayı ekleyin (değerden anahtara doğru).
1 key2val.push(_value);Kopyala
İleriye doğru aramayı ekleyin (anahtardan değere doğru). Değerleri sırayla atadığımız için onu son dizi değerinden sonra ekleyebiliriz.
1 return key2val.length;2 } // cacheWriteKopyala
Yeni değerin depolandığı hücre olan key2val
'in yeni uzunluğunu döndürün.
1 function _calldataVal(uint startByte, uint length)2 private pure returns (uint)Kopyala
Bu işlev, isteğe bağlı uzunluktaki çağrı verisinden bir değer okur (en fazla 32 bayt, kelime boyutu).
1 {2 uint _retVal;34 require(length < 0x21,5 "_calldataVal length limit is 32 bytes");6 require(length + startByte <= msg.data.length,7 "_calldataVal trying to read beyond calldatasize");Kopyala
Bu, dahili bir fonksiyondur, yani kodun geri kalanı doğru yazılırsa bu testlere ihtiyaç olmaz. Ancak pek de fazla masraflı değiller, yani yine de kullanabiliriz.
1 assembly {2 _retVal := calldataload(startByte)3 }Kopyala
Bu kod Yul(opens in a new tab)'da yazılmıştır. Çağrı verisinden 32 baytlık bir değer okur. Bu, çağrı verisi startByte+32
'den önce dursa bile çalışır, çünkü EVM'de başlatılmamış olan bu alan 0 olarak değerlendilir.
1 _retVal = _retVal >> (256-length*8);Kopyala
İlla da 32 baytlık bir değer istemiyoruz. Bu kod, fazlalık baytlardan kurtulur.
1 return _retVal;2 } // _calldataVal345 // Read a single parameter from the calldata, starting at _fromByte6 function _readParam(uint _fromByte) internal7 returns (uint _nextByte, uint _parameterValue)8 {Kopyala
Çağrı verisinden tekli bir parametre okuyun. Sadece okuduğumuz değeri değil, ayrıca sonraki baytın da konumunu okumamız gerektiğine dikkat edin, çünkü parametrelerin uzunluğu 1 bayt ile 33 bayt arasında değişebilir.
1 // The first byte tells us how to interpret the rest2 uint8 _firstByte;34 _firstByte = uint8(_calldataVal(_fromByte, 1));Kopyala
Solidity, tehlikeli olma potansiyeli taşıyan dahili tip dönüşümleri(opens in a new tab) engelleyerek hataların sayısını azaltmaya çalışır. Bir düşürme, örnek olarak 256 bitten 8 bite düşürme açık olmalıdır.
12 // Read the value, but do not write it to the cache3 if (_firstByte == uint8(DONT_CACHE))4 return(_fromByte+33, _calldataVal(_fromByte+1, 32));56 // Read the value, and write it to the cache7 if (_firstByte == uint8(INTO_CACHE)) {8 uint _param = _calldataVal(_fromByte+1, 32);9 cacheWrite(_param);10 return(_fromByte+33, _param);11 }1213 // If we got here it means that we need to read from the cache1415 // Number of extra bytes to read16 uint8 _extraBytes = _firstByte / 16;Tümünü gösterKopyala
Alt nibble(opens in a new tab)'ı alın ve önbellekten değeri okuyabilmek için diğer baytlarla birleştirin.
1 uint _key = (uint256(_firstByte & 0x0F) << (8*_extraBytes)) +2 _calldataVal(_fromByte+1, _extraBytes);34 return (_fromByte+_extraBytes+1, cacheRead(_key));56 } // _readParam789 // Read n parameters (functions know how many parameters they expect)10 function _readParams(uint _paramNum) internal returns (uint[] memory) {Tümünü gösterKopyala
Sahip olduğumuz parametrelerin sayısını çağrı verisinin kendisinden de alabiliriz, fakat bize çağrı yapan fonksiyonlar ne kadar parametre beklediklerini bilmektedir. Onların bize söylemesine izin vermek daha kolaydır.
1 // The parameters we read2 uint[] memory params = new uint[](_paramNum);34 // Parameters start at byte 4, before that it's the function signature5 uint _atByte = 4;67 for(uint i=0; i<_paramNum; i++) {8 (_atByte, params[i]) = _readParam(_atByte);9 }Tümünü gösterKopyala
İhtiyacınız olan sayıya ulaşana kadar parametreleri okumaya devam edin. Eğer çağrı verisinin sonunun ötesine geçersek, _readParams
aramayı eski haline döndürecektir.
12 return(params);3 } // readParams45 // For testing _readParams, test reading four parameters6 function fourParam() public7 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 } // fourParamTümünü gösterKopyala
Foundry'nin bir büyük faydası testlerin Solidity'de (aşağıdaki Önbelleğin test edilmesi bölümüne bakın) yazılmasına izin vermesidir. Bu, birim testlerini çok daha kolay hale getiriyor. Bu, testin doğru olduklarını onaylayabilmesi için dört parametreyi okuyan ve döndüren bir fonksiyondur.
1 // Get a value, return bytes that will encode it (using the cache if possible)2 function encodeVal(uint _val) public view returns(bytes memory) {Kopyala
encodeVal
, zincir dışı kodların önbelleği kullanan çağrı verileri oluşturmak için yardım istediklerinde çağırdığı bir fonksiyondur. Tek bir değer alır ve onu şifreleyen baytları verir. Bu fonksiyon bir view
fonksiyonudur; bu yüzden bir işleme ihtiyaç duymaz ve harici olarak çağrıldığında hiç gaz harcamaz.
1 uint _key = val2key[_val];23 // The value isn't in the cache yet, add it4 if (_key == 0)5 return bytes.concat(INTO_CACHE, bytes32(_val));Kopyala
EVM'de, başlatılmamış her depolamanın sıfır olduğu varsayılır. Yani eğer orada olmayan bir değerin anahtarını ararsak bir sıfır alırız. Bu durumda şifrelemeyi yapan baytlar, INTO_CACHE
şeklindedir (yani bir dahaki sefere önbelleğe alınacaktır) ve ardından asıl değer gelir.
1 // If the key is <0x10, return it as a single byte2 if (_key < 0x10)3 return bytes.concat(bytes1(uint8(_key)));Kopyala
Tek baytlar en kolay olanlardır. Bir bytes<n>
tipini herhangi bir uzunluktaki bir bayt dizisine dönüştürmek için bytes.concat
(opens in a new tab) kullanırız. İsmine rağmen, sadece bir bağımsız değişken sağlandığında bile normal bir şekilde çalışır.
1 // Two byte value, encoded as 0x1vvv2 if (_key < 0x1000)3 return bytes.concat(bytes2(uint16(_key) | 0x1000));Kopyala
163'den daha az bir anahtarımız olduğunda, onu 2 baytta ifade edebiliriz. Önce 256 bitlik bir değer olan _key
öğesini 16 bitlik değere çevirir ve mantık kullanırız veya ek baytların sayısını ilk bayta ekleriz. Sonra bytes2
değerine dönüştürürüz, bu da bytes
'a dönüştürülebilir.
1 // There is probably a clever way to do the following lines as a loop,2 // but it's a view function so I'm optimizing for programmer time and3 // simplicity.45 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)));Tümünü gösterKopyala
Diğer değerler (3 bayt, 4 bayt, vs.) aynı şekilde, fakat farklı alan boyutlarıyla işlenir.
1 // If we get here, something is wrong.2 revert("Error in encodeVal, should not happen");Kopyala
Eğer buraya geldiysek, 16*25615'ten az olmayan bir anahtar aldık demektir. Fakat cacheWrite
anahtarları sınırlar, bu yüzden 14*25616'ya bile çıkamayız (bunun da bir 0xFE tarzında bir ilk baytı olurdu, yani DONT_CACHE
gibi görünürdü). Fakat ilerde bir programcı girip de bir hata tanımlar diye bir test yapmak bize pek de pahalıya patlamaz.
1 } // encodeVal23} // CacheKopyala
Önbelleği test etme
Foundry'nin faydalarından biri de, testleri testleri Solidity'de yazmanıza izin vermesidir(opens in a new tab), bu sayede birim testi yazma kolaylaşır. Cache
sınıfı için olan testler buradadır(opens in a new tab). Test kodu, testlerin kendileri de bu eğilimde olduğu gibi kendini tekrar eden bir konu olduğu için bu belge sadece ilgi çekici kısımları anlatacaktır.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.13;34import "forge-std/Test.sol";567// Need to run `forge test -vv` for the console.8import "forge-std/console.sol";Kopyala
Bu, sadece test paketini ve console.log
'u kullanmak için gerekli bir standarttır.
1import "src/Cache.sol";Kopyala
Test ettiğimiz sözleşmeyi bilmemiz gerekir.
1contract CacheTest is Test {2 Cache cache;34 function setUp() public {5 cache = new Cache();6 }Kopyala
setUp
fonksiyonu her testten önce çağrılır. Bu durumda sadece yeni bir önbellek oluşturacağız ki, testlerimiz birbirini etkilemesin.
1 function testCaching() public {Kopyala
Testler, adları test
ile başlayan fonksiyonlardır. Bu fonksiyon, değerler yazarak ve onları tekrar okuyarak temel önbellek işlevselliğini kontrol eder.
1 for(uint i=1; i<5000; i++) {2 cache.cacheWrite(i*i);3 }45 for(uint i=1; i<5000; i++) {6 assertEq(cache.cacheRead(i), i*i);Kopyala
assert...
fonksiyonları(opens in a new tab) kullanarak asıl testi işte böyle yaparsınız. Bu durumda, yazdığımız değerin okuduğumuz değer olduğunu doğrularız. cache.cacheWrite
sonucunu atabiliriz, çünkü önbellek anahtarlarının doğrusal olarak atandığını biliyoruz.
1 }2 } // testCaching345 // Cache the same value multiple times, ensure that the key stays6 // the same7 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 }Tümünü gösterKopyala
Önce, her bir değeri önbelleğe yazarız ve anahtarların aynı olduğundan emin oluruz (ikinci yazmanın gerçekleşmediği anlamına gelir).
1 for(uint i=1; i<100; i+=3) {2 uint _key = cache.cacheWrite(i);3 assertEq(_key, i);4 }5 } // testRepeatCachingKopyala
Teoride, ardışık önbellek yazılarını etkilemeyen bir hata mevcut olabilir. Bu yüzden ardışık olmayan bazı yazılar yazacağız ve değerlerin hala yeniden yazılmamış olup olmadığını göreceğiz.
1 // Read a uint from a memory buffer (to make sure we get back the parameters2 // we sent out)3 function toUint256(bytes memory _bytes, uint256 _start) internal pure4 returns (uint256)Kopyala
Bir bytes memory
arabelleğinden 256 bitlik bir kelime okuyun. Bu yardımcı fonksiyon, önbelleği kullanan bir fonksiyon çağrısı yaptığımızda doğru sonuçları aldığımızı onaylamamızı sağlar.
1 {2 require(_bytes.length >= _start + 32, "toUint256_outOfBounds");3 uint256 tempUint;45 assembly {6 tempUint := mload(add(add(_bytes, 0x20), _start))7 }Kopyala
Yul uint256
öğesinin ötesindeki veri yapılarını desteklemez; yani _bytes
bellek arabelleği gibi daha sofistike bir veri yapısına başvurduğunuzda o yapının adresini alırsınız. Solidity bytes memory
değerlerini uzunluğu içeren 32 baytlık bir kelime olarak depolar. Ardından asıl baytlar gelir, yani bayt numarasını _start
almak için _bytes+32+_start
değerini hesaplamamız gerekir.
12 return tempUint;3 } // toUint25645 // Function signature for fourParams(), courtesy of6 // https://www.4byte.directory/signatures/?bytes4_signature=0x3edc1e6d7 bytes4 constant FOUR_PARAMS = 0x3edc1e6d;89 // Just some constant values to see we're getting the correct values back10 uint256 constant VAL_A = 0xDEAD60A7;11 uint256 constant VAL_B = 0xBEEF;12 uint256 constant VAL_C = 0x600D;13 uint256 constant VAL_D = 0x600D60A7;Tümünü gösterKopyala
Test için ihtiyacımız olan bazı sabit değerler.
1 function testReadParam() public {Kopyala
fourParams()
çağrısı, parametreleri doğru okuyabilmemiz için readParams
'ı kullanan bir fonksiyondur.
1 address _cacheAddr = address(cache);2 bool _success;3 bytes memory _callInput;4 bytes memory _callOutput;Kopyala
Önbelleği kullanan bir fonksiyonu çağırmak için normal ABI mekanizmasını kullanamayız, bu yüzden düşük seviye olan <address>.call()
(opens in a new tab) mekanizmasını kullanmamız gerekir. Bu mekanizma bytes memory
'yi girdi olarak alır ve çıktı olarak (bir Boole değeri ile birlikte) verir.
1 // First call, the cache is empty2 _callInput = bytes.concat(3 FOUR_PARAMS,Kopyala
Aynı sözleşmenin hem önbelleklenmiş fonksiyonları (işlemlerden doğrudan gelen çağrılar için) hem de önbelleklenmemiş fonksiyonları (diğer akıllı sözleşmelerden gelen çağrılar için) desteklemesi kullanışlıdır. Bunu yapabilmek için Solidity mekanizmasının her şeyi a fallback
fonksiyonuna(opens in a new tab) koymasının yerine doğru fonksiyonu çağıracağına güvenmeye devam etmemiz gerekir. Bunu yapmak, birleştirilebilirliği çok daha kolay hale getirir. Fonksiyonu tanımlamak için çoğu durumda tek bir bayt yeterlidir, yani üç baytı (16*3=48 gaz) boşa harcıyoruz. Bununla birlikte, ben bunu yazarken 48 gaz 0,07 sent ediyor, bu da daha basit, daha az hataya yatkın bir kod için makul bir ücrettir.
1 // First value, add it to the cache2 cache.INTO_CACHE(),3 bytes32(VAL_A),Kopyala
İlk değer: Önbelleğe yazılması gerekenin tam bir değer olduğunu söyleyen bir işaret ve ardından gelen değerin 32 baytlık kısmı. VAL_B
'ın önbelleğe yazılmaması ve VAL_C
'nin hem üçüncü hem de dördüncü parametre olması dışında diğer üç değer benzerdir.
1 .2 .3 .4 );5 (_success, _callOutput) = _cacheAddr.call(_callInput);Kopyala
Burası, Cache
sözleşmesini asıl çağıracağımız yerdir.
1 assertEq(_success, true);Kopyala
Çağrının başarılı olmasını umuyoruz.
1 assertEq(cache.cacheRead(1), VAL_A);2 assertEq(cache.cacheRead(2), VAL_C);Kopyala
Boş bir önbellekle başlıyor ve ardından VAL_A
ile VAL_C
öğelerini ekliyoruz. Birincinin anahtar 1'e, ikincinin de anahtar 2'ye sahip olmasını bekleriz.
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);
Çıktımız, o 4 parametredir. Burada doğru olduğunu onaylıyoruz.
1 // Second call, we can use the cache2 _callInput = bytes.concat(3 FOUR_PARAMS,45 // First value in the Cache6 bytes1(0x01),Kopyala
16'nın altında olan önbellek anahtarları sadece bir bayttır.
1 // Second value, don't add it to the cache2 cache.DONT_CACHE(),3 bytes32(VAL_B),45 // Third and fourth values, same value6 bytes1(0x02),7 bytes1(0x02)8 );9 .10 .11 .12 } // testReadParamTümünü gösterKopyala
Çağrıdan sonra yapılan testler, ilk çağrıdan sonra yapılanlarla aynı.
1 function testEncodeVal() public {Kopyala
Bu fonksiyon, testReadParam
ile benzerdir, parametreleri doğrudan yazmak için encodeVal()
kullanıyor olmamız dışında.
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 } // testEncodeValTümünü gösterKopyala
testEncodeVal()
'deki tek ekstra test, _callInput
'un uzunluğunun doğruluğunu onaylamaktır. İlk çağrı için bu değer 4+33*4'tür. İkinci için ise, zaten tüm değerler önbellekte olduğundan 4+1*4 şeklindedir.
1 // Test encodeVal when the key is more than a single byte2 // Maximum three bytes because filling the cache to four bytes takes3 // too long.4 function testEncodeValBig() public {5 // Put a number of values in the cache.6 // To keep things simple, use key n for value n.7 for(uint i=1; i<0x1FFF; i++) {8 cache.cacheWrite(i);9 }Tümünü gösterKopyala
Yukarıdaki testEncodeVal
fonksiyonu, önbelleğe sadece 4 değer yazarr, bu yüzden fonksiyonun çoklu bayt değerleriyle ilgilenen kısımları(opens in a new tab) kontrol edilmez. Fakat o kod karışık ve hataya açıktır.
Bu fonksiyonun ilk kısmı, önbelleğe 1 ila 0x1FFF değerlerini sırayla yazan bir döngüdür, bu sayede bu değerleri şifreleyebilecek ve nereye gittiklerini bilebileceğiz.
1 .2 .3 .45 _callInput = bytes.concat(6 FOUR_PARAMS,7 cache.encodeVal(0x000F), // One byte 0x0F8 cache.encodeVal(0x0010), // Two bytes 0x10109 cache.encodeVal(0x0100), // Two bytes 0x110010 cache.encodeVal(0x1000) // Three bytes 0x20100011 );Tümünü gösterKopyala
Bir bayt, iki bayt ve üç bayt değerlerini test edin. Yeterli yığın girdisini yazmak çok uzun süreceğinden (en az 0x10000000, yaklaşık olarak bir milyarın çeyreği) bunun ötesinde test yapmıyoruz.
1 .2 .3 .4 .5 } // testEncodeValBig678 // Test what with an excessively small buffer we get a revert9 function testShortCalldata() public {Tümünü gösterKopyala
Yeterli parametrenin olmadığı anormal durumda ne olduğunu test edin.
1 .2 .3 .4 (_success, _callOutput) = _cacheAddr.call(_callInput);5 assertEq(_success, false);6 } // testShortCalldataKopyala
Döndüğü için alacağımız sonuç false
olmalıdır.
1 // Call with cache keys that aren't there2 function testNoCacheKey() public {3 .4 .5 .6 _callInput = bytes.concat(7 FOUR_PARAMS,89 // First value, add it to the cache10 cache.INTO_CACHE(),11 bytes32(VAL_A),1213 // Second value14 bytes1(0x0F),15 bytes2(0x1234),16 bytes11(0xA10102030405060708090A)17 );Tümünü göster
Bu fonksiyon tamamen meşru dört parametre alır, önbelleğin boş olması sebebiyle okuyacak hiçbir değer olmaması dışında.
1 .2 .3 .4 // Test what with an excessively long buffer everything works file5 function testLongCalldata() public {6 address _cacheAddr = address(cache);7 bool _success;8 bytes memory _callInput;9 bytes memory _callOutput;1011 // First call, the cache is empty12 _callInput = bytes.concat(13 FOUR_PARAMS,1415 // First value, add it to the cache16 cache.INTO_CACHE(), bytes32(VAL_A),1718 // Second value, add it to the cache19 cache.INTO_CACHE(), bytes32(VAL_B),2021 // Third value, add it to the cache22 cache.INTO_CACHE(), bytes32(VAL_C),2324 // Fourth value, add it to the cache25 cache.INTO_CACHE(), bytes32(VAL_D),2627 // And another value for "good luck"28 bytes4(0x31112233)29 );Tümünü gösterKopyala
Bu fonksiyon, 5 değer gönderir. Beşinci değerin görmezden gelindiğini biliyoruz çünkü geçerli bir önbellek girdisi değildir ve dahil edilmemiş olsa geri dönme surumuna neden olurdu.
1 (_success, _callOutput) = _cacheAddr.call(_callInput);2 assertEq(_success, true);3 .4 .5 .6 } // testLongCalldata78} // CacheTest9Tümünü gösterKopyala
Bir örnek uygulama
Solidity'de test yazmak çok güzeldir fakat günün sonunda bir merkeziyetsiz uygulamanın kullanışlı olabilmesi için zincirin dışından talepleri işleyebilmesi gerekir. Bu belge "Bir Kez Yaz, Çok Kez Oku" anlamına gelen WORM
ile bir merkeziyetsiz uygulamada önbelleğe almanın nasıl kullanacağını gösterir. Eğer bir anahtar henüz yazılmamışsa, ona bir değer yazabilirsiniz. Eğer anahtar çoktan yazılmışsa, bir geri dönüş alırsınız.
Sözleşme
Sözleşme budur(opens in a new tab). Genel olarak Cache
ve CacheTest
ile çoktan yapmış olduğumuz şeyleri tekrar ediyor olduğu için sadece ilgi çekici olan kısımları ele alacağız.
1import "./Cache.sol";23contract WORM is Cache {Kopyala
Cache
'i kullanmanın en kolay yolu, onu kendi sözleşmemize aktarmaktır.
1 function writeEntryCached() external {2 uint[] memory params = _readParams(2);3 writeEntry(params[0], params[1]);4 } // writeEntryCachedKopyala
Bu fonksiyon, yukarıdaki CacheTest
'in içindeki fourParam
'a benzer. ABI spesifikasyonlarına uymadığımız için bu fonksiyonun içine herhangi bir parametre beyan etmememiz en iyisidir.
1 // Make it easier to call us2 // Function signature for writeEntryCached(), courtesy of3 // https://www.4byte.directory/signatures/?bytes4_signature=0xe4e4f2d34 bytes4 constant public WRITE_ENTRY_CACHED = 0xe4e4f2d3;Kopyala
ABI spesifikasyonlarına uymadığımız için writeEntryCached
öğesini çağıran harici kodun çağrı verisini worm.writeEntryCached
kullanmak yerine manuel olarak yazması gerekecektir. Bu sabit değere sahip olmak yazmayı kolaylaştırıyor.
WRITE_ENTRY_CACHED
değerini bir durum değişkeni olarak tanımlamış olsak da, bunu harici olarak okuyabilmek için worm.WRITE_ENTRY_CACHED()
getter fonksiyonunu kullanmanın gerekli olduğunu da not edin.
1 function readEntry(uint key) public view2 returns (uint _value, address _writtenBy, uint _writtenAtBlock)Kopyala
Okuma fonksiyonu bir view
'dır, yani bir işleme ihtiyaç duymaz ve gaz harcamaz. Sonuç olarak, parametre için önbelleği kullanmanın bir faydası yoktur. Görünüm fonksiyonlarında daha basit olan standart mekanizmayı kullanmak en iyisidir.
Test kodu
Bu, sözleşmenin test kodudur(opens in a new tab). Yine sadece ilgi çekici olan kısma bakalım.
1 function testWReadWrite() public {2 worm.writeEntry(0xDEAD, 0x60A7);34 vm.expectRevert(bytes("entry already written"));5 worm.writeEntry(0xDEAD, 0xBEEF);Kopyala
Bu (vm.expectRevert
)(opens in a new tab), yeni çağrının başarısız olması gerektiğini ve bunun için belirtilen sebebi Foundry'de belirtme şeklimizdir. Bu, çağrı verisini oluşturup düşük seviye (<contract>.call()
, vs.) arayüz kullanarak sözleşmeyi çağırmak yerine <contract>.<function name>()
söz dizimini kullandığımız durumlarda geçerli olur.
1 function testReadWriteCached() public {2 uint cacheGoat = worm.cacheWrite(0x60A7);Kopyala
Burada cacheWrite
'ın önbellek anahtarını döndürmesi gerçeğinden faydalanıyoruz. Bu, oluşturma sürecinde kullanmayı beklediğimiz bir şey değil, çünkü cacheWrite
durum değiştirir ve bu yüzden sadece bir işlem sırasında çağrılabilir. İşlemlerin dönüş değerleri yoktur, eğer sonuçları olursa bu sonuçların olaylar olarak ifade edilmiş olmaları gerekir. Yani cacheWrite
dönüş değerine sadece zincir üstü kod tarafından erişilebilir ve zincir üstü kod, parametre önbelleğe alımını desteklemez.
1 (_success,) = address(worm).call(_callInput);Kopyala
<contract address>.call()
'un iki değeri varken sadece ilk değeri önemsediğimizi Solidity'ye bu şekilde ifade ederiz.
1 (_success,) = address(worm).call(_callInput);2 assertEq(_success, false);Kopyala
Düşük seviye <address>.call()
fonksiyonunu kullanmamız sebebiyle, vm.expectRevert()
'ü kullanamayız ve çağrıdan alacağımız boole başarı değerine bakmamız gerekir.
1 event EntryWritten(uint indexed key, uint indexed value);23 .4 .5 .67 _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);Tümünü gösterKopyala
Kodun Foundry'de bir olayı doğru ifade ettiğini(opens in a new tab) bu şekilde doğrularız.
İstemci
Solidity testleriyle sahip olamayacağınız tek şey, kendi uygulamanıza kesip yapıştırabileceğiniz JavaScript kodudur. O kodu yazmak için Optimism'in(opens in a new tab) yeni test ağı olan Optimism Goerli(opens in a new tab)'ye WORM dağıttım. 0xd34335b1d818cee54e3323d3246bd31d94e6a78a
(opens in a new tab) adresindedir.
İstemcinin Javascript kodunu burada görebilirsiniz(opens in a new tab). Kullanmak için:
Git deposunu klonlayın:
1git clone https://github.com/qbzzt/20220915-all-you-can-cache.gitGerekli paketleri yükleyin:
1cd javascript2yarnKurulum dosyasını kopyalayın:
1cp .env.example .envKurulumunuz için
.env
'i düzenleyin:Parametre Değer MNEMONIC-ANIMSATICI Bir işleme ödeyebilmek için yeterli ETH bulunduran bir hesap için bir anımsatıcı. You can get free ETH for the Optimism Goerli ağı için bedava ETH'yi buradan alabilirsiniz(opens in a new tab). OPTIMISM_GOERLI_URL Optimisim Goerli'ye giden URL. Herkese açık bitiş noktası olan https://goerli.optimism.io
, oran sınırlıdır fakat ihtiyacımız olan şey için yeterlidirindex.js
komutunu çalıştırın.1node index.jsBu örnek uygulama ilk olarak WORM'a bir girdi yazar ve çağrı verisi ile Etherscan'deki işlemin bağlantısını görüntüler. Sonra da bu girişi geri okur, kullandığı anahtarı ve girdideki değerleri gösterir (değer, blok numarası ve yazarı).
Bu istemcinin çoğu normal Merkeziyetsiz Uygulama JavaScript'idir. Yani yine ilgi çekici kısımları ele alacağız.
1.2.3.4const main = async () => {5 const func = await worm.WRITE_ENTRY_CACHED()67 // Need a new key every time8 const key = await worm.encodeVal(Number(new Date()))
Verilmiş olan bu yuvanın içine sadece bir kere yazılabildiğinden yuvaları yeniden kullanmadığımızdan emin olmak için zaman damgasını kullanırız.
1const val = await worm.encodeVal("0x600D")23// Write an entry4const calldata = func + key.slice(2) + val.slice(2)
Ether'ler çağrı verisinin bir onaltılık dizi olmasını, 0x
ve ardından da onaltılık bir çift sayı bekler. Hem key
hem de val
0x
ile başladığından o başlıkları kaldırmamız gerekir.
1const tx = await worm.populateTransaction.writeEntryCached()2tx.data = calldata34sentTx = await wallet.sendTransaction(tx)
Solidity test kodunda olduğu gibi, önbelleğe alınmış bir fonksiyonu normal şekilde çağıramayız. Bunun yerine, daha düşük seviyede bir mekanizma kullanmaya ihtiyacımız var.
1 .2 .3 .4 // Read the entry just written5 const realKey = '0x' + key.slice(4) // remove the FF flag6 const entryRead = await worm.readEntry(realKey)7 .8 .9 .Tümünü göster
Girdileri okumak için normal mekanizmayı kullanabiliriz. Parametre önbelleklemesini view
fonksiyonlarıyla kullanmaya gerek yoktur.
Sonuç
Bu belgedeki kod, bir kavram ispatıdır; amaç, fikrin anlaşılmasını kolaylaştırmaktır. Oluşturmaya hazır bir sistem için biraz ilave işlevsellik eklemek isteyebilirsiniz:
uint256
olmayan değerleri işleyin. Örnek olarak, dizeler.Küresel önbellek yerine belki kullanıcılar ile önbellekler arasında bir eşlemeye sahip olmak. Farklı kullanıcılar farklı değerler kullanır.
Adresler için kullanılan değerler farklı amaçlar için kullanılanlardan bağımsızdır. Sadece adresler için ayrı bir önbelleğe sahip olmak mantıklı olabilir.
Güncel olarak, önbellek anahtarları "ilk gelene en küçük anahtar" algoritmasına göre çalışmaktadır. İlk on altı değer tek bir bayt olarak gönderilebilir. Sonraki 4080 değer iki bayt olarak gönderilebilir. Sonraki yaklaşık bir milyon değer ise 3 bayt olarak gönderilebilir, vs. Bir oluşturma sistemi, önbellek girişleri için kullanım sayaçları tutmalıdır ve onları, en yaygın on altı değerin bir bayt, sonraki 4080 en yaygın değerin iki bayt olacağı şekilde yeniden düzenlemelidir.
Yine de, bu risk barındıran bir işlemdir. Aşağıdaki olay dizisini hayal edin:
Noam Naive, jeton göndermek istediği adresi şifrelemek için
encodeVal
'ı çağırır. O adres, uygulamada kullanan ilk adreslerden biridir, bu yüzden şifrelenmiş değer 0x06 olur. Bu, bir işlem değil, birview
fonksiyonudur. Yani Noam ile kullandığı düğüm arasındadır ve başka hiç kimse, hakkında bir bilgiye sahip değildirOwen Owner, önbelleği yeniden düzenleme işlemini çalıştırıyor. Çok az kişi gerçek anlamda bu adresi kullanıyor, bu yüzden artık 0x201122 diye şifreleniyor. 0x06, farklı bir değere 1018 atanmış.
Noam Naive, jetonlarını 0x06'ya gönderiyor.
0x0000000000000000000000000de0b6b3a7640000
adresine gidiyorlar ve kimse bu adresin özel kodunu bilmediği için orada takılıp kalıyorlar. Noam mutlu değil.
Önbelleği yeniden düzenleme işlemi sırasında bu ve bellek havuzundaki bununla bağlantılı işlemler problemini çözmenin çok sayıda yolu olsa da, bunun farkında olmalısınız.
Burada Optimism ile önbelleklemeyi gösterdim, çünkü ben bir Optimism çalışanıyım ve bu da benim en iyi bildiğim toplamadır. Fakat dahili işlemeye minimum maliyet yükleyen her toplama için çalışması gerekir. Dolayısıyla karşılaştırma yaptığımızda işlem verilerini L1'e yazmak daha büyük maliyettir.
Son düzenleme: @nhsz(opens in a new tab), 15 Ağustos 2023