Pontos-chave de PDAs no Anchor
Uma Program Derived Address (PDA) é uma chave pública derivada de bytes de seed e um program ID que fica fora da curva ed25519, portanto não tem chave privada. Seu programa Anchor é o único ator que pode autorizar ações para esse endereço, provando seeds e bump ao runtime durante um CPI.
Esta página é o mapa conceitual de PDAs, Signer Seeds & Anchor - derivação orientada por constraints, armazenamento de bump, assinatura PDA, padrões de authority, derivação no client e modos de falha que aparecem em produção no Agave 4.1.1.
Resumo
- O Anchor transforma PDAs em contas determinísticas validadas: constraints
seedsebumpprovam identidade de endereço em toda instrução, e a mesma tupla de seeds é o que o programa usa depois para assinar CPIs. - Por que importa: Escrows, vaults, mints, contas de config e estado por usuário quase sempre ficam em PDAs. Seeds erradas, constraints ausentes ou signer seeds quebradas significam tokens presos, estado falsificável ou drift silencioso de endereço entre client e programa.
- Conceitos-chave: seeds, bump canônico,
find_program_address/create_program_address,#[account(seeds, bump)],ctx.bumps, bump armazenado,CpiContext::new_with_signer, authority PDA, paridade de seeds no client. - Quando usar este modelo: Projetar grafos de contas Anchor que precisam de endereços determinísticos, custódia pelo programa ou sharding por entidade; conectar clients TypeScript com @solana/kit 7.0.0; auditar instruções que tocam vaults ou mint authorities.
- Limitações / trade-offs: PDAs não podem ser contas
Signercomuns; fórmulas de seed são para sempre depois que contas existem; arrays de seed e busca de bump custam CU; client e código on-chain devem permanecer byte-idênticos em seeds e program ID. - Tópicos relacionados: PDAs no Anchor, Assinatura com PDAs, Armazenamento de bumps, Authorities PDA, Derivação de PDA no client, Erros comuns de PDA.
Fundamentos
Programas Solana não mantêm estado de aplicação dentro do executável. Registros duráveis vivem em contas que o programa possui. Uma PDA é um tipo especial de endereço para essas contas (e para papéis de signer puros que nunca mantêm dados): é computada, não gerada a partir de um keypair.
(address, bump) = find_program_address(seeds, program_id)find_program_address tenta valores de bump de 255 para baixo até o ponto ficar fora da curva. Esse primeiro valor válido é o bump canônico. create_program_address verifica um candidato com bump conhecido; não busca.
Como não existe chave privada para um endereço fora da curva, uma assinatura de transação normal não pode autorizar essa chave. Somente um programa que fornece seeds e bump corretos (sob o program ID usado na derivação) pode marcar a PDA como signer em uma instrução aninhada via invoke_signed / Anchor with_signer.
No Anchor 0.32.1 você declara essa verificação em vez de implementá-la manualmente:
#[account(
seeds = [b"escrow", maker.key().as_ref(), escrow_id.to_le_bytes().as_ref()],
bump,
)]
pub escrow: Account<'info, Escrow>,O Anchor re-deriva a PDA a partir dessas seeds e declare_id!, depois exige que a chave da conta passada corresponda. bump = account.bump opcional usa um byte armazenado para que a validação não re-busque. Caminhos de init usam init, payer, space e as mesmas seeds para que a conta seja criada no endereço derivado em um passo.
Uma PDA não é Signer<'info> na struct accounts. Signers keypair provam presença com assinatura de transação; PDAs provam presença com seeds. Use Account, SystemAccount ou outros wrappers tipados com seeds / bump, e assine apenas ao emitir um CPI.
Mecânica e interações
Constraints como verdade on-chain
Toda instrução que toca uma PDA deve re-afirmar seeds. Clients escolhem qual chave pública passar; sem constraints, um atacante pode substituir uma conta lookalike que seu handler trata como vault canônico ou registro de usuário.
| Preocupação | Mecanismo Anchor | O que impõe |
|---|---|---|
| Identidade de endereço | seeds = [...], bump / bump = ... | Chave passada igual à PDA para program ID + seeds |
| Criação | init, payer, space, seeds | Aloca e atribui no endereço derivado |
| Mutação | mut + seeds | Mesmas verificações de identidade, flag writable |
| Corpo tipado | Account<'info, T> | Owner, discriminator, deserialize |
| Assinatura em CPI | new_with_signer + fatias de seed | PDA aparece como signer para programas callee |
Mantenha componentes de seed pequenos e estáveis: prefixo estático (b"vault"), pubkey distintiva e talvez id little-endian. Pilhas de seed longas ou variáveis queimam CU e convidam bugs no client. Documente a tupla de seeds ao lado do tipo de conta para Rust, testes e TypeScript permanecerem alinhados.
Armazenamento de bumps
No primeiro init, o Anchor expõe o mapa de bump resolvido como ctx.bumps.<field_name>. Grave isso nos dados da conta:
ctx.accounts.vault.bump = ctx.bumps.vault;Instruções posteriores usam bump = vault.bump na constraint e o mesmo byte nas signer seeds de CPI. Benefícios:
- CU: sem busca repetida de bump em caminhos quentes.
- Determinismo: CPI sempre usa o bump que tornou o endereço válido na criação.
- Clareza: um campo é a fonte da verdade para assinatura.
Não aceite bump fornecido pelo client como argumento de instrução para verificações críticas de segurança. Prefira armazenamento on-chain e constraints. Se contas legadas não têm campo bump, adicione instrução de migração que recomputa com find_program_address uma vez e grava o resultado; não invente um bump.
Veja Armazenamento de bumps para padrões e notas de migração.
Assinatura com PDAs
Quando a PDA deve autorizar um CPI (transferência de token de vault ATA, mint_to, transferência system de lamports mantidos por PDA, etc.), construa signer seeds que correspondam às seeds da constraint, com bump como seed final de um byte:
let bump = ctx.accounts.vault.bump;
let seeds: &[&[u8]] = &[b"vault", authority.key().as_ref(), &[bump]];
let signer_seeds = &[seeds];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.vault_ata.to_account_info(),
to: ctx.accounts.user_ata.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
},
signer_seeds,
),
amount,
)?;Ordem e bytes das seeds devem corresponder ao init e às constraints. O meta authority no CPI deve ser o account info da PDA, não uma wallet de usuário. Uma PDA pode assinar múltiplos CPIs na mesma instrução com as mesmas seeds. Bump errado ou seed ausente falha o CPI como assinatura ausente (a transação inteira reverte). Equivalente nativo: invoke_signed. Detalhes em Assinatura com PDAs.
Authorities PDA
Programas usam PDAs como authorities quando o protocolo, não um keypair humano, deve controlar ativos ou supply:
- Vault / escrow PDA possui ATA com depósitos; CPIs de saque usam seeds do vault.
- Treasury PDA recebe taxas; caminhos de gasto exigem regras impostas pelo programa mais signer seeds.
- Mint authority PDA minta recompensas ou itens de jogo; opcionalmente renuncie depois definindo mint authority como
Nonevia CPI quando o design congela supply.
Sempre restrinja relacionamentos de mint e ATA (token::authority, associated_token::authority, igualdade de chave de mint) para que assinatura PDA válida não opere no mint ou conta de token errados. Prefira PDAs separadas por mercado, pool ou feature em vez de uma authority PDA global: blast radius menor se um bug aparecer em um caminho, e melhor paralelismo Sealevel quando mercados diferentes escrevem chaves diferentes.
Veja Authorities PDA.
Derivação no client
Clients devem passar as mesmas chaves públicas que o programa re-derivará. Com @solana/kit 7.0.0, use getProgramDerivedAddress (ou helpers gerados) com:
- O endereço do programa do IDL /
declare_id!(não uma chave de deploy aleatória de um env file antigo). - Bytes de seed idênticos on-chain: prefixos de string como bytes ASCII correspondendo a
b"...", pubkeys como 32 bytes brutos, inteiros como little-endian (paridadeto_le_bytes). - A mesma ordem de seeds.
const [escrowPda] = await getProgramDerivedAddress({
programAddress: programId,
seeds: [
new TextEncoder().encode("escrow"),
getBytesEncoder().encode(makerAddress),
// u64 little-endian correspondendo a Rust to_le_bytes
u64ToLeBytes(escrowId),
],
});Compartilhe constantes ou gere seed builders a partir do IDL para que TypeScript não derive do Rust. Testes de integração devem afirmar que PDAs derivadas no client igualam endereços criados em testes Anchor. Veja Derivação de PDA no client.
Considerações avançadas e aplicações
PDAs de programa externo. Ao fazer CPI para outro programa que possui uma PDA, passe esse endereço mas não assine por ela a menos que você seja esse programa. seeds::program do Anchor valida PDAs estrangeiras contra outro program ID. Seu programa só assina PDAs derivadas sob seu program ID.
Init vs re-init. Seeds fixam um slot no espaço de endereços. Após close, as mesmas seeds podem reabrir o mesmo endereço; projete caminhos de close com cuidado se a lógica assume "uma vez inicializado, confiável para sempre".
Sharding e CU. PDAs por usuário mantêm locks writable disjuntos sob Sealevel. Mantenha config global readonly em caminhos quentes. Armazene bumps, mantenha seeds curtas e simule antes da mainnet: busca de bump e pilhas profundas de CPI custam CU.
Ciclo de vida de authority. Mova mint ou freeze authority para PDA apenas em init controlado. Deixar keypair como mint authority após deploy de "vault PDA" é bug de segurança.
Matriz de testes. Para cada tipo de PDA: init happy-path, seeds erradas no client, program ID errado, CPI com signer seeds boas e ruins, e mismatch de token authority. Casos de falha estão catalogados em Erros comuns de PDA.
Equívocos comuns
- "Uma PDA é só uma conta normal com nome chique." É um endereço fora da curva; identidade e assinatura dependem de seeds e program ID, não de chave privada.
- "Se eu passar a pubkey certa do client, constraints seeds são opcionais." Sem re-derivação on-chain, o client escolhe confiança. Atacantes passam contas lookalike.
- "PDAs pertencem em
Signer<'info>porque assinam CPIs." Signers em nível de transação são keypairs. PDAs viram signers apenas para invocações aninhadas via prova de seeds. - "Posso mudar fórmulas de seed em upgrade e manter as mesmas contas." Endereços são fixos por seeds e program ID. Novas fórmulas são novos endereços; planeje migração.
- "O bump é segredo." O bump é público e recuperável; segurança vem da lógica do programa e constraints, não do sigilo do bump. Ainda assim armazene e reutilize o bump canônico por CU e consistência.
- "Endianness no client não importa para seeds numéricas." Rust
to_le_bytese buffers JS big-endian produzem PDAs diferentes. Corresponda LE explicitamente. - "Uma authority PDA compartilhada é mais simples e tão segura." Mais simples até um bug drenar todo mercado. Prefira PDAs com escopo quando escala ou isolamento importam.
- "
with_signeré só para Token." Qualquer CPI que exija a PDA como signer (System Program, Token, Token-2022, callees customizados) precisa de signer seeds. - "UncheckedAccount serve para PDAs durante desenvolvimento." É o caminho padrão para ownership e verificações de seed ausentes. Use contas tipadas com seeds desde o primeiro rascunho.
- "find_program_address a cada CPI é grátis." É correto mas custa CU; armazene bump após init para caminhos quentes.
FAQs
O que é uma PDA em uma frase?
Um endereço determinístico fora da curva derivado de seeds e program ID, sem chave privada, que só pode ser assinado por esse programa via invoke_signed / Anchor with_signer.
Qual versão do Anchor e stack de client esta seção assume?
Anchor 0.32.1 on-chain, Rust 1.91.1, tooling Agave 4.1.1 / Solana CLI 3.0.10, e @solana/kit 7.0.0 para derivação no client e construção de transações.
Como funcionam constraints seeds e bump do Anchor?
Elas re-derivam o endereço do programa a partir das expressões de seed listadas e bump esperado, depois exigem que a chave pública da conta corresponda. Init cria a conta nesse endereço; instruções posteriores re-verificam identidade toda vez.
Uma PDA pode ser declarada como Signer em struct Accounts?
Não. Use conta tipada (ou system account) com seeds e bump. Forneça signer seeds apenas ao construir CPI que precisa da PDA como authority.
Onde o bump deve ficar?
Nos dados da conta, definido uma vez na inicialização a partir de ctx.bumps, depois referenciado com bump = account.bump e em arrays de seed de CPI. Veja Armazenamento de bumps.
Como uma PDA move tokens?
A PDA (ou ATA owned pela PDA) mantém os tokens; o programa faz CPI no Token program com CpiContext::new_with_signer usando seeds do vault para que a PDA seja authority. Veja Assinatura com PDAs e Authorities PDA.
Qual program ID é usado ao derivar PDAs para meu programa Anchor?
O program ID de declare_id! / endereço deployado no IDL. Código client deve usar esse mesmo ID, não a chave de outro programa.
Arrays de seeds incluem bump?
Na sintaxe de constraint do Anchor, bump é separado (bump ou bump = ...). Para create_program_address e signer seeds de CPI, bump é a seed final de um byte anexada à lista de seeds.
Como depurar ConstraintSeeds ou falhas de assinatura em CPI?
Log ou imprima endereço derivado no client, seeds esperadas on-chain, program ID e bump. Diff ordem de seeds, codificação de string, endianness de inteiro e se a conta authority do CPI é a PDA. Veja Erros comuns de PDA.
Contas PDA são rent-exempt?
PDAs com dados precisam de lamports rent-exempt para seu space, financiados pelo payer no init. PDAs signer puras ainda precisam de planejamento deliberado de lamports conforme o padrão.
Uma PDA pode assinar múltiplos CPIs em uma instrução?
Sim. Reutilize as mesmas fatias de signer seed para cada chamada new_with_signer.
Como TypeScript deve derivar a mesma PDA que Rust?
Use getProgramDerivedAddress do @solana/kit 7.0.0 com bytes de seed, ordem, inteiros little-endian e endereço de programa idênticos. Prefira constantes compartilhadas ou codegen. Veja Derivação de PDA no client.
Quando devo dividir PDAs authority em vez de uma authority global?
Quando você tem múltiplos mercados ou domínios de risco, ou quer que Sealevel execute fluxos não sobrepostos em paralelo. Um caminho authority global writable serializa tráfego e concentra impacto de exploit.
Upgrade do programa muda endereços PDA?
Não, se program ID e fórmulas de seed permanecem iguais. Upgrades mudam apenas bytecode do ProgramData. Novas seeds ou novo program ID geram novas PDAs.
Relacionados
- PDAs no Anchor - seeds, bump e constraints de init
- Assinatura com PDAs -
with_signere fatias de seed de CPI - Armazenamento de bumps - persistência e reutilização do bump canônico
- Authorities PDA - vaults, tesourarias e mint authorities
- Derivação de PDA no client - seeds correspondentes em TypeScript com kit
- Erros comuns de PDA - mismatch de seeds, constraints ausentes, erros de assinatura
Versões da stack: Esta página foi escrita para Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1 e @solana/kit 7.0.0.