Перейти к содержанию

Fork-тестирование в Foundry: выбранный локальный блок

В предыдущем уроке окружение задавалось инструментами времени и блока. Теперь рассмотрим другой источник начального состояния: известный блок собственного локального узла. Fork позволяет исследовать его копию в проверяющей среде.

Подготовим отдельный файл test/CourseCatalogLocalFork.t.sol и паспорт снимка. Локальный узел, размещение контракта и fork-проверки сейчас не запускаются. Поэтому адрес, блок и digest остаются параметрами, которые читатель заполнит после подготовки собственной лаборатории; настоящие значения мы не выдумываем.

Что требуется до создания копии

В предыдущих сценариях new CourseCatalog создавал пустой объект непосредственно внутри подготовки. Для fork нужен уже существующий экземпляр в выбранной цепочке и блок, где он имеет нужное состояние. Удобно заранее разместить реестр на собственном Anvil, опубликовать курс и сохранить данные выпуска.

Этот будущий подготовительный процесс содержит реальные действия локального узла, хотя средства учебные. Его нельзя считать состоявшимся только потому, что статья описывает команды. Мы отдельно фиксируем ожидаемую цепочку 31337, адрес экземпляра, адрес издателя, digest, номер и хеш блока.

RPC должен предоставлять состояние выбранного блока. Если узел перезапущен с другой историей или старые данные недоступны, тот же номер блока не воспроизводит прежний снимок. Для серьёзного отчёта требуется происхождение узла и сохранение необходимого состояния, а не только число в переменной.

Записываем параметры подключения

В foundry.toml можно добавить именованный endpoint local. Он указывает на обычный адрес собственной лаборатории, но сам по себе не запускает узел и не гарантирует, что там находится правильная цепочка. Фактическую конфигурацию читатель должен проверить отдельно.

[rpc_endpoints]
local = "http://127.0.0.1:8545"

Этот порт относится к будущему RPC, а не к статическому серверу уроков. Если лаборатория использует другой адрес, его следует явно заменить. Не нужно подставлять случайный публичный mainnet endpoint в пример, который рассчитан на известный собственный экземпляр.

Параметры снимка зададим через именованные переменные окружения. Их значения не включают приватный ключ: fork-чтению и моделированию отправителя он не нужен. Требуется лишь точное описание состояния, которое должно существовать на выбранном блоке.

Переменная Значение читателя
COURSE_FORK_BLOCK Номер сохранённого локального блока
COURSE_CATALOG_ADDRESS Адрес реестра в этом блоке
COURSE_PUBLISHER_ADDRESS Издатель соответствующего экземпляра
COURSE_EXPECTED_DIGEST Отпечаток активного курса 17

Создаём отдельную подготовку

Исходник использует те же Test и CourseCatalog, что основная лаборатория. Он не создаёт новый реестр через new: прикладной адрес берётся из снимка, а createSelectFork выбирает копию определённого блока.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {Test} from "forge-std/Test.sol";
import {CourseCatalog} from "../src/CourseCatalog.sol";

contract CourseCatalogLocalForkTest is Test {
    CourseCatalog internal catalog;
    address internal expectedPublisher;
    bytes32 internal expectedDigest;

    function setUp() public {
        uint256 chosenBlock = vm.envUint("COURSE_FORK_BLOCK");
        vm.createSelectFork("local", chosenBlock);
        assertEq(block.chainid, 31337);
        address instance = vm.envAddress("COURSE_CATALOG_ADDRESS");
        assertTrue(instance.code.length > 0);
        catalog = CourseCatalog(instance);
        expectedPublisher = vm.envAddress("COURSE_PUBLISHER_ADDRESS");
        expectedDigest = vm.envBytes32("COURSE_EXPECTED_DIGEST");
    }

    function testKnownCourseMatchesSavedExpectation() public view {
        assertEq(catalog.publisher(), expectedPublisher);
        (bytes32 digest, bool active) = catalog.courses(17);
        assertEq(digest, expectedDigest);
        assertTrue(active);
    }
}

Проверка code.length обнаруживает пустой адрес, но не устанавливает полное соответствие исходнику. У другой программы тоже может быть код и похожий getter. В двадцать девятом уроке рассмотрим происхождение байткода и конфигурации; здесь дополнительно сверяем известный издатель и ожидаемую запись.

Значение expectedDigest должно происходить из сохранённого выпуска, а не вычисляться чтением того же getter внутри подготовки. Иначе функция снова превратится в сравнение результата с самим собой. Происхождение ожидания относится к паспорту снимка и требует явного объяснения.

Изменяем только проверяющую копию

В обычной fork-проверке прикладные Solidity-вызовы исполняются в копии среды, а данные исходного узла используются для чтения состояния. Такой вызов не становится автоматически отправленной RPC-транзакцией. Для отдельной отправки потребовался бы другой процесс, который не входит в этот сценарий.

Можно добавить функцию, которая моделирует издателя через prank, отключает курс в копии и сравнивает (expectedDigest, false). Она исследует дальнейший переход из сохранённого состояния. Адрес издателя не становится реальным подписантом исходной сети из-за локальной подмены контекста.

Не используем режим broadcast или настоящий приватный ключ. Наш предмет — совместимость известного состояния с дальнейшим локальным поведением. Это полезная граница для безопасного повторения сценария, но не доказательство работоспособности реальной процедуры размещения.

Отделяем локальный снимок от production

Цепочка 31337 и endpoint на loopback относятся к учебной конфигурации. Однако одно число chain ID не доказывает уникальность истории: разные локальные узлы могут иметь такой же идентификатор. Поэтому нужны адрес, блок, его происхождение и сохранённый выпуск вместе.

Даже успешное будущее чтение такого fork не подтверждает production-адрес, финальность публичной сети, настройки RPC-провайдера или доступность индексатора. Эти компоненты должны получить собственные условия исследования. Нельзя переносить вывод о локальном экземпляре на весь действующий сайт.

Если нужны несколько снимков, создаём их явно и сохраняем идентификаторы fork. Переключение между ними меняет наблюдаемое состояние. Модель, подготовленная для одного выпуска, не должна незаметно применяться к другому снимку с иной историей курса.

Обрабатываем отсутствие подготовки

При сохранении блока полезно отдельно записать его хеш и источник получения. Номер обозначает положение, но не уникальное содержание истории после перезапуска локального узла. Наш короткий Solidity-сценарий сравнивает прикладную запись, а паспорт должен дополнительно объяснять происхождение самого снимка. Для строгой сверки потребуется процедура проверки блока через выбранный RPC.

Если найдено несовпадение, сначала различаем три причины: другой снимок, другая конфигурация экземпляра и неправильный переход прикладной программы. Только последняя является дефектом исследуемой логики. Ясный паспорт позволяет не тратить время на исправление контракта, когда лаборатория просто обращается к перезапущенному узлу с новой историей.

Без необходимых переменных или доступного локального узла этот отдельный файл не имеет достаточного окружения для выполнения. Отчёт должен назвать отсутствие подготовки, а не замаскировать его успешным пропуском. Для обычных unit-сценариев такие внешние параметры не нужны, поэтому их сохраняем отдельно.

В итоговом комплекте fork-часть будет размещена в отдельном каталоге примеров, чтобы незаполненный снимок не мешал основной лаборатории. Читатель подключает её сознательно после подготовки узла. Сейчас написаны только условия и исходник; фактического блока или экземпляра не создано.

В следующем уроке разберём дерево вызовов и сравнение газа. Формы fork-API доступны в Vm 1.9.7, общий механизм — в руководстве Foundry. Оглавление курса.

Оглавление серии