НОВОЕ: Helius приобретает Light Protocol
как отправлять транзакции Solana с помощью долговечных nonce
Блог/Разработка

Транзакции Solana: долговечные nonce (2023)

Руководитель по работе с разработчикамиHunter Davis в LinkedIn
Чтение: 6 мин

Введение

При отправке транзакции в Solana разработчику необходимо получить последний хеш блока через Solana RPC. Этот важный шаг помогает предотвратить атаки повторного воспроизведения. После подписания и отправки транзакции с определенным хешем блока никто не сможет использовать тот же хеш для ее повторного воспроизведения или отправки.

Представьте, что вам нужно отправить транзакцию, требующую подписи из офлайн-хранилища или аппаратного кошелька, например Ledger. Однако хеши блоков быстро истекают, из-за чего транзакция может стать недействительной. Эту проблему решают долговечные nonce, позволяющие безопасно проводить офлайн-транзакции.

Из этого руководства вы узнаете:

  1. Что такое долговечный nonce.
  2. Для чего нужен долговечный nonce.
  3. Как использовать долговечные nonce в транзакциях.

Транзакции: требования

Прежде чем начать, убедитесь, что у вас есть:

  • Базовые знания JavaScript.
  • Установленный NodeJS.
  • Установленный Solana CLI.
  • Установленный Git

Настройка окружения

  1. Клонируйте наш пример репозитория с готовыми утилитами:
Код
git clone
  1. Перейдите в папку проекта и установите npm:
Код
cd durable-noncenpm install
  1. Перейдите в папку wallets внутри папки nonce. В ней будут храниться локальные ключи для тестирования:
Код
cd wallets
  1. После установки Solana CLI создайте кошелек для оплачивающей пары ключей:
Код
solana-keygen new -o ./wallet.json
  1. Теперь укажите этот кошелек в CLI, чтобы получить Solana через airdrop:
Код
solana config set --keypair ./wallet.json
  1. Теперь вы можете получить Solana на этот адрес через airdrop, выполнив следующую команду:
Код
solana aidrop 2
  1. Также нужно создать и пополнить еще один кошелек для управляющего nonce:
Код
solana-keygen new -o ./nonceAuth.json
  1. Для созданного публичного ключа можно получить 1 SOL через airdrop на сайте-кране здесь.

Окружение настроено, и теперь можно перейти к следующим шагам.

Что такое долговечный nonce?

Аккаунт долговечного nonce в Solana можно представить как банковскую ячейку. При создании такого аккаунта Solana присваивает ему уникальный постоянный код — «долговечный nonce». В отличие от обычных nonce, которые меняются с каждой транзакцией, он остается неизменным и служит постоянной точкой отсчета.

Это особенно полезно для «офлайн-транзакций». При создании транзакции вы указываете этот nonce из своего аккаунта. Solana сверяет его с сохраненным значением и при совпадении разрешает транзакцию. Таким образом, аккаунт долговечного nonce служит механизмом хранения и проверки. Он подтверждает подлинность транзакций с учетом высокой скорости сети Solana и сценариев работы офлайн.

Долговечные nonce можно использовать в различных сценариях, например:

  • Запланированные транзакции: можно настроить выполнение транзакций в определенное время в будущем. Долговечные nonce обеспечивают безопасное выполнение таких транзакций.
  • Мультиподписные кошельки: в кошельках с мультиподписью долговечные nonce обеспечивают дополнительный уровень безопасности и координации между несколькими подписантами.
  • Программы, требующие взаимодействия в будущем: некоторым программам в Solana необходимо взаимодействовать с другими программами или сервисами через определенные интервалы. Долговечные nonce помогают сохранять целостность этих взаимодействий.
  • Взаимодействие с другими блокчейнами: когда Solana взаимодействует с другими блокчейнами, долговечные nonce помогают обеспечить действительность межсетевых транзакций.

Теперь можно приступить к созданию примера.

Транзакции Solana: этапы реализации

Шаг 1. Настройте зависимости и константы

На этом шаге вы импортируете необходимые модули и утилиты, а также определите константы и пары ключей для примера. Эти зависимости и константы будут использоваться на всех этапах обработки транзакции.

Код
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;

Шаг 2. Создайте функцию sendTransaction

Функция sendTransaction управляет отправкой транзакции с использованием долговечного nonce. Она отвечает за создание и подтверждение nonce, а также выполнение транзакции.

Код
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);
  }
}

Шаг 3. Создайте функцию nonce

Функция nonce создает и инициализирует аккаунт долговечного nonce. Для этого она рассчитывает арендную плату за аккаунт, получает последний хеш блока и формирует транзакции для создания и инициализации аккаунта nonce.

  1. Перед созданием аккаунта nonce необходимо рассчитать арендную плату за хранение его данных и получить последний хеш блока.
Код
async function nonce() {
  const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
  1. Теперь сформируем транзакцию для создания аккаунта nonce. Для выделения пространства под аккаунт nonce используется инструкция SystemProgram.createAccount.
Код
const createNonceTx = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: nonceAuthKeypair.publicKey,
      newAccountPubkey: nonceKeypair.publicKey,
      lamports: rent,
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    })
  );
  1. Подпишем транзакцию управляющими парами ключей и отправим ее в сеть Solana. Эта транзакция создает аккаунт долговечного 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" });
  1. После отправки транзакции подтвердим ее статус, чтобы убедиться, что аккаунт nonce успешно создан.
Код
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);
  1. Чтобы полноценно использовать аккаунт nonce, необходимо инициализировать его значение. Создадим новую транзакцию для выполнения инструкции SystemProgram.nonceInitialize.
Код
// Initialize the nonce value within the account
    const initializeNonceTx = new Transaction().add(
      SystemProgram.nonceInitialize({
        noncePubkey: nonceKeypair.publicKey,
        authorizedPubkey: nonceAuthKeypair.publicKey,
      })
    );
  1. Как и на предыдущем шаге, подпишем транзакцию и отправим ее в сеть для инициализации аккаунта 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" });
  1. Наконец, подтвердим статус транзакции инициализации, чтобы убедиться, что аккаунт nonce правильно инициализирован.
Код
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 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;
  }
}

Шаг 4. Создайте функцию getNonce

Определите функцию getNonce, которая получает значение nonce из созданного аккаунта nonce.

Код
async function getNonce() {
  const nonceAccount = await fetchNonceInfo();
  return nonceAccount.nonce;
}

Шаг 5. Создайте функцию createTx

Определите функцию createTx. Она создает пример транзакции с инструкцией продвижения nonce и инструкцией перевода. Функция использует ранее полученный 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;
}

Шаг 6. Создайте функцию signOffline

Определите функцию signOffline, которая подписывает транзакцию офлайн. Она имитирует задержку офлайн-режима, а затем подписывает транзакцию парами ключей отправителя и управляющего 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;
}

Шаг 7. Создайте функцию executeTx

Функция executeTx отправляет подписанную транзакцию в сеть Solana для выполнения. Это заключительный этап процесса, на котором транзакция передается в сеть.

Код
async function executeTx() {
  const signedTx = await readAndDecodeTransaction("./signedTxn.json");
  const sig = await connection.sendRawTransaction(signedTx.serialize());
  console.log("Tx sent: ", sig);
}

Шаг 8. Создайте функцию fetchNonceInfo

Функция fetchNonceInfo получает информацию о nonce из созданного аккаунта nonce. При необходимости она выполняет до трех повторных попыток. Это помогает убедиться, что используемый в транзакции nonce актуален и действителен.

Код
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");
}

Шаг 9. Вызовите функцию sendTransaction

Наконец, вызовите функцию sendTransaction, чтобы запустить процесс транзакции. Она объединяет все описанные ранее шаги для создания, подписания и выполнения транзакции с долговечным nonce.

Код
ts-node main

После запуска sendTransaction будет создана подпись успешно выполненной транзакции. Эта подпись необходима для отслеживания и проверки транзакции в сети Solana.

Код
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent:  64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5U

Теперь вы успешно использовали долговечный nonce в транзакции!

Полный код

Код
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: использование Helius RPC

Helius может выступать мощным посредником при взаимодействии с Solana RPC, упрощая получение данных о хешах блоков, необходимых для долговечных nonce. С Helius вы можете надежнее управлять жизненным циклом транзакций Solana, особенно в офлайн-сценариях. Он предоставляет удобный доступ к хешам блоков и помогает разработчикам сделать приложения устойчивее к истечению срока действия транзакций.

Таким образом, долговечные nonce в транзакциях Solana позволяют безопасно и надежно обрабатывать офлайн-транзакции и подтверждать их подлинность. Следуя шагам из этого руководства, разработчики могут реализовать долговечные nonce в своих приложениях Solana, повысив их безопасность и гибкость.

‍

‍

‍

Подпишитесь на Helius

Следите за новостями разработки на Solana и получайте уведомления о наших публикациях