Производный адрес программы (PDA)

Программные производные адреса (PDA) предоставляют разработчикам Solana два основных варианта использования:

  • Детерминированные учетные записи: PDA предоставляют механизм для детерминированного получения адреса с использованием комбинации необязательных "семян" (предопределенных входных данных) и определенного идентификатора программы.
  • Возможность подписи программ: Среда выполнения Solana позволяет программам "подписываться" для КПК, которые получены на основе идентификатора программы.

Можно представить КПК как способ создания хешмапоподобных структур на цепочке из заранее определенного набора входных данных (например, строк, чисел и других учетных записей).

Преимущество такого подхода в том, что он избавляет от необходимости отслеживать точный адрес. Вместо этого достаточно вспомнить конкретные данные, использованные для его получения.

Производный адрес программы

Важно понимать, что простое выведение программного производного адреса (PDA) не приводит к автоматическому созданию учетной записи на этом адресе. Учетные записи с PDA в качестве внутрицепочечного адреса должны быть явно созданы с помощью программы, используемой для получения адреса. Получение PDA можно сравнить с поиском адреса на карте. Наличие адреса не означает, что в этом месте что-то построено.

Info

В этом разделе мы рассмотрим детали получения PDAs. Подробности того, как программы используют PDA для подписи, будут рассмотрены в разделе о межпрограммных обращениях (CPI), так как там требуется контекст для обеих концепций.

Ключевые точки #

  • PDA - это адреса, полученные детерминированным с использованием комбинации атрибутов определяемых пользователем семян и идентификатор программы.

  • PDA - это адреса, которые выпадают из кривой Ed25519 и не имеют закрытого ключа соответственно.

  • Solana programs can programmatically "sign" on behalf of PDAs that are derived using its program ID.

  • Получение PDA не приводит к автоматическому созданию учетной записи на цепи.

  • Учетная запись, использующая PDA в качестве адреса, должна быть явно создана с помощью специальной инструкции в программе Solana.

Что такое PDA #

PDA - это адреса, детерминируемые и выглядящие как стандартные публичные ключи, но не имеют связанных приватных ключей. Это означает, что ни один из внешних пользователь не может генерировать действительную подпись для адреса. Тем не менее, runtime позволяет программно "подписать" программы для КПК без использования персонального ключа.

Для контекста, пары ключей Solana - это точки на кривой Ed25519 (криптография с эллиптическими кривыми), которые имеют открытый ключ и соответствующий закрытый ключ. Мы часто используем открытые ключи в качестве уникальных идентификаторов для новых учетных записей в сети, а закрытые ключи - для подписи.

Адрес кривой

PDA - это точка, которая намеренно выведена для падения с кривой Ed25519 с помощью заранее определенного набора входных данных. Точка, которая не находится на кривой Ed25519, не имеет действительного соответствующего закрытого ключа и не может быть использована для криптографических операций (подписи).

PDA можно использовать в качестве адреса (уникального идентификатора) для учетной записи на цепочке, что позволяет легко хранить, отображать и извлекать состояние программы.

Выключенный адрес кривой

Как вывести PDA #

Для выведения PDA требуется 3 входа.

  • Необязательные семена: Предопределенные входные данные (например, строка, число, адреса других учетных записей), используемые для получения КПК. Эти данные преобразуются в буфер байтов.
  • Затравка: Дополнительный вход (со значением в диапазоне 255-0), который используется для гарантии того, что будет сгенерирован правильный КПК (вне кривой). Эта затравка (начиная с 255) добавляется к дополнительным затравкам при генерации PDA, чтобы "сбить" точку с кривой Ed25519. Зерно неровности иногда называют "nonce".
  • Идентификатор программы: адрес программы, на основе которой создается PDA. Это также программа, которая может "подписывать" от имени PDA.

Вывод PDA

Примеры ниже содержат ссылки на Solana Playground, где вы можете запустить примеры в браузере.

Адрес программы поиска #

Чтобы вывести PDA, мы можем использовать метод findProgramAddressSync из @solana/web3.js. Существуют эквиваленты этой функции в других языках программирования (например, Rust), но в этом разделе мы рассмотрим примеры с использованием Javascript.

При использовании метода findProgramAddressSync мы передаем:

  • предопределенные необязательные семена, преобразованные в буфер байтов, и
  • идентификатор (адрес) программы, используемый для получения PDA.

После того, как найден правильный PDA, метод findProgramAddressSync возвращает адрес (PDA) и семя бампа, использованное для получения PDA.

В приведенном ниже примере PDA создается без предоставления каких-либо необязательных семян.

import { PublicKey } from "@solana/web3.js";
 
const programId = new PublicKey("11111111111111111111111111111111");
 
const [PDA, bump] = PublicKey.findProgramAddressSync([], programId);
 
console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);

Вы можете запустить этот пример на Playana Playground. Вывод семян PDA всегда будет одинаковым:

PDA: Cu7NwqCXSmsR5vgGA3Vw9uYVViPi3kQvkbKByVQ8nPY9
Bump: 255

Следующий пример ниже добавляет необязательное семя "helloWorld".

import { PublicKey } from "@solana/web3.js";
 
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
 
const [PDA, bump] = PublicKey.findProgramAddressSync(
  [Buffer.from(string)],
  programId,
);

Вы можете запустить этот пример на Playana Playground. Вывод семян PDA всегда будет одинаковым:

PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
Bump: 254

Обратите внимание, что семена кусков составляет 254. Это означает, что 255 получала точку на кривой Ed25519 и не является PDA.

Семя неровности, возвращаемое findProgramAddressSync, - это первое значение (между 255-0) для заданной комбинации необязательных семян и идентификатора программы, которое выводит действительный PDA.

Info

Это первое действительное семя неровности называется «канонической неровностью». Для безопасности программы рекомендуется использовать только каноническую кочку при работе с PDA.

Создать адрес программы #

Под капотом findProgramAddressSync будет итеративно добавлять дополнительное семя неровности (nonce) в буфер семян и вызывать метод createProgramAddressSync. Зерно неровности начинается со значения 255 и уменьшается на 1 до тех пор, пока не будет найдено действительное PDA (вне кривой).

Вы можете повторить предыдущий пример, используя createProgramAddressSync и явно передав семя неровности, равное 254.

import { PublicKey } from "@solana/web3.js";
 
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const bump = 254;
 
const PDA = PublicKey.createProgramAddressSync(
  [Buffer.from(string), Buffer.from([bump])],
  programId,
);
 
console.log(`PDA: ${PDA}`);

Вы можете запустить этот пример на Playana Playground. Учитывая те же самые сиды и ID программы, вывод PDA будет соответствовать предыдущим:

PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X

Каноническая неровность #

Под "канонической неровностью" понимается первая затравка неровности (начиная с 255 и уменьшаясь на 1), из которой получен действительный PDA. Для обеспечения безопасности программы рекомендуется использовать только PDA, полученные из канонической неровности.

Используя предыдущий пример, в приведенном ниже примере сделана попытка получить PDA, используя все семена неровности от 255 до 0.

import { PublicKey } from "@solana/web3.js";
 
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
 
// Loop through all bump seeds for demonstration
for (let bump = 255; bump >= 0; bump--) {
  try {
    const PDA = PublicKey.createProgramAddressSync(
      [Buffer.from(string), Buffer.from([bump])],
      programId,
    );
    console.log("bump " + bump + ": " + PDA);
  } catch (error) {
    console.log("bump " + bump + ": " + error);
  }
}

Запустите пример на Playground и вы увидите следующий вывод:

bump 255: Error: Invalid seeds, address must fall off the curve
bump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
bump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4y
bump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHH
bump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdP
bump 250: Error: Invalid seeds, address must fall off the curve
...
// remaining bump outputs

Как и ожидалось, семя 255 выдает ошибку, и первым семенем, с помощью которого был получен действительный PDA, является 254.

Однако обратите внимание, что затравки 253-251 приводят к действительным PDA с разными адресами. Это означает, что при одинаковых опциональных семенах и programId семя неровности с другим значением все равно может вывести действительный PDA.

Warning

При создании программ Solana рекомендуется включать проверки безопасности, которые подтверждают, что переданный программе PDA получен с помощью канонического бампа. В противном случае могут возникнуть уязвимости, позволяющие передавать программе неожиданные учетные записи.

Создание учётных записей PDA #

Этот пример программы на Playground демонстрирует, как создать учетную запись, используя КПК в качестве адреса новой учетной записи . Пример программы написан с помощью Anchor framework.

В файле lib.rs вы найдете следующую программу, которая содержит единственную инструкцию для создания новой учетной записи с использованием PDA в качестве адреса учетной записи. В новой учетной записи хранится адрес пользователя и семя шишки, использованное для получения PDA.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("75GJVCJNhaukaa2vCCqhreY31gaphv7XTScBChmr1ueR");
 
#[program]
pub mod pda_account {
    use super::*;
 
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let account_data = &mut ctx.accounts.pda_account;
        // store the address of the `user`
        account_data.user = *ctx.accounts.user.key;
        // store the canonical bump
        account_data.bump = ctx.bumps.pda_account;
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
 
    #[account(
        init,
        // set the seeds to derive the PDA
        seeds = [b"data", user.key().as_ref()],
        // use the canonical bump
        bump,
        payer = user,
        space = 8 + DataAccount::INIT_SPACE
    )]
    pub pda_account: Account<'info, DataAccount>,
    pub system_program: Program<'info, System>,
}
 
#[account]
 
#[derive(InitSpace)]
pub struct DataAccount {
    pub user: Pubkey,
    pub bump: u8,
}

Семяны, используемые для получения PDA, включают в себя строку data с жестким кодом и адрес учетной записи user, предоставленный в инструкции. Система Anchor автоматически определяет канонические семена неровностей.

#[account(
    init,
    seeds = [b"data", user.key().as_ref()],
    bump,
    payer = user,
    space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,

Ограничение init предписывает Anchor вызвать системную программу для создания новой учетной записи с PDA в качестве адреса. По сути это делается через CPI.

#[account(
    init,
    seeds = [b"data", user.key().as_ref()],
    bump,
    payer = user,
    space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,

В тестовом файле (pda-account.test.ts), расположенном внутри вышеприведенной ссылки Solana Playground , вы найдете Javascript, эквивалентный получению PDA.

const [PDA] = PublicKey.findProgramAddressSync(
  [Buffer.from("data"), user.publicKey.toBuffer()],
  program.programId,
);

Затем транзакция будет отправлена для вызова инструкции initialize для создания новой он-цепной учетной записи с PDA в качестве адреса. Как только транзакция отправлена, используется для получения учетной записи в цепочке, созданной по адресу.

it("Is initialized!", async () => {
  const transactionSignature = await program.methods
    .initialize()
    .accounts({
      user: user.publicKey,
      pdaAccount: PDA,
    })
    .rpc();
 
  console.log("Transaction Signature:", transactionSignature);
});
 
it("Fetch Account", async () => {
  const pdaAccount = await program.account.dataAccount.fetch(PDA);
  console.log(JSON.stringify(pdaAccount, null, 2));
});

Обратите внимание, что если вы запускаете инструкцию initialize более одного раза с использованием того же адреса user как seed, то транзакция будет прервана. Это происходит потому, что учетная запись по производному адресу уже существует.