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

Отзыв сертификата и модель полномочий

Оглавление серии «Реестр» · Урок 4 из 10.

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

Предположим, у учебной организации есть редактор документов и руководитель. Руководитель владеет контрактом, редактору разрешена выдача. Позднее редактор больше не работает в организации. Запрет новой выдачи не обязан автоматически отзывать все прежние документы: они могли быть корректными. Однако организация может отдельно отозвать ошибочный документ. Именно эту модель реализуем. Она является выбором лаборатории; другие требования потребуют другой схемы полномочий, а не простого переименования кнопки.

Разрешение адресам

Добавим mapping(address => bool) public issuers. В конструкторе разрешаем выдачу владельцу. Функция setIssuer доступна только owner, принимает адрес и новый признак. Нулевой адрес запрещён. После изменения создаётся событие IssuerChanged, которое обозначает новую политику для будущих вызовов. В отличие от записи сертификата, значение этого mapping может меняться: сегодня адрес разрешён, завтра запрещён. Исходная запись сертификата от этого не переписывает собственного издателя.

Функция issue теперь проверяет issuers[msg.sender], а не только равенство владельцу. Остальные ограничения сохраняются. Один известный хеш нельзя зарегистрировать повторно даже после отзыва. Это помогает не скрывать историю выдачи за заменой содержимого того же ключа. Если продукту нужны переиздания, следует добавить версионирование или отдельные идентификаторы. Запрет перезаписи является предметным правилом, а не обязательным свойством Solidity mapping: технически изменить значение можно, но наша функция этого не допускает.

Полный контракт полномочий и отзыва

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

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

contract CertificateRegistry {
    struct Certificate { address issuer; uint64 issuedAt; bool revoked; }
    address public immutable owner;
    mapping(bytes32 => Certificate) private certificates;
    error NotAuthorized();
    error InvalidDigest();
    error AlreadyIssued();
    error NotFound();
    error AlreadyRevoked();
    mapping(address => bool) public issuers;
    event IssuerChanged(address indexed issuer, bool allowed);
    event Issued(bytes32 indexed digest, address indexed issuer, uint64 issuedAt);
    event Revoked(bytes32 indexed digest, address indexed actor);
    constructor() { owner = msg.sender; issuers[msg.sender] = true; }
    function get(bytes32 digest) external view returns (Certificate memory) {
        return certificates[digest];
    }
    function issue(bytes32 digest) external {
        if (!issuers[msg.sender]) revert NotAuthorized();
        if (digest == bytes32(0)) revert InvalidDigest();
        if (certificates[digest].issuer != address(0)) revert AlreadyIssued();
        uint64 timestamp = uint64(block.timestamp);
        certificates[digest] = Certificate(msg.sender, timestamp, false);
        emit Issued(digest, msg.sender, timestamp);
    }
    function setIssuer(address issuer, bool allowed) external {
        if (msg.sender != owner) revert NotAuthorized();
        if (issuer == address(0)) revert NotAuthorized();
        issuers[issuer] = allowed;
        emit IssuerChanged(issuer, allowed);
    }
    function revoke(bytes32 digest) external {
        Certificate storage item = certificates[digest];
        if (item.issuer == address(0)) revert NotFound();
        if (msg.sender != owner && msg.sender != item.issuer) revert NotAuthorized();
        if (item.revoked) revert AlreadyRevoked();
        item.revoked = true;
        emit Revoked(digest, msg.sender);
    }
}

setIssuer не включена в веб-форму курса. Управление адресами читатель может изучать владельцем через инструменты локальной лаборатории. Публичный ABI позднее будет содержать функцию, но наличие определения ABI не создаёт кнопку и не отменяет проверку контракта. Это полезное различие: функциональность на уровне цепочки шире учебного пользовательского интерфейса. В реальном административном кабинете потребуется отдельно показывать, какой аккаунт выполняет действие и каков его ожидаемый эффект.

Storage-ссылка меняет запись

В revoke получаем Certificate storage item. Это ссылка на существующее значение в постоянном хранилище. Присваивание item.revoked = true меняет именно mapping. Если бы вместо этого использовали memory, изменилась бы копия, а чтение следующего вызова увидело прежнее значение. Такой случай особенно неприятен потому, что отдельные строки выглядят правдоподобно. Размещению данных нужно уделять внимание там, где функция обещает изменение, а не только при объявлении структуры.

Сначала проверяем наличие записи по ненулевому издателю. Затем сравниваем отправителя с владельцем и первоначальным издателем. Только эти адреса могут выполнить отзыв. После этого проверяем, не отозван ли документ раньше. Повтор даёт AlreadyRevoked, а неизвестный хеш — NotFound. Различимые ошибки помогают интерфейсу объяснить, почему операция невозможна, но правило остаётся в контракте и действует независимо от качества браузерной формы.

Издатель может отозвать свою запись даже после снятия разрешения новой выдачи. Так сформулировано условие функции: оно смотрит на первоначального издателя, а не на текущее issuers. Это сознательная особенность модели. Можно выбрать иной порядок, разрешив отзыв только действующим издателям, но тогда бывший издатель потеряет возможность исправить свою ошибку. Организация должна определить желаемое поведение; нельзя случайно получить такую политику из повторного использования первой удобной проверки.

Событие и актуальное значение

После изменения создаётся Revoked с хешем и адресом действующего лица. Первоначальный issuer в записи сохраняется. Это позволяет отличить того, кто выдал документ, от того, кто позднее отозвал его от имени владельца. В журнале остаются оба действия. Само время отзыва в структуре пока не хранится; его можно связать с блоком события. Если оно нужно как самостоятельное поле предметных данных, следует добавить его явно и согласовать ABI.

История выдачи не превращает отозванный документ в действительный. Проверка должна сначала читать текущее значение get, затем учитывать доверие к издателю и признак revoked. Журналы помогают объяснить происхождение изменения. Они не заменяют актуальный getter при принятии решения. В дальнейшем покажем оба пути из браузера: историю в ограниченном диапазоне блоков и отдельное чтение состояния, чтобы избежать ошибки «нашли выдачу, значит всё хорошо».

Ограничения владельца

Владелец хранится как immutable, поэтому передать его невозможно. Даже если владелец запретит собственную выдачу в issuers, он сохранит административные функции по сравнению с owner. Это разные полномочия. Такая структура может быть неудобной для настоящей организации, где административная учётная запись должна сменяться. В лаборатории простота позволяет изучить связи без дополнительного протокола передачи. Для будущего продукта потребуется отдельно спроектировать смену владельца и действия при потере доступа.

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

Ожидаемые изменения

Самостоятельно рассмотрите последовательность: владелец разрешил адрес B, B выдал хеш, владелец снял разрешение B. Новая выдача B должна быть запрещена, прежняя запись должна остаться активной. После отдельного отзыва владельцем get должен сохранить адрес B и показать revoked == true. Неизвестный адрес C не сможет отозвать запись. Это ожидаемое поведение исходника; выполнение в процессе написания не проводилось.

Различия storage и memory описаны в размещении данных Solidity, правила контекста отправителя — в специальных переменных. Завершили предметную модель контракта. Следующий урок не добавит очередное поле: он свяжет исходник с локальной EVM, адресом и блоком развёртывания, чтобы браузер позднее обращался к конкретному экземпляру, а не к абстрактному имени файла.

Исходники этого шага

Архив серии содержит полную папку lesson-04. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.