Создание аккаунта токена

Как создать token account с расширением Confidential Transfer

Расширение Confidential Transfer обеспечивает приватные переводы токенов, добавляя дополнительное состояние к token account. В этом разделе объясняется, как создать token account с включённым расширением.

На следующей диаграмме показаны шаги по созданию token account с расширением Confidential Transfer:

Create Token Account with Confidential Transfer Extension

Состояние token account с Confidential Transfer

Расширение добавляет состояние ConfidentialTransferAccount к token account:

Confidential Token Account State
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct ConfidentialTransferAccount {
/// `true` if this account has been approved for use. All confidential
/// transfer operations for the account will fail until approval is
/// granted.
pub approved: PodBool,
/// The public key associated with ElGamal encryption
pub elgamal_pubkey: PodElGamalPubkey,
/// The low 16 bits of the pending balance (encrypted by `elgamal_pubkey`)
pub pending_balance_lo: EncryptedBalance,
/// The high 48 bits of the pending balance (encrypted by `elgamal_pubkey`)
pub pending_balance_hi: EncryptedBalance,
/// The available balance (encrypted by `encryption_pubkey`)
pub available_balance: EncryptedBalance,
/// The decryptable available balance
pub decryptable_available_balance: DecryptableBalance,
/// If `false`, the extended account rejects any incoming confidential
/// transfers
pub allow_confidential_credits: PodBool,
/// If `false`, the base account rejects any incoming transfers
pub allow_non_confidential_credits: PodBool,
/// The total number of `Deposit` and `Transfer` instructions that have
/// credited `pending_balance`
pub pending_balance_credit_counter: PodU64,
/// The maximum number of `Deposit` and `Transfer` instructions that can
/// credit `pending_balance` before the `ApplyPendingBalance`
/// instruction is executed
pub maximum_pending_balance_credit_counter: PodU64,
/// The `expected_pending_balance_credit_counter` value that was included in
/// the last `ApplyPendingBalance` instruction
pub expected_pending_balance_credit_counter: PodU64,
/// The actual `pending_balance_credit_counter` when the last
/// `ApplyPendingBalance` instruction was executed
pub actual_pending_balance_credit_counter: PodU64,
}

ConfidentialTransferAccount содержит несколько полей для управления конфиденциальными переводами:

  • approved: Статус подтверждения аккаунта для конфиденциальных переводов. Если в конфигурации auto_approve_new_accounts mint account задано значение true, все token accounts автоматически подтверждаются для конфиденциальных переводов.

  • elgamal_pubkey: Публичный ключ ElGamal, используемый для шифрования балансов и сумм переводов.

  • pending_balance_lo: Зашифрованные младшие 16 бит ожидающего баланса. Баланс разделён на старшую и младшую части для эффективного дешифрования.

  • pending_balance_hi: Зашифрованные старшие 48 бит ожидающего баланса. Баланс разделён на старшую и младшую части для эффективного дешифрования.

  • available_balance: Зашифрованный баланс, доступный для переводов.

  • decryptable_available_balance: Доступный баланс, зашифрованный с помощью ключа стандарта расширенного шифрования (AES), для эффективного дешифрования владельцем аккаунта.

  • allow_confidential_credits: Если значение true, разрешает входящие конфиденциальные переводы.

  • allow_non_confidential_credits: Если значение true, разрешает входящие неконфиденциальные переводы.

  • pending_balance_credit_counter: Подсчитывает входящие кредиты ожидающего баланса из инструкций депозита и перевода.

  • maximum_pending_balance_credit_counter: Предельное количество ожидающих кредитов перед тем, как потребуется инструкция ApplyPendingBalance для конвертации ожидающего баланса в доступный.

  • expected_pending_balance_credit_counter: Значение pending_balance_credit_counter, предоставленное клиентом через instruction data в последний раз, когда была обработана инструкция ApplyPendingBalance.

  • actual_pending_balance_credit_counter: Значение pending_balance_credit_counter в token account на момент обработки последней инструкции ApplyPendingBalance.

Ожидающий и доступный баланс

Конфиденциальные балансы разделены на ожидающие и доступные, чтобы предотвратить DoS-атаки. Без этого разделения злоумышленник мог бы многократно отправлять токены на token account, блокируя возможность владельца token account переводить токены. Владелец token account не мог бы выполнять переводы, поскольку зашифрованный баланс изменялся бы в промежутке между отправкой транзакции и её обработкой, что приводило бы к сбою транзакции.

Все депозиты и суммы переводов изначально добавляются к ожидающему балансу. Владельцы token account должны использовать инструкцию ApplyPendingBalance, чтобы конвертировать ожидающий баланс в доступный. Входящие переводы или депозиты не влияют на доступный баланс token account.

Разделение ожидающего баланса на старшую и младшую части

Конфиденциальный ожидающий баланс разделён на pending_balance_lo и pending_balance_hi, поскольку расшифровка ElGamal требует большего объёма вычислений для больших чисел. Реализацию арифметики зашифрованных текстов можно найти здесь, которая используется в инструкции ApplyPendingBalance здесь.

Счётчики кредитов ожидающего баланса

При вызове инструкции ApplyPendingBalance для конвертации ожидающего баланса в доступный:

  1. Клиент получает текущие ожидающий и доступный балансы, шифрует их сумму и предоставляет decryptable_available_balance, зашифрованный с помощью AES-ключа владельца token account.

  2. Ожидаемый и фактический счётчики кредитов отслеживают изменения значения счётчика в промежутке между созданием и обработкой инструкции ApplyPendingBalance:

    • expected_pending_balance_credit_counter: значение pending_balance_credit_counter в момент, когда клиент создаёт инструкцию ApplyPendingBalance
    • actual_pending_balance_credit_counter: значение pending_balance_credit_counter в token account на момент обработки инструкции ApplyPendingBalance

Совпадение счётчиков ожидаемого/фактического значения указывает на то, что decryptable_available_balance соответствует available_balance.

При получении состояния token account для чтения decryptable_available_balance, различные значения счётчиков ожидаемого/фактического требуют от клиента поиска последних инструкций депозита/перевода, соответствующих разнице счётчиков, для расчёта корректного баланса.

Процесс сверки баланса

Когда счётчики ожидаемого и фактического отложенного баланса различаются, выполните следующие шаги для сверки decryptable_available_balance:

  1. Начните с decryptable_available_balance из token account
  2. Получите последние транзакции, включая инструкции депозита и перевода, вплоть до разницы счётчиков (фактический - ожидаемый):
    • Добавьте публичные суммы из инструкций депозита
    • Расшифруйте и добавьте суммы зашифрованного текста назначения из инструкций перевода

Необходимые инструкции

Создание и настройка token account для конфиденциальных переводов использует следующие инструкции, которые умещаются в одной транзакции:

  1. Создание token account: Вызовите инструкцию AssociatedTokenAccountInstruction::Create Associated Token Program для создания token account по его детерминированному адресу.

  2. Перераспределение пространства аккаунта: Вызовите инструкцию TokenInstruction::Reallocate Token Extension Program для добавления пространства под состояние ConfidentialTransferAccount.

  3. Проверка доказательства корректности pubkey: Создайте аккаунт, принадлежащий программе ZK ElGamal Proof, затем вызовите её инструкцию VerifyPubkeyValidity для верификации доказательства и сохранения результата проверки в аккаунте состояния контекста.

  4. Настройка конфиденциальных переводов: Вызовите инструкцию Token Extension Program ConfidentialTransferInstruction::ConfigureAccount, ссылаясь на аккаунт состояния контекста доказательства через ProofLocation::ContextStateAccount, для инициализации состояния ConfidentialTransferAccount.

Только владелец токен-аккаунта может настроить токен-аккаунт для конфиденциальных переводов.

Инструкция ConfigureAccount требует генерации на стороне клиента ключей шифрования и доказательства, которое может быть сгенерировано только владельцем token account.

Доказательство корректности pubkey подтверждает, что открытый ключ ElGamal аккаунта является действительным. Оно генерируется с помощью build_pubkey_validity_proof_data, верифицируется в блокчейне программой ZK ElGamal Proof в аккаунт состояния контекста, а затем ссылается из ConfigureAccount через ProofLocation::ContextStateAccount, поэтому байты доказательства не передаются в самой инструкции токена. Подробности реализации см. здесь:

Пример кода

Следующий код создаёт associated token account и настраивает его для конфиденциальных переводов на основе существующего конфиденциального минта.

Конфиденциальные переводы зависят от программы ZK ElGamal Proof, которая включена в mainnet и devnet. Стандартный solana-test-validator не активирует её, однако локальный validator с форком mainnet, например Surfpool, — активирует. Запустите пример на одном из них (в коде используется devnet) с пополненным аккаунтом плательщика и замените заглушку минта на минт, созданный согласно Создание минта.

Rust

// The native ZK ElGamal Proof program verifies the proof on chain.
const ZK_PROOF_PROGRAM_ID: Pubkey =
solana_pubkey::pubkey!("ZkE1Gama1Proof11111111111111111111111111111");
fn main() -> Result<()> {
// Use a cluster whose ZK ElGamal Proof program is enabled (mainnet, devnet).
let rpc_client = RpcClient::new_with_commitment(
String::from("https://api.devnet.solana.com"),
CommitmentConfig::confirmed(),
);
// The Solana CLI default keypair, used as fee payer, mint authority, and
// token account owner.
let payer = load_keypair()?;
let decimals: u8 = 2;
// Setup: create a confidential mint for the token account.
let mint = create_confidential_mint(&rpc_client, &payer, decimals)?;
let token_account = get_associated_token_address_with_program_id(
&payer.pubkey(),
&mint,
&spl_token_2022::id(),
);
// 1. Create the associated token account.
let create_ata_ix = create_associated_token_account(
&payer.pubkey(), // funding account
&payer.pubkey(), // token account owner
&mint,
&spl_token_2022::id(),
);
// 2. Add space for the ConfidentialTransferAccount extension.
let realloc_ix = reallocate(
&spl_token_2022::id(),
&token_account,
&payer.pubkey(), // payer
&payer.pubkey(), // owner
&[&payer.pubkey()],
&[ExtensionType::ConfidentialTransferAccount],
)?;
// 3. Derive the owner's ElGamal keypair and AES key from a signature over
// the token account address. The same signer and address always derive
// the same keys, so the owner can recover them from their wallet.
let (elgamal_keypair, aes_key) = derive_confidential_keys(&payer, &token_account.to_bytes())
.map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;
// Initial decryptable available balance of 0, encrypted with the AES key.
let decryptable_balance: PodAeCiphertext = aes_key.encrypt(0).into();
let maximum_pending_balance_credit_counter: u64 = 65_536;
// 4. Generate the pubkey-validity proof, then pre-verify it into a context
// state account owned by the ZK ElGamal Proof program. configure_account
// references the verified proof by account, so no proof bytes travel in
// the token instruction itself.
let proof_data = build_pubkey_validity_proof_data(&elgamal_keypair)
.map_err(|e| anyhow::anyhow!("generate pubkey validity proof: {e}"))?;
let proof_account = Keypair::new();
let context_state_size = size_of::<ProofContextState<PubkeyValidityProofContext>>();
let context_state_rent =
rpc_client.get_minimum_balance_for_rent_exemption(context_state_size)?;
let create_proof_account_ix = system_instruction::create_account(
&payer.pubkey(),
&proof_account.pubkey(),
context_state_rent,
context_state_size as u64,
&ZK_PROOF_PROGRAM_ID,
);
let proof_account_address: Address = proof_account.pubkey().to_bytes().into();
let owner_address: Address = payer.pubkey().to_bytes().into();
let verify_proof_ix = ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(
Some(ContextStateInfo {
context_state_account: &proof_account_address,
context_state_authority: &owner_address,
}),
&proof_data,
);
// 5. Configure the account, pointing at the pre-verified proof account.
let proof_location: ProofLocation<PubkeyValidityProofData> =
ProofLocation::ContextStateAccount(&proof_account.pubkey());
let configure_account_ixs = configure_account(
&spl_token_2022::id(),
&token_account,
&mint,
&decryptable_balance,
maximum_pending_balance_credit_counter,
&payer.pubkey(), // owner
&[],
proof_location,
)?;
// Everything fits in a single transaction.
let mut instructions = vec![
create_ata_ix,
realloc_ix,
create_proof_account_ix,
verify_proof_ix,
];
instructions.extend(configure_account_ixs);
let blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[&payer, &proof_account],
blockhash,
);
let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
println!("Configured token account {token_account} for confidential transfers: {signature}");
Ok(())
}

Typescript

const client = await createClient()
.use(signerFromFile(join(homedir(), ".config/solana/id.json")))
.use(
solanaRpc({
rpcUrl: "https://api.devnet.solana.com"
})
);
// The Solana CLI default keypair, used as fee payer, mint authority, and
// token account owner.
const owner = client.payer;
const decimals = 2;
// Setup: create a confidential mint for the token account.
const mint = await createConfidentialMint(client, owner, decimals);
const [tokenAccount] = await findAssociatedTokenPda({
owner: owner.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
mint
});
// Derive recoverable ElGamal and AES keys bound to (owner, mint). Re-deriving
// from the same wallet always yields the same keys, so the owner can recover
// them rather than having to back up a separate secret.
const derivedElGamal = await deriveElGamalKeypairForOwnerMint({
signer: owner,
owner: owner.address,
mint
});
const elgamalKeypair = ElGamalKeypair.fromSecretKey(
ElGamalSecretKey.fromBytes(derivedElGamal.secretKey)
);
const aesKey = AeKey.fromBytes(
await deriveAeKeyForOwnerMint({ signer: owner, owner: owner.address, mint })
);
// Build the create-ATA + reallocate + verify-proof + configure plan, then send.
// The helper returns an instruction plan because the steps may span more than
// one transaction.
const plan = await getCreateConfidentialTransferAccountInstructionPlan({
rpc: client.rpc,
payer: owner,
owner,
mint,
elgamalKeypair,
aesKey
});
const result = await client.sendTransaction(plan);
console.log(
`Configured token account ${tokenAccount} for confidential transfers: ${result.context.signature}`
);

TypeScript-хелперы находятся в подпути @solana-program/token-2022/confidential и основаны на @solana/zk-sdk для примитивов шифрования. owner и client берутся из вашей настройки @solana/kit; возвращаемый план инструкций отправляется с поддержкой планов инструкций @solana/kit, которая распределяет работу по нескольким транзакциям, если доказательства слишком велики для одной.

Is this page helpful?

Содержание

Редактировать страницу