Каждая транзакция в Solana содержит недавний blockhash — ссылку на актуальное состояние сети, подтверждающую, что транзакция создана «сейчас». Сеть отклоняет любую транзакцию с blockhash старше примерно 150 блоков (60–90 секунд), предотвращая повторные атаки и устаревшие отправки. Это отлично работает для платежей в реальном времени. Но такой подход не подходит для сценариев, где между подписанием и отправкой требуется пауза, например:
| Сценарий | Почему стандартные транзакции не подходят |
|---|---|
| Казначейские операции | CFO в Токио подписывает, контролёр в Нью-Йорке утверждает — 90 секунд недостаточно |
| Комплаенс-процедуры | Транзакции требуют юридической/комплаенс-проверки перед выполнением |
| Подпись в холодном хранилище | Air-gapped устройства требуют ручной передачи подписанных транзакций |
| Подготовка пакетов | Подготовка зарплаты или выплат в рабочее время, выполнение ночью |
| Координация мультиподписи | Несколько утверждающих в разных часовых поясах |
| Запланированные платежи | Платежи, которые должны быть выполнены в будущем |
В традиционных финансах подписанный чек не теряет силу через 90 секунд. Некоторые операции в блокчейне тоже не должны. Долговечные nonces решают эту задачу, заменяя недавний blockhash на сохранённое, постоянное значение, которое обновляется только при использовании — так транзакции остаются действительными до момента отправки.
Как это работает
Вместо использования недавнего blockhash (действителен примерно 150 блоков) применяется nonce-аккаунт — специальный аккаунт, который хранит уникальное значение, используемое вместо blockhash. Каждая транзакция с этим nonce должна "продвигать" его первой инструкцией. Каждое значение nonce может быть использовано только для одной транзакции.
Nonce-аккаунт требует ~0,0015 SOL для освобождения от rent. Один nonce-аккаунт = одна ожидающая транзакция одновременно. Для параллельных процессов создайте несколько nonce-аккаунтов.
Создание nonce-аккаунта
Создание nonce-аккаунта требует двух инструкций в одной транзакции:
- Создайте аккаунт с помощью
getCreateAccountInstructionиз System Program - Инициализируйте его как nonce с помощью
getInitializeNonceAccountInstruction
Генерация keypair
Сгенерируйте новую keypair для использования в качестве адреса nonce-аккаунта и рассчитайте необходимое пространство и rent.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();
Инструкция создания аккаунта
Создайте аккаунт, принадлежащий System Program, с достаточным количеством lamports для освобождения от rent.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();const createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});
Инструкция инициализации nonce
Инициализируйте аккаунт как nonce-аккаунт, указав authority, который сможет продвигать nonce.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();const createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});const initNonceIx = getInitializeNonceAccountInstruction({nonceAccount: nonceKeypair.address,nonceAuthority: sender.address});
Сборка транзакции
Соберите транзакцию с обеими инструкциями.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();const createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});const initNonceIx = getInitializeNonceAccountInstruction({nonceAccount: nonceKeypair.address,nonceAuthority: sender.address});const { value: blockhash } = await rpc.getLatestBlockhash().send();const createNonceTx = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),(tx) =>appendTransactionMessageInstructions([createNonceAccountIx, initNonceIx],tx));
Подпись и отправка
Подпишите и отправьте транзакцию для создания и инициализации nonce-аккаунта.
Сборка отложенной транзакции
Вместо недавнего blockhash используйте значение nonce из nonce-аккаунта как срок действия транзакции.
Получить nonce
Получите данные из nonce-аккаунта. Используйте значение nonce из nonce-аккаунта как срок действия транзакции.
{version: 1,state: 1,authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',lamportsPerSignature: 5000n}
Создать инструкцию перевода
Создайте инструкцию для вашего платежа. В этом примере показан перевод токенов.
Сборка транзакции с долговечным nonce
Используйте setTransactionMessageLifetimeUsingDurableNonce, чтобы установить
nonce как blockhash и автоматически добавить инструкцию advance nonce в начало.
Подписать транзакцию
Подпишите транзакцию. Теперь она использует долговечный nonce вместо стандартного blockhash.
Сохранение или отправка транзакции
После подписания закодируйте транзакцию для хранения. Когда будете готовы, отправьте её в сеть.
Кодирование для хранения
Закодируйте подписанную транзакцию в base64. Сохраните это значение в вашей базе данных.
Отправка транзакции
Отправьте подписанную транзакцию, когда будете готовы. Транзакция останется действительной, пока nonce не будет обновлён.
Демонстрация
// Generate keypairs for sender and recipientconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();console.log("Sender Address:", sender.address);console.log("Recipient Address:", recipient.address);// Demo Setup: Create RPC connection, mint, and token accountsconst { rpc, rpcSubscriptions, mint } = await demoSetup(sender, recipient);// =============================================================================// Step 1: Create a Nonce Account// =============================================================================const nonceKeypair = await generateKeyPairSigner();console.log("\nNonce Account Address:", nonceKeypair.address);const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();// Instruction to create new account for the nonceconst createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});// Instruction to initialize the nonce accountconst initNonceIx = getInitializeNonceAccountInstruction({nonceAccount: nonceKeypair.address,nonceAuthority: sender.address});// Build and send nonce account creation transactionconst { value: blockhash } = await rpc.getLatestBlockhash().send();const createNonceTx = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),(tx) =>appendTransactionMessageInstructions([createNonceAccountIx, initNonceIx],tx));const signedCreateNonceTx =await signTransactionMessageWithSigners(createNonceTx);assertIsTransactionWithBlockhashLifetime(signedCreateNonceTx);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedCreateNonceTx,{ commitment: "confirmed" });console.log("Nonce Account created.");// =============================================================================// Step 2: Token Payment with Durable Nonce// =============================================================================// Fetch current nonce value from the nonce accountconst { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);console.log("Nonce Account data:", nonceData);const [senderAta] = await findAssociatedTokenPda({mint: mint.address,owner: sender.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});const [recipientAta] = await findAssociatedTokenPda({mint: mint.address,owner: recipient.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});console.log("\nMint Address:", mint.address);console.log("Sender Token Account:", senderAta);console.log("Recipient Token Account:", recipientAta);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n // 0.25 tokens});// Create transaction message using durable nonce lifetime// setTransactionMessageLifetimeUsingDurableNonce automatically prepends// the AdvanceNonceAccount instructionconst transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) =>setTransactionMessageLifetimeUsingDurableNonce({nonce: nonceData.blockhash as string as Nonce,nonceAccountAddress: nonceKeypair.address,nonceAuthorityAddress: nonceData.authority},tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithDurableNonceLifetime(signedTransaction);const transactionSignature = getSignatureFromTransaction(signedTransaction);// Encode the transaction to base64, optionally save and send at a later timeconst base64EncodedTransaction =getBase64EncodedWireTransaction(signedTransaction);console.log("\nBase64 Encoded Transaction:", base64EncodedTransaction);// Send the encoded transaction, blockhash does not expireawait rpc.sendTransaction(base64EncodedTransaction, {encoding: "base64",skipPreflight: true}).send();console.log("\n=== Token Payment with Durable Nonce Complete ===");console.log("Transaction Signature:", transactionSignature);// =============================================================================// Demo Setup Helper Function// =============================================================================
Аннулирование ожидающей транзакции
Каждый nonce-аккаунт blockhash можно использовать только один раз. Чтобы
аннулировать ожидающую транзакцию или подготовить nonce-аккаунт к повторному
использованию, обновите его вручную:
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// Submit this instruction (with a regular blockhash) to invalidate any pending transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority});
Это сгенерирует новое значение nonce, и любая транзакция, подписанная со старым значением, станет навсегда недействительной.
Многосторонний процесс согласования
Десериализуйте транзакцию, чтобы добавить дополнительные подписи, затем снова сериализуйте для хранения или отправки:
import {getBase64Decoder,getTransactionDecoder,getBase64EncodedWireTransaction,partiallySignTransaction} from "@solana/kit";// Deserialize the stored transactionconst txBytes = getBase64Decoder().decode(serializedString);const partiallySignedTx = getTransactionDecoder().decode(txBytes);// Each approver adds their signatureconst fullySignedTx = await partiallySignTransaction([newSigner],partiallySignedTx);// Serialize again for storage or submissionconst serialized = getBase64EncodedWireTransaction(fullySignedTx);
Транзакцию можно сериализовать, хранить и передавать между участниками согласования. После сбора всех необходимых подписей отправьте её в сеть.
Особенности продакшена
Управление nonce-аккаунтами:
- Создайте пул nonce-аккаунтов для параллельной подготовки транзакций
- Отслеживайте, какие nonces "используются" (имеют неподтверждённые подписанные транзакции)
- Реализуйте повторное использование nonces после отправки или отмены транзакций
Безопасность:
- Владелец nonce-аккаунта контролирует возможность аннулирования транзакций. Для дополнительного контроля и разделения обязанностей рассмотрите возможность отделения владельца nonce от подписантов транзакций
- Любой, у кого есть сериализованные байты транзакции, может отправить её в сеть
Связанные ресурсы
Is this page helpful?