Nonce транзакции

Nonce является одним из наиболее важных и наименее понятных компонентов транзакции. Определение в Желтой книге (см. [ссылки]) гласит:

nonce: Скалярное значение, равное количеству транзакций, отправленных с этого адреса, или, в случае счетов с ассоциированным кодом, количеству контрактов-созданий, выполненных этим счетом.

Строго говоря, nonce является атрибутом адреса отправителя; то есть он имеет значение только в контексте адреса отправителя. Однако nonce не хранится в явном виде как часть состояния счета в блокчейне. Вместо этого он вычисляется динамически, путем подсчета количества подтвержденных транзакций, которые исходили от адреса.

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

  1. Представьте, что вы хотите совершить две транзакции. Вам нужно совершить важный платеж в размере 6 ether, а также другой платеж в размере 8 ether. Вы подписываете и отправляете сначала транзакцию на 6 ether, потому что она важнее, а затем подписываете и транслируете вторую, на 8 ether. К сожалению, вы упустили из виду тот факт, что на вашем счету всего 10 ether, поэтому сеть не может принять обе транзакции: одна из них не пройдет. Поскольку вы отправили сначала более важную транзакцию на 6 ether, вы, понятно, ожидаете, что она пройдет, а транзакция на 8 ether будет отклонена. Однако в такой децентрализованной системе, как Ethereum, узлы могут получать транзакции в любом порядке; нет никакой гарантии, что конкретный узел получит одну транзакцию раньше другой. Поэтому почти наверняка некоторые узлы получат сначала транзакцию на 6 ether, а другие - на 8 ether. Если не включать nonce, то будет случайным, какая транзакция будет принята, а какая отклонена. Однако с включенным nonce первая отправленная вами транзакция будет иметь nonce, допустим, 3, в то время как транзакция на 8 ether будет иметь следующее значение nonce (т.е. 4). Таким образом, эта транзакция будет игнорироваться до тех пор, пока не будут обработаны транзакции с кодами от 0 до 3, даже если она будет получена первой.

  2. Теперь представьте, что у вас есть счет со 100 ether. Вы находите в Интернете человека, который принимает оплату в ether за Алатырь-камень, который вы очень хотите купить. Вы посылаете ему 2 ether, и он высылает вам Алатырь-камень. Прекрасно. Чтобы осуществить этот платеж в 2 ether, вы подписали транзакцию, отправив 2 ether со своего счета на его счет, а затем передали ее в сеть Ethereum для проверки и включения в блокчейн. Теперь, без значения nonce в транзакции, вторая транзакция, отправляющая 2 ether на тот же адрес во второй раз, будет выглядеть точно так же, как и первая. Это означает, что любой, кто увидит вашу транзакцию в сети Ethereum (а это значит, что каждый, включая получателя или ваших врагов), может "воспроизвести" транзакцию снова, снова и снова, пока все ваши эфиры не исчезнут, просто скопировав и вставив вашу оригинальную транзакцию и повторно отправив ее в сеть. Однако благодаря значению nonce, включенному в данные транзакции, каждая транзакция уникальна, даже при многократной отправке одного и того же количества ether на один и тот же адрес получателя. Таким образом, благодаря тому, что увеличивающееся значение nonce является частью транзакции, никто не сможет "продублировать" совершенный вами платеж.

Подводя итог, важно отметить, что использование nonce фактически жизненно необходимо для протокола, основанного на счетах, в отличие от механизма "выхода неизрасходованных транзакций" (UTXO) протокола Bitcoin.

Отслеживание нецелочисленных данных

С практической точки зрения, nonce - это актуальный подсчет количества подтвержденных (т.е. находящихся на цепи) транзакций, которые были произведены со счета. Чтобы узнать значение nonce для адреса, вы можете опросить блокчейн, например, через интерфейс web3.

Откройте консоль JavaScript в Geth (или предпочитаемом вами интерфейсе web3) в тестовой сети Ropsten, затем введите:

> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f")
40

Совет: Nonce - это счетчик, первое значение которого 0. В нашем примере количество транзакций равно 40, то есть были просмотрены nonce с 0 по 39. Nonce следующей транзакции должен быть равен 40.

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

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

Предупреждение: Будьте осторожны при использовании функции getTransactionCount для подсчета отложенных транзакций, поскольку вы можете столкнуться с некоторыми проблемами, если отправите несколько транзакций подряд.

Давайте рассмотрим пример:

> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
40
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41

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

Когда вы создаете приложение, которое конструирует транзакции, оно не может полагаться на getTransactionCount для отложенных транзакций. Только когда количество ожидающих и подтвержденных транзакций равно (все незавершенные транзакции подтверждены), вы можете доверять результату getTransactionCount для запуска вашего счетчика nonce. После этого отслеживайте nonce в своем приложении до тех пор, пока каждая транзакция не подтвердится.

Пробелы в nonce, дублирование nonce и подтверждение

Важно отслеживать nonce, если вы создаете транзакции программно, особенно если вы делаете это из нескольких независимых процессов одновременно.

Сеть Ethereum обрабатывает транзакции последовательно, на основе nonce. Это означает, что если вы передадите транзакцию с nonce 0, а затем передадите транзакцию с nonce 2, вторая транзакция не будет включена ни в один блок. Она будет храниться в mempool, в то время как сеть Ethereum будет ждать появления недостающего nonce. Все узлы будут считать, что недостающий nonce просто задержался и что транзакция с nonce 2 была получена не по порядку.

Если вы затем передадите транзакцию с отсутствующим nonce 1, обе транзакции (nonces 1 и 2) будут обработаны и включены (если они действительны, конечно). Как только вы заполните пробел, сеть сможет добыть транзакцию, не соответствующую последовательности, которую она хранила в mempool.

Это означает, что если вы последовательно создадите несколько транзакций и одна из них не будет официально включена ни в один блок, все последующие транзакции "застрянут", ожидая недостающий nonce. Транзакция может создать непреднамеренный "пробел" в последовательности nonce, потому что она например недействительна или имеет недостаточное количество газа. Чтобы возобновить работу, необходимо передать действительную транзакцию с недостающим nonce. Следует также помнить, что как только транзакция с "недостающим" nonce будет подтверждена сетью, все транзакции с последующими nonce будут постепенно становиться действительными; "отозвать" транзакцию невозможно!

С другой стороны, если вы случайно продублируете nonce, например, передадите две транзакции с одинаковым nonce, но разными получателями или значениями, то одна из них будет подтверждена, а другая отклонена. Какая из них будет подтверждена, определяется последовательностью, в которой они приходят на первый проверяющий узел, который их получает - т.е. это будет достаточно случайным.

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

Параллельность, возникновение транзакций и нераспространенные данные

Параллелизм - сложный аспект в компьютерной науке, и иногда он возникает неожиданно, особенно в децентрализованных и распределенных системах реального времени, таких как Ethereum.

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

Теперь представьте, что у вас есть несколько независимых приложений кошельков, которые генерируют транзакции с одного и того же адреса или адресов. Одним из примеров такой ситуации может быть биржа, обрабатывающая снятие средств с горячего кошелька биржи (кошелек, ключи которого хранятся онлайн, в отличие от холодного кошелька, где ключи никогда не находятся онлайн). В идеале необходимо иметь несколько компьютеров, обрабатывающих снятие средств, чтобы они не стали узким местом или единой точкой отказа. Однако это быстро становится проблематичным, поскольку наличие более одного компьютера, производящего изъятия, приведет к некоторым сложным проблемам параллелизма, не последней из которых является выбор nonce. Как координируются несколько компьютеров, генерирующих, подписывающих и транслирующих транзакции с одного и того же счета "горячего" кошелька?

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

Другой подход заключается в том, чтобы генерировать транзакции, но не присваивать им nonce (и, следовательно, оставлять их неподписанными - помните, что nonce является неотъемлемой частью данных транзакции и поэтому должен быть включен в цифровую подпись, подтверждающую подлинность транзакции). Затем их можно поставить в очередь на один узел, который подписывает их, а также отслеживает nonce. Хотя, опять же, это было бы узким местом в процессе: подписание и отслеживание nonce - это та часть вашей операции, которая, скорее всего, будет перегружена под нагрузкой, в то время как генерация неподписанной транзакции - это та часть, которую вам не нужно распараллеливать. У вас будет некоторый параллелизм, но он будет отсутствовать в критической части процесса.

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