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 programaDirecció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.

Info

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 CurvaDirecció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 CurvaDirecció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 PDADerivació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.

Info

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.

Warning

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.

lib.rs
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.