Кожна транзакція Solana містить нещодавній blockhash — посилання на недавній стан мережі, що підтверджує створення транзакції "зараз". Мережа відхиляє будь-яку транзакцію з blockhash старішим за ~150 блоків (~60-90 секунд), запобігаючи атакам повторного відтворення та застарілим поданням. Це ідеально працює для платежів у реальному часі. Але це порушує робочі процеси, які потребують проміжку між підписанням та поданням, наприклад:
| Сценарій | Чому стандартні транзакції не працюють |
|---|---|
| Операції казначейства | Фінансовий директор у Токіо підписує, контролер у Нью-Йорку затверджує — 90 секунд недостатньо |
| Робочі процеси комплаєнсу | Транзакції потребують юридичної перевірки/перевірки відповідності перед виконанням |
| Підписання холодного сховища | Ізольовані машини вимагають ручного передавання підписаних транзакцій |
| Підготовка пакетів | Підготовка зарплати або виплат у робочий час, виконання вночі |
| Координація мультипідпису | Кілька затверджувачів у різних часових поясах |
| Заплановані платежі | Планування платежів для виконання в майбутню дату |
У традиційних фінансах підписаний чек не втрачає чинності через 90 секунд. Певні блокчейн-операції також не повинні. Довговічні nonce вирішують це, замінюючи нещодавній 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, який може його просувати.
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 використовуйте blockhash nonce-акаунта як час
життя транзакції.
Отримання nonce
Отримайте дані з nonce-акаунта. Використовуйте blockhash з nonce-акаунта як
час життя транзакції.
{version: 1,state: 1,authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',lamportsPerSignature: 5000n}
Створення інструкції переказу
Створіть інструкцію для вашого платежу. Цей приклад показує переказ токенів.
Побудова транзакції з durable nonce
Використовуйте setTransactionMessageLifetimeUsingDurableNonce, який встановлює
nonce як blockhash і автоматично додає інструкцію просування nonce на початок.
Підписання транзакції
Підпишіть транзакцію. Тепер вона використовує durable 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-акаунтів для паралельної підготовки транзакцій
- Відстежуйте, які nonce «використовуються» (мають очікувані підписані транзакції)
- Реалізуйте повторне використання nonce після надсилання або скасування транзакцій
Безпека:
- Власник nonce контролює, чи можна анулювати транзакції. Розгляньте можливість відокремлення власника nonce від підписувачів транзакцій для додаткового контролю та розподілу обов'язків
- Будь-хто, хто має серіалізовані байти транзакції, може надіслати її в мережу
Пов'язані ресурси
Is this page helpful?