Testowanie inteligentnych kontraktów
Publiczne blockchainy, takie jak Ethereum, są niezmienne, co utrudnia zmianę kodu inteligentnego kontraktu po wdrożeniu. Istnieją wzorce aktualizacji kontraktów do przeprowadzania „wirtualnych aktualizacji”, ale są one trudne do wdrożenia i wymagają konsensusu społecznego. Co więcej, aktualizacja może naprawić błąd tylko po jego odkryciu — jeśli atakujący odkryje lukę jako pierwszy, Twój inteligentny kontrakt jest narażony na exploit.
Z tych powodów testowanie inteligentnych kontraktów przed wdrożeniem do sieci głównej (Mainnet) jest minimalnym wymogiem dla bezpieczeństwa. Istnieje wiele technik testowania kontraktów i oceny poprawności kodu; to, co wybierzesz, zależy od Twoich potrzeb. Niemniej jednak zestaw testów składający się z różnych narzędzi i podejść jest idealny do wychwytywania zarówno drobnych, jak i poważnych luk w zabezpieczeniach w kodzie kontraktu.
Wymagania wstępne
Ta strona wyjaśnia, jak testować inteligentne kontrakty przed wdrożeniem w sieci Ethereum. Zakłada, że znasz już inteligentne kontrakty.
Czym jest testowanie inteligentnych kontraktów?
Testowanie inteligentnych kontraktów to proces weryfikacji, czy kod inteligentnego kontraktu działa zgodnie z oczekiwaniami. Testowanie jest przydatne do sprawdzania, czy dany inteligentny kontrakt spełnia wymagania dotyczące niezawodności, użyteczności i bezpieczeństwa.
Chociaż podejścia są różne, większość metod testowania wymaga wykonania inteligentnego kontraktu z małą próbką danych, które ma obsługiwać. Jeśli kontrakt generuje poprawne wyniki dla danych przykładowych, zakłada się, że działa prawidłowo. Większość narzędzi testowych zapewnia zasoby do pisania i wykonywania przypadków testowych (opens in a new tab), aby sprawdzić, czy wykonanie kontraktu jest zgodne z oczekiwanymi wynikami.
Dlaczego testowanie inteligentnych kontraktów jest ważne?
Ponieważ inteligentne kontrakty często zarządzają aktywami finansowymi o dużej wartości, drobne błędy programistyczne mogą i często prowadzą do ogromnych strat dla użytkowników (opens in a new tab). Rygorystyczne testowanie może jednak pomóc wcześnie odkryć wady i problemy w kodzie inteligentnego kontraktu i naprawić je przed uruchomieniem w sieci głównej.
Chociaż możliwa jest aktualizacja kontraktu w przypadku wykrycia błędu, aktualizacje są złożone i mogą skutkować błędami (opens in a new tab), jeśli zostaną przeprowadzone nieprawidłowo. Aktualizacja kontraktu dodatkowo neguje zasadę niezmienności i obciąża użytkowników dodatkowymi założeniami dotyczącymi zaufania. Z drugiej strony, kompleksowy plan testowania kontraktu łagodzi ryzyko związane z bezpieczeństwem inteligentnych kontraktów i zmniejsza potrzebę przeprowadzania złożonych aktualizacji logiki po wdrożeniu.
Metody testowania inteligentnych kontraktów
Metody testowania inteligentnych kontraktów Ethereum dzielą się na dwie szerokie kategorie: testowanie automatyczne i testowanie ręczne. Testowanie automatyczne i ręczne oferują unikalne korzyści i kompromisy, ale można połączyć oba, aby stworzyć solidny plan analizy kontraktów.
Testowanie automatyczne
Testowanie automatyczne wykorzystuje narzędzia, które automatycznie sprawdzają kod inteligentnego kontraktu pod kątem błędów w wykonaniu. Korzyść z testowania automatycznego wynika z użycia skryptów (opens in a new tab) do kierowania oceną funkcjonalności kontraktu. Oskryptowane testy można zaplanować tak, aby były uruchamiane wielokrotnie przy minimalnej interwencji człowieka, co czyni testowanie automatyczne bardziej wydajnym niż ręczne podejścia do testowania.
Testowanie automatyczne jest szczególnie przydatne, gdy testy są powtarzalne i czasochłonne; trudne do przeprowadzenia ręcznie; podatne na błędy ludzkie; lub obejmują ocenę krytycznych funkcji kontraktu. Narzędzia do testowania automatycznego mogą jednak mieć wady — mogą przeoczyć niektóre błędy i generować wiele fałszywie pozytywnych wyników (opens in a new tab). Dlatego idealnym rozwiązaniem jest połączenie testowania automatycznego z testowaniem ręcznym inteligentnych kontraktów.
Testowanie ręczne
Testowanie ręczne jest wspomagane przez człowieka i polega na wykonywaniu każdego przypadku testowego w zestawie testów jeden po drugim podczas analizy poprawności inteligentnego kontraktu. Różni się to od testowania automatycznego, w którym można jednocześnie uruchomić wiele izolowanych testów na kontrakcie i uzyskać raport pokazujący wszystkie nieudane i udane testy.
Testowanie ręczne może być przeprowadzane przez jedną osobę zgodnie z pisemnym planem testów, który obejmuje różne scenariusze testowe. Można również zaangażować wiele osób lub grup do interakcji z inteligentnym kontraktem w określonym czasie w ramach testowania ręcznego. Testerzy porównają rzeczywiste zachowanie kontraktu z oczekiwanym zachowaniem, oznaczając każdą różnicę jako błąd.
Skuteczne testowanie ręczne wymaga znacznych zasobów (umiejętności, czasu, pieniędzy i wysiłku) i możliwe jest — z powodu błędu ludzkiego — przeoczenie pewnych błędów podczas wykonywania testów. Testowanie ręczne może być jednak również korzystne — na przykład tester (np. audytor) może użyć intuicji do wykrycia przypadków brzegowych, które narzędzie do testowania automatycznego by przeoczyło.
Automatyczne testowanie inteligentnych kontraktów
Testy jednostkowe
Testy jednostkowe oceniają funkcje kontraktu oddzielnie i sprawdzają, czy każdy komponent działa poprawnie. Dobre testy jednostkowe powinny być proste, szybkie w uruchomieniu i zapewniać jasny obraz tego, co poszło nie tak, jeśli testy się nie powiodą.
Testy jednostkowe są przydatne do sprawdzania, czy funkcje zwracają oczekiwane wartości i czy pamięć kontraktu jest prawidłowo aktualizowana po wykonaniu funkcji. Co więcej, uruchamianie testów jednostkowych po wprowadzeniu zmian w bazie kodu kontraktu gwarantuje, że dodanie nowej logiki nie wprowadzi błędów. Poniżej znajdują się pewne wytyczne dotyczące przeprowadzania skutecznych testów jednostkowych:
Wytyczne dotyczące testów jednostkowych inteligentnych kontraktów
1. Zrozum logikę biznesową i przepływ pracy swojego kontraktu
Przed napisaniem testów jednostkowych warto wiedzieć, jakie funkcjonalności oferuje inteligentny kontrakt oraz w jaki sposób użytkownicy będą uzyskiwać dostęp do tych funkcji i z nich korzystać. Jest to szczególnie przydatne do uruchamiania testów ścieżki optymistycznej (happy path) (opens in a new tab), które określają, czy funkcje w kontrakcie zwracają prawidłowe dane wyjściowe dla prawidłowych danych wejściowych użytkownika. Wyjaśnimy tę koncepcję na (skróconym) przykładzie kontraktu aukcyjnego (opens in a new tab)
constructor(
uint biddingTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
auctionEndTime = block.timestamp + biddingTime;
}
function bid() external payable {
if (block.timestamp > auctionEndTime)
revert AuctionAlreadyEnded();
if (msg.value <= highestBid)
revert BidNotHighEnough(highestBid);
if (highestBid != 0) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
function withdraw() external returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
if (!payable(msg.sender).send(amount)) {
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
function auctionEnd() external {
if (block.timestamp < auctionEndTime)
revert AuctionNotYetEnded();
if (ended)
revert AuctionEndAlreadyCalled();
ended = true;
emit AuctionEnded(highestBidder, highestBid);
beneficiary.transfer(highestBid);
}
}
Jest to prosty kontrakt aukcyjny zaprojektowany do przyjmowania ofert w okresie licytacji. Jeśli highestBid wzrośnie, poprzedni licytant, który zaoferował najwyższą kwotę, otrzymuje swoje pieniądze z powrotem; po zakończeniu okresu licytacji beneficiary wywołuje kontrakt, aby otrzymać swoje pieniądze.
Testy jednostkowe dla takiego kontraktu obejmowałyby różne funkcje, które użytkownik może wywołać podczas interakcji z kontraktem. Przykładem może być test jednostkowy, który sprawdza, czy użytkownik może złożyć ofertę w trakcie trwania aukcji (tj. wywołania bid() kończą się sukcesem) lub taki, który sprawdza, czy użytkownik może złożyć wyższą ofertę niż obecna highestBid.
Zrozumienie operacyjnego przepływu pracy kontraktu pomaga również w pisaniu testów jednostkowych, które sprawdzają, czy wykonanie spełnia wymagania. Na przykład kontrakt aukcyjny określa, że użytkownicy nie mogą składać ofert po zakończeniu aukcji (tj. gdy auctionEndTime jest mniejsze niż block.timestamp). W związku z tym deweloper może uruchomić test jednostkowy, który sprawdza, czy wywołania funkcji bid() kończą się sukcesem, czy niepowodzeniem po zakończeniu aukcji (tj. gdy auctionEndTime > block.timestamp).
2. Oceń wszystkie założenia związane z wykonaniem kontraktu
Ważne jest, aby udokumentować wszelkie założenia dotyczące wykonania kontraktu i napisać testy jednostkowe w celu zweryfikowania ich poprawności. Oprócz zapewnienia ochrony przed nieoczekiwanym wykonaniem, testowanie asercji zmusza do myślenia o operacjach, które mogłyby złamać model bezpieczeństwa inteligentnego kontraktu. Przydatną wskazówką jest wyjście poza „testy optymistyczne” i napisanie testów negatywnych, które sprawdzają, czy funkcja kończy się niepowodzeniem dla błędnych danych wejściowych.
Wiele frameworków do testów jednostkowych pozwala na tworzenie asercji — prostych instrukcji określających, co kontrakt może, a czego nie może zrobić — i uruchamianie testów w celu sprawdzenia, czy te asercje są spełnione podczas wykonywania. Deweloper pracujący nad opisanym wcześniej kontraktem aukcyjnym mógłby sformułować następujące asercje dotyczące jego zachowania przed uruchomieniem testów negatywnych:
-
Użytkownicy nie mogą składać ofert, gdy aukcja się zakończyła lub jeszcze się nie rozpoczęła.
-
Kontrakt aukcyjny cofa transakcję (revert), jeśli oferta jest poniżej akceptowalnego progu.
-
Użytkownicy, którym nie uda się wygrać licytacji, otrzymują zwrot swoich środków.
Uwaga: Innym sposobem testowania założeń jest pisanie testów, które wyzwalają modyfikatory funkcji (opens in a new tab) w kontrakcie, w szczególności instrukcje require, assert i if…else.
3. Mierz pokrycie kodu
Pokrycie kodu (opens in a new tab) to metryka testowania, która śledzi liczbę gałęzi, wierszy i instrukcji w kodzie wykonanych podczas testów. Testy powinny mieć dobre pokrycie kodu, aby zminimalizować ryzyko nietestowanych luk w zabezpieczeniach. Bez wystarczającego pokrycia można błędnie założyć, że kontrakt jest bezpieczny, ponieważ wszystkie testy kończą się powodzeniem, podczas gdy luki w zabezpieczeniach nadal istnieją w nietestowanych ścieżkach kodu. Odnotowanie wysokiego pokrycia kodu daje jednak pewność, że wszystkie instrukcje/funkcje w inteligentnym kontrakcie zostały wystarczająco przetestowane pod kątem poprawności.
4. Używaj dobrze rozwiniętych frameworków testowych
Jakość narzędzi używanych do przeprowadzania testów jednostkowych dla inteligentnych kontraktów ma kluczowe znaczenie. Idealny framework testowy to taki, który jest regularnie utrzymywany; zapewnia przydatne funkcje (np. możliwości logowania i raportowania); i musi być szeroko stosowany oraz sprawdzony przez innych deweloperów.
Frameworki do testów jednostkowych dla inteligentnych kontraktów w języku Solidity są dostępne w różnych językach (głównie JavaScript, Python i Rust). Zapoznaj się z poniższymi przewodnikami, aby uzyskać informacje o tym, jak rozpocząć uruchamianie testów jednostkowych za pomocą różnych frameworków testowych:
- Uruchamianie testów jednostkowych za pomocą Brownie (opens in a new tab)
- Uruchamianie testów jednostkowych za pomocą Foundry (opens in a new tab)
- Uruchamianie testów jednostkowych za pomocą Waffle (opens in a new tab)
- Uruchamianie testów jednostkowych za pomocą Remix (opens in a new tab)
- Uruchamianie testów jednostkowych za pomocą Ape (opens in a new tab)
- Uruchamianie testów jednostkowych za pomocą Hardhat (opens in a new tab)
- Uruchamianie testów jednostkowych za pomocą Wake (opens in a new tab)
Testy integracyjne
Podczas gdy testy jednostkowe debugują funkcje kontraktu w izolacji, testy integracyjne oceniają komponenty inteligentnego kontraktu jako całość. Testy integracyjne mogą wykryć problemy wynikające z wywołań między kontraktami lub interakcji między różnymi funkcjami w tym samym inteligentnym kontrakcie. Na przykład testy integracyjne mogą pomóc sprawdzić, czy takie rzeczy jak dziedziczenie (opens in a new tab) i wstrzykiwanie zależności działają prawidłowo.
Testowanie integracyjne jest przydatne, jeśli Twój kontrakt przyjmuje architekturę modułową lub łączy się z innymi kontraktami onchain podczas wykonywania. Jednym ze sposobów przeprowadzania testów integracyjnych jest na określonej wysokości (przy użyciu narzędzia takiego jak Forge (opens in a new tab) lub Hardhat (opens in a new tab)) i symulowanie interakcji między Twoim kontraktem a wdrożonymi kontraktami.
Rozwidlony blockchain będzie zachowywał się podobnie do sieci głównej (Mainnet) i będzie miał konta z powiązanymi stanami i saldami. Działa on jednak tylko jako lokalne środowisko programistyczne w piaskownicy (sandbox), co oznacza, że nie będziesz potrzebować prawdziwego ETH do transakcji, a Twoje zmiany nie wpłyną na rzeczywisty protokół Ethereum.
Testowanie oparte na właściwościach
Testowanie oparte na właściwościach to proces sprawdzania, czy inteligentny kontrakt spełnia pewną zdefiniowaną właściwość. Właściwości potwierdzają fakty dotyczące zachowania kontraktu, które powinny pozostać prawdziwe w różnych scenariuszach — przykładem właściwości inteligentnego kontraktu może być „Operacje arytmetyczne w kontrakcie nigdy nie powodują przepełnienia (overflow) ani niedomiaru (underflow)”.
Analiza statyczna i analiza dynamiczna to dwie powszechne techniki wykonywania testów opartych na właściwościach, a obie mogą zweryfikować, czy kod programu (w tym przypadku inteligentnego kontraktu) spełnia pewną predefiniowaną właściwość. Niektóre narzędzia do testowania opartego na właściwościach są wyposażone w predefiniowane reguły dotyczące oczekiwanych właściwości kontraktu i sprawdzają kod pod kątem tych reguł, podczas gdy inne pozwalają na tworzenie niestandardowych właściwości dla inteligentnego kontraktu.
Analiza statyczna
Analizator statyczny przyjmuje jako dane wejściowe kod źródłowy inteligentnego kontraktu i generuje wyniki deklarujące, czy kontrakt spełnia właściwość, czy nie. W przeciwieństwie do analizy dynamicznej, analiza statyczna nie obejmuje wykonywania kontraktu w celu przeanalizowania go pod kątem poprawności. Zamiast tego analiza statyczna wnioskuje o wszystkich możliwych ścieżkach, które inteligentny kontrakt mógłby obrać podczas wykonywania (tj. badając strukturę kodu źródłowego w celu określenia, co oznaczałoby to dla działania kontraktu w czasie wykonywania).
Lintowanie (opens in a new tab) i testowanie statyczne (opens in a new tab) to powszechne metody przeprowadzania analizy statycznej na kontraktach. Obie wymagają analizy niskopoziomowych reprezentacji wykonania kontraktu, takich jak drzewa składni abstrakcyjnej (opens in a new tab) i grafy przepływu sterowania (opens in a new tab) generowane przez kompilator.
W większości przypadków analiza statyczna jest przydatna do wykrywania problemów z bezpieczeństwem, takich jak użycie niebezpiecznych konstrukcji, błędy składniowe lub naruszenia standardów kodowania w kodzie kontraktu. Wiadomo jednak, że analizatory statyczne są na ogół zawodne w wykrywaniu głębszych luk w zabezpieczeniach i mogą generować nadmierną liczbę fałszywie pozytywnych wyników.
Analiza dynamiczna
Analiza dynamiczna generuje symboliczne dane wejściowe (np. w wykonywaniu symbolicznym (opens in a new tab)) lub konkretne dane wejściowe (np. w fuzzingu (opens in a new tab)) do funkcji inteligentnego kontraktu, aby sprawdzić, czy jakikolwiek ślad wykonania narusza określone właściwości. Ta forma testowania opartego na właściwościach różni się od testów jednostkowych tym, że przypadki testowe obejmują wiele scenariuszy, a program obsługuje generowanie przypadków testowych.
Fuzzing (opens in a new tab) jest przykładem techniki analizy dynamicznej do weryfikacji dowolnych właściwości w inteligentnych kontraktach. Fuzzer wywołuje funkcje w docelowym kontrakcie z losowymi lub zniekształconymi wariacjami zdefiniowanej wartości wejściowej. Jeśli inteligentny kontrakt wejdzie w stan błędu (np. taki, w którym asercja kończy się niepowodzeniem), problem jest oznaczany, a dane wejściowe, które kierują wykonanie w stronę podatnej ścieżki, są generowane w raporcie.
Fuzzing jest przydatny do oceny mechanizmu walidacji danych wejściowych inteligentnego kontraktu, ponieważ niewłaściwa obsługa nieoczekiwanych danych wejściowych może skutkować niezamierzonym wykonaniem i wywołać niebezpieczne skutki. Ta forma testowania opartego na właściwościach może być idealna z wielu powodów:
-
Pisanie przypadków testowych obejmujących wiele scenariuszy jest trudne. Test właściwości wymaga jedynie zdefiniowania zachowania i zakresu danych, za pomocą których zachowanie to ma być testowane — program automatycznie generuje przypadki testowe na podstawie zdefiniowanej właściwości.
-
Twój zestaw testów może nie pokrywać w wystarczającym stopniu wszystkich możliwych ścieżek w programie. Nawet przy 100% pokryciu możliwe jest przeoczenie przypadków brzegowych.
-
Testy jednostkowe dowodzą, że kontrakt wykonuje się poprawnie dla danych przykładowych, ale to, czy kontrakt wykonuje się poprawnie dla danych wejściowych spoza próbki, pozostaje nieznane. Testy właściwości wykonują docelowy kontrakt z wieloma wariacjami danej wartości wejściowej, aby znaleźć ślady wykonania, które powodują niepowodzenia asercji. W ten sposób test właściwości zapewnia więcej gwarancji, że kontrakt wykonuje się poprawnie dla szerokiej klasy danych wejściowych.
Wytyczne dotyczące przeprowadzania testów opartych na właściwościach dla inteligentnych kontraktów
Uruchamianie testów opartych na właściwościach zazwyczaj rozpoczyna się od zdefiniowania właściwości (np. braku przepełnienia liczb całkowitych (opens in a new tab)) lub zbioru właściwości, które chcesz zweryfikować w inteligentnym kontrakcie. Podczas pisania testów właściwości może być również konieczne zdefiniowanie zakresu wartości, w ramach którego program może generować dane dla wejść transakcji.
Po prawidłowym skonfigurowaniu narzędzie do testowania właściwości wykona funkcje inteligentnego kontraktu z losowo wygenerowanymi danymi wejściowymi. Jeśli wystąpią jakiekolwiek naruszenia asercji, powinieneś otrzymać raport z konkretnymi danymi wejściowymi, które naruszają ocenianą właściwość. Zapoznaj się z poniższymi przewodnikami, aby rozpocząć przeprowadzanie testów opartych na właściwościach za pomocą różnych narzędzi:
- Analiza statyczna inteligentnych kontraktów za pomocą Slither (opens in a new tab)
- Analiza statyczna inteligentnych kontraktów za pomocą Wake (opens in a new tab)
- Testowanie oparte na właściwościach za pomocą Brownie (opens in a new tab)
- Fuzzing kontraktów za pomocą Foundry (opens in a new tab)
- Fuzzing kontraktów za pomocą Echidna (opens in a new tab)
- Fuzzing kontraktów za pomocą Wake (opens in a new tab)
- Wykonywanie symboliczne inteligentnych kontraktów za pomocą Manticore (opens in a new tab)
- Wykonywanie symboliczne inteligentnych kontraktów za pomocą Mythril (opens in a new tab)
Ręczne testowanie inteligentnych kontraktów
Ręczne testowanie inteligentnych kontraktów często pojawia się na późniejszym etapie cyklu rozwoju, po uruchomieniu testów automatycznych. Ta forma testowania ocenia inteligentny kontrakt jako jeden w pełni zintegrowany produkt, aby sprawdzić, czy działa on zgodnie z wymaganiami technicznymi.
Testowanie kontraktów na lokalnym blockchainie
Chociaż zautomatyzowane testowanie przeprowadzane w lokalnym środowisku programistycznym może dostarczyć przydatnych informacji do debugowania, będziesz chciał wiedzieć, jak Twój inteligentny kontrakt zachowuje się w środowisku produkcyjnym. Jednak wdrożenie do głównego łańcucha Ethereum wiąże się z opłatami za gaz — nie wspominając o tym, że Ty lub Twoi użytkownicy możecie stracić prawdziwe pieniądze, jeśli Twój inteligentny kontrakt nadal ma błędy.
Testowanie kontraktu na lokalnym blockchainie (znanym również jako sieć deweloperska) jest zalecaną alternatywą dla testowania w sieci głównej (Mainnet). Lokalny blockchain to kopia blockchaina Ethereum działająca lokalnie na Twoim komputerze, która symuluje zachowanie warstwy wykonawczej Ethereum. W związku z tym możesz programować transakcje w celu interakcji z kontraktem bez ponoszenia znacznych kosztów ogólnych.
Uruchamianie kontraktów na lokalnym blockchainie może być przydatne jako forma ręcznego testowania integracyjnego. Inteligentne kontrakty są wysoce komponowalne, co pozwala na integrację z istniejącymi protokołami — ale nadal musisz upewnić się, że tak złożone interakcje onchain przynoszą prawidłowe wyniki.
Więcej o sieciach deweloperskich.
Testowanie kontraktów w sieciach testowych
Sieć testowa (testnet) działa dokładnie tak samo jak sieć główna Ethereum, z tą różnicą, że używa etheru (ETH) bez rzeczywistej wartości. Wdrożenie kontraktu w sieci testowej oznacza, że każdy może wejść z nim w interakcję (np. za pośrednictwem frontendu zdecentralizowanej aplikacji (dapp)), nie narażając środków na ryzyko.
Ta forma testowania ręcznego jest przydatna do oceny kompleksowego przepływu aplikacji z punktu widzenia użytkownika. W tym przypadku beta testerzy mogą również przeprowadzać uruchomienia próbne i zgłaszać wszelkie problemy z logiką biznesową kontraktu i ogólną funkcjonalnością.
Wdrożenie w sieci testowej po przetestowaniu na lokalnym blockchainie jest idealne, ponieważ ta pierwsza jest bliższa zachowaniu Wirtualnej Maszyny Ethereum (EVM). Dlatego powszechne jest, że wiele natywnych projektów Ethereum wdraża dappy w sieciach testowych, aby ocenić działanie inteligentnych kontraktów w warunkach rzeczywistych.
Więcej o sieciach testowych Ethereum.
Testowanie a weryfikacja formalna
Chociaż testowanie pomaga potwierdzić, że kontrakt zwraca oczekiwane wyniki dla niektórych danych wejściowych, nie może jednoznacznie udowodnić tego samego dla danych wejściowych nieużywanych podczas testów. Testowanie inteligentnego kontraktu nie może zatem zagwarantować „poprawności funkcjonalnej” (tj. nie może wykazać, że program zachowuje się zgodnie z wymaganiami dla wszystkich zestawów wartości wejściowych).
Weryfikacja formalna to podejście do oceny poprawności oprogramowania poprzez sprawdzenie, czy formalny model programu jest zgodny z formalną specyfikacją. Model formalny to abstrakcyjna matematyczna reprezentacja programu, podczas gdy specyfikacja formalna definiuje właściwości programu (tj. logiczne asercje dotyczące wykonania programu).
Ponieważ właściwości są zapisane w terminach matematycznych, możliwe staje się zweryfikowanie, czy formalny (matematyczny) model systemu spełnia specyfikację przy użyciu logicznych reguł wnioskowania. W związku z tym mówi się, że narzędzia do weryfikacji formalnej dostarczają „matematycznego dowodu” poprawności systemu.
W przeciwieństwie do testowania, weryfikacja formalna może być użyta do sprawdzenia, czy wykonanie inteligentnego kontraktu spełnia formalną specyfikację dla wszystkich wykonań (tj. nie ma błędów) bez konieczności wykonywania go z danymi przykładowymi. Nie tylko skraca to czas spędzony na uruchamianiu dziesiątek testów jednostkowych, ale jest również bardziej skuteczne w wychwytywaniu ukrytych luk w zabezpieczeniach. Niemniej jednak techniki weryfikacji formalnej leżą w pewnym spektrum w zależności od trudności ich wdrożenia i użyteczności.
Więcej o weryfikacji formalnej inteligentnych kontraktów.
Testowanie a audyty i programy bug bounty
Jak wspomniano, rygorystyczne testowanie rzadko może zagwarantować brak błędów w kontrakcie; podejścia oparte na weryfikacji formalnej mogą zapewnić silniejsze gwarancje poprawności, ale obecnie są trudne w użyciu i wiążą się ze znacznymi kosztami.
Mimo to możesz dodatkowo zwiększyć prawdopodobieństwo wychwycenia luk w kontraktach, zlecając niezależny przegląd kodu. Audyty inteligentnych kontraktów (opens in a new tab) i programy bug bounty (opens in a new tab) to dwa sposoby na to, aby inni przeanalizowali Twoje kontrakty.
Audyty są przeprowadzane przez audytorów doświadczonych w znajdowaniu przypadków luk w zabezpieczeniach i złych praktyk programistycznych w inteligentnych kontraktach. Audyt zazwyczaj obejmuje testowanie (i ewentualnie weryfikację formalną), a także ręczny przegląd całej bazy kodu.
Z kolei program bug bounty zazwyczaj polega na zaoferowaniu nagrody finansowej osobie (powszechnie określanej jako hakerzy whitehat (opens in a new tab)), która odkryje lukę w inteligentnym kontrakcie i ujawni ją deweloperom. Programy bug bounty są podobne do audytów, ponieważ polegają na proszeniu innych o pomoc w znalezieniu wad w inteligentnych kontraktach.
Główna różnica polega na tym, że programy bug bounty są otwarte dla szerszej społeczności deweloperów/hakerów i przyciągają szeroką klasę etycznych hakerów oraz niezależnych specjalistów ds. bezpieczeństwa o unikalnych umiejętnościach i doświadczeniu. Może to być przewagą nad audytami inteligentnych kontraktów, które opierają się głównie na zespołach mogących posiadać ograniczoną lub wąską wiedzę specjalistyczną.
Narzędzia i biblioteki do testowania
Narzędzia do testów jednostkowych
-
solidity-coverage (opens in a new tab) - Narzędzie do pomiaru pokrycia kodu dla inteligentnych kontraktów napisanych w języku Solidity.
-
Waffle (opens in a new tab) - Framework do zaawansowanego tworzenia i testowania inteligentnych kontraktów (oparty na Ethers.js).
-
Remix Tests (opens in a new tab) - Narzędzie do testowania inteligentnych kontraktów w języku Solidity. Działa pod wtyczką „Solidity Unit Testing” w Remix IDE, która służy do pisania i uruchamiania przypadków testowych dla kontraktu.
-
OpenZeppelin Test Helpers (opens in a new tab) - Biblioteka asercji do testowania inteligentnych kontraktów Ethereum. Upewnij się, że Twoje kontrakty zachowują się zgodnie z oczekiwaniami!
-
Framework do testów jednostkowych Brownie (opens in a new tab) - Brownie wykorzystuje Pytest, bogaty w funkcje framework testowy, który pozwala pisać małe testy przy użyciu minimalnej ilości kodu, dobrze skaluje się w przypadku dużych projektów i jest wysoce rozszerzalny.
-
Foundry Tests (opens in a new tab) - Foundry oferuje Forge, szybki i elastyczny framework testowy Ethereum zdolny do wykonywania prostych testów jednostkowych, sprawdzania optymalizacji gazu i fuzzingu kontraktów.
-
Hardhat Tests (opens in a new tab) - Framework do testowania inteligentnych kontraktów oparty na Ethers.js, Mocha i Chai.
-
ApeWorx (opens in a new tab) - Oparty na języku Python framework do programowania i testowania inteligentnych kontraktów przeznaczonych dla Wirtualnej Maszyny Ethereum (EVM).
-
Wake (opens in a new tab) - Oparty na języku Python framework do testów jednostkowych i fuzzingu z silnymi możliwościami debugowania i obsługą testowania międzyłańcuchowego (cross-chain), wykorzystujący pytest i Anvil w celu zapewnienia najlepszego doświadczenia użytkownika i wydajności.
Narzędzia do testowania opartego na właściwościach
Narzędzia do analizy statycznej
-
Slither (opens in a new tab) - Oparty na języku Python framework do analizy statycznej Solidity służący do znajdowania luk w zabezpieczeniach, poprawy zrozumienia kodu i pisania niestandardowych analiz dla inteligentnych kontraktów.
-
Ethlint (opens in a new tab) - Linter do wymuszania najlepszych praktyk w zakresie stylu i bezpieczeństwa dla języka programowania inteligentnych kontraktów Solidity.
-
Cyfrin Aderyn (opens in a new tab) - Oparty na języku Rust analizator statyczny zaprojektowany specjalnie z myślą o bezpieczeństwie i rozwoju inteligentnych kontraktów Web3.
-
Wake (opens in a new tab) - Oparty na języku Python framework do analizy statycznej z detektorami luk w zabezpieczeniach i jakości kodu, drukarkami do wyodrębniania przydatnych informacji z kodu oraz obsługą pisania niestandardowych podmodułów.
-
Slippy (opens in a new tab) - Prosty i potężny linter dla Solidity.
Narzędzia do analizy dynamicznej
-
Echidna (opens in a new tab) - Szybki fuzzer kontraktów do wykrywania luk w zabezpieczeniach w inteligentnych kontraktach poprzez testowanie oparte na właściwościach.
-
Diligence Fuzzing (opens in a new tab) - Zautomatyzowane narzędzie do fuzzingu przydatne do wykrywania naruszeń właściwości w kodzie inteligentnego kontraktu.
-
Manticore (opens in a new tab) - Framework do dynamicznego wykonywania symbolicznego służący do analizy kodu bajtowego EVM.
-
Mythril (opens in a new tab) - Narzędzie do oceny kodu bajtowego EVM służące do wykrywania luk w kontraktach przy użyciu analizy skażenia (taint analysis), analizy konkolowej (concolic analysis) i sprawdzania przepływu sterowania.
-
Diligence Scribble (opens in a new tab) - Scribble to język specyfikacji i narzędzie do weryfikacji w czasie wykonywania, które pozwala na adnotowanie inteligentnych kontraktów właściwościami umożliwiającymi automatyczne testowanie kontraktów za pomocą narzędzi takich jak Diligence Fuzzing lub MythX.
Powiązane samouczki
- Przegląd i porównanie różnych produktów do testowania _
- Jak używać Echidna do testowania inteligentnych kontraktów
- Jak używać Manticore do znajdowania błędów w inteligentnych kontraktach
- Jak używać Slither do znajdowania błędów w inteligentnych kontraktach
- Jak mockować kontrakty Solidity do testowania
- Jak uruchamiać testy jednostkowe w Solidity za pomocą Foundry (opens in a new tab)
Dalsza lektura
- Szczegółowy przewodnik po testowaniu inteligentnych kontraktów Ethereum (opens in a new tab)
- Jak testować inteligentne kontrakty Ethereum (opens in a new tab)
- Przewodnik MolochDAO po testach jednostkowych dla deweloperów (opens in a new tab)
- Jak testować inteligentne kontrakty jak gwiazda rocka (opens in a new tab)
Samouczki: Testowanie inteligentnych kontraktów na Ethereum
- Jak stworzyć i przetestować dApp w lokalnej, wieloklienckiej sieci testowej – Przewodnik po wdrażaniu inteligentnego kontraktu w lokalnej sieci testowej i przeprowadzaniu testów.
- Jak mockować inteligentne kontrakty Solidity do testowania – Średniozaawansowany samouczek dotyczący korzystania z danych mockowanych i wdrażania testów jednostkowych.
- Jak używać Echidna do testowania inteligentnych kontraktów – Zaawansowane podejście do fuzzingu i testowania inteligentnych kontraktów.