토큰 전송

한 token account에서 다른 token account로 토큰을 기밀 전송하는 방법

한 token account에서 다른 token account로 토큰을 기밀 전송하려면, 송신자와 수신자 모두 ConfidentialTransferAccount 상태로 구성되고 기밀 전송이 승인된 token account를 가지고 있어야 합니다. 또한 송신자의 token account에는 전송 가능한 기밀 잔액이 있어야 합니다.

토큰을 기밀로 전송하려면:

  1. 클라이언트 측에서 세 가지 증명을 생성합니다:

    동등성 증명 (CiphertextCommitmentEqualityProofData): 전송 후 새로운 사용 가능 잔액 암호문이 해당하는 Pedersen 커밋먼트와 일치하는지 검증하여, 소스 계정의 새로운 사용 가능 잔액이 new_balance = current_balance - transfer_amount 로 올바르게 계산되었음을 보장합니다.

    암호문 유효성 증명 (BatchedGroupedCiphertext3HandlesValidityProofData): 전송 금액 암호문이 세 당사자(소스, 목적지, 감사자) 모두에 대해 올바르게 생성되었는지 검증하여, 전송 금액이 각 당사자의 공개 키로 올바르게 암호화되었음을 보장합니다.

    범위 증명 (BatchedRangeProofU128Data): 새로운 사용 가능 잔액과 전송 금액(하위/상위 비트로 분할)이 모두 음수가 아니며 지정된 범위 내에 있는지 검증합니다.

  2. 각 증명에 대해:

    • ZK ElGamal 증명 프로그램을 호출하여 증명 데이터를 검증합니다.
    • 다른 명령어에서 사용할 수 있도록 증명별 메타데이터를 증명 "컨텍스트 상태" 계정에 저장합니다.
  3. 증명 컨텍스트 상태 계정을 제공하여 ConfidentialTransferInstruction::Transfer 명령어를 호출합니다.

  4. 증명 컨텍스트 상태 계정을 닫아 생성 시 사용된 SOL을 회수합니다.

다음 다이어그램은 송신자의 token account에서 수신자의 token account로 토큰을 전송하는 과정을 보여줍니다.

Transfer Tokens

필요한 명령어

한 token account에서 다른 token account로 토큰을 기밀 전송하려면 다음을 수행해야 합니다:

  • 클라이언트 측에서 동등성 증명, 암호문 유효성 증명, 범위 증명 생성
  • ZK ElGamal 증명 프로그램을 호출하여 증명을 검증하고 "컨텍스트 상태" 계정 초기화
  • 세 개의 증명 계정을 제공하여 ConfidentialTransferInstruction::Transfer 명령어를 호출합니다.
  • 세 개의 증명 계정을 닫아 rent를 회수합니다.

아래 Rust 예제는 spl-token-confidential-transfer-proof-generation 크레이트로 증명을 생성하고, ZK ElGamal Proof 프로그램을 통해 각 증명을 컨텍스트 상태 계정으로 검증하며, 전송 명령어에서 세 계정을 참조한 후 이를 닫습니다. TypeScript 예제는 @solana-program/token-2022/confidentialgetConfidentialTransferInstructionPlan 헬퍼를 사용하며, 동일한 증명 계정 구성, 전송, 닫기 작업을 멀티 트랜잭션 명령어 플랜으로 조합합니다.

예제 코드

다음 예제는 한 계정에서 다른 계정으로 토큰을 기밀 전송합니다. 두 계정 모두 기밀 전송이 구성되어 있어야 하며, 발신자는 사용 가능한 기밀 잔액을 보유해야 합니다.

기밀 전송은 ZK ElGamal Proof 프로그램에 의존하며, 이 프로그램은 메인넷과 데브넷에서 활성화되어 있습니다. 기본 solana-test-validator는 이를 활성화하지 않지만, Surfpool과 같은 메인넷 포킹 로컬 validator는 활성화합니다. 자금이 충전된 페이어로 해당 환경 중 하나(코드는 데브넷 사용)에서 예제를 실행하고, 플레이스홀더를 민트 주소와 발신자 및 수신자 계정으로 교체하세요.

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(),
);
// Sender = fee payer = token account owner. Both the sender and recipient
// accounts must already be configured for confidential transfers, and the
// sender must have an available confidential balance (deposit then apply
// pending balance beforehand).
let sender = load_keypair()?;
let amount: u64 = 100;
// Setup: create confidential accounts and fund the sender.
let recipient_keypair = Keypair::new();
let (mint, sender_token_account, recipient_token_account) =
setup_transfer_accounts(&rpc_client, &sender, &recipient_keypair, amount)?;
// Read the recipient's ElGamal public key from their confidential account.
let recipient_acc = rpc_client.get_account(&recipient_token_account)?;
let recipient_state = StateWithExtensions::<TokenAccount>::unpack(&recipient_acc.data)?;
let recipient_elgamal_pubkey: ElGamalPubkey = recipient_state
.get_extension::<ConfidentialTransferAccount>()?
.elgamal_pubkey
.try_into()
.map_err(|e| anyhow::anyhow!("recipient ElGamal pubkey: {e:?}"))?;
// Read the optional auditor ElGamal public key from the mint.
let mint_acc = rpc_client.get_account(&mint)?;
let mint_state = StateWithExtensions::<Mint>::unpack(&mint_acc.data)?;
let mint_ext = mint_state.get_extension::<ConfidentialTransferMint>()?;
let auditor_elgamal_pubkey: Option<ElGamalPubkey> =
Option::<PodElGamalPubkey>::from(mint_ext.auditor_elgamal_pubkey)
.map(|pod| {
ElGamalPubkey::try_from(pod).map_err(|e| anyhow::anyhow!("auditor pubkey: {e:?}"))
})
.transpose()?;
// Derive the sender's keys and read their current confidential balance.
let (sender_elgamal, sender_aes) =
derive_confidential_keys(&sender, &sender_token_account.to_bytes())
.map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;
let sender_acc = rpc_client.get_account(&sender_token_account)?;
let sender_state = StateWithExtensions::<TokenAccount>::unpack(&sender_acc.data)?;
let sender_ext = sender_state.get_extension::<ConfidentialTransferAccount>()?;
let current_available: ElGamalCiphertext = sender_ext
.available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("available balance: {e:?}"))?;
let current_decryptable: AeCiphertext = sender_ext
.decryptable_available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("decryptable balance: {e:?}"))?;
// Generate the three transfer proofs (equality, ciphertext-validity, range).
let proof_data = transfer_split_proof_data(
&current_available,
&current_decryptable,
amount,
&sender_elgamal,
&sender_aes,
&recipient_elgamal_pubkey,
auditor_elgamal_pubkey.as_ref(),
)
.map_err(|e| anyhow::anyhow!("transfer_split_proof_data: {e}"))?;
// Create one context state account per proof, owned by the ZK program.
let equality_account = Keypair::new();
let validity_account = Keypair::new();
let range_account = Keypair::new();
let equality_size = size_of::<ProofContextState<CiphertextCommitmentEqualityProofContext>>();
let validity_size =
size_of::<ProofContextState<BatchedGroupedCiphertext3HandlesValidityProofContext>>();
let range_size = size_of::<ProofContextState<BatchedRangeProofContext>>();
let create = |account: &Keypair, space: usize| -> Result<Instruction> {
Ok(system_instruction::create_account(
&sender.pubkey(),
&account.pubkey(),
rpc_client.get_minimum_balance_for_rent_exemption(space)?,
space as u64,
&ZK_PROOF_PROGRAM_ID,
))
};
let equality_create_ix = create(&equality_account, equality_size)?;
let validity_create_ix = create(&validity_account, validity_size)?;
let range_create_ix = create(&range_account, range_size)?;
// The sender is the context-state authority for all three proof accounts.
let authority: Address = sender.pubkey().to_bytes().into();
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,
);
let validity_verify_ix = ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity
.encode_verify_proof(
Some(ContextStateInfo {
context_state_account: &Address::from(validity_account.pubkey().to_bytes()),
context_state_authority: &authority,
}),
&proof_data
.ciphertext_validity_proof_data_with_ciphertext
.proof_data,
);
let range_verify_ix = ProofInstruction::VerifyBatchedRangeProofU128.encode_verify_proof(
Some(ContextStateInfo {
context_state_account: &Address::from(range_account.pubkey().to_bytes()),
context_state_authority: &authority,
}),
&proof_data.range_proof_data,
);
// Transaction 1: create all three accounts and verify the validity proof.
send_tx(
&rpc_client,
&[
equality_create_ix,
validity_create_ix,
range_create_ix,
validity_verify_ix,
],
&[&sender, &equality_account, &validity_account, &range_account],
)?;
// Transaction 2: verify the range proof (the largest, on its own).
send_tx(&rpc_client, &[range_verify_ix], &[&sender])?;
// Compute the sender's new decryptable available balance after the transfer.
let current_plaintext = current_decryptable
.decrypt(&sender_aes)
.context("decrypt available balance")?;
let new_plaintext = current_plaintext
.checked_sub(amount)
.context("insufficient available balance")?;
let new_decryptable: PodAeCiphertext = sender_aes.encrypt(new_plaintext).into();
let auditor_lo = proof_data
.ciphertext_validity_proof_data_with_ciphertext
.ciphertext_lo;
let auditor_hi = proof_data
.ciphertext_validity_proof_data_with_ciphertext
.ciphertext_hi;
let transfer_ix = inner_transfer(
&spl_token_2022::id(),
&sender_token_account,
&mint,
&recipient_token_account,
&new_decryptable,
&auditor_lo,
&auditor_hi,
&sender.pubkey(),
&[],
ProofLocation::ContextStateAccount(&equality_account.pubkey()),
ProofLocation::ContextStateAccount(&validity_account.pubkey()),
ProofLocation::ContextStateAccount(&range_account.pubkey()),
)?;
// Transaction 3: verify the equality proof, run the transfer, and close the
// three proof accounts to reclaim their rent.
let close = |account: &Keypair| {
close_context_state(
ContextStateInfo {
context_state_account: &Address::from(account.pubkey().to_bytes()),
context_state_authority: &authority,
},
&authority,
)
};
let instructions = [
equality_verify_ix,
transfer_ix,
close(&equality_account),
close(&validity_account),
close(&range_account),
];
let blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&sender.pubkey()),
&[&sender],
blockhash,
);
let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
println!("Transferred {amount} tokens confidentially: {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
})
)
// Temporary custom plugin to skip the default compute-budget estimate
// so proof instructions fit within the transaction message cap.
// The Solana CLI default keypair, used as fee payer, mint authority, and sender.
const owner = client.payer;
const recipient = await generateKeyPairSigner();
const depositAmount = 100n;
const amount = 25n;
const decimals = 2;
// Setup: create source and destination confidential accounts, then fund source.
const mint = await createConfidentialMint(client, owner, decimals);
const auditorElgamalPubkey = await getAuditorElgamalPubkey(client, mint);
const sourceToken = await createConfidentialTokenAccount(client, owner, mint);
const destinationToken = await createConfidentialTokenAccount(
client,
recipient,
mint
);
await mintPublicTokens(client, owner, mint, sourceToken, depositAmount);
await depositTokens(client, owner, mint, sourceToken, depositAmount, decimals);
await applyPendingBalance(client, owner, mint, sourceToken);
// Derive the sender's recoverable ElGamal and AES keys, bound to (owner, mint).
const { elgamalKeypair: sourceElgamalKeypair, aesKey } =
await deriveConfidentialKeys(owner, mint);
// The helper reads the recipient key from the destination account; pass the
// configured auditor key so the proof matches the mint configuration.
const sourceTokenAccount = (await fetchToken(client.rpc, sourceToken)).data;
const destinationTokenAccount = (await fetchToken(client.rpc, destinationToken))
.data;
// Builds the proof context-state accounts, the transfer, and the closes as a
// multi-transaction plan (the three proofs are too large for one transaction).
const plan = await getConfidentialTransferInstructionPlan({
rpc: client.rpc,
payer: owner,
authority: owner,
mint,
sourceToken,
sourceTokenAccount,
destinationToken,
destinationTokenAccount,
auditorElgamalPubkey,
amount,
sourceElgamalKeypair,
aesKey
});
const result = await client.sendTransactions(plan);
const summary = summarizeTransactionPlanResult(result);
const signature =
summary.successfulTransactions[summary.successfulTransactions.length - 1]
.context.signature;
console.log(`Transferred ${amount} tokens confidentially: ${signature}`);

Is this page helpful?

목차

페이지 편집