Una dirección derivada de programa (PDA) es una cuenta cuya dirección se deriva de la dirección del programa que la creó y de las seeds pasadas en la transacción init. Hasta este punto, solo hemos utilizado PDAs.
También es posible crear una cuenta fuera del programa, y luego hacer init de esa cuenta dentro del programa.
Curiosamente, la cuenta que creamos fuera del programa tendrá una clave privada, pero veremos que esto no tendrá las implicaciones de seguridad que parecería tener. Nos referiremos a ella como una “cuenta keypair”.
Revisando la creación de cuentas
Antes de adentrarnos en las cuentas keypair, repasemos cómo hemos estado creando cuentas en nuestros tutoriales de Solana hasta ahora. Este es el mismo código base (boilerplate) que hemos estado utilizando, y crea direcciones derivadas de programa (PDA):
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("4wLnxvLwgXGT4eNg3D456K6Fxa1RieaUdERSPQ3WEpuV");
#[program]
pub mod keypair_vs_pda {
use super::*;
pub fn initialize_pda(ctx: Context<InitializePDA>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializePDA<'info> {
// This is the program derived address
#[account(init,
payer = signer,
space=size_of::<MyPDA>() + 8,
seeds = [],
bump)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyPDA {
x: u64,
}
El siguiente es el código Typescript asociado para llamar a initialize:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KeypairVsPda } from "../target/types/keypair_vs_pda";
describe("keypair_vs_pda", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KeypairVsPda as Program<KeypairVsPda>;
it("Is initialized -- PDA version", async () => {
const seeds = [];
const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("the storage account address is", myPda.toBase58());
const tx = await program.methods.initializePda().accounts({myPda: myPda}).rpc();
});
});
Todo esto debería resultar familiar hasta ahora, excepto que llamamos explícitamente a nuestras cuentas “PDA”.
Program Derived Address
Una cuenta es una Program Derived Address (PDA) si la dirección de la cuenta se deriva de la dirección del programa, es decir, el programId en findProgramAddressSync(seeds, program.programId). También es una función de las seeds.
Específicamente, sabemos que es una PDA porque seeds y bump están presentes en la macro init.
Cuenta Keypair
El siguiente código se verá muy similar al código anterior, pero presta atención al hecho de que la macro init carece de seeds y bump:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("4wLnxvLwgXGT4eNg3D456K6Fxa1RieaUdERSPQ3WEpuV");
#[program]
pub mod keypair_vs_pda {
use super::*;
pub fn initialize_keypair_account(ctx: Context<InitializeKeypairAccount>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeKeypairAccount<'info> {
// This is the keypair account
#[account(init,
payer = signer,
space = size_of::<MyKeypairAccount>() + 8,)]
pub my_keypair_account: Account<'info, MyKeypairAccount>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyKeypairAccount {
x: u64,
}
Cuando seeds y bump están ausentes, el programa Anchor ahora espera que creemos una cuenta primero y luego pasemos la cuenta al programa. Dado que creamos la cuenta nosotros mismos, su dirección no se “derivará de” la dirección del programa. En otras palabras, no será una cuenta derivada de programa (PDA).
Crear una cuenta para el programa es tan simple como generar un nuevo keypair (de la misma manera que usamos para probar diferentes signers en Anchor). Sí, esto puede sonar un poco aterrador, el hecho de que poseamos la clave secreta de una cuenta que el programa está usando para almacenar datos — revisaremos esto en un momento. Por ahora, aquí está el código Typescript para crear una nueva cuenta y pasarla al programa anterior. A continuación, llamaremos la atención sobre las partes importantes:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KeypairVsPda } from "../target/types/keypair_vs_pda";
// this airdrops sol to an address
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("keypair_vs_pda", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KeypairVsPda as Program<KeypairVsPda>;
it("Is initialized -- keypair version", async () => {
const newKeypair = anchor.web3.Keypair.generate();
await airdropSol(newKeypair.publicKey, 1e9); // 1 SOL
console.log("the keypair account address is", newKeypair.publicKey.toBase58());
await program.methods.initializeKeypairAccount()
.accounts({myKeypairAccount: newKeypair.publicKey})
.signers([newKeypair]) // the signer must be the keypair
.rpc();
});
});
Algunas cosas sobre las que queremos llamar la atención:
- Añadimos una función de utilidad
airdropSolpara hacer airdrop de SOL al nuevo keypair que creamosnewKeypair. Sin el SOL, no podría pagar la transacción. Además, debido a que esta es la misma cuenta que se usará para almacenar datos, necesita un saldo en SOL para estar exenta de renta (rent-exempt). Al hacer el airdrop de SOL, se necesita una rutina adicional deconfirmTransactionporque parece haber condiciones de carrera en el entorno de ejecución (runtime) sobre cuándo se hace realmente el airdrop del SOL y cuándo se confirma la transacción. - Cambiamos los
signersdel predeterminado anewKeypair. Al crear una cuenta keypair, no es posible crear una cuenta de la cual no poseas la clave privada.
No es posible inicializar una cuenta keypair de la cual no posees la clave privada
Si pudieras crear una cuenta con una dirección arbitraria, eso supondría un gran riesgo de seguridad, ya que podrías insertar datos maliciosos en una cuenta arbitraria.
Ejercicio: Modifica la prueba para generar un segundo keypair secondKeypair. Usa la clave pública del segundo keypair y reemplaza .accounts({myKeypairAccount: newKeypair.publicKey}) con .accounts({myKeypairAccount: secondKeypair.publicKey}). No cambies el signer. Deberías ver que la prueba falla. No necesitas hacer airdrop de SOL al nuevo keypair ya que no es el signer de la transacción.
Deberías ver un error como el siguiente:

¿Qué pasa si intentamos falsificar la dirección de la PDA?
Ejercicio: En lugar de pasar secondKeypair del ejercicio anterior, deriva una PDA con:
const seeds = [];
const [pda, _bump] = anchor
.web3
.PublicKey
.findProgramAddressSync(
seeds,
program.programId);
luego reemplaza el argumento myKeypairAccount por .accounts({myKeypairAccount: pda}).
Deberías ver nuevamente un error de unknown signer.
El entorno de ejecución de Solana no te permitirá hacer esto. Si un programa tuviera PDAs apareciendo repentinamente cuando no han sido inicializadas, esto llevaría a graves problemas de seguridad.
¿Es un problema que alguien tenga la clave privada de la cuenta?
Parecería que la persona en posesión de la clave privada sería capaz de gastar SOL de la cuenta, y posiblemente llevarla por debajo del umbral de exención de renta. Sin embargo, el entorno de ejecución de Solana evita que esto suceda cuando la cuenta es inicializada por un programa.
Para ver esto, considera la siguiente prueba unitaria:
- Crear una cuenta keypair en Typescript
- Hacer airdrop de sol a la cuenta keypair
- Transferir sol desde la cuenta keypair a otra dirección (tiene éxito)
- Inicializar la cuenta keypair
- Intentar transferir sol desde la cuenta keypair usando el keypair como signer (falla)
El código se muestra a continuación:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KeypairVsPda } from "../target/types/keypair_vs_pda";
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("keypair_vs_pda", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KeypairVsPda as Program<KeypairVsPda>;
it("Writing to keypair account fails", async () => {
const newKeypair = anchor.web3.Keypair.generate();
var receiverWallet = anchor.web3.Keypair.generate();
await airdropSol(newKeypair.publicKey, 10);
var transaction = new anchor.web3.Transaction().add(
anchor.web3.SystemProgram.transfer({
fromPubkey: newKeypair.publicKey,
toPubkey: receiverWallet.publicKey,
lamports: 1 * anchor.web3.LAMPORTS_PER_SOL,
}),
);
await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [newKeypair]);
console.log('sent 1 SOL');
await program.methods.initializeKeypairAccount()
.accounts({myKeypairAccount: newKeypair.publicKey})
.signers([newKeypair]) // the signer must be the keypair
.rpc();
console.log("initialized");
// try to transfer again, this fails
var transaction = new anchor.web3.Transaction().add(
anchor.web3.SystemProgram.transfer({
fromPubkey: newKeypair.publicKey,
toPubkey: receiverWallet.publicKey,
lamports: 1 * anchor.web3.LAMPORTS_PER_SOL,
}),
);
await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [newKeypair]);
});
});
Aquí está el mensaje de error esperado:

Aunque poseamos las claves privadas de esta cuenta, no podemos “gastar SOL” de la cuenta, ya que ahora es propiedad del programa.
Introducción a la propiedad (ownership) y la inicialización
¿Cómo sabe el entorno de ejecución de Solana que debe bloquear la transferencia de SOL después de la inicialización?
Ejercicio: modifica la prueba con el código a continuación. Presta atención a las declaraciones de console log que se han añadido. Están registrando un campo de metadatos “owner” (propietario) en las cuentas y la dirección del programa:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KeypairVsPda } from "../target/types/keypair_vs_pda";
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("keypair_vs_pda", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KeypairVsPda as Program<KeypairVsPda>;
it("Console log account owner", async () => {
console.log(`The program address is ${program.programId}`);
const newKeypair = anchor.web3.Keypair.generate();
// get account owner before initialization
await airdropSol(newKeypair.publicKey, 10);
const accountInfoBefore = await anchor.getProvider().connection.getAccountInfo(newKeypair.publicKey);
console.log(`initial keypair account owner is ${accountInfoBefore.owner}`);
await program.methods.initializeKeypairAccount()
.accounts({myKeypairAccount: newKeypair.publicKey})
.signers([newKeypair]) // the signer must be the keypair
.rpc();
// get account owner after initialization
const accountInfoAfter = await anchor.getProvider().connection.getAccountInfo(newKeypair.publicKey);
console.log(`initial keypair account owner is ${accountInfoAfter.owner}`);
});
});
La captura de pantalla a continuación muestra el resultado esperado:

Después de la inicialización, el propietario (owner) de la cuenta keypair cambió de 111...111 al programa desplegado. Todavía no hemos cubierto en profundidad la importancia de la propiedad de las cuentas o el system program (la dirección de todo unos) en nuestros tutoriales de Solana. Sin embargo, esto debería darte una idea de lo que hace la “inicialización” y por qué el propietario de la clave privada ya no puede transferir SOL fuera de la cuenta.
¿Debería usar PDAs o cuentas Keypair?
Una vez que la cuenta está inicializada, se comportan de la misma manera, por lo que en la práctica no hay mucha diferencia.
La única diferencia significativa (que no afectará a la mayoría de las aplicaciones) es que las PDAs solo pueden ser inicializadas con un tamaño de 10,240 bytes, pero una cuenta keypair puede ser inicializada hasta el tamaño máximo de 10 MB. Sin embargo, una PDA puede ser redimensionada hasta el límite de 10 MB.
La mayoría de las aplicaciones utilizan PDAs ya que pueden ser direccionadas de forma programática a través del parámetro seeds, pero para acceder a una cuenta keypair debes conocer la dirección de antemano. Incluimos una discusión sobre las cuentas keypair porque varios tutoriales en línea las usan como ejemplos, por lo que queremos que tengas algo de contexto. En la práctica, sin embargo, las PDAs son la forma preferida de almacenar datos.
Aprende más con RareSkills
¡Continúa con nuestro curso de Solana para seguir aprendiendo!
Publicado originalmente el 6 de marzo de 2024