
Solana-Transaktionen: Dauerhafte Nonces (2023)
Einführung
Wenn du als Entwickler eine Transaktion auf Solana übermittelst, musst du den neuesten Blockhash über Solana RPC abrufen. Dieser wichtige Schritt schützt vor Replay-Angriffen. Sobald eine Transaktion mit einem bestimmten Blockhash signiert und übermittelt wurde, kann niemand denselben Hash verwenden, um sie erneut abzuspielen oder zu übermitteln.
Stell dir vor, du musst eine Transaktion übermitteln, die eine Signatur aus einem Offline-Cold-Storage oder einer Hardware-Wallet wie Ledger benötigt. Blockhashes laufen jedoch schnell ab, wodurch deine Transaktion ungültig werden kann. Hier kommen dauerhafte Nonces ins Spiel: Sie ermöglichen sichere Offline-Transaktionen.
Am Ende dieses Leitfadens verstehst du:
- Was eine dauerhafte Nonce ist.
- Welchen Zweck eine dauerhafte Nonce erfüllt.
- Wie du dauerhafte Nonces in Transaktionen verwendest.
Transaktionen: Voraussetzungen
Bevor du beginnst, stelle Folgendes sicher:
- Du verfügst über grundlegende JavaScript-Kenntnisse.
- NodeJS ist installiert.
- Solana CLI ist installiert.
- Git ist installiert.
Umgebung einrichten
- Klone unser Beispiel-Repository mit vorhandenen Hilfsfunktionen:
git clone- Wechsle in den Projektordner und installiere npm:
cd durable-noncenpm install- Wechsle im Ordner „nonce“ in den Ordner „wallets“. Dort speichern wir unsere lokalen Schlüssel für die Tests:
cd wallets- Erstelle mit der installierten Solana CLI eine Wallet für das zahlende Schlüsselpaar:
solana-keygen new -o ./wallet.json- Lege diese Wallet nun in der CLI als deine Wallet fest, um Solana per Airdrop zu erhalten:
solana config set --keypair ./wallet.json- Jetzt kannst du mit folgendem Befehl Solana per Airdrop an diese Adresse senden:
solana aidrop 2- Außerdem müssen wir eine weitere Wallet für die Nonce-Autorität erstellen und aufladen:
solana-keygen new -o ./nonceAuth.json- An den erzeugten öffentlichen Schlüssel können wir über eine Faucet-Website 1 SOL per Airdrop senden: hier.
Nachdem unsere Umgebung eingerichtet ist, können wir mit den nächsten Schritten fortfahren.
Was ist eine dauerhafte Nonce?
Ein Konto für dauerhafte Nonces auf Solana lässt sich mit einem Schließfach vergleichen. Wenn du dieses Konto anlegst, weist Solana ihm einen eindeutigen, stabilen Code zu, der als „dauerhafte Nonce“ bezeichnet wird. Anders als typische Nonces, die sich mit jeder Transaktion ändern, bleibt diese Nonce unverändert und dient als konsistente Referenz.
Das ist besonders für „Offline“-Transaktionen nützlich. Beim Erstellen einer Transaktion verweist du auf die Nonce deines Kontos. Solana gleicht sie mit dem gespeicherten Wert ab. Stimmen beide überein, wird die Transaktion freigegeben. Ein Konto für dauerhafte Nonces dient somit sowohl zur Speicherung als auch zur Validierung. Es gewährleistet die Authentizität von Transaktionen und unterstützt zugleich die hohe Geschwindigkeit sowie Offline-Szenarien im Solana-Netzwerk.
Dauerhafte Nonces lassen sich für verschiedene Anwendungsfälle nutzen, darunter:
- Geplante Transaktionen: Du kannst Transaktionen so einrichten, dass sie zu einem festgelegten zukünftigen Zeitpunkt ausgeführt werden. Dauerhafte Nonces sorgen dafür, dass diese geplanten Transaktionen sicher ausgeführt werden.
- Multisig-Wallets: Bei Wallets mit mehreren Signaturen bieten dauerhafte Nonces eine zusätzliche Sicherheits- und Koordinationsebene für mehrere Unterzeichner.
- Programme mit zukünftigen Interaktionen: Einige Programme auf Solana müssen in bestimmten Abständen mit anderen Programmen oder Diensten interagieren. Dauerhafte Nonces helfen dabei, die Integrität dieser Interaktionen zu wahren.
- Interaktion mit anderen Blockchains: Wenn Solana mit anderen Blockchains interagiert, tragen dauerhafte Nonces dazu bei, die Gültigkeit von Cross-Chain-Transaktionen sicherzustellen.
Jetzt können wir mit unserem Beispiel beginnen.
Solana-Transaktionen: Implementierungsschritte
Schritt 1: Abhängigkeiten und Konstanten einrichten
In diesem Schritt importierst du die erforderlichen Module und Hilfsfunktionen. Außerdem definierst du Konstanten und Schlüsselpaare für das Beispiel. Diese Abhängigkeiten und Konstanten werden während des gesamten Transaktionsprozesses verwendet.
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;Schritt 2: Die Funktion sendTransaction erstellen
Die Funktion sendTransaction steuert den Versand einer Transaktion mit einer dauerhaften Nonce. Sie übernimmt das Erstellen und Bestätigen der Nonce sowie das Ausführen der Transaktion.
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);
}
}Schritt 3: Die Funktion nonce erstellen
Die Funktion nonce erstellt und initialisiert das Konto für die dauerhafte Nonce. Dazu berechnet sie die für das Konto erforderliche Rent, ruft den neuesten Blockhash ab und erstellt Transaktionen, um das Nonce-Konto anzulegen und zu initialisieren.
- Bevor wir das Nonce-Konto erstellen, müssen wir die erforderliche Rent für die Speicherung der Kontodaten berechnen und den neuesten Blockhash abrufen.
async function nonce() {
const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();- Jetzt erstellen wir eine Transaktion, die das Nonce-Konto anlegt. Dabei verwenden wir die Anweisung
SystemProgram.createAccount, um Speicherplatz für das Nonce-Konto zuzuweisen.
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);- Wir signieren die Transaktion mit den Schlüsselpaaren der Autorität und senden sie an das Solana-Netzwerk. Diese Transaktion erstellt das Konto für die dauerhafte Nonce.
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" });- Nach dem Senden bestätigen wir den Status der Transaktion, um sicherzustellen, dass das Nonce-Konto erfolgreich erstellt wurde.
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);- Damit wir das Nonce-Konto vollständig nutzen können, müssen wir seinen Wert initialisieren. Dazu erstellen wir eine neue Transaktion, die die Anweisung
SystemProgram.nonceInitializeausführt.
// Initialize the nonce value within the account
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);- Wie im vorherigen Schritt signieren wir die Transaktion und senden sie an das Netzwerk, um das Nonce-Konto zu initialisieren.
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" });- Abschließend bestätigen wir den Status der Initialisierungstransaktion, um sicherzustellen, dass das Nonce-Konto korrekt initialisiert wurde.
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;
}
}Die vollständige Funktion sollte ungefähr so aussehen:
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;
}
}Schritt 4: Die Funktion getNonce erstellen
Definiere die Funktion getNonce. Sie ruft den Nonce-Wert aus dem erstellten Nonce-Konto ab.
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}Schritt 5: Die Funktion createTx erstellen
Definiere die Funktion createTx. Sie erstellt eine Beispieltransaktion, die sowohl die Anweisung zum Fortschreiben der Nonce als auch eine Übertragungsanweisung enthält. Die Funktion verwendet die zuvor abgerufene Nonce, um die Authentizität der Transaktion sicherzustellen.
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;
}Schritt 6: Die Funktion signOffline erstellen
Definiere die Funktion signOffline. Sie signiert die Transaktion offline. Vor der Signatur simuliert sie eine Offline-Verzögerung und signiert die Transaktion anschließend mit den Schlüsselpaaren des Absenders und der Nonce-Autorität.
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;
}Schritt 7: Die Funktion executeTx erstellen
Die Funktion executeTx sendet die signierte Transaktion zur Ausführung an das Solana-Netzwerk. Dies ist der letzte Schritt im Transaktionsprozess, bei dem die Transaktion an das Netzwerk übertragen wird.
async function executeTx() {
const signedTx = await readAndDecodeTransaction("./signedTxn.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log("Tx sent: ", sig);
}Schritt 8: Die Funktion fetchNonceInfo erstellen
Die Funktion fetchNonceInfo ruft Nonce-Informationen aus dem erstellten Nonce-Konto ab. Falls erforderlich, versucht sie dies bis zu dreimal. Dadurch bleibt die für die Transaktion verwendete Nonce aktuell und gültig.
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");
}Schritt 9: Die Funktion sendTransaction aufrufen
Rufe abschließend die Funktion sendTransaction auf, um den Transaktionsprozess zu starten. Diese Funktion führt alle zuvor definierten Schritte zusammen, um eine Transaktion mit einer dauerhaften Nonce zu erstellen, zu signieren und auszuführen.
ts-node mainBeim Ausführen von sendTransaction wird für eine erfolgreiche Transaktion eine Transaktionssignatur ausgegeben. Diese Signatur ist entscheidend, um die Transaktion im Solana-Netzwerk zu verfolgen und zu verifizieren.
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent: 64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5UDu hast jetzt erfolgreich eine dauerhafte Nonce in einer Transaktion verwendet!
Vollständiger Code
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();Solana-Transaktionen: Helius RPC verwenden
Helius kann als leistungsstarker Vermittler für die Interaktion mit Solana RPC dienen. Das vereinfacht den Abruf der Blockhash-Informationen, die für dauerhafte Nonces erforderlich sind. Mit Helius kannst du den Lebenszyklus deiner Solana-Transaktionen zuverlässiger verwalten, insbesondere in Offline-Szenarien. Du erhältst einfachen Zugriff auf Blockhashes und kannst deine Anwendungen dadurch besser gegen abgelaufene Transaktionen absichern.
Zusammengefasst bieten dauerhafte Nonces in Solana-Transaktionen eine sichere und zuverlässige Möglichkeit, Offline-Transaktionen abzuwickeln und ihre Authentizität sicherzustellen. Mit den Schritten aus diesem Leitfaden können Entwickler dauerhafte Nonces in ihren Solana-Anwendungen implementieren und so Sicherheit und Flexibilität verbessern.
Ähnliche Artikel
Helius abonnieren
Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen


