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

Первый контракт и постоянное хранилище

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

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

Закрепим Solidity 0.8.30 и Foundry 1.3.1. Это опубликованные стабильные версии выбранной лаборатории, а не заявление о самых новых выпусках. Строка pragma solidity 0.8.30 требует конкретный компилятор, а foundry.toml задаёт ту же версию и целевую EVM Cancun. Согласование исходника и конфигурации помогает избежать ситуации, когда пример незаметно компилируется другой версией и читатель пытается понять отличия результатов по одному фрагменту кода.

Контракт как отдельный объект состояния

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

Структура Certificate содержит issuer, issuedAt и revoked. address представляет публичный адрес в EVM. uint64 подходит для учебной отметки времени в секундах; отдельно обсудим, что её источником станет время блока. bool хранит признак отзыва. Такой набор не утверждает знания учащегося и не хранит его имя. Поля структуры выражают именно ограниченную модель предыдущего урока, поэтому интерфейс не должен позже показывать больше доказательств, чем содержат данные.

Полный исходник состояния чтения

Файл src/CertificateRegistry.sol ниже является полным. SPDX-комментарий обозначает лицензию авторского учебного исходника. В структуре нет поля хеша: оно станет ключом отображения, поэтому дублировать его внутри значения не требуется для текущих операций. Несколько пользовательских ошибок объявлены заранее для следующих действий. Их наличие само по себе не выполняет проверку: ошибка должна быть явно вызвана в соответствующей ветке функции.

// 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();
    constructor() { owner = msg.sender; }
    function get(bytes32 digest) external view returns (Certificate memory) {
        return certificates[digest];
    }
}

mapping(bytes32 => Certificate) связывает идентификатор с записью. Mapping не является готовым списком всех выданных сертификатов. Он позволяет обратиться к значению по известному ключу, но не хранит удобную перечисляемую коллекцию ключей. Если продукту понадобится список, потребуется отдельный индекс, например по событиям, либо дополнительная структура. В этом курсе чтение отдельного сертификата и изучение истории будут разделены, чтобы не принять поиск ключа за автоматическое перечисление всего реестра.

Нулевое значение и отсутствие записи

Для ещё не записанного ключа mapping возвращает значение по умолчанию: нулевой адрес, нулевое время и false. Признак revoked == false поэтому сам по себе не означает действительный сертификат. Сначала нужно установить, что запись существует. В нашей модели это ненулевой issuer, поскольку выдача не позволит записать сертификат с отсутствующим издателем. Такой порядок проверки позднее сохранит веб-интерфейс: сначала наличие, затем статус и доверие к адресу.

Это важное отличие от словаря, который может возвращать специальное отсутствие значения. Здесь структура существует как результат чтения даже без отдельной записи. Если интерфейс покажет любой false в поле отзыва как «сертификат действителен», он ошибётся для всех неизвестных хешей. Разберите семантику нулевых значений на уровне контракта, прежде чем добавлять красивые подписи. В иной схеме можно хранить отдельный признак существования, но тогда он тоже должен иметь однозначные правила изменения.

Доступ к хранилищу через getter

Mapping объявлен private, а функция get предоставляет явное чтение. private ограничивает обращение из Solidity-кода, но не обещает секретность сведений в публичной цепочке. Хранилище и транзакции могут быть доступны внешним наблюдателям. Именно поэтому персональные документы не положены туда изначально. Не нужно пытаться сделать конфиденциальный реестр одним модификатором доступа: модель размещения данных и модель вызова функций решают разные задачи.

external view означает, что функция доступна внешнему вызывающему коду и не меняет состояние контракта. Возвращаемая структура имеет размещение memory: создаётся копия для результата. Изменение такой копии не будет менять исходный mapping. Позднее при отзыве потребуется storage-ссылка на реальную запись. Различие места хранения важно именно в момент изменения: одинаково выглядящий доступ к полям может относиться к временной копии или к постоянному состоянию.

Владелец устанавливается один раз

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

immutable фиксирует значение после конструктора. Здесь нет функции смены владельца. Это упрощает пример, но создаёт важное ограничение: потеря доступа к владельцу не решается кнопкой восстановления контракта. В учебной сети можно начать состояние заново, а в настоящем продукте пришлось бы заранее проектировать администрирование, резервирование полномочий и процедуру восстановления. Не следует переносить эту минимальную модель на ценные документы без отдельного анализа требований и аудита реализации.

Конфигурация компилятора

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

[profile.default]
src = "src"
out = "out"
solc = "0.8.30"
evm_version = "cancun"
optimizer = true
optimizer_runs = 200

При самостоятельном изучении проследите вызов get для любого ненулевого ключа до появления функции выдачи. Ожидается пустая структура, потому что в этом состоянии нет операции записи. Это полезнее искусственно подставленных результатов: связь между кодом и состоянием видна непосредственно. Устройство mapping и размещения данных описано в типах Solidity 0.8.30, конструкторы — в структуре контракта. Следующий урок добавит единственную операцию выдачи и событие, которое будет обозначать успешную регистрацию нового идентификатора.

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

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