En la primera parte de este tutorial, creamos cuentas de almacenamiento en Rust nativo utilizando pares de claves (keypairs), donde la cuenta requería una clave privada para firmar su inicialización. Ahora exploraremos un enfoque diferente utilizando direcciones derivadas de programas (Program Derived Addresses o PDAs), que no tienen claves privadas pero aún pueden usarse como cuentas de almacenamiento a través de un mecanismo de firma especial.
Creando cuentas de almacenamiento con PDAs
Antes de sumergirnos en el código, entendamos qué hace que la creación de cuentas PDA sea diferente de las cuentas basadas en pares de claves:
Cuentas Keypair (Pares de claves):
- Tienen una clave privada que puede firmar transacciones
- El par de claves debe firmar su propia inicialización
- Requieren
isSigner: trueal crear la cuenta
Cuentas PDA:
- Derivadas de forma determinista a partir de semillas (seeds) y un ID de programa
- No tienen clave privada, por lo que no pueden firmar directamente transacciones o instrucciones
- Nuestro programa actúa como firmante en nombre de la PDA utilizando
invoke_signed() - Requieren las semillas utilizadas para derivar la dirección como prueba de propiedad
Esta diferencia fundamental significa que utilizaremos invoke_signed() en lugar de invoke() al crear cuentas PDA, ya que el System Program necesita una firma para inicializar cualquier cuenta.
Construyendo el programa de almacenamiento PDA
Reemplaza el código en src/lib.rs (de la Parte 1) con esta versión. En el siguiente código:
- Importamos dependencias adicionales para la creación de la PDA (
invoke_signed,Rent,Sysvar) - Obtenemos las cuentas requeridas (cuenta de almacenamiento, firmante, programa del sistema, renta)
- Verificamos que recibimos el programa del sistema correcto y que la cuenta del firmante es válida
- Creamos
CounterDatacon un valor de 100 y lo serializamos con Borsh - Creamos una cuenta de almacenamiento PDA usando
invoke_signedcon una semilla (seed) y un bump para derivar la dirección de la PDA - Escribimos los datos serializados directamente en la cuenta
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction, system_program,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
// This represents the data we'll store in our account
// We've added Borsh derive macros for serialization and deserialization
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterData {
pub count: u64,
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Storage Write Program: Creating PDA storage account and writing data");
let accounts_iter = &mut accounts.iter();
// Get the accounts we need
// next_account_info() extracts the next AccountInfo from the iterator
let storage_account = next_account_info(accounts_iter)?;
let signer = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let rent = next_account_info(accounts_iter)?;
// Verify the system program
if system_program.key != &system_program::ID {
msg!("Invalid system program");
return Err(ProgramError::IncorrectProgramId);
}
// Verify the signer is a signer
if !signer.is_signer {
msg!("Signer must be a signer");
return Err(ProgramError::MissingRequiredSignature);
}
// Create our counter data
let counter_data = CounterData { count: 100 };
let serialized_data = counter_data.try_to_vec()?;
let space = serialized_data.len();
msg!("Creating PDA storage account with {} bytes", space);
msg!("Serialized data: {:?}", serialized_data);
// Get rent info
let rent_sysvar = Rent::from_account_info(rent)?;
let lamports = rent_sysvar.minimum_balance(space);
// Define the seed for our PDA
let seed = b"storage";
let (expected_pda, bump_seed) = Pubkey::find_program_address(&[seed], program_id);
// Verify the provided account is the expected PDA
if storage_account.key != &expected_pda {
msg!("Invalid PDA provided");
return Err(ProgramError::InvalidAccountData);
}
// Create the account using system program with PDA signing
let create_account_ix = system_instruction::create_account(
signer.key,
storage_account.key,
lamports,
space as u64,
program_id,
);
// Accounts needed for the create_account instruction
let accounts = &[
signer.clone(),
storage_account.clone(),
system_program.clone(),
];
// Seeds for PDA signing (seed + bump)
let signer_seeds = &[&seed[..], &[bump_seed]];
invoke_signed(&create_account_ix, accounts, &[&signer_seeds[..]])?;
// Write data to the account
let mut account_data = storage_account.try_borrow_mut_data()?;
account_data.copy_from_slice(&serialized_data);
msg!("Data written to PDA storage account");
Ok(())
}
Ahora que hemos visto la implementación completa, examinemos el mecanismo que hace posible la creación de cuentas PDA.
Uso de invoke_signed() para la creación de PDAs
invoke_signed() permite que nuestro programa actúe como firmante para una PDA proporcionando las semillas utilizadas para derivar esa dirección. El entorno de ejecución (runtime) de Solana verifica que las semillas realmente derivan la PDA, y si lo hacen, trata a la PDA como si hubiera firmado la transacción.
Sin este mecanismo, el System Program rechazaría la instrucción create_account porque la dirección de la PDA no tendría una firma válida.
Entendiendo el parámetro signers_seeds
invoke_signed() puede manejar múltiples PDAs firmando en una sola llamada CPI. Es por esto que signers_seeds tiene una estructura anidada — es un array de arrays de semillas PDA.
Aquí está nuestra estructura de semillas para una PDA:
let seed = b"storage";
let bump_seed = bump_seed;
// Seeds that derive our PDA: ["storage" + bump]
let signer_seeds: &[&[&[u8]]] = &[
&[seed, &[bump_seed]] // ← seeds for our one PDA
];
invoke_signed(&create_account_ix, accounts, signer_seeds)?;
Desglosando los tres niveles de anidamiento (de afuera hacia adentro):
&[ // Outer: array of PDA seed sets (we have 1 PDA signing)
&[ // Middle: this PDA's seed components (we have 2)
seed, // Component 1: "storage"
&[bump_seed] // Component 2: bump byte
]
]
- Exterior
&[...]: Un conjunto de semillas por cada PDA firmante (en nuestro caso, solo 1) - Medio
&[...]: Múltiples componentes de semilla para cada PDA (usamos 2: el string y el bump) - Interior
&[u8]: Los bytes individuales de cada componente de la semilla
Si tuviéramos dos PDAs firmando, se vería así:
let signer_seeds: &[&[&[u8]]] = &[
&[seed1, &[bump1]], // First PDA's seeds
&[seed2, &[bump2]], // Second PDA's seeds
];
Explicando el proceso de creación de la cuenta de almacenamiento PDA
Ahora que entendemos cómo funciona invoke_signed(), veamos exactamente cómo lo usamos para crear nuestra cuenta de almacenamiento PDA.
En el código anterior, puedes ver que comenzamos derivando la dirección de la PDA:

Esto deriva una dirección determinista basada en el ID de nuestro programa y la semilla “storage”. El bump_seed es un solo byte que asegura que la dirección es válida.
A continuación, verificamos que la cuenta enviada por el cliente coincida con la PDA que esperamos:

Esto asegura que el cliente está pasando la dirección PDA correcta que hemos derivado.
Finalmente, creamos la cuenta de almacenamiento y escribimos la estructura serializada en ella usando invoke_signed:

El System Program crea la cuenta en la dirección determinista de la PDA, y nuestro programa se convierte en el propietario.
Probando la creación del almacenamiento PDA
Ahora vamos a probar el enfoque con PDA. Reemplaza tu client/client.ts para probar el almacenamiento PDA. En este cliente:
- Creamos un par de claves firmante y derivamos una dirección de almacenamiento PDA con una semilla “storage” y una semilla bump, utilizando
PublicKey.findProgramAddressSync - Hacemos un airdrop de SOL a la cuenta del firmante
- Pasamos las cuentas requeridas a nuestro programa para la creación del almacenamiento PDA (cuenta PDA, cuenta firmante, programa del sistema, renta)
- Ejecutamos la transacción para crear la cuenta PDA y escribir los datos
- Leemos nuevamente los datos de la cuenta y verificamos que se escribieron correctamente (que el valor del count es exactamente 100)
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
SYSVAR_RENT_PUBKEY,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
const PROGRAM_ID = new PublicKey('YOUR_PROGRAM_ID_HERE'); // Replace with your actual program ID
const connection = new Connection('<http://localhost:8899>', 'confirmed');
async function testPDAStorage() {
console.log('Testing PDA Storage Creation\\n');
// Create accounts
const signer = Keypair.generate();
// Create PDA for storage
const [pdaStorage, _bump] = PublicKey.findProgramAddressSync(
[Buffer.from("storage")],
PROGRAM_ID
);
// Fund the signer account
console.log('Funding signer account...');
await connection.requestAirdrop(signer.publicKey, LAMPORTS_PER_SOL);
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(`Signer: ${signer.publicKey.toString()}`);
console.log(`PDA Storage: ${pdaStorage.toString()}\\n`);
// Test PDA storage account
console.log('=== Testing PDA Storage ===');
const pdaIx = new TransactionInstruction({
keys: [
{ pubkey: pdaStorage, isSigner: false, isWritable: true },
{ pubkey: signer.publicKey, isSigner: true, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
],
programId: PROGRAM_ID,
data: Buffer.alloc(0),
});
const pdaTx = new Transaction().add(pdaIx);
const pdaSig = await sendAndConfirmTransaction(connection, pdaTx, [signer]);
console.log(`PDA transaction: ${pdaSig}\\n`);
// Verify PDA data was written correctly
console.log('=== Verifying PDA Data ===');
const pdaAccountInfo = await connection.getAccountInfo(pdaStorage);
if (pdaAccountInfo && pdaAccountInfo.data.length > 0) {
console.log('PDA account data length:', pdaAccountInfo.data.length, 'bytes');
console.log('Raw PDA data:', Array.from(pdaAccountInfo.data));
// Deserialize the PDA data back to verify
const pdaData = new Uint8Array(pdaAccountInfo.data);
// DataView lets us read binary data as specific types (u64 in this case)
// getBigUint64(0, true) reads 8 bytes starting at offset 0, little-endian
const pdaCount = new DataView(pdaData.buffer).getBigUint64(0, true);
console.log('Deserialized PDA count value:', pdaCount.toString());
if (pdaCount === 100n) {
console.log('Success! PDA data was written correctly.');
} else {
console.log('Error: Expected PDA count 100, got', pdaCount.toString());
}
} else {
console.log('Error: Could not read PDA account data');
}
}
testPDAStorage().catch(console.error);
Nuevamente, asegúrate de que la variable PROGRAM_ID esté configurada con el ID de tu programa.
También, asegúrate de que tu validador local de Solana esté en ejecución y que el programa haya sido desplegado en él.
Ahora ejecuta la prueba:
cd client
npm run test
Verás que se crea la cuenta de almacenamiento PDA con un valor de contador (counter value) de 100.
Esto demuestra cómo crear cuentas de almacenamiento y escribir datos en programas de Solana en Rust puro utilizando las PDAs.
Este artículo es parte de una serie de tutoriales sobre desarrollo en Solana.