トークンアカウントの作成

Confidential Transfer拡張機能を使用したtoken accountの作成方法

Confidential Transfer拡張機能は、token accountに追加のステートを加えることで、プライベートなトークン転送を可能にします。このセクションでは、この拡張機能を有効にしたtoken accountの作成方法について説明します。

以下の図は、Confidential Transfer拡張機能を使用してtoken accountを作成する際の手順を示しています:

Create Token Account with Confidential Transfer Extension

Confidential TransferのToken Accountステート

この拡張機能は、token accountに ConfidentialTransferAccount ステートを追加します:

Confidential Token Account State
#[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 encryption
pub 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 balance
pub decryptable_available_balance: DecryptableBalance,
/// If `false`, the extended account rejects any incoming confidential
/// transfers
pub allow_confidential_credits: PodBool,
/// If `false`, the base account rejects any incoming transfers
pub 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 executed
pub maximum_pending_balance_credit_counter: PodU64,
/// The `expected_pending_balance_credit_counter` value that was included in
/// the last `ApplyPendingBalance` instruction
pub expected_pending_balance_credit_counter: PodU64,
/// The actual `pending_balance_credit_counter` when the last
/// `ApplyPendingBalance` instruction was executed
pub actual_pending_balance_credit_counter: PodU64,
}

ConfidentialTransferAccount には、コンフィデンシャル転送を管理するためのいくつかのフィールドが含まれています:

  • 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:デポジットおよび転送instructionsからの受信ペンディング残高クレジットをカウントします。

  • maximum_pending_balance_credit_counter:ペンディング残高を利用可能残高に変換するために ApplyPendingBalance instructionが必要になるまでの、ペンディングクレジットのカウント上限。

  • expected_pending_balance_credit_counterApplyPendingBalance instructionが最後に処理された際に、instruction dataを通じてクライアントから提供された pending_balance_credit_counter の値。

  • actual_pending_balance_credit_counter: token accountの pending_balance_credit_counter 値は、最後に ApplyPendingBalance instructions が処理された時点のものです。

保留中の残高と利用可能な残高

機密残高は、DoS攻撃を防ぐために保留中の残高と利用可能な残高に分離されています。この分離がなければ、攻撃者がtoken accountにトークンを繰り返し送信し、token accountオーナーのトークン送金機能を妨害することができます。トランザクションが送信された時点と処理された時点の間で暗号化された残高が変化するため、token accountオーナーはトークンを送金できなくなり、トランザクションが失敗します。

すべての入金額および送金額は、最初に保留中の残高に追加されます。token accountオーナーは ApplyPendingBalance instructions を使用して、保留中の残高を利用可能な残高に変換する必要があります。受信した送金や入金は、token accountの利用可能な残高に影響を与えません。

保留中の残高の上位/下位分割

機密保留残高は、ElGamalの復号化が大きな数値に対してより多くの計算を必要とするため、pending_balance_lopending_balance_hi に分割されています。暗号文の演算実装はこちらで確認でき、ApplyPendingBalance instructions での使用箇所はこちらです。

保留中の残高クレジットカウンター

保留中の残高を利用可能な残高に変換するために ApplyPendingBalance instructions を呼び出す際:

  1. クライアントは現在の保留中の残高と利用可能な残高を参照し、その合計を暗号化して、token accountオーナーのAESキーを使用して暗号化された decryptable_available_balance を提供します。

  2. 期待値と実際の保留クレジットカウンターは、ApplyPendingBalance instructions が作成されてから処理されるまでの間のカウンター値の変化を追跡します:

    • expected_pending_balance_credit_counter: クライアントが ApplyPendingBalance instructions を作成した時点での pending_balance_credit_counter
    • actual_pending_balance_credit_counter: ApplyPendingBalance instructions が処理された時点でのtoken accountの pending_balance_credit_counter

予想カウンターと実際のカウンターが一致している場合、decryptable_available_balanceavailable_balanceと一致していることを示します。

token accountの状態を取得してdecryptable_available_balanceを読み取る際、予想カウンターと実際のカウンターの値が異なる場合、クライアントはカウンターの差分に一致する最近の入金/送金instructionsを検索し、正確な残高を計算する必要があります。

残高照合プロセス

予想カウンターと実際の保留中残高カウンターが異なる場合は、以下の手順でdecryptable_available_balanceを照合してください:

  1. token accountからdecryptable_available_balanceを取得する
  2. カウンターの差分(実際 - 予想)までの入金および送金instructionsを含む最新のトランザクションを取得する:
    • 入金instructionsから公開金額を加算する
    • 送金instructionsから宛先の暗号文金額を復号して加算する

必要なinstructions

機密転送用のtoken accountの作成と設定には以下のinstructionsを使用し、これらはすべて1つのトランザクションに収まります:

  1. token accountの作成: Associated Token ProgramのAssociatedTokenAccountInstruction::Create instructionを呼び出して、決定論的アドレスにtoken accountを作成します。

  2. アカウントスペースの再割り当て: Token Extensions ProgramのTokenInstruction::Reallocate instructionを呼び出して、*rsConfidentialTransferAccount*状態のためのスペースを追加します。

  3. pubkeyの有効性証明の検証: ZK ElGamal Proofプログラムが所有するアカウントを作成し、そのVerifyPubkeyValidity instructionを呼び出して証明を検証し、検証済みの結果をそのコンテキスト状態アカウントに保存します。

  4. 機密転送の設定: Token Extensions ProgramのConfidentialTransferInstruction::ConfigureAccount instructionを呼び出し、*rsProofLocation::ContextStateAccount*を通じて証明コンテキスト状態アカウントを参照して、*rsConfidentialTransferAccount*状態を初期化します。

トークンアカウントの所有者のみが機密転送用のトークンアカウントを設定できます

ConfigureAccount instructionには、token accountの所有者のみが生成できる暗号化キーと証明のクライアント側での生成が必要です。

pubkeyの有効性証明は、アカウントのElGamal公開鍵が有効であることを検証します。この証明は*rsbuild_pubkey_validity_proof_data*で生成され、ZK ElGamal Proofプログラムによってオンチェーンでコンテキスト状態アカウントに検証・保存され、*rsProofLocation::ContextStateAccountを通じてrsConfigureAccount*から参照されます。そのため、tokenのinstruction自体には証明のバイト列は含まれません。実装の詳細については以下を参照してください:

コード例

以下のコードは、associated token accountを作成し、既存のコンフィデンシャルミントに対してコンフィデンシャル転送を設定します。

コンフィデンシャル転送はZK ElGamal Proofプログラムに依存しており、メインネットとdevnetで有効になっています。標準的なsolana-test-validatorではこれが有効になっていませんが、Surfpoolのようなメインネットフォーキングのローカルvalidatorでは有効です。資金が入ったペイヤーを使用して、これらのいずれか(コードはdevnetを使用)に対してサンプルを実行し、ミントのプレースホルダーをCreate a 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を基盤としています。ownerclient@solana/kitのセットアップから取得されます。返されたinstructionsプランは@solana/kitのinstructionsプランサポートを使って送信され、プルーフが1つのトランザクションに収まりきらない場合は複数のトランザクションに分割されます。

Is this page helpful?

目次

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