Program Derived Address (PDA)
I Program Derived Addresses (PDA) offrono agli sviluppatori su Solana due casi d'uso principali:
- Indirizzi di account deterministici: i PDA forniscono un meccanismo per creare deterministicamente un indirizzo utilizzando una combinazione di "seed" opzionali (input predefiniti) e uno specifico ID del programma.
- Abilitazione della firma del programma: il runtime di Solana consente ai programmi di "firmare" per i PDA che sono derivati dall'indirizzo del programma.
Puoi pensare ai PDA come a un modo per creare strutture simili a hashmap on-chain da un insieme predefinito di input (ad esempio stringhe, numeri e altri indirizzi di account).
Il vantaggio di questo approccio è che elimina la necessità di tenere traccia di un indirizzo esatto. Invece, devi semplicemente ricordare gli input specifici utilizzati per la sua derivazione.
Program Derived Address
È importante capire che derivare semplicemente un Program Derived Address (PDA) non crea automaticamente un account on-chain a quell'indirizzo. Gli account con un PDA come indirizzo on-chain devono essere creati esplicitamente attraverso il programma utilizzato per derivare l'indirizzo. Puoi pensare alla derivazione di un PDA come alla ricerca di un indirizzo su una mappa. Avere solo un indirizzo non significa che ci sia qualcosa costruito in quella posizione.
Questa sezione copre i dettagli della derivazione dei PDA. La sezione sulle Cross Program Invocations (CPI) spiega come i programmi utilizzano i PDA per la firma.
Punti chiave
- I PDA sono indirizzi derivati deterministicamente utilizzando una combinazione di seed predefiniti, un bump seed e l'ID di un programma.
- I PDA sono indirizzi che cadono fuori dalla curva Ed25519 e non hanno una chiave privata corrispondente.
- I programmi Solana possono firmare per conto dei PDA derivati dal proprio ID programma.
- Derivare un PDA non crea automaticamente un account on-chain.
- Un account che utilizza un PDA come indirizzo deve essere creato tramite un'istruzione all'interno di un programma Solana.
Cos'è un PDA
I PDA sono indirizzi che derivano in modo deterministico e sembrano chiavi pubbliche, ma non hanno chiavi private. Questo significa che non è possibile generare una firma valida per l'indirizzo. Tuttavia, il runtime di Solana consente ai programmi di "firmare" per i PDA senza bisogno di una chiave privata.
Per contesto, le Keypair di Solana sono punti sulla curva Ed25519 (crittografia a curva ellittica) con una chiave pubblica e la corrispondente chiave privata. Le chiavi pubbliche sono utilizzate come indirizzi (identificatori unici) per gli account on-chain.
Indirizzo sulla curva
Un PDA è un punto che viene intenzionalmente derivato per cadere fuori dalla curva Ed25519 utilizzando un insieme predefinito di input. Un punto che non si trova sulla curva Ed25519 non ha una chiave privata corrispondente valida e non può eseguire operazioni crittografiche (firma).
Un PDA può servire come indirizzo (identificatore unico) per un account on-chain, fornendo un metodo per memorizzare, mappare e recuperare facilmente lo stato del programma.
Indirizzo fuori dalla curva
Come derivare un PDA
La derivazione di un PDA richiede tre input:
- seed opzionali: Input predefiniti (ad es. stringhe, numeri, altri indirizzi di account) per la derivazione del PDA.
- bump seed: Un byte aggiuntivo aggiunto ai seed opzionali per garantire che venga generato un PDA valido (fuori dalla curva). Il bump seed inizia da 255 e diminuisce di 1 fino a quando non viene trovato un PDA valido.
- Program ID: L'indirizzo del programma da cui deriva il PDA. Questo programma può firmare per conto del PDA.
Derivazione PDA
Usa le seguenti funzioni dai rispettivi SDK per derivare un PDA.
SDK | Funzione |
---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Per derivare un PDA, fornisci i seguenti input alla funzione SDK:
- I seed opzionali predefiniti convertiti in byte
- L'ID del programma (indirizzo) utilizzato per la derivazione
Una volta trovato un PDA valido, la funzione restituisce sia l'indirizzo (PDA) che il bump seed utilizzato per la derivazione.
Esempi
I seguenti esempi mostrano come derivare un PDA utilizzando i rispettivi SDK.
Clicca il pulsante "Run" per eseguire il codice.
Derivare un PDA con seed stringa opzionale
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}`);
Derivare un PDA con seed indirizzo opzionale
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}`);
Derivare un PDA con molteplici seed opzionali
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}`);
Canonical Bump
La derivazione PDA richiede un "bump seed", un byte aggiuntivo aggiunto ai seed opzionali. La funzione di derivazione itera attraverso i valori di bump, partendo da 255 e decrementando di 1, fino a quando un valore produce un indirizzo valido fuori dalla curva. Il primo valore di bump che produce un indirizzo valido fuori dalla curva è il "canonical bump."
I seguenti esempi mostrano la derivazione PDA utilizzando tutti i possibili bump seed (da 255 a 0):
Esempio Kit non incluso perché la funzione createProgramDerivedAddress non è esportata.
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
Il bump seed 255 genera un errore e il primo bump seed a derivare un PDA valido è 254.
Nota che i bump seed 253-251 derivano tutti PDA validi con indirizzi diversi.
Questo significa che dati gli stessi seed opzionali e programId
, un bump seed
con un valore diverso può comunque derivare un PDA valido.
Quando si sviluppano programmi Solana, includi sempre controlli di sicurezza per garantire che un PDA passato al programma sia derivato dal canonical bump. Non includere questi controlli potrebbe introdurre vulnerabilità che consentono l'utilizzo di account imprevisti nelle istruzioni del programma. È buona pratica utilizzare solo il canonical bump quando si derivano i PDA.
Creare account PDA
Il programma di esempio seguente mostra come creare un account utilizzando un PDA come indirizzo del nuovo account. Il programma di esempio utilizza il framework Anchor.
Il programma include una singola istruzione initialize
per creare un nuovo
account utilizzando un PDA come indirizzo dell'account. Il nuovo account
memorizza l'indirizzo del user
e il seed bump
utilizzato per derivare il
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,}
In questo esempio, i seed per la derivazione PDA includono la stringa fissa
data
e l'indirizzo dell'account user
fornito nell'istruzione. Il framework
Anchor trovà automaticamente il seed canonico bump
.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
Il vincolo init
indica ad Anchor di invocare il System Program per creare un
nuovo account utilizzando il PDA come indirizzo. Anchor lo fa attraverso una
CPI.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
Il file di test contiene il codice Typescript per derivare il PDA.
const [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);
La transazione nel file di test invoca l'istruzione initialize
per creare un
nuovo account on-chain utilizzando il PDA come indirizzo. In questo esempio,
Anchor può dedurre l'indirizzo PDA negli account dell'istruzione, quindi non è
necessario fornirlo esplicitamente.
it("Is initialized!", async () => {const transactionSignature = await program.methods.initialize().accounts({user: user.publicKey}).rpc();console.log("Transaction Signature:", transactionSignature);});
Il file di test mostra anche come recuperare l'account on-chain creato a quell'indirizzo una volta inviata la transazione.
it("Fetch Account", async () => {const pdaAccount = await program.account.dataAccount.fetch(PDA);console.log(JSON.stringify(pdaAccount, null, 2));});
Nota che in questo esempio, se invochi l'istruzione initialize
più di una
volta utilizzando lo stesso indirizzo user
come seed, la transazione fallirà.
Questo accade perché esiste già un account all'indirizzo derivato.
Is this page helpful?