Резюме
PDA-адреса вычисляются путём хеширования seeds + идентификатор программы + bump через SHA-256 до тех пор, пока результат не окажется вне кривой Ed25519. Канонический bump — это первое значение, при котором получается адрес вне кривой. Максимум 16 seeds, максимум 32 байта на каждый seed.
Введение
В Solana
Keypair
значения — это точки на кривой Ed25519. keypair
состоит из публичного ключа (используется как адрес аккаунта) и секретного ключа
(используется для создания подписей). Любой, у кого есть секретный ключ, может
подписывать транзакции для этого адреса.
Два аккаунта с адресами на кривой
PDA специально вычисляется так, чтобы находиться вне кривой Ed25519. Поскольку
это невалидная точка кривой, секретного ключа не существует, и ни одна внешняя
сторона не может создать подпись. Только программа, производящая PDA, может
авторизовать операции с этим PDA через invoke_signed.
Адрес вне кривой
PDA и аккаунты на keypair
| Свойство | Аккаунт на keypair | PDA-аккаунт |
|---|---|---|
| Тип адреса | На кривой 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 реализована в функции SDK
create_program_address.
Алгоритм работает следующим образом:
- Проверить, что количество seed'ов не превышает
MAX_SEEDS(16), а длина каждого seed не превышаетMAX_SEED_LEN(32 байта). Если хотя бы одно из условий не выполняется, вернутьPubkeyError::MaxSeedLengthExceeded. - Выполнить SHA-256 хеширование всех seed'ов, идентификатора программы и строки
"ProgramDerivedAddress"вместе, чтобы получить 32-байтовый результат. - Проверить, является ли результат допустимой точкой на кривой Ed25519.
- Если результат НАХОДИТСЯ на кривой, вернуть
PubkeyError::InvalidSeeds(адрес будет иметь соответствующий приватный ключ, что нарушает безопасность PDA). - Если результат НЕ находится на кривой, вернуть его как 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}`);
Получение 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}`);
Получение 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}`);
Перебор всех 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);}}
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
В этом примере bump 255 создаёт адрес на кривой и не проходит проверку. Первый валидный bump — 254, он становится каноническим.
Is this page helpful?