Autor: Certik
Poplatky za Gas na hlavní síti Ethereum byly vždy palčivým problémem, zejména během přetížení sítě. Během špiček musí uživatelé často platit velmi vysoké transakční poplatky. Proto je optimalizace poplatků za Gas během fáze vývoje smart kontraktů zvláště důležitá. Optimalizace spotřeby Gas nejenže účinně snižuje náklady na transakce, ale také zvyšuje efektivitu transakcí a přináší uživatelům ekonomičtější a efektivnější zkušenost s blockchainem.
Tento článek shrnuje mechanismus poplatků za Gas v Ethereum virtuální mašině (EVM), základní koncepty optimalizace poplatků za Gas a nejlepší praktiky pro optimalizaci poplatků za Gas při vývoji smart kontraktů. Doufáme, že tyto informace poskytnou vývojářům inspiraci a praktickou pomoc, a také pomohou běžným uživatelům lépe porozumět způsobu fungování poplatků za Gas v EVM, aby společně čelili výzvám v blockchainovém ekosystému.
Úvod do mechanismu poplatků za Gas v EVM
V sítích kompatibilních s EVM označuje "Gas" jednotku používanou k měření výpočetní síly potřebné k provedení konkrétní operace.
Níže je znázorněno rozložení struktury EVM. Spotřeba Gas je rozdělena do tří částí: provádění operací, volání externích zpráv a čtení/zápis do paměti a úložiště.
Zdroj: Oficiální stránky Ethereum [1]
Protože každá transakce vyžaduje výpočetní zdroje, je účtován poplatek, aby se zabránilo nekonečným smyčkám a útokům typu DoS. Náklady spojené s dokončením transakce se nazývají "poplatek za Gas".
Od účinnosti EIP-1559 (londýnský hard fork) se poplatky za Gas počítají podle následujícího vzorce:
Poplatek za Gas = jednotky použitého gasu * (základní poplatek + priorita poplatku)
Základní poplatek bude zničen, priorita poplatku slouží jako pobídka, aby ověřovatelé přidali transakce do blockchainu. Nastavením vyšší prioritní ceny při odesílání transakcí lze zvýšit pravděpodobnost zahrnutí transakce do dalšího bloku. To je podobné "dýšku", které uživatelé platí ověřovatelům.
1. Pochopte optimalizaci Gas v EVM
Při kompilaci smart kontraktů pomocí Solidity se kontrakty převádějí na řadu "operací", tedy opkódů.
Každý blok opkódů (například vytvoření kontraktu, provedení zpráv, přístup k účtovému úložišti a provádění operací na virtuální mašině) má uznávané náklady na spotřebu Gas, které jsou zaznamenány v žluté knize Etherea [2].
Po mnoha úpravách EIP byl upraven náklad na Gas pro některé operace, což se může lišit od hodnot uvedených v žluté knize. Pro podrobnosti o aktuálních nákladech na opkódy se podívejte sem [3].
2. Základní koncepty optimalizace Gas
Hlavním principem optimalizace Gas je upřednostnit operace s nízkými náklady na EVM blockchainu a vyhnout se operacím s vysokými náklady na Gas.
V EVM jsou níže uvedené operace s nízkými náklady:
Čtení a zápis paměťových proměnných
Čtení konstant a neměnných proměnných
Čtení a zápis lokálních proměnných
Čtení proměnných calldata, jako jsou pole a struktury calldata
Volání interních funkcí
Nákladné operace zahrnují:
Čtení a zápis stavových proměnných uložených ve smlouvě
Volání externích funkcí
Cyklické operace
Nejlepší praktiky pro optimalizaci nákladů na Gas v EVM
Na základě výše uvedených základních konceptů jsme pro komunitu vývojářů sestavili seznam nejlepších praktik pro optimalizaci poplatků za Gas. Dodržováním těchto praktik mohou vývojáři snížit spotřebu Gas smart kontraktů a snížit náklady na transakce, čímž vytvoří efektivnější a uživatelsky přívětivější aplikace.
1. Minimalizujte používání úložišť
Ve Solidity je Storage (úložiště) omezený zdroj, jehož spotřeba Gas je mnohem vyšší než u Memory (paměti). Každé čtení nebo zápis dat z úložiště smart kontraktu generuje vysoké náklady na Gas.
Podle definice v žluté knize Etherea jsou náklady na operace se souvisejícími daty více než 100krát vyšší než náklady na operace s pamětí. Například instrukce OPcodes mload a mstore spotřebovávají pouze 3 jednotky Gas, zatímco operace s úložištěm, jako je sload a sstore, vyžadují minimálně 100 jednotek, i za ideálních podmínek.
Metody pro omezení použití úložiště zahrnují:
Uložení dočasných dat v paměti
Snížení počtu úprav úložiště: uchovávejte mezivýsledky v paměti a po dokončení všech výpočtů přiřaďte výsledky proměnným úložiště.
2. Balení proměnných
Počet uložených slotů (storage slot) používaných ve smart kontraktech a způsob, jakým vývojáři reprezentují data, výrazně ovlivňuje spotřebu Gas.
Kompilátor Solidity balí při kompilaci sousední proměnné do slotů a jako základní jednotku pro ukládání proměnných používá 32 bajtů. Balení proměnných znamená rozumné uspořádání proměnných tak, aby se více proměnných vešlo do jednoho úložného slotu.
Na levé straně je méně efektivní implementace, která spotřebovává 3 úložné sloty; na pravé straně je efektivnější implementace.
Tímto drobným vylepšením mohou vývojáři ušetřit 20,000 jednotek Gas (uložení nepoužívaného úložného slotu vyžaduje 20,000 Gas), ale nyní potřebují pouze dva úložné sloty.
Protože každý úložný slot spotřebovává Gas, balení proměnných optimalizuje spotřebu Gas tím, že snižuje počet potřebných úložných slotů.
3. Optimalizace datových typů
Proměnná může být reprezentována více než jedním datovým typem, ale různé datové typy mají různé náklady na operace. Výběr správného datového typu pomáhá optimalizovat používání Gas.
Například v Solidity lze celočíselné hodnoty rozdělit na různé velikosti: uint8, uint16, uint32 atd. Protože EVM provádí operace po 256 bitech, použití uint8 znamená, že EVM musí nejprve převést na uint256, což přidává dodatečné náklady na Gas.
Můžeme porovnat náklady na Gas pro uint8 a uint256 pomocí kódu uvedeného na obrázku. Funkce UseUint() spotřebovává 120,382 jednotek Gas, zatímco funkce UseUInt8() spotřebovává 166,111 jednotek Gas.
Sám o sobě je použití uint256 levnější než uint8. Pokud však použijeme dříve doporučenou optimalizaci balení proměnných, je to jiná situace. Pokud mohou vývojáři zabalit čtyři uint8 proměnné do jednoho úložného slotu, celkové náklady na iteraci nad nimi budou nižší než náklady na čtyři uint256 proměnné. Takto smart kontrakt může číst a zapisovat do jednoho slotu a v jednom kroku vložit čtyři uint8 proměnné do paměti/úložiště.
4. Použití proměnných pevné velikosti namísto dynamických proměnných
Pokud lze data udržet do 32 bajtů, doporučuje se použít datový typ bytes32 místo bytes nebo strings. Obecně platí, že proměnné pevné velikosti spotřebují méně Gas než proměnné proměnné. Pokud lze omezit délku bajtů, snažte se vybírat minimální délku od bytes1 do bytes32.
5. Mapy vs. pole
Seznamy dat v Solidity mohou být reprezentovány dvěma datovými typy: pole (Arrays) a mapy (Mappings), ale jejich syntaxe a struktura jsou zcela odlišné.
Mapy jsou většinou efektivnější a levnější, zatímco pole mají iterovatelnost a podporují balení datových typů. Proto se doporučuje upřednostnit mapy při správě seznamů dat, pokud není potřeba iterovat nebo optimalizovat spotřebu Gas pomocí balení datových typů.
6. Použití calldata místo memory
Proměnné deklarované v parametrech funkcí mohou být uloženy v calldata nebo paměti. Hlavní rozdíl mezi nimi spočívá v tom, že paměť lze modifikovat funkcí, zatímco calldata je neměnná.
Pamatujte na tento princip: pokud jsou parametry funkce pouze pro čtení, měli byste upřednostnit použití calldata před pamětí. Tím se vyhnete zbytečným kopírovacím operacím z calldata funkce do paměti.
Příklad 1: Použití memory
Při používání klíčového slova memory se hodnoty pole kopírují z kódovaného calldata do paměti během dekódování ABI. Náklady na provádění tohoto blokového kódu činí 3,694 jednotek Gas.
Příklad 2: Použití calldata
Při přímém čtení hodnot z calldata se vynechá prostřední operace paměti. Tento způsob optimalizace snižuje náklady na provádění na pouhých 2,413 jednotek Gas, což zvyšuje efektivitu Gas o 35%.
7. Používejte klíčová slova Constant/Immutable, jak jen to jde
Pevné/neproměnné proměnné nejsou ukládány v úložišti smlouvy. Tyto proměnné jsou vypočítávány při kompilaci a uloženy v bytecode kontraktu. Proto jsou náklady na přístup k nim mnohem nižší než u úložišť, a doporučuje se používat klíčová slova Constant nebo Immutable, pokud je to možné.
8. Používejte unchecked, když je zajištěno, že nedojde k přetečení/podtečení
Když vývojáři mohou potvrdit, že aritmetické operace nezpůsobí přetečení nebo podtečení, mohou použít klíčové slovo unchecked, které bylo zavedené v Solidity v0.8.0, aby se vyhnuli zbytečným kontrolám přetečení nebo podtečení, čímž ušetří náklady na Gas.
Na níže uvedeném obrázku, s podmínkovým omezením i<length, proměnná i nikdy nemůže přetečit. Zde je length definováno jako uint256, což znamená, že maximální hodnota i je max(uint)-1. Proto je zvýšení i v bloku bez kontroly považováno za bezpečné a ještě šetří Gas.
Dále, kompilátory verze 0.8.0 a vyšší již nevyžadují použití knihovny SafeMath, protože kompilátor sám o sobě obsahuje ochranné funkce proti přetečení a podtečení.
9. Optimalizace modifierů
Kód modifikátorů je vložen do funkcí, které byly modifikovány, a pokaždé, když je modifikátor použit, je jeho kód zkopírován. To zvyšuje velikost bytecode a zvyšuje spotřebu Gas. Následuje způsob, jak optimalizovat náklady na Gas modifikátorů:
Optimalizováno před:
Optimalizováno:
V tomto případě, přepracováním logiky na interní funkci _checkOwner(), která umožňuje opakované použití této interní funkce v modifikátoru, je možné snížit velikost bytecode a snížit náklady na Gas.
10. Optimalizace krátkého hodnocení
Pro operátory || a && se logické operace provádějí pomocí krátkého hodnocení, což znamená, že pokud první podmínka již určuje výsledek logického výrazu, druhá podmínka nebude hodnocena.
Aby se optimalizovala spotřeba Gas, měly by být podmínky s nízkými náklady umístěny na začátek, aby bylo možné případně přeskočit nákladné výpočty.
Obecná dodatečná doporučení
1. Odstraňte nepotřebný kód
Pokud existují v kontraktu nepoužívané funkce nebo proměnné, doporučuje se je odstranit. To je nejpřímější způsob, jak snížit náklady na nasazení kontraktu a udržet jeho velikost malou.
Několik užitečných doporučení:
Používejte nejefektivnější algoritmy pro výpočty. Pokud se v kontraktu přímo používají výsledky některých výpočtů, měly by se tyto nadbytečné výpočty odstranit. V podstatě by měly být odstraněny jakékoli nepoužívané výpočty.
V Ethereu mohou vývojáři získat odměny za Gas uvolněním úložného prostoru. Pokud již není určitá proměnná potřebná, měla by být odstraněna pomocí klíčového slova delete nebo nastavena na výchozí hodnotu.
Optimalizace cyklů: vyhněte se nákladným cyklovým operacím, co nejvíce slučujte cykly a přesouvejte opakované výpočty mimo tělo cyklu.
2. Použití předkompilovaných kontraktů
Předkompilované kontrakty nabízejí složité knihovní funkce, jako jsou šifrování a hashování. Protože kód se nevykonává na EVM, ale na místních uzlech klientů, je zapotřebí méně Gas. Používáním předkompilovaných kontraktů můžete ušetřit Gas tím, že snížíte výpočetní zátěž potřebnou k provádění smart kontraktů.
Příklady předkompilovaných kontraktů zahrnují algoritmus digitálního podpisu eliptické křivky (ECDSA) a hashovací algoritmus SHA2-256. Použitím těchto předkompilovaných kontraktů ve smart kontraktech mohou vývojáři snížit náklady na Gas a zvýšit efektivitu běhu aplikací.
Pro úplný seznam předkompilovaných kontraktů podporovaných sítí Ethereum viz [4].
3. Použití inline assembleru
Inline assembler (in-line assembly) umožňuje vývojářům psát nízkoúrovňový, ale vysoce efektivní kód, který může být přímo prováděn EVM, aniž by bylo třeba použít drahé Solidity opkódy. Inline assembler také umožňuje přesněji řídit používání paměti a úložiště, čímž dále snižuje náklady na Gas. Kromě toho může inline assembler vykonávat některé složité operace, které by bylo obtížné dosáhnout pouze pomocí Solidity, což poskytuje větší flexibilitu pro optimalizaci spotřeby Gas.
Níže je uveden příklad kódu, který šetří Gas pomocí inline assembleru:
Z výše uvedeného obrázku je vidět, že ve srovnání se standardním případem má druhý příklad s použitím inline assembleru vyšší efektivitu Gas.
Používání inline assembleru může však představovat riziko a snadno vést k chybám. Proto by měl být používán opatrně, pouze pro zkušené vývojáře.
4. Použití řešení Layer 2
Použití řešení Layer 2 může snížit množství dat, které je třeba ukládat a počítat na hlavním Ethereum.
Řešení Layer 2, jako jsou rollupy, sidechainy a stavové kanály, mohou odlehčit transakční zátěž z hlavního etherového řetězce, což umožňuje rychlejší a levnější transakce.
Tato řešení snižují počet transakcí na blockchainu tím, že shromažďují velké množství transakcí dohromady, což snižuje náklady na Gas. Použití řešení Layer 2 také zvyšuje škálovatelnost Etherea, což umožňuje více uživatelům a aplikacím zapojit se do sítě, aniž by došlo k přetížení sítě a zácpám.
5. Použití optimalizačních nástrojů a knihoven
Existuje několik optimalizačních nástrojů, jako je solc optimizer, Truffle build optimizer a Remix Solidity compiler.
Tyto nástroje mohou pomoci minimalizovat velikost bytecode, odstranit nepotřebný kód a snížit počet operací potřebných k provádění smart kontraktů. V kombinaci s dalšími optimalizačními knihovnami Gas, jako je "solmate", mohou vývojáři efektivně snížit náklady na Gas a zvýšit efektivitu smart kontraktů.
Závěr
Optimalizace spotřeby Gas je důležitým krokem pro vývojáře, neboť minimalizuje náklady na transakce a zvyšuje efektivitu smart kontraktů v sítích kompatibilních s EVM. Upřednostněním provádění nákladově efektivních operací, snížením používání úložiště, využíváním inline assembleru a dodržováním dalších nejlepších praktik diskutovaných v tomto článku mohou vývojáři efektivně snížit spotřebu Gas svých kontraktů.
Je však třeba mít na paměti, že během optimalizace musí být vývojáři opatrní, aby se vyhnuli zavedení bezpečnostních zranitelností. Při optimalizaci kódu a snižování spotřeby Gas by nikdy neměla být obětována inherentní bezpečnost smart kontraktu.
[1]: https://ethereum.org/en/developers/docs/gas/
[2]: https://ethereum.github.io/yellowpaper/paper.pdf
[3]: https://www.evm.codes/
[4]: https://www.evm.codes/precompiled