Производные адреса PDA

Резюме

PDA-адреса вычисляются путём хеширования seeds + идентификатор программы + bump через SHA-256 до тех пор, пока результат не окажется вне кривой Ed25519. Канонический bump — это первое значение, при котором получается адрес вне кривой. Максимум 16 seeds, максимум 32 байта на каждый seed.

Введение

В Solana Keypair значения — это точки на кривой Ed25519. keypair состоит из публичного ключа (используется как адрес аккаунта) и секретного ключа (используется для создания подписей). Любой, у кого есть секретный ключ, может подписывать транзакции для этого адреса.

Два аккаунта с адресами на кривойДва аккаунта с адресами на кривой

PDA специально вычисляется так, чтобы находиться вне кривой Ed25519. Поскольку это невалидная точка кривой, секретного ключа не существует, и ни одна внешняя сторона не может создать подпись. Только программа, производящая PDA, может авторизовать операции с этим PDA через invoke_signed.

Адрес вне кривойАдрес вне кривой

PDA и аккаунты на keypair

СвойствоАккаунт на keypairPDA-аккаунт
Тип адресаНа кривой Ed25519Вне кривой Ed25519
Есть приватный ключДаНет
Может подписыватьДа (с приватным ключом)Нет
Может подписывать при CPIНет (если подпись не включена в транзакцию)Да (через invoke_signed)
ДеривацияГенерация keypair Ed25519Детерминировано из seeds + идентификатора программы
Типичное применениеПользовательские кошельки, Program IDАккаунты данных, принадлежащие программе

Необязательные seed'ы

Необязательные seed'ы — это определяемые пользователем байтовые строки, которые используются в качестве входных данных для генерации PDA. Они позволяют создавать уникальные, детерминированные адреса, привязанные к конкретной программе. Например, использование ["user", user_pubkey] в качестве seed'ов приводит к созданию отдельного PDA для каждого пользователя.

Seed'ы должны соответствовать следующим ограничениям:

  • Максимум 16 seed'ов на одну генерацию (MAX_SEEDS)
  • Максимум 32 байта на один seed (MAX_SEED_LEN)

Bump seed

Bump seed — это один байт (0–255), который добавляется к необязательным seed'ам при генерации. find_program_address производит поиск от 255 до 0, вызывая create_program_address с каждым значением, пока результат не выйдет за пределы кривой Ed25519. Первое подходящее значение становится каноническим bump.

Программы всегда должны использовать канонический bump, чтобы обеспечить уникальное и детерминированное соответствие между seed'ами и адресом.

Всегда используйте канонический bump при генерации PDA. Использование неканонического bump создаёт второй валидный адрес для тех же seed'ов, что может привести к уязвимостям, когда злоумышленник подставляет другой аккаунт вместо ожидаемого.

Генерация PDAГенерация PDA

Алгоритм генерации

Генерация PDA реализована в функции SDK create_program_address. Алгоритм работает следующим образом:

  1. Проверить, что количество seed'ов не превышает MAX_SEEDS (16), а длина каждого seed не превышает MAX_SEED_LEN (32 байта). Если хотя бы одно из условий не выполняется, вернуть PubkeyError::MaxSeedLengthExceeded.
  2. Выполнить SHA-256 хеширование всех seed'ов, идентификатора программы и строки "ProgramDerivedAddress" вместе, чтобы получить 32-байтовый результат.
  3. Проверить, является ли результат допустимой точкой на кривой Ed25519.
  4. Если результат НАХОДИТСЯ на кривой, вернуть PubkeyError::InvalidSeeds (адрес будет иметь соответствующий приватный ключ, что нарушает безопасность PDA).
  5. Если результат НЕ находится на кривой, вернуть его как PDA.

Стоимость вычислительных единиц

Системный вызов в блокчейне для create_program_address взимает 1 500 CU за каждый вызов.

try_find_program_address syscall взимает 1 500 CU при входе (до начала цикла), а затем дополнительно 1 500 CU за каждую неудачную попытку bump внутри цикла.

Типовые шаблоны seed

Seed зависят от конкретного приложения. Часто встречающиеся шаблоны:

ШаблонSeedsСценарий использования
Глобальный синглтон["global"]Единый конфиг-аккаунт для всей программы
Аккаунт пользователя["user", user_pubkey]Один аккаунт на пользователя на программу
На пользователя и сущность["vault", user_pubkey, mint_pubkey]Токен-хранилища, на пользователя и токен
Счетчик / последовательный["order", user_pubkey, &order_id.to_le_bytes()]Последовательные записи на пользователя

Seed конкатенируются перед хешированием, поэтому ["ab", "cd"] и ["abcd"] приводят к одному и тому же PDA. Используйте seed фиксированной длины или разделитель, чтобы избежать коллизий. Например, ["ab", "-", "cd"] однозначен.

Примеры: получение PDA

Получение PDA вычисляет только адрес. Аккаунт по этому адресу не создаётся автоматически в блокчейне. Его нужно явно создать отдельной инструкцией (обычно create_account через CPI).

SDK Solana предоставляют функции для получения PDA. Каждая функция принимает:

  • Program ID: адрес программы, используемой для получения PDA. Эта программа может подписывать от имени PDA.
  • Необязательные seed: заранее определённые входные данные, такие как строки, числа или другие адреса аккаунтов.
SDKФункция
@solana/kit (TypeScript)getProgramDerivedAddress
@solana/web3.js (TypeScript)findProgramAddressSync
solana_sdk (Rust)find_program_address

В примерах ниже показано, как получить PDA с помощью SDK Solana. Нажмите ▷ Запустить, чтобы выполнить код.

Получение PDA с помощью строкового seed

В примере ниже показано, как получить PDA, используя program ID и необязательный строковый seed.

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}`);
Console
Click to execute the code.

Получение PDA с помощью адресного seed

В примере ниже показано, как получить PDA, используя program ID и необязательный адресный seed.

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}`);
Console
Click to execute the code.

Получение PDA с несколькими seed

В примере ниже показано, как получить PDA, используя program ID и несколько необязательных seed.

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}`);
Console
Click to execute the code.

Перебор всех bump

Следующие примеры показывают получение PDA с использованием всех возможных bump seed (от 255 до 0), иллюстрируя, как find_program_address возвращает канонический bump:

Пример для Kit не включён, так как функция createProgramDerivedAddress не экспортируется.

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);
}
}
Console
Click to execute the code.
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
...
// remaining bump outputs

В этом примере bump 255 создаёт адрес на кривой и не проходит проверку. Первый валидный bump — 254, он становится каноническим.

Is this page helpful?

Управляется

© 2026 Solana Foundation.
Все права защищены.
Связаться с нами