新着:Parsed Events Scan APIをお試しください
ElusivとLight Protocolで実現するSolanaのプライバシー
ブログ/開発

ElusivとLightで実現するSolanaのプライバシー:SOLのプライバシーについて知っておくべきこと

Developer Experience EngineerXの0xIchigoLinkedInの0xIchigoGitHubの0xIchigo
読了時間:16分

前提条件

  • Node.jsがインストールされていること(必要な依存関係のインストール、スクリプトの実行、$SOLの非公開送信に使用します)
  • TypeScriptの基礎知識
  • @solana/web3.jsの基礎知識

この記事の内容

プライバシーは、現代のブロックチェーン技術の中核です。公開台帳システムでは、トランザクション、アカウント残高、そして多くの場合はプログラムのソースコードまで、誰でも閲覧できるよう公開されています。この前例のない透明性はブロックチェーン技術の大きな特徴として支持されていますが、多くのアプリケーションや日常利用では、プライバシーが不可欠な要件となる場合があります。

ElusivとLight Protocolはどちらも、zk-SNARKSを活用し、非公開トランザクション、非公開プログラム、分散型コンプライアンスソリューションを通じて、Solana上のプライバシーに最先端のソリューションを提供します。この記事では、プライバシーの重要性を掘り下げ、Solanaのようなブロックチェーンにプライバシー機能が必要な理由を解説します。さらに、ElusivのTypeScript SDKとLightのzk.jsモジュールを使用してSOLを非公開で送信する方法を、手順に沿って紹介します。

プライバシーは重要です

プライバシーは、自律性と人間の尊厳を守るために欠かせない基本的人権です。言論の自由や結社の自由など、ほかの権利を行使する基盤でもあるため、自由で開かれた社会にはプライバシーが必要です。これにより、政治生活から切り離された空間が生まれ、誰もが第三者に常時監視されることなく、考え、話し、意見を表明し、行動できるようになります。Edward Snowdenによる情報公開、WikiLeaksが暴露した情報、FacebookとCambridge Analyticaのデータスキャンダルといった近年の社会的論争は、プライバシーの重要性と境界をめぐる激しい議論を巻き起こしました。

誰もがプライバシーを重視すべきです

一見すると、透明性を基本原則の1つとして掲げる分散型技術上に構築された公開台帳に、プライバシーを実現する機能を持たせるべきだと主張するのは、矛盾しているように思えるかもしれません。熱心なサイファーパンクであっても一般ユーザーであっても、誰もがプライバシーを重視すべきです。オンラインで有害なことや違法なことをしていないのだから、自分がブロックチェーン上で何をしているか見られても気にする必要はないと思うかもしれません。次の思考実験を考えてみましょう。

ここに2人の登場人物がいます。1人はプライバシーを大切にし、詮索されずに買い物をしたい一般ユーザーのAliceです。もう1人は、以降MAと略す悪意のある行為者です。悪意のある行為者には、たとえば抑圧的な政府、従業員を支配しようとする雇用主、サイバー犯罪者などが該当します。Aliceが、政府によって全国民が常時監視される国に住んでいる、または特定の合法的な購入(政治献金や特定分野の書籍購入など)を厳しく禁じる企業に勤めているとします。ある日、AliceはSolanaのような従来型のブロックチェーンを使用し、安全で非公開の決済手段だと信じて買い物をします。その後、次のような事態が起こります。

  • Aliceのトランザクションが公開台帳に記録されます。トランザクションには、Aliceの公開鍵、受取人の公開鍵、送信額が含まれます
  • Aliceの行動を監視または制御しようとするMAが、Aliceの公開鍵を使ってトランザクションの追跡を始めます
  • MAはAliceの購入を発見し、自らの規則に照らして「好ましくない」と判断します
  • MAはAliceに対して措置を講じます。状況によっては、訴訟、投獄、職場での制裁、社会的な糾弾、恐喝などに発展する可能性があります

この思考実験は、ブロックチェーン上のプライバシー不足が一般ユーザーに与え得る厳しい現実を示しています。オンチェーン調査員や悪意のあるデータ収集技術が増えるにつれ、一般ユーザーのトランザクションを追跡することは極めて容易になりつつあり、現実世界で重大な結果を招く可能性があります。デジタル化が進む世界でプライバシーがなければ、個人の主権が損なわれ、すべての意思決定が監視され、存在のあらゆる側面が商品化されるおそれがあります。プライバシーを実現する技術を導入しなければ、ブロックチェーンはこの流れを加速させるだけです。そこで登場するのが、ElusivとLight Protocolです。

Elusivとは?

Elusivは自らを、「ユーザーとアプリケーションがユニバーサル暗号化を利用できるようにする」「ブロックチェーンベースのゼロ知識プライバシープロトコル」と説明しています。Elusivは、ユーザーとネットワークにゼロ知識暗号とマルチパーティ計算を適用することで、セキュリティ、安全性、分散性を犠牲にせずプライバシーを実現しようとしています。このユニバーサル暗号化という目標により、Elusivはユーザー自身がデータを管理できるツールを提供します。ユーザーは、ブロックチェーン上で何を共有し、何を共有しないかを選択できるようになります。

Elusivの仕組み

Elusivでは、ユーザーがElusivプログラムによって管理される共有プールへ資金を送り、非公開残高に入金します。ユーザーはプールからSOLなどのトークンを送受信したり、資金を引き出したりできます。この共有プールモデルでも、Elusivへの入金とElusivからの支払いは外部から確認できる点に注意が必要です。ただし、ユーザーとトランザクションが増えるほど、時間の経過とともに特定のアカウントと入出金を結び付けることは難しくなります。10 SOLを入金して10 SOLを引き出す1人のユーザーを関連付ける方が、何百万人もの中から少額の入出金を繰り返すユーザーを関連付けるより簡単です。匿名性には仲間が多いほど有利です。

プール内の資金には入金者に関する情報が含まれませんが、ゼロ知識証明を使用することで、秘密値がユーザーの資金に関連付けられ、未使用資金に対する権限を証明できます。Elusivで使用されるゼロ知識証明は、zk-SNARKS(Zero-Knowledge Succinct Non-Interactive Argument of Knowledge)と呼ばれます。zk-SNARK証明の複雑な詳細には踏み込まず、秘密のパスワードそのものを友人に一切教えず、自分がそれを知っていることを数学的に証明する方法だと考えてください。友人は追加の質問をしなくても、あなたが真実を話しているかをすぐに確認できます。このように、Elusivはプール内の残高をSolana全体に公開することなく、未使用資金を追跡できます。ゼロ知識証明について詳しく学びたい方は、Porterによるゼロ知識証明の講義をご覧ください。

Light Protocolとは?

Light Protocolは自らを、Solana上で非公開プログラムを直接実行できる「Solanaの次世代zkLayer」と説明しています。LightがSolana上でプライバシーを実現するうえで、基礎となる概念は3つあります。

  • 非公開状態
  • UTXOモデル
  • オンチェーンでの非公開状態遷移の検証

Light Protocolの仕組み

Light Protocolでは、オンチェーンの状態を暗号化できます。この状態はユーザーが「所有」し、復号する排他的な権利を持ちます。Elusivの資金プール方式とは異なり、Light ProtocolはSolanaにUTXO(Unspent Transaction Outputs)モデルを導入します。UTXOはシールドされたキーペアに紐付けられ、2種類の残高を保持できます。1つはSOL(Light Protocolで手数料トークンとして使用されます)、もう1つは別のSPLトークンです。この方式では、トランザクションごとに新しいUTXOを生成して線形リストへ追加するため、インプレース更新を回避できます。どの状態が誰によって更新されているかという情報を漏らさず、状態を秘匿できます。

Solana上のプログラムロジックは、ほぼ常に独自の制約を持つエスクローです。Light Protocolはこの設計を発展させ、クライアント側のゼロ知識回路(zk-SNARKS)にプログラムロジックをエンコードし、計算と証明を生成します。この証明はオンチェーンで共有され、その回路専用に作成された検証プログラムが有効性を確認します。検証に成功すると、カスタム検証Programが別のネイティブ検証プログラムを呼び出し、さまざまなセキュリティチェックを完了して、新しいコミットメントをLightのMerkle treeプログラムに追加します。

この記事の範囲外ではありますが、Lightでは独自のPrivate Solana Program(PSP)をゼロから作成できます。独自のPSPを作成する前に、この記事の残りを読み、ドキュメントを確認することをお勧めします。ドキュメントには、独自のカスタムPSPを作成するチュートリアルが含まれており、こちらから参照できます。

興味深い仕組みですが、Solanaでのプログラミングに直接応用するにはどうすればよいのでしょうか?

Elusivを使用してSOLを非公開で送信する

完全なコード

コード
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512";
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));

import {
  Connection,
  PublicKey,
  Keypair,
  ConfirmedSignatureInfo,
  Cluster,
  LAMPORTS_PER_SOL,
  clusterApiUrl,
} from "@solana/web3.js";
import { Elusiv, TokenType, SEED_MESSAGE } from "@elusiv/sdk";

const CLUSTER: Cluster = "devnet";
const RPC = "https://rpc-devnet.helius.xyz";

const main = async () => {
  const connection: Connection = new Connection(RPC);
  const keyPair: Keypair = generateKeypair();
  const recipientPublicKey: PublicKey = new PublicKey(generateKeypair().publicKey);

  await airdropSol(keyPair);

  const seed: Uint8Array = ed.sign(Buffer.from(SEED_MESSAGE, "utf-8"), keyPair.secretKey.slice(0, 32));

  const elusiv: Elusiv = await Elusiv.getElusivInstance(seed, keyPair.publicKey, connection, CLUSTER);

  const balance: bigint = await elusiv.getLatestPrivateBalance("LAMPORTS");

  try {
    if (balance > BigInt(0)) {
      const signature = await send(elusiv, recipientPublicKey, 1 * LAMPORTS_PER_SOL, "LAMPORTS");

      console.log(`Sent with signature ${signature.signature}`);
    } else {
      console.log("Private balance is empty. Topping up...");

      const amount = 1 * LAMPORTS_PER_SOL;
      const tokenType = "LAMPORTS";

      const topUpTxData = await elusiv.buildTopUpTx(amount, tokenType);

      topUpTxData.tx.partialSign(keyPair);

      const topUpSig: ConfirmedSignatureInfo = await elusiv.sendElusivTx(topUpTxData);
      console.log(`Top-up complete with signature ${topUpSig.signature}`);

      const signature: ConfirmedSignatureInfo = await send(elusiv, recipientPublicKey, 1 * LAMPORTS_PER_SOL, "LAMPORTS");

      console.log(`Sent with signature ${signature.signature}`);
    }
  } catch (e) {
    console.error(`Error sending SOL via Elusiv: ${e}`);
  } finally {
    console.log("Exiting program...");
    process.exit(0);
  }
};

const send = async (
  elusiv: Elusiv,
  recipient: PublicKey,
  amount: number,
  tokenType: TokenType
): Promise => {
  const txt = await elusiv.buildSendTx(amount, recipient, tokenType);
  return elusiv.sendElusivTx(txt);
};

const generateKeypair = (): Keypair => {
  let keyPair = Keypair.generate();

  console.log(`Public key: ${keyPair.publicKey.toBase58()}`);
  console.log(`Private key: ${keyPair.secretKey}`);

  return keyPair;
};

const airdropSol = async (wallet: Keypair) => {
  try {
    const connection = new Connection(clusterApiUrl("devnet"));
    const airdropSignature = await connection.requestAirdrop(new PublicKey(wallet.publicKey), 1 * LAMPORTS_PER_SOL);
    const latestBlockHash = await connection.getLatestBlockhash();

    await connection.confirmTransaction({
      blockhash: latestBlockHash.blockhash,
      lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
      signature: airdropSignature,
    });
    console.log(`Airdropped 1 SOL to ${wallet.publicKey.toBase58()}`);
  } catch (e) {
    console.error(`Error sending SOL via Elusiv: ${e}`);
  }
};

main();

Elusivコードの詳細

コード
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512";
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));

まず、ウォレット接続を設定していないため、noble-ed25519ライブラリをインポートします。本番環境では、代わりにSolana Wallet Adapterを使用してトランザクションに署名します。ここではsign機能のみが必要ですが、import { sign } from "@noble/ed25519";を使用すると、etc.sha512Syncが設定されていないというエラーが返されることがあります。このエラーを避けるため、自分で設定します。ここはあまり気にする必要はありません。

コード
import {
  Connection,
  PublicKey,
  Keypair,
  ConfirmedSignatureInfo,
  Cluster,
  LAMPORTS_PER_SOL,
  clusterApiUrl,
} from "@solana/web3.js";
import { Elusiv, TokenType, SEED_MESSAGE } from "@elusiv/sdk";

次に、必要なモジュールとともにSolanaのweb3.jsとElusivのTypeScript SDKをインポートします。

コード
const CLUSTER: Cluster = "devnet";
const RPC = "https://rpc-devnet.helius.xyz";

ここでは、CLUSTERを"devnet"として定義し、RPCをHelius devnet RPCに設定します。このスクリプトをmainnetで使用する場合は、CLUSTERに"mainnet"を割り当て、いずれかのプランからHelius APIキーを取得してください 😎

コード
const main = async () => {
	const connection: Connection = new Connection(RPC);
	const keyPair: Keypair = generateKeypair();
	const recipientPublicKey: PublicKey = new PublicKey(generateKeypair().publicKey);

	// Rest of the code
}

main関数のブロック内で複数の関数をawaitするため、この関数を非同期として指定します。次に、RPCを使用してdevnetへの接続を確立し、ユーティリティ関数generateKeypairからKeypairオブジェクトを作成します。また、ランダムなアドレスでシールドされたSOLを受け取れるように、前述のgenerateKeypair関数を使用してrecipientPublicKeyも作成します。参考までに、generateKeypair関数は次のように定義されています。

コード
const generateKeypair = (): Keypair => {
	let keyPair = Keypair.generate();

	console.log(`Public key: ${keyPair.publicKey.toBase58()}`);
	console.log(`Private key: ${keyPair.secretKey}`);

	return keyPair;
};

この関数は、このスクリプトで使用するテスト用Solanaキーペアを作成します。mainnetで使用する場合は、このキーペアを自身のウォレットのキーペアに置き換えます。秘密鍵は平文で保存せず、正しいファイルパスを使用して適切にインポートすることを必ず徹底してください。

コード
await airdropSol(keyPair);

新しく生成したテスト用キーペアを使用するため、devnet SOLが必要です。次のように定義されたaidropSolユーティリティ関数を使用し、新しいキーペアへSOLをエアドロップします。

コード
const airdropSol = async (wallet: Keypair) => {
  try {
    const connection = new Connection(clusterApiUrl("devnet"));
    const airdropSignature = await connection.requestAirdrop(new PublicKey(wallet.publicKey), 1 * LAMPORTS_PER_SOL);
    const latestBlockHash = await connection.getLatestBlockhash();

    await connection.confirmTransaction({
      blockhash: latestBlockHash.blockhash,
      lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
      signature: airdropSignature,
    });
    console.log(`Airdropped 1 SOL to ${wallet.publicKey.toBase58()}`);
  } catch (e) {
    console.error(`Error sending SOL via Elusiv: ${e}`);
  }
};

ここでは、devnet上で自分自身に1 SOL(1 * LAMPORTS_PER_SOL)をエアドロップします。latestBlockHashを取得し、ブロックハッシュと最後の有効ブロック高をconnection.confirmTransaction()へ渡すことで、正しいアドレスに1 SOLがエアドロップされたことを確認します。

コード
const seed: Uint8Array = ed.sign(Buffer.from(SEED_MESSAGE, "utf-8"), keyPair.secretKey.slice(0, 32));

Elusivでは特定の処理を決定論的にするために入力シードが必要なので、SEED_MESSAGEをインポートして使用します。SEED_MESSAGEはElusivシードの生成に使用されるため、署名が必要なメッセージです。これにより、アプリケーションは関連する秘密値を使って非公開資産を復号し、資金を追跡できます。Buffer.from()を使用し、utf-8エンコーディングでSEED_MESSAGEから新しいバッファ(生のバイナリデータ構造)を作成します。Solanaのキーペア型では、先頭の32バイトが秘密鍵、末尾の32バイトが公開鍵となるため、sliceを使用する必要があります。こうして、秘密鍵を使用して必要なシードメッセージに署名します。

コード
const elusiv: Elusiv = await Elusiv.getElusivInstance(seed, keyPair.publicKey, connection, CLUSTER);

ここでは、seed、keyPair.publicKey、connection、CLUSTERを使用して、Elusivのインスタンスであるelusivを定義します。つまり、devnet RPC、自分の公開鍵、先ほど署名した長いメッセージを使って、devnet上にElusivインスタンスを作成します。

コード
const balance: bigint = await elusiv.getLatestPrivateBalance("LAMPORTS");

次に、Elusivプール内に保有するLamportsの量である非公開残高を取得します。これはBigIntとして返されます。main関数の残りは、次のように定義されたtry-catch-finally block内にカプセル化されています。

コード
try {
	if (balance > BigInt(0)) {
      const signature = await send(elusiv, recipientPublicKey, 1 * LAMPORTS_PER_SOL, "LAMPORTS");

      console.log(`Sent with signature ${signature.signature}`);
    } else {
      // Rest of code
	}
}

非公開残高がある場合、balanceは0より大きくなり、if-elseブロックの最初のブロックが実行されます。if条件で0をBigIntとしてキャストするのは、getLatestPrivateBalanceの戻り値の型がBigIntだからです。その後、send()の戻り値に設定された変数signatureを作成します。send()関数は次のように定義されています。

コード
const send = async (
  elusiv: Elusiv,
  recipient: PublicKey,
  amount: number,
  tokenType: TokenType
): Promise => {
  const txt = await elusiv.buildSendTx(amount, recipient, tokenType);
  return elusiv.sendElusivTx(txt);
};

この関数はElusiv経由でトランザクションを送信し、ConfirmedSignatureInfo型(ステータス付きの確認済み署名)のPromiseを返します。引数として、Elusivインスタンス、受取人の公開鍵、送信額、トークンの種類を受け取ります。トークンはLamportsに限らず、USDC、USDT、mSOL、BONK、SAMOなども使用できます。次に、elusiv.buildSendTx()を使用してtxtを設定します。buildSendTx()はElusivインスタンスのメソッドで、非公開残高を使用して指定した受取人へトークンを送るトランザクションを構築します。その後、構築したトランザクションtxtをreturn elusiv.sendElusivTx(txt)文に渡し、トランザクションを送信して返します。

非公開残高がない場合は、elseブロックが実行されます。

コード
console.log("Private balance is empty. Topping up...");

const amount = 1 * LAMPORTS_PER_SOL;
const tokenType = "LAMPORTS";

const topUpTxData = await elusiv.buildTopUpTx(amount, tokenType);

topUpTxData.tx.partialSign(keyPair);

const topUpSig: ConfirmedSignatureInfo = await elusiv.sendElusivTx(topUpTxData);
console.log(`Top-up complete with signature ${topUpSig.signature}`);

const signature: ConfirmedSignatureInfo = await send(elusiv, recipientPublicKey, 1 * LAMPORTS_PER_SOL, "LAMPORTS");
console.log(`Sent with signature ${signature.signature}`);

非公開残高がなく、これから入金することをユーザーにログで通知します。次に、Elusivインスタンスにある.buildTopUpTx()メソッドを使用して、入金トランザクションを構築します。SOLを送るため、1 SOLの値とLamportsのトークン型を渡します。自分の公開鍵から入金するため、トランザクションのキーペアに対して.partialSign()を使用し、入金に署名します。その後、elusiv.sendElusivTx(topUpTxData)を使用して構築済みトランザクションを送信し、トランザクション署名をログに記録します。

try-catch-finallyブロックの残りは次のとおりです。

コード
} catch (e) {
	console.error(`Error sending SOL via Elusiv: ${e}`);
} finally {
	console.log("Exiting program...");
	process.exit(0);
}

Elusiv経由でSOLを送信する際にエラーが発生した場合は、console.error()を使用してコンソールに記録します。その後、process.exit(0)を使用してプログラムを終了します。

このプロセス全体を開始するため、ファイルの末尾でmain()関数を呼び出します。

Light Protocolを使用してSOLを非公開で送信する

完全なコード

コード
import { BN } from "@coral-xyz/anchor";
import { PublicKey, Keypair, Connection } from "@solana/web3.js";
import { User, Provider as LightProvider, TestRelayer, confirmConfig, airdropSol } from "@lightprotocol/zk.js";

const initializeSolanaWallet = async (): Promise => {
  const wallet = Keypair.generate();

  console.log("Wallet initialized");
  return wallet;
};

const requestAirdrop = async (connection: Connection, publicKey: PublicKey): Promise => {
  await airdropSol({
    connection,
    lamports: 2e9,
    recipientPublicKey: publicKey,
  });
  console.log("Airdrop requested...");
};

const setupTestRelayer = async (solanaWallet: Keypair): Promise => {
  const testRelayer = new TestRelayer({
    relayerPubkey: solanaWallet.publicKey,
    relayerRecipientSol: solanaWallet.publicKey,
    relayerFee: new BN(100_000),
    payer: solanaWallet,
  });
  console.log("Test relayer initialized");
  return testRelayer;
};

const initializeLightProvider = async (solanaWallet: Keypair, testRelayer: TestRelayer): Promise => {
  const lightProvider = await LightProvider.init({
    wallet: solanaWallet,
    relayer: testRelayer,
    confirmConfig,
  });

  console.log("Light provider initialized");
  return lightProvider;
};

const initializeLightUser = async (lightProvider: LightProvider): Promise => {
  const user = await User.init({ provider: lightProvider });
  console.log("Light user initialized");
  return user;
};

const performShieldOperation = async (user: User) => {
  await user.shield({
    publicAmountSol: "1.1",
    token: "SOL",
  });
  console.log("Performed shield operation");
};

const executePrivateTransfer = async (user: User, testRecipientPublicKey: string) => {
  const response = await user.transfer({
    amountSol: "1",
    token: "SOL",
    recipient: testRecipientPublicKey,
  });
  console.log(`Executed private transfer! Txt hash: ${response.txHash}`);
};

const main = async () => {
  try {
    const solanaWallet = await initializeSolanaWallet();
    const connection = new Connection("http://127.0.0.1:8899");

    await requestAirdrop(connection, solanaWallet.publicKey);

    const testRelayer = await setupTestRelayer(solanaWallet);
    const lightProvider = await initializeLightProvider(solanaWallet, testRelayer);
    const user = await initializeLightUser(lightProvider);

    await performShieldOperation(user);

    const testRecipientKeypair = Keypair.generate();

    await requestAirdrop(connection, testRecipientKeypair.publicKey);

    const lightProviderRecipient = await initializeLightProvider(testRecipientKeypair, testRelayer);
    const testRecipient = await initializeLightUser(lightProviderRecipient);

    await executePrivateTransfer(user, testRecipient.account.getPublicKey());
    console.log(`Successfully sent 1 $SOL to ${testRecipient.account.getPublicKey()} privately!`);
  } catch (e) {
    console.error(`Error sending SOL via Light: ${e}`);
  } finally {
    console.log("Exiting program...");
    process.exit(0);
  }
};

main();

コードの詳細

コード
import { BN } from "@coral-xyz/anchor";
import { PublicKey, Keypair, Connection } from "@solana/web3.js";
import { User, Provider as LightProvider, TestRelayer, confirmConfig, airdropSol } from "@lightprotocol/zk.js";

まず、Light Protocolを使用してSolanaとやり取りするために必要なモジュールを、Anchor、Solana/web3.js、Lightからインポートします。その後、main関数に進む前に、複数のユーティリティ関数を定義します。Lightの初期化プロセスは、Elusivの実装より複雑です。

コード
const initializeSolanaWallet = async (): Promise => {
  const wallet = Keypair.generate();

  console.log("Wallet initialized");
  return wallet;
};

ここでは、Keypair.generate()メソッドを使用して新しいSolanaウォレットを生成するinitializeSolanaWallet関数を作成します。

コード
const requestAirdrop = async (connection: Connection, publicKey: PublicKey): Promise => {
  await airdropSol({
    connection,
    lamports: 2e9,
    recipientPublicKey: publicKey,
  });
  console.log("Airdrop requested...");
};

Elusivとは異なり、Lightのライブラリには、指定した公開鍵へLamportsをエアドロップするairdropSol関数が用意されています。渡されたpublicKeyへSOLを送信するため、ユーティリティ関数requestAirdropを作成します。

コード
const setupTestRelayer = async (solanaWallet: Keypair): Promise => {
  const testRelayer = new TestRelayer({
    relayerPubkey: solanaWallet.publicKey,
    relayerRecipientSol: solanaWallet.publicKey,
    relayerFee: new BN(100_000),
    payer: solanaWallet,
  });
  console.log("Test relayer initialized");
  return testRelayer;
};

Light経由でシールドされたSOLを送信するには、TestRelayerモジュールを使用してテスト用リレイヤーを設定する必要があります。リレイヤーは、状態遷移を証明するZK証明をクライアントから対応するオンチェーンプログラムへ転送するWebサーバーであり、Lightエコシステムで重要な役割を果たします。また、証明の検証に成功した際に手数料を受け取る代わりに、トランザクション手数料を支払います。手数料は100k Lamportsで、new BN(100_000)を使用して設定します。

コード
const initializeLightProvider = async (solanaWallet: Keypair, testRelayer: TestRelayer): Promise => {
  const lightProvider = await LightProvider.init({
    wallet: solanaWallet,
    relayer: testRelayer,
    confirmConfig,
  });

  console.log("Light provider initialized");
  return lightProvider;
};

次に、その名のとおりのinitializeLightProvider()関数を使用してLight Providerを初期化します。この関数はProviderを設定し、Lightが提供する設定confirmConfigを使用して、Solanaウォレットおよびテスト用リレイヤーと連携させます。この設定はConfirmOptions型で、トランザクションの検証手順やコミットメントレベルなどを処理します。

コード
const initializeLightUser = async (lightProvider: LightProvider): Promise => {
  const user = await User.init({ provider: lightProvider });
  console.log("Light user initialized");
  return user;
};

initializeLightUser関数の役割は明確です。渡されたlightProviderを使用して、Light Protocolのユーザーを初期化します。

コード
const performShieldOperation = async (user: User) => {
  await user.shield({
    publicAmountSol: "1.1",
    token: "SOL",
  });
  console.log("Performed shield operation");
};

performShieldOperation関数を使用すると、SOLをシールドして非公開にできます。ここでは手数料を考慮し、シールドされたSOLを1 SOLだけ送る一方で、1 SOLを超える量をシールドします。

コード
const executePrivateTransfer = async (user: User, testRecipientPublicKey: string) => {
  const response = await user.transfer({
    amountSol: "1",
    token: "SOL",
    recipient: testRecipientPublicKey,
  });
  console.log(`Executed private transfer! Txt hash: ${response.txHash}`);
};

実際に非公開送金を実行するため、これが最も重要なユーティリティ関数と言えるでしょう。ここでは、userから指定された受取人testRecipientPublicKeyへ、1 SOLを非公開で送金します。

コード
const main = async () => {
  try {
    const solanaWallet = await initializeSolanaWallet();
    const connection = new Connection("http://127.0.0.1:8899");

    await requestAirdrop(connection, solanaWallet.publicKey);

    const testRelayer = await setupTestRelayer(solanaWallet);
    const lightProvider = await initializeLightProvider(solanaWallet, testRelayer);
    const user = await initializeLightUser(lightProvider);

    await performShieldOperation(user);

    const testRecipientKeypair = Keypair.generate();

    await requestAirdrop(connection, testRecipientKeypair.publicKey);

    const lightProviderRecipient = await initializeLightProvider(testRecipientKeypair, testRelayer);
    const testRecipient = await initializeLightUser(lightProviderRecipient);

    await executePrivateTransfer(user, testRecipient.account.getPublicKey());
    console.log(`Successfully sent 1 $SOL to ${testRecipient.account.getPublicKey()} privately!`);
  } catch (e) {
    console.error(`Error sending SOL via Light: ${e}`);
  } finally {
    console.log("Exiting program...");
    process.exit(0);
  }
};

main関数は、このスクリプトのエントリーポイントです。スコープ内で複数の非同期関数を呼び出すため、非同期として指定します。ウォレットの初期化から非公開送金の実行まで、すべての手順を順番に処理します。まず、Solanaウォレットと接続を設定します。次に、ウォレットへSOLをエアドロップし、リレイヤーとLight Providerを初期化します。その後、ウォレットをLight Userとして初期化し、SOLをシールドします。続いてテスト用の受取人を作成し、SOLをエアドロップしてLight Userとして初期化した後、1 SOLを非公開で送信します。Elusivのコードと同様に、エラーを捕捉し、完了後にプログラムを自動終了するため、ロジックをtry-catch-finallyブロックに配置します。

Elusivではdevnetを使用していたのに対し、ここでは接続先を「http://127.0.0.1:8899」に設定していることに気付くかもしれません。この記事の執筆時点では、Light Protocolはdevnet/mainnetにデプロイされていないため、Heliusキーは(まだ 👀)機能しない点に注意してください。localhostでは、コードを実行するために必要なアカウントとプログラムを事前に読み込んだtest-validatorを起動する必要があります。そのためにLight Protocol CLIをインストールし、次のコマンドを実行します。

コード
light test-validator

これにより、バックグラウンドでvalidatorが起動します。現在、UTXOクラス内にいくつかのユニットテストがあるため、テストを簡単にするアサーションライブラリのchai も手動で追加する必要があります。ただし、この問題は次のリリースで解決される予定です。このスクリプトのpackage.jsonは、次のようになります。

コード
{
  "name": "helius-tutorial",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@lightprotocol/cli": "^0.1.1-alpha.21",
    "@types/node": "^20.6.0",
    "chai": "^4.3.8",
    "typescript": "^5.2.2"
  },
  "dependencies": {
    "@coral-xyz/anchor": "^0.28.1-beta.2",
    "@lightprotocol/zk.js": "^0.3.2-alpha.14",
    "@solana/web3.js": "^1.78.4"
  }
}

まとめ

おめでとうございます!このチュートリアルでは、プライバシーの詳細と重要性、そしてElusivとLight Protocolを使用してSOLを非公開で取引する方法を解説しました。Elusivでは、非公開残高の取得、SOLの非公開送信、さらに非公開残高が尽きた場合や存在しない場合の自動入金を実装しました。Light Protocolでは、複雑で高度にモジュール化された初期化プロセスを設定し、SOLをシールドして非公開送金を実行する方法を確認しました。

では、どちらを使うべきでしょうか?答えは、用途によります。Elusivのコードでは、共有プールから非公開残高へ入金し、トランザクションを非公開で送信することに重点を置きました。このスクリプトは、Elusivプールを管理する定義済みのスマートコントラクトとやり取りして、これを実現します。Light Protocolでは、より柔軟ですが複雑な初期化プロセスを構築し、プライバシーを保護するSolanaプログラムを複数のユーザーが利用するワークフローを作成しました。必要であれば、Lightを使用して独自のPrivate Solana Programs(PSPs)も作成できます。ドキュメントや具体的な実装などの面で、どちらにもトレードオフがありますが、両プロトコルともプライバシーと安全なトランザクションを目指しています。アプリケーションでプライバシー機能をより細かく制御し、アプリケーション状態の特定部分を暗号化したい場合は、Light Protocolを検討してください。よりシンプルなものを構築し、devnet/mainnet上のトークン送金にプライバシーレイヤーを追加したい場合は、Elusivの方が適している可能性があります。

この記事で紹介した技術、コード、知見は、Solana上でプライバシー重視のアプリケーションを構築するための確かな出発点になります。ブロックチェーン技術におけるプライバシーの重要性は、いくら強調してもしすぎることはありません。分散型システムへの需要が高まるにつれ、ユーザーに力を与え、権利を保護し、初期のブロックチェーン技術が目指した本来の目的を実現するプライバシー機能の必要性も高まります。プライバシーは重要です。ElusivとLight Protocolを活用すれば、Solanaをより安全なブロックチェーンにできます!

匿名の皆さん、ここまで読んでいただきありがとうございます!

追加リソースと参考資料

‍

Heliusを購読

Solana開発の最新情報や新しい記事の公開通知を受け取れます