Ana içeriğe geç

Önbelleğe alabileceğiniz her şey

katman 2önbelleğe almadepolama
Orta düzey
Ori Pomerantz
15 Eylül 2022
20 dakikalık okuma minute read

İş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:

  1. İlk bayt 0xFF ise, sonraki 32 baytı parametre değeri olarak alın ve önbelleğe yazın.

  2. İlk bayt 0xFE ise, sonraki 32 baytı parametre değeri olarak alın ancak önbelleğe yazmayın.

  3. 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ı
    0x0F0x0F
    0x10,0x100x10
    0x12,0xAC0x02AC
    0x2D,0xEA, 0xD60x0DEAD6

Ö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: 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;
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.

1
2 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 preserve
2 // 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 } // cacheRead
Kopyala

Önbellekten değer okuma.

1 // Write a value to the cache if it's not there already
2 // Only public to enable the test to work
3 function cacheWrite(uint _value) public returns (uint) {
4 // If the value is already in the cache, return the current key
5 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 can
2 // hold is 0x0D followed by 15 0xFF's. If the cache length is already that
3 // large, fail.
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");
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 key
2 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 } // cacheWrite
Kopyala

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;
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");
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 } // _calldataVal
3
4
5 // Read a single parameter from the calldata, starting at _fromByte
6 function _readParam(uint _fromByte) internal
7 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 rest
2 uint8 _firstByte;
3
4 _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.

1
2 // Read the value, but do not write it to the cache
3 if (_firstByte == uint8(DONT_CACHE))
4 return(_fromByte+33, _calldataVal(_fromByte+1, 32));
5
6 // Read the value, and write it to the cache
7 if (_firstByte == uint8(INTO_CACHE)) {
8 uint _param = _calldataVal(_fromByte+1, 32);
9 cacheWrite(_param);
10 return(_fromByte+33, _param);
11 }
12
13 // If we got here it means that we need to read from the cache
14
15 // Number of extra bytes to read
16 uint8 _extraBytes = _firstByte / 16;
Tümünü göster
Kopyala

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);
3
4 return (_fromByte+_extraBytes+1, cacheRead(_key));
5
6 } // _readParam
7
8
9 // Read n parameters (functions know how many parameters they expect)
10 function _readParams(uint _paramNum) internal returns (uint[] memory) {
Tümünü göster
Kopyala

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 read
2 uint[] memory params = new uint[](_paramNum);
3
4 // Parameters start at byte 4, before that it's the function signature
5 uint _atByte = 4;
6
7 for(uint i=0; i<_paramNum; i++) {
8 (_atByte, params[i]) = _readParam(_atByte);
9 }
Tümünü göster
Kopyala

İ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.

1
2 return(params);
3 } // readParams
4
5 // For testing _readParams, test reading four parameters
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
Tümünü göster
Kopyala

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];
2
3 // The value isn't in the cache yet, add it
4 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 byte
2 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 0x1vvv
2 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 and
3 // simplicity.
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)));
Tümünü göster
Kopyala

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 } // encodeVal
2
3} // Cache
Kopyala

Ö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: UNLICENSED
2pragma solidity ^0.8.13;
3
4import "forge-std/Test.sol";
5
6
7// 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;
3
4 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 }
4
5 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 } // testCaching
3
4
5 // Cache the same value multiple times, ensure that the key stays
6 // the same
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 }
Tümünü göster
Kopyala

Ö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 } // testRepeatCaching
Kopyala

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 parameters
2 // we sent out)
3 function toUint256(bytes memory _bytes, uint256 _start) internal pure
4 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;
4
5 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.

1
2 return tempUint;
3 } // toUint256
4
5 // Function signature for fourParams(), courtesy of
6 // https://www.4byte.directory/signatures/?bytes4_signature=0x3edc1e6d
7 bytes4 constant FOUR_PARAMS = 0x3edc1e6d;
8
9 // Just some constant values to see we're getting the correct values back
10 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öster
Kopyala

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 empty
2 _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 cache
2 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 cache
2 _callInput = bytes.concat(
3 FOUR_PARAMS,
4
5 // First value in the Cache
6 bytes1(0x01),
Kopyala

16'nın altında olan önbellek anahtarları sadece bir bayttır.

1 // Second value, don't add it to the cache
2 cache.DONT_CACHE(),
3 bytes32(VAL_B),
4
5 // Third and fourth values, same value
6 bytes1(0x02),
7 bytes1(0x02)
8 );
9 .
10 .
11 .
12 } // testReadParam
Tümünü göster
Kopyala

Ç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 } // testEncodeVal
Tümünü göster
Kopyala

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 byte
2 // Maximum three bytes because filling the cache to four bytes takes
3 // 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öster
Kopyala

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 .
4
5 _callInput = bytes.concat(
6 FOUR_PARAMS,
7 cache.encodeVal(0x000F), // One byte 0x0F
8 cache.encodeVal(0x0010), // Two bytes 0x1010
9 cache.encodeVal(0x0100), // Two bytes 0x1100
10 cache.encodeVal(0x1000) // Three bytes 0x201000
11 );
Tümünü göster
Kopyala

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 } // testEncodeValBig
6
7
8 // Test what with an excessively small buffer we get a revert
9 function testShortCalldata() public {
Tümünü göster
Kopyala

Yeterli parametrenin olmadığı anormal durumda ne olduğunu test edin.

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

Döndüğü için alacağımız sonuç false olmalıdır.

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 );
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 file
5 function testLongCalldata() public {
6 address _cacheAddr = address(cache);
7 bool _success;
8 bytes memory _callInput;
9 bytes memory _callOutput;
10
11 // First call, the cache is empty
12 _callInput = bytes.concat(
13 FOUR_PARAMS,
14
15 // First value, add it to the cache
16 cache.INTO_CACHE(), bytes32(VAL_A),
17
18 // Second value, add it to the cache
19 cache.INTO_CACHE(), bytes32(VAL_B),
20
21 // Third value, add it to the cache
22 cache.INTO_CACHE(), bytes32(VAL_C),
23
24 // Fourth value, add it to the cache
25 cache.INTO_CACHE(), bytes32(VAL_D),
26
27 // And another value for "good luck"
28 bytes4(0x31112233)
29 );
Tümünü göster
Kopyala

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 } // testLongCalldata
7
8} // CacheTest
9
Tümünü göster
Kopyala

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";
2
3contract 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 } // writeEntryCached
Kopyala

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 us
2 // Function signature for writeEntryCached(), courtesy of
3 // https://www.4byte.directory/signatures/?bytes4_signature=0xe4e4f2d3
4 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 view
2 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);
3
4 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);
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);
Tümünü göster
Kopyala

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:

  1. Git deposunu klonlayın:

    1git clone https://github.com/qbzzt/20220915-all-you-can-cache.git
  2. Gerekli paketleri yükleyin:

    1cd javascript
    2yarn
  3. Kurulum dosyasını kopyalayın:

    1cp .env.example .env
  4. Kurulumunuz için .env'i düzenleyin:

    ParametreDeğer
    MNEMONIC-ANIMSATICIBir 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_URLOptimisim 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 yeterlidir
  5. index.js komutunu çalıştırın.

    1node index.js

    Bu ö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()
6
7 // Need a new key every time
8 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")
2
3// Write an entry
4const 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 = calldata
3
4sentTx = 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 written
5 const realKey = '0x' + key.slice(4) // remove the FF flag
6 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:

    1. 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, bir view fonksiyonudur. Yani Noam ile kullandığı düğüm arasındadır ve başka hiç kimse, hakkında bir bilgiye sahip değildir

    2. Owen 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ış.

    3. 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: @bicebaris(opens in a new tab), 23 Kasım 2023

Bu rehber yararlı oldu mu?