
Приоритетные комиссии: как устроены комиссии за транзакции в Solana
О чем эта статья?
Solana работает быстро. Но даже в самом быстром из доступных блокчейнов пользователям нужна оптимальная обработка важных транзакций. Приоритетные комиссии позволяют поместить транзакцию пользователя в начало очереди на исполнение. Это дополнительные необязательные комиссии, которые пользователь может добавить к своей транзакции.
В этой статье мы кратко рассмотрим особенности обработки транзакций в Solana. Разберем транзакции, их жизненный цикл и работу комиссий. Затем рассмотрим приоритетные комиссии, их программную реализацию и рекомендации по использованию.
Транзакции и их жизненный цикл
Транзакции используются для вызова программ Solana и изменения состояния. Это наборы инструкций, то есть директив для отдельных вызовов программ. Они указывают валидатору, какие действия выполнить, с какими аккаунтами и имеются ли необходимые разрешения.
Общий жизненный цикл транзакции в Solana выглядит так:
- У пользователя есть четкая цель и действие, которое он хочет выполнить. Например, Alice хочет отправить Bob 10 SOL
- Пользователь создает транзакцию для нужного действия. Например, Alice создает транзакцию с инструкцией перевести 10 SOL со своего аккаунта на аккаунт Bob. Alice также добавляет недавний хеш блока и подписывает транзакцию своим закрытым ключом
- Пользователь отправляет транзакцию в сеть. Затем он получает информацию о том, была ли транзакция успешно добавлена. Например, Alice отправляет транзакцию со статусом подтверждения confirmed. После подтверждения транзакции она получает подпись транзакции. Alice может использовать эту подпись в обозревателе блоков, например Orb, чтобы убедиться, что она успешно отправила Bob 10 SOL. С ее аккаунта списано 10 SOL, а на аккаунт Bob зачислено 10 SOL
Для отправки подписанной транзакции в сеть пользователи обращаются к RPC-провайдеру, например Helius. RPC-сервисы Helius получают транзакцию и проверяют текущее расписание лидеров. В Solana только определенные валидаторы отвечают за добавление записей в реестр в конкретные моменты времени. Лидер отвечает за создание блока в текущем слоте и назначается на четыре последовательных слота. Подписанная транзакция отправляется текущему и двум следующим лидерам.
Текущий лидер проверяет подписанную транзакцию и выполняет другие этапы предварительной обработки, прежде чем запланировать ее исполнение. В отличие от других блокчейнов L1, таких как Ethereum, в Solana нет глобальной очереди транзакций. Большинство валидаторов используют реализацию планировщика от Solana Labs. Однако валидаторы с клиентом Jito используют псевдомемпул, то есть MempoolStream, для упорядочивания транзакций. Стандартный планировщик работает в нескольких потоках, каждый из которых поддерживает очередь ожидающих исполнения транзакций. Транзакции упорядочиваются в блоках с помощью сочетания принципа «первым пришел — первым обслужен» (FIFO) и приоритетных комиссий. Важно учитывать, что этот порядок по своей природе недетерминирован: транзакции распределяются между потоками исполнения отчасти случайным образом.
После исполнения транзакция распространяется через Turbine, а соответствующие комиссии списываются.
Как работают комиссии за транзакции в Solana
Комиссии за транзакции — это небольшие суммы, которые взимаются за обработку транзакций в Solana. Текущий лидер обрабатывает отправленные через сеть транзакции и создает записи в реестре. Когда транзакция подтверждается как часть состояния, взимается комиссия, поддерживающая экономическую модель Solana. Комиссии за транзакции приносят Solana несколько основных преимуществ. Они:
- Обеспечивают вознаграждение валидаторов
- Снижают нагрузку на сетевые ресурсы, устанавливая для транзакций стоимость в реальном времени
- Обеспечивают долгосрочную экономическую устойчивость сети за счет установленной протоколом минимальной комиссии
В краткосрочной перспективе безопасность сети Solana обеспечивают инфляционные вознаграждения на уровне протокола. Для этого в сети задан общий уровень инфляции, позволяющий вознаграждать валидаторов. В долгосрочной перспективе безопасность Solana поддерживают комиссии за транзакции. Фиксированная часть каждой комиссии — изначально 50% — сжигается, а остаток отправляется текущему лидеру. Solana сжигает комиссии, чтобы поддерживать стоимость SOL и лишать злонамеренных валидаторов стимула подвергать транзакции цензуре.
Комиссии за транзакции рассчитываются на основе статически заданной базовой комиссии за подпись и вычислительных ресурсов, использованных во время транзакции и измеряемых в вычислительных единицах (CU). Базовая комиссия может составлять от 50% до 1000% от целевого количества лампортов за подпись. Сейчас целевое значение по умолчанию составляет 10 000 на подпись. Каждой транзакции выделяется максимальный бюджет CU, называемый вычислительным бюджетом. При превышении бюджета среда выполнения останавливает транзакцию и возвращает ошибку. Максимальный бюджет одной транзакции составляет 1,4 млн CU, а лимит пространства блока — 48 млн CU.
Что такое приоритетные комиссии?
Из-за этих ограничений ресурсоемкие транзакции могут заполнить пространство блока и задержать другие транзакции. Solana ввела необязательную комиссию, которая позволяет транзакции получить преимущество перед другими транзакциями в очереди лидера. Она называется приоритетной комиссией. Ее уплата фактически повышает приоритет транзакции и ускоряет ее исполнение. Это полезно для срочных транзакций и транзакций с высокой стоимостью. Приоритет комиссии транзакции определяется количеством запрошенных вычислительных единиц. Чем больше вычислительных единиц запрашивает транзакция, тем больше придется заплатить для сохранения ее приоритета в очереди. Повышенная плата за дополнительные вычислительные единицы предотвращает спам ресурсоемкими транзакциями.
Приоритетная комиссия равна произведению вычислительного бюджета транзакции и цены вычислительной единицы, измеряемой в микролампортах: priorityFees = computeBudget * computeUnitPrice
Вычислительный бюджет
computeBudget задает максимальное количество вычислительных единиц, которое может использовать транзакция, стоимость различных операций и операционные ограничения, которые она должна соблюдать. Вычислительные ресурсы расходуются на следующие операции:
- Исполнение инструкций SBF
- Передача данных между программами
- Выполнение системных вызовов, например журналирования, создания адреса программы и CPI
При межпрограммных вызовах (CPI) вызываемая программа работает в рамках вычислительного бюджета вызывающей, то есть родительской, программы. Если вызываемая программа расходует весь оставшийся вычислительный бюджет или превышает заданный лимит, вся цепочка вызовов программ завершается с ошибкой. В том числе прекращается исполнение исходной транзакции, запустившей процесс.
Текущий вычислительный бюджет можно посмотреть здесь.
Как программно реализовать приоритетные комиссии
Приоритетная комиссия транзакции задается с помощью инструкции SetComputeUnitPricehttps://github.com/solana-labs/solana/blob/2971e84ec87815adb1e4def95cbcd8d0d96845f5/sdk/src/compute_budget.rs#L59и необязательной инструкции SetComputeUnitLimit. Если инструкция SetComputeUnitPrice не указана, транзакция по умолчанию получит самый низкий приоритет, поскольку дополнительная комиссия отсутствует. Если инструкция SetComputeUnitLimit не указана, лимит рассчитывается как произведение количества инструкций в транзакции и стандартного лимита вычислительных единиц. Среда выполнения использует цену и лимит вычислительных единиц для расчета приоритетной комиссии, которая определяет приоритет данной транзакции.
Чтобы программно добавить приоритетную комиссию, нужно включить эти инструкции в нужную транзакцию. В Javascript это выглядит так:
import {
Keypair,
Connection,
PublicKey,
Transaction,
SystemProgram,
LAMPORTS_PER_SOL,
sendAndConfirmTransaction,
ComputeBudgetProgram,
} from "@solana/web3.js";
async function main() {
// Initialize an RPC client
const clusterUrl = "http://127.0.0.1:8899";
const connection = new Connection(clusterUrl, "confirmed");
// Initialize new sender and receiver keypairs
const fromKeypair = Keypair.generate();
const toPubkey = new PublicKey(Keypair.generate().publicKey);
// Airdrop SOL to the from_keypair
const airdropAmount = 100 * LAMPORTS_PER_SOL;
try {
const signature = await connection.requestAirdrop(
fromKeypair.publicKey,
airdropAmount
);
console.log("Airdrop requested. Signature:", signature);
await connection.confirmTransaction({
signature,
confirmation: "confirmed",
});
} catch (e) {
console.error("Failed to request airdrop:", e);
return;
}
// Check if airdrop was successful
const balance = await connection.getBalance(fromKeypair.publicKey);
if (balance < airdropAmount) {
console.error(
"Airdrop was not successful. The current balance is insufficient"
);
return;
}
// Airdrop SOL to the toPubkey
const airdropAmountTo = 100 * LAMPORTS_PER_SOL; // 1 SOL in lamports
try {
const signature = await connection.requestAirdrop(toPubkey, airdropAmount);
console.log("Airdrop requested. Signature:", signature);
await connection.confirmTransaction({
signature,
confirmation: "confirmed",
});
} catch (e) {
console.error("Failed to request airdrop:", e);
return;
}
// Check if airdrop was successful
const balanceTo = await connection.getBalance(toPubkey);
if (balance < airdropAmount) {
console.error(
"Airdrop was not successful. The current balance is insufficient"
);
return;
}
console.log(`Account balance: ${balance / LAMPORTS_PER_SOL} SOL`);
// Create the priority fee instructions
const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 1,
});
const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
units: 200_000,
});
// Create the transfer instruction
const transferIx = SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey,
lamports: 100_000,
});
// Create the transaction with priority fees
const transaction = new Transaction().add(
computePriceIx,
computeLimitIx,
transferIx
);
// Fetch the recent blockhash and sign the transaction
transaction.recentBlockhash = (
await connection.getLatestBlockhash()
).blockhash;
transaction.sign(fromKeypair);
// Send the transaction
try {
const txid = await sendAndConfirmTransaction(connection, transaction, [
fromKeypair,
]);
console.log("Transaction sent successfully with signature", txid);
} catch (e) {
console.error("Failed to send transaction:", e);
}
}
main();В этом фрагменте мы:
- Настраиваем тестовую среду с помощью Localhost
- Создаем два новых кошелька:
fromKeypairиtoPubkey - Начисляем каждому кошельку по 100 SOL через airdrop
- Проверяем успешность airdrop
- Создаем инструкции приоритетной комиссии:
computePriceIxиcomputeLimitIx - Создаем инструкцию перевода 100 000 лампортов с
fromKeypairнаtoPubkey - Создаем новую транзакцию и добавляем в нее все инструкции
- Добавляем в транзакцию последний хеш блока и подписываем ее
- Отправляем транзакцию и подтверждаем успешную отправку
Вот и все — приоритетные комиссии представляют собой инструкции, которые добавляются в транзакцию для оплаты более быстрого исполнения. Большая часть этого фрагмента настраивает среду разработки и два кошелька для перевода SOL между ними. Нас интересует создание инструкций для добавления приоритетных комиссий и их включение в транзакцию:
// Other code
const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 1,
});
const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
units: 200_000,
});
// Other code
const transaction = new Transaction().add(computePriceIx, computeLimitIx, transferIx);
// Rest of the codeРекомендации
Порядок этих инструкций важен, поскольку они исполняются последовательно. Например, если транзакция превысит стандартный лимит вычислений, то есть 200 тыс. CU, до его увеличения, она завершится с ошибкой. Допустим, у нас есть следующая транзакция:
- Инструкция 1 использует 100 тыс. CU
- Инструкция 2 использует 150 тыс. CU
- Инструкция 3 увеличивает лимит вычислений
Эта транзакция завершится с ошибкой, если не поменять местами инструкции 2 и 3. Поэтому инструкцию лимита вычислений рекомендуется добавлять раньше других инструкций транзакции. Помните: для добавления приоритетных комиссий к транзакции необязательно использовать инструкцию SetComputeLimit — она полностью опциональна. Расположение инструкции SetComputePrice значения не имеет.
Для получения списка недавно уплаченных приоритетных комиссий рекомендуется использовать RPC-метод getRecentPrioritizationFees. Эти данные позволяют оценить подходящую приоритетную комиссию, чтобы транзакция была обработана кластером, а расходы оставались минимальными. Кроме того, Helius предлагает новый API приоритетных комиссий, который мы рассмотрим в следующем разделе.
Транзакциям также следует запрашивать минимальное количество вычислительных единиц, необходимое для исполнения, чтобы снизить комиссии. Учтите, что стоимость не корректируется, если количество запрошенных вычислительных единиц превышает фактически использованное транзакцией.
Некоторые провайдеры кошельков, например Phantom, учитывают, что dApp могут задавать приоритетные комиссии за транзакции. Однако они не рекомендуют делать это, поскольку такой подход часто создает лишние сложности для конечных пользователей. Вместо этого разработчикам dApp советуют позволить Phantom применять приоритетные комиссии от имени пользователя. Например, Solfare автоматически определяет нагрузку на Solana и немного повышает комиссию, чтобы ваша транзакция получила приоритет перед другими.
Helius Priority Fee API
Расчет приоритетных комиссий для каждого слота с помощью RPC-метода getRecentPrioritizationFees интуитивно понятен. Однако он может быть сложным из-за постоянно меняющегося состояния сети и характера ответа getRecentPrioritizationFees': метод возвращает список значений за последние 150 блоков, который полезен только для оценки минимального размера комиссии.
Helius Priority Fee API предоставляет новый метод getPriorityFeeEstimate, который сводит ответ к одному значению с учетом глобального и локального рынков комиссий. Для расчета метод использует предварительно заданный набор процентилей. Эти процентили, или уровни, варьируются от NONE (нулевой процентиль) до UNSAFE_MAX (сотый процентиль, помеченный как небезопасный, чтобы пользователи случайно не израсходовали все средства). Пользователи также могут запросить все уровни приоритета и скорректировать используемый в расчете диапазон с помощью lookbackSlots. Поэтому, как и при использовании getRecentPrioritizationFees, пользователи могут учитывать в расчете оценки определенное количество предыдущих слотов.
Например, с помощью следующего скрипта можно рассчитать все уровни приоритетной комиссии для программы Jupiter v6:
const url = `https://mainnet.helius-rpc.com/?api-key=`;
const getRecentPrioritizationFees = async () => {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getPriorityFeeEstimate",
params: [{
"accountKeys": ["JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"],
"options": {
"includeAllPriorityFeeLevels": true,
}
}]
}),
});
const data = await response.json();
console.log("Fee: ", data);
};
getRecentPrioritizationFees();Этот эндпоинт активно разрабатывается. Дополнительную информацию см. в документации Helius.
Заключение
Изучение транзакций Solana раскрывает сложную систему, которая поддерживает баланс между эффективностью сети и экономическими стимулами. Понимание работы транзакций, комиссий и приоритетных комиссий помогает разработчикам и пользователям принимать более обоснованные решения и оптимизировать взаимодействие с Solana. Возможность программно добавлять приоритетные комиссии открывает новые возможности для срочных транзакций и транзакций с высокой стоимостью. Это повышает гибкость и эффективность операций в Solana.
Если вы дочитали до этого места, спасибо, anon! Обязательно укажите ниже свой адрес электронной почты, чтобы не пропускать новости Solana. Готовы узнать больше? Присоединяйтесь к нашему Discord и начните создавать будущее на самом производительном блокчейне уже сегодня.
Дополнительные материалы
Похожие статьи
Подпишитесь на Helius
Следите за новостями разработки на Solana и получайте уведомления о наших публикациях


