Program Derived Address (PDA)
Program Derived Addresses (PDAs) надають розробникам на Solana два основні випадки використання:
- Детерміновані адреси рахунків: PDAs забезпечують механізм для детермінованого створення адреси, використовуючи комбінацію опціональних "seeds" (попередньо визначених вхідних даних) та конкретного ідентифікатора програми.
- Можливість підписання програмою: Середовище виконання Solana дозволяє програмам "підписувати" PDAs, які виведені з адреси програми.
Ви можете розглядати PDAs як спосіб створення структур, подібних до хеш-мап, на блокчейні з попередньо визначеного набору вхідних даних (наприклад, рядків, чисел та інших адрес рахунків).
Перевага цього підходу полягає в тому, що він усуває необхідність відстежувати точну адресу. Натомість вам просто потрібно пам'ятати конкретні вхідні дані, використані для її виведення.
Program Derived Address
Важливо розуміти, що просте виведення Program Derived Address (PDA) не створює автоматично рахунок на блокчейні за цією адресою. Рахунки з PDA як адресою на блокчейні повинні бути явно створені через програму, яка використовувалася для виведення адреси. Ви можете розглядати виведення PDA як пошук адреси на карті. Наявність адреси не означає, що там щось побудовано.
Цей розділ охоплює деталі виведення PDAs. Розділ про Cross Program Invocations (CPIs) пояснює, як програми використовують PDAs для підписання.
Ключові моменти
- PDAs - це адреси, виведені детерміновано з використанням комбінації попередньо визначених seeds, bump seed та ідентифікатора програми.
- PDAs - це адреси, які не лежать на кривій Ed25519 і не мають відповідного приватного ключа.
- Програми Solana можуть підписувати від імені PDAs, виведених з ідентифікатора програми.
- Виведення PDA не створює автоматично рахунок на блокчейні.
- Рахунок, що використовує PDA як свою адресу, повинен бути створений через інструкцію в програмі Solana.
Що таке PDA
PDA — це адреси, які визначаються детерміністично і виглядають як публічні ключі, але не мають приватних ключів. Це означає, що неможливо згенерувати дійсний підпис для цієї адреси. Однак середовище виконання Solana дозволяє програмам "підписувати" від імені PDA без необхідності мати приватний ключ.
Для контексту, Solana Keypairs є точками на кривій Ed25519 (криптографія на еліптичних кривих) з публічним ключем та відповідним приватним ключем. Публічні ключі використовуються як адреси (унікальні ідентифікатори) для облікових записів у блокчейні.
Адреса на кривій
PDA — це точка, яка навмисно виводиться за межі кривої Ed25519 за допомогою заздалегідь визначеного набору вхідних даних. Точка, яка не знаходиться на кривій Ed25519, не має дійсного відповідного приватного ключа і не може виконувати криптографічні операції (підписування).
PDA може служити адресою (унікальним ідентифікатором) для облікового запису в блокчейні, надаючи метод для легкого зберігання, відображення та отримання стану програми.
Адреса поза кривою
Як отримати PDA
Для отримання PDA потрібні три вхідні параметри:
- Опціональні seed: Заздалегідь визначені вхідні дані (наприклад, рядки, числа, інші адреси облікових записів) для отримання PDA.
- Bump seed: Додатковий байт, який додається до опціональних seed, щоб забезпечити генерацію дійсного PDA (поза кривою). Bump seed починається з 255 і зменшується на 1 доки не буде знайдено дійсний PDA.
- Program ID: Адреса програми, з якої отримується PDA. Ця програма може підписувати від імені PDA.
Отримання PDA
Використовуйте наступні функції з відповідних SDK для отримання PDA.
SDK | Функція |
---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Щоб отримати PDA, надайте такі вхідні дані до функції SDK:
- Попередньо визначені необов'язкові seed, перетворені на байти
- Ідентифікатор програми (адреса), що використовується для отримання
Після знаходження дійсної PDA функція повертає як адресу (PDA), так і bump seed, використаний для отримання.
Приклади
Наступні приклади показують, як отримати PDA за допомогою відповідних SDK.
Натисніть кнопку "Run", щоб виконати код.
Отримання PDA з необов'язковим рядковим seed
import { Address, getProgramDerivedAddress } from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const seeds = ["helloWorld"];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
Отримання PDA з необов'язковим адресним seed
import {Address,getAddressEncoder,getProgramDerivedAddress} from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const addressEncoder = getAddressEncoder();const optionalSeedAddress = addressEncoder.encode("B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka" as Address);const seeds = [optionalSeedAddress];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
Отримання PDA з кількома необов'язковими seed
import {Address,getAddressEncoder,getProgramDerivedAddress} from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const optionalSeedString = "helloWorld";const addressEncoder = getAddressEncoder();const optionalSeedAddress = addressEncoder.encode("B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka" as Address);const seeds = [optionalSeedString, optionalSeedAddress];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
Канонічний bump
Для отримання PDA потрібен "bump seed", додатковий байт, який додається до необов'язкових seeds. Функція отримання перебирає значення bump, починаючи з 255 і зменшуючи на 1, доки значення не створить дійсну адресу поза кривою. Перше значення bump, яке створює дійсну адресу поза кривою, є "канонічним bump".
Наступні приклади показують отримання PDA з використанням усіх можливих bump seeds (від 255 до 0):
Приклад Kit не включено, оскільки функція createProgramDerivedAddress не експортується.
import { PublicKey } from "@solana/web3.js";const programId = new PublicKey("11111111111111111111111111111111");const optionalSeed = "helloWorld";// Loop through all bump seeds (255 down to 0)for (let bump = 255; bump >= 0; bump--) {try {const PDA = PublicKey.createProgramAddressSync([Buffer.from(optionalSeed), Buffer.from([bump])],programId);console.log("bump " + bump + ": " + PDA);} catch (error) {console.log("bump " + bump + ": " + error);}}
bump 255: Error: Invalid seeds, address must fall off the curvebump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6Xbump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4ybump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHHbump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdPbump 250: Error: Invalid seeds, address must fall off the curve...// remaining bump outputs
Bump seed 255 викликає помилку, і першим bump seed, який створює дійсний PDA, є 254.
Зауважте, що bump seeds 253-251 усі створюють дійсні PDA з різними адресами. Це
означає, що за тих самих необов'язкових seeds і programId
, bump seed з іншим
значенням все одно може створити дійсний PDA.
Під час створення програм Solana завжди включайте перевірки безпеки, щоб переконатися, що PDA, переданий до програми, отримано з канонічного bump. Відсутність таких перевірок може створити вразливості, які дозволять використовувати неочікувані облікові записи в інструкціях програми. Найкращою практикою є використання лише канонічного bump при отриманні PDA.
Створення облікових записів PDA
Приклад програми нижче показує, як створити обліковий запис, використовуючи PDA як адресу нового облікового запису. Приклад програми використовує фреймворк Anchor.
Програма містить єдину інструкцію initialize
для створення нового облікового
запису з використанням PDA як адреси облікового запису. Новий обліковий запис
зберігає адресу user
та bump
seed, який використовується для отримання PDA.
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 bumpaccount_data.bump = ctx.bumps.pda_account;Ok(())}}#[derive(Accounts)]pub struct Initialize<'info> {#[account(mut)]pub user: Signer<'info>,#[account(init,// define the seeds to derive the PDAseeds = [b"data", user.key().as_ref()],// use the canonical bumpbump,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,}
У цьому прикладі seed для виведення PDA включають фіксований рядок data
та
адресу облікового запису user
, наданого в інструкції. Фреймворк Anchor
автоматично знаходить канонічний seed bump
.
#[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 викликати System Program для створення нового
облікового запису, використовуючи PDA як адресу. Anchor робить це через
CPI.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
Тестовий файл містить код Typescript для виведення PDA.
const [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);
Транзакція в тестовому файлі викликає інструкцію initialize
для створення
нового облікового запису в мережі, використовуючи PDA як адресу. У цьому
прикладі Anchor може визначити адресу PDA в облікових записах інструкції, тому
її не потрібно вказувати явно.
it("Is initialized!", async () => {const transactionSignature = await program.methods.initialize().accounts({user: user.publicKey}).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, транзакція
завершиться невдачею. Це стається тому, що обліковий запис уже існує за
виведеною адресою.
Is this page helpful?