Ruka hadi kwenye maudhui makuu

Kache kadiri uwezavyo

tabaka la 2
kache
uhifadhi
kuongeza viwango
Kati
Ori Pomerantz
15 Septemba 2022
22 dakika za kusoma

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:

  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 usiiandike 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 data za mwitoUfunguo wa kache
    0x0F0x0F
    0x10,0x100x10
    0x12,0xAC0x02AC
    0x2D,0xEA, 0xD60x0DEAD6

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.

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

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.

Soma vigezo hadi uwe na nambari unayohitaji. Ikiwa tutapita mwisho wa data za mwito, _readParams itatengua mwito.

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.

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.

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.

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.

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().

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.

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.

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

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.

Kazi hii inapata vigezo nne halali kabisa, isipokuwa kwamba kache ni tupu kwa hivyo hakuna thamani hapo za kusoma.

Kazi hii inatuma thamani tano. Tunajua kwamba thamani ya tano inapuuzwa kwa sababu sio ingizo halali la kache, ambalo lingesababisha kutengua kama isingejumuishwa.

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.

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:

  1. Nakili hifadhi ya git:

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

    cd javascript
    yarn
    
  3. Nakili faili ya usanidi:

    cp .env.example .env
    
  4. Hariri .env kwa ajili ya usanidi wako:

    KigezoThamani
    MNEMONICMnemonic 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_URLURL 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.
  5. Endesha index.js.

    node index.js
    

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

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:

    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, sio muamala, kwa hivyo ni kati ya Noam na nodi anayotumia, na hakuna mtu mwingine anayejua kuihusu

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

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

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