Транзакции и инструкции

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

Ниже приведены основные сведения о том, как выполняются транзакции:

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

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

Транзакция упрощеннаТранзакция упрощенна

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

Ключевые точки #

  • Solana транзакции состоят из инструкций, которые взаимодействуют с различными программами в сети, где каждая инструкция описывает конкретную операцию .

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

  • Инструкции в транзакции обрабатываются в том порядке, в котором они перечислены.

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

  • Максимальный размер транзакции составляет 1232 байта.

Простой пример #

Ниже приведена диаграмма, представляющая транзакцию с единой инструкцией для передать SOL отправителю получателю.

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

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

Передача SOLПередача SOL

Счет-отправитель должен быть включен в транзакцию в качестве подписывающего лица (is_signer), чтобы одобрить списание своего баланса lamport. Счета отправителя и получателя должны быть мутабельными (is_writable), поскольку инструкция изменяет баланс lamport для обоих счетов.

После отправки транзакции вызывается системная программа для обработки инструкции перевода. Затем системная программа обновляет остатки lamport на счетах отправителя и получателя соответственно.

Процесс передачи SOLПроцесс передачи SOL

Простой SOL трансфер #

Вот пример из Solana Playground, как создать инструкцию передачи SOL с помощью метода SystemProgram.transfer:

// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL
 
// Create a transfer instruction for transferring SOL from wallet_1 to wallet_2
const transferInstruction = SystemProgram.transfer({
  fromPubkey: sender.publicKey,
  toPubkey: receiver.publicKey,
  lamports: transferAmount * LAMPORTS_PER_SOL, // Convert transferAmount to lamports
});
 
// Add the transfer instruction to a new transaction
const transaction = new Transaction().add(transferInstruction);

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

Транзакции #

A Solana transaction состоит из:

  1. Signatures: Набор подписей, включенных в транзакцию.
  2. Сообщение: Список инструкций для атомарной обработки.

Формат транзакцийФормат транзакций

Структура сообщения о сделке включает:

  • Заголовок сообщения: Определяет число учетной записи подписчика и только для чтения.
  • Адреса счетов: Массив адресов счетов, требуемых в соответствии с инструкциями по транзакции.
  • Последний блокчейн: Действует как временная метка для транзакции.
  • Инструкции: Массив инструкций для выполнения.

Сообщение транзакцииСообщение транзакции

Размер транзакции #

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

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

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

Формат транзакцийФормат транзакций

Заголовок сообщения #

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

  1. Количество необходимых подписей для транзакции.
  2. Количество адресов учетных записей, доступных только для чтения, для которых требуются подписи.
  3. Количество адресов счетов, доступных только для чтения, которые не требуют подписей.

Заголовок сообщенияЗаголовок сообщения

Формат компакт-массива #

Компактный массив в контексте сообщения транзакции ссылается на массив сериализованный в следующем формате:

  1. Длина массива, закодированная как compact-u16.
  2. Отдельные элементы массива, перечисленные последовательно после длины кодировки

Формат компактного массиваФормат компактного массива

Этот метод кодирования используется для указания длины массивов Адреса клиентов и Instructions в сообщении транзакций .

Массив адресов клиента #

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

Этот массив начинается с кодировки количества адресов счетов в формате compact-u16, затем следуют адреса, упорядоченные по привилегиям для счетов. Метаданные в заголовке сообщения используются для определения количества счетов в каждой секции.

  • Учетные записи, доступные для записи, и подписывающие лица
  • Учетные записи, доступные только для чтения, и подписавшие их лица
  • Учетные записи, доступные для записи и не подписанные
  • Учетные записи, доступные только для чтения и не подписанные Компактный массив адресов аккаунта

Компактный массив адресов аккаунтаКомпактный массив адресов аккаунта

Последние Blockhash #

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

Максимальный возраст блочного хэша транзакции составляет 150 блоков (~1 минута предполагая 400 блоков раз). Если блокхэш транзакции на 150 блоков старше последнего блок хэша, он считается истекшим. Это означает, что транзакции, которые не обрабатываются в течение определенного промежутка времени, никогда не будут выполняться.

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

Массив инструкций #

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

Как и в массиве адресов аккаунтов, этот компактный массив начинается с кодировки compact-u16 с кодировкой с последующим массивом инструкций. Каждая инструкция в массиве задает следующую информацию:

  1. ID программы: Определяет последовательную программу, которая будет обрабатывать инструкцию . Это представлено в виде индекса u8, указывающего на адрес учетной записи в массиве адресов аккаунта.
  2. Компактный массив адресных индексов счета: Массив u8 индексов, указывающих на массив счетов для каждого счета, требуемого инструкцией.
  3. Компактный массив непрозрачных данных u8: задействован u8 байтовый массив, специфичный для программы. Эти данные определяют инструкцию для вызова программы вместе с с любыми дополнительными данными, которые требуется инструкция (например, аргумент функции ).

Формат компактного массиваФормат компактного массива

Пример структуры транзакции #

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

  • header: Включает данные, используемые для указания прав чтения/записи и подписчика в массиве accountKeys.

  • accountKey: Массив с адресами аккаунта для всех инструкций по транзакции .

  • recentBlockhash: Блокхэш включен в транзакцию после создания транзакции.

  • instructions: Массив содержит все инструкции по транзакции. Каждый account и programIdIndex в инструкции ссылается на массив accountKeys по индексу.

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

"transaction": {
    "message": {
      "header": {
        "numReadonlySignedAccounts": 0,
        "numReadonlyUnsignedAccounts": 1,
        "numRequiredSignatures": 1
      },
      "accountKeys": [
        "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
        "5snoUseZG8s8CDFHrXY2ZHaCrJYsW457piktDmhyb5Jd",
        "11111111111111111111111111111111"
      ],
      "recentBlockhash": "DzfXchZJoLMG3cNftcf2sw7qatkkuwQf4xH15N5wkKAb",
      "instructions": [
        {
          "accounts": [
            0,
            1
          ],
          "data": "3Bxs4NN8M2Yn4TLb",
          "programIdIndex": 2,
          "stackHeight": null
        }
      ],
      "indexToProgramIds": {}
    },
    "signatures": [
      "5LrcE2f6uvydKRquEJ8xp19heGxSvqsVbcqUeFoiWbXe8JNip7ftPQNTAVPyTK7ijVdpkzmKKaAQR7MWMmujAhXD"
    ]
  }

Инструкции #

Инструкция - это запрос на выполнение определенного действия в цепи и наименьшая непрерывная единица логики выполнения в программе

При создании инструкции для добавления к транзакции каждая инструкция должна содержать следующую информацию:

  • Program address: определяет вызванную программу.
  • Accounts: Перечисляет все счета, с которых инструкция считывает или на которые записывает данные, включая другие программы, используя структуру AccountMeta.
  • Instruction Data: Массив байтов, указывающий, какой обработчик инструкций в программе следует вызвать, а также любые дополнительные данные, требуемые обработчиком инструкций (аргументы функции).

Инструкция транзакцииИнструкция транзакции

AccountMeta #

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

  • pubkey: on-chain адрес аккаунта
  • is_signer: укажите, требуется ли учетная запись в качестве подписчика транзакции
  • is_wriable: укажите, будут ли изменены данные учетной записи

Эта информация называется AccountMeta.

AccountMetaAccountMeta

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

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

Пример структуры инструкции #

Ниже приведен пример структуры инструкции SOL transfer о деталях ключей аккаунта, ID программы и данные, требуемые инструкцией.

  • keys: Включает AccountMeta для каждого аккаунта, требуемого инструкцией.
  • programId: Адрес программы, содержащей логику выполнения для вызванной инструкции.
  • data: Инструкция для инструкции как буфер байт
{
  "keys": [
    {
      "pubkey": "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
      "isSigner": true,
      "isWritable": true
    },
    {
      "pubkey": "BpvxsLYKQZTH42jjtWHZpsVSa7s6JVwLKwBptPSHXuZc",
      "isSigner": false,
      "isWritable": true
    }
  ],
  "programId": "11111111111111111111111111111111",
  "data": [2,0,0,0,128,150,152,0,0,0,0,0]
}

Расширенный пример #

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

Ручная передача SOL #

Вот Playground пример того, как вручную собрать SOL инструкцию:

// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL
 
// Instruction index for the SystemProgram transfer instruction
const transferInstructionIndex = 2;
 
// Create a buffer for the data to be passed to the transfer instruction
const instructionData = Buffer.alloc(4 + 8); // uint32 + uint64
// Write the instruction index to the buffer
instructionData.writeUInt32LE(transferInstructionIndex, 0);
// Write the transfer amount to the buffer
instructionData.writeBigUInt64LE(BigInt(transferAmount * LAMPORTS_PER_SOL), 4);
 
// Manually create a transfer instruction for transferring SOL from sender to receiver
const transferInstruction = new TransactionInstruction({
  keys: [
    { pubkey: sender.publicKey, isSigner: true, isWritable: true },
    { pubkey: receiver.publicKey, isSigner: false, isWritable: true },
  ],
  programId: SystemProgram.programId,
  data: instructionData,
});
 
// Add the transfer instruction to a new transaction
const transaction = new Transaction().add(transferInstruction);

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