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

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. Небольшая схема определяет обязательные сведения и собственную версию. При обновлении приложения старая запись либо интерпретируется по известной схеме, либо честно объявляется несовместимой. Потеря способности прочитать старый кеш не меняет фактический выпуск в сети.

Оглавление серии