Resumen
Los PDA se derivan mediante el hash de seeds + ID de programa + bump vía SHA-256 hasta que el resultado esté fuera de la curva Ed25519. El bump canónico es el primer valor que produce una dirección fuera de la curva. Máximo 16 seeds, máximo 32 bytes por seed.
Contexto
Los valores de
Keypair
de Solana son puntos en la curva Ed25519. Un
keypair consiste en una clave pública (usada como dirección de cuenta) y una
clave secreta (usada para producir firmas). Cualquiera con la clave secreta
puede firmar transacciones para esa dirección.
Dos cuentas con direcciones en la curva
Un PDA se deriva intencionalmente para caer fuera de la curva Ed25519. Como no
es un punto válido de la curva, no existe clave secreta, y ninguna parte externa
puede producir una firma. Solo el programa que lo deriva puede autorizar
operaciones en el PDA mediante invoke_signed.
Dirección fuera de la curva
Cuentas PDA vs keypair
| Propiedad | Cuenta keypair | Cuenta PDA |
|---|---|---|
| Tipo de dirección | En la curva Ed25519 | Fuera de la curva Ed25519 |
| Tiene clave privada | Sí | No |
| Puede firmar transacciones | Sí (con clave privada) | No |
| Puede firmar durante CPI | No (a menos que la firma esté incluida en la transacción) | Sí (vía invoke_signed) |
| Derivación | Generar keypair Ed25519 | Determinista desde seeds + ID de programa |
| Uso típico | Wallets de usuario, ID de programa | Cuentas de datos propiedad del programa |
Seeds opcionales
Los seeds opcionales son cadenas de bytes definidas por el usuario que sirven
como entradas para la derivación de PDA. Crean direcciones únicas y
deterministas con alcance a un programa. Por ejemplo, usar
["user", user_pubkey] como seeds deriva un PDA diferente para cada usuario.
Los seeds deben seguir estas restricciones:
- Máximo 16 seeds por derivación (
MAX_SEEDS) - Máximo 32 bytes por seed (
MAX_SEED_LEN)
Bump seed
El bump seed es un solo byte (0-255) que se añade a los seeds opcionales durante
la derivación.
find_program_address
busca desde 255 hacia abajo hasta 0, llamando a create_program_address con
cada valor hasta que el resultado quede fuera de la curva Ed25519. El primer
valor que tiene éxito es el bump canónico.
Los programas siempre deben usar el bump canónico para garantizar un mapeo único y determinista de seeds a dirección.
Siempre usa el bump canónico al derivar PDAs. Usar un bump no canónico crea una segunda dirección válida para los mismos seeds, lo que puede generar vulnerabilidades donde un atacante sustituye una cuenta diferente a la esperada.
PDA Derivation
Algoritmo de derivación
La derivación de PDA se implementa en la función
create_program_address
del SDK. El algoritmo funciona de la siguiente manera:
- Validar que el número de seeds no exceda
MAX_SEEDS(16) y que ningún seed individual excedaMAX_SEED_LEN(32 bytes). Si alguna verificación falla, devolverPubkeyError::MaxSeedLengthExceeded. - Aplicar hash SHA-256 a todos los seeds, el ID del programa y la cadena
"ProgramDerivedAddress"juntos para producir un resultado de 32 bytes. - Verificar si el resultado es un punto válido en la curva Ed25519.
- Si el resultado ESTÁ en la curva, devolver
PubkeyError::InvalidSeeds(la dirección tendría una clave privada correspondiente, lo que viola la propiedad de seguridad de PDA). - Si el resultado NO está en la curva, devolverlo como el PDA.
Costos de unidades de cómputo
La
syscall en cadena
para create_program_address cobra
1.500 CU
por llamada.
La
syscall try_find_program_address
cobra 1.500 CU al inicio (antes del bucle), luego 1.500 CU adicionales por cada
intento de bump fallido dentro del bucle.
Patrones comunes de seed
Los seeds son específicos de la aplicación. Los patrones comunes incluyen:
| Patrón | Seeds | Caso de uso |
|---|---|---|
| Singleton global | ["global"] | Cuenta de configuración única para todo el programa |
| Cuenta por usuario | ["user", user_pubkey] | Una cuenta por usuario por programa |
| Por usuario por entidad | ["vault", user_pubkey, mint_pubkey] | Bóvedas de tokens, por usuario por token |
| Contador / secuencial | ["order", user_pubkey, &order_id.to_le_bytes()] | Registros secuenciales por usuario |
Los seeds se concatenan antes del hash, por lo que ["ab", "cd"] e ["abcd"]
producen la misma PDA. Usa seeds de longitud fija o un separador para evitar
colisiones. Por ejemplo, ["ab", "-", "cd"] no es ambiguo.
Ejemplos: derivar una PDA
Derivar una PDA solo calcula una dirección. No crea una cuenta en cadena en esa
dirección. La cuenta debe crearse explícitamente mediante una instrucción
separada (típicamente create_account vía CPI).
Los SDK de Solana proporcionan funciones para la derivación de PDA. Cada función toma:
- ID del programa: la dirección del programa utilizado para derivar la PDA. Este programa puede firmar en nombre de la PDA.
- Seeds opcionales: entradas predefinidas como cadenas, números u otras direcciones de cuentas.
| SDK | Función |
|---|---|
@solana/kit (TypeScript) | getProgramDerivedAddress |
@solana/web3.js (TypeScript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Los ejemplos a continuación derivan una PDA usando los SDK de Solana. Haz clic en ▷ Ejecutar para ejecutar el código.
Derivar una PDA con un seed de cadena
El ejemplo a continuación deriva una PDA usando un ID de programa y un seed de cadena opcional.
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}`);
Derivar una PDA con un seed de dirección
El ejemplo a continuación deriva una PDA usando un ID de programa y un seed de dirección opcional.
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}`);
Derivar una PDA con múltiples seeds
El ejemplo a continuación deriva una PDA usando un ID de programa y múltiples seeds opcionales.
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}`);
Iterando todos los bumps
Los siguientes ejemplos muestran la derivación de PDA usando todos los posibles
bump seeds (255 a 0), ilustrando cómo find_program_address devuelve el
bump canónico:
El ejemplo de Kit no está incluido porque la función
createProgramDerivedAddress
no está exportada.
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
En este ejemplo, el bump 255 produce una dirección en la curva y falla. El primer bump válido es 254, lo que lo convierte en el bump canónico.
Is this page helpful?