Одноразовые номера устойчивых транзакций

Проблема

Чтобы предотвратить повтор, транзакции Solana содержат поле одноразового номера, заполненное «последним» значением хэша блока. Транзакция, содержащая слишком старый блокхеш (на момент написания этой статьи ~2 минуты), отклоняется сетью как недействительная. К сожалению, в некоторых случаях использования, таких как кастодиальные услуги, требуется больше времени для создания подписи для транзакции. Необходим механизм для включения этих потенциально автономных участников сети.

Требования

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

Решение на основе контракта

Здесь мы описываем основанное на контракте решение проблемы, при котором клиент может «спрятать» одноразовое значение для будущего использования в поле recent_blockhash транзакции. Этот подход похож на атомарную инструкцию Compare and Swap, реализованную некоторыми процессорными ISA.

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

  1. Значение устойчивого одноразового номера используется в поле recent_blockhash
  2. Инструкция «AdvanceNonceAccount» выдается первой в транзакции.

Механика контрактов

TODO: svgbob это в блок-схему

Start
Create Account
  state = Uninitialized
NonceInstruction
  if state == Uninitialized
    if account.balance < rent_exempt
      error InsufficientFunds
    state = Initialized
  elif state != Initialized
    error BadState
  if sysvar.recent_blockhashes.is_empty()
    error EmptyRecentBlockhashes
  if !sysvar.recent_blockhashes.contains(stored_nonce)
    error NotReady
  stored_hash = sysvar.recent_blockhashes[0]
  success
WithdrawInstruction(to, lamports)
  if state == Uninitialized
    if !signers.contains(owner)
      error MissingRequiredSignatures
  elif state == Initialized
    if !sysvar.recent_blockhashes.contains(stored_nonce)
      error NotReady
    if lamports != account.balance && lamports + rent_exempt > account.balance
      error InsufficientFunds
  account.balance -= lamports
  to.balance += lamports
  success

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

Чтобы инициализировать вновь созданную учетную запись, необходимо выполнить инструкцию InitializeNonceAccount. Эта инструкция принимает один параметр, Pubkey [полномочия] учетной записи (../offline-signing/durable-nonce.md#nonce-authority). Одноразовые учетные записи должны быть освобождены от аренды, чтобы соответствовать требованиям к сохранению данных этой функции, и поэтому требуют внесения достаточного количества ламповых портов, прежде чем их можно будет инициализировать. После успешной инициализации последний блок-хэш кластера сохраняется вместе с указанным одноразовым ключом Pubkey.

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

Из-за требования rent-exempt учетных записей nonce для вывода средств со счета используется пользовательская инструкция по снятию средств. Инструкция WithdrawNonceAccount принимает один аргумент, указывает на вывод средств и обеспечивает освобождение от арендной платы, предотвращая падение баланса счета ниже минимума, освобожденного от арендной платы. Исключением из этой проверки является случай, когда окончательный баланс будет равен нулю лампортов, что делает учетную запись подходящей для удаления. Для этой детали закрытия учетной записи есть дополнительное требование, согласно которому сохраненное значение одноразового номера не должно совпадать с самым последним хэшем блока в соответствии с AdvanceNonceAccount.

[nonce полномочия] учетной записи (../offline-signing/durable-nonce.md#nonce-authority) можно изменить с помощью инструкции AuthorizeNonceAccount. Он принимает один параметр, Pubkey нового органа. Выполнение этой инструкции предоставляет новым полномочиям полный контроль над счетом и его балансом.

AdvanceNonceAccount, WithdrawNonceAccount и AuthorizeNonceAccount требуют текущих одноразовых полномочий для учетной записи, чтобы подписать транзакцию.

Поддержка во время выполнения

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

Любая транзакция, не прошедшая обычную проверку check_hash_age, будет проверена на наличие одноразового номера устойчивой транзакции. Об этом сигнализирует включение инструкции «AdvanceNonceAccount» в качестве первой инструкции в транзакции.

Если среда выполнения определяет, что используется одноразовый номер устойчивой транзакции, она предпримет следующие дополнительные действия для проверки транзакции:

  1. Загружается NonceAccount, указанный в инструкции Nonce.
  2. NonceState десериализуется из поля данных NonceAccount и подтверждается, что он находится в состоянии Initialized.
  3. Значение nonce, хранящееся в NonceAccount, проверяется на соответствие значению, указанному в поле recent_blockhash транзакции.

Если все три вышеуказанные проверки завершаются успешно, транзакция может продолжить проверку.

Поскольку с транзакций, завершившихся ошибкой «InstructionError», взимается комиссия, а изменения их состояния откатываются, существует возможность кражи комиссии, если отменяется инструкция «AdvanceNonceAccount». Злонамеренный валидатор может воспроизвести неудачную транзакцию до тех пор, пока сохраненный одноразовый номер не будет успешно расширен. Изменения во время выполнения предотвращают такое поведение. Когда длительная транзакция одноразового номера завершается с ошибкой InstructionError помимо инструкции AdvanceNonceAccount, учетная запись одноразового номера, как обычно, откатывается в состояние, предшествующее выполнению. Затем среда выполнения увеличивает свое значение одноразового номера, и расширенная учетная запись одноразового номера сохраняется, как если бы это было успешно.