Transactions v2 — таблицы поиска адресов в цепочке

Проблема

Сообщения, передаваемые валидаторам Solana, не должны превышать размер IPv6 MTU, чтобы обеспечить быструю и надежную сетевую передачу информации о кластере по протоколу UDP. Сетевой стек Solana использует консервативный размер MTU, равный 1280 байтам, который после учета заголовков оставляет 1232 байта для пакетных данных, таких как сериализованные транзакции.

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

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

Предложенное решение

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

Программа таблицы поиска адресов

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

После того, как адреса сохранены в цепочке в учетной записи таблицы поиска адресов, на них можно кратко ссылаться в транзакции с использованием 1-байтового индекса u8, а не полного 32-байтового адреса. Это потребует нового формата транзакций для использования этих кратких ссылок, а также обработки во время выполнения для поиска и загрузки адресов из таблиц поиска в цепочке.

Состояние

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

Поскольку транзакции используют индекс u8 для поиска адресов, каждая таблица адресов может хранить до 256 адресов. В дополнение к сохраненным адресам учетные записи таблицы адресов также отслеживают различные метаданные, описанные ниже.

/// The maximum number of addresses that a lookup table can hold
pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256;

/// The serialized size of lookup table metadata
pub const LOOKUP_TABLE_META_SIZE: usize = 56;

pub struct LookupTableMeta {
    /// Lookup tables cannot be closed until the deactivation slot is
    /// no longer "recent" (not accessible in the `SlotHashes` sysvar).
    pub deactivation_slot: Slot,
    /// The slot that the table was last extended. Address tables may
    /// only be used to lookup addresses that were extended before
    /// the current bank's slot.
    pub last_extended_slot: Slot,
    /// The start index where the table was last extended from during
    /// the `last_extended_slot`.
    pub last_extended_slot_start_index: u8,
    /// Authority address which must sign for each modification.
    pub authority: Option<Pubkey>,
    // Padding to keep addresses 8-byte aligned
    pub _padding: u16,
    // Raw list of addresses follows this serialized structure in
    // the account's data, starting from `LOOKUP_TABLE_META_SIZE`.
}

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

pub struct BufferMeta {
    /// Authority address which must sign for each modification.
    pub authority: Pubkey,

    // Serialized list of stored addresses follows the above metadata.
}

Очистка

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

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

Расходы

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

Версионные транзакции

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

Новый формат транзакции должен отличаться от текущего формата транзакции. Текущие транзакции могут содержать не более 19 подписей (по 64 байта каждая), но заголовок сообщения кодирует num_required_signatures как u8. Поскольку старший бит u8 никогда не будет установлен для действительной транзакции, мы можем включить его, чтобы обозначить, должна ли транзакция быть декодирована в версионном формате или нет.

Новый формат транзакции

#[derive(Serialize, Deserialize)]
pub struct Transaction {
    #[serde(with = "short_vec")]
    pub signatures: Vec<Signature>,
    /// The message to sign.
    pub message: Message,
}

// Uses custom serialization. If the first bit is set, the remaining bits
// in the first byte will encode a version number. If the first bit is not
// set, the first byte will be treated as the first byte of an encoded
// legacy message.
pub enum VersionedMessage {
    Legacy(Message),
    V0(v0::Message),
}

// The structure of the new v0 Message
#[derive(Serialize, Deserialize)]
pub struct Message {
  // unchanged
  pub header: MessageHeader,

  // unchanged
  #[serde(with = "short_vec")]
  pub account_keys: Vec<Pubkey>,

  // unchanged
  pub recent_blockhash: Hash,

  // unchanged
  //
  // # Notes
  //
  // Account and program indexes will index into the list of addresses
  // constructed from the concatenation of three key lists:
  //   1) message `account_keys`
  //   2) ordered list of keys loaded from address table `writable_indexes`
  //   3) ordered list of keys loaded from address table `readable_indexes`
  #[serde(with = "short_vec")]
  pub instructions: Vec<CompiledInstruction>,

  /// List of address table lookups used to load additional accounts
  /// for this transaction.
  #[serde(with = "short_vec")]
  pub address_table_lookups: Vec<MessageAddressTableLookup>,
}

/// Address table lookups describe an on-chain address lookup table to use
/// for loading more readonly and writable accounts in a single tx.
#[derive(Serialize, Deserialize)]
pub struct MessageAddressTableLookup {
  /// Address lookup table account key
  pub account_key: Pubkey,
  /// List of indexes used to load writable account addresses
  #[serde(with = "short_vec")]
  pub writable_indexes: Vec<u8>,
  /// List of indexes used to load readonly account addresses
  #[serde(with = "short_vec")]
  pub readonly_indexes: Vec<u8>,
}

Изменения размера

Изменения метаданных

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

изменения RPC

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

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

Ограничения

Вопросы безопасности

Повторная инициализация таблицы поиска

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

Потребление ресурсов

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

Передний ход

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

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

Отказ в обслуживании

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

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

Дублирующие аккаунты

Транзакции не могут загружать учетную запись более одного раза, будь то напрямую через account_keys или косвенно через address_table_lookups.

Другие предложения

  1. Префиксы учетных записей

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

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

  1. Программа построителя транзакций

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

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

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

  1. Индексы счетов эпох

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

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

  1. Списки адресов

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

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

  1. Увеличьте размер транзакции

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