トークンの転送

あるtoken accountから別のtoken accountへトークンを秘密裏に転送する方法

あるtoken accountから別のtoken accountへトークンを秘密裏に転送するには、送信者と受信者の両方が ConfidentialTransferAccount 状態に設定され、秘密転送が承認されたtoken accountを持っている必要があります。また、送信者のtoken accountには転送可能な秘密残高が存在している必要があります。

トークンを秘密裏に転送するには:

  1. クライアント側で 3つの証明 を作成します:

    等価証明 (CiphertextCommitmentEqualityProofData):転送後の新しい利用可能残高の暗号文が、対応する Pedersen commitment と一致することを検証し、送信元アカウントの新しい利用可能残高が new_balance = current_balance - transfer_amount として正しく計算されていることを保証します。

    暗号文有効性証明 (BatchedGroupedCiphertext3HandlesValidityProofData):転送金額の暗号文が3者(送信元、送信先、および監査者)すべてに対して適切に生成されていることを検証し、各当事者の公開鍵のもとで転送金額が正しく暗号化されていることを保証します。

    範囲証明 (BatchedRangeProofU128Data):新しい利用可能残高と転送金額(低ビット/高ビットに分割)がすべて非負かつ指定された範囲内であることを検証します。

  2. 各証明について:

    • ZK ElGamal proof プログラムを呼び出して証明データを検証します。
    • 証明固有のメタデータを証明「コンテキスト状態」アカウントに保存し、他のinstructionsで使用できるようにします。
  3. 証明コンテキスト状態アカウントを指定して ConfidentialTransferInstruction::Transfer instructionを呼び出します。

  4. 証明コンテキスト状態アカウントをクローズして、作成に使用したSOLを回収します。

以下の図は、送信者のtoken accountから受信者のtoken accountへトークンを転送する際の手順を示しています。

Transfer Tokens

必要なinstructions

あるtoken accountから別のtoken accountへトークンを秘密裏に転送するには、以下を実行する必要があります:

  • クライアント側で等価証明、暗号文有効性証明、および範囲証明を生成する
  • Zk ElGamal proof プログラムを呼び出して証明を検証し、「コンテキスト状態」アカウントを初期化する
  • 3つの証明アカウントを指定して ConfidentialTransferInstruction::Transfer instructionを呼び出す
  • 3つの証明アカウントをクローズしてrentを回収する

以下のRustの例では、spl-token-confidential-transfer-proof-generationクレートを使用して証明を生成し、ZK ElGamal Proofプログラムを通じて各証明をコンテキスト状態アカウントに検証し、転送instructionsで3つのアカウントを参照した後、それらを閉じます。TypeScriptの例では、@solana-program/token-2022/confidentialgetConfidentialTransferInstructionPlanヘルパーを使用しており、同じ証明アカウントの組み立て、転送、およびマルチトランザクションinstructionsプランとしてのクローズを行います。

サンプルコード

以下の例では、あるアカウントから別のアカウントへトークンを機密転送します。両方のアカウントはすでに機密転送用に設定されている必要があり、送信者には利用可能な機密残高が必要です。

機密転送は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?

目次

ページを編集
© 2026 Solana Foundation. 無断転載を禁じます。