Kache kadiri uwezavyo
Unapotumia mikusanyiko gharama ya baiti katika muamala ni ghali zaidi kuliko gharama ya sloti ya uhifadhi. Kwa hivyo, inaleta maana kuhifadhi taarifa nyingi iwezekanavyo kwenye kache mnyororoni.
Katika makala haya unajifunza jinsi ya kuunda na kutumia mkataba wa 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) kwa idadi ndogo sana ya baiti, na jinsi ya kuandika msimbo wa nje ya mnyororo unaotumia kache hii.
Ikiwa unataka kuruka makala na kuona tu msimbo wa chanzo, uko hapa (opens in a new tab). Mfumo wa uundaji ni Foundry (opens in a new tab).
Muundo wa jumla
Kwa kurahisisha tutachukulia kuwa vigezo vyote vya muamala ni uint256, vyenye urefu wa baiti 32. Tunapopokea muamala, tutachanganua kila kigezo kama hivi:
-
Ikiwa baiti ya kwanza ni
0xFF, chukua baiti 32 zinazofuata kama thamani ya kigezo na uiandike kwenye kache. -
Ikiwa baiti ya kwanza ni
0xFE, chukua baiti 32 zinazofuata kama thamani ya kigezo lakini usiiandike kwenye kache. -
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 data za mwito Ufunguo wa kache 0x0F 0x0F 0x10,0x10 0x10 0x12,0xAC 0x02AC 0x2D,0xEA, 0xD6 0x0DEAD6
Udhibiti wa kache
Kache inatekelezwa katika Kache.sol (opens in a new tab). Hebu tuipitie mstari kwa mstari.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Cache {
bytes1 public constant INTO_CACHE = 0xFF;
bytes1 public constant DONT_CACHE = 0xFE;
Viwango hivi visivyobadilika vinatumika kufasiri matukio maalum ambapo tunatoa taarifa zote na ama tunataka ziandikwe kwenye kache au la. Kuandika kwenye kache kunahitaji operesheni mbili za SSTORE (opens in a new tab) kwenye sloti za uhifadhi ambazo hazijatumika hapo awali kwa gharama ya gesi 22100 kila moja, kwa hivyo tunaifanya iwe ya hiari.
mapping(uint => uint) public val2key;
Uchoraji ramani (opens in a new tab) kati ya thamani na funguo zake. Taarifa hii ni muhimu ili kusimba thamani kabla ya kutuma muamala.
// Eneo n lina thamani kwa ufunguo n+1, kwa sababu tunahitaji kuhifadhi
// sifuri kama "haipo kwenye kache".
uint[] public key2val;
Tunaweza kutumia safu kwa uchoraji ramani kutoka kwa funguo hadi thamani kwa sababu tunakabidhi funguo, na kwa urahisi tunafanya hivyo kwa mfuatano.
function cacheRead(uint _key) public view returns (uint) {
require(_key <= key2val.length, "Reading uninitialize cache entry");
return key2val[_key-1];
} // cacheRead
Soma thamani kutoka kwenye kache.
// Andika thamani kwenye kache ikiwa haipo tayari
// Ni ya umma tu ili kuwezesha jaribio kufanya kazi
function cacheWrite(uint _value) public returns (uint) {
// Ikiwa thamani tayari ipo kwenye kache, rudisha ufunguo wa sasa
if (val2key[_value] != 0) {
return val2key[_value];
}
Hakuna maana ya kuweka thamani sawa kwenye kache zaidi ya mara moja. Ikiwa thamani tayari ipo, rudisha tu ufunguo uliopo.
// Kwa kuwa 0xFE ni hali maalum, ufunguo mkubwa zaidi ambao kache inaweza
// kushikilia ni 0x0D ikifuatiwa na 0xFF 15. Ikiwa urefu wa kache tayari ni huo
// mkubwa, shindwa.
// 1 2 3 4 5 6 7 8 9 A B C D E F
require(key2val.length+1 < 0x0DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
"cache overflow");
Sidhani kama tutawahi kupata kache kubwa kiasi hicho (takriban maingizo 1.8*1037, ambayo yangehitaji takriban TB 1027 kuhifadhi). Hata hivyo, mimi ni mzee wa kutosha kukumbuka "640kB zingetosha kila wakati" (opens in a new tab). Jaribio hili ni la bei nafuu sana.
// Andika thamani ukitumia ufunguo unaofuata
val2key[_value] = key2val.length+1;
Ongeza utafutaji wa kinyume (kutoka kwa thamani hadi ufunguo).
key2val.push(_value);
Ongeza utafutaji wa mbele (kutoka kwa ufunguo hadi thamani). Kwa sababu tunakabidhi thamani kwa mfuatano tunaweza kuiongeza tu baada ya thamani ya mwisho ya safu.
return key2val.length;
} // cacheWrite
Rudisha urefu mpya wa key2val, ambayo ni seli ambapo thamani mpya imehifadhiwa.
function _calldataVal(uint startByte, uint length)
private pure returns (uint)
Kazi hii inasoma thamani kutoka kwa data za mwito za urefu wa kiholela (hadi baiti 32, saizi ya neno).
{
uint _retVal;
require(length < 0x21,
"_calldataVal length limit is 32 bytes");
require(length + startByte <= msg.data.length,
"_calldataVal trying to read beyond calldatasize");
Kazi hii ni ya ndani, kwa hivyo ikiwa msimbo uliosalia umeandikwa kwa usahihi majaribio haya hayahitajiki. Hata hivyo, hayagharimu sana kwa hivyo tunaweza kuwa nayo pia.
assembly {
_retVal := calldataload(startByte)
}
Msimbo huu upo katika Yul (opens in a new tab). Inasoma thamani ya baiti 32 kutoka kwa data za mwito. Hii inafanya kazi hata kama data za mwito zitasimama kabla ya startByte+32 kwa sababu nafasi isiyoanzishwa katika EVM inachukuliwa kuwa sifuri.
_retVal = _retVal >> (256-length*8);
Sio lazima tutake thamani ya baiti 32. Hii inaondoa baiti za ziada.
return _retVal;
} // _calldataVal
// Soma kigezo kimoja kutoka kwenye data za mwito, kuanzia kwenye _fromByte
function _readParam(uint _fromByte) internal
returns (uint _nextByte, uint _parameterValue)
{
Soma kigezo kimoja kutoka kwa data za mwito. Kumbuka kwamba tunahitaji kurudisha sio tu thamani tunayosoma, lakini pia eneo la baiti inayofuata kwa sababu vigezo vinaweza kuanzia urefu wa baiti 1 hadi baiti 33.
// Baiti ya kwanza inatuambia jinsi ya kufasiri zilizosalia
uint8 _firstByte;
_firstByte = uint8(_calldataVal(_fromByte, 1));
Solidity inajaribu kupunguza idadi ya hitilafu kwa kukataza ubadilishaji wa aina isiyo wazi (opens in a new tab) unaoweza kuwa hatari. Kushusha daraja, kwa mfano kutoka biti 256 hadi biti 8, kunahitaji kuwa wazi.
// Soma thamani, lakini usiiandike kwenye kache
if (_firstByte == uint8(DONT_CACHE))
return(_fromByte+33, _calldataVal(_fromByte+1, 32));
// Soma thamani, na uiandike kwenye kache
if (_firstByte == uint8(INTO_CACHE)) {
uint _param = _calldataVal(_fromByte+1, 32);
cacheWrite(_param);
return(_fromByte+33, _param);
}
// Ikiwa tumefika hapa inamaanisha kuwa tunahitaji kusoma kutoka kwenye kache
// Idadi ya baiti za ziada za kusoma
uint8 _extraBytes = _firstByte / 16;
Chukua nibble (opens in a new tab) ya chini na uiunganishe na baiti zingine ili kusoma thamani kutoka kwenye kache.
uint _key = (uint256(_firstByte & 0x0F) << (8*_extraBytes)) +
_calldataVal(_fromByte+1, _extraBytes);
return (_fromByte+_extraBytes+1, cacheRead(_key));
} // _readParam
// Soma vigezo n (kazi zinajua ni vigezo vingapi zinatarajia)
function _readParams(uint _paramNum) internal returns (uint[] memory) {
Tungeweza kupata idadi ya vigezo tulivyo navyo kutoka kwa data za mwito yenyewe, lakini kazi zinazotuita zinajua ni vigezo vingapi wanavyotarajia. Ni rahisi kuwaacha watuambie.
// Vigezo tunavyosoma
uint[] memory params = new uint[](_paramNum);
// Vigezo vinaanza kwenye baiti ya 4, kabla ya hapo ni saini ya kazi
uint _atByte = 4;
for(uint i=0; i<_paramNum; i++) {
(_atByte, params[i]) = _readParam(_atByte);
}
Soma vigezo hadi uwe na nambari unayohitaji. Ikiwa tutapita mwisho wa data za mwito, _readParams itatengua mwito.
return(params);
} // readParams
// Kwa ajili ya kujaribu _readParams, jaribu kusoma vigezo vinne
function fourParam() public
returns (uint256,uint256,uint256,uint256)
{
uint[] memory params;
params = _readParams(4);
return (params[0], params[1], params[2], params[3]);
} // fourParam
Faida moja kubwa ya Foundry ni kwamba inaruhusu majaribio kuandikwa katika Solidity (tazama Kujaribu kache hapa chini). Hii inafanya majaribio ya kitengo kuwa rahisi sana. Hii ni kazi inayosoma vigezo nne na kuzirudisha ili jaribio liweze kuthibitisha kuwa zilikuwa sahihi.
// Pata thamani, rudisha baiti ambazo zitaisimba (ukitumia kache ikiwezekana)
function encodeVal(uint _val) public view returns(bytes memory) {
encodeVal ni kazi ambayo msimbo wa nje ya mnyororo huita ili kusaidia kuunda data za mwito zinazotumia kache. Inapokea thamani moja na kurudisha baiti zinazoisimba. Kazi hii ni view, kwa hivyo haihitaji muamala na inapoitwa kwa nje haigharimu gesi yoyote.
uint _key = val2key[_val];
// Thamani bado haipo kwenye kache, iongeze
if (_key == 0)
return bytes.concat(INTO_CACHE, bytes32(_val));
Katika EVM uhifadhi wote ambao haujaanzishwa unachukuliwa kuwa sifuri. Kwa hivyo tukitafuta ufunguo wa thamani ambayo haipo, tunapata sifuri. Katika hali hiyo baiti zinazoisimba ni INTO_CACHE (kwa hivyo itahifadhiwa kwenye kache wakati ujao), ikifuatiwa na thamani halisi.
// Ikiwa ufunguo ni <0x10, urudishe kama baiti moja
if (_key < 0x10)
return bytes.concat(bytes1(uint8(_key)));
Baiti moja ndizo rahisi zaidi. Tunatumia tu bytes.concat (opens in a new tab) kugeuza aina ya bytes<n> kuwa safu ya baiti ambayo inaweza kuwa ya urefu wowote. Licha ya jina, inafanya kazi vizuri inapotolewa na hoja moja tu.
// Thamani ya baiti mbili, iliyosimbwa kama 0x1vvv
if (_key < 0x1000)
return bytes.concat(bytes2(uint16(_key) | 0x1000));
Tunapokuwa na ufunguo ambao ni chini ya 163, tunaweza kuueleza katika baiti mbili. Kwanza tunabadilisha _key, ambayo ni thamani ya biti 256, kuwa thamani ya biti 16 na kutumia 'au' ya kimantiki kuongeza idadi ya baiti za ziada kwenye baiti ya kwanza. Kisha tunaiweka tu kwenye thamani ya bytes2, ambayo inaweza kubadilishwa kuwa bytes.
// Pengine kuna njia janja ya kufanya mistari ifuatayo kama kitanzi,
// lakini ni kazi ya kutazama kwa hivyo ninaboresha kwa ajili ya muda wa mtayarishaji programu na
// urahisi.
if (_key < 16*256**2)
return bytes.concat(bytes3(uint24(_key) | (0x2 * 16 * 256**2)));
if (_key < 16*256**3)
return bytes.concat(bytes4(uint32(_key) | (0x3 * 16 * 256**3)));
.
.
.
if (_key < 16*256**14)
return bytes.concat(bytes15(uint120(_key) | (0xE * 16 * 256**14)));
if (_key < 16*256**15)
return bytes.concat(bytes16(uint128(_key) | (0xF * 16 * 256**15)));
Thamani zingine (baiti 3, baiti 4, n.k.) zinashughulikiwa kwa njia sawa, tu na saizi tofauti za uwanja.
// Ikiwa tutafika hapa, kuna kitu kibaya.
revert("Error in encodeVal, should not happen");
Ikiwa tutafika hapa inamaanisha tulipata ufunguo ambao sio chini ya 16*25615. Lakini cacheWrite inaweka kikomo kwa funguo kwa hivyo hatuwezi hata kufika hadi 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 baadaye ataleta hitilafu.
} // encodeVal
} // Cache
Kujaribu kache
Moja ya faida za Foundry ni kwamba inakuruhusu kuandika majaribio katika Solidity (opens in a new tab), ambayo inafanya iwe rahisi kuandika majaribio ya kitengo. Majaribio ya darasa la Cache yako hapa (opens in a new tab). Kwa sababu msimbo wa majaribio unajirudia, kama majaribio yanavyokuwa, makala haya yanaelezea tu sehemu za kuvutia.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
// Unahitaji kuendesha `forge test -vv` kwa ajili ya kiweko.
import "forge-std/console.sol";
Hii ni boilerplate tu ambayo ni muhimu kutumia kifurushi cha majaribio na console.log.
import "src/Cache.sol";
Tunahitaji kujua mkataba tunaoujaribu.
contract CacheTest is Test {
Cache cache;
function setUp() public {
cache = new Cache();
}
Kazi ya setUp inaitwa kabla ya kila jaribio. Katika kesi hii tunaunda tu kache mpya, ili majaribio yetu yasiathiriane.
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.
for(uint i=1; i<5000; i++) {
cache.cacheWrite(i*i);
}
for(uint i=1; i<5000; i++) {
assertEq(cache.cacheRead(i), i*i);
Hivi ndivyo unavyofanya majaribio halisi, ukitumia kazi za assert... (opens in a new tab). Katika kesi hii, tunathibitisha kwamba thamani tuliyoandika ndiyo tuliyosoma. Tunaweza kutupa matokeo ya cache.cacheWrite kwa sababu tunajua kwamba funguo za kache zinakabidhiwa kwa mstari.
}
} // testCaching
// Hifadhi kwenye kache thamani sawa mara nyingi, hakikisha kwamba ufunguo unabaki
// sawa
function testRepeatCaching() public {
for(uint i=1; i<100; i++) {
uint _key1 = cache.cacheWrite(i);
uint _key2 = cache.cacheWrite(i);
assertEq(_key1, _key2);
}
Kwanza tunaandika kila thamani mara mbili kwenye kache na kuhakikisha funguo ni sawa (ikimaanisha uandishi wa pili haukufanyika kweli).
for(uint i=1; i<100; i+=3) {
uint _key = cache.cacheWrite(i);
assertEq(_key, i);
}
} // testRepeatCaching
Kinadharia kunaweza kuwa na hitilafu ambayo haiathiri uandishi wa kache mfululizo. Kwa hivyo hapa tunafanya baadhi ya maandishi ambayo sio mfululizo na kuona thamani bado haziandikwi upya.
// Soma uint kutoka kwenye bafa ya kumbukumbu (kuhakikisha tunapata tena vigezo
// tulivyotuma)
function toUint256(bytes memory _bytes, uint256 _start) internal pure
returns (uint256)
Soma neno la biti 256 kutoka kwa bafa ya bytes memory. Kazi hii ya matumizi inaturuhusu kuthibitisha kwamba tunapokea matokeo sahihi tunapoendesha mwito wa kazi unaotumia kache.
{
require(_bytes.length >= _start + 32, "toUint256_outOfBounds");
uint256 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x20), _start))
}
Yul haiauni miundo ya data zaidi ya uint256, kwa hivyo unaporejelea muundo wa data wa kisasa zaidi, kama vile bafa ya kumbukumbu _bytes, unapata anwani ya muundo huo. Solidity inahifadhi thamani za bytes memory kama neno la baiti 32 ambalo lina urefu, likifuatiwa na baiti halisi, kwa hivyo ili kupata nambari ya baiti _start tunahitaji kukokotoa _bytes+32+_start.
return tempUint;
} // toUint256
// Saini ya kazi kwa fourParams(), kwa hisani ya
// https://www.4byte.directory/signatures/?bytes4_signature=0x3edc1e6d
bytes4 constant FOUR_PARAMS = 0x3edc1e6d;
// Baadhi tu ya thamani zisizobadilika ili kuona tunapata thamani sahihi
uint256 constant VAL_A = 0xDEAD60A7;
uint256 constant VAL_B = 0xBEEF;
uint256 constant VAL_C = 0x600D;
uint256 constant VAL_D = 0x600D60A7;
Baadhi ya viwango visivyobadilika tunavyohitaji kwa majaribio.
function testReadParam() public {
Ita fourParams(), kazi inayotumia readParams, ili kujaribu tunaweza kusoma vigezo kwa usahihi.
address _cacheAddr = address(cache);
bool _success;
bytes memory _callInput;
bytes memory _callOutput;
Hatuwezi kutumia utaratibu wa kawaida wa ABI kuita kazi kwa kutumia kache, kwa hivyo tunahitaji kutumia utaratibu wa kiwango cha chini wa <address>.call() (opens in a new tab). Utaratibu huo unachukua bytes memory kama ingizo, na kurudisha hiyo (pamoja na thamani ya Boolean) kama towe.
// Mwito wa kwanza, kache ni tupu
_callInput = bytes.concat(
FOUR_PARAMS,
Ni muhimu kwa mkataba huo huo kusaidia kazi zote mbili zilizohifadhiwa kwenye kache (kwa miito moja kwa moja kutoka kwa miamala) na kazi ambazo hazijahifadhiwa kwenye kache (kwa miito kutoka kwa mikataba mingine mahiri). 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 kunafanya utangamano kuwa rahisi sana. Baiti moja ingetosha kutambua kazi katika hali nyingi, kwa hivyo tunapoteza baiti tatu (gesi 16*3=48). Hata hivyo, ninapoandika haya gesi hizo 48 zinagharimu senti 0.07, ambayo ni gharama nzuri ya msimbo rahisi, usio na uwezekano wa kuwa na hitilafu.
// Thamani ya kwanza, iongeze kwenye kache
cache.INTO_CACHE(),
bytes32(VAL_A),
Thamani ya kwanza: Bendera inayosema ni thamani kamili inayohitaji kuandikwa kwenye kache, ikifuatiwa na baiti 32 za thamani. Thamani zingine tatu zinafanana, isipokuwa kwamba VAL_B haiandikwi kwenye kache na VAL_C ni kigezo cha tatu na cha nne.
.
.
.
);
(_success, _callOutput) = _cacheAddr.call(_callInput);
Hapa ndipo tunapoita mkataba wa Cache.
assertEq(_success, true);
Tunatarajia mwito ufanikiwe.
assertEq(cache.cacheRead(1), VAL_A);
assertEq(cache.cacheRead(2), VAL_C);
Tunaanza na kache tupu na kisha kuongeza VAL_A ikifuatiwa na VAL_C. Tungetarajia ya kwanza kuwa na ufunguo 1, na ya pili kuwa na 2.
assertEq(toUint256(_callOutput,0), VAL_A);
assertEq(toUint256(_callOutput,32), VAL_B);
assertEq(toUint256(_callOutput,64), VAL_C);
assertEq(toUint256(_callOutput,96), VAL_C);
Towe ni vigezo nne. Hapa tunathibitisha kuwa ni sahihi.
// Mwito wa pili, tunaweza kutumia kache
_callInput = bytes.concat(
FOUR_PARAMS,
// Thamani ya kwanza kwenye Kache
bytes1(0x01),
Funguo za kache chini ya 16 ni baiti moja tu.
// Thamani ya pili, usiongeze kwenye kache
cache.DONT_CACHE(),
bytes32(VAL_B),
// Thamani ya tatu na ya nne, thamani sawa
bytes1(0x02),
bytes1(0x02)
);
.
.
.
} // testReadParam
Majaribio baada ya mwito yanafanana na yale ya baada ya mwito wa kwanza.
function testEncodeVal() public {
Kazi hii inafanana na testReadParam, isipokuwa kwamba badala ya kuandika vigezo waziwazi tunatumia encodeVal().
.
.
.
_callInput = bytes.concat(
FOUR_PARAMS,
cache.encodeVal(VAL_A),
cache.encodeVal(VAL_B),
cache.encodeVal(VAL_C),
cache.encodeVal(VAL_D)
);
.
.
.
assertEq(_callInput.length, 4+1*4);
} // testEncodeVal
Jaribio pekee la ziada katika testEncodeVal() ni kuthibitisha kwamba urefu wa _callInput ni sahihi. Kwa mwito wa kwanza ni 4+33*4. Kwa wa pili, ambapo kila thamani tayari iko kwenye kache, ni 4+1*4.
// Jaribu encodeVal wakati ufunguo ni zaidi ya baiti moja
// Kiwango cha juu cha baiti tatu kwa sababu kujaza kache hadi baiti nne inachukua
// muda mrefu sana.
function testEncodeValBig() public {
// Weka idadi ya thamani kwenye kache.
// Ili kuweka mambo rahisi, tumia ufunguo n kwa thamani n.
for(uint i=1; i<0x1FFF; i++) {
cache.cacheWrite(i);
}
Kazi ya testEncodeVal hapo juu inaandika tu thamani nne kwenye kache, kwa hivyo sehemu ya kazi inayoshughulikia thamani za baiti nyingi (opens in a new tab) haijakaguliwa. Lakini msimbo huo ni mgumu na una uwezekano wa kuwa na makosa.
Sehemu ya kwanza ya kazi hii ni kitanzi kinachoandika thamani zote kutoka 1 hadi 0x1FFF kwenye kache kwa mpangilio, kwa hivyo tutaweza kusimba thamani hizo na kujua zinakokwenda.
.
.
.
_callInput = bytes.concat(
FOUR_PARAMS,
cache.encodeVal(0x000F), // Baiti moja 0x0F
cache.encodeVal(0x0010), // Baiti mbili 0x1010
cache.encodeVal(0x0100), // Baiti mbili 0x1100
cache.encodeVal(0x1000) // Baiti tatu 0x201000
);
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 staki (angalau 0x10000000, takriban robo bilioni).
.
.
.
.
} // testEncodeValBig
// Jaribu nini kwa bafa ndogo kupita kiasi tunapata tengua
function testShortCalldata() public {
Jaribu kile kinachotokea katika hali isiyo ya kawaida ambapo hakuna vigezo vya kutosha.
.
.
.
(_success, _callOutput) = _cacheAddr.call(_callInput);
assertEq(_success, false);
} // testShortCalldata
Kwa kuwa inatengua, matokeo tunayopaswa kupata ni false.
// Ita na funguo za kache ambazo hazipo
function testNoCacheKey() public {
.
.
.
_callInput = bytes.concat(
FOUR_PARAMS,
// Thamani ya kwanza, iongeze kwenye kache
cache.INTO_CACHE(),
bytes32(VAL_A),
// Thamani ya pili
bytes1(0x0F),
bytes2(0x1234),
bytes11(0xA10102030405060708090A)
);
Kazi hii inapata vigezo nne halali kabisa, isipokuwa kwamba kache ni tupu kwa hivyo hakuna thamani hapo za kusoma.
.
.
.
// Jaribu nini kwa bafa ndefu kupita kiasi kila kitu kinafanya kazi sawa
function testLongCalldata() public {
address _cacheAddr = address(cache);
bool _success;
bytes memory _callInput;
bytes memory _callOutput;
// Mwito wa kwanza, kache ni tupu
_callInput = bytes.concat(
FOUR_PARAMS,
// Thamani ya kwanza, iongeze kwenye kache
cache.INTO_CACHE(), bytes32(VAL_A),
// Thamani ya pili, iongeze kwenye kache
cache.INTO_CACHE(), bytes32(VAL_B),
// Thamani ya tatu, iongeze kwenye kache
cache.INTO_CACHE(), bytes32(VAL_C),
// Thamani ya nne, iongeze kwenye kache
cache.INTO_CACHE(), bytes32(VAL_D),
// Na thamani nyingine kwa ajili ya "bahati nzuri"
bytes4(0x31112233)
);
Kazi hii inatuma thamani tano. Tunajua kwamba thamani ya tano inapuuzwa kwa sababu sio ingizo halali la kache, ambalo lingesababisha kutengua kama isingejumuishwa.
(_success, _callOutput) = _cacheAddr.call(_callInput);
assertEq(_success, true);
.
.
.
} // testLongCalldata
} // CacheTest
Programu tumizi ya mfano
Kuandika majaribio katika Solidity ni vizuri sana, lakini mwisho wa siku programu tumizi iliyogatuliwa (dapp) inahitaji kuwa na uwezo wa kuchakata maombi kutoka nje ya mnyororo ili iwe na manufaa. Makala haya yanaonyesha jinsi ya kutumia kache katika dapp na WORM, ambayo inasimama kwa "Andika Mara Moja, Soma Nyingi". Ikiwa ufunguo bado haujaandikwa, unaweza kuandika thamani kwake. Ikiwa ufunguo tayari umeandikwa, unapata kutengua.
Mkataba
Huu ndio mkataba (opens in a new tab). Unarudia zaidi kile ambacho tayari tumefanya na Cache na CacheTest, kwa hivyo tunashughulikia tu sehemu zinazovutia.
import "./Cache.sol";
contract WORM is Cache {
Njia rahisi zaidi ya kutumia Cache ni kuirithi katika mkataba wetu wenyewe.
function writeEntryCached() external {
uint[] memory params = _readParams(2);
writeEntry(params[0], params[1]);
} // writeEntryCached
Kazi hii inafanana na fourParam katika CacheTest hapo juu. Kwa sababu hatufuati vipimo vya ABI, ni bora kutotangaza vigezo vyovyote kwenye kazi.
// Ifanye iwe rahisi kutuita
// Saini ya kazi kwa writeEntryCached(), kwa hisani ya
// https://www.4byte.directory/signatures/?bytes4_signature=0xe4e4f2d3
bytes4 constant public WRITE_ENTRY_CACHED = 0xe4e4f2d3;
Msimbo wa nje unaoita writeEntryCached utahitaji kuunda data za mwito kwa mikono, badala ya kutumia worm.writeEntryCached, kwa sababu hatufuati vipimo vya ABI. Kuwa na thamani hii isiyobadilika kunafanya iwe rahisi kuiandika.
Kumbuka kwamba ingawa tunafafanua WRITE_ENTRY_CACHED kama kigezo cha hali, ili kuisoma kwa nje ni muhimu kutumia kazi ya getter kwa ajili yake, worm.WRITE_ENTRY_CACHED().
function readEntry(uint key) public view
returns (uint _value, address _writtenBy, uint _writtenAtBlock)
Kazi ya kusoma ni view, kwa hivyo haihitaji muamala na haigharimu gesi. Kama matokeo, hakuna faida ya kutumia kache kwa kigezo. Kwa kazi za kutazama ni bora kutumia utaratibu wa kawaida ambao ni rahisi.
Msimbo wa majaribio
Huu ni msimbo wa majaribio kwa mkataba (opens in a new tab). Tena, hebu tuangalie tu kile kinachovutia.
function testWReadWrite() public {
worm.writeEntry(0xDEAD, 0x60A7);
vm.expectRevert(bytes("entry already written"));
worm.writeEntry(0xDEAD, 0xBEEF);
Hivi (vm.expectRevert) (opens in a new tab) ndivyo tunavyobainisha katika jaribio la Foundry kwamba mwito unaofuata unapaswa kushindwa, na sababu iliyoripotiwa ya kushindwa. Hii inatumika tunapotumia sintaksia <contract>.<function name>() badala ya kuunda data za mwito na kuita mkataba kwa kutumia kiolesura cha kiwango cha chini (<contract>.call(), n.k.).
function testReadWriteCached() public {
uint cacheGoat = worm.cacheWrite(0x60A7);
Hapa tunatumia ukweli kwamba cacheWrite inarudisha ufunguo wa kache. Hili sio jambo ambalo tungetarajia 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 mnyororoni, na msimbo wa mnyororoni hauhitaji kache ya kigezo.
(_success,) = address(worm).call(_callInput);
Hivi ndivyo tunavyoiambia Solidity kwamba ingawa <contract address>.call() ina thamani mbili za kurudisha, tunajali tu ya kwanza.
(_success,) = address(worm).call(_callInput);
assertEq(_success, false);
Kwa kuwa tunatumia kazi ya kiwango cha chini ya <address>.call(), hatuwezi kutumia vm.expectRevert() na inabidi tuangalie thamani ya mafanikio ya boolean tunayopata kutoka kwa mwito.
event EntryWritten(uint indexed key, uint indexed value);
.
.
.
_callInput = bytes.concat(
worm.WRITE_ENTRY_CACHED(), worm.encodeVal(a), worm.encodeVal(b));
vm.expectEmit(true, true, false, false);
emit EntryWritten(a, b);
(_success,) = address(worm).call(_callInput);
Hii ndiyo njia tunayothibitisha kwamba msimbo unatoa tukio kwa usahihi (opens in a new tab) katika Foundry.
Mteja
Jambo moja ambalo hupati na majaribio ya Solidity ni msimbo wa JavaScript ambao unaweza kukata na kubandika kwenye programu yako mwenyewe. Toleo la asili la mafunzo haya lilisambaza WORM kwenye Optimism Goerli, ambayo tangu wakati huo imestaafishwa. Ili kuendesha mteja leo, sambaza tena WORM kwenye mtandao unaotumika wa OP Stack kama vile OP Sepolia (opens in a new tab), kisha utumie anwani ya mkataba inayotokana katika mteja wa JavaScript.
Unaweza kuona msimbo wa JavaScript kwa ajili ya mteja hapa (opens in a new tab). Hifadhi ya mfano iliandikwa kwa ajili ya Optimism Goerli, kwa hivyo kabla ya kuiendesha, sasisha mwisho wa RPC na URL za kichunguzi katika javascript/.env.example na javascript/index.js kwa mtandao wako lengwa. Ili kuitumia:
-
Nakili hifadhi ya git:
git clone https://github.com/qbzzt/20220915-all-you-can-cache.git -
Sakinisha vifurushi muhimu:
cd javascript yarn -
Nakili faili ya usanidi:
cp .env.example .env -
Hariri
.envkwa ajili ya usanidi wako:Kigezo Thamani MNEMONIC Mnemonic kwa ajili ya akaunti ambayo ina ETH ya kutosha kulipia muamala. Nyaraka za bomba za Optimism (opens in a new tab) zinaorodhesha mabomba ya sasa ya mtandao wa majaribio. OPTIMISM_GOERLI_URL URL ya RPC kwa mtandao ambapo unasambaza tena WORM. Kwa OP Sepolia, tumia mwisho wa RPC wa OP Sepolia kama vile https://sepolia.optimism.io, au mwisho mwingine kutoka kwa mtoa huduma wako. -
Endesha
index.js.node index.jsProgramu tumizi hii ya mfano kwanza inaandika ingizo kwenye WORM, ikionyesha data za mwito na kiungo cha muamala kwenye kichunguzi cha bloku. Kisha inasoma tena ingizo hilo, na kuonyesha ufunguo inaotumia na thamani katika ingizo (thamani, nambari ya kitalu, na mwandishi).
Sehemu kubwa ya mteja ni JavaScript ya kawaida ya programu tumizi iliyogatuliwa (dapp). Kwa hivyo tena tutapitia tu sehemu zinazovutia.
.
.
.
const main = async () => {
const func = await worm.WRITE_ENTRY_CACHED()
// Inahitaji ufunguo mpya kila wakati
const key = await worm.encodeVal(Number(new Date()))
Sloti fulani inaweza kuandikwa mara moja tu, kwa hivyo tunatumia muhuri wa muda ili kuhakikisha hatutumii tena sloti.
const val = await worm.encodeVal("0x600D")
// Andika ingizo
const calldata = func + key.slice(2) + val.slice(2)
Ethers inatarajia data za mwito kuwa mfuatano wa heksadesimali, 0x ikifuatiwa na nambari shufwa ya tarakimu za heksadesimali. Kwa kuwa key na val zote zinaanza na 0x, tunahitaji kuondoa vichwa hivyo.
const tx = await worm.populateTransaction.writeEntryCached()
tx.data = calldata
sentTx = await wallet.sendTransaction(tx)
Kama ilivyo kwa msimbo wa majaribio wa Solidity, hatuwezi kuita kazi iliyohifadhiwa kwenye kache kwa kawaida. Badala yake, tunahitaji kutumia utaratibu wa kiwango cha chini.
.
.
.
// Soma ingizo lililoandikwa hivi punde
const realKey = '0x' + key.slice(4) // ondoa bendera ya FF
const entryRead = await worm.readEntry(realKey)
.
.
.
Kwa kusoma maingizo tunaweza kutumia utaratibu wa kawaida. Hakuna haja ya kutumia kache ya kigezo na kazi za view.
Hitimisho
Msimbo katika makala haya ni uthibitisho wa dhana, madhumuni ni kufanya wazo liwe rahisi kueleweka. Kwa mfumo ulio tayari kwa uzalishaji unaweza kutaka kutekeleza utendaji wa ziada:
-
Kushughulikia thamani ambazo sio
uint256. Kwa mfano, mifuatano. -
Badala ya kache ya kimataifa, labda uwe na uchoraji ramani kati ya watumiaji na kache. Watumiaji tofauti hutumia thamani tofauti.
-
Thamani zinazotumika kwa anwani ni tofauti na zile zinazotumika kwa madhumuni mengine. Inaweza kuleta maana kuwa na kache tofauti kwa ajili ya anwani tu.
-
Kwa sasa, funguo za kache ziko kwenye algoriti ya "anayekuja kwanza, 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 vihesabio vya matumizi kwenye maingizo ya kache na kuyapanga upya ili thamani kumi na sita zinazojulikana zaidi ziwe baiti moja, thamani 4080 zinazojulikana zaidi zinazofuata ziwe baiti mbili, n.k.
Hata hivyo, hiyo ni operesheni inayoweza kuwa hatari. Fikiria mfuatano ufuatao wa matukio:
-
Noam Naive anaita
encodeValili kusimba anwani anayotaka kutuma tokeni. Anwani hiyo ni mojawapo ya za kwanza kutumika kwenye programu, kwa hivyo thamani iliyosimbwa ni 0x06. Hii ni kazi yaview, sio muamala, kwa hivyo ni kati ya Noam na nodi anayotumia, na hakuna mtu mwingine anayejua kuihusu -
Owen Owner anaendesha operesheni ya kupanga upya kache. Watu wachache sana hutumia anwani hiyo, kwa hivyo sasa imesimbwa kama 0x201122. Thamani tofauti, 1018, inakabidhiwa 0x06.
-
Noam Naive anatuma tokeni zake kwa 0x06. Zinaenda kwenye anwani
0x0000000000000000000000000de0b6b3a7640000, na kwa kuwa hakuna anayejua ufunguo wa siri wa anwani hiyo, zinakwama tu hapo. Noam hafurahii.
Kuna njia za kutatua tatizo hili, na tatizo linalohusiana la miamala ambayo iko kwenye mempool wakati wa kupanga upya kache, lakini lazima uwe na ufahamu nalo.
-
Nilionyesha kache hapa na Optimism, kwa sababu mimi ni mfanyakazi wa Optimism na huu ndio mkusanyiko ninaoujua vizuri zaidi. Lakini inapaswa kufanya kazi na mkusanyiko wowote unaotoza gharama ndogo kwa usindikaji wa ndani, ili kwa kulinganisha kuandika data ya muamala kwa tabaka la 1 (l1) ndiyo gharama kubwa.