Utwórz token account

Jak utworzyć token account z rozszerzeniem Confidential Transfer

Rozszerzenie Confidential Transfer umożliwia prywatne przelewy tokenów poprzez dodanie dodatkowego stanu do token account. W tej sekcji wyjaśniono, jak utworzyć token account z włączonym tym rozszerzeniem.

Poniższy diagram przedstawia kroki związane z tworzeniem token account z rozszerzeniem Confidential Transfer:

Create Token Account with Confidential Transfer Extension

Stan Token Account z rozszerzeniem Confidential Transfer

Rozszerzenie dodaje stan ConfidentialTransferAccount do 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 zawiera kilka pól do zarządzania poufnymi przelewami:

  • approved: Status zatwierdzenia konta dla poufnych przelewów. Jeśli konfiguracja auto_approve_new_accounts mint account jest ustawiona jako true, wszystkie token accounts są automatycznie zatwierdzone do poufnych przelewów.

  • elgamal_pubkey: Klucz publiczny ElGamal używany do szyfrowania sald i kwot przelewów.

  • pending_balance_lo: Zaszyfrowane młodsze 16 bitów oczekującego salda. Saldo jest podzielone na część wysoką i niską dla wydajnego odszyfrowywania.

  • pending_balance_hi: Zaszyfrowane starsze 48 bitów oczekującego salda. Saldo jest podzielone na część wysoką i niską dla wydajnego odszyfrowywania.

  • available_balance: Zaszyfrowane saldo dostępne do przelewów.

  • decryptable_available_balance: Dostępne saldo zaszyfrowane kluczem Advanced Encryption Standard (AES) umożliwiające wydajne odszyfrowanie przez właściciela konta.

  • allow_confidential_credits: Jeśli true, zezwala na przychodzące poufne przelewy.

  • allow_non_confidential_credits: Jeśli true, zezwala na przychodzące niepoufne przelewy.

  • pending_balance_credit_counter: Zlicza przychodzące uznania oczekującego salda z instrukcji depozytów i przelewów.

  • maximum_pending_balance_credit_counter: Limit liczby oczekujących uznań przed wymaganiem instrukcji ApplyPendingBalance do przekształcenia oczekującego salda w dostępne saldo.

  • expected_pending_balance_credit_counter: Wartość pending_balance_credit_counter dostarczona przez klienta poprzez instruction data podczas ostatniego przetwarzania instrukcji ApplyPendingBalance.

  • actual_pending_balance_credit_counter: Wartość pending_balance_credit_counter na token account w momencie przetworzenia ostatniej instrukcji ApplyPendingBalance.

Saldo oczekujące a saldo dostępne

Poufne salda są podzielone na salda oczekujące i dostępne, aby zapobiec atakom DoS. Bez tego podziału atakujący mógłby wielokrotnie wysyłać tokeny na token account, blokując właścicielowi token account możliwość transferu tokenów. Właściciel token account nie byłby w stanie transferować tokenów, ponieważ zaszyfrowane saldo zmieniałoby się między momentem przesłania transakcji a jej przetworzeniem, co skutkowałoby niepowodzeniem transakcji.

Wszystkie depozyty i kwoty transferów są początkowo dodawane do salda oczekującego. Właściciele token account muszą użyć instrukcji ApplyPendingBalance, aby przekonwertować saldo oczekujące na saldo dostępne. Przychodzące transfery lub depozyty nie wpływają na dostępne saldo token account.

Podział salda oczekującego na wysokie/niskie

Poufne saldo oczekujące jest podzielone na pending_balance_lo i pending_balance_hi, ponieważ deszyfrowanie ElGamal wymaga większej liczby obliczeń dla większych liczb. Implementację arytmetyki na szyfrogramach można znaleźć tutaj, która jest używana w instrukcji ApplyPendingBalance tutaj.

Liczniki kredytów salda oczekującego

Podczas wywoływania instrukcji ApplyPendingBalance w celu przekonwertowania salda oczekującego na saldo dostępne:

  1. Klient pobiera aktualne salda oczekujące i dostępne, szyfruje ich sumę i dostarcza decryptable_available_balance zaszyfrowane kluczem AES właściciela token account.

  2. Oczekiwane i rzeczywiste liczniki kredytów oczekujących śledzą zmiany wartości licznika między momentem utworzenia a przetworzeniem instrukcji ApplyPendingBalance:

    • expected_pending_balance_credit_counter: Wartość pending_balance_credit_counter w momencie, gdy klient tworzy instrukcję ApplyPendingBalance
    • actual_pending_balance_credit_counter: Wartość pending_balance_credit_counter na token account w momencie przetworzenia instrukcji ApplyPendingBalance

Zgodność oczekiwanych/rzeczywistych liczników wskazuje, że decryptable_available_balance pasuje do available_balance.

Podczas pobierania stanu token account w celu odczytania decryptable_available_balance, różne wartości oczekiwanych/rzeczywistych liczników wymagają od klienta wyszukania ostatnich instrukcji depozytów/transferów pasujących do różnicy liczników w celu obliczenia poprawnego salda.

Proces uzgadniania salda

Gdy oczekiwane i rzeczywiste liczniki oczekującego salda różnią się, wykonaj następujące kroki, aby uzgodnić decryptable_available_balance:

  1. Zacznij od decryptable_available_balance z token account
  2. Pobierz najnowsze transakcje zawierające instrukcje depozytów i transferów do wartości różnicy liczników (rzeczywisty - oczekiwany):
    • Dodaj publiczne kwoty z instrukcji depozytów
    • Odszyfruj i dodaj kwoty docelowego szyfrogramu z instrukcji transferów

Wymagane instrukcje

Tworzenie i konfigurowanie token account do poufnych transferów wykorzystuje następujące instrukcje, które wszystkie mieszczą się w jednej transakcji:

  1. Utwórz token account: Wywołaj instrukcję AssociatedTokenAccountInstruction::Create Associated Token Program, aby utworzyć token account pod jego deterministycznym adresem.

  2. Przydziel przestrzeń dla konta: Wywołaj instrukcję TokenInstruction::Reallocate Token Extensions Program, aby dodać przestrzeń dla stanu ConfidentialTransferAccount.

  3. Zweryfikuj dowód poprawności pubkey: Utwórz konto należące do programu ZK ElGamal Proof, a następnie wywołaj jego instrukcję VerifyPubkeyValidity w celu weryfikacji dowodu i zapisania zweryfikowanego wyniku na tym koncie stanu kontekstu.

  4. Skonfiguruj poufne transfery: Wywołaj instrukcję ConfidentialTransferInstruction::ConfigureAccount Token Extensions Program, odwołując się do konta stanu kontekstu dowodu poprzez ProofLocation::ContextStateAccount, aby zainicjować stan ConfidentialTransferAccount.

Tylko właściciel token account może skonfigurować token account do confidential transfers.

Instrukcja ConfigureAccount wymaga po stronie klienta wygenerowania kluczy szyfrowania oraz dowodu, który może zostać wygenerowany wyłącznie przez właściciela token account.

Dowód poprawności pubkey weryfikuje, że klucz publiczny ElGamal konta jest poprawny. Generowany jest przy użyciu build_pubkey_validity_proof_data, weryfikowany onchain przez program ZK ElGamal Proof do konta stanu kontekstu, a następnie przywoływany z ConfigureAccount poprzez ProofLocation::ContextStateAccount, dzięki czemu żadne bajty dowodu nie są przesyłane w samej instrukcji token. Szczegóły implementacji — patrz:

Przykładowy kod

Poniższy kod tworzy associated token account i konfiguruje go do poufnych transferów w oparciu o istniejący poufny mint.

Poufne transfery są uzależnione od programu ZK ElGamal Proof, który jest włączony w sieci mainnet i devnet. Standardowy solana-test-validator nie włącza go, natomiast lokalny validator forwardujący mainnet, taki jak Surfpool, już tak. Uruchom przykład na jednej z tych sieci (kod używa devnet) z zasilonym kontem płatnika i zastąp symbol zastępczy mintu mintem utworzonym zgodnie z Utwórz Mint.

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}`
);

Pomocniki TypeScript znajdują się w podścieżce @solana-program/token-2022/confidential i są oparte na @solana/zk-sdk dla prymitywów szyfrowania. owner i client pochodzą z konfiguracji @solana/kit; zwrócony plan instrukcji jest wysyłany z obsługą planów instrukcji @solana/kit, która dzieli pracę na wiele transakcji, gdy dowody są zbyt duże dla jednej.

Is this page helpful?

Spis treści

Edytuj stronę