Шаблоны параллелизма для одноразового номера учетной записи

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

Обычно это не вызывает проблем, когда транзакции отправляются через интерфейс web3 на узел Ethereum (например, Geth, Parity), который затем подписывает их. Последовательность выполняется внутри узла и, таким образом, прозрачна для конечного пользователя.

Это не тот случай, когда транзакции создаются и подписываются пользовательским приложением (т. е. не Geth или Parity) перед отправкой на узел (например, через web3.eth.sendRawTransaction).

Создание необработанной транзакции требует получения одноразового номера учетной записи, что можно сделать с помощью web3.eth.getTransactionCount, как описано в этом вопросе . Это прекрасно работает, когда параллелизм не задействован.

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

«web3.eth.getTransactionCount» возвращает только количество подтвержденных транзакций (см. этот вопрос ). В результате, когда транзакции подписываются параллельно, если мы не будем разумно манипулировать счетчиком, только одна из них будет успешной.

Мой вопрос: каковы некоторые хорошие шаблоны для обработки этой ситуации?

Кандидат 1: несколько ключей/аккаунтов на пользователя

Это позволяет параллельные транзакции. Однако это вносит дополнительную сложность в пользовательское приложение. Кроме того, это не слишком масштабируемо: подумайте, нужны ли десятки или сотни одновременных транзакций. Кроме того, учитывая, что иерархический детерминированный кошелек не является родным для Ethereum (я знаю, что существует LightWallet ), это не похоже на простое решение.

Кандидат 2: Зациклиться и повторить попытку

Это создает цикл, который проверяет результат отправки транзакции. Если возвращаемый результат указывает на повторяющийся одноразовый номер (например, Geth вернет «транзакцию замещения по заниженной цене»), повторите попытку с новым одноразовым номером.

Это решение не является переносимым: разные клиенты возвращают разные строки для одной и той же ошибки. На практике я, кажется, заметил, что Geth иногда даже не возвращает указанную выше строку, а молча отбрасывает другие транзакции (не подтверждено, так как я не нашел надежного способа воспроизвести). В результате обнаружение не работает надежно.

Кандидат 3: одноэлементный «менеджер одноразовых номеров»

Каждая конструкция транзакции включает в себя «запрос» одноразового номера от глобального «менеджера одноразовых номеров». Одноэлементный «менеджер одноразовых номеров» может иметь некоторую умную логику для раздачи номеров одноразовых номеров по возрастанию. Это использует поведение, обсуждаемое здесь .

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

Кандидат 4: делегировать локальный узел Geth/Parity

Это не совсем решение. Наиболее очевидная проблема заключается в том, что дополнительная зависимость не кажется оправданной, если она касается только поля nonce.

Кроме того, если пользовательское приложение уже управляет закрытыми ключами, это создает дополнительную сложность: закрытые ключи должны совместно управляться (или копироваться) узлом Geth или Parity. Опять же, сложность (и потенциальная уязвимость) не кажется оправданной, если она касается только одноразового номера.


Не по теме. Мне кажется, что учетная запись nonce — не лучший дизайн в Ethereum. Это заставляет пользовательские приложения либо обрабатывать детали протокола низкого уровня, либо вводить зависимость от узла (Geth/Parity). В любом случае это добавляет непропорционально громоздкости пользовательским приложениям.

Ответы (3)

Я еще не дошел до того момента, когда я могу протестировать любую из этих вещей, но я чувствую, что Singleton Nonce Manager будет подходящим вариантом с несколькими улучшениями:

  • Сделайте его отправителем одноэлементной транзакции
  • Это имеет максимальный буфер или «заголовок» ожидающих транзакций на адрес «от», который соответствует или меньше, чем максимальное количество транзакций, которое одноранговый узел может иметь в своей очереди пула транзакций на адрес «от». Давайте назовем эти незавершенные транзакции «слотами».
  • Реализована как скользящая очередь, в которой самая старая подтвержденная транзакция удаляется из «хвоста».
  • Каждый новый слот получает новый одноразовый номер
  • Каждый новый слот связан с асинхронным вызовом sendRawTransaction. При любом сбое слот освобождается.
  • Любые новые транзакции добавляются в самый нижний свободный слот.
  • В случае, если более высокие слоты ожидаются в течение более чем N миллисекунд после того, как слот был освобожден из-за ошибки, отмените самую высокую транзакцию, отправив себе 0 eth с таким же одноразовым номером, что и самый высокий одноразовый номер слота, и воспроизведя эту транзакцию в освобожденном слоте.

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

Хороший дизайн. Большое спасибо. Кольцевой буфер выглядит блестящим решением.
«Кольцевой буфер». Да. Это гораздо лучшая фраза, чем «скользящая очередь». 👍
После нескольких дней размышлений о том, что я схожу с ума, и мое программирование просто отстой (что оно и делает), это та же проблема, с которой я столкнулся: потенциально много транзакций, подписанных приложениями, одновременно с управлением одноразовыми номерами. Ethers.js, «лучшая» версия Web3, имеет предстоящий NonceManager, но счетчик одноразовых номеров по-прежнему полагается на сеть для подсчета транзакций, поэтому я не совсем уверен, как это предназначено для решения проблемы. Ты, @LinmaoSong, открыл что-нибудь за последние два года?
@EvilJordan извините, к сожалению, нет. Это одноэлементный шаблон на уровне протокола, поэтому одноэлементный менеджер наименее плох.
Почему количество слотов в одноразовом буфере должно быть « меньше, чем максимальное количество транзакций, которое одноранговый узел может иметь в своей очереди пула транзакций на адрес отправителя »? Мне кажется, нам это ограничение не нужно, пока мы не пытаемся публиковать все транзакции одновременно.

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

nonce: web3.toHex(web3.eth.getTransactionCount(fromAddress, "pending")),
Спасибо Райан. Это работает только в том случае, если транзакция создается только после того, как другие транзакции были отправлены в И уже приняты в пул транзакций. Рассмотрим две транзакции, созданные одновременно, даже с флагом «ожидание», они получат один и тот же результат. Так что проблема все же есть.
@LinmaoSong У меня был возврат от getTransactionCount(), который все еще оставался неизменным (то есть - не увеличивался) накануне после того, как я получил хэш Tx, и не видел, чтобы он увеличивался до тех пор, пока транзакция не была добыта. Обратите внимание, это проходило через Web3.js, если это имеет значение. Из-за этого я получал дубликаты nonce.

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

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

Спасибо. Мне кажется, что хранение одноразового номера (в БД или где-то еще) - это, по сути, одноэлементный шаблон, о котором я упоминал выше. Как бы вы справились с проблемой сбоя транзакции нижнего одноразового номера?
Я имею в виду, что я бы определенно пошел по пути обеспечения того, чтобы трансляция состоялась, с помощью каких-то других средств. Например, динамически устанавливайте каждую цену на газ выше средней цены на газ, чтобы сеть действительно ее обрабатывала. Может быть, увеличить цену на газ немного выше, чтобы гарантировать, что он всегда «на опережение». Вы всегда можете просмотреть транзакцию или набор транзакций, и если покажется, что отсутствующей нет, просто ретранслируйте отсутствующую. это большая бухгалтерская работа, но потенциально она того стоит.