Выдача записи и события контракта
Оглавление серии «Реестр» · Урок 3 из 10.
Реестр умеет читать структуру по хешу, но все ключи пока отсутствуют. Добавим выдачу сертификата владельцем контракта. Один идентификатор можно будет зарегистрировать только один раз, нулевой хеш запрещён. После успешной записи контракт создаст событие Issued. Ожидаемый результат — у известного хеша появляется издатель и время регистрации, а повторный вызов завершается понятной ошибкой. В этом уроке разбираем исходник; отправку в локальную сеть рассмотрим после модели отзыва.
Операция выдачи не получает PDF, имя человека или готовую цену газа. Её единственный предметный аргумент — bytes32 digest, рассчитанный ранее из файла. Издатель определяется по отправителю, а время — по текущему блоку. Это сокращает возможность случайно подставить чужой адрес в поле записи. Но сама доверенность отправителя зависит от проверки полномочий внутри функции. Открытая функция не становится безопасной только потому, что её вызывают из аккуратной веб-формы.
Правило проверяется контрактом
На этом шаге выдавать может только owner. В начале функции сравниваем msg.sender с сохранённым владельцем. При несовпадении вызываем пользовательскую ошибку NotAuthorized. Это правило выполняется для любого обращения к контракту: из нашего интерфейса, другого сайта или команды RPC. Скрытая кнопка в браузере была бы лишь подсказкой интерфейса. Настоящее ограничение должно находиться там, где хранится и изменяется состояние, иначе обход страницы позволил бы выдать запись постороннему адресу.
Затем проверяем ненулевой хеш. Нулевое значение технически является допустимым bytes32, но в нашем протоколе зарезервировано для ошибочного ввода. Нормальная функция SHA-256 возвращает произвольные 32 байта; запрет одного значения является явной политикой реестра, а не утверждением о невозможности такого результата у алгоритма. Политику удобно сделать понятной и одинаковой в контракте и браузерной проверке. Интерфейс позднее отвергнет такой ввод раньше запроса, но контракт сохраняет собственную защиту.
Полный контракт с выдачей
Ниже полный CertificateRegistry.sol третьего состояния. Чтение и конструктор остаются прежними. Новые участники — событие и функция issue. Все проверки стоят перед изменением 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();
event Issued(bytes32 indexed digest, address indexed issuer, uint64 issuedAt);
constructor() { owner = msg.sender; }
function get(bytes32 digest) external view returns (Certificate memory) {
return certificates[digest];
}
function issue(bytes32 digest) external {
if (msg.sender != owner) 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);
}
}
Наличие записи определяем по issuer != address(0). Если ключ уже зарегистрирован, вызываем AlreadyIssued. Пока запись нельзя ни заменить, ни удалить, поэтому одному документу соответствует первоначальный издатель. Такая модель проста, но выбор не универсален. Если один и тот же файл должен заверяться несколькими организациями, ключом могла бы стать пара издатель-хеш или отдельный идентификатор выдачи. Сначала нужно назвать предметную сущность, затем проектировать mapping под её уникальность.
Время регистрации и арифметика
block.timestamp приходит из контекста блока. Преобразуем его в uint64, совпадающий с полем структуры. Диапазон выбранного типа намного шире обычных временных горизонтов учебного приложения, но явное сужение типа в Solidity всё равно требует понимания. Не следует выводить общее правило, что любые преобразования больших чисел безопасны. Здесь заранее определено поле для времени в секундах и ограниченный профиль, а не произвольная финансовая арифметика с неизвестными границами значений.
Отметка блока не гарантирует точного совпадения с настенными часами пользователя. Её пригодность зависит от требований приложения и свойств цепочки. В локальной сети время можно изменять средствами лаборатории; реестр не становится независимым источником реального календарного события только из-за вызова block.timestamp. В дальнейшем веб-интерфейс должен подписывать это поле как время регистрации в цепочке. Для даты окончания курса или подписания документа потребуется другая явно определённая информация.
Событие описывает случившуюся запись
Issued содержит два indexed-поля: хеш и издателя. Они помогают искать соответствующие события по темам журнала. Третье поле, время, остаётся в данных события. Это не замена mapping: журнал полезен для истории и поиска, а get — для актуального состояния конкретного сертификата. Позднее появится отзыв, и старое событие выдачи останется в истории даже после изменения признака. Интерфейс не должен считать найденное Issued достаточным доказательством текущей активности.
При revert изменения вызова откатываются, включая созданные в нём журналы. Поэтому событие успешной выдачи следует ожидать только после выполнения всех проверок и записи. Нет смысла предварительно показывать его как полученный результат сразу после нажатия кнопки. Браузер узнает, что отправлена транзакция, отдельно от знания об успешном включении в блок. Этот порядок разберём в уроке о симуляции, подписи и подтверждении операции.
События доступны внешним наблюдателям публичной цепочки. Хеш документа и адрес издателя не становятся скрытыми из-за того, что само PDF не опубликовано. В нашей локальной лаборатории сведения остаются внутри учебной сети, но структура отражает публичную модель EVM. Поэтому в примерах нет реальных сертификатов учащихся. Для настоящей системы необходимо отдельно оценить, допустима ли такая связь идентификатора документа и публичного адреса, и какие данные вообще должны попадать в журнал.
Ошибки являются частью интерфейса
Пользовательские ошибки дают ABI различимые причины отказа. Браузерная библиотека сможет декодировать NotAuthorized, InvalidDigest и AlreadyIssued, если соответствующие определения присутствуют в ABI. Сам Solidity-исходник не превращается автоматически в браузерный контракт вызова. После изменения функций и ошибок нужно согласовать ABI и адрес развёрнутого кода. Иначе браузер может показать общую ошибку там, где контракт намеренно предоставил конкретный результат проверки.
Нельзя исправлять повторную выдачу отправкой случайного нового хеша. Хеш относится к байтам документа, а не служит произвольным уникальным номером для обхода ограничения. Если документ действительно перевыпущен и его байты изменились, понадобится новая запись и ясное правило связи со старой. В нашей серии отзыв обозначит прекращение активности прежней записи. Замена не будет стирать факт первоначальной выдачи, что сохранит объяснимую историю изменений.
Ожидаемые случаи
При самостоятельном выполнении владелец должен успешно выдать ненулевой неизвестный хеш. Повтор с тем же хешем должен вернуть AlreadyIssued. Другой адрес должен получить NotAuthorized ещё до записи. Нулевой идентификатор должен вернуть InvalidDigest. Это ожидаемые ветви исходника, а не отчёт о запуске: компиляция и транзакции при написании не выполнялись. Механизмы описаны в ошибках Solidity и событиях контрактов. Следующий шаг введёт нескольких издателей и отзыв, сохранив первоначальные данные сертификата.
Исходники этого шага
Архив серии содержит полную папку lesson-03. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.