Utwórz token account

Jak utworzyć token account z Poufnymi Saldami

Poufne Salda umożliwiają prywatne transfery tokenów poprzez dodanie dodatkowego stanu do token account. Ta sekcja wyjaśnia, jak utworzyć token account z wymaganym rozszerzeniem włączonym.

Poniższy diagram przedstawia kroki związane z tworzeniem token account z włączonymi Poufnymi Saldami:

Create Token Account with Confidential Balances

Stan Token Account dla Poufnych Sald

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 zarządzających Poufnymi Saldami:

  • approved: Status zatwierdzenia konta dla Poufnych Sald. Jeśli konfiguracja auto_approve_new_accounts mint account jest ustawiona na true, wszystkie token accounts są automatycznie zatwierdzane dla Poufnych Sald.

  • 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 dla Poufnych Sald wykorzystuje następujące instrukcje, które 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. Konfigurowanie Poufnych Sald: 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 włączyć Poufne Salda na token account.

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

Dowód ważności pubkey weryfikuje, że klucz publiczny ElGamal konta jest prawidłowy. Jest generowany za pomocą build_pubkey_validity_proof_data, weryfikowany on-chain przez program ZK ElGamal Proof do konta stanu kontekstu, a następnie odwoływany z ConfigureAccount poprzez ProofLocation::ContextStateAccount, dzięki czemu żadne bajty dowodu nie są przesyłane w samej instrukcji tokena. Szczegóły implementacji można znaleźć tutaj:

Przykładowy kod

Poniższy kod tworzy associated token account i konfiguruje go do używania Poufnych Sald z istniejącym kompatybilnym mintem.

Poufne Salda są zależne od programu ZK ElGamal Proof, który jest włączony w sieci mainnet i devnet. Standardowy solana-test-validator go nie włącza, ale lokalny walidator z forkiem mainnet, taki jak Surfpool, już tak. Uruchom przykład na jednej z tych sieci (kod używa devnet) z zasilonym płatnikiem i zastąp symbol zastępczy minta mintem utworzonym zgodnie z Create a 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!("Enabled Confidential Balances on token account {token_account}: {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(
`Enabled Confidential Balances on token account ${tokenAccount}: ${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ę