18 min read
Overview
Durable nonces are a handy tool that can be used to avoid transaction expirations. In this guide, we will show you how to use durable nonces to sign and send offline transactions on Solana without concern for your transaction expiring.
What You Will Do
In this guide, you will:
- Create a durable nonce account
- Create and serialize a transaction using the durable nonce
- Sign the transaction in a simulated offline environment
- Send the signed transaction to the Solana network
- Attempt several scenarios to test how nonces work
What You Will Need
- Basic knowledge of Solana Fundamentals
- Experience with Basic Solana Transactions
- Solana CLI latest version installed
Dependency | Version |
---|---|
node.js | 18.12.1 |
tsc | 5.0.2 |
ts-node | 10.9.1 |
solana-cli | 1.14.16 |
@solana/web3.js | 1.74.0 |
bs58 | 5.0.0 |
What is a Nonce?
A nonce is a number that is used only once. In the context of Solana, a nonce is a number used to prevent replay attacks. A replay attack is when a transaction is intercepted and resent to the network.
Typical Solana transactions include a recent blockhash in the transaction data so that the runtime can verify that the transaction is unique. To limit the amount of history that the runtime needs to double-check, Solana only looks at the last 150 blocks. This means the second transaction will fail if two identical transactions are sent within 150 blocks of each other. It also means stale transactions (older than 150 blocks) will fail.
Unfortunately, if you send transactions offline (or have other particularly time-consuming constraints), you may have issues with expiring transactions. This is where durable nonces come in. Solana allows you to create a special type of account, a nonce account. You can think of this account like your own private blockhash queue. You can generate new unique IDs, move forward to the next ID, or even transfer control of the queue to someone else. This account holds a unique value or nonce. You can include the nonce instead of the recent blockhash when creating a transaction. To prevent replay attacks, the nonce is changed each time by calling advanceNonceAccount
in the first instruction of the transaction. Transactions that attempt to use a nonce account without the nonce advancing will fail. Here's an example:
// The nonceAdvance method is on the SystemProgram class and returns a TransactionInstruction (like SystemProgram.transfer)
const advanceIx = SystemProgram.nonceAdvance({
authorizedPubkey: nonceAuthKeypair.publicKey,
noncePubkey: nonceKeypair.publicKey
})
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TRANSFER_AMOUNT,
});
const sampleTx = new Transaction();
// Add the nonce advance instruction to the transaction first
sampleTx.add(advanceIx, transferIx);
This is what Solana's Durable Nonces offer: a way to prepare a transaction in advance with a unique ID that will not get rejected for being too old. It is a way to set up transactions for later execution while preventing fraud and maintaining order in the transaction queue.
Create Durable Nonce Script
Set Up Your Environment
Let's create a new Node.js project and install the Solana-Web3.js library. In your terminal, enter the following commands in order:
mkdir offline-tx && cd offline-tx && echo > app.ts
npm init -y # or yarn init -y
npm install @solana/web3.js@1 bs58 # or yarn add @solana/web3.js@1 bs58
Open the app.ts
file in your favorite editor and add the following imports:
import { Connection, Keypair, LAMPORTS_PER_SOL, NonceAccount, NONCE_ACCOUNT_LENGTH, SystemProgram, Transaction, TransactionSignature, TransactionConfirmationStatus, SignatureStatus } from "@solana/web3.js";
import { encode, decode } from 'bs58';
import fs from 'fs';
We import necessary dependencies from @solana/web3.js
, bs58
(a JS package for doing base-58 encoding and decoding), and fs
(to allow us to read and write files to our project directory).
Let's declare a few constants that we will use throughout the guide. Add the following code to your app.ts
file below your imports:
const RPC_URL = 'http://127.0.0.1:8899';
const TRANSFER_AMOUNT = LAMPORTS_PER_SOL * 0.01;
const nonceAuthKeypair = Keypair.generate();
const nonceKeypair = Keypair.generate();
const senderKeypair = Keypair.generate();
const connection = new Connection(RPC_URL);
Let's break down what each of these is:
RPC_URL
- the URL of the default local Solana cluster (if you prefer to use devnet or mainnet, simply change the Connection URL to your QuickNode RPC endpoint)TRANSFER_AMOUNT
- the number of SOL we will transfer in our sample transactionnonceAuthKeypair
- the keypair for the nonce authority accountnonceKeypair
- the keypair for the nonce accountsenderKeypair
- the keypair for the sender accountconnection
- the connection to a local Solana cluster
Finally, create an async function called main
and add the following code:
async function main() {
const { useNonce, waitTime } = parseCommandLineArgs();
console.log(`Attempting to send a transaction using a ${useNonce ? "nonce" : "recent blockhash"}. Waiting ${waitTime}ms before signing to simulate an offline transaction.`)
try {
// Step 1 - Fund the nonce authority account
await fundAccounts([nonceAuthKeypair, senderKeypair]);
// Step 2 - Create the nonce account
await createNonce();
// Step 3 - Create a transaction
await createTx(useNonce);
// Step 4 - Sign the transaction offline
await signOffline(waitTime, useNonce);
// Step 5 - Execute the transaction
await executeTx();
} catch (error) {
console.error(error);
}
}
We are outlining the steps to create a nonce, generate a transaction, sign it offline, and execute it. We will fill in the details of each step as we go. We will also be using command line arguments to enable some scenario testing later in the guide. We will use a boolean, useNonce
, and a waitTime
in ms to help us test our offline signing.