Địa chỉ dẫn xuất từ chương trình
Địa chỉ tài khoản Solana trỏ đến vị trí của tài khoản trên blockchain. Nhiều địa chỉ tài khoản là khóa công khai của một keypair, trong trường hợp này khóa riêng tương ứng được sử dụng để ký các giao dịch liên quan đến tài khoản.
Một lựa chọn thay thế hữu ích cho địa chỉ khóa công khai là địa chỉ dẫn xuất từ chương trình (PDA). PDA cung cấp một phương thức dễ dàng để lưu trữ, ánh xạ và truy xuất trạng thái chương trình. PDA là một địa chỉ được tạo ra một cách xác định bằng cách sử dụng ID chương trình và kết hợp các đầu vào được xác định trước tùy chọn. PDA trông giống với địa chỉ khóa công khai, nhưng không có khóa riêng tương ứng.
Môi trường thực thi Solana cho phép các chương trình ký cho PDA mà không cần khóa riêng. Sử dụng PDA loại bỏ nhu cầu theo dõi địa chỉ của tài khoản. Thay vào đó, bạn có thể nhớ lại các đầu vào cụ thể được sử dụng để dẫn xuất PDA. (Để tìm hiểu cách chương trình sử dụng PDA để ký, xem phần Gọi chương trình chéo.)
Nền tảng
Keypair của Solana là các điểm trên đường cong Ed25519 (mật mã đường cong elliptic). Chúng bao gồm một khóa công khai và một khóa riêng. Khóa công khai trở thành địa chỉ tài khoản, và khóa riêng được sử dụng để tạo ra chữ ký hợp lệ cho tài khoản.
Hai tài khoản với địa chỉ trên đường cong
PDA được cố ý dẫn xuất để nằm ngoài đường cong Ed25519. Điều này có nghĩa là nó không có khóa riêng tương ứng hợp lệ và không thể thực hiện các hoạt động mật mã. (Chẳng hạn như cung cấp chữ ký.) Tuy nhiên, Solana cho phép các chương trình ký cho PDA mà không cần khóa riêng.
Địa chỉ ngoài đường cong
Bạn có thể xem PDAs như một cách để tạo ra các cấu trúc giống như hashmap trên chuỗi bằng cách sử dụng một tập hợp các đầu vào được xác định trước. (Ví dụ: chuỗi, số và các địa chỉ tài khoản khác.)
Program Derived Address
Tạo một PDA
Trước khi tạo một tài khoản với PDA, bạn phải tạo địa chỉ trước. Việc tạo một PDA không tự động tạo một tài khoản trên chuỗi tại địa chỉ đó— tài khoản phải được tạo một cách rõ ràng thông qua chương trình được sử dụng để tạo PDA. Bạn có thể xem PDA giống như một địa chỉ trên bản đồ: chỉ vì một địa chỉ tồn tại không có nghĩa là có bất cứ thứ gì được xây dựng ở đó.
Các SDK của Solana hỗ trợ việc tạo PDA với các hàm được hiển thị trong bảng dưới đây. Mỗi hàm nhận các đầu vào sau:
- Program ID: Địa chỉ của chương trình được sử dụng để tạo PDA. Chương trình này có thể ký thay mặt cho PDA.
- Optional seeds: Các đầu vào được xác định trước, chẳng hạn như chuỗi, số hoặc các địa chỉ tài khoản khác.
| SDK | Hàm |
|---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Hàm sử dụng program ID và optional seeds, sau đó lặp qua các giá trị bump để cố gắng tạo một địa chỉ chương trình hợp lệ. Việc lặp lại các giá trị bump bắt đầu từ 255 và giảm dần 1 cho đến khi tìm thấy một PDA hợp lệ. Sau khi tìm thấy một PDA hợp lệ, hàm trả về PDA và bump seed.
Bump seed là một byte bổ sung được thêm vào optional seeds để đảm bảo một địa chỉ ngoài đường cong hợp lệ được tạo ra.
Quá trình tạo PDA
Canonical bump
Bump seed là một byte bổ sung được thêm vào các seeds tùy chọn. Hàm tạo sẽ lặp qua các giá trị bump, bắt đầu từ 255 và giảm dần 1 đơn vị, cho đến khi một giá trị tạo ra một địa chỉ hợp lệ nằm ngoài đường cong. Giá trị bump đầu tiên tạo ra một địa chỉ hợp lệ nằm ngoài đường cong được gọi là "canonical bump."
Các ví dụ sau đây cho thấy quá trình tạo PDA sử dụng tất cả các bump seeds có thể có (từ 255 đến 0):
Ví dụ về Kit không được đưa vào vì hàm createProgramDerivedAddress không được xuất ra.
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
Trong ví dụ này, bump seed đầu tiên gây ra lỗi. Bump seed đầu tiên tạo ra PDA hợp lệ là 254. Các bump seeds 253-251 cũng tạo ra các PDA hợp lệ và duy nhất.
Điều này có nghĩa là với cùng một seeds tùy chọn và programId, một bump seed
với giá trị khác vẫn có thể tạo ra một PDA hợp lệ.
Luôn bao gồm các kiểm tra bảo mật để đảm bảo PDA được truyền vào chương trình được tạo ra từ canonical bump. Nếu không làm như vậy có thể tạo ra các lỗ hổng cho phép các tài khoản không mong muốn được sử dụng trong các lệnh của chương trình. Thông lệ tốt nhất là chỉ sử dụng canonical bump khi tạo PDA.
Ví dụ
Các ví dụ dưới đây tạo ra một PDA sử dụng Solana SDKs. Nhấp vào ▷ Run để thực thi mã.
Tạo PDA với một string seed
Ví dụ dưới đây tạo ra một PDA sử dụng một program ID và một string seed tùy chọn.
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}`);
Tạo một PDA với seed là địa chỉ
Ví dụ dưới đây tạo một PDA sử dụng program ID và một seed địa chỉ tùy chọn.
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}`);
Tạo một PDA với nhiều seed
Ví dụ dưới đây tạo một PDA sử dụng program ID và nhiều seed tùy chọn.
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}`);
Tạo tài khoản PDA
Ví dụ dưới đây sử dụng Anchor framework để
tạo một tài khoản mới với địa chỉ được tạo từ chương trình. Chương trình bao gồm
một hướng dẫn initialize duy nhất để tạo tài khoản mới,
sẽ lưu trữ địa chỉ người dùng và
bump seed được sử dụng để tạo 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,}
Ràng buộc init hướng dẫn Anchor
gọi System Program để tạo một tài
khoản mới sử dụng PDA làm địa chỉ. Các seed được sử dụng để tạo
PDA là:
- Địa chỉ của tài khoản người dùng được cung cấp trong hướng dẫn
- Chuỗi cố định: "data"
- Bump seed chính thức
Trong ví dụ này, ràng buộc bump không được gán giá trị, vì vậy Anchor sẽ sử dụng
find_program_address để tạo PDA và tìm bump.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
Tệp kiểm thử dưới đây chứa một giao dịch gọi lệnh
initialize để tạo một tài khoản mới với địa chỉ được tạo
từ chương trình. Tệp này chứa mã để tạo PDA.
Ví dụ này cũng cho thấy cách truy xuất tài khoản mới sẽ được tạo.
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));});});
Nếu bạn gọi lệnh initialize một lần nữa với cùng seed địa chỉ user, giao
dịch sẽ thất bại. Điều này xảy ra vì đã tồn tại một tài khoản tại địa chỉ đã
được tạo.
Is this page helpful?