Chuyển token

Cách chuyển token bí mật từ token account này sang token account khác

Để chuyển token một cách bí mật từ token account này sang token account khác, cả người gửi và người nhận đều phải có token account được cấu hình với trạng thái ConfidentialTransferAccount và được phê duyệt cho chuyển khoản bí mật. Token account của người gửi cũng phải có số dư bí mật khả dụng để thực hiện chuyển khoản.

Để chuyển token một cách bí mật:

  1. Tạo ba bằng chứng phía client:

    Bằng chứng đẳng thức (CiphertextCommitmentEqualityProofData): Xác minh rằng bản mã số dư khả dụng mới sau khi chuyển khoản khớp với cam kết Pedersen tương ứng, đảm bảo số dư khả dụng mới của tài khoản nguồn được tính toán chính xác là new_balance = current_balance - transfer_amount.

    Bằng chứng hợp lệ bản mã (BatchedGroupedCiphertext3HandlesValidityProofData): Xác minh rằng các bản mã số tiền chuyển khoản được tạo đúng cách cho cả ba bên (nguồn, đích và kiểm toán viên), đảm bảo số tiền chuyển khoản được mã hóa chính xác bằng khóa công khai của từng bên.

    Bằng chứng phạm vi (BatchedRangeProofU128Data): Xác minh rằng số dư khả dụng mới và số tiền chuyển khoản (được chia thành các bit thấp/cao) đều không âm và nằm trong một phạm vi xác định.

  2. Đối với mỗi bằng chứng:

    • Gọi chương trình bằng chứng ZK ElGamal để xác minh dữ liệu bằng chứng.
    • Lưu trữ siêu dữ liệu đặc thù của bằng chứng trong tài khoản "context state" của bằng chứng để sử dụng trong các lệnh khác.
  3. Gọi lệnh ConfidentialTransferInstruction::Transfer, cung cấp các tài khoản context state của bằng chứng.

  4. Đóng các tài khoản context state của bằng chứng để thu hồi SOL đã dùng để tạo chúng.

Sơ đồ sau đây mô tả các bước liên quan đến việc chuyển token từ token account của người gửi sang token account của người nhận.

Transfer Tokens

Các lệnh bắt buộc

Để chuyển token một cách bí mật từ token account này sang token account khác, bạn phải:

  • Tạo bằng chứng đẳng thức, bằng chứng hợp lệ bản mã và bằng chứng phạm vi phía client
  • Gọi chương trình bằng chứng Zk ElGamal để xác minh các bằng chứng và khởi tạo các tài khoản "context state"
  • Gọi lệnh ConfidentialTransferInstruction::Transfer cung cấp ba tài khoản bằng chứng.
  • Đóng ba tài khoản bằng chứng để thu hồi rent.

Ví dụ Rust bên dưới tạo các bằng chứng bằng crate spl-token-confidential-transfer-proof-generation, xác minh từng bằng chứng vào tài khoản trạng thái context thông qua chương trình ZK ElGamal Proof, tham chiếu ba tài khoản trong lệnh chuyển, và đóng chúng sau đó. Ví dụ TypeScript sử dụng helper getConfidentialTransferInstructionPlan từ @solana-program/token-2022/confidential, giúp tập hợp các tài khoản bằng chứng, lệnh chuyển và đóng tài khoản tương tự thành một kế hoạch lệnh đa giao dịch.

Mã Ví Dụ

Ví dụ sau đây chuyển token một cách bảo mật từ tài khoản này sang tài khoản khác. Cả hai tài khoản phải được cấu hình sẵn cho chuyển token bảo mật, và người gửi phải có số dư bảo mật khả dụng.

Chuyển token bảo mật phụ thuộc vào chương trình ZK ElGamal Proof, vốn được kích hoạt trên mainnet và devnet. Một solana-test-validator thông thường không kích hoạt chương trình này, nhưng một validator cục bộ fork từ mainnet như Surfpool thì có. Hãy chạy ví dụ trên một trong số đó (mã sử dụng devnet) với tài khoản payer đã được nạp tiền, và thay thế các placeholder bằng mint của bạn cùng các tài khoản người gửi và người nhận.

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?

Mục lục

Chỉnh sửa trang