Confidential Transfer 확장을 사용하여 token account를 생성하는 방법
Confidential Transfer 확장은 token account에 추가 상태를 더함으로써 프라이빗 토큰 전송을 가능하게 합니다. 이 섹션에서는 이 확장이 활성화된 token account를 생성하는 방법을 설명합니다.
다음 다이어그램은 Confidential Transfer 확장을 사용하여 token account를 생성하는 데 관련된 단계를 보여줍니다:
Confidential Transfer Token Account 상태
이 확장은 token account에 ConfidentialTransferAccount 상태를 추가합니다:
#[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 encryptionpub 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 balancepub decryptable_available_balance: DecryptableBalance,/// If `false`, the extended account rejects any incoming confidential/// transferspub allow_confidential_credits: PodBool,/// If `false`, the base account rejects any incoming transferspub 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 executedpub maximum_pending_balance_credit_counter: PodU64,/// The `expected_pending_balance_credit_counter` value that was included in/// the last `ApplyPendingBalance` instructionpub expected_pending_balance_credit_counter: PodU64,/// The actual `pending_balance_credit_counter` when the last/// `ApplyPendingBalance` instruction was executedpub actual_pending_balance_credit_counter: PodU64,}
*rsConfidentialTransferAccount*에는 기밀 전송을 관리하기 위한 여러 필드가
포함되어 있습니다:
-
approved: 기밀 전송에 대한 계정의 승인 상태입니다. mint account의
auto_approve_new_accounts설정이true로 지정된 경우, 모든 token account는 기밀 전송에 대해 자동으로 승인됩니다. -
elgamal_pubkey: 잔액 및 전송 금액을 암호화하는 데 사용되는 ElGamal 공개 키입니다.
-
pending_balance_lo: 대기 중인 잔액의 암호화된 하위 16비트입니다. 잔액은 효율적인 복호화를 위해 상위 및 하위 부분으로 분할됩니다.
-
pending_balance_hi: 대기 중인 잔액의 암호화된 상위 48비트입니다. 잔액은 효율적인 복호화를 위해 상위 및 하위 부분으로 분할됩니다.
-
available_balance: 전송에 사용 가능한 암호화된 잔액입니다.
-
decryptable_available_balance: 계정 소유자의 효율적인 복호화를 위해 AES(Advanced Encryption Standard) 키로 암호화된 사용 가능한 잔액입니다.
-
allow_confidential_credits: true인 경우, 수신 기밀 전송을 허용합니다.
-
allow_non_confidential_credits: true인 경우, 수신 비기밀 전송을 허용합니다.
-
pending_balance_credit_counter: 입금 및 전송 명령어로부터 수신되는 대기 중인 잔액 크레딧을 계산합니다.
-
maximum_pending_balance_credit_counter: 대기 중인 잔액을 사용 가능한 잔액으로 전환하기 위해
ApplyPendingBalance명령어가 필요하기 전까지의 대기 크레딧 수 한도입니다. -
expected_pending_balance_credit_counter: 마지막으로
ApplyPendingBalance명령어가 처리되었을 때 클라이언트가 instruction data를 통해 제공한pending_balance_credit_counter값입니다. -
actual_pending_balance_credit_counter: 마지막
ApplyPendingBalance명령어가 처리된 시점의 token account에 있는pending_balance_credit_counter값입니다.
대기 잔액 vs 사용 가능 잔액
기밀 잔액은 DoS 공격을 방지하기 위해 대기 잔액과 사용 가능 잔액으로 구분됩니다. 이러한 구분이 없다면, 공격자가 token account에 토큰을 반복적으로 전송하여 token account 소유자의 토큰 전송 기능을 차단할 수 있습니다. 트랜잭션이 제출된 시점과 처리된 시점 사이에 암호화된 잔액이 변경되어 트랜잭션이 실패하기 때문에, token account 소유자는 토큰을 전송할 수 없게 됩니다.
모든 입금 및 전송 금액은 처음에 대기 잔액에 추가됩니다. Token account 소유자는
ApplyPendingBalance 명령어를 사용하여 대기 잔액을 사용 가능 잔액으로
전환해야 합니다. 수신 전송이나 입금은 token account의 사용 가능 잔액에 영향을
주지 않습니다.
대기 잔액 크레딧 카운터
대기 잔액을 사용 가능 잔액으로 전환하기 위해 ApplyPendingBalance 명령어를
호출할 때:
-
클라이언트는 현재 대기 잔액 및 사용 가능 잔액을 조회하고, 그 합계를 암호화하여 token account 소유자의 AES 키로 암호화된
decryptable_available_balance를 제공합니다. -
예상 및 실제 대기 크레딧 카운터는
ApplyPendingBalance명령어가 생성된 시점과 처리된 시점 사이의 카운터 값 변화를 추적합니다:expected_pending_balance_credit_counter: 클라이언트가ApplyPendingBalance명령어를 생성할 때의pending_balance_credit_counter값actual_pending_balance_credit_counter:ApplyPendingBalance명령어가 처리된 시점의 token account에 있는pending_balance_credit_counter값
예상/실제 카운터가 일치하면 decryptable_available_balance가
available_balance와 일치함을 나타냅니다.
token account의 상태를 가져와 decryptable_available_balance를 읽을 때,
예상/실제 카운터 값이 다를 경우 클라이언트는 카운터 차이에 해당하는 최근
입금/전송 명령어를 조회하여 정확한 잔액을 계산해야 합니다.
잔액 조정 프로세스
예상 및 실제 대기 중인 잔액 카운터가 다를 경우, 다음 단계에 따라
decryptable_available_balance를 조정하십시오:
- token account에서
decryptable_available_balance를 가져오는 것부터 시작합니다 - 카운터 차이(실제 - 예상)만큼 입금 및 전송 명령어를 포함한 가장 최근
트랜잭션을 가져옵니다:
- 입금 명령어에서 공개 금액을 더합니다
- 전송 명령어에서 대상 암호문 금액을 복호화하여 더합니다
필수 명령어
기밀 전송을 위한 token account 생성 및 구성에는 다음 명령어들이 사용되며, 이 모두는 단일 트랜잭션 내에 포함됩니다:
-
Token Account 생성: Associated Token Program의
AssociatedTokenAccountInstruction::Create명령어를 호출하여 결정론적 주소에 token account를 생성합니다. -
계정 공간 재할당: Token Extension Program의
TokenInstruction::Reallocate명령어를 호출하여ConfidentialTransferAccount상태를 위한 공간을 추가합니다. -
Pubkey 유효성 증명 검증: ZK ElGamal Proof 프로그램이 소유한 계정을 생성한 후,
VerifyPubkeyValidity명령어를 호출하여 증명을 검증하고 해당 컨텍스트 상태 계정에 검증된 결과를 저장합니다. -
기밀 전송 구성: Token Extension Program의 ConfidentialTransferInstruction::ConfigureAccount 명령어를 호출하고, *rs
ProofLocation::ContextStateAccount*를 통해 증명 컨텍스트 상태 계정을 참조하여ConfidentialTransferAccount상태를 초기화합니다.
토큰 계정 소유자만 기밀 전송을 위한 토큰 계정을 구성할 수 있습니다.
ConfigureAccount 명령어는 token account 소유자만 생성할 수 있는 암호화
키와 증명을 클라이언트 측에서 생성해야 합니다.
pubkey 유효성 증명은 계정의 ElGamal 공개 키가 유효함을 검증합니다. 이 증명은
*rsbuild_pubkey_validity_proof_data*로 생성되고, ZK ElGamal Proof 프로그램에
의해 온체인에서 컨텍스트 상태 계정으로 검증되며, 이후
*rsProofLocation::ContextStateAccount*를 통해 *rsConfigureAccount*에서
참조됩니다. 따라서 토큰 명령어 자체에는 증명 바이트가 포함되지 않습니다. 구현
세부 사항은 다음을 참조하십시오:
예제 코드
다음 코드는 associated token account을 생성하고 기존의 기밀 민트에 대해 기밀 전송을 위한 설정을 구성합니다.
기밀 전송은 메인넷과 데브넷에서 활성화된 ZK ElGamal Proof 프로그램에 의존합니다.
일반 solana-test-validator는 이를 활성화하지 않지만,
Surfpool과 같은 메인넷 포킹 로컬 validator는
활성화합니다. 해당 환경 중 하나(코드는 데브넷을 사용)에서 자금이 충전된 payer로
예제를 실행하고,
민트 생성에 따라
생성된 민트로 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}`);
TypeScript 헬퍼는 @solana-program/token-2022/confidential 서브패스에
위치하며, 암호화 기본 요소를 위해 @solana/zk-sdk를 기반으로 합니다. owner
및 client는 @solana/kit 설정에서 제공되며, 반환된 명령 플랜은
@solana/kit의 명령 플랜 지원을 통해 전송됩니다. 이는 증명이 하나의
트랜잭션에 담기에 너무 클 경우 여러 트랜잭션으로 작업을 분산합니다.
Is this page helpful?