Dirección Derivada de un Programa (PDA)
Las direcciones derivadas de programas (PDAs) ofrecen a los desarrolladores de Solana dos casos de uso principales:
- Direcciones de cuenta deterministas: Las PDAs proporcionan un mecanismo para derivar de forma determinista una dirección utilizando una combinación de "semillas" opcionales (entradas predefinidas) y el identificador de un programa.
- Permitir a los programas firmar: El tiempo de ejecución de Solana permite a los programas "firmar" para PDAs que se derivan de su identificador.
Puedes pensar en las PDA como una forma de crear estructuras tipo diccionario en la cadena de bloques a partir de un conjunto predefinido de entradas (por ejemplo, cadenas, números y otras direcciones de cuenta).
La ventaja de este enfoque es que elimina la necesidad de seguir una dirección exacta. En su lugar, basta con recordar las entradas específicas utilizadas para su derivación.
Dirección derivada de un programa
Es importante entender que calcular la dirección derivada de un programa (PDA) no crea automáticamente una cuenta en esa dirección. Las cuentas con una PDA como dirección en la cadena de bloques deben crearse explícitamente a través del programa utilizado para derivar la dirección. Puede pensar en derivar una PDA como si encontrara una dirección en un mapa. El mero hecho de tener una dirección no significa que haya algo construido en ese lugar.
En esta sección se tratarán los detalles de la derivación de las PDA. Los detalles sobre cómo los programas utilizan las PDA para firmar se tratarán en la sección sobre Invocaciones entre programas (CPI) ya que requiere contexto para ambos conceptos.
Puntos clave #
-
Las PDA son direcciones derivadas de forma determinista utilizando una combinación de semillas definidas por el usuario, un bump y el identificador de un programa.
-
Las PDA son direcciones que caen fuera de la curva Ed25519 y no tienen su correspondiente clave privada.
-
Los programas en Solana pueden "firmar" programáticamente para PDAs que se deriven usando su identificador.
-
La obtención de una PDA no crea automáticamente una cuenta en la cadena.
-
Una cuenta que utilice una PDA como dirección debe crearse explícitamente mediante una instrucción específica dentro de un programa de Solana.
Qué es una PDA #
Las PDA son direcciones que se derivan de forma determinista y parecen llaves públicas estándar, pero no tienen llave privada asociada. Esto significa que ningún usuario externo puede generar una firma válida para la dirección. Sin embargo, el tiempo de ejecución de Solana permite a los programas "firmar" programáticamente por una PDA sin necesidad de una llave privada.
Para contextualizar, los pares de llaves en Solana son puntos de la curva Ed25519 (criptografía de curva elíptica) que tienen una llave pública y su correspondiente llave privada. A menudo utilizamos llaves públicas como identificadores únicos para cuentas nuevas en la cadena de bloques y llaves privadas para firmar.
Dirección Dentro de la Curva
Una PDA es un punto que se deriva intencionadamente para caer fuera de la curva Ed25519 utilizando un conjunto predefinido de entradas. Un punto que no está en la curva Ed25519 no tiene una llave privada correspondiente válida y no puede utilizarse para operaciones criptográficas (firmar).
Una PDA puede utilizarse entonces como dirección (identificador único) para una cuenta en la cadena, lo que proporciona un método para almacenar, asignar y recuperar fácilmente el estado del programa.
Dirección Fuera de la Curva
Cómo derivar una PDA #
La derivación de una PDA requiere 3 entradas.
- Semillas opcionales: Entradas predefinidas (por ejemplo, cadena, número, otras direcciones de cuenta) utilizadas para derivar una PDA. Estas entradas se convierten en un buffer de bytes.
- Semilla bump: Una entrada adicional (con un valor entre 255-0) que se utiliza para garantizar que se genera una PDA válida (fuera de la curva). Esta semilla bump (que empieza por 255) se añade a las semillas opcionales cuando se genera una PDA para garantizar que el punto está fuera de la curva Ed25519. La semilla bump se denomina a veces "nonce".
- Identificador del programa: La dirección del programa del que se deriva la PDA. Este es también el programa que puede "firmar" en nombre de la PDA
Derivación de una PDA
Los siguientes ejemplos incluyen enlaces a Solana Playground, donde puede ejecutar los ejemplos en un editor dentro del navegador.
FindProgramAddress #
Para derivar una PDA, podemos utilizar el método
findProgramAddressSync
de @solana/web3.js
. Existen
equivalentes de esta función en otros lenguajes de programación (por ejemplo,
Rust),
pero en esta sección, recorreremos ejemplos utilizando Javascript.
Cuando se utiliza el método findProgramAddressSync
, pasamos:
- Las semillas opcionales predefinidas convertidas en un buffer de bytes, y
- El identificador del programa (dirección) usado para derivar el PDA
Una vez encontrada una PDA válida, findProgramAddressSync
devuelve tanto la
dirección (PDA) como la semilla bump utilizada para derivar la PDA.
El siguiente ejemplo deriva una PDA sin proporcionar ninguna semilla opcional.
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}`);
Puede ejecutar este ejemplo en Solana Playground. La salida de la PDA y de la semilla bump será siempre la misma:
PDA: Cu7NwqCXSmsR5vgGA3Vw9uYVViPi3kQvkbKByVQ8nPY9
Bump: 255
El siguiente ejemplo a continuación añade una semilla opcional "helloWorld".
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const [PDA, bump] = PublicKey.findProgramAddressSync(
[Buffer.from(string)],
programId,
);
console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);
También puede ejecutar este ejemplo en Solana Playground. La salida de la PDA y de la semilla bump será siempre la misma:
PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
Bump: 254
Tenga en cuenta que la semilla bump es 254. Esto significa que 255 derivó un punto en la curva Ed25519 y no es una PDA válida.
La semilla bump devuelta por findProgramAddressSync
es el primer valor (entre
255-0) para la combinación dada de semillas opcionales y el identificador del
programa que deriva una PDA válida.
Esta primera semilla bump válida se denomina "bump canónico". Por seguridad del programa, se recomienda utilizar únicamente el bump canónico cuando se trabaje con PDA.
CreateProgramAddress #
Bajo el capó, findProgramAddressSync
añadirá iterativamente una bump seed
adicional (nonce) al buffer de seeds y llamará al método
createProgramAddressSync
.
La semilla bump comienza con un valor de 255 y va disminuyendo de 1 en 1 hasta
que se encuentra una PDA válida (fuera de la curva).
Puedes replicar el ejemplo anterior utilizando createProgramAddressSync
y
pasando explícitamente la semilla bump de 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}`);
Puede ejecutar este ejemplo en Solana Playground. Dadas las mismas semillas y el mismo identificador del programa, la salida de la PDA coincidirá con la anterior:
PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
Bump Canónico #
El "bump canónico" se refiere a la primera semilla bump (comenzando en 255 y disminuyendo en 1) que deriva una PDA válida. Por seguridad del programa, se recomienda utilizar únicamente PDA derivados de un bump canónico.
Utilizando el ejemplo anterior como referencia, el siguiente ejemplo intenta derivar una PDA utilizando cada semilla bump entre 255 y 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);
}
}
Ejecuta el ejemplo en Solana Playground y deberías ver la siguiente salida:
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
...
// salidas restantes de bump
Como era de esperar, la semilla bump 255 arroja un error y la primera semilla bump que deriva una PDA válida es 254.
Sin embargo, ten en cuenta que las semillas bump 253-251 obtienen PDAs válidas con diferentes direcciones. Esto significa que dadas las mismas semillas opcionales e identificador de un programa, una semilla bump con un valor diferente podría derivar una PDA válida.
Al crear programas en Solana, se recomienda incluir comprobaciones de seguridad que validen que una PDA pasada al programa se deriva usando el bump canónico. No hacerlo puede introducir vulnerabilidades que permitan suministrar cuentas inesperadas a un programa.
Crear cuentas PDA #
Este programa de ejemplo en Solana Playground demuestra cómo crear una cuenta utilizando una PDA como dirección de la cuenta. El programa de ejemplo está escrito usando el marco de trabajo de Anchor.
En el archivo lib.rs
encontrarás el siguiente programa que incluye una
instrucción para crear una cuenta utilizando una PDA como dirección. La nueva
cuenta almacena la dirección del user
y la semilla bump
utilizada para
derivar la 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 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,
}
Las semillas utilizadas para derivar la PDA incluyen la cadena de caracteres
codificada data
y la dirección de la cuenta user
proporcionada en la
instrucción. El marco de trabajo de Anchor obtiene automáticamente el bump
canónico.
#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
El init
indica a Anchor que invoque al programa del sistema para crear una
cuenta utilizando la PDA como dirección. Bajo el capó, esto se hace a través de
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>,
En el archivo de prueba (pda-account.test.ts
) que se encuentra en el enlace de
Solana Playground proporcionado anteriormente, encontrará el equivalente en
Javascript para derivar la PDA.
const [PDA] = PublicKey.findProgramAddressSync(
[Buffer.from("data"), user.publicKey.toBuffer()],
program.programId,
);
A continuación, se envía una transacción para invocar la instrucción
initialize
para crear una nueva cuenta en la cadena de bloques utilizando la
PDA como dirección. Una vez enviada la transacción, la PDA se utiliza para
buscar la cuenta que se creó en la dirección.
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));
});
Ten en cuenta que si invocas la instrucción initialize
más de una vez
utilizando la misma dirección user
como semilla, la transacción fallará. Esto
se debe a que ya existirá una cuenta en la dirección derivada.