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

Pausable в Solidity: пауза и восстановление публикаций

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

Создадим отдельный PausableEditorialCatalog. В нашей политике pause запрещает publish, но оставляет deactivate для редакционного отзыва. Чтение тоже сохраняется. Это конкретный выбор учебной модели, а не обязательное свойство всех pausable-контрактов.

Модификатор применяется явно

Наследование Pausable само по себе не остановит методы. Библиотека предоставляет состояние, getter и модификаторы, которые нужно добавить к выбранным функциям. Поэтому наличие импорта не доказывает, что реестр действительно остановится по нужному пути.

В модели есть две дополнительные роли. PAUSER_ROLE разрешает срочную остановку, RESUMER_ROLE разрешает возобновление. Мы разделили их, чтобы полномочие быстро прекратить записи не означало автоматического права снять ограничение. Администратор ролей всё равно остаётся важным субъектом, способным назначать участников.

Полный вариант реестра

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";

contract PausableEditorialCatalog is AccessControl, Pausable {
    error InvalidAccount();
    error InvalidCourse();
    error NotActive();
    bytes32 public constant PUBLISHER_ROLE = keccak256("PUBLISHER_ROLE");
    bytes32 public constant REVOKER_ROLE = keccak256("REVOKER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant RESUMER_ROLE = keccak256("RESUMER_ROLE");
    struct Course { bytes32 contentHash; bool active; }
    mapping(uint256 => Course) public courses;
    event Published(uint256 indexed id, bytes32 digest);
    event Deactivated(uint256 indexed id);

    constructor(address admin, address publisher_, address responder) {
        if (admin == address(0) || publisher_ == address(0)
            || responder == address(0)) revert InvalidAccount();
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(PUBLISHER_ROLE, publisher_);
        _grantRole(REVOKER_ROLE, responder);
        _grantRole(PAUSER_ROLE, responder);
        _grantRole(RESUMER_ROLE, admin);
    }
    function publish(uint256 id, bytes32 digest)
        external onlyRole(PUBLISHER_ROLE) whenNotPaused
    {
        if (id == 0 || digest == bytes32(0)) revert InvalidCourse();
        courses[id] = Course(digest, true);
        emit Published(id, digest);
    }
    function deactivate(uint256 id) external onlyRole(REVOKER_ROLE) {
        if (!courses[id].active) revert NotActive();
        courses[id].active = false;
        emit Deactivated(id);
    }
    function pause() external onlyRole(PAUSER_ROLE) { _pause(); }
    function resume() external onlyRole(RESUMER_ROLE) { _unpause(); }
}

Код использует OpenZeppelin Contracts 5.4.0 и Solidity 0.8.30. В конструкторе responder получает отзыв и остановку. Admin получает восстановление и административный граф. Publish сначала проверяет роль, затем отсутствие паузы. Если одновременно неверны роль и состояние, первая ошибка зависит от этого порядка.

Пауза не меняет выпуск

Пусть курс 17 содержит H1,true. После pause он по-прежнему содержит H1,true. Флаг paused относится к возможности метода publish, а active — к конкретному выпуску. Интерфейс должен показать оба значения, не превращая глобальную остановку публикаций в отзыв каждого материала.

Читатель может продолжать скачивать выпуск и проверять hash. Если responder отдельно выполнит deactivate(17), тогда active становится false. Это разрешено нашей политикой во время паузы. Такой маршрут полезен при ошибочном материале, но не является универсальной моделью финансового emergency-stop.

Состояние Читать курс Публиковать Отозвать активный выпуск
Не на паузе Да При нужной роли При нужной роли
На паузе Да Запрещено При нужной роли

Из таблицы видно, что сама пауза не ограничивает каждый возможный путь управления. GrantRole остаётся доступен по административному графу. Если требование включает запрет назначений, его нужно отдельно спроектировать. Нельзя считать весь контракт неподвижным только от применения whenNotPaused к одному методу.

Кто способен восстановить работу

Если responder остановил публикации и потерял ключ, admin по нашему распределению может вызвать resume. Если admin тоже недоступен, штатная возможность восстановления зависит от остальных назначений графа. Модель должна быть согласована заранее: иначе emergency-операция станет постоянной остановкой.

Возобновление не исправляет содержание автоматически. Перед ним нужно понять причину инцидента, заменить ошибочный редакционный инструмент и при необходимости отозвать материал. Контракт знает только флаг и роли. Серверная заметка об исправлении не выполняет эти действия сама.

Повтор pause при уже установленном состоянии отклоняется по правилам Pausable; повтор resume при отсутствии паузы тоже имеет свой отказ. Поэтому неизвестный результат после отправки сначала восстанавливают чтением paused и сведений действия. Бесконечный повтор не является безопасной процедурой восстановления интерфейса.

Транзакции между намерением и остановкой

Пауза начинает действовать после исполнения. Уже подготовленная публикация может попасть раньше или позже неё. Если раньше — она выполнится при допустимых условиях. Если позже — модификатор остановит её. Отправка команды pause из браузера не мгновенно замораживает очередь всей сети.

Также pause не возвращает старый hash и не отменяет предыдущие события. Для исправления записи нужна отдельная разрешённая операция после выбранного состояния. Отчёт должен сохранять порядок и результат, а не утверждать, что все ошибочные публикации исчезли от одного переключения.

Для упражнения возьмите последовательность H1, pause, попытка H2, deactivate, resume, H3. Ожидаемая карта показывает, какие шаги меняют выпуск и какие меняют доступность метода. Такое разделение помогает будущему интерфейсу честно объяснять остановку редакции. Исходник Pausable 5.4.0 содержит точные проверки состояния.

Возобновление после исправления клиента

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

Контракт не проверяет качество исправленного клиента. Он видит адрес и флаг. Поэтому принятие решения resume относится к процедуре организации, а технический метод отражает её результат. Не стоит называть successful resume доказательством устранения причины инцидента.

При компрометации publisher одним pause может быть недостаточно. Пока публикации остановлены, соответствующий администратор должен решить вопрос роли. Если просто снять паузу, прежний скомпрометированный субъект снова получает возможность действия. Карта полномочий и глобальная доступность метода работают совместно.

Теперь изменим политику: допустим, deactivate тоже получил whenNotPaused. Во время паузы responder уже не может отозвать ошибочный выпуск этим методом. Это может быть сознательным требованием другой системы, но не соответствует нашей выбранной цели. Приём emergency-stop должен описывать каждую остановленную операцию.

Упражнение помогает увидеть ещё один слой: курс остаётся читаемым и активным по записи, публикация заблокирована, отзыв разрешён responder. Для интерфейса это три независимых сообщения. Одна красная плашка «сайт остановлен» скроет полезный архив и не объяснит, какие действия действительно недоступны.

В отчёте сохраните момент принятой паузы и связанные события. Если таймаут произошёл до результата, статус остаётся неизвестным. Проверка getter paused восстанавливает текущее состояние, но связь с конкретной отправленной попыткой уточняется отдельно по истории. Один индексированный административный отчёт должен относиться к выбранному экземпляру; пауза другого реестра не объясняет ограничение этого.

Для самостоятельного уточнения: Pausable.sol (5.4.0).

Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.

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