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

ERC-4337 EntryPoint: проверка и исполнение операции

Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 22 из 30.

Теперь у нас есть явное намерение опубликовать курс 17 от имени смарт-аккаунта. Чтобы оно стало изменением реестра, необходимо пройти проверку аккаунта и выполнить целевой вызов. В этом уроке разделим эти два этапа и построим учебную модель проверки. Она поможет обнаружить опасную ошибку: разрешение на подпись сообщения нельзя автоматически считать разрешением на выполнение произвольного вызова.

Модель написана для обсуждения состояния и не запускалась. Это не реализация IAccount и не готовый контракт EntryPoint. Профиль ERC-4337 ещё не связан с реальной инфраструктурой. Вместо непроверенного кошелька создадим небольшую чистую функцию, которая показывает договор между проверкой разрешения и исполнением библиотеки.

Точка входа и доверенный вызывающий

В механизме ERC-4337 специальный контракт EntryPoint организует обработку операций. Аккаунт должен отличать разрешённого вызывающего проверки от произвольного адреса. Иначе внешний участник сможет обратиться к служебному пути, который приложение считало доступным только инфраструктуре. Также аккаунт проверяет авторизацию подписанного запроса в контексте выбранной сети и EntryPoint. Описание интерфейса аккаунта в ERC-4337.

Сам факт, что вызов поступил от правильного EntryPoint, не разрешает любое действие. Это доказывает происхождение вызова, но решение о владельце, сроке, nonce и разрешённой операции остаётся логикой аккаунта. Аналогично серверный запрос от доверенного прокси не заменяет проверку сессии читателя. У этих систем разные протоколы, но полезная граница ответственности похожа.

Для библиотеки задаём строгую учебную политику: разрешается только publish для курса 17 в конкретном CourseCatalog, без передачи нативной валюты. Такой узкий договор легче объяснить и проверить на бумаге. Универсальный кошелёк с произвольным execute потребует гораздо большего анализа: пакетных вызовов, маршрутов делегирования, обновляемых модулей и обхода ограничений через вложенные операции.

Чистая функция решения

Передадим в функцию уже проверенный результат криптографического адаптера. Флаг signatureValid в этом примере не приходит из браузерного JSON: его вычисляет доверенная реализация подписи. Поля контекста также относятся к серверу или контрактной среде, а не к утверждениям пользователя.

export function validateTeachingIntent(intent, context) {
  if (context.caller !== context.entryPoint) return { ok: false, reason: 'caller' };
  if (intent.chainId !== context.chainId) return { ok: false, reason: 'chain' };
  if (intent.sender !== context.account) return { ok: false, reason: 'sender' };
  if (!context.signatureValid) return { ok: false, reason: 'signature' };
  if (context.now >= context.validUntil) return { ok: false, reason: 'expired' };
  if (intent.target !== context.catalogAddress) return { ok: false, reason: 'target' };
  if (intent.action !== 'publish' || intent.courseId !== 17n) {
    return { ok: false, reason: 'scope' };
  }
  if (intent.value !== 0n) return { ok: false, reason: 'value' };
  return { ok: true, reason: 'authorized' };
}

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

Функция не изменяет реестр. Значение ok означает, что политика допускает намерение при указанном контексте. Оно ещё не сообщает, что publish завершился. Нулевой хеш, неправильная роль publisher или изменение состояния способны привести к ошибке на этапе выполнения. Предварительная оценка полезна, но между ней и включением операции проходит время.

Исполнение остаётся отдельной обязанностью

Вторая часть модели показывает результат вызова. Не используем блок catch, который превращает ошибку в успешную публикацию только потому, что подпись была корректной. Сохраняем обе ступени в одном объекте наблюдения.

export async function executeTeachingIntent(intent, context, callCatalog) {
  const validation = validateTeachingIntent(intent, context);
  if (!validation.ok) return { stage: 'validation', success: false, validation };
  try {
    const result = await callCatalog(intent); // отдельный адаптер исполнения
    return { stage: 'execution', success: true, result };
  } catch (error) {
    return { stage: 'execution', success: false, reason: String(error) };
  }
}

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

Аккаунт для подписи и аккаунт для вызовов

В уроке про ERC-1271 мы показали маленький контракт, который отвечает на вопрос о валидности подписи. У него не было механизма выполнения публикации. Нельзя считать наличие isValidSignature достаточным признаком полноценного смарт-кошелька. Два интерфейса решают разные задачи и могут существовать независимо.

Также адрес с кодом не всегда описывает неизменную категорию аккаунта. EIP-7702 добавляет механизм делегированного поведения для обычных адресов. Поэтому современный проект выбирает поддержку конкретного типа аккаунта и схемы авторизации, а не строит всю модель на одном вечном правиле «есть код — контракт, нет кода — обычный кошелёк». Спецификация EIP-7702.

В нашей лаборатории делегирование не включено. Это явная граница: обсуждаемый аккаунт является отдельным контрактом и должен уже быть publisher своего экземпляра CourseCatalog. Смешивать этот вариант с обновлением существующего адреса без отдельной программы проверки не следует.

Ожидаемый разбор

Представим три одинаковых намерения с разными контекстами. В первом caller не равен разрешённому EntryPoint. Модель возвращает причину caller до обращения к реестру. Во втором вызывающий правильный, но подпись относится к другой сети. Возвращается chain. В третьем все проверки допускают операцию, но реестр отклоняет publish из-за неверного publisher. Результат относится к execution и остаётся неуспешным.

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

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

Что нельзя проверять только в браузере

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

Чистая функция полезна как спецификация: она делает каждую причину отказа явной. При реализации для выбранного аккаунта следует сопоставить каждую строку политики с реальной проверкой и описать неподдерживаемые случаи. Если условие осталось только в клиентском компоненте, это ограничение удобства интерфейса, а не предел полномочия подписанта.

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