# Durable Transaction Nonces This guide covers Solana's durable transaction nonces, which enable transactions to remain valid indefinitely by replacing the time-limited recent blockhash mechanism. Essential for offline signing, multi-signature coordination, and scheduled transaction execution. ## Introduction ### The Expiration Problem Solana transactions normally include a `recent_blockhash` field that serves two purposes: 1. **Double-spend prevention**: Ensures each transaction is unique and can only be processed once 2. **Transaction freshness**: Limits transaction validity to prevent spam and stale transactions **The limitation:** - Recent blockhashes expire after **150 blocks** (~60-90 seconds) - Transactions with expired blockhashes are permanently rejected - Cannot be re-validated, even with identical content **Critical constraint:** ``` Transaction must be: 1. Signed with recent blockhash 2. Submitted to network 3. Processed by validator 4. Confirmed in block All within ~60-90 seconds! ``` ### Problems This Creates **Hardware Wallet Users:** - Fetch blockhash from network - Transfer to air-gapped device - User reviews and signs (can take minutes) - Transfer back to online device - Submit to network - **Risk**: Blockhash expires during manual review **Multi-Signature Wallets (DAOs, Squads, Realms):** - Create transaction with blockhash - Send to signer 1 for approval (hours/days) - Send to signer 2 for approval (hours/days) - Send to signer N for approval - **Risk**: Blockhash expires while collecting signatures **Scheduled Transactions:** - Want to pre-sign transaction for future execution - E.g., vesting unlock, scheduled payment, conditional trade - **Risk**: Cannot pre-sign hours/days in advance **Cross-Chain Bridges:** - Wait for finality on source chain (minutes/hours) - Sign transaction on destination chain - **Risk**: Blockhash expires during cross-chain confirmation ### The Solution: Durable Nonces Durable nonces replace `recent_blockhash` with a **stored on-chain value** that: - ✅ Never expires (remains valid indefinitely) - ✅ Changes with each use (prevents replay attacks) - ✅ Enables offline signing without time pressure - ✅ Supports multi-signature coordination - ✅ Allows pre-signing transactions for future execution **Key insight**: Instead of using the blockchain's recent history (blockhashes) to ensure uniqueness, durable nonces use a **dedicated account** that stores a nonce value and advances it after each transaction. ## How Durable Nonces Work ### Core Mechanism 1. **Nonce Account**: On-chain account (owned by System Program) storing a 32-byte nonce value 2. **Transaction Structure**: Use nonce value as `recent_blockhash` field 3. **Nonce Advancement**: First instruction MUST advance the nonce to prevent replay 4. **Authority Control**: Only nonce authority can advance nonce or authorize transactions ### Transaction Flow ``` Normal Transaction: 1. Fetch recent blockhash (expires in 90s) 2. Build transaction with blockhash 3. Sign transaction 4. Submit (must be within 90s) 5. Process and confirm Durable Nonce Transaction: 1. Create nonce account (one-time setup) 2. Fetch current nonce value (no expiration!) 3. Build transaction with nonce as blockhash 4. Add advance_nonce instruction (MUST be first) 5. Sign transaction (no time pressure) 6. Submit anytime (minutes, hours, days later) 7. Process: advances nonce, executes instructions ``` ### Double-Spend Prevention **Without expiration, how does it prevent double-spending?** The nonce value **changes** after each transaction: ```rust // Transaction 1 with nonce value "ABC123..." { recent_blockhash: "ABC123...", // Current nonce value instructions: [ advance_nonce_account(...), // Changes nonce to "XYZ789..." transfer(...), ] } // If you try to submit Transaction 1 again: // Runtime checks: Is "ABC123..." the current nonce? // NO! It's now "XYZ789..." // Transaction REJECTED (nonce mismatch) ``` **Critical**: The runtime **always** advances the nonce, even if the transaction fails after the advance instruction. This prevents replay attacks. ## Nonce Account Structure ### Account Layout Nonce accounts are owned by the System Program and have this structure: ```rust pub struct NonceState { pub version: NonceVersion, } pub enum NonceVersion { Legacy(Box), Current(Box), } pub struct NonceData { pub authority: Pubkey, // Who can authorize nonce operations pub durable_nonce: Hash, // The actual nonce value (32 bytes) pub fee_calculator: FeeCalculator, // Historic fee data } ``` **Account requirements:** - **Owner**: System Program (`11111111111111111111111111111111`) - **Size**: 80 bytes - **Rent exemption**: Required (minimum ~0.00144768 SOL) - **Authority**: Can be any pubkey (keypair or PDA) ### Nonce Authority The authority pubkey controls the nonce account: - **Can**: Advance nonce, withdraw funds, authorize nonce transactions, change authority - **Cannot**: Execute other instructions without nonce advancement (runtime enforces this) **Authority options:** - **Keypair**: Direct control (hot wallet, cold wallet) - **PDA**: Program-controlled nonces (advanced use case) - **Multisig**: Multiple signers required (DAO wallets) ## Creating Nonce Accounts ### Using Native Rust ```rust use solana_sdk::{ instruction::Instruction, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, sysvar::rent::Rent, transaction::Transaction, }; use solana_client::rpc_client::RpcClient; fn create_nonce_account( rpc_client: &RpcClient, payer: &Keypair, nonce_account: &Keypair, authority: &Pubkey, ) -> Result<(), Box> { // Calculate rent-exempt balance for nonce account let rent = rpc_client.get_minimum_balance_for_rent_exemption(80)?; // Create account instruction let create_account_ix = system_instruction::create_account( &payer.pubkey(), &nonce_account.pubkey(), rent, // Lamports (rent-exempt minimum) 80, // Space (nonce account size) &solana_program::system_program::id(), // Owner (System Program) ); // Initialize nonce instruction let initialize_nonce_ix = system_instruction::initialize_nonce_account( &nonce_account.pubkey(), authority, // Nonce authority ); // Build transaction let recent_blockhash = rpc_client.get_latest_blockhash()?; let transaction = Transaction::new_signed_with_payer( &[create_account_ix, initialize_nonce_ix], Some(&payer.pubkey()), &[payer, nonce_account], // Both payer and nonce account must sign recent_blockhash, ); // Send transaction let signature = rpc_client.send_and_confirm_transaction(&transaction)?; println!("Created nonce account: {}", signature); Ok(()) } ``` ### Single-Step Creation There's also a convenience function that combines both steps: ```rust let instruction = system_instruction::create_nonce_account( &payer.pubkey(), &nonce_account.pubkey(), authority, rent_lamports, ); // This creates a single instruction that: // 1. Creates the account // 2. Initializes it as a nonce account ``` ### Using CLI ```bash # Generate keypair for nonce account solana-keygen new -o nonce-account.json # Create nonce account solana create-nonce-account nonce-account.json 0.0015 # Verify creation solana nonce nonce-account.json # Output: Current nonce value (32-byte hash) ``` ## Querying Nonce Accounts ### Fetching Nonce Value ```rust use solana_sdk::account::Account; use solana_program::system_program; fn get_nonce_value( rpc_client: &RpcClient, nonce_pubkey: &Pubkey, ) -> Result> { // Fetch account data let account = rpc_client.get_account(nonce_pubkey)?; // Verify it's a nonce account if account.owner != system_program::id() { return Err("Account is not owned by System Program".into()); } // Deserialize nonce data let nonce_data = bincode::deserialize::(&account.data)?; match nonce_data { NonceState::Current(data) => Ok(data.durable_nonce), NonceState::Legacy(data) => Ok(data.durable_nonce), } } ``` ### Parsing Nonce Account ```rust use solana_program::nonce::state::{Data, State}; fn parse_nonce_account(account_data: &[u8]) -> Result> { let state: State = bincode::deserialize(account_data)?; match state { State::Initialized(data) => Ok(data), State::Uninitialized => Err("Nonce account not initialized".into()), } } // Access nonce components fn display_nonce_info(nonce_data: &Data) { println!("Authority: {}", nonce_data.authority); println!("Nonce value: {}", nonce_data.blockhash); println!("Fee calculator: {:?}", nonce_data.fee_calculator); } ``` ## Building Transactions with Durable Nonces ### Transaction Structure **Critical requirements:** 1. **First instruction** MUST be `advance_nonce_account` 2. Use nonce value as `recent_blockhash` 3. Sign with nonce authority (in addition to other required signers) ```rust use solana_sdk::{ hash::Hash, instruction::Instruction, message::Message, signature::{Keypair, Signer}, system_instruction, transaction::Transaction, }; fn build_nonce_transaction( nonce_pubkey: &Pubkey, nonce_authority: &Keypair, nonce_value: Hash, instructions: Vec, payer: &Keypair, ) -> Transaction { // 1. Create advance_nonce instruction (MUST BE FIRST) let advance_nonce_ix = system_instruction::advance_nonce_account( nonce_pubkey, &nonce_authority.pubkey(), ); // 2. Combine with your instructions let mut all_instructions = vec![advance_nonce_ix]; all_instructions.extend(instructions); // 3. Build message with nonce as blockhash let message = Message::new_with_blockhash( &all_instructions, Some(&payer.pubkey()), &nonce_value, // Use nonce value instead of recent blockhash! ); // 4. Sign with both payer and nonce authority let mut signers = vec![payer]; if nonce_authority.pubkey() != payer.pubkey() { signers.push(nonce_authority); } Transaction::new(&signers, message, nonce_value) } ``` ### Complete Example: Transfer with Durable Nonce ```rust fn transfer_with_nonce( rpc_client: &RpcClient, nonce_account: &Pubkey, nonce_authority: &Keypair, payer: &Keypair, recipient: &Pubkey, amount: u64, ) -> Result<(), Box> { // 1. Fetch current nonce value let nonce_value = get_nonce_value(rpc_client, nonce_account)?; // 2. Create transfer instruction let transfer_ix = system_instruction::transfer( &payer.pubkey(), recipient, amount, ); // 3. Build transaction with nonce let transaction = build_nonce_transaction( nonce_account, nonce_authority, nonce_value, vec![transfer_ix], payer, ); // 4. Can now submit immediately or store for later // No expiration pressure! let signature = rpc_client.send_and_confirm_transaction(&transaction)?; println!("Transfer completed: {}", signature); Ok(()) } ``` ### Serializing for Offline Signing ```rust use base58::ToBase58; fn serialize_for_offline_signing(transaction: &Transaction) -> String { // Serialize transaction to bytes let serialized = bincode::serialize(transaction).unwrap(); // Encode as base58 for transport serialized.to_base58() } fn deserialize_signed_transaction(base58_tx: &str) -> Transaction { use base58::FromBase58; let bytes = base58_tx.from_base58().unwrap(); bincode::deserialize(&bytes).unwrap() } ``` ## Managing Nonce Accounts ### Advancing Nonce **Automatic advancement**: When you submit a transaction with a durable nonce, the runtime automatically advances the nonce as part of processing the `advance_nonce_account` instruction. **Manual advancement** (without submitting transaction): ```rust fn advance_nonce_manually( rpc_client: &RpcClient, nonce_account: &Pubkey, nonce_authority: &Keypair, payer: &Keypair, ) -> Result<(), Box> { let advance_ix = system_instruction::advance_nonce_account( nonce_account, &nonce_authority.pubkey(), ); let recent_blockhash = rpc_client.get_latest_blockhash()?; let transaction = Transaction::new_signed_with_payer( &[advance_ix], Some(&payer.pubkey()), &[payer, nonce_authority], recent_blockhash, ); rpc_client.send_and_confirm_transaction(&transaction)?; Ok(()) } ``` **When to manually advance:** - Before reusing nonce for a new transaction - To invalidate a previously signed transaction - Regular rotation for security ### Withdrawing from Nonce Account ```rust fn withdraw_from_nonce( rpc_client: &RpcClient, nonce_account: &Pubkey, nonce_authority: &Keypair, recipient: &Pubkey, amount: u64, payer: &Keypair, ) -> Result<(), Box> { let withdraw_ix = system_instruction::withdraw_nonce_account( nonce_account, &nonce_authority.pubkey(), recipient, amount, ); let recent_blockhash = rpc_client.get_latest_blockhash()?; let transaction = Transaction::new_signed_with_payer( &[withdraw_ix], Some(&payer.pubkey()), &[payer, nonce_authority], recent_blockhash, ); rpc_client.send_and_confirm_transaction(&transaction)?; Ok(()) } ``` **Important**: Must maintain rent-exempt minimum balance. Can only withdraw to zero if closing the account. ### Changing Nonce Authority ```rust fn change_nonce_authority( rpc_client: &RpcClient, nonce_account: &Pubkey, current_authority: &Keypair, new_authority: &Pubkey, payer: &Keypair, ) -> Result<(), Box> { let authorize_ix = system_instruction::authorize_nonce_account( nonce_account, ¤t_authority.pubkey(), new_authority, ); let recent_blockhash = rpc_client.get_latest_blockhash()?; let transaction = Transaction::new_signed_with_payer( &[authorize_ix], Some(&payer.pubkey()), &[payer, current_authority], recent_blockhash, ); rpc_client.send_and_confirm_transaction(&transaction)?; Ok(()) } ``` **Use cases:** - Transfer control to PDA for program-managed nonces - Rotate keys for security - Transfer to multisig for DAO control ## Offline Signing Workflows ### Hardware Wallet Flow **Setup (online device):** ```rust // 1. Create nonce account (one-time) create_nonce_account(&rpc_client, &payer, &nonce_account, &hw_wallet_pubkey)?; // 2. Fetch nonce value let nonce_value = get_nonce_value(&rpc_client, &nonce_account.pubkey())?; // 3. Build unsigned transaction let unsigned_tx = build_nonce_transaction( &nonce_account.pubkey(), &hw_wallet_keypair, // Will be replaced with actual signature nonce_value, vec![transfer_ix], &payer, ); // 4. Serialize for hardware wallet let serialized = serialize_for_offline_signing(&unsigned_tx); // 5. Transfer to hardware wallet (USB, QR code, etc.) ``` **Signing (air-gapped hardware wallet):** ```rust // 1. Receive serialized transaction let tx = deserialize_signed_transaction(&serialized); // 2. Display to user for review (no time pressure!) // User reviews: recipient, amount, etc. // 3. Sign with hardware wallet private key // (Hardware wallet handles this internally) // 4. Export signed transaction let signed_serialized = serialize_for_offline_signing(&signed_tx); // 5. Transfer back to online device ``` **Submission (online device):** ```rust // 1. Receive signed transaction let signed_tx = deserialize_signed_transaction(&signed_serialized); // 2. Submit to network (can be hours/days after signing!) let signature = rpc_client.send_and_confirm_transaction(&signed_tx)?; ``` ### Multi-Signature Coordination **DAO Proposal Execution Flow:** ```rust // 1. Proposer creates transaction with nonce let nonce_value = get_nonce_value(&rpc_client, &dao_nonce_account)?; let proposal_tx = build_nonce_transaction( &dao_nonce_account, &dao_authority, // PDA controlled by governance nonce_value, vec![execute_proposal_ix], &proposer, ); // 2. Serialize and store in DAO state let tx_data = bincode::serialize(&proposal_tx)?; // Store tx_data in proposal account // 3. Members vote over time (hours/days) // Each vote increments approval count // 4. When threshold reached, anyone can execute let stored_tx: Transaction = bincode::deserialize(&proposal.tx_data)?; // 5. Submit (nonce ensures it's still valid!) rpc_client.send_and_confirm_transaction(&stored_tx)?; ``` ### CLI Multi-Sig Example **First co-signer (offline):** ```bash solana transfer \ --from sender.json \ --sign-only \ --nonce nonce-account.json \ --nonce-authority nonce-authority.json \ --blockhash \ --fee-payer co-sender.json \ receiver.json 0.1 # Output: # Pubkey=Signature # 5nZ8nY5...=4SBv7Xp... ``` **Second co-signer (online, hours/days later):** ```bash solana transfer \ --from sender.json \ --nonce nonce-account.json \ --nonce-authority nonce-authority.json \ --blockhash \ --fee-payer sender.json \ --signer 5nZ8nY5...=4SBv7Xp... \ receiver.json 0.1 ``` ## Security Considerations ### The Neodyme Vulnerability (2020) **Historic issue**: Before Solana v1.3, there was a critical vulnerability in how durable nonce transactions were processed: **The bug:** 1. Transaction with durable nonce starts processing 2. Runtime advances nonce (changes state) 3. Later instruction in transaction fails 4. Runtime rolls back ALL state changes 5. **BUG**: Nonce advancement was rolled back too! 6. Attacker could replay the transaction **The exploit:** ```rust // Malicious transaction: { instructions: [ advance_nonce(...), // Advances nonce write_arbitrary_data(...), // Attacker's payload fail_intentionally(...), // Forces transaction to fail ] } // After rollback: // - Nonce reverted to original value // - Arbitrary data write WAS NOT rolled back // - Can replay transaction infinitely! ``` **Impact**: Could write arbitrary data to any account by replaying failed transactions. **Fix** (Solana v1.3+): Nonce advancement is now **permanent** even on transaction failure. The runtime explicitly handles nonce accounts separately from normal rollback logic. **Lesson**: This demonstrates why nonce advancement MUST happen regardless of transaction success/failure. ### Best Practices **1. Never reuse nonce without advancing** ```rust // BAD: Reusing nonce value let nonce = get_nonce_value(&rpc, &nonce_account)?; let tx1 = build_nonce_transaction(&nonce_account, &auth, nonce, vec![ix1], &payer); let tx2 = build_nonce_transaction(&nonce_account, &auth, nonce, vec![ix2], &payer); // If tx1 fails, tx2 might also fail with "nonce mismatch" // GOOD: Advance between uses let nonce1 = get_nonce_value(&rpc, &nonce_account)?; let tx1 = build_nonce_transaction(&nonce_account, &auth, nonce1, vec![ix1], &payer); rpc.send_and_confirm_transaction(&tx1)?; // Fetch fresh nonce (it was advanced) let nonce2 = get_nonce_value(&rpc, &nonce_account)?; let tx2 = build_nonce_transaction(&nonce_account, &auth, nonce2, vec![ix2], &payer); ``` **2. Protect nonce authority** ```rust // Use cold storage for nonce authority // OR use PDA with program logic to restrict usage let authority_pda = Pubkey::find_program_address( &[b"nonce_authority", dao.key().as_ref()], program_id, ); ``` **3. Maintain rent exemption** ```rust // Check before withdrawal let nonce_account = rpc.get_account(&nonce_pubkey)?; let rent = rpc.get_minimum_balance_for_rent_exemption(80)?; if nonce_account.lamports - withdraw_amount < rent { return Err("Would violate rent exemption".into()); } ``` **4. Verify nonce advancement in transaction** ```rust // In your program that uses nonce transactions: pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { // First account should be nonce account let nonce_account = &accounts[0]; // Verify it's a valid nonce account if nonce_account.owner != &system_program::id() { return Err(ProgramError::InvalidAccountData); } // Verify advance_nonce was called // (Runtime enforces this, but you can add checks) Ok(()) } ``` **5. Monitor nonce account balance** ```rust // Periodic check (e.g., daily job) fn check_nonce_health(rpc: &RpcClient, nonce: &Pubkey) -> Result<(), String> { let account = rpc.get_account(nonce) .map_err(|_| "Nonce account not found")?; let rent = rpc.get_minimum_balance_for_rent_exemption(80) .map_err(|_| "Failed to fetch rent")?; if account.lamports < rent { return Err(format!( "Nonce account below rent exemption: {} < {}", account.lamports, rent )); } Ok(()) } ``` ## Use Cases ### 1. Scheduled Payments (Vesting) ```rust // Pre-sign monthly vesting releases fn create_vesting_schedule( rpc: &RpcClient, nonce_account: &Pubkey, nonce_authority: &Keypair, recipient: &Pubkey, amount_per_month: u64, months: usize, ) -> Result, Box> { let mut transactions = Vec::new(); for month in 0..months { // Fetch current nonce let nonce = get_nonce_value(rpc, nonce_account)?; // Create transfer let transfer_ix = system_instruction::transfer( &nonce_authority.pubkey(), recipient, amount_per_month, ); // Build nonce transaction let tx = build_nonce_transaction( nonce_account, nonce_authority, nonce, vec![transfer_ix], nonce_authority, ); transactions.push(tx); // Advance nonce for next month's transaction advance_nonce_manually(rpc, nonce_account, nonce_authority, nonce_authority)?; } Ok(transactions) } // Executor submits each month fn execute_vesting_payment( rpc: &RpcClient, pre_signed_tx: &Transaction, ) -> Result<(), Box> { // No time pressure - can submit anytime! rpc.send_and_confirm_transaction(pre_signed_tx)?; Ok(()) } ``` ### 2. Conditional Trades (Limit Orders) ```rust // Pre-sign trade execution at specific price fn create_limit_order( nonce: &Pubkey, authority: &Keypair, swap_instruction: Instruction, // Execute when price reached ) -> Transaction { let nonce_value = /* fetch nonce */; build_nonce_transaction( nonce, authority, nonce_value, vec![swap_instruction], authority, ) } // Bot monitors price and submits when condition met fn execute_limit_order(rpc: &RpcClient, current_price: f64, limit_tx: &Transaction) { if current_price >= target_price { rpc.send_transaction(limit_tx).ok(); // Submit pre-signed transaction } } ``` ### 3. Cross-Chain Bridges ```rust // Sign Solana transaction while waiting for Ethereum finality async fn bridge_from_ethereum_to_solana( eth_tx_hash: H256, solana_mint_ix: Instruction, nonce_account: &Pubkey, nonce_authority: &Keypair, ) -> Result<(), Box> { // 1. Pre-sign Solana mint transaction let nonce = get_nonce_value(&solana_rpc, nonce_account)?; let mint_tx = build_nonce_transaction( nonce_account, nonce_authority, nonce, vec![solana_mint_ix], nonce_authority, ); // 2. Wait for Ethereum finality (12+ minutes) wait_for_ethereum_finality(eth_tx_hash).await?; // 3. Submit Solana transaction (still valid!) solana_rpc.send_and_confirm_transaction(&mint_tx)?; Ok(()) } ``` ### 4. DAO Governance Execution Already covered in multi-sig example above - proposals can be voted on over days/weeks, then executed with pre-signed transaction. ## CLI Reference **Create nonce account:** ```bash solana create-nonce-account ``` **Get current nonce:** ```bash solana nonce ``` **Manually advance nonce:** ```bash solana new-nonce ``` **Get nonce account info:** ```bash solana nonce-account ``` **Withdraw from nonce:** ```bash solana withdraw-from-nonce-account ``` **Change nonce authority:** ```bash solana authorize-nonce-account ``` **Sign transaction offline:** ```bash solana \ --sign-only \ --nonce \ --nonce-authority \ --blockhash ``` **Submit pre-signed transaction:** ```bash solana \ --nonce \ --nonce-authority \ --blockhash \ --signer ``` ## Limitations and Considerations **Transaction size:** - Adding `advance_nonce_account` instruction adds ~40 bytes - May push transaction over size limit if already near maximum **Extra signature requirement:** - Nonce authority must sign (if different from fee payer) - Increases transaction complexity **Rent cost:** - Each nonce account requires ~0.0015 SOL rent-exempt minimum - For many scheduled transactions, can become expensive **Nonce advancement overhead:** - Compute units to advance nonce (~few hundred CU) - Minimal but worth considering for CU-constrained transactions **Cannot mix recent blockhashes and nonces:** - Transaction uses either recent blockhash OR durable nonce - Cannot use both in the same transaction ## Resources ### Official Documentation - [Introduction to Durable Nonces](https://solana.com/developers/guides/advanced/introduction-to-durable-nonces) - [Durable Transaction Nonces Proposal](https://docs.anza.xyz/implemented-proposals/durable-tx-nonces) - [CLI Nonce Examples](https://docs.anza.xyz/cli/examples/durable-nonce) ### Code Examples - [Durable Nonces Repository](https://github.com/0xproflupin/solana-durable-nonces) - [System Program Source](https://github.com/solana-labs/solana/blob/master/sdk/program/src/system_instruction.rs) ### Security Analysis - [Neodyme: Nonce Upon a Time](https://neodyme.io/en/blog/nonce-upon-a-time/) - Historic vulnerability analysis ### Technical References - [solana-sdk NonceState](https://docs.rs/solana-sdk/latest/solana_sdk/nonce/state/enum.State.html) - [System Program Instructions](https://docs.rs/solana-sdk/latest/solana_sdk/system_instruction/)