Адреса, похідна від програми
Адреса рахунку Solana вказує на місцезнаходження рахунку в блокчейні. Багато адрес рахунків є публічним ключем keypair, і в такому випадку відповідний приватний ключ використовується для підписання транзакцій, пов'язаних із рахунком.
Корисною альтернативою адресі публічного ключа є адреса, похідна від програми (PDA). PDA надають простий метод для зберігання, відображення та отримання стану програми. PDA — це адреса, яка створюється детерміновано з використанням ID програми та комбінації опціональних попередньо визначених вхідних даних. PDA виглядають подібно до адрес публічних ключів, але не мають відповідного приватного ключа.
Середовище виконання Solana дозволяє програмам підписувати PDA без необхідності приватного ключа. Використання PDA усуває необхідність відстежувати адресу рахунку. Замість цього ви можете згадати конкретні вхідні дані, використані для виведення PDA. (Щоб дізнатися, як програми використовують PDA для підписання, див. розділ Міжпрограмні виклики.)
Передумови
Keypair Solana — це точки на кривій Ed25519 (криптографія на еліптичних кривих). Вони складаються з публічного ключа та приватного ключа. Публічний ключ стає адресою рахунку, а приватний ключ використовується для генерації дійсного підпису для рахунку.
Два рахунки з адресами на кривій
PDA навмисно виводиться так, щоб не потрапляти на криву Ed25519. Це означає, що вона не має дійсного відповідного приватного ключа і не може виконувати криптографічні операції. (Наприклад, надавати підпис.) Однак Solana дозволяє програмам підписувати PDA без необхідності приватного ключа.
Off Curve Address
Ви можете розглядати PDA як спосіб створення структур, подібних до хеш-мап, у блокчейні, використовуючи заздалегідь визначений набір вхідних даних. (Наприклад, рядки, числа та інші адреси облікових записів.)
Program Derived Address
Отримання PDA
Перед створенням облікового запису з PDA, ви повинні спочатку отримати адресу. Отримання PDA не створює автоматично обліковий запис у блокчейні за цією адресою — обліковий запис повинен бути явно створений через програму, яка використовується для отримання PDA. Ви можете розглядати PDA як адресу на карті: лише тому, що адреса існує, не означає, що там щось побудовано.
SDK Solana підтримують створення PDA за допомогою функцій, показаних у таблиці нижче. Кожна функція отримує такі вхідні дані:
- Program ID: Адреса програми, яка використовується для отримання PDA. Ця програма може підписуватися від імені PDA.
- Optional seeds: Заздалегідь визначені вхідні дані, такі як рядки, числа або інші адреси облікових записів.
| SDK | Функція |
|---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Функція використовує Program ID та optional seeds, а потім перебирає значення bump для спроби створити дійсну адресу програми. Перебір значень bump починається з 255 і зменшується на 1, доки не буде знайдено дійсний PDA. Після знаходження дійсного PDA функція повертає PDA та bump seed.
Bump seed — це додатковий байт, який додається до optional seeds, щоб забезпечити генерацію дійсної адреси поза кривою.
Отримання PDA
Канонічний bump
Bump seed — це додатковий байт, який додається до необов'язкових seed. Функція отримання перебирає значення bump, починаючи з 255 і зменшуючи на 1, доки значення не створить дійсну адресу поза кривою. Перше значення, яке створює дійсну адресу поза кривою, називається "канонічним bump".
Наступні приклади показують отримання PDA з використанням усіх можливих bump seed (від 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 викликає помилку. Перший bump seed, який отримує дійсний PDA, — 254. Bump seed 253-251 також отримують унікальні, дійсні PDA.
Це означає, що за тих самих необов'язкових seed та programId, bump seed з
іншим значенням все одно може отримати дійсний PDA.
Завжди включайте перевірки безпеки, щоб переконатися, що PDA, переданий програмі, отримано з канонічного bump. Невиконання цієї вимоги може призвести до вразливостей, які дозволяють використовувати неочікувані облікові записи в інструкціях програми. Найкращою практикою є використання лише канонічного bump при отриманні PDA.
Приклади
Наведені нижче приклади отримують PDA за допомогою SDK Solana. Натисніть ▷ Run, щоб виконати код.
Отримання PDA з рядковим seed
Наведений нижче приклад отримує PDA, використовуючи ID програми та необов'язковий рядковий 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
Приклад нижче отримує 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 з кількома seeds
Приклад нижче отримує PDA, використовуючи ідентифікатор програми та кілька необов'язкових seeds.
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}`);
Створення облікового запису PDA
Приклад нижче використовує фреймворк Anchor
для створення нового облікового запису з програмно-похідною адресою. Програма
містить єдину інструкцію initialize для створення нового
облікового запису, який зберігатиме адресу користувача
та 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 bumpdaccount_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,}
Обмеження init вказує Anchor
викликати System Program для створення
нового облікового запису, використовуючи PDA як адресу. Seeds,
використані для створення PDA, це:
- Адреса облікового запису користувача, надана в інструкції
- Фіксований рядок: "data"
- Канонічний bump seed
У цьому прикладі обмеження bump не призначено значення, тому Anchor
використовуватиме find_program_address для отримання PDA та знаходження bump.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
Тестовий файл нижче містить транзакцію, яка викликає інструкцію
initialize для створення нового облікового запису з
адресою, похідною від програми. Файл містить код для
отримання PDA.
Приклад також показує, як отримати новий обліковий запис, який буде створено.
import * as anchor from "@coral-xyz/anchor";import { Program } from "@coral-xyz/anchor";import { PdaAccount } from "../target/types/pda_account";import { PublicKey } from "@solana/web3.js";describe("pda-account", () => {const provider = anchor.AnchorProvider.env();anchor.setProvider(provider);const program = anchor.workspace.PdaAccount as Program<PdaAccount>;const user = provider.wallet as anchor.Wallet;// Derive the PDA address using the seeds specified on the programconst [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);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 з тим самим seed адреси
user, транзакція завершиться невдачею. Це відбувається тому, що обліковий
запис уже існує за отриманою адресою.
Is this page helpful?