Web3: согласованное чтение состояния на одном блоке
Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 5 из 30.
Карточка курса часто объединяет несколько ответов: данные выпуска, адрес издателя, номер блока и время наблюдения. Если каждый ответ относится к отдельному latest, между запросами может появиться новая публикация. В этом уроке построим согласованный снимок и обозначим его происхождение. Результат — интерфейс знает, к какому блоку относится показанное состояние.
Используем publicClient, catalogAbi и конфигурацию экземпляра из предыдущих уроков. У реестра остаются только базовые методы; никакой функции снимка в контракт не добавляется. Согласование выполняется параметрами клиентских чтений. Пример не запускался, а приведённые переходы описывают ожидаемую работу в локальной лаборатории.
Почему номер блока важен
Допустим, первый запрос читает активный выпуск A. Затем издатель вызывает publish(17, B). Второй запрос получает номер уже нового блока. Если карточка соединяет выпуск A с новым номером, она неправильно описывает происхождение данных. Сам контракт работал правильно; ошибку внесло приложение при соединении независимых наблюдений.
Для некоторых полей такая гонка незаметна. publisher в базовом контракте неизменяем, поэтому его чтение из двух соседних блоков даёт одинаковый адрес. Но это свойство конкретного поля не отменяет проблему для courses. Архитектура снимка должна оставаться понятной, если в следующем проекте появятся изменяемые роли, цена или несколько связанных записей.
Выберем номер блока один раз и передадим его всем запросам состояния. Тогда они описывают один уровень цепочки. Дополнительно получим хеш этого блока: при реорганизации номер может остаться прежним, а блок и его состояние — измениться. Номер нужен для порядка, хеш — для различения наблюдаемого блока.
Модуль согласованного чтения
Код ниже заменяет функцию readCourse более подробной функцией readSnapshot. Параметр address уже нормализован и относится к выбранной локальной конфигурации.
import { publicClient } from './client.js';
import { catalogAbi } from './catalog-abi.js';
export async function readSnapshot(address, courseId = 17n) {
const blockNumber = await publicClient.getBlockNumber();
const block = await publicClient.getBlock({ blockNumber });
const [publisher, values] = await Promise.all([
publicClient.readContract({
address, abi: catalogAbi, functionName: 'publisher', blockNumber,
}),
publicClient.readContract({
address, abi: catalogAbi, functionName: 'courses',
args: [courseId], blockNumber,
}),
]);
const after = await publicClient.getBlock({ blockNumber });
if (after.hash !== block.hash) throw new Error('Блок снимка изменился');
return {
courseId,
publisher,
contentHash: values[0],
active: values[1],
blockNumber,
blockHash: block.hash,
observedAt: new Date().toISOString(),
};
}
Независимые чтения запускаются параллельно, но используют одинаковый номер блока. Повторное чтение заголовка обнаруживает очевидную смену блока за время запроса. Это учебная защита от гонки, а не доказательство честности удалённого RPC. Endpoint может вернуть противоречивые или ложные ответы; более сильная модель требует собственного узла или проверки доказательств состояния.
Есть и тонкая граница: запрос по номеру не является атомарным обещанием одного неизменного блока на всех удалённых серверах. Смена ветви может произойти в промежутке между ответами. В сложном продукте рассматривают запросы по хешу блока и поддержку соответствующего стандарта RPC, а также политику повторного чтения. Здесь не утверждаем, что небольшой модуль решает все варианты распределённой согласованности.
Время наблюдения и время блока
observedAt — время браузера, когда сформирован объект. Оно удобно для подписи «прочитано недавно», но зависит от часов устройства. Время блока относится к данным сети и имеет другое назначение. Ни одно из них само по себе не сообщает, что выпуск является последним на момент, когда человек начал читать статью.
В интерфейсе можно показать «состояние на блоке N» и кнопку обновления. Автоматическое обновление полезно, если его частота не создаёт лишней нагрузки. Уведомление о новом блоке означает повод запросить свежий снимок, а не необходимость бесконечно перерисовывать карточку. Во время редактирования формы новый снимок должен сохранять введённый пользователем текст и отдельно сообщать о смене выпуска.
Локальная лаборатория может оставаться на одном номере блока долгое время. Это не обязательно проблема: блоки там создаются управляемым действием. Поэтому условие «последний блок старше некоторого времени» не следует превращать в универсальную ошибку любой сети. Политика свежести определяется окружением и задачей.
Отсутствие исторического состояния
Не всякий RPC способен читать произвольные старые блоки. Endpoint может ограничивать историю или не хранить необходимое состояние. Ошибка исторического чтения отличается от отсутствующей записи курса: невозможно получить нужный снимок, а не подтверждено отсутствие публикации. Интерфейс должен назвать это ограничение, особенно если пользователь открыл сохранённую ссылку на старый выпуск.
Для текущей карточки обычно нужен недавний блок, но история выпусков строится другим способом — по событиям и индексатору. Не стоит пытаться восстановить все прошлые изменения тысячами случайных getter-вызовов. Снимок описывает состояние на выбранном уровне, а журнал событий объясняет, как оно туда пришло.
Гонка с пользовательским намерением
Предположим, издатель готовил деактивацию выпуска A, а свежий снимок уже показывает выпуск B. Базовый deactivate(17) не принимает ожидаемый отпечаток и деактивирует текущую активную запись. Значит, интерфейс должен предупредить о смене состояния и запросить новое согласие. Одного повторного моделирования может быть недостаточно для понимания человеком: моделирование успешно, но смысл действия поменялся.
Если бизнес-процесс требует строгой привязки к выпуску, контракту понадобится отдельный метод с ожидаемой редакцией или отпечатком. Такой метод нельзя молча приписать нашему базовому ABI. В этой серии честно показываем ограничение исходного контракта и принимаем явное новое подтверждение пользователя при смене наблюдения.
Разберите последовательность «прочитан блок — публикация — завершено чтение — отправлена деактивация». Запишите, какие данные устаревают и что должно быть показано издателю. Ожидаемый результат — вы умеете связывать наблюдение со временем и не выдаёте кеш за вечную истину. Параметры чтения блока приведены в getBlock и readContract.
Снимок и действие в одной вкладке
Сохранённый снимок полезен как основание экрана подтверждения. Он показывает, какие сведения видел человек, когда сформировал намерение. Если после отправки выяснится, что выпуск изменился, журнал сможет различить ошибку чтения и изменение между наблюдением и исполнением. Для такого расследования сохраняется хеш блока, а не только время на часах браузера.
Не следует сохранять в истории весь ответ узла без нормализации: он может содержать поля, которые не нужны приложению и меняются между версиями endpoint. Небольшая схема определяет обязательные сведения и собственную версию. При обновлении приложения старая запись либо интерпретируется по известной схеме, либо честно объявляется несовместимой. Потеря способности прочитать старый кеш не меняет фактический выпуск в сети.