Ruka kwenda kwenye maudhui makuu

Yote unayoweza kuhifadhi kwenye kache

safu ya 2
kuhifadhi kache
ghala
Wastani
Ori Pomerantz
15 Septemba 2022
21 soma ndani ya dakika

Unapotumia unda-mpya, gharama ya baiti katika muamala ni ghali zaidi kuliko gharama ya sehemu ya ghala. Kwa hiyo, ni jambo la busara kuhifadhi taarifa nyingi iwezekanavyo kwenye kache kwenye chain.

Katika makala haya utajifunza jinsi ya kuunda na kutumia mkataba wa kuhifadhi kache kwa njia ambayo thamani yoyote ya kigezo ambayo ina uwezekano wa kutumika mara nyingi itahifadhiwa kwenye kache na kupatikana kwa matumizi (baada ya mara ya kwanza) na idadi ndogo zaidi ya baiti, na jinsi ya kuandika msimbo wa offchain unaotumia kache hii.

Ikiwa unataka kuruka makala na kuona msimbo chanzo, upo hapa (opens in a new tab). Safu ya uundaji ni Foundry (opens in a new tab).

Muundo wa jumla

Kwa ajili ya kurahisisha, tutachukulia vigezo vyote vya muamala ni uint256, urefu wa baiti 32. Tunapopokea muamala, tutachanganua kila kigezo kama hivi:

  1. Ikiwa baiti ya kwanza ni 0xFF, chukua baiti 32 zinazofuata kama thamani ya kigezo na uiandike kwenye kache.

  2. Ikiwa baiti ya kwanza ni 0xFE, chukua baiti 32 zinazofuata kama thamani ya kigezo lakini _usi_iandike kwenye kache.

  3. Kwa thamani nyingine yoyote, chukua biti nne za juu kama idadi ya baiti za ziada, na biti nne za chini kama biti muhimu zaidi za ufunguo wa kache. Hapa kuna baadhi ya mifano:

    Baiti katika calldataUfunguo wa kache
    0x0F0x0F
    0x10,0x100x10
    0x12,0xAC0x02AC
    0x2D,0xEA, 0xD60x0DEAD6

Udhibiti wa kache

Kache inatekelezwa katika Cache.sol (opens in a new tab). Wacha tuipitie mstari kwa mstari.

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;

Vigezo hivi vya kudumu hutumiwa kutafsiri visa maalum ambapo tunatoa taarifa zote na ama tunataka iandikwe kwenye kache au la. Kuandika kwenye kache kunahitaji operesheni mbili za SSTORE (opens in a new tab) katika sehemu za ghala ambazo hazijatumika hapo awali kwa gharama ya gesi 22100 kila moja, kwa hivyo tunafanya iwe hiari.

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

Uhusiano (opens in a new tab) kati ya thamani na funguo zake. Taarifa hii ni muhimu ili kusimba thamani kabla ya kutuma muamala.

1 // Mahali n pana thamani ya ufunguo n+1, kwa sababu tunahitaji kuhifadhi
2 // sufuri kama "sio kwenye kache".
3 uint[] public key2val;

Tunaweza kutumia safu kwa ajili ya uhusiano kutoka kwa funguo hadi thamani kwa sababu tunakabidhi funguo, na kwa kurahisisha tunafanya hivyo kwa mfuatano.

1 function cacheRead(uint _key) public view returns (uint) {
2 require(_key <= key2val.length, "Inasoma ingizo la kache lisiloanzishwa");
3 return key2val[_key-1];
4 } // somaKache

Soma thamani kutoka kwenye kache.

1 // Andika thamani kwenye kache ikiwa bado haipo
2 // Ni ya umma tu ili kuwezesha jaribio kufanya kazi
3 function cacheWrite(uint _value) public returns (uint) {
4 // Ikiwa thamani tayari iko kwenye kache, rudisha ufunguo wa sasa
5 if (val2key[_value] != 0) {
6 return val2key[_value];
7 }

Hakuna maana ya kuweka thamani ile ile kwenye kache zaidi ya mara moja. Ikiwa thamani tayari ipo, rudisha tu ufunguo uliopo.

1 // Kwa kuwa 0xFE ni kisa maalum, ufunguo mkubwa zaidi ambao kache inaweza
2 // kushikilia ni 0x0D ikifuatiwa na 0xFF mara 15. Ikiwa urefu wa kache tayari ni
3 // mkubwa kiasi hicho, shindwa.
4 // 1 2 3 4 5 6 7 8 9 A B C D E F
5 require(key2val.length+1 < 0x0DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
6 "mfuriko wa kache");

Sidhani kama tutawahi kupata kache kubwa kiasi hicho (takriban maingizo 1.8*1037, ambayo yangehitaji takriban 1027 TB kuhifadhi). Hata hivyo, nina umri wa kutosha kukumbuka "640kB zingekuwa za kutosha daima" (opens in a new tab). Jaribio hili ni la bei nafuu sana.

1 // Andika thamani ukitumia ufunguo unaofuata
2 val2key[_value] = key2val.length+1;

Ongeza utafutaji wa kinyume (kutoka thamani hadi ufunguo).

1 key2val.push(_value);

Ongeza utafutaji wa mbele (kutoka ufunguo hadi thamani). Kwa sababu tunakabidhi thamani kwa mfuatano, tunaweza tu kuiongeza baada ya thamani ya mwisho ya safu.

1 return key2val.length;
2 } // andikaKache

Rudisha urefu mpya wa key2val, ambao ni seli ambapo thamani mpya imehifadhiwa.

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

Kazi hii inasoma thamani kutoka kwa calldata yenye urefu wowote (hadi baiti 32, ukubwa wa neno).

1 {
2 uint _retVal;
3
4 require(length < 0x21,
5 "kikomo cha urefu wa _calldataVal ni baiti 32");
6 require(length + startByte <= msg.data.length,
7 " _calldataVal inajaribu kusoma zaidi ya ukubwa wa calldata");

Kazi hii ni ya ndani, kwa hivyo ikiwa msimbo uliobaki umeandikwa kwa usahihi, majaribio haya hayahitajiki. Hata hivyo, hayana gharama kubwa kwa hivyo tunaweza kuwa nayo.

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

Msimbo huu uko katika Yul (opens in a new tab). Inasoma thamani ya baiti 32 kutoka kwa calldata. Hii inafanya kazi hata kama calldata itaacha kabla ya startByte+32 kwa sababu nafasi isiyoanzishwa katika EVM inachukuliwa kuwa sufuri.

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

Hatuhitaji lazima thamani ya baiti 32. Hii huondoa baiti za ziada.

1 return _retVal;
2 } // _thamaniCalldata
3
4
5 // Soma kigezo kimoja kutoka kwa calldata, kuanzia _fromByte
6 function _readParam(uint _fromByte) internal
7 returns (uint _nextByte, uint _parameterValue)
8 {

Soma kigezo kimoja kutoka kwa calldata. Kumbuka kwamba tunahitaji kurudisha sio tu thamani tunayosoma, bali pia eneo la baiti inayofuata kwa sababu vigezo vinaweza kuwa na urefu kutoka baiti 1 hadi baiti 33.

1 // Baiti ya kwanza inatuambia jinsi ya kutafsiri iliyobaki
2 uint8 _firstByte;
3
4 _firstByte = uint8(_calldataVal(_fromByte, 1));

Solidity inajaribu kupunguza idadi ya hitilafu kwa kuzuia ubadilishaji wa aina unaoweza kuwa hatari (opens in a new tab). Upungufu, kwa mfano kutoka biti 256 hadi biti 8, unahitaji kuwa wazi.

1
2 // Soma thamani, lakini usiiandike kwenye kache
3 if (_firstByte == uint8(DONT_CACHE))
4 return(_fromByte+33, _calldataVal(_fromByte+1, 32));
5
6 // Soma thamani, na uiandike kwenye kache
7 if (_firstByte == uint8(INTO_CACHE)) {
8 uint _param = _calldataVal(_fromByte+1, 32);
9 cacheWrite(_param);
10 return(_fromByte+33, _param);
11 }
12
13 // Ikiwa tumefika hapa inamaanisha tunahitaji kusoma kutoka kwa kache
14
15 // Idadi ya baiti za ziada za kusoma
16 uint8 _extraBytes = _firstByte / 16;
Onyesha yote

Chukua nibble (opens in a new tab) ya chini na uichanganye na baiti zingine ili kusoma thamani kutoka kwa kache.

1 uint _key = (uint256(_firstByte & 0x0F) << (8*_extraBytes)) +
2 _calldataVal(_fromByte+1, _extraBytes);
3
4 return (_fromByte+_extraBytes+1, cacheRead(_key));
5
6 } // _somaKigezo
7
8
9 // Soma vigezo n (kazi zinajua ni vigezo vingapi wanavyotarajia)
10 function _readParams(uint _paramNum) internal returns (uint[] memory) {
Onyesha yote

Tungeweza kupata idadi ya vigezo tulivyo navyo kutoka kwa calldata yenyewe, lakini kazi zinazotuita zinajua ni vigezo vingapi wanavyotarajia. Ni rahisi kuwaacha watuambie.

1 // Vigezo tunavyosoma
2 uint[] memory params = new uint[](_paramNum);
3
4 // Vigezo vinaanza kwenye baiti 4, kabla ya hapo ni saini ya kazi
5 uint _atByte = 4;
6
7 for(uint i=0; i<_paramNum; i++) {
8 (_atByte, params[i]) = _readParam(_atByte);
9 }
Onyesha yote

Soma vigezo hadi upate nambari unayohitaji. Ikiwa tutapita mwisho wa calldata, _readParams itabatilisha wito.

1
2 return(params);
3 } // somaVigezo
4
5 // Kwa kujaribu _readParams, jaribu kusoma vigezo vinne
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 } // kigezoNne
Onyesha yote

Faida moja kubwa ya Foundry ni kwamba inaruhusu majaribio kuandikwa katika Solidity (angalia Kujaribu kache hapa chini). Hii inafanya majaribio ya kitengo kuwa rahisi zaidi. Hii ni kazi ambayo inasoma vigezo vinne na kuvirudisha ili jaribio liweze kuthibitisha kuwa vilikuwa sahihi.

1 // Pata thamani, rudisha baiti ambazo zitaisimba (kwa kutumia kache ikiwezekana)
2 function encodeVal(uint _val) public view returns(bytes memory) {

encodeVal ni kazi ambayo msimbo wa offchain huita ili kusaidia kuunda calldata inayotumia kache. Inapokea thamani moja na kurudisha baiti zinazoisimba. Kazi hii ni view, kwa hivyo haihitaji muamala na inapoitwa kutoka nje haina gharama yoyote ya gesi.

1 uint _key = val2key[_val];
2
3 // Thamani bado haiko kwenye kache, iongeze
4 if (_key == 0)
5 return bytes.concat(INTO_CACHE, bytes32(_val));

Katika EVM ghala zote ambazo hazijaanzishwa huchukuliwa kuwa sufuri. Kwa hivyo, ikiwa tutatafuta ufunguo wa thamani ambayo haipo, tunapata sufuri. Katika hali hiyo baiti zinazoisimba ni INTO_CACHE (kwa hivyo itahifadhiwa kwenye kache wakati ujao), ikifuatiwa na thamani halisi.

1 // Ikiwa ufunguo ni <0x10, rudisha kama baiti moja
2 if (_key < 0x10)
3 return bytes.concat(bytes1(uint8(_key)));

Baiti moja ndiyo rahisi zaidi. Tunatumia tu bytes.concat (opens in a new tab) kubadilisha aina ya bytes<n> kuwa safu ya baiti ambayo inaweza kuwa na urefu wowote. Licha ya jina, inafanya kazi vizuri inapotolewa na hoja moja tu.

1 // Thamani ya baiti mbili, imesimbwa kama 0x1vvv
2 if (_key < 0x1000)
3 return bytes.concat(bytes2(uint16(_key) | 0x1000));

Tunapokuwa na ufunguo ambao ni chini ya 163, tunaweza kuuelezea kwa baiti mbili. Kwanza tunabadilisha _key, ambayo ni thamani ya biti 256, kuwa thamani ya biti 16 na kutumia OR ya kimantiki kuongeza idadi ya baiti za ziada kwenye baiti ya kwanza. Kisha tunaibadilisha kuwa thamani ya bytes2, ambayo inaweza kubadilishwa kuwa bytes.

1 // Labda kuna njia ya kijanja ya kufanya mistari ifuatayo kama kitanzi,
2 // lakini ni kazi ya kutazama kwa hivyo ninaboresha muda wa mtayarishaji programu na
3 // urahisi.
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)));
Onyesha yote

Thamani zingine (baiti 3, baiti 4, n.k.) zinashughulikiwa kwa njia ile ile, ila tu na saizi tofauti za uga.

1 // Ikiwa tutafika hapa, kuna kitu kibaya.
2 revert("Hitilafu katika encodeVal, haipaswi kutokea");

Ikiwa tutafika hapa inamaanisha tumepata ufunguo ambao si chini ya 16*25615. Lakini cacheWrite inaweka kikomo kwa funguo kwa hivyo hatuwezi hata kufikia 14*25616 (ambayo ingekuwa na baiti ya kwanza ya 0xFE, kwa hivyo ingeonekana kama DONT_CACHE). Lakini haitugharimu sana kuongeza jaribio endapo mtayarishaji programu wa siku zijazo ataleta hitilafu.

1 } // simbaThamani
2
3} // Kache

Kujaribu kache

Moja ya faida za Foundry ni kwamba inakuwezesha kuandika majaribio katika Solidity (opens in a new tab), ambayo inafanya iwe rahisi kuandika majaribio ya kitengo. Majaribio ya darasa la Cache yapo hapa (opens in a new tab). Kwa sababu msimbo wa majaribio unajirudia, kama majaribio yanavyokuwa, makala haya yanaelezea sehemu za kuvutia tu.

1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.13;
3
4import "forge-std/Test.sol";
5
6
7// Inahitaji kuendesha `forge test -vv` kwa konsoli.
8import "forge-std/console.sol";

Huu ni msimbo wa kiolezo tu ambao ni muhimu kutumia kifurushi cha majaribio na console.log.

1import "src/Cache.sol";

Tunahitaji kujua mkataba tunaoujaribu.

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

Kazi ya setUp inaitwa kabla ya kila jaribio. Katika kesi hii tunaunda kache mpya, ili majaribio yetu yasiathiriane.

1 function testCaching() public {

Majaribio ni kazi ambazo majina yake huanza na test. Kazi hii inakagua utendaji wa msingi wa kache, kuandika thamani na kuzisoma tena.

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);

Hivi ndivyo unavyofanya majaribio halisi, ukitumia kazi za assert... (opens in a new tab). Katika kesi hii, tunakagua kuwa thamani tuliyoandika ndiyo tunayoisoma. Tunaweza kupuuza matokeo ya cache.cacheWrite kwa sababu tunajua kwamba funguo za kache zinakabidhiwa kwa mpangilio.

1 }
2 } // jaribuKache
3
4
5 // Hifadhi thamani ile ile mara nyingi kwenye kache, hakikisha ufunguo unabaki
6 // vile vile
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 }
Onyesha yote

Kwanza tunaandika kila thamani mara mbili kwenye kache na kuhakikisha funguo ni sawa (ikimaanisha uandishi wa pili haukutokea kweli).

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

Kwa nadharia kunaweza kuwa na hitilafu ambayo haiathiri uandishi wa kache unaofuatana. Kwa hivyo hapa tunafanya maandishi kadhaa ambayo hayafuatani na tunaona thamani bado haziandikwi upya.

1 // Soma uint kutoka kwenye bafa ya kumbukumbu (kuhakikisha tunapata vigezo
2 // tulivyotuma)
3 function toUint256(bytes memory _bytes, uint256 _start) internal pure
4 returns (uint256)

Soma neno la biti 256 kutoka kwenye bafa ya bytes memory. Kazi hii ya matumizi inatuwezesha kuthibitisha kwamba tunapokea matokeo sahihi tunapoendesha wito wa kazi unaotumia kache.

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

Yul haitumii miundo ya data zaidi ya uint256, kwa hivyo unapoelekeza kwenye muundo wa data wa hali ya juu zaidi, kama vile bafa ya kumbukumbu _bytes, unapata anwani ya muundo huo. Solidity huhifadhi thamani za bytes memory kama neno la baiti 32 ambalo lina urefu, ikifuatiwa na baiti halisi, kwa hivyo ili kupata baiti namba _start tunahitaji kukokotoa _bytes+32+_start.

1
2 return tempUint;
3 } // kwaUint256
4
5 // Saini ya kazi kwa fourParams(), kwa hisani ya
6 // https://www.4byte.directory/signatures/?bytes4_signature=0x3edc1e6d
7 bytes4 constant FOUR_PARAMS = 0x3edc1e6d;
8
9 // Thamani za kudumu tu ili kuona tunapata thamani sahihi
10 uint256 constant VAL_A = 0xDEAD60A7;
11 uint256 constant VAL_B = 0xBEEF;
12 uint256 constant VAL_C = 0x600D;
13 uint256 constant VAL_D = 0x600D60A7;
Onyesha yote

Baadhi ya thamani za kudumu tunazohitaji kwa majaribio.

1 function testReadParam() public {

Ita fourParams(), kazi inayotumia readParams, ili kujaribu kama tunaweza kusoma vigezo kwa usahihi.

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

Hatuwezi kutumia utaratibu wa kawaida wa ABI kuita kazi kwa kutumia kache, kwa hivyo tunahitaji kutumia utaratibu wa kiwango cha chini cha <address>.call() (opens in a new tab). Utaratibu huo unachukua bytes memory kama ingizo, na kuirudisha (pamoja na thamani ya Boolean) kama towe.

1 // Wito wa kwanza, kache haina kitu
2 _callInput = bytes.concat(
3 FOUR_PARAMS,

Ni muhimu kwa mkataba ule ule kuunga mkono kazi zote mbili za kache (kwa wito kutoka kwa miamala moja kwa moja) na kazi zisizo za kache (kwa wito kutoka kwa mikataba-erevu mingine). Ili kufanya hivyo tunahitaji kuendelea kutegemea utaratibu wa Solidity kuita kazi sahihi, badala ya kuweka kila kitu katika kazi ya fallback (opens in a new tab). Kufanya hivi hurahisisha sana utangamano. Baiti moja ingetosheleza kutambua kazi katika visa vingi, kwa hivyo tunapoteza baiti tatu (gesi 16*3=48). Hata hivyo, ninapoandika haya, gesi hizo 48 zinagharimu senti 0.07, ambayo ni gharama nzuri kwa msimbo rahisi na usio na hitilafu nyingi.

1 // Thamani ya kwanza, iongeze kwenye kache
2 cache.INTO_CACHE(),
3 bytes32(VAL_A),

Thamani ya kwanza: Bendera inayosema ni thamani kamili inayohitaji kuandikwa kwenye kache, ikifuatiwa na baiti 32 za thamani hiyo. Thamani tatu zingine ni sawa, isipokuwa VAL_B haiandikwi kwenye kache na VAL_C ni kigezo cha tatu na cha nne.

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

Hapa ndipo tunapoita mkataba wa Cache.

1 assertEq(_success, true);

Tunatarajia wito ufanikiwe.

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

Tunaanza na kache tupu na kisha tunaongeza VAL_A ikifuatiwa na VAL_C. Tungetarajia ya kwanza kuwa na ufunguo 1, na ya pili kuwa na 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);

Towe ni vigezo vinne. Hapa tunathibitisha kuwa ni sahihi.

1 // Wito wa pili, tunaweza kutumia kache
2 _callInput = bytes.concat(
3 FOUR_PARAMS,
4
5 // Thamani ya kwanza kwenye Kache
6 bytes1(0x01),

Funguo za kache chini ya 16 ni baiti moja tu.

1 // Thamani ya pili, usiiongeze kwenye kache
2 cache.DONT_CACHE(),
3 bytes32(VAL_B),
4
5 // Thamani ya tatu na ya nne, thamani sawa
6 bytes1(0x02),
7 bytes1(0x02)
8 );
9 .
10 .
11 .
12 } // jaribuSomaKigezo
Onyesha yote

Majaribio baada ya wito ni sawa na yale ya baada ya wito wa kwanza.

1 function testEncodeVal() public {

Kazi hii ni sawa na testReadParam, isipokuwa badala ya kuandika vigezo kwa uwazi tunatumia 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 } // jaribuSimbaThamani
Onyesha yote

Jaribio pekee la ziada katika testEncodeVal() ni kuthibitisha kuwa urefu wa _callInput ni sahihi. Kwa wito wa kwanza ni 4+33*4. Kwa wa pili, ambapo kila thamani tayari iko kwenye kache, ni 4+1*4.

1 // Jaribu encodeVal wakati ufunguo una zaidi ya baiti moja
2 // Upeo wa baiti tatu kwa sababu kujaza kache hadi baiti nne huchukua
3 // muda mrefu sana.
4 function testEncodeValBig() public {
5 // Weka idadi ya thamani kwenye kache.
6 // Ili kurahisisha mambo, tumia ufunguo n kwa thamani n.
7 for(uint i=1; i<0x1FFF; i++) {
8 cache.cacheWrite(i);
9 }
Onyesha yote

Kazi ya testEncodeVal hapo juu inaandika thamani nne tu kwenye kache, kwa hivyo sehemu ya kazi inayoshughulikia thamani za baiti nyingi (opens in a new tab) haikaguliwi. Lakini msimbo huo ni mgumu na rahisi kupata hitilafu.

Sehemu ya kwanza ya kazi hii ni kitanzi kinachoandika thamani zote kutoka 1 hadi 0x1FFF kwenye kache kwa mpangilio, ili tuweze kusimba thamani hizo na kujua zinakokwenda.

1 .
2 .
3 .
4
5 _callInput = bytes.concat(
6 FOUR_PARAMS,
7 cache.encodeVal(0x000F), // Baiti moja 0x0F
8 cache.encodeVal(0x0010), // Baiti mbili 0x1010
9 cache.encodeVal(0x0100), // Baiti mbili 0x1100
10 cache.encodeVal(0x1000) // Baiti tatu 0x201000
11 );
Onyesha yote

Jaribu thamani za baiti moja, baiti mbili, na baiti tatu. Hatujaribu zaidi ya hapo kwa sababu itachukua muda mrefu sana kuandika maingizo ya kutosha ya steki (angalau 0x10000000, takriban robo bilioni).

1 .
2 .
3 .
4 .
5 } // jaribuSimbaThamaniKubwa
6
7
8 // Jaribu kinachotokea na bafa ndogo sana tunapata urejeshaji
9 function testShortCalldata() public {
Onyesha yote

Jaribu kinachotokea katika hali isiyo ya kawaida ambapo hakuna vigezo vya kutosha.

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

Kwa kuwa inarejesha, matokeo tunayopaswa kupata ni false.

1 // Ita kwa funguo za kache ambazo hazipo
2 function testNoCacheKey() public {
3 .
4 .
5 .
6 _callInput = bytes.concat(
7 FOUR_PARAMS,
8
9 // Thamani ya kwanza, iongeze kwenye kache
10 cache.INTO_CACHE(),
11 bytes32(VAL_A),
12
13 // Thamani ya pili
14 bytes1(0x0F),
15 bytes2(0x1234),
16 bytes11(0xA10102030405060708090A)
17 );
Onyesha yote

Kazi hii inapata vigezo vinne halali kabisa, isipokuwa kwamba kache haina kitu kwa hivyo hakuna thamani za kusoma.

1 .
2 .
3 .
4 // Jaribu kinachotokea na bafa ndefu sana, kila kitu kinafanya kazi vizuri
5 function testLongCalldata() public {
6 address _cacheAddr = address(cache);
7 bool _success;
8 bytes memory _callInput;
9 bytes memory _callOutput;
10
11 // Wito wa kwanza, kache haina kitu
12 _callInput = bytes.concat(
13 FOUR_PARAMS,
14
15 // Thamani ya kwanza, iongeze kwenye kache
16 cache.INTO_CACHE(), bytes32(VAL_A),
17
18 // Thamani ya pili, iongeze kwenye kache
19 cache.INTO_CACHE(), bytes32(VAL_B),
20
21 // Thamani ya tatu, iongeze kwenye kache
22 cache.INTO_CACHE(), bytes32(VAL_C),
23
24 // Thamani ya nne, iongeze kwenye kache
25 cache.INTO_CACHE(), bytes32(VAL_D),
26
27 // Na thamani nyingine kwa "bahati njema"
28 bytes4(0x31112233)
29 );
Onyesha yote

Kazi hii inatuma thamani tano. Tunajua kwamba thamani ya tano inapuzwa kwa sababu si ingizo halali la kache, ambalo lingesababisha urejeshaji kama lisingejumuishwa.

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

Programu ya mfano

Kuandika majaribio katika Solidity ni vizuri sana, lakini mwisho wa siku mfumo mtawanyo wa kimamlaka (dapp) unahitaji kuweza kushughulikia maombi kutoka nje ya chaini ili kuwa na manufaa. Makala haya yanaonyesha jinsi ya kutumia uhifadhi wa kache katika mfumo mtawanyo wa kimamlaka (dapp) na WORM, ambayo inasimama kwa "Andika Mara Moja, Soma Mara Nyingi". Ikiwa ufunguo bado haujaandikwa, unaweza kuandika thamani kwake. Ikiwa ufunguo tayari umeandikwa, unapata urejeshaji.

Mkataba

Huu ndio mkataba (opens in a new tab). Mara nyingi inarudia kile ambacho tayari tumefanya na Cache na CacheTest, kwa hivyo tunashughulikia tu sehemu za kuvutia.

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

Njia rahisi zaidi ya kutumia Cache ni kuirithi katika mkataba wetu wenyewe.

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

Kazi hii ni sawa na fourParam katika CacheTest hapo juu. Kwa sababu hatufuati vipimo vya ABI, ni bora kutotangaza vigezo vyovyote kwenye kazi.

1 // Fanya iwe rahisi kutuita
2 // Saini ya kazi kwa writeEntryCached(), kwa hisani ya
3 // https://www.4byte.directory/signatures/?bytes4_signature=0xe4e4f2d3
4 bytes4 constant public WRITE_ENTRY_CACHED = 0xe4e4f2d3;

Msimbo wa nje unaoita writeEntryCached utahitaji kujenga calldata mwenyewe, badala ya kutumia worm.writeEntryCached, kwa sababu hatufuati vipimo vya ABI. Kuwa na thamani hii ya kudumu hufanya iwe rahisi kuiandika.

Kumbuka kwamba ingawa tunafafanua WRITE_ENTRY_CACHED kama kigezo cha hali, ili kuisoma kutoka nje ni muhimu kutumia kazi ya kupata, worm.WRITE_ENTRY_CACHED().

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

Kazi ya kusoma ni view, kwa hivyo haihitaji muamala na haina gharama ya gesi. Kwa hivyo, hakuna faida ya kutumia kache kwa kigezo. Kwa kazi za kutazama ni bora kutumia utaratibu wa kawaida ambao ni rahisi zaidi.

Msimbo wa majaribio

Huu ni msimbo wa majaribio wa mkataba (opens in a new tab). Tena, hebu tuangalie tu yale ya kuvutia.

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

Hivi (vm.expectRevert) (opens in a new tab) ndivyo tunavyobainisha katika jaribio la Foundry kwamba wito unaofuata unapaswa kushindwa, na sababu iliyoripotiwa ya kushindwa. Hii inatumika tunapotumia sintaksia <contract>.<function name>() badala ya kujenga calldata na kuita mkataba kwa kutumia kiolesura cha kiwango cha chini (<contract>.call(), n.k.).

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

Hapa tunatumia ukweli kwamba cacheWrite inarudisha ufunguo wa kache. Hiki si kitu tunachotarajia kutumia katika uzalishaji, kwa sababu cacheWrite inabadilisha hali, na kwa hivyo inaweza kuitwa tu wakati wa muamala. Miamala haina thamani za kurudisha, ikiwa ina matokeo, matokeo hayo yanapaswa kutolewa kama matukio. Kwa hivyo, thamani ya kurudisha ya cacheWrite inapatikana tu kutoka kwa msimbo wa onchain, na msimbo wa onchain hauhitaji uhifadhi wa kache wa vigezo.

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

Hivi ndivyo tunavyoiambia Solidity kwamba ingawa <contract address>.call() ina thamani mbili za kurudisha, tunajali tu ya kwanza.

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

Kwa kuwa tunatumia kazi ya kiwango cha chini <address>.call(), hatuwezi kutumia vm.expectRevert() na tunapaswa kuangalia thamani ya mafanikio ya boolean tunayopata kutoka kwa wito.

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);
Onyesha yote

Hii ndiyo njia tunayotumia kuthibitisha kwamba msimbo unatoa tukio kwa usahihi (opens in a new tab) katika Foundry.

Mteja

Jambo moja usilopata na majaribio ya Solidity ni msimbo wa JavaScript unaoweza kunakili na kubandika kwenye programu yako mwenyewe. Ili kuandika msimbo huo, nilipeleka WORM kwenye Optimism Goerli (opens in a new tab), testnet mpya ya Optimism (opens in a new tab). Inapatikana kwenye anwani 0xd34335b1d818cee54e3323d3246bd31d94e6a78a (opens in a new tab).

Unaweza kuona msimbo wa JavaScript wa mteja hapa (opens in a new tab). Ili kuitumia:

  1. Fanya nakala ya hifadhi ya git:

    1git clone https://github.com/qbzzt/20220915-all-you-can-cache.git
  2. Sakinisha vifurushi vinavyohitajika:

    1cd javascript
    2yarn
  3. Nakili faili ya usanidi:

    1cp .env.example .env
  4. Hariri .env kwa usanidi wako:

    KigezoThamani
    MNEMONICNeno la siri la akaunti ambayo ina ETH ya kutosha kulipia muamala. Unaweza kupata ETH ya bure kwa mtandao wa Optimism Goerli hapa (opens in a new tab).
    OPTIMISM_GOERLI_URLURL ya Optimism Goerli. Sehemu ya umma, https://goerli.optimism.io, ina kikomo cha viwango lakini inatosha kwa tunachohitaji hapa
  5. Endesha index.js.

    1node index.js

    Programu hii ya mfano kwanza inaandika ingizo kwa WORM, ikionyesha calldata na kiungo cha muamala kwenye Etherscan. Kisha inasoma tena ingizo hilo, na kuonyesha ufunguo inaotumia na thamani katika ingizo (thamani, nambari ya bloku, na mwandishi).

Sehemu kubwa ya mteja ni JavaScript ya kawaida ya Dapp. Kwa hivyo tena tutapitia tu sehemu za kuvutia.

1.
2.
3.
4const main = async () => {
5 const func = await worm.WRITE_ENTRY_CACHED()
6
7 // Unahitaji ufunguo mpya kila wakati
8 const key = await worm.encodeVal(Number(new Date()))

Sehemu fulani inaweza kuandikwa mara moja tu, kwa hivyo tunatumia muhuri wa muda kuhakikisha haturudii kutumia sehemu.

1const val = await worm.encodeVal("0x600D")
2
3// Andika ingizo
4const calldata = func + key.slice(2) + val.slice(2)

Ethers inatarajia data ya wito iwe mfuatano wa heksi, 0x ikifuatiwa na idadi shufwa ya tarakimu za heksadesimali. Kwa kuwa key na val zote zinaanza na 0x, tunahitaji kuondoa vichwa hivyo.

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

Kama ilivyo kwa msimbo wa majaribio wa Solidity, hatuwezi kuita kazi iliyohifadhiwa kwenye kache kawaida. Badala yake, tunahitaji kutumia utaratibu wa kiwango cha chini.

1 .
2 .
3 .
4 // Soma ingizo lililoandikwa sasa hivi
5 const realKey = '0x' + key.slice(4) // ondoa bendera ya FF
6 const entryRead = await worm.readEntry(realKey)
7 .
8 .
9 .
Onyesha yote

Kwa kusoma maingizo tunaweza kutumia utaratibu wa kawaida. Hakuna haja ya kutumia uhifadhi wa kache wa vigezo na kazi za view.

Hitimisho

Msimbo katika makala haya ni uthibitisho wa dhana, lengo ni kurahisisha wazo kueleweka. Kwa mfumo ulio tayari kwa uzalishaji unaweza kutaka kutekeleza utendaji fulani wa ziada:

  • Shughulikia thamani ambazo si uint256. Kwa mfano, mifuatano.

  • Badala ya kache ya kimataifa, labda uwe na uhusiano kati ya watumiaji na kache. Watumiaji tofauti hutumia thamani tofauti.

  • Thamani zinazotumiwa kwa anwani ni tofauti na zile zinazotumiwa kwa madhumuni mengine. Inaweza kuwa na maana kuwa na kache tofauti kwa ajili ya anwani tu.

  • Hivi sasa, funguo za kache ziko kwenye algorithm ya "wa kwanza kuja, ufunguo mdogo zaidi". Thamani kumi na sita za kwanza zinaweza kutumwa kama baiti moja. Thamani 4080 zinazofuata zinaweza kutumwa kama baiti mbili. Thamani takriban milioni zinazofuata ni baiti tatu, n.k. Mfumo wa uzalishaji unapaswa kuweka vihesabu vya matumizi kwenye maingizo ya kache na kuzipanga upya ili thamani kumi na sita za kawaida zaidi ziwe baiti moja, thamani 4080 zinazofuata za kawaida zaidi ziwe baiti mbili, n.k.

    Hata hivyo, hiyo ni operesheni inayoweza kuwa hatari. Fikiria mfuatano ufuatao wa matukio:

    1. Noam Naive anaita encodeVal ili kusimba anwani anayotaka kutuma tokeni. Anwani hiyo ni mojawapo ya za kwanza kutumika kwenye programu, kwa hivyo thamani iliyosimbwa ni 0x06. Hii ni kazi ya view, si muamala, kwa hivyo ni kati ya Noam na nodi anayoitumia, na hakuna mwingine anayejua kuihusu

    2. Owen Mmiliki anaendesha operesheni ya kupanga upya kache. Watu wachache sana hutumia anwani hiyo, kwa hivyo sasa imesimbwa kama 0x201122. Thamani tofauti, 1018, imekabidhiwa 0x06.

    3. Noam Naive anatuma tokeni zake kwa 0x06. Zinakwenda kwenye anwani 0x0000000000000000000000000de0b6b3a7640000, na kwa kuwa hakuna anayejua ufunguo binafsi wa anwani hiyo, zimekwama tu hapo. Noam hana furaha.

    Kuna njia za kutatua tatizo hili, na tatizo linalohusiana na miamala ambayo iko kwenye mempool wakati wa kupanga upya kache, lakini ni lazima ufahamu.

Nilionyesha uhifadhi wa kache hapa na Optimism, kwa sababu mimi ni mfanyakazi wa Optimism na hii ndiyo unda-mpya ninaifahamu vizuri zaidi. Lakini inapaswa kufanya kazi na unda-mpya yoyote inayotoza gharama ndogo kwa usindikaji wa ndani, ili kwa kulinganisha uandishi wa data ya muamala kwa L1 uwe gharama kuu.

Tazama hapa kwa kazi zangu zaidi (opens in a new tab).

Ukurasa ulihaririwa mwisho: 3 Machi 2026

Umesaidika na mafunzo haya?