トークンの出金

機密利用可能残高からトークンを引き出す方法

機密利用可能残高からパブリック残高へトークンを引き出すには:

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

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

    範囲証明 (BatchedRangeProofU64Data): 引き出し後の残りの利用可能残高が非負であり、指定された範囲内であることを検証します。

  2. 各証明について:

    • ZK ElGamal 証明プログラムを呼び出して証明データを検証します。
    • 他のinstructionsで使用するために、証明固有のメタデータを証明「コンテキスト状態」アカウントに保存します。
  3. 2つの証明アカウントを提供して ConfidentialTransferInstruction::Withdraw instructionを呼び出します。

  4. 証明アカウントを閉じて、それらの作成に使用したSOLを回収します。

次の図は、機密利用可能残高からパブリック残高へトークンを引き出す際の手順を示しています:

Withdraw Tokens

必要なinstructions

機密利用可能残高からパブリック残高へトークンを引き出すには、以下を実行する必要があります:

  • クライアント側で等価性証明と範囲証明を生成する
  • ZK ElGamal 証明プログラムを呼び出して証明を検証し、「コンテキスト状態」アカウントを初期化する
  • 2つの証明アカウントを提供して ConfidentialTransferInstruction::Withdraw instructionを呼び出す
  • rentを回収するために2つの証明アカウントを閉じる

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

コード例

以下の例では、機密の利用可能残高からトークンをパブリック残高に引き出します。アカウントはあらかじめ機密転送用に設定されており、利用可能な機密残高を保持している必要があります。

機密転送はZK ElGamal Proofプログラムによってサポートされており、メインネットおよびdevnetで有効化されています。標準的なsolana-test-validatorではこれが有効化されていませんが、Surfpoolのようなメインネットフォーキング対応のローカルvalidatorでは有効化されています。資金を持つ支払者を使用してそのいずれかに対してサンプルを実行し(コードはdevnetを使用)、プレースホルダーをご自身のミントおよび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?

目次

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