Версии контракта Solidity: реестр CourseCatalogV2
Базовый реестр хранит только текущий выпуск. Историю можно восстановить по событиям, но другой контракт не читает журнал так же, как обычное поле. Если приложению требуется обращаться к определённой прошлой версии на цепочке, нужно явно добавить модель версий.
Создадим отдельный CourseCatalogV2. Это новый тип и новый экземпляр, а не скрытая правка исходного CourseCatalog. Прежний курс Foundry продолжает проверять базовый контракт. Новая модель сохраняет идею издателя и текущего указателя, но дополнительно нумерует каждый успешный выпуск.
Текущая запись и постоянные выпуски
Для номера 17 первое объявление получает revision 1, второе — 2. Отпечаток конкретной версии затем не заменяется. Текущая запись указывает на последний опубликованный hash и активность. При отключении текущий hash и версия сохраняются, а активность становится false.
publish(17,H1): revision=1, releases[17][1]=H1, current=H1,true
publish(17,H2): revision=2, releases[17][2]=H2, current=H2,true
deactivate(17): revision=2, current=H2,false
releases[17][1] по-прежнему H1
Теперь у номера курса и номера выпуска разные обязанности. courseId=17 стабилен, revision=2 выбирает одну историческую запись. Нельзя присвоить номер 2 случайному файлу и назвать его версией реестра: он возникает только через успешную публикацию в этой модели.
Полный самостоятельный контракт
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
contract CourseCatalogV2 {
error Unauthorized();
error InvalidCourse();
error NotActive();
struct Course { bytes32 contentHash; bool active; }
address public immutable publisher;
mapping(uint256 => Course) public courses;
mapping(uint256 => uint256) public revision;
mapping(uint256 => mapping(uint256 => bytes32)) public releases;
event PublishedV2(
uint256 indexed courseId,
uint256 indexed revision,
bytes32 contentHash
);
event DeactivatedV2(uint256 indexed courseId, uint256 revision);
constructor(address publisher_) {
if (publisher_ == address(0)) revert Unauthorized();
publisher = publisher_;
}
modifier onlyPublisher() {
if (msg.sender != publisher) revert Unauthorized();
_;
}
function publish(uint256 id, bytes32 digest) external onlyPublisher {
if (id == 0 || digest == bytes32(0)) revert InvalidCourse();
uint256 next = revision[id] + 1;
revision[id] = next;
releases[id][next] = digest;
courses[id] = Course(digest, true);
emit PublishedV2(id, next, digest);
}
function deactivate(uint256 id) external onlyPublisher {
if (!courses[id].active) revert NotActive();
courses[id].active = false;
emit DeactivatedV2(id, revision[id]);
}
}
Вложенный mapping выбирает сначала курс, затем его версию. Только метод publish пишет новый next, поэтому ранее использованный номер версии не присваивается заново через доступный интерфейс. Чтение неизвестной пары даёт нулевой hash, и мы снова используем ненулевой отпечаток как признак объявленного выпуска.
Повтор того же файла тоже является публикацией
Если издатель снова передаст H1, номер версии всё равно увеличится. В этом договоре revision нумерует успешные объявления, а не уникальные файлы. Это решение сохраняет простую последовательность событий. Если требуются только уникальные выпуски, проверка повторов станет дополнительным правилом.
Следовательно, фразы «вторая версия файла» и «вторая публикация» могут обозначать разные вещи. Для интерфейса выберем точную подпись «публикация №2» либо явно определим редакционное значение revision. Не следует выводить уникальность содержимого из монотонного номера.
Повтор после деактивации создаёт новую публикацию и активирует текущий указатель. Предыдущий номер остаётся доступен. Ручной отзыв не удаляет историю и не меняет прошлый hash. Если требуются причины отзыва каждой исторической версии, понадобится отдельный статус, которого здесь нет.
Ограничения нового состояния
Журнал растёт с каждой публикацией. Одиночный publish не проходит по всей истории, поэтому число прежних выпусков не создаёт линейный цикл. Но общее постоянное состояние увеличивается, и это нужно учитывать в требованиях хранения. Добавлять такой журнал стоит для реальной необходимости on-chain-чтения.
Мы также не создали функцию «все версии сразу». Клиент знает текущий revision и может читать отдельные номера либо ограниченные диапазоны через дополнительный интерфейс. Если понадобятся тысячи выпусков одного курса, пакетный getter требует своего предела, как описано ранее.
Счётчик использует проверяемую арифметику Solidity 0.8.30. При недостижимом для обычной библиотеки переполнении вызов откатился бы, а не вернулся к нулю. Сам по себе огромный диапазон не повод отключать проверку через unchecked: это не нужная оптимизация нашего договора.
Связь с внешним манифестом
Манифест выпуска может содержать courseId и заявленный редакционный номер. Однако значение revision в контракте назначается по успешной последовательности, а не доверяет любому JSON. Если редактор заранее подготовил файл с ожиданием revision 2, другой выпуск может изменить очередной номер. Нужно либо согласовать процедуру публикации, либо отдельно ввести сравнение ожидаемого состояния.
В текущем примере манифест содержит собственный releaseLabel, а контрактный revision сохраняется в записи опубликованного объявления после результата. Так мы не выдаём предварительное ожидание за известный номер цепочки. Другой вариант — метод compare-and-set, который требует ожидаемую прежнюю версию, но это новая функция с новым сценарием отказа.
| Утверждение | Базовый реестр | CourseCatalogV2 |
|---|---|---|
| Чтение текущего hash | Есть | Есть |
| Неизменяемый издатель | Есть | Есть |
| On-chain getter прошлой версии | Нет | Есть |
| История уникальных файлов | Не обещана | Не обещана |
| Отзыв удаляет прежний hash | Нет | Нет |
Новый экземпляр потребует своей записи развёртывания и подходящего ABI событий PublishedV2. Сайт не должен загрузить V2-ABI по старому адресу и рассчитывать, что состояние само мигрировало. Переход между реестрами относится к отдельной процедуре выпуска, а не к добавлению номера в карточку.
Мы получили явное расширение, где текущий указатель и постоянная история разделены. Это позволяет развивать библиотеку, сохраняя точность базовой модели. Вложенные mapping описывают форму использованного состояния.
Неизменность версии и доверие к издателю
В этой программе прежний releases[id][revision] не переписывается доступными методами. Это свойство интерфейса конкретного обычного экземпляра. Оно не сообщает, что прежний текст был качественным или что сайт обязательно продолжает хранить его файл. Неизменяемая запись и доступность внешнего артефакта остаются отдельными условиями.
Издатель способен создать новую revision с исправленным hash. Читатель, который выбирает определённую прежнюю пару, продолжает видеть прежний hash. Читатель текущего курса видит последнее объявление. Такой выбор позволяет одновременно поддерживать архивные ссылки и развитие материалов, не смешивая два маршрута чтения.
Если понадобится отмечать ошибки старых выпусков, нельзя изменить сам сохранённый hash и назвать это отзывом. Можно добавить отдельное состояние статуса или внешнюю редакционную заметку. Тогда неизменность артефакта и актуальное предупреждение остаются разными данными. Их следует показывать вместе, чтобы архив не выглядел действующим только благодаря неизменному значению.
Рост истории также требует политики вне цепочки: где хранятся артефакты каждого номера, как проверяются резервные копии и какой манифест связывает их с объявлениями. Контрактный журнал не заменяет такую эксплуатацию. Наличие тысячи hash без файлов не создаёт доступную тысячу уроков.
Для учебного разбора сравните два равных hash на revisions 1 и 2. Это два объявления одного файла, а не два разных байтовых выпуска. Если приложению нужна уникальность содержимого, её договор придётся дополнить отдельным условием и способом обработки повторов.
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.