Przejdź do głównej treści

Zmniejszanie kontraktów w celu walki z limitem rozmiaru kontraktu

Solidity
inteligentne kontrakty
przechowywanie
Średniozaawansowany
Markus Waas
26 czerwca 2020
5 minut czytania

Dlaczego istnieje limit?

Dnia 22 listopada 2016 r. (opens in a new tab) twarde rozwidlenie (hard fork) Spurious Dragon wprowadziło EIP-170 (opens in a new tab), które dodało limit rozmiaru inteligentnego kontraktu wynoszący 24,576 kb. Dla Ciebie jako programisty Solidity oznacza to, że gdy dodajesz coraz więcej funkcjonalności do swojego kontraktu, w pewnym momencie osiągniesz ten limit i podczas wdrażania zobaczysz błąd:

Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on Mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.

Limit ten został wprowadzony, aby zapobiec atakom typu odmowa usługi (DOS). Każde wywołanie kontraktu jest stosunkowo tanie pod względem gazu. Jednak wpływ wywołania kontraktu na węzły Ethereum rośnie nieproporcjonalnie w zależności od rozmiaru kodu wywoływanego kontraktu (odczyt kodu z dysku, wstępne przetwarzanie kodu, dodawanie danych do dowodu Merkle'a). Zawsze, gdy masz do czynienia z sytuacją, w której atakujący potrzebuje niewielu zasobów, aby spowodować dużo pracy dla innych, pojawia się potencjał do ataków DOS.

Początkowo było to mniejszym problemem, ponieważ jednym z naturalnych limitów rozmiaru kontraktu jest limit gazu bloku. Oczywiście kontrakt musi zostać wdrożony w ramach transakcji, która zawiera cały kod bajtowy kontraktu. Jeśli dołączysz tylko tę jedną transakcję do bloku, możesz zużyć cały ten gaz, ale nie jest on nieskończony. Od czasu aktualizacji London, limit gazu bloku może wahać się od 15 do 30 milionów jednostek w zależności od zapotrzebowania sieci.

Poniżej przyjrzymy się kilku metodom uporządkowanym według ich potencjalnego wpływu. Pomyśl o tym w kategoriach utraty wagi. Najlepsza strategia dla kogoś, kto chce osiągnąć swoją docelową wagę (w naszym przypadku 24 kb), to skupienie się najpierw na metodach o dużym wpływie. W większości przypadków samo naprawienie diety pozwoli Ci to osiągnąć, ale czasami potrzebujesz czegoś więcej. Wtedy możesz dodać trochę ćwiczeń (średni wpływ) lub nawet suplementy (mały wpływ).

Duży wpływ

Rozdziel swoje kontrakty

To zawsze powinno być Twoje pierwsze podejście. Jak możesz rozdzielić kontrakt na kilka mniejszych? Zazwyczaj zmusza to do wymyślenia dobrej architektury dla Twoich kontraktów. Mniejsze kontrakty są zawsze preferowane z perspektywy czytelności kodu. Przy dzieleniu kontraktów zadaj sobie następujące pytania:

  • Które funkcje do siebie pasują? Każdy zestaw funkcji może najlepiej sprawdzić się we własnym kontrakcie.
  • Które funkcje nie wymagają odczytywania stanu kontraktu lub tylko jego określonego podzbioru?
  • Czy możesz rozdzielić przechowywanie danych i funkcjonalność?

Biblioteki

Jednym z prostych sposobów na oddzielenie kodu funkcjonalności od przechowywania danych jest użycie biblioteki (opens in a new tab). Nie deklaruj funkcji biblioteki jako wewnętrznych (internal), ponieważ zostaną one dodane do kontraktu (opens in a new tab) bezpośrednio podczas kompilacji. Jeśli jednak użyjesz funkcji publicznych, będą one w rzeczywistości znajdować się w oddzielnym kontrakcie biblioteki. Rozważ zastosowanie dyrektywy using for (opens in a new tab), aby korzystanie z bibliotek było wygodniejsze.

Proxy

Bardziej zaawansowaną strategią byłby system proxy. Biblioteki używają w tle DELEGATECALL, co po prostu wykonuje funkcję innego kontraktu ze stanem kontraktu wywołującego. Sprawdź ten wpis na blogu (opens in a new tab), aby dowiedzieć się więcej o systemach proxy. Dają one więcej funkcjonalności, np. umożliwiają aktualizacje, ale dodają również wiele złożoności. Nie dodawałbym ich tylko po to, aby zmniejszyć rozmiar kontraktu, chyba że z jakiegoś powodu jest to Twoja jedyna opcja.

Średni wpływ

Usuń funkcje

To powinno być oczywiste. Funkcje znacznie zwiększają rozmiar kontraktu.

  • Zewnętrzne (External): Często dodajemy wiele funkcji widoku (view) dla wygody. Jest to całkowicie w porządku, dopóki nie osiągniesz limitu rozmiaru. Wtedy warto naprawdę zastanowić się nad usunięciem wszystkich poza absolutnie niezbędnymi.
  • Wewnętrzne (Internal): Możesz również usunąć funkcje wewnętrzne/prywatne i po prostu wstawić ich kod bezpośrednio (inline), o ile funkcja jest wywoływana tylko raz.

Unikaj dodatkowych zmiennych

function get(uint id) returns (address,address) {
    MyStruct memory myStruct = myStructs[id];
    return (myStruct.addr1, myStruct.addr2);
}
function get(uint id) returns (address,address) {
    return (myStructs[id].addr1, myStructs[id].addr2);
}

Taka prosta zmiana robi różnicę 0,28 kb. Istnieje duża szansa, że znajdziesz wiele podobnych sytuacji w swoich kontraktach, a te mogą naprawdę zsumować się do znaczących wartości.

Skróć wiadomości o błędach

Długie wiadomości o wycofaniu (revert), a w szczególności wiele różnych wiadomości o wycofaniu, może niepotrzebnie rozdmuchać kontrakt. Zamiast tego używaj krótkich kodów błędów i dekoduj je w swoim kontrakcie. Długa wiadomość może stać się znacznie krótsza:

require(msg.sender == owner, "Only the owner of this contract can call this function");
require(msg.sender == owner, "OW1");

Używaj niestandardowych błędów zamiast wiadomości o błędach

Niestandardowe błędy (custom errors) zostały wprowadzone w Solidity 0.8.4 (opens in a new tab). Są one świetnym sposobem na zmniejszenie rozmiaru Twoich kontraktów, ponieważ są kodowane w ABI jako selektory (podobnie jak funkcje).

error Unauthorized();

if (msg.sender != owner) {
    revert Unauthorized();
}

Rozważ niską wartość przebiegów (runs) w optymalizatorze

Możesz również zmienić ustawienia optymalizatora. Domyślna wartość 200 oznacza, że próbuje on zoptymalizować kod bajtowy tak, jakby funkcja była wywoływana 200 razy. Jeśli zmienisz ją na 1, w zasadzie mówisz optymalizatorowi, aby optymalizował pod kątem uruchomienia każdej funkcji tylko raz. Funkcja zoptymalizowana do jednorazowego uruchomienia oznacza, że jest zoptymalizowana pod kątem samego wdrożenia. Pamiętaj, że zwiększa to koszty gazu za uruchamianie funkcji, więc możesz nie chcieć tego robić.

Mały wpływ

Unikaj przekazywania struktur do funkcji

Jeśli używasz ABIEncoderV2 (opens in a new tab), pomocne może być nieprzekazywanie struktur (structs) do funkcji. Zamiast przekazywać parametr jako strukturę, przekaż wymagane parametry bezpośrednio. W tym przykładzie zaoszczędziliśmy kolejne 0,1 kb.

function get(uint id) returns (address,address) {
    return _get(myStruct);
}

function _get(MyStruct memory myStruct) private view returns(address,address) {
    return (myStruct.addr1, myStruct.addr2);
}
function get(uint id) returns(address,address) {
    return _get(myStructs[id].addr1, myStructs[id].addr2);
}

function _get(address addr1, address addr2) private view returns(address,address) {
    return (addr1, addr2);
}

Deklaruj poprawną widoczność dla funkcji i zmiennych

  • Funkcje lub zmienne, które są wywoływane tylko z zewnątrz? Zadeklaruj je jako external zamiast public.
  • Funkcje lub zmienne wywoływane tylko z wnętrza kontraktu? Zadeklaruj je jako private lub internal zamiast public.

Usuń modyfikatory

Modyfikatory, zwłaszcza gdy są intensywnie używane, mogą mieć znaczący wpływ na rozmiar kontraktu. Rozważ ich usunięcie i użycie w ich miejsce funkcji.

modifier checkStuff() {}

function doSomething() checkStuff {}
function checkStuff() private {}

function doSomething() { checkStuff(); }

Te wskazówki powinny pomóc Ci znacznie zmniejszyć rozmiar kontraktu. Jeszcze raz, nie mogę tego wystarczająco podkreślić: zawsze skupiaj się na dzieleniu kontraktów, jeśli to możliwe, aby uzyskać największy wpływ.

Ostatnia aktualizacja strony: 3 kwietnia 2026