기밀 사용 가능 잔액에서 토큰을 출금하는 방법
기밀 사용 가능 잔액에서 공개 잔액으로 토큰을 출금하려면:
-
클라이언트 측에서 두 가지 증명을 생성합니다:
동등성 증명 (CiphertextCommitmentEqualityProofData): 출금 후 남은 사용 가능 잔액 암호문이 해당 페더슨 커밋먼트와 일치하는지 검증하여, 계정의 새로운 사용 가능 잔액이
remaining_balance = current_balance - withdraw_amount로 올바르게 계산되었음을 보장합니다.범위 증명 (BatchedRangeProofU64Data): 출금 후 남은 사용 가능 잔액이 음수가 아니며 지정된 범위 내에 있는지 검증합니다.
-
각 증명에 대해:
- ZK ElGamal 증명 프로그램을 호출하여 증명 데이터를 검증합니다.
- 증명별 메타데이터를 증명 "컨텍스트 상태" 계정에 저장하여 다른 명령어에서 사용합니다.
-
두 증명 계정을 제공하며 ConfidentialTransferInstruction::Withdraw 명령어를 호출합니다.
-
증명 계정을 닫아 생성 시 사용한 SOL을 회수합니다.
다음 다이어그램은 기밀 사용 가능 잔액에서 공개 잔액으로 토큰을 출금하는 단계를 보여줍니다:
필요한 명령어
기밀 사용 가능 잔액에서 공개 잔액으로 토큰을 출금하려면 다음을 수행해야 합니다:
- 클라이언트 측에서 동등성 증명 및 범위 증명 생성
- ZK ElGamal 증명 프로그램을 호출하여 증명을 검증하고 "컨텍스트 상태" 계정 초기화
- 두 증명 계정을 제공하며 ConfidentialTransferInstruction::Withdraw 명령어 호출
- 두 증명 계정을 닫아 rent 회수
아래 Rust 예제는 spl-token-confidential-transfer-proof-generation 크레이트로
증명을 생성하고, ZK ElGamal 증명 프로그램을 통해 각 증명을 컨텍스트 상태
계정으로 검증하며, 출금 명령어에서 두 계정을 참조한 후 이를 닫습니다. TypeScript
예제는 @solana-program/token-2022/confidential의
getConfidentialWithdrawInstructionPlan 헬퍼를 사용하며, 이는 동일한 증명 계정
생성, 출금 및 닫기를 멀티 트랜잭션 명령어 계획으로 조합합니다.
예제 코드
다음 예제는 기밀 가용 잔액에서 토큰을 공개 잔액으로 출금합니다. 계정은 이미 기밀 전송이 구성되어 있어야 하며 사용 가능한 기밀 잔액을 보유하고 있어야 합니다.
기밀 전송은 ZK ElGamal Proof 프로그램에 의존하며, 이는 메인넷과 데브넷에서
활성화되어 있습니다. 기본 solana-test-validator는 이를 활성화하지 않지만,
Surfpool과 같은 메인넷 포킹 로컬 validator는
활성화합니다. 자금이 충전된 payer로 해당 환경 중 하나(코드는 데브넷 사용)에서
예제를 실행하고, 플레이스홀더를 본인의 mint와 token account로 교체하세요.
Rust
const ZK_PROOF_PROGRAM_ID: Pubkey =solana_pubkey::pubkey!("ZkE1Gama1Proof11111111111111111111111111111");fn main() -> Result<()> {let rpc_client = RpcClient::new_with_commitment(String::from("https://api.devnet.solana.com"),CommitmentConfig::confirmed(),);// Owner = fee payer = token account owner. The account must already be// configured for confidential transfers with an available confidential// balance to withdraw from.let owner = load_keypair()?;let amount: u64 = 100;let decimals: u8 = 2;// Setup: create an available confidential balance to withdraw.let (mint, token_account) = setup_withdrawable_account(&rpc_client, &owner, amount, decimals)?;// Derive the owner's keys and read the current confidential available balance.let (elgamal_keypair, aes_key) = derive_confidential_keys(&owner, &token_account.to_bytes()).map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;let account_data = rpc_client.get_account(&token_account)?;let account = StateWithExtensions::<TokenAccount>::unpack(&account_data.data)?;let ct_extension = account.get_extension::<ConfidentialTransferAccount>()?;// The ElGamal available-balance ciphertext is required to build the proof.let available_balance: ElGamalCiphertext = ct_extension.available_balance.try_into().map_err(|e| anyhow::anyhow!("decode available balance: {e:?}"))?;// Read the plaintext balance from the AES-encrypted decryptable balance.// ElGamal's decrypt_u32 only recovers values up to 2^32 raw units, so it// fails for realistic balances; the AES field has no such limit.let decryptable_balance: AeCiphertext = ct_extension.decryptable_available_balance.try_into().map_err(|e| anyhow::anyhow!("decode decryptable balance: {e:?}"))?;let current_available = decryptable_balance.decrypt(&aes_key).context("decrypt available balance")?;// Generate the equality and range proofs for the withdrawal.let proof_data = withdraw_proof_data(&available_balance, current_available, amount, &elgamal_keypair).map_err(|e| anyhow::anyhow!("withdraw_proof_data: {e}"))?;let new_available = current_available.checked_sub(amount).context("insufficient confidential balance")?;let new_decryptable: PodAeCiphertext = aes_key.encrypt(new_available).into();// The owner is the context-state authority for both proof accounts.let authority: Address = owner.pubkey().to_bytes().into();// Equality proof context state account.let equality_account = Keypair::new();let equality_size = size_of::<ProofContextState<CiphertextCommitmentEqualityProofContext>>();let equality_create_ix = system_instruction::create_account(&owner.pubkey(),&equality_account.pubkey(),rpc_client.get_minimum_balance_for_rent_exemption(equality_size)?,equality_size as u64,&ZK_PROOF_PROGRAM_ID,);let equality_verify_ix = ProofInstruction::VerifyCiphertextCommitmentEquality.encode_verify_proof(Some(ContextStateInfo {context_state_account: &Address::from(equality_account.pubkey().to_bytes()),context_state_authority: &authority,}),&proof_data.equality_proof_data,);send_tx(&rpc_client, &[equality_create_ix], &[&owner, &equality_account])?;send_tx(&rpc_client, &[equality_verify_ix], &[&owner])?;// Range proof context state account.let range_account = Keypair::new();let range_size = size_of::<ProofContextState<BatchedRangeProofContext>>();let range_create_ix = system_instruction::create_account(&owner.pubkey(),&range_account.pubkey(),rpc_client.get_minimum_balance_for_rent_exemption(range_size)?,range_size as u64,&ZK_PROOF_PROGRAM_ID,);let range_verify_ix = ProofInstruction::VerifyBatchedRangeProofU64.encode_verify_proof(Some(ContextStateInfo {context_state_account: &Address::from(range_account.pubkey().to_bytes()),context_state_authority: &authority,}),&proof_data.range_proof_data,);send_tx(&rpc_client, &[range_create_ix], &[&owner, &range_account])?;send_tx(&rpc_client, &[range_verify_ix], &[&owner])?;// Withdraw, referencing both pre-verified proof accounts.let withdraw_ixs = withdraw(&spl_token_2022::id(),&token_account,&mint,amount,decimals,&new_decryptable,&owner.pubkey(),&[&owner.pubkey()],ProofLocation::ContextStateAccount(&equality_account.pubkey()),ProofLocation::ContextStateAccount(&range_account.pubkey()),)?;let blockhash = rpc_client.get_latest_blockhash()?;let transaction =Transaction::new_signed_with_payer(&withdraw_ixs, Some(&owner.pubkey()), &[&owner], blockhash);let signature = rpc_client.send_and_confirm_transaction(&transaction)?;// Close both proof accounts to reclaim their rent.for account in [&equality_account, &range_account] {let close_ix = close_context_state(ContextStateInfo {context_state_account: &Address::from(account.pubkey().to_bytes()),context_state_authority: &authority,},&authority,);send_tx(&rpc_client, &[close_ix], &[&owner])?;}println!("Withdrew {amount} tokens to the public balance: {signature}");Ok(())}
Typescript
const client = await createClient().use(signerFromFile(join(homedir(), ".config/solana/id.json"))).use(solanaRpc({rpcUrl: "https://api.devnet.solana.com",maxConcurrency: 1}));// The Solana CLI default keypair, used as fee payer, mint authority, and// token account owner.const owner = client.payer;const depositAmount = 100n;const amount = 25n;const decimals = 2;// Setup: create a confidential account with an available confidential balance.const mint = await createConfidentialMint(client, owner, decimals);const token = await createConfidentialTokenAccount(client, owner, mint);await mintPublicTokens(client, owner, mint, token, depositAmount);await depositTokens(client, owner, mint, token, depositAmount, decimals);await applyPendingBalance(client, owner, mint, token);// Derive the owner's recoverable ElGamal and AES keys, bound to (owner, mint).const { elgamalKeypair, aesKey } = await deriveConfidentialKeys(owner, mint);const tokenAccount = (await fetchToken(client.rpc, token)).data;// Builds the equality + range proof accounts, the withdraw, and the closes as a// multi-transaction instruction plan.const plan = await getConfidentialWithdrawInstructionPlan({rpc: client.rpc,payer: owner,authority: owner,token,mint,tokenAccount,amount,decimals,elgamalKeypair,aesKey});const result = await client.sendTransactions(plan);const summary = summarizeTransactionPlanResult(result);const signature =summary.successfulTransactions[summary.successfulTransactions.length - 1].context.signature;console.log(`Withdrew ${amount} tokens to the public balance: ${signature}`);
Is this page helpful?