Ogni transazione Solana include un blockhash recente, un riferimento a uno stato recente della rete che dimostra che la transazione è stata creata "ora". La rete rifiuta qualsiasi transazione con un blockhash più vecchio di ~150 blocchi (~60-90 secondi), prevenendo attacchi replay e invii obsoleti. Questo funziona perfettamente per i pagamenti in tempo reale. Ma interrompe i flussi di lavoro che richiedono un intervallo tra firma e invio, come:
| Scenario | Perché le transazioni standard falliscono |
|---|---|
| Operazioni di tesoreria | Il CFO a Tokyo firma, il Controller a NYC approva: 90 secondi non bastano |
| Flussi di conformità | Le transazioni richiedono revisione legale/di conformità prima dell'esecuzione |
| Firma da cold storage | Le macchine air-gapped richiedono trasferimento manuale delle transazioni firmate |
| Preparazione batch | Prepara buste paga o erogazioni durante l'orario lavorativo, esegui durante la notte |
| Coordinamento multi-firma | Più approvatori in fusi orari diversi |
| Pagamenti programmati | Programma pagamenti da eseguire in una data futura |
Nella finanza tradizionale, un assegno firmato non scade in 90 secondi. Anche certe operazioni blockchain non dovrebbero. I nonce durevoli risolvono questo problema sostituendo il blockhash recente con un valore persistente memorizzato che avanza solo quando lo usi, offrendoti transazioni che rimangono valide finché non sei pronto a inviarle.
Come funziona
Invece di un blockhash recente (valido ~150 blocchi), usi un account nonce, un account speciale che memorizza un valore univoco. Ogni transazione che usa questo nonce deve "avanzarlo" come prima istruzione, prevenendo attacchi replay.
┌─────────────────────────────────────────────────────────────────────────────┐│ STANDARD BLOCKHASH ││ ││ ┌──────┐ ┌──────────┐ ││ │ Sign │ ───▶ │ Submit │ ⏱️ Must happen within ~90 seconds ││ └──────┘ └──────────┘ ││ │ ││ └───────── Transaction expires if not submitted in time │└─────────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────────┐│ DURABLE NONCE ││ ││ ┌──────┐ ┌───────┐ ┌─────────┐ ┌──────────┐ ││ │ Sign │ ───▶ │ Store │ ───▶ │ Approve │ ───▶ │ Submit │ ││ └──────┘ └───────┘ └─────────┘ └──────────┘ ││ ││ Transaction remains valid until you submit it │└─────────────────────────────────────────────────────────────────────────────┘
L'account nonce costa ~0.0015 SOL per l'esenzione dal rent. Un account nonce = una transazione in sospeso alla volta. Per flussi di lavoro paralleli, crea più account nonce.
Configurazione: creare un account nonce
La creazione di un account nonce richiede due istruzioni in una singola transazione:
- Creare l'account utilizzando
getCreateAccountInstructiondal System Program - Inizializzarlo come nonce utilizzando
getInitializeNonceAccountInstruction
import { generateKeyPairSigner } from "@solana/kit";import {getNonceSize,getCreateAccountInstruction,getInitializeNonceAccountInstruction,SYSTEM_PROGRAM_ADDRESS} from "@solana-program/system";// Generate a keypair for the nonce account addressconst nonceKeypair = await generateKeyPairSigner();// Get required account size for rent calculationconst space = BigInt(getNonceSize());// 1. Create the account (owned by System Program)getCreateAccountInstruction({payer,newAccount: nonceKeypair,lamports: rent,space,programAddress: SYSTEM_PROGRAM_ADDRESS});// 2. Initialize as nonce accountgetInitializeNonceAccountInstruction({nonceAccount: nonceKeypair.address,nonceAuthority: authorityAddress // Controls nonce advancement});// Assemble and send transaction to the network
Costruire una transazione differita
Due differenze chiave rispetto alle transazioni standard:
- Utilizzare il valore nonce come blockhash
- Aggiungere
advanceNonceAccountcome prima istruzione
Recuperare il valore nonce
import { fetchNonce } from "@solana-program/system";const nonceAccount = await fetchNonce(rpc, nonceAddress);const nonceValue = nonceAccount.data.blockhash; // Use this as your "blockhash"
Impostare la durata della transazione con nonce
Invece di utilizzare un blockhash recente che scade, utilizza il valore nonce:
import { setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";setTransactionMessageLifetimeUsingBlockhash({blockhash: nonceAccount.data.blockhash,lastValidBlockHeight: BigInt(2n ** 64n - 1n) // Effectively never expires},transactionMessage);
Avanzare il nonce (prima istruzione obbligatoria)
Ogni transazione con nonce durevole deve includere advanceNonceAccount
come prima istruzione. Questo previene attacchi replay invalidando il valore
nonce dopo l'uso e aggiornando il valore nonce.
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// MUST be the first instruction in your transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority // Signer that controls the nonce});
Firmare e memorizzare
Dopo la costruzione, firma la transazione e serializzala per la memorizzazione:
import {signTransactionMessageWithSigners,getTransactionEncoder,getBase64EncodedWireTransaction} from "@solana/kit";// Sign the transactionconst signedTx = await signTransactionMessageWithSigners(transactionMessage);// Serialize for storage (database, file, etc.)const txBytes = getTransactionEncoder().encode(signedTx);const serialized = getBase64EncodedWireTransaction(txBytes);
Memorizza la stringa serializzata nel tuo database: rimane valida finché il nonce non viene avanzato.
Flusso di lavoro di approvazione multi-parte
Deserializza la transazione per aggiungere firme aggiuntive, quindi serializza nuovamente per la memorizzazione o l'invio:
import {getBase64Decoder,getTransactionDecoder,getTransactionEncoder,getBase64EncodedWireTransaction} from "@solana/kit";// Deserialize the stored transactionconst txBytes = getBase64Decoder().decode(serializedString);const partiallySignedTx = getTransactionDecoder().decode(txBytes);// Each approver adds their signatureconst fullySignedTx = await newSigner.signTransactions([partiallySignedTx]);// Serialize again for storageconst txBytes = getTransactionEncoder().encode(fullySignedTx);const serialized = getBase64EncodedWireTransaction(txBytes);
La transazione può essere serializzata, memorizzata e passata tra gli approvatori. Una volta raccolte tutte le firme richieste, inviala alla rete.
Esegui quando pronto
Quando le approvazioni sono complete, invia la transazione serializzata alla rete:
const signature = await rpc.sendTransaction(serializedTransaction, { encoding: "base64" }).send();
Ogni nonce può essere utilizzato una sola volta. Se una transazione fallisce o decidi di non inviarla, devi avanzare il nonce prima di preparare un'altra transazione con lo stesso account nonce.
Avanzamento di un nonce utilizzato o abbandonato
Per invalidare una transazione in sospeso o preparare il nonce per il riutilizzo, avanzalo manualmente:
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// Submit this instruction (with a regular blockhash) to invalidate any pending transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority});
Questo genera un nuovo valore nonce, rendendo qualsiasi transazione firmata con il vecchio valore permanentemente non valida.
Considerazioni per la produzione
Gestione dell'account nonce:
- Crea un pool di account nonce per la preparazione parallela delle transazioni
- Tieni traccia di quali nonce sono "in uso" (hanno transazioni firmate in sospeso)
- Implementa il riciclo dei nonce dopo che le transazioni sono state inviate o abbandonate
Sicurezza:
- L'autorità nonce controlla se le transazioni possono essere invalidate. Considera di separare l'autorità nonce dai firmatari delle transazioni per un controllo aggiuntivo e separazione dei compiti
- Chiunque abbia i byte della transazione serializzata può inviarla alla rete
Risorse correlate
Is this page helpful?