
Transacciones de Solana: nonces duraderos (2023)
Introducción
Cuando envías una transacción en Solana como desarrollador, debes obtener el blockhash más reciente mediante el RPC de Solana. Este paso crucial ayuda a mitigar los ataques de repetición. Garantiza que, una vez que una transacción se firma y envía con un blockhash específico, nadie pueda usar el mismo hash para repetir o volver a enviar esa transacción.
Imagina que necesitas enviar una transacción que requiere la firma de un almacenamiento en frío sin conexión o una billetera de hardware como Ledger. Sin embargo, los blockhashes vencen rápidamente y podrían invalidar tu transacción. Aquí es donde entran los nonces duraderos, que permiten realizar transacciones seguras sin conexión.
Al finalizar esta guía, comprenderás:
- Qué es un nonce duradero.
- Para qué sirve un nonce duradero.
- Cómo usar nonces duraderos en transacciones.
Transacciones: requisitos previos
Antes de comenzar, asegúrate de tener:
- Conocimientos básicos de JavaScript.
- NodeJS instalado.
- Solana CLI instalado.
- Git instalado
Configuración del entorno
- Clona nuestro repositorio de ejemplo con las utilidades existentes:
git clone- Ve a la carpeta del proyecto e instala npm:
cd durable-noncenpm install- Ve a la carpeta de billeteras dentro de la carpeta nonce. Allí almacenaremos nuestras claves locales para las pruebas:
cd wallets- Con Solana CLI instalado, crea una billetera para el par de claves pagador:
solana-keygen new -o ./wallet.json- Ahora configúrala como tu billetera en la CLI para recibir Solana mediante un airdrop:
solana config set --keypair ./wallet.json- Ahora puedes enviar Solana mediante un airdrop a esta dirección ejecutando lo siguiente:
solana aidrop 2- También debemos crear y financiar otra billetera para la autoridad del nonce:
solana-keygen new -o ./nonceAuth.json- Para la clave pública generada, podemos usar un sitio de faucet para enviarle 1 SOL mediante un airdrop, aquí.
Ahora que configuramos nuestro entorno, podemos continuar con los siguientes pasos.
¿Qué es un nonce duradero?
Una cuenta de nonce duradero en Solana puede verse como una caja de seguridad. Cuando creas esta cuenta, Solana le asigna un código único y estable llamado «nonce duradero». A diferencia de los nonces habituales, que cambian con cada transacción, este permanece estable y sirve como referencia constante.
Esto resulta especialmente útil para las transacciones «sin conexión». Cuando creas una transacción, haces referencia a este nonce desde tu cuenta. Solana lo valida comparándolo con el valor almacenado y, si coinciden, aprueba la transacción. Por lo tanto, una cuenta de nonce duradero funciona como mecanismo de almacenamiento y validación. Garantiza la autenticidad de las transacciones y, al mismo tiempo, se adapta al ritmo acelerado y a los casos sin conexión de la red de Solana.
Los nonces duraderos pueden usarse en varios casos, como los siguientes:
- Transacciones programadas: Puedes configurar transacciones para que se ejecuten en un momento determinado en el futuro. Los nonces duraderos garantizan que estas transacciones programadas se ejecuten de forma segura.
- Billeteras multifirma: En el contexto de las billeteras multifirma, los nonces duraderos ofrecen una capa adicional de seguridad y coordinación entre varios firmantes.
- Programas que necesitan interacciones futuras: Algunos programas de Solana requieren interactuar con otros programas o servicios en intervalos específicos. Los nonces duraderos ayudan a mantener la integridad de estas interacciones.
- Interacción con otras blockchains: Cuando Solana interactúa con otras blockchains, los nonces duraderos ayudan a garantizar la validez de las transacciones entre cadenas.
Ahora podemos comenzar a crear nuestro ejemplo.
Transacciones de Solana: pasos para la implementación
Paso 1: Configura las dependencias y constantes
En este paso, importarás los módulos y las utilidades necesarios, y definirás las constantes y los pares de claves del ejemplo. Estas dependencias y constantes se usarán durante todo el proceso de la transacción.
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
NonceAccount,
NONCE_ACCOUNT_LENGTH,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { encodeAndWriteTransaction, loadWallet, readAndDecodeTransaction } from "./utils";
const nonceAuthKeypair = loadWallet('./wallets/nonceAuth.json');
const nonceKeypair = Keypair.generate();
const senderKeypair = loadWallet('./wallets/wallet.json');
const connection = new Connection("https://devnet.helius-rpc.com/?api-key=");
const waitTime = 120000;
const TranferAmount = LAMPORTS_PER_SOL * 0.01;Paso 2: Crea la función sendTransaction
La función sendTransaction coordina el proceso para enviar una transacción con un nonce duradero. Esta función se encarga de crear y confirmar el nonce, además de ejecutar la transacción.
async function sendTransaction() {
console.log("Starting Nonce Transaction")
try {
const nonceCreationTxSig = await nonce();
const confirmationStatus = await connection.confirmTransaction(nonceCreationTxSig);
if (!confirmationStatus.value.err) {
console.log("Nonce account creation confirmed.");
const nonce = await getNonce();
await createTx(nonce);
await signOffline(waitTime);
await executeTx();
} else {
console.error("Nonce account creation transaction failed:", confirmationStatus.value.err);
}
} catch (error) {
console.error(error);
}
}Paso 3: Crea la función nonce
La función nonce se encarga de crear e inicializar la cuenta de nonce duradero. Esto implica calcular el alquiler requerido para la cuenta, obtener el blockhash más reciente y crear las transacciones que crearán e inicializarán la cuenta de nonce.
- Antes de crear la cuenta de nonce, debemos calcular el alquiler requerido para almacenar los datos de la cuenta y obtener el blockhash más reciente.
async function nonce() {
const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();- Ahora crearemos una transacción para crear la cuenta de nonce. Para ello, usaremos la instrucción
SystemProgram.createAccounta fin de asignar espacio para la cuenta de nonce.
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);- Firmaremos la transacción con los pares de claves de la autoridad y la enviaremos a la red de Solana. Esta transacción crea la cuenta de nonce duradero.
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
const signature = await connection.sendRawTransaction(createNonceTx.serialize(), { skipPreflight: false, preflightCommitment: "single" });- Después de enviar la transacción, confirmaremos su estado para verificar que la cuenta de nonce se haya creado correctamente.
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error("Nonce account creation transaction failed: " + confirmationStatus.value.err);
}
console.log("Nonce account created:", signature);- Para aprovechar por completo la cuenta de nonce, debemos inicializar su valor. Crearemos una nueva transacción para ejecutar la instrucción
SystemProgram.nonceInitialize.
// Initialize the nonce value within the account
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);- Al igual que en el paso anterior, firmaremos la transacción y la enviaremos a la red para inicializar la cuenta de nonce.
const { blockhash: initBlockhash, lastValidBlockHeight: initLastValidBlockHeight } = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair);
const initSignature = await connection.sendRawTransaction(initializeNonceTx.serialize(), { skipPreflight: false, preflightCommitment: "single" });- Por último, confirmaremos el estado de la transacción de inicialización para verificar que la cuenta de nonce se haya inicializado correctamente.
const initConfirmationStatus = await connection.confirmTransaction(initSignature);
if (initConfirmationStatus.value.err) {
throw new Error("Nonce initialization transaction failed: " + initConfirmationStatus.value.err);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}La función completa debería verse así:
async function nonce() {
// For creating the nonce account
const rent = await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH
);
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
// Create the nonce account
const signature = await connection.sendRawTransaction(
createNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error(
"Nonce account creation transaction failed: " +
confirmationStatus.value.err
);
}
console.log("Nonce account created:", signature);
// Now, initialize the nonce
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);
const {
blockhash: initBlockhash,
lastValidBlockHeight: initLastValidBlockHeight,
} = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair); // Only sign with nonceAuthKeypair
const initSignature = await connection.sendRawTransaction(
initializeNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const initConfirmationStatus = await connection.confirmTransaction(
initSignature
);
if (initConfirmationStatus.value.err) {
throw new Error(
"Nonce initialization transaction failed: " +
initConfirmationStatus.value.err
);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}Paso 4: Crea la función getNonce
Define la función getNonce, que se encarga de obtener el valor del nonce desde la cuenta de nonce creada.
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}Paso 5: Crea la función createTx
Define la función createTx, que crea una transacción de ejemplo con una instrucción para avanzar el nonce y una instrucción de transferencia. Usa el nonce obtenido previamente para garantizar la autenticidad de la transacción.
async function createTx(nonce) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
return serialisedTx;
}Paso 6: Crea la función signOffline
Define la función signOffline, que se encarga de firmar la transacción sin conexión. Simula una demora sin conexión antes de firmar la transacción con los pares de claves del remitente y de la autoridad del nonce.
async function createTx(nonce) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
return serialisedTx;
}Paso 7: Crea la función executeTx
La función executeTx se encarga de enviar la transacción firmada a la red de Solana para su ejecución. Este es el último paso del proceso, en el que la transacción se transmite a la red.
async function executeTx() {
const signedTx = await readAndDecodeTransaction("./signedTxn.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log("Tx sent: ", sig);
}Paso 8: Crea la función fetchNonceInfo
La función fetchNonceInfo obtiene información del nonce desde la cuenta de nonce creada y, si es necesario, vuelve a intentarlo hasta tres veces. Esto ayuda a garantizar que el nonce usado en la transacción esté actualizado y sea válido.
async function fetchNonceInfo(retries = 3) {
while (retries > 0) {
const accountInfo = await connection.getAccountInfo(nonceKeypair.publicKey);
if (accountInfo) {
const nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
return nonceAccount;
}
retries--;
if (retries > 0) {
console.log(`Retry fetching nonce in 3 seconds. ${retries} retries left.`);
await new Promise(res => setTimeout(res, 3000)); // wait for 3 seconds
}
}
throw new Error("No account info found");
}Paso 9: Llama a la función sendTransaction
Por último, llama a la función sendTransaction para iniciar el proceso de la transacción. Esta función reúne todos los pasos definidos anteriormente para crear, firmar y ejecutar una transacción con un nonce duradero.
ts-node mainAl ejecutar sendTransaction, se generará la firma de una transacción exitosa. Esta firma es un dato esencial para rastrear y verificar la transacción en la red de Solana.
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent: 64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5U¡Ya usaste un nonce duradero en una transacción exitosa!
Código completo
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
NonceAccount,
NONCE_ACCOUNT_LENGTH,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
encodeAndWriteTransaction,
loadWallet,
readAndDecodeTransaction,
} from "./utils";
const TranferAmount = LAMPORTS_PER_SOL * 0.01;
const nonceAuthKeypair = loadWallet("./wallets/nonceAuth.json");
const nonceKeypair = Keypair.generate();
const senderKeypair = loadWallet("./wallets/wallet.json");
const connection = new Connection(
"https://devnet.helius-rpc.com/?api-key="
);
const waitTime = 120000;
async function sendTransaction() {
try {
// Create nonce and get its creation transaction signature
const nonceCreationTxSig = await nonce();
// Ensure nonce account creation is confirmed before moving forward
const confirmationStatus = await connection.confirmTransaction(
nonceCreationTxSig
);
if (!confirmationStatus.value.err) {
console.log("Nonce account creation confirmed.");
const nonce = await getNonce();
await createTx(nonce);
await signOffline(waitTime);
await executeTx();
} else {
console.error(
"Nonce account creation transaction failed:",
confirmationStatus.value.err
);
}
} catch (error) {
console.error(error);
}
}
async function nonce() {
// For creating the nonce account
const rent = await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH
);
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
// Create the nonce account
const signature = await connection.sendRawTransaction(
createNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error(
"Nonce account creation transaction failed: " +
confirmationStatus.value.err
);
}
console.log("Nonce account created:", signature);
// Now, initialize the nonce
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);
const {
blockhash: initBlockhash,
lastValidBlockHeight: initLastValidBlockHeight,
} = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair); // Only sign with nonceAuthKeypair
const initSignature = await connection.sendRawTransaction(
initializeNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const initConfirmationStatus = await connection.confirmTransaction(
initSignature
);
if (initConfirmationStatus.value.err) {
throw new Error(
"Nonce initialization transaction failed: " +
initConfirmationStatus.value.err
);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}
async function createTx(nonce: string) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(
sampleTx,
"./unsigned.json",
false
);
return serialisedTx;
}
async function signOffline(waitTime = 120000): Promise {
await new Promise((resolve) => setTimeout(resolve, waitTime));
const unsignedTx = readAndDecodeTransaction("./unsigned.json");
unsignedTx.sign(senderKeypair, nonceAuthKeypair); // Sign with both keys
const serialisedTx = encodeAndWriteTransaction(unsignedTx, "./signed.json");
return serialisedTx;
}
async function executeTx() {
const signedTx = readAndDecodeTransaction("./signed.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log(" Tx sent: ", sig);
}
async function fetchNonceInfo(retries = 3): Promise {
while (retries > 0) {
const accountInfo = await connection.getAccountInfo(nonceKeypair.publicKey);
if (accountInfo) {
const nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
return nonceAccount;
}
retries--;
if (retries > 0) {
console.log(
`Retry fetching nonce in 3 seconds. ${retries} retries left.`
);
await new Promise((res) => setTimeout(res, 3000)); // wait for 3 seconds
}
}
throw new Error("No account info found");
}
sendTransaction();Transacciones de Solana: uso de los RPC de Helius
Helius puede actuar como un potente intermediario para interactuar con los RPC de Solana. Simplifica el proceso de obtener la información de blockhash necesaria para los nonces duraderos. Con Helius, puedes gestionar el ciclo de vida de tus transacciones de Solana de manera más confiable, especialmente en casos sin conexión. Ofrece acceso simplificado a los blockhashes y ayuda a los desarrolladores a crear aplicaciones más resistentes al vencimiento de las transacciones.
En resumen, los nonces duraderos de las transacciones de Solana ofrecen una forma segura y confiable de gestionar transacciones sin conexión y garantizar su autenticidad. Al seguir los pasos descritos en esta guía, los desarrolladores pueden implementar nonces duraderos en sus aplicaciones de Solana para mejorar la seguridad y la flexibilidad.
Artículos relacionados
Suscríbete a Helius
Mantente al día con las novedades del desarrollo en Solana y recibe actualizaciones cuando publiquemos


