ప్రధాన కంటెంట్‌కు దాటవేయి

మీరు కాష్ చేయగలిగినదంతా

లేయర్ 2
కాషింగ్
నిల్వ
స్కేలింగ్
మధ్యస్థ స్థాయి
ఓరి పోమెరాంట్జ్
15 సెప్టెంబర్, 2022
19 నిమిషాల పఠనం

రోల్అప్‌లను ఉపయోగిస్తున్నప్పుడు లావాదేవీలోని ఒక బైట్ ఖర్చు నిల్వ స్లాట్ ఖర్చు కంటే చాలా ఖరీదైనది. కాబట్టి, సాధ్యమైనంత ఎక్కువ సమాచారాన్ని ఆన్‌చైన్‌లో కాష్ చేయడం సమంజసం.

ఈ కథనంలో, బహుళ సార్లు ఉపయోగించబడే అవకాశం ఉన్న ఏదైనా పారామీటర్ విలువ కాష్ చేయబడి, (మొదటి సారి తర్వాత) చాలా తక్కువ సంఖ్యలో బైట్‌లతో ఉపయోగించడానికి అందుబాటులో ఉండే విధంగా కాషింగ్ కాంట్రాక్ట్‌ను ఎలా సృష్టించాలో మరియు ఉపయోగించాలో, అలాగే ఈ కాష్‌ను ఉపయోగించే ఆఫ్‌చైన్ కోడ్‌ను ఎలా వ్రాయాలో మీరు నేర్చుకుంటారు.

మీరు కథనాన్ని దాటవేసి, కేవలం సోర్స్ కోడ్‌ను చూడాలనుకుంటే, అది ఇక్కడ ఉంది (opens in a new tab). డెవలప్‌మెంట్ స్టాక్ Foundry (opens in a new tab).

మొత్తం డిజైన్

సరళత కోసం అన్ని లావాదేవీ పారామీటర్‌లు uint256, 32 బైట్‌ల పొడవు ఉన్నాయని మనం అనుకుందాం. మనం లావాదేవీని స్వీకరించినప్పుడు, ప్రతి పారామీటర్‌ను ఈ విధంగా అన్వయిస్తాము:

  1. మొదటి బైట్ 0xFF అయితే, తదుపరి 32 బైట్‌లను పారామీటర్ విలువగా తీసుకుని, దానిని కాష్‌కు వ్రాయండి.

  2. మొదటి బైట్ 0xFE అయితే, తదుపరి 32 బైట్‌లను పారామీటర్ విలువగా తీసుకోండి కానీ దానిని కాష్‌కు వ్రాయవద్దు (not).

  3. మరే ఇతర విలువకైనా, ఎగువ నాలుగు బిట్‌లను అదనపు బైట్‌ల సంఖ్యగా మరియు దిగువ నాలుగు బిట్‌లను కాష్ కీ యొక్క అత్యంత ముఖ్యమైన బిట్‌లుగా తీసుకోండి. ఇక్కడ కొన్ని ఉదాహరణలు ఉన్నాయి:

    కాల్ డేటాలోని బైట్‌లుకాష్ కీ
    0x0F0x0F
    0x10,0x100x10
    0x12,0xAC0x02AC
    0x2D,0xEA, 0xD60x0DEAD6

కాష్ మానిప్యులేషన్

కాష్ Cache.sol (opens in a new tab)లో అమలు చేయబడింది. దానిని లైన్ బై లైన్ పరిశీలిద్దాం.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;


contract Cache {

    bytes1 public constant INTO_CACHE = 0xFF;
    bytes1 public constant DONT_CACHE = 0xFE;

మనం మొత్తం సమాచారాన్ని అందించే మరియు దానిని కాష్‌లోకి వ్రాయాలనుకుంటున్నామా లేదా అనే ప్రత్యేక సందర్భాలను అర్థం చేసుకోవడానికి ఈ స్థిరాంకాలు ఉపయోగించబడతాయి. కాష్‌లోకి వ్రాయడానికి గతంలో ఉపయోగించని నిల్వ స్లాట్‌లలోకి రెండు SSTORE (opens in a new tab) ఆపరేషన్‌లు అవసరం, ఒక్కొక్క దానికి 22100 గ్యాస్ ఖర్చవుతుంది, కాబట్టి మనం దానిని ఐచ్ఛికం చేస్తాము.


    mapping(uint => uint) public val2key;

విలువల మరియు వాటి కీల మధ్య మ్యాపింగ్ (opens in a new tab). మీరు లావాదేవీని పంపే ముందు విలువలను ఎన్‌కోడ్ చేయడానికి ఈ సమాచారం అవసరం.

    // స్థానం n, కీ n+1 కోసం విలువను కలిగి ఉంటుంది, ఎందుకంటే మనం భద్రపరచాలి
    // సున్నాను "కాష్‌లో లేదు" అని.
    uint[] public key2val;

కీల నుండి విలువలకు మ్యాపింగ్ చేయడానికి మనం శ్రేణిని (array) ఉపయోగించవచ్చు ఎందుకంటే మనం కీలను కేటాయిస్తాము మరియు సరళత కోసం మనం దానిని వరుసగా చేస్తాము.

    function cacheRead(uint _key) public view returns (uint) {
        require(_key <= key2val.length, "Reading uninitialize cache entry");
        return key2val[_key-1];
    }  // cacheRead

కాష్ నుండి విలువను చదవండి.

    // విలువ ఇప్పటికే కాష్‌లో లేకుంటే దాన్ని కాష్‌లో రాయండి
    // పరీక్ష పనిచేయడానికి వీలుగా పబ్లిక్ మాత్రమే చేయబడింది
    function cacheWrite(uint _value) public returns (uint) {
        // విలువ ఇప్పటికే కాష్‌లో ఉంటే, ప్రస్తుత కీని తిరిగి ఇవ్వండి
        if (val2key[_value] != 0) {
            return val2key[_value];
        }

ఒకే విలువను కాష్‌లో ఒకటి కంటే ఎక్కువసార్లు ఉంచడంలో అర్థం లేదు. విలువ ఇప్పటికే ఉంటే, ఇప్పటికే ఉన్న కీని తిరిగి ఇవ్వండి.

        // 0xFE అనేది ఒక ప్రత్యేక సందర్భం కాబట్టి, కాష్ కలిగి ఉండగల అతిపెద్ద కీ
        // 0x0D తర్వాత 15 0xFFలు. కాష్ పొడవు ఇప్పటికే అంత
        // పెద్దదిగా ఉంటే, విఫలం అవుతుంది.
        //                              1 2 3 4 5 6 7 8 9 A B C D E F
        require(key2val.length+1 < 0x0DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
            "cache overflow");

మనకు అంత పెద్ద కాష్ ఎప్పుడైనా వస్తుందని నేను అనుకోను (సుమారు 1.8*1037 ఎంట్రీలు, నిల్వ చేయడానికి సుమారు 1027 TB అవసరం). అయినప్పటికీ, "640kB ఎల్లప్పుడూ సరిపోతుంది" (opens in a new tab) అని గుర్తుంచుకునేంత వయస్సు నాకు ఉంది. ఈ పరీక్ష చాలా చౌకైనది.

        // తదుపరి కీని ఉపయోగించి విలువను రాయండి
        val2key[_value] = key2val.length+1;

రివర్స్ లుక్అప్‌ను జోడించండి (విలువ నుండి కీకి).

        key2val.push(_value);

ఫార్వర్డ్ లుక్అప్‌ను జోడించండి (కీ నుండి విలువకు). మనం విలువలను వరుసగా కేటాయిస్తాము కాబట్టి మనం దానిని చివరి శ్రేణి విలువ తర్వాత జోడించవచ్చు.

        return key2val.length;
    }  // cacheWrite

కొత్త విలువ నిల్వ చేయబడిన సెల్ అయిన key2val యొక్క కొత్త పొడవును తిరిగి ఇవ్వండి.

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

ఈ ఫంక్షన్ ఏకపక్ష పొడవు (32 బైట్‌ల వరకు, పద పరిమాణం) ఉన్న కాల్ డేటా నుండి విలువను చదువుతుంది.

    {
        uint _retVal;

        require(length < 0x21,
            "_calldataVal length limit is 32 bytes");
        require(length + startByte <= msg.data.length,
            "_calldataVal trying to read beyond calldatasize");

ఈ ఫంక్షన్ అంతర్గతమైనది, కాబట్టి మిగిలిన కోడ్ సరిగ్గా వ్రాయబడితే ఈ పరీక్షలు అవసరం లేదు. అయినప్పటికీ, వాటికి ఎక్కువ ఖర్చు ఉండదు కాబట్టి మనం వాటిని ఉంచుకోవచ్చు.

        assembly {
            _retVal := calldataload(startByte)
        }

ఈ కోడ్ Yul (opens in a new tab)లో ఉంది. ఇది కాల్ డేటా నుండి 32 బైట్ విలువను చదువుతుంది. EVMలో ప్రారంభించబడని స్థలం సున్నాగా పరిగణించబడుతుంది కాబట్టి కాల్ డేటా startByte+32 కంటే ముందే ఆగిపోయినా ఇది పని చేస్తుంది.

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

మనకు తప్పనిసరిగా 32 బైట్ విలువ అవసరం లేదు. ఇది అదనపు బైట్‌లను వదిలించుకుంటుంది.

        return _retVal;
    } // _calldataVal


    // _fromByte వద్ద ప్రారంభించి, కాల్ డేటా నుండి ఒకే పరామితిని చదవండి
    function _readParam(uint _fromByte) internal
        returns (uint _nextByte, uint _parameterValue)
    {

కాల్ డేటా నుండి ఒకే పారామీటర్‌ను చదవండి. పారామీటర్‌లు 1 బైట్ పొడవు నుండి 33 బైట్‌ల వరకు ఉండవచ్చు కాబట్టి మనం చదివిన విలువను మాత్రమే కాకుండా, తదుపరి బైట్ యొక్క స్థానాన్ని కూడా తిరిగి ఇవ్వాలని గమనించండి.

        // మిగిలిన వాటిని ఎలా అర్థం చేసుకోవాలో మొదటి బైట్ చెబుతుంది
        uint8 _firstByte;

        _firstByte = uint8(_calldataVal(_fromByte, 1));

ప్రమాదకరమైన ఇంప్లిసిట్ టైప్ కన్వర్షన్‌లను (opens in a new tab) నిషేధించడం ద్వారా బగ్‌ల సంఖ్యను తగ్గించడానికి Solidity ప్రయత్నిస్తుంది. డౌన్‌గ్రేడ్, ఉదాహరణకు 256 బిట్‌ల నుండి 8 బిట్‌లకు, స్పష్టంగా ఉండాలి.

దిగువ నిబుల్ (nibble) (opens in a new tab)ని తీసుకుని, కాష్ నుండి విలువను చదవడానికి దానిని ఇతర బైట్‌లతో కలపండి.

కాల్ డేటా నుండే మన వద్ద ఉన్న పారామీటర్‌ల సంఖ్యను మనం పొందవచ్చు, కానీ మనల్ని పిలిచే ఫంక్షన్‌లకు అవి ఎన్ని పారామీటర్‌లను ఆశిస్తున్నాయో తెలుసు. వాటిని మనకు చెప్పనివ్వడం సులభం.

మీకు అవసరమైన సంఖ్య వచ్చే వరకు పారామీటర్‌లను చదవండి. మనం కాల్ డేటా ముగింపును దాటితే, _readParams కాల్‌ను రివర్ట్ చేస్తుంది.

Foundry యొక్క ఒక పెద్ద ప్రయోజనం ఏమిటంటే ఇది పరీక్షలను Solidityలో వ్రాయడానికి అనుమతిస్తుంది (దిగువన కాష్‌ను పరీక్షించడం చూడండి). ఇది యూనిట్ పరీక్షలను చాలా సులభతరం చేస్తుంది. ఇది నాలుగు పారామీటర్‌లను చదివి వాటిని తిరిగి ఇచ్చే ఫంక్షన్, తద్వారా అవి సరైనవో కాదో పరీక్ష ధృవీకరించగలదు.

    // ఒక విలువను పొందండి, దాన్ని ఎన్‌కోడ్ చేసే బైట్‌లను తిరిగి ఇవ్వండి (వీలైతే కాష్‌ని ఉపయోగించి)
    function encodeVal(uint _val) public view returns(bytes memory) {

encodeVal అనేది కాష్‌ను ఉపయోగించే కాల్ డేటాను సృష్టించడంలో సహాయపడటానికి ఆఫ్‌చైన్ కోడ్ కాల్ చేసే ఫంక్షన్. ఇది ఒకే విలువను స్వీకరిస్తుంది మరియు దానిని ఎన్‌కోడ్ చేసే బైట్‌లను తిరిగి ఇస్తుంది. ఈ ఫంక్షన్ ఒక view, కాబట్టి దీనికి లావాదేవీ అవసరం లేదు మరియు బాహ్యంగా కాల్ చేసినప్పుడు ఎటువంటి గ్యాస్ ఖర్చు ఉండదు.

        uint _key = val2key[_val];

        // విలువ ఇంకా కాష్‌లో లేదు, దాన్ని జోడించండి
        if (_key == 0)
            return bytes.concat(INTO_CACHE, bytes32(_val));

EVMలో ప్రారంభించబడని నిల్వ అంతా సున్నాలుగా భావించబడుతుంది. కాబట్టి మనం లేని విలువ కోసం కీని వెతికితే, మనకు సున్నా వస్తుంది. ఆ సందర్భంలో దానిని ఎన్‌కోడ్ చేసే బైట్‌లు INTO_CACHE (కాబట్టి ఇది తదుపరిసారి కాష్ చేయబడుతుంది), ఆపై అసలు విలువ ఉంటుంది.

        // కీ <0x10 అయితే, దాన్ని ఒకే బైట్‌గా తిరిగి ఇవ్వండి
        if (_key < 0x10)
            return bytes.concat(bytes1(uint8(_key)));

సింగిల్ బైట్‌లు అత్యంత సులభమైనవి. bytes<n> రకాన్ని ఏ పొడవుకైనా ఉండగల బైట్ శ్రేణిగా మార్చడానికి మనం కేవలం bytes.concat (opens in a new tab)ని ఉపయోగిస్తాము. పేరు అలా ఉన్నప్పటికీ, కేవలం ఒక ఆర్గ్యుమెంట్‌తో అందించినప్పుడు ఇది బాగా పనిచేస్తుంది.

        // రెండు బైట్ల విలువ, 0x1vvvగా ఎన్‌కోడ్ చేయబడింది
        if (_key < 0x1000)
            return bytes.concat(bytes2(uint16(_key) | 0x1000));

మనకు 163 కంటే తక్కువ కీ ఉన్నప్పుడు, మనం దానిని రెండు బైట్‌లలో వ్యక్తపరచవచ్చు. మనం మొదట 256 బిట్ విలువ అయిన _keyని 16 బిట్ విలువగా మారుస్తాము మరియు మొదటి బైట్‌కు అదనపు బైట్‌ల సంఖ్యను జోడించడానికి లాజికల్ ORని ఉపయోగిస్తాము. ఆపై మనం దానిని bytes2 విలువగా మారుస్తాము, దానిని bytesగా మార్చవచ్చు.

ఇతర విలువలు (3 బైట్‌లు, 4 బైట్‌లు మొదలైనవి) అదే విధంగా నిర్వహించబడతాయి, కేవలం విభిన్న ఫీల్డ్ పరిమాణాలతో.

        // మనం ఇక్కడికి వస్తే, ఏదో తప్పు జరిగినట్లు.
        revert("Error in encodeVal, should not happen");

మనం ఇక్కడికి చేరుకున్నామంటే మనకు 16*25615 కంటే తక్కువ కాని కీ వచ్చిందని అర్థం. కానీ cacheWrite కీలను పరిమితం చేస్తుంది కాబట్టి మనం 14*25616 వరకు కూడా పొందలేము (దీని మొదటి బైట్ 0xFE ఉంటుంది, కాబట్టి ఇది DONT_CACHE లాగా కనిపిస్తుంది). కానీ భవిష్యత్తులో ప్రోగ్రామర్ బగ్‌ను ప్రవేశపెడితే ఒక పరీక్షను జోడించడానికి మనకు పెద్దగా ఖర్చు కాదు.

    } // encodeVal

}  // Cache

కాష్‌ను పరీక్షించడం

Foundry యొక్క ప్రయోజనాల్లో ఒకటి ఏమిటంటే ఇది పరీక్షలను Solidityలో వ్రాయడానికి మిమ్మల్ని అనుమతిస్తుంది (opens in a new tab), ఇది యూనిట్ పరీక్షలను వ్రాయడాన్ని సులభతరం చేస్తుంది. Cache క్లాస్ కోసం పరీక్షలు ఇక్కడ (opens in a new tab) ఉన్నాయి. పరీక్షలు సాధారణంగా పునరావృతమవుతాయి కాబట్టి, ఈ కథనం ఆసక్తికరమైన భాగాలను మాత్రమే వివరిస్తుంది.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";


// కన్సోల్ కోసం `forge test -vv` రన్ చేయాలి.
import "forge-std/console.sol";

ఇది కేవలం పరీక్ష ప్యాకేజీ మరియు console.logని ఉపయోగించడానికి అవసరమైన బాయిలర్‌ప్లేట్.

import "src/Cache.sol";

మనం పరీక్షిస్తున్న కాంట్రాక్ట్ గురించి తెలుసుకోవాలి.

contract CacheTest is Test {
    Cache cache;

    function setUp() public {
        cache = new Cache();
    }

ప్రతి పరీక్షకు ముందు setUp ఫంక్షన్ కాల్ చేయబడుతుంది. ఈ సందర్భంలో మనం కేవలం కొత్త కాష్‌ను సృష్టిస్తాము, తద్వారా మన పరీక్షలు ఒకదానికొకటి ప్రభావితం చేయవు.

    function testCaching() public {

పరీక్షలు అనేవి testతో ప్రారంభమయ్యే పేర్లను కలిగి ఉన్న ఫంక్షన్‌లు. ఈ ఫంక్షన్ ప్రాథమిక కాష్ కార్యాచరణను తనిఖీ చేస్తుంది, విలువలను వ్రాయడం మరియు వాటిని మళ్లీ చదవడం చేస్తుంది.

        for(uint i=1; i<5000; i++) {
            cache.cacheWrite(i*i);
        }

        for(uint i=1; i<5000; i++) {
            assertEq(cache.cacheRead(i), i*i);

assert... ఫంక్షన్‌లను (opens in a new tab) ఉపయోగించి మీరు అసలు పరీక్షను ఈ విధంగా చేస్తారు. ఈ సందర్భంలో, మనం వ్రాసిన విలువ మనం చదివినదేనా అని తనిఖీ చేస్తాము. కాష్ కీలు సరళంగా కేటాయించబడతాయని మనకు తెలుసు కాబట్టి మనం cache.cacheWrite ఫలితాన్ని విస్మరించవచ్చు.

మొదట మనం ప్రతి విలువను కాష్‌కు రెండుసార్లు వ్రాస్తాము మరియు కీలు ఒకేలా ఉన్నాయని నిర్ధారించుకుంటాము (అంటే రెండవ వ్రాత నిజంగా జరగలేదు).

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

సిద్ధాంతపరంగా వరుస కాష్ వ్రాతలను ప్రభావితం చేయని బగ్ ఉండవచ్చు. కాబట్టి ఇక్కడ మనం వరుసగా లేని కొన్ని వ్రాతలను చేస్తాము మరియు విలువలు ఇప్పటికీ తిరిగి వ్రాయబడలేదని చూస్తాము.

    // మెమరీ బఫర్ నుండి uintని చదవండి (మనం పంపిన పారామితులను తిరిగి పొందుతామని నిర్ధారించుకోవడానికి
    // మనం పంపినవి)
    function toUint256(bytes memory _bytes, uint256 _start) internal pure
        returns (uint256)

bytes memory బఫర్ నుండి 256 బిట్ పదాన్ని చదవండి. కాష్‌ను ఉపయోగించే ఫంక్షన్ కాల్‌ను రన్ చేసినప్పుడు మనం సరైన ఫలితాలను అందుకుంటామని ధృవీకరించడానికి ఈ యుటిలిటీ ఫంక్షన్ అనుమతిస్తుంది.

    {
        require(_bytes.length >= _start + 32, "toUint256_outOfBounds");
        uint256 tempUint;

        assembly {
            tempUint := mload(add(add(_bytes, 0x20), _start))
        }

Yul uint256కి మించిన డేటా నిర్మాణాలకు మద్దతు ఇవ్వదు, కాబట్టి మీరు మెమరీ బఫర్ _bytes వంటి మరింత అధునాతన డేటా నిర్మాణాన్ని సూచించినప్పుడు, మీకు ఆ నిర్మాణం యొక్క చిరునామా వస్తుంది. Solidity bytes memory విలువలను పొడవును కలిగి ఉన్న 32 బైట్ పదంగా నిల్వ చేస్తుంది, ఆపై అసలు బైట్‌లు ఉంటాయి, కాబట్టి బైట్ సంఖ్య _startని పొందడానికి మనం _bytes+32+_startని లెక్కించాలి.

పరీక్ష కోసం మనకు అవసరమైన కొన్ని స్థిరాంకాలు.

    function testReadParam() public {

మనం పారామీటర్‌లను సరిగ్గా చదవగలమో లేదో పరీక్షించడానికి, readParamsని ఉపయోగించే ఫంక్షన్ అయిన fourParams()ని కాల్ చేయండి.

        address _cacheAddr = address(cache);
        bool _success;
        bytes memory _callInput;
        bytes memory _callOutput;

కాష్‌ని ఉపయోగించి ఫంక్షన్‌ను కాల్ చేయడానికి మనం సాధారణ ABI యంత్రాంగాన్ని ఉపయోగించలేము, కాబట్టి మనం తక్కువ స్థాయి <address>.call() (opens in a new tab) యంత్రాంగాన్ని ఉపయోగించాలి. ఆ యంత్రాంగం bytes memoryని ఇన్‌పుట్‌గా తీసుకుంటుంది మరియు దానిని (అలాగే బూలియన్ విలువను) అవుట్‌పుట్‌గా తిరిగి ఇస్తుంది.

        // మొదటి కాల్, కాష్ ఖాళీగా ఉంది
        _callInput = bytes.concat(
            FOUR_PARAMS,

ఒకే కాంట్రాక్ట్ కాష్ చేయబడిన ఫంక్షన్‌లకు (లావాదేవీల నుండి నేరుగా కాల్‌ల కోసం) మరియు కాష్ చేయబడని ఫంక్షన్‌లకు (ఇతర స్మార్ట్ కాంట్రాక్ట్‌ల నుండి కాల్‌ల కోసం) మద్దతు ఇవ్వడం ఉపయోగకరంగా ఉంటుంది. అలా చేయడానికి మనం ప్రతిదీ ఒక fallback ఫంక్షన్‌లో (opens in a new tab) ఉంచడానికి బదులుగా, సరైన ఫంక్షన్‌ను కాల్ చేయడానికి Solidity యంత్రాంగంపై ఆధారపడటం కొనసాగించాలి. ఇలా చేయడం వల్ల కూర్పు సామర్థ్యం చాలా సులభం అవుతుంది. చాలా సందర్భాలలో ఫంక్షన్‌ను గుర్తించడానికి ఒకే బైట్ సరిపోతుంది, కాబట్టి మనం మూడు బైట్‌లను (16*3=48 గ్యాస్) వృధా చేస్తున్నాము. అయినప్పటికీ, నేను దీనిని వ్రాస్తున్నప్పుడు ఆ 48 గ్యాస్ ధర 0.07 సెంట్లు, ఇది సరళమైన, తక్కువ బగ్‌లకు గురయ్యే కోడ్ యొక్క సహేతుకమైన ఖర్చు.

            // మొదటి విలువ, దాన్ని కాష్‌కు జోడించండి
            cache.INTO_CACHE(),
            bytes32(VAL_A),

మొదటి విలువ: ఇది కాష్‌కు వ్రాయవలసిన పూర్తి విలువ అని చెప్పే ఫ్లాగ్, ఆపై విలువ యొక్క 32 బైట్‌లు ఉంటాయి. VAL_B కాష్‌కు వ్రాయబడదు మరియు VAL_C మూడవ పారామీటర్ మరియు నాల్గవది రెండూ కావడం మినహా మిగిలిన మూడు విలువలు ఒకేలా ఉంటాయి.

             .
             .
             .
        );
        (_success, _callOutput) = _cacheAddr.call(_callInput);

ఇక్కడే మనం వాస్తవానికి Cache కాంట్రాక్ట్‌ను కాల్ చేస్తాము.

        assertEq(_success, true);

కాల్ విజయవంతమవుతుందని మనం ఆశిస్తున్నాము.

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

మనం ఖాళీ కాష్‌తో ప్రారంభించి, ఆపై VAL_Aని మరియు దాని తర్వాత VAL_Cని జోడిస్తాము. మొదటి దానికి కీ 1, మరియు రెండవ దానికి 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);

అవుట్‌పుట్ నాలుగు పారామీటర్‌లు. ఇక్కడ మనం అది సరైనదో కాదో ధృవీకరిస్తాము.

        // రెండవ కాల్, మనం కాష్‌ని ఉపయోగించవచ్చు
        _callInput = bytes.concat(
            FOUR_PARAMS,

            // కాష్‌లోని మొదటి విలువ
            bytes1(0x01),

16 కంటే తక్కువ ఉన్న కాష్ కీలు కేవలం ఒక బైట్ మాత్రమే.

కాల్ తర్వాత పరీక్షలు మొదటి కాల్ తర్వాత ఉన్న వాటితో సమానంగా ఉంటాయి.

    function testEncodeVal() public {

ఈ ఫంక్షన్ testReadParamని పోలి ఉంటుంది, అయితే పారామీటర్‌లను స్పష్టంగా వ్రాయడానికి బదులుగా మనం encodeVal()ని ఉపయోగిస్తాము.

testEncodeVal()లో ఉన్న ఏకైక అదనపు పరీక్ష ఏమిటంటే _callInput పొడవు సరైనదో కాదో ధృవీకరించడం. మొదటి కాల్ కోసం ఇది 4+33*4. ప్రతి విలువ ఇప్పటికే కాష్‌లో ఉన్న రెండవ దానికి, ఇది 4+1*4.

పై testEncodeVal ఫంక్షన్ కాష్‌లోకి నాలుగు విలువలను మాత్రమే వ్రాస్తుంది, కాబట్టి బహుళ-బైట్ విలువలతో వ్యవహరించే ఫంక్షన్ భాగం (opens in a new tab) తనిఖీ చేయబడదు. కానీ ఆ కోడ్ సంక్లిష్టమైనది మరియు లోపాలకు గురయ్యే అవకాశం ఉంది.

ఈ ఫంక్షన్ యొక్క మొదటి భాగం 1 నుండి 0x1FFF వరకు ఉన్న అన్ని విలువలను క్రమంలో కాష్‌కు వ్రాసే లూప్, కాబట్టి మనం ఆ విలువలను ఎన్‌కోడ్ చేయగలుగుతాము మరియు అవి ఎక్కడికి వెళ్తున్నాయో తెలుసుకోగలుగుతాము.

ఒక బైట్, రెండు బైట్ మరియు మూడు బైట్ విలువలను పరీక్షించండి. తగినన్ని స్టాక్ ఎంట్రీలను వ్రాయడానికి చాలా సమయం పడుతుంది కాబట్టి (కనీసం 0x10000000, సుమారు పావు బిలియన్) మనం అంతకు మించి పరీక్షించము.

తగినన్ని పారామీటర్‌లు లేని అసాధారణ సందర్భంలో ఏమి జరుగుతుందో పరీక్షించండి.

        .
        .
        .
        (_success, _callOutput) = _cacheAddr.call(_callInput);
        assertEq(_success, false);
    }   // testShortCalldata

ఇది రివర్ట్ అవుతుంది కాబట్టి, మనకు రావలసిన ఫలితం false.

ఈ ఫంక్షన్ నాలుగు ఖచ్చితమైన చట్టబద్ధమైన పారామీటర్‌లను పొందుతుంది, అయితే కాష్ ఖాళీగా ఉంది కాబట్టి అక్కడ చదవడానికి విలువలు లేవు.

ఈ ఫంక్షన్ ఐదు విలువలను పంపుతుంది. ఐదవ విలువ విస్మరించబడిందని మనకు తెలుసు ఎందుకంటే ఇది చెల్లుబాటు అయ్యే కాష్ ఎంట్రీ కాదు, ఒకవేళ అది చేర్చబడకపోతే రివర్ట్‌కు కారణమయ్యేది.

ఒక నమూనా అప్లికేషన్

Solidityలో పరీక్షలు వ్రాయడం చాలా మంచిది, కానీ అంతిమంగా ఒక వికేంద్రీకృత అప్లికేషన్ (dapp) ఉపయోగకరంగా ఉండాలంటే చైన్ వెలుపల నుండి వచ్చే అభ్యర్థనలను ప్రాసెస్ చేయగలగాలి. ఈ కథనం "రైట్ వన్స్, రీడ్ మెనీ" (Write Once, Read Many) అని అర్థం వచ్చే WORMతో డాప్‌లో కాషింగ్‌ను ఎలా ఉపయోగించాలో ప్రదర్శిస్తుంది. కీ ఇంకా వ్రాయబడకపోతే, మీరు దానికి విలువను వ్రాయవచ్చు. కీ ఇప్పటికే వ్రాయబడి ఉంటే, మీకు రివర్ట్ వస్తుంది.

కాంట్రాక్ట్

ఇది కాంట్రాక్ట్ (opens in a new tab). ఇది ఎక్కువగా మనం ఇప్పటికే Cache మరియు CacheTestతో చేసిన వాటినే పునరావృతం చేస్తుంది, కాబట్టి మనం ఆసక్తికరంగా ఉన్న భాగాలను మాత్రమే కవర్ చేస్తాము.

import "./Cache.sol";

contract WORM is Cache {

Cacheని ఉపయోగించడానికి సులభమైన మార్గం దానిని మన స్వంత కాంట్రాక్ట్‌లో వారసత్వంగా పొందడం.

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

ఈ ఫంక్షన్ పైన ఉన్న CacheTestలోని fourParamని పోలి ఉంటుంది. మనం ABI స్పెసిఫికేషన్‌లను అనుసరించము కాబట్టి, ఫంక్షన్‌లోకి ఎటువంటి పారామీటర్‌లను ప్రకటించకపోవడం ఉత్తమం.

    // మమ్మల్ని కాల్ చేయడం సులభతరం చేయండి
    // writeEntryCached() కోసం ఫంక్షన్ సంతకం, సౌజన్యంతో
    // https://www.4byte.directory/signatures/?bytes4_signature=0xe4e4f2d3
    bytes4 constant public WRITE_ENTRY_CACHED = 0xe4e4f2d3;

writeEntryCachedని కాల్ చేసే బాహ్య కోడ్ worm.writeEntryCachedని ఉపయోగించడానికి బదులుగా కాల్ డేటాను మాన్యువల్‌గా నిర్మించవలసి ఉంటుంది, ఎందుకంటే మనం ABI స్పెసిఫికేషన్‌లను అనుసరించము. ఈ స్థిరమైన విలువను కలిగి ఉండటం వలన దానిని వ్రాయడం సులభం అవుతుంది.

మనం WRITE_ENTRY_CACHEDని స్టేట్ వేరియబుల్‌గా నిర్వచించినప్పటికీ, దానిని బాహ్యంగా చదవడానికి దాని కోసం గెట్టర్ ఫంక్షన్ worm.WRITE_ENTRY_CACHED()ని ఉపయోగించడం అవసరమని గమనించండి.

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

రీడ్ ఫంక్షన్ ఒక view, కాబట్టి దీనికి లావాదేవీ అవసరం లేదు మరియు గ్యాస్ ఖర్చు ఉండదు. ఫలితంగా, పారామీటర్ కోసం కాష్‌ను ఉపయోగించడం వల్ల ఎటువంటి ప్రయోజనం లేదు. వ్యూ ఫంక్షన్‌లతో సరళమైన ప్రామాణిక యంత్రాంగాన్ని ఉపయోగించడం ఉత్తమం.

టెస్టింగ్ కోడ్

ఇది కాంట్రాక్ట్ కోసం టెస్టింగ్ కోడ్ (opens in a new tab). మళ్ళీ, ఆసక్తికరంగా ఉన్న వాటిని మాత్రమే చూద్దాం.

    function testWReadWrite() public {
        worm.writeEntry(0xDEAD, 0x60A7);

        vm.expectRevert(bytes("entry already written"));
        worm.writeEntry(0xDEAD, 0xBEEF);

తదుపరి కాల్ విఫలం కావాలని మరియు వైఫల్యానికి నివేదించబడిన కారణాన్ని Foundry పరీక్షలో మనం ఈ విధంగా (vm.expectRevert) (opens in a new tab) పేర్కొంటాము. కాల్ డేటాను నిర్మించడం మరియు తక్కువ స్థాయి ఇంటర్‌ఫేస్ (<contract>.call() మొదలైనవి) ఉపయోగించి కాంట్రాక్ట్‌ను కాల్ చేయడానికి బదులుగా మనం <contract>.<function name>() సింటాక్స్‌ను ఉపయోగించినప్పుడు ఇది వర్తిస్తుంది.

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

ఇక్కడ మనం cacheWrite కాష్ కీని తిరిగి ఇస్తుంది అనే వాస్తవాన్ని ఉపయోగిస్తాము. ఇది మనం ప్రొడక్షన్‌లో ఉపయోగిస్తామని ఆశించేది కాదు, ఎందుకంటే cacheWrite స్థితిని మారుస్తుంది, కాబట్టి లావాదేవీ సమయంలో మాత్రమే కాల్ చేయబడుతుంది. లావాదేవీలకు రిటర్న్ విలువలు ఉండవు, వాటికి ఫలితాలు ఉంటే ఆ ఫలితాలు ఈవెంట్‌లుగా విడుదల చేయబడతాయి. కాబట్టి cacheWrite రిటర్న్ విలువ ఆన్‌చైన్ కోడ్ నుండి మాత్రమే యాక్సెస్ చేయబడుతుంది మరియు ఆన్‌చైన్ కోడ్‌కు పారామీటర్ కాషింగ్ అవసరం లేదు.

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

<contract address>.call()కి రెండు రిటర్న్ విలువలు ఉన్నప్పటికీ, మనం మొదటి దాని గురించి మాత్రమే పట్టించుకుంటామని Solidityకి ఈ విధంగా చెబుతాము.

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

మనం తక్కువ స్థాయి <address>.call() ఫంక్షన్‌ను ఉపయోగిస్తాము కాబట్టి, మనం vm.expectRevert()ని ఉపయోగించలేము మరియు కాల్ నుండి మనకు లభించే బూలియన్ సక్సెస్ విలువను చూడాలి.

Foundryలో కోడ్ ఈవెంట్‌ను సరిగ్గా విడుదల చేస్తుందని (opens in a new tab) మనం ధృవీకరించే విధానం ఇది.

క్లయింట్

Solidity పరీక్షలతో మీకు లభించని ఒక విషయం ఏమిటంటే, మీరు మీ స్వంత అప్లికేషన్‌లో కట్ చేసి పేస్ట్ చేయగల JavaScript కోడ్. ఈ ట్యుటోరియల్ యొక్క అసలు వెర్షన్ WORMని Optimism Goerliకి డిప్లాయ్ చేసింది, అది అప్పటి నుండి విరమించబడింది. ఈ రోజు క్లయింట్‌ను రన్ చేయడానికి, OP Sepolia (opens in a new tab) వంటి మద్దతు ఉన్న OP స్టాక్ నెట్‌వర్క్‌కు WORMని మళ్లీ డిప్లాయ్ చేయండి, ఆపై ఫలితంగా వచ్చిన కాంట్రాక్ట్ చిరునామాను JavaScript క్లయింట్‌లో ఉపయోగించండి.

మీరు క్లయింట్ కోసం JavaScript కోడ్‌ను ఇక్కడ చూడవచ్చు (opens in a new tab). నమూనా రిపోజిటరీ Optimism Goerli కోసం వ్రాయబడింది, కాబట్టి దానిని రన్ చేయడానికి ముందు, మీ లక్ష్య నెట్‌వర్క్ కోసం javascript/.env.example మరియు javascript/index.jsలో RPC ఎండ్‌పాయింట్ మరియు ఎక్స్‌ప్లోరర్ URLలను అప్‌డేట్ చేయండి. దీన్ని ఉపయోగించడానికి:

  1. git రిపోజిటరీని క్లోన్ చేయండి:

    git clone https://github.com/qbzzt/20220915-all-you-can-cache.git
    
  2. అవసరమైన ప్యాకేజీలను ఇన్‌స్టాల్ చేయండి:

    cd javascript
    yarn
    
  3. కాన్ఫిగరేషన్ ఫైల్‌ను కాపీ చేయండి:

    cp .env.example .env
    
  4. మీ కాన్ఫిగరేషన్ కోసం .envని సవరించండి:

    పారామీటర్విలువ
    MNEMONICలావాదేవీకి చెల్లించడానికి తగినంత ETH ఉన్న ఖాతా కోసం నిమోనిక్ (mnemonic). Optimism యొక్క ఫాసెట్ డాక్స్ (opens in a new tab) ప్రస్తుత టెస్ట్‌నెట్ ఫాసెట్‌లను జాబితా చేస్తాయి.
    OPTIMISM_GOERLI_URLమీరు WORMని మళ్లీ డిప్లాయ్ చేసే నెట్‌వర్క్ కోసం RPC URL. OP Sepolia కోసం, https://sepolia.optimism.io వంటి OP Sepolia RPC ఎండ్‌పాయింట్‌ను లేదా మీ ప్రొవైడర్ నుండి మరొక ఎండ్‌పాయింట్‌ను ఉపయోగించండి.
  5. index.jsని రన్ చేయండి.

    node index.js
    

    ఈ నమూనా అప్లికేషన్ మొదట WORMకి ఒక ఎంట్రీని వ్రాస్తుంది, కాల్ డేటాను మరియు బ్లాక్ ఎక్స్‌ప్లోరర్‌లో లావాదేవీకి లింక్‌ను ప్రదర్శిస్తుంది. ఆపై అది ఆ ఎంట్రీని తిరిగి చదువుతుంది మరియు అది ఉపయోగించే కీని మరియు ఎంట్రీలోని విలువలను (విలువ, బ్లాక్ నంబర్ మరియు రచయిత) ప్రదర్శిస్తుంది.

క్లయింట్‌లో ఎక్కువ భాగం సాధారణ Dapp JavaScript. కాబట్టి మళ్ళీ మనం ఆసక్తికరమైన భాగాలను మాత్రమే పరిశీలిస్తాము.

.
.
.
const main = async () => {
    const func = await worm.WRITE_ENTRY_CACHED()

    // ప్రతిసారీ కొత్త కీ అవసరం
    const key = await worm.encodeVal(Number(new Date()))

ఒక నిర్దిష్ట స్లాట్‌లోకి ఒకసారి మాత్రమే వ్రాయవచ్చు, కాబట్టి మనం స్లాట్‌లను మళ్లీ ఉపయోగించకుండా చూసుకోవడానికి టైమ్‌స్టాంప్‌ను ఉపయోగిస్తాము.

const val = await worm.encodeVal("0x600D")

// ఒక ఎంట్రీని వ్రాయండి
const calldata = func + key.slice(2) + val.slice(2)

Ethers కాల్ డేటా హెక్స్ స్ట్రింగ్‌గా ఉండాలని ఆశిస్తుంది, 0x తర్వాత సరి సంఖ్యలో హెక్సాడెసిమల్ అంకెలు ఉంటాయి. key మరియు val రెండూ 0xతో ప్రారంభమవుతాయి కాబట్టి, మనం ఆ హెడర్‌లను తీసివేయాలి.

const tx = await worm.populateTransaction.writeEntryCached()
tx.data = calldata

sentTx = await wallet.sendTransaction(tx)

Solidity టెస్టింగ్ కోడ్ మాదిరిగానే, మనం కాష్ చేయబడిన ఫంక్షన్‌ను సాధారణంగా కాల్ చేయలేము. బదులుగా, మనం తక్కువ స్థాయి యంత్రాంగాన్ని ఉపయోగించాలి.

ఎంట్రీలను చదవడానికి మనం సాధారణ యంత్రాంగాన్ని ఉపయోగించవచ్చు. view ఫంక్షన్‌లతో పారామీటర్ కాషింగ్‌ను ఉపయోగించాల్సిన అవసరం లేదు.

ముగింపు

ఈ కథనంలోని కోడ్ ఒక ప్రూఫ్ ఆఫ్ కాన్సెప్ట్, ఈ ఆలోచనను సులభంగా అర్థం చేసుకునేలా చేయడమే దీని ఉద్దేశ్యం. ప్రొడక్షన్-రెడీ సిస్టమ్ కోసం మీరు కొన్ని అదనపు కార్యాచరణలను అమలు చేయాలనుకోవచ్చు:

  • uint256 కాని విలువలను నిర్వహించండి. ఉదాహరణకు, స్ట్రింగ్‌లు.

  • గ్లోబల్ కాష్‌కు బదులుగా, బహుశా వినియోగదారులు మరియు కాష్‌ల మధ్య మ్యాపింగ్‌ను కలిగి ఉండవచ్చు. వేర్వేరు వినియోగదారులు వేర్వేరు విలువలను ఉపయోగిస్తారు.

  • చిరునామాల కోసం ఉపయోగించే విలువలు ఇతర ప్రయోజనాల కోసం ఉపయోగించే వాటికి భిన్నంగా ఉంటాయి. కేవలం చిరునామాల కోసం ప్రత్యేక కాష్‌ను కలిగి ఉండటం సమంజసం కావచ్చు.

  • ప్రస్తుతం, కాష్ కీలు "ముందు వచ్చిన వారికి, చిన్న కీ" (first come, smallest key) అల్గారిథమ్‌పై ఉన్నాయి. మొదటి పదహారు విలువలను ఒకే బైట్‌గా పంపవచ్చు. తదుపరి 4080 విలువలను రెండు బైట్‌లుగా పంపవచ్చు. తదుపరి సుమారు మిలియన్ విలువలు మూడు బైట్‌లు మొదలైనవి. ప్రొడక్షన్ సిస్టమ్ కాష్ ఎంట్రీలపై వినియోగ కౌంటర్‌లను ఉంచాలి మరియు వాటిని పునర్వ్యవస్థీకరించాలి, తద్వారా పదహారు అత్యంత సాధారణ విలువలు ఒక బైట్, తదుపరి 4080 అత్యంత సాధారణ విలువలు రెండు బైట్‌లు మొదలైనవి ఉంటాయి.

    అయితే, అది ప్రమాదకరమైన ఆపరేషన్ కావచ్చు. కింది సంఘటనల క్రమాన్ని ఊహించుకోండి:

    1. నోమ్ నైవ్ (Noam Naive) తాను టోకెన్‌లను పంపాలనుకుంటున్న చిరునామాను ఎన్‌కోడ్ చేయడానికి encodeValని కాల్ చేస్తాడు. ఆ చిరునామా అప్లికేషన్‌లో మొదట ఉపయోగించిన వాటిలో ఒకటి, కాబట్టి ఎన్‌కోడ్ చేయబడిన విలువ 0x06. ఇది ఒక view ఫంక్షన్, లావాదేవీ కాదు, కాబట్టి ఇది నోమ్ మరియు అతను ఉపయోగించే నోడ్ మధ్య ఉంటుంది మరియు దీని గురించి మరెవరికీ తెలియదు

    2. ఓవెన్ ఓనర్ (Owen Owner) కాష్ రీఆర్డరింగ్ ఆపరేషన్‌ను రన్ చేస్తాడు. వాస్తవానికి చాలా తక్కువ మంది ఆ చిరునామాను ఉపయోగిస్తారు, కాబట్టి ఇది ఇప్పుడు 0x201122గా ఎన్‌కోడ్ చేయబడింది. వేరొక విలువ, 1018కి 0x06 కేటాయించబడింది.

    3. నోమ్ నైవ్ తన టోకెన్‌లను 0x06కి పంపుతాడు. అవి 0x0000000000000000000000000de0b6b3a7640000 చిరునామాకు వెళ్తాయి మరియు ఆ చిరునామాకు సంబంధించిన ప్రైవేట్ కీ ఎవరికీ తెలియదు కాబట్టి, అవి అక్కడే ఇరుక్కుపోతాయి. నోమ్ సంతోషంగా లేడు.

    ఈ సమస్యను మరియు కాష్ రీఆర్డర్ సమయంలో మెంపూల్‌లో ఉన్న లావాదేవీల సంబంధిత సమస్యను పరిష్కరించడానికి మార్గాలు ఉన్నాయి, కానీ మీరు దాని గురించి తెలుసుకోవాలి.

నేను ఇక్కడ Optimismతో కాషింగ్‌ను ప్రదర్శించాను, ఎందుకంటే నేను Optimism ఉద్యోగిని మరియు నాకు బాగా తెలిసిన రోలప్ ఇదే. కానీ అంతర్గత ప్రాసెసింగ్ కోసం కనీస ఖర్చును వసూలు చేసే ఏ రోలప్‌తోనైనా ఇది పని చేయాలి, తద్వారా పోల్చి చూస్తే లావాదేవీ డేటాను L1కి వ్రాయడం ప్రధాన ఖర్చు అవుతుంది.

నా మరిన్ని పనుల కోసం ఇక్కడ చూడండి (opens in a new tab).