ERC-4337: nonce и параллельные операции аккаунта
Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 25 из 30.
Вход в библиотеку использует одноразовый nonce, который сервер потребляет после проверки подписи. Операции смарт-аккаунта тоже нуждаются в защите от повторов, но их nonce относится к другой системе. Теперь разберём составной счётчик операций, отдельные последовательности и влияние порядка на публикации курса 17. Результатом станет небольшая модель, которая показывает, почему разные очереди не разрешают считать независимыми любые действия.
Код ниже является чистой учебной арифметикой и не запускался. Он не запрашивает фактический nonce у EntryPoint и не подменяет адаптер подготовки операций. Реальный профиль аккаунта определяет, какие ключи последовательностей разрешены и как они связаны с полномочиями. Само наличие числа key не доказывает существование отдельной безопасной роли пользователя.
Один термин, разные области
Nonce сообщения входа связывает одно разрешение с серверным потоком авторизации. Nonce операции предотвращает повторное выполнение в выбранном сетевом контексте. Nonce внешней транзакции bundler относится к аккаунту, который доставляет пакет. Эти значения не следует хранить в одной общей колонке nonce без указания типа.
Например, читатель может войти в кабинет дважды и получить две разные серверные задачи. Это не изменяет счётчик публикаций преподавательского смарт-аккаунта. Аналогично bundler может отправить новую внешнюю транзакцию, а наша ранее подписанная операция останется той же. Разделение позволяет объяснить пользователю, что именно повторяется и какая система отвечает за порядок.
ERC-4337 описывает разбиение nonce на ключ последовательности и её счётчик: старшие 192 бита используются как key, младшие 64 — как sequence. Правила поддержки и авторизации ключей относятся к аккаунту. Раздел о составном nonce в ERC-4337.
Упакуем и разберём значение
JavaScript Number не подходит для этих диапазонов. Используем BigInt и проверяем границы перед упаковкой. Сдвиг не должен молча создавать число, которое выбранное представление не допускает.
const KEY_LIMIT = 1n << 192n;
const SEQUENCE_LIMIT = 1n << 64n;
const SEQUENCE_MASK = SEQUENCE_LIMIT - 1n;
export function packTeachingNonce(key, sequence) {
if (key < 0n || key >= KEY_LIMIT) throw new Error('Ключ вне диапазона');
if (sequence < 0n || sequence >= SEQUENCE_LIMIT) {
throw new Error('Счётчик вне диапазона');
}
return (key << 64n) | sequence;
}
export function splitTeachingNonce(nonce) {
if (nonce < 0n || nonce >= (1n << 256n)) throw new Error('Неверный nonce');
return { key: nonce >> 64n, sequence: nonce & SEQUENCE_MASK };
}
Для key равного 2 и sequence равного 5 функция возвращает одно целое значение. Обратное разбиение должно снова дать эти два компонента. Это ожидаемый результат по формулам, а не отчёт о запуске. На границе sequence равного двум в степени 64 упаковка должна отклонить вход, поскольку он уже не помещается в младшие биты.
При записи в JSON переводим оба компонента в десятичные строки. На чтении сначала проверяем их форму и диапазон, затем возвращаем BigInt. Иначе кеш способен округлить значение ещё до попадания в адаптер, а ошибка будет выглядеть как загадочный отказ подписи.
Две очереди не делают состояния независимыми
Предположим, что аккаунт поддерживает очередь публикаций и отдельную очередь административных действий. Два намерения в разных очередях могут готовиться параллельно. Но если оба меняют courses(17), конечное состояние по-прежнему зависит от порядка выполнения. Составной nonce решает задачу последовательности внутри канала, а не задачу конфликта предметной области.
Первое намерение публикует хеш A, второе публикует хеш B. Базовый CourseCatalog полностью заменяет запись при каждом publish. Если B выполнится последним, текущим станет B; если A последним — A. Наличие разных ключей nonce не превращает это в слияние двух редакций. Для контроля ожидаемой предыдущей версии потребуется другой контрактный метод с проверкой условия, которого в нашем базовом реестре нет.
Поэтому приложение сохраняет expectedContentHash как описание цели, но не обещает, что такая версия останется последней навсегда. После подтверждения конкретной операции перечитываем текущую запись и показываем, если последующая публикация уже заменила её. История операций и актуальное состояние библиотеки являются разными представлениями.
Учебная очередь подготовки
Обычный браузерный счётчик не должен самостоятельно назначать окончательный nonce нескольким вкладкам. В нашей модели сначала получаем наблюдение адаптера, затем готовим одну задачу в соответствующем контексте. Резерв приложения не является изменением счётчика в сети.
export function preparationKey(context, key, sequence) {
return [
context.environmentId,
context.chainId,
context.account.toLowerCase(),
key.toString(),
sequence.toString(),
].join(':');
}
export function mayPrepare(reservations, id) {
return !reservations.has(id);
}
Этот Set защищает только от дублирования в одном процессе. Между вкладками или устройствами требуется координация и повторная проверка непосредственно перед подписью. Даже общий серверный резерв не заставляет сеть принять выбранный nonce: другой участник может успеть выполнить операцию независимо от приложения.
Если подготовленная задача отклонена как устаревшая, нельзя просто заменить nonce внутри уже подписанного пакета. Подпись относится к определённым данным. Необходимо заново подготовить операцию по правилам профиля и дать пользователю увидеть актуальное намерение. Особенно важно сохранить причину изменения, когда предыдущая задача остаётся в состоянии unknown.
Ожидаемое поведение
Для двух операций одной последовательности первая использует текущий счётчик, а следующая ожидает следующий допустимый номер. Если первая ещё не выяснена, интерфейс не изображает вторую как гарантированно независимую. Конкретные правила очереди и повторной доставки задаёт профиль, а не наша чистая функция упаковки.
Для разных ключей возможно параллельное продвижение, если аккаунт действительно разрешает эти ключи. При этом публикации одного курса сохраняют предметный конфликт. Пользователь видит две отдельные карточки и два идентификатора, а текущая версия курса определяется наблюдением цепочки. По одному значению nonce нельзя угадать, какая редакция стала последней.
Для переполнения компонента наша модель возвращает ошибку до обращения к сети. Для сброса локального узла старые резервы получают другое environmentId и не используются повторно. Для неясного результата запрос статуса относится к исходной операции, а не автоматически выделяет следующий счётчик.
После урока мы можем назвать область каждого nonce и объяснить его порядок. В следующей главе ограничим сам ключ, который подписывает операции: возможность избежать повторов ещё не означает право на произвольное действие.
Счётчик после неуспешного действия
Не предполагаем, что любая ошибка оставляет nonce прежним. Отказ до принятия и неуспешное выполнение уже включённой операции относятся к разным стадиям. При подготовке следующей задачи получаем актуальное наблюдение адаптера, а не вычисляем его из красной карточки интерфейса. Это особенно важно после восстановления неизвестного результата.