メインコンテンツへスキップ

ステルスアドレスの使用

ステルスアドレス
プライバシー
暗号技術
Rust
wasm
中級
Ori Pomerantz
2025年11月30日
29 分の読書

あなたはBillです。 理由は省きますが、あなたは「世界女王アリス」キャンペーンに寄付をして、アリスに寄付したことを知らせ、彼女が勝った場合に報酬を得たいと考えています。 残念ながら、彼女の勝利は保証されていません。 「太陽系女帝キャロル」という競合キャンペーンがあります。 もしキャロルが勝ち、あなたがアリスに寄付したことを彼女が知ったら、あなたは面倒なことになります。 そのため、自分のアカウントからアリスのアカウントに200 ETHを単純に送金することはできません。

ERC-5564 (opens in a new tab)がその解決策です。 このERCは、匿名での送金にステルスアドレス (opens in a new tab)を使用する方法を説明しています。

警告: ステルスアドレスの背後にある暗号技術は、私たちが知る限り、健全です。 しかし、潜在的なサイドチャネル攻撃が存在します。 以下では、このリスクを軽減するために何ができるかを見ていきます。

ステルスアドレスの仕組み

この記事では、ステルスアドレスを2つの方法で説明します。 1つ目はその使用方法です。 この記事の残りの部分を理解するには、このパートで十分です。 次に、その背後にある数学的な説明があります。 暗号技術に興味がある場合は、この部分もお読みください。

簡易版 (ステルスアドレスの使い方)

アリスは2つの秘密鍵を作成し、対応する公開鍵を公開します (これらを組み合わせて単一の倍長のメタアドレスにすることができます)。 ビルも秘密鍵を作成し、対応する公開鍵を公開します。

一方の当事者の公開鍵と他方の秘密鍵を使用することで、アリスとビルだけが知っている共有シークレットを導き出すことができます (公開鍵だけからは導き出すことはできません)。 この共有シークレットを使用して、ビルはステルスアドレスを取得し、そこに資産を送ることができます。

アリスも共有シークレットからアドレスを取得しますが、公開した公開鍵の秘密鍵を知っているため、そのアドレスから引き出すことができる秘密鍵も取得できます。

数学的な仕組み (なぜステルスアドレスがこのように機能するのか)

標準的なステルスアドレスでは、楕円曲線暗号 (ECC) (opens in a new tab) を使用して、より少ない鍵ビットでより優れたパフォーマンスを得ながら、同じレベルのセキュリティを維持します。 しかし、ほとんどの場合、それを無視して通常の算術を使用していると見なすことができます。

誰もが知っている数字、G があります。 G を掛けることができます。 しかし、ECCの性質上、G で割ることは事実上不可能です。 イーサリアムにおける公開鍵暗号技術の一般的な仕組みは、秘密鍵 Ppriv を使用してトランザクションに署名し、それが公開鍵 Ppub = GPpriv によって検証されるというものです。

アリスは2つの秘密鍵、KprivVpriv を作成します。 Kpriv はステルスアドレスから資金を使用するために、Vpriv はアリスに属するアドレスを表示するために使用されます。 アリスは次に公開鍵 Kpub = GKprivVpub = GVpriv を公開します。

ビルは3番目の秘密鍵 Rpriv を作成し、Rpub = GRpriv を中央レジストリに公開します (ビルはアリスに送ることもできましたが、ここではキャロルが聞き耳を立てていると仮定します)。

ビルは RprivVpub = GRprivVpriv を計算し、アリスもこれを知っていると期待します (下記で説明)。 この値は S (共有シークレット) と呼ばれます。 これにより、ビルは公開鍵 Ppub = Kpub+G*hash(S) を得ます。 この公開鍵から、彼はアドレスを計算し、好きなリソースをそこに送ることができます。 将来、アリスが勝った場合、ビルは彼女に Rpriv を伝えて、リソースが自分からのものであることを証明できます。

アリスは RpubVpriv = GRprivVpriv を計算します。 これにより、彼女も同じ共有シークレット S を得ます。 彼女は秘密鍵 Kpriv を知っているので、Ppriv = Kpriv+hash(S) を計算できます。 この鍵により、彼女は Ppub = GPpriv = GKpriv+G*hash(S) = Kpub+G*hash(S) から得られるアドレスの資産にアクセスできます。

アリスがDaveの「世界征服キャンペーンサービス」に業務を委託できるように、別の閲覧用鍵があります。 アリスは、デイブに公開アドレスを知らせて、より多くの資金が利用可能になったときに通知してもらうことには前向きですが、デイブにキャンペーン資金を使われることは望んでいません。

閲覧と使用は別の鍵を使うため、アリスはデイブに Vpriv を渡すことができます。 するとデイブは S = RpubVpriv = GRprivVpriv を計算でき、それによって公開鍵 (Ppub = Kpub+G*hash(S)) を得ることができます。 しかし、Kpriv がなければ、デイブは秘密鍵を取得できません。

まとめると、これらが異なる参加者によって知られている値です。

アリス公開済みビルデイブ
GGGG
Kpriv---
Vpriv--Vpriv
Kpub = GKprivKpubKpubKpub
Vpub = GVprivVpubVpubVpub
--Rpriv-
RpubRpubRpub = GRprivRpub
S = RpubVpriv = GRprivVpriv-S = RprivVpub = GRprivVprivS = RpubVpriv = GRprivVpriv
Ppub = Kpub+G*hash(S)-Ppub = Kpub+G*hash(S)Ppub = Kpub+G*hash(S)
Address=f(Ppub)-Address=f(Ppub)Address=f(Ppub)Address=f(Ppub)
Ppriv = Kpriv+hash(S)---

ステルスアドレスがうまくいかない場合

ブロックチェーン上に秘密はありません。 ステルスアドレスはプライバシーを提供できますが、そのプライバシーはトラフィック分析に対して脆弱です。 簡単な例を挙げると、ビルがあるアドレスに資金を供給し、すぐにトランザクションを送信して Rpub 値を公開するとします。 アリスの Vpriv がなければ、これがステルスアドレスであると確信することはできませんが、そうである可能性が高いです。 次に、そのアドレスからアリスのキャンペーン資金アドレスにすべてのETHを送金する別のトランザクションが見られます。 証明はできないかもしれませんが、ビルがアリスのキャンペーンに寄付した可能性が高いです。 キャロルは間違いなくそう思うでしょう。

ビルにとって、Rpub の公開とステルスアドレスへの資金供給を分離することは簡単です (異なる時間に、異なるアドレスから行います)。 しかし、それだけでは不十分です。 キャロルが探すパターンは、ビルがあるアドレスに資金を提供し、その後アリスのキャンペーン資金がそこから引き出すというものです。

1つの解決策は、アリスのキャンペーンが直接資金を引き出すのではなく、第三者への支払いに使用することです。 アリスのキャンペーンがデイブの世界征服キャンペーンサービスに10 ETHを送金した場合、キャロルはビルがデイブの顧客の1人に寄付したことしか知りません。 デイブに十分な顧客がいれば、キャロルはビルが彼女と競合するアリスに寄付したのか、それともキャロルが気にしていないアダム、アルバート、アビゲイルに寄付したのかを知ることはできません。 アリスは支払いにハッシュ化された値を含め、その後デイブにプリイメージを提供することで、それが自分の寄付であることを証明できます。 あるいは、前述のように、アリスがデイブに彼女の Vpriv を渡せば、彼はすでに支払いが誰からのものかを知っています。

この解決策の主な問題は、その秘密がビルに利益をもたらす場合に、アリスが秘密を守ることを気にしなければならないという点です。 アリスは、ビルの友人であるボブも彼女に寄付するように、自分の評判を維持したいかもしれません。 しかし、彼女がビルを暴露することを気にしない可能性もあります。なぜなら、そうすれば彼はキャロルが勝った場合に何が起こるかを恐れるからです。 ビルは結果的にアリスをさらに支援することになるかもしれません。

複数のステルスレイヤーの使用

ビルのプライバシーを保護するためにアリスに頼る代わりに、ビル自身がそれを行うことができます。 彼は架空の人物、ボブとベラのために複数のメタアドレスを生成できます。 ビルは次にETHをボブに送り、「ボブ」(実際にはビル) がそれをベラに送ります。 「ベラ」(これもビル) がそれをアリスに送ります。

キャロルは依然としてトラフィック分析を行い、ビルからボブ、ベラ、アリスへのパイプラインを見ることができます。 しかし、「ボブ」と「ベラ」が他の目的でETHを使用している場合、アリスがステルスアドレスから既知のキャンペーンアドレスにすぐに引き出したとしても、ビルがアリスに何かを転送したようには見えません。

ステルスアドレスアプリケーションの作成

この記事では、GitHub (opens in a new tab) で入手可能なステルスアドレスアプリケーションについて説明します。

ツール

使用できるTypeScriptのステルスアドレスライブラリ (opens in a new tab)があります。 しかし、暗号操作はCPUを集中的に使用することがあります。 私はそれらをRust (opens in a new tab)のようなコンパイル言語で実装し、WASM (opens in a new tab)を使用してブラウザでコードを実行することを好みます。

Vite (opens in a new tab)React (opens in a new tab)を使用します。 これらは業界標準のツールです。もし馴染みがない場合は、このチュートリアルを使用できます。 Viteを使用するには、Nodeが必要です。

ステルスアドレスの動作を見る

  1. 必要なツールをインストールします: Rust (opens in a new tab)Node (opens in a new tab)

  2. GitHubリポジトリをクローンします。

    1git clone https://github.com/qbzzt/251022-stealth-addresses.git
    2cd 251022-stealth-addresses
  3. 前提条件をインストールし、Rustコードをコンパイルします。

    1cd src/rust-wasm
    2rustup target add wasm32-unknown-unknown
    3cargo install wasm-pack
    4wasm-pack build --target web
  4. Webサーバーを起動します。

    1cd ../..
    2npm install
    3npm run dev
  5. アプリケーション (opens in a new tab)にアクセスします。 このアプリケーションページには2つのフレームがあります。1つはアリスのユーザーインターフェース用、もう1つはビルのユーザーインターフェース用です。 2つのフレームは通信しません。便宜上、同じページにあるだけです。

  6. アリスとして、ステルス・メタアドレスを生成をクリックします。 これにより、新しいステルスアドレスと対応する秘密鍵が表示されます。 ステルス・メタアドレスをクリップボードにコピーします。

  7. ビルとして、新しいステルス・メタアドレスを貼り付け、アドレスを生成をクリックします。 これにより、アリスに資金を送るためのアドレスが表示されます。

  8. アドレスとビルの公開鍵をコピーし、アリスのユーザーインターフェースの「ビルによって生成されたアドレスの秘密鍵」エリアに貼り付けます。 これらのフィールドに入力すると、そのアドレスの資産にアクセスするための秘密鍵が表示されます。

  9. オンライン計算機 (opens in a new tab)を使用して、秘密鍵がアドレスに対応していることを確認できます。

プログラムの仕組み

WASMコンポーネント

WASMにコンパイルされるソースコードはRust (opens in a new tab)で書かれています。 src/rust_wasm/src/lib.rs (opens in a new tab)で確認できます。 このコードは主に、JavaScriptコードとeth-stealth-addressesライブラリ (opens in a new tab)間のインターフェースです。

Cargo.toml

RustにおけるCargo.toml (opens in a new tab)は、JavaScriptのpackage.json (opens in a new tab)に類似しています。 パッケージ情報、依存関係の宣言などが含まれています。

1[package]
2name = "rust-wasm"
3version = "0.1.0"
4edition = "2024"
5
6[dependencies]
7eth-stealth-addresses = "0.1.0"
8hex = "0.4.3"
9wasm-bindgen = "0.2.104"
10getrandom = { version = "0.2", features = ["js"] }
すべて表示

getrandom (opens in a new tab)パッケージは、乱数を生成する必要があります。 それは純粋なアルゴリズム的手法では実行できません。エントロピーの源として物理的なプロセスへのアクセスが必要です。 この定義は、実行中のブラウザに問い合わせることで、そのエントロピーを取得することを指定します。

1console_error_panic_hook = "0.1.7"

このライブラリ (opens in a new tab)は、WASMコードがパニックを起こして続行できなくなったときに、より意味のあるエラーメッセージを提供します。

1[lib]
2crate-type = ["cdylib", "rlib"]

WASMコードを生成するために必要な出力タイプです。

lib.rs

これが実際のRustコードです。

1use wasm_bindgen::prelude::*;

RustからWASMパッケージを作成するための定義です。 それらはここ (opens in a new tab)で文書化されています。

1use eth_stealth_addresses::{
2 generate_stealth_meta_address,
3 generate_stealth_address,
4 compute_stealth_key
5};

eth-stealth-addresses ライブラリ (opens in a new tab)から必要な関数です。

1use hex::{decode,encode};

Rustは通常、値にバイト配列 (opens in a new tab) ([u8; <size>]) を使用します。 しかし、JavaScriptでは、通常16進数文字列を使用します。 hexライブラリ (opens in a new tab)は、ある表現から別の表現へと変換してくれます。

1#[wasm_bindgen]

JavaScriptからこの関数を呼び出すことができるように、WASMバインディングを生成します。

1pub fn wasm_generate_stealth_meta_address() -> String {

複数のフィールドを持つオブジェクトを返す最も簡単な方法は、JSON文字列を返すことです。

1 let (address, spend_private_key, view_private_key) =
2 generate_stealth_meta_address();

generate_stealth_meta_address (opens in a new tab)は3つのフィールドを返します:

  • メタアドレス(KpubVpub)
  • 閲覧用秘密鍵(Vpriv)
  • 支払い用秘密鍵(Kpriv)

タプル (opens in a new tab)構文を使用すると、これらの値を再度分離できます。

1 format!("{{\"address\":\"{}\",\"view_private_key\":\"{}\",\"spend_private_key\":\"{}\"}}",
2 encode(address),
3 encode(view_private_key),
4 encode(spend_private_key)
5 )
6}

format! (opens in a new tab)マクロを使用して、JSONエンコードされた文字列を生成します。 hex::encode (opens in a new tab)を使用して、配列を16進数文字列に変更します。

1fn str_to_array<const N: usize>(s: &str) -> Option<[u8; N]> {

この関数は、(JavaScriptから提供された) 16進数文字列をバイト配列に変換します。 JavaScriptコードから提供された値を解析するために使用します。 この関数は、Rustが配列とベクターをどのように扱うかによって複雑になっています。

<const N: usize>という式はジェネリック (opens in a new tab)と呼ばれます。 N は返される配列の長さを制御するパラメータです。 この関数は実際にはstr_to_array::<n>として呼び出され、nは配列の長さです。

戻り値は Option<[u8; N]> であり、これは返される配列がオプショナル (opens in a new tab)であることを意味します。 これは、失敗する可能性のある関数に対するRustの典型的なパターンです。

例えば、str_to_array::10("bad060a7")を呼び出すと、関数は10個の値を持つ配列を返すはずですが、入力は4バイトしかありません。 関数は失敗する必要があり、Noneを返すことでそれを行います。 str_to_array::4("bad060a7")の戻り値は Some<[0xba, 0xd0, 0x60, 0xa7]> になります。

1 // decodeはResult<Vec<u8>, _>を返す
2 let vec = decode(s).ok()?;

hex::decode (opens in a new tab)関数はResult<Vec<u8>, FromHexError>を返します。 Result (opens in a new tab)型には、成功した結果 (Ok(value)) またはエラー (Err(error)) のいずれかが含まれます。

.ok()メソッドはResultOptionに変換し、その値は成功した場合はOk()の値、そうでなければNoneになります。 最後に、疑問符演算子 (opens in a new tab)は、Optionが空の場合に現在の関数を中止し、Noneを返します。 そうでなければ、値をアンラップしてそれを返します (この場合、vecに値を割り当てます)。

これは奇妙で複雑なエラー処理方法に見えるかもしれませんが、ResultOptionは、すべてのエラーが何らかの方法で処理されることを保証します。

1 if vec.len() != N { return None; }

バイト数が正しくない場合は失敗であり、Noneを返します。

1 // try_intoはvecを消費し、[u8; N]を作成しようと試みる
2 let array: [u8; N] = vec.try_into().ok()?;

Rustには2つの配列型があります。 配列 (opens in a new tab)は固定サイズです。 ベクター (opens in a new tab)は大きくなったり小さくなったりできます。 hex::decodeはベクターを返しますが、eth_stealth_addressesライブラリは配列を受け取ることを期待しています。 .try_into() (opens in a new tab)は、値を別の型、たとえばベクターを配列に変換します。

1 Some(array)
2}

Rustでは、関数の最後で値を返す際にreturn (opens in a new tab)キーワードを使用する必要はありません。

1#[wasm_bindgen]
2pub fn wasm_generate_stealth_address(stealth_address: &str) -> Option<String> {

この関数は、VpubKpub の両方を含む公開メタアドレスを受け取ります。 これは、ステルスアドレス、公開する公開鍵 (Rpub)、および公開されたアドレスのうちどれがアリスに属する可能性があるかを特定する速度を上げる1バイトのスキャン値を返します。

スキャン値は共有シークレット (S = GRprivVpriv) の一部です。 この値はアリスが利用でき、これを確認する方が f(Kpub+G*hash(S)) が公開されたアドレスと等しいかどうかを確認するよりもはるかに高速です。

1 let (address, r_pub, scan) =
2 generate_stealth_address(&str_to_array::<66>(stealth_address)?);

ライブラリのgenerate_stealth_address (opens in a new tab)を使用します。

1 format!("{{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}}",
2 encode(address),
3 encode(r_pub),
4 encode(&[scan])
5 ).into()
6}

JSONエンコードされた出力文字列を準備します。

1#[wasm_bindgen]
2pub fn wasm_compute_stealth_key(
3 address: &str,
4 bill_pub_key: &str,
5 view_private_key: &str,
6 spend_private_key: &str
7) -> Option<String> {
8 .
9 .
10 .
11}
すべて表示

この関数は、ライブラリのcompute_stealth_key (opens in a new tab)を使用して、アドレスから引き出すための秘密鍵(Rpriv)を計算します。 この計算には以下の値が必要です:

  • アドレス (Address=f(Ppub))
  • ビルによって生成された公開鍵(Rpub)
  • 閲覧用秘密鍵(Vpriv)
  • 支払い用秘密鍵(Kpriv)
1#[wasm_bindgen(start)]

#[wasm_bindgen(start)] (opens in a new tab)は、WASMコードが初期化されるときに関数が実行されることを指定します。

1pub fn main() {
2 console_error_panic_hook::set_once();
3}

このコードは、パニック出力をJavaScriptコンソールに送信することを指定します。 動作を確認するには、アプリケーションを使用し、ビルに無効なメタアドレスを与えます (16進数の1桁を変更するだけ)。 JavaScriptコンソールに次のエラーが表示されます:

1rust_wasm.js:236 panicked at /home/ori/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs:701:9:
2assertion `left == right` failed
3 left: 0
4 right: 1

続いてスタックトレースが表示されます。 次に、ビルに有効なメタアドレスを与え、アリスには無効なアドレスまたは無効な公開鍵のいずれかを与えます。 次のエラーが表示されます:

1rust_wasm.js:236 panicked at /home/ori/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/eth-stealth-addresses-0.1.0/src/lib.rs:78:9:
2keys do not generate stealth address

再び、スタックトレースが続きます。

ユーザーインターフェース

ユーザーインターフェースはReact (opens in a new tab)を使用して書かれ、Vite (opens in a new tab)によって提供されます。 これらについては、このチュートリアルで学ぶことができます。 ここでは、ブロックチェーンやウォレットと直接対話しないため、WAGMI (opens in a new tab)は必要ありません。

ユーザーインターフェースで唯一自明でない部分は、WASMの接続性です。 その仕組みは次のとおりです。

vite.config.js

このファイルにはViteの設定 (opens in a new tab)が含まれています。

1import { defineConfig } from 'vite'
2import react from '@vitejs/plugin-react'
3import wasm from "vite-plugin-wasm";
4
5// https://vite.dev/config/
6export default defineConfig({
7 plugins: [react(), wasm()],
8})

2つのViteプラグインが必要です: react (opens in a new tab)wasm (opens in a new tab)

App.jsx

このファイルは、アプリケーションのメインコンポーネントです。 これは、AliceBillという2つのコンポーネントを含むコンテナで、これらのユーザーのユーザーインターフェースです。 WASMに関連する部分は初期化コードです。

1import init from './rust-wasm/pkg/rust_wasm.js'

wasm-pack (opens in a new tab)を使用すると、ここで使用する2つのファイルが作成されます。1つは実際のコードを含むwasmファイル (ここではsrc/rust-wasm/pkg/rust_wasm_bg.wasm)、もう1つはそれを使用するための定義を含むJavaScriptファイル (ここではsrc/rust_wasm/pkg/rust_wasm.js)です。 そのJavaScriptファイルのデフォルトエクスポートは、WASMを初期化するために実行する必要があるコードです。

1function App() {
2 .
3 .
4 .
5 useEffect(() => {
6 const loadWasm = async () => {
7 try {
8 await init();
9 setWasmReady(true)
10 } catch (err) {
11 console.error('Error loading wasm:', err)
12 alert("Wasm error: " + err)
13 }
14 }
15
16 loadWasm()
17 }, []
18 )
すべて表示

useEffectフック (opens in a new tab)を使用すると、状態変数が変更されたときに実行される関数を指定できます。 ここでは、状態変数のリストが空 ([]) なので、この関数はページが読み込まれたときに一度だけ実行されます。

エフェクト関数はすぐに戻る必要があります。 WASM init (これは.wasmファイルをロードする必要があるため時間がかかります) のような非同期コードを使用するために、内部でasync (opens in a new tab)関数を定義し、awaitなしで実行します。

Bill.jsx

これはビルのユーザーインターフェースです。 アリスから提供されたステルス・メタアドレスに基づいてアドレスを作成するという単一のアクションがあります。

1import { wasm_generate_stealth_address } from './rust-wasm/pkg/rust_wasm.js'

デフォルトのエクスポートに加えて、wasm-packによって生成されたJavaScriptコードは、WASMコードのすべての関数に対応する関数をエクスポートします。

1 <button onClick={() => {
2 setPublicAddress(JSON.parse(wasm_generate_stealth_address(stealthMetaAddress)))
3 }}>

wasm-packによって作成されたJavaScriptファイルからエクスポートされた関数を呼び出すだけで、WASM関数を呼び出すことができます。

Alice.jsx

Alice.jsxのコードも同様ですが、アリスには2つのアクションがあります。

  • メタアドレスを生成する
  • ビルによって公開されたアドレスの秘密鍵を取得する

結論

ステルスアドレスは万能薬ではありません。正しく使用する必要があります。 しかし、正しく使用すれば、公開ブロックチェーン上でプライバシーを確保することができます。

私の他の作品はこちらでご覧いただけます (opens in a new tab).

最終更新: 2025年11月14日

このチュートリアルは役に立ちましたか?