ステルス・アドレスの使用
あなたはビルだとします。理由はさておき、あなたは「世界女王アリス」のキャンペーンに寄付し、アリスが勝ったときに報酬をもらえるように、自分が寄付したことをアリスに知ってもらいたいと考えています。残念ながら、彼女の勝利は保証されていません。「太陽系女帝キャロル」という競合するキャンペーンがあります。もしキャロルが勝って、あなたがアリスに寄付したことを知ったら、あなたはトラブルに巻き込まれるでしょう。そのため、あなたのアカウントからアリスのアカウントへ単に200 ETHを送金することはできません。
ERC-5564 (opens in a new tab)にその解決策があります。このERCでは、匿名での送金にステルス・アドレス (opens in a new tab)を使用する方法を説明しています。
警告: ステルス・アドレスの背後にある暗号技術は、私たちの知る限り健全です。しかし、潜在的なサイドチャネル攻撃が存在します。以下で、このリスクを軽減するためにできることについて説明します。
ステルス・アドレスの仕組み
この記事では、ステルス・アドレスについて2つの方法で説明を試みます。1つ目はその使用方法です。この記事の残りの部分を理解するには、この部分だけで十分です。次に、その背後にある数学的な説明があります。暗号技術に興味がある場合は、この部分も読んでください。
シンプルなバージョン(ステルス・アドレスの使用方法)
アリスは2つの秘密鍵を作成し、対応する公開鍵(これらを組み合わせて1つの2倍の長さのメタアドレスにすることができます)を公開します。ビルも秘密鍵を作成し、対応する公開鍵を公開します。
一方の当事者の公開鍵ともう一方の当事者の秘密鍵を使用することで、アリスとビルだけが知っている共有シークレットを導出できます(公開鍵だけから導出することはできません)。この共有シークレットを使用して、ビルはステルス・アドレスを取得し、そこに資産を送金できます。
アリスも共有シークレットからアドレスを取得しますが、彼女は自分が公開した公開鍵に対応する秘密鍵を知っているため、そのアドレスから引き出すための秘密鍵も取得できます。
数学的な説明(なぜステルス・アドレスはこのように機能するのか)
標準的なステルス・アドレスは、同じレベルのセキュリティを維持しながら、より少ない鍵ビット数でより良いパフォーマンスを得るために楕円曲線暗号(ECC) (opens in a new tab)を使用します。しかし、ほとんどの場合、それを無視して通常の算術を使用していると見なすことができます。
誰もが知っている数字、Gがあります。Gを掛けることはできます。しかし、ECCの性質上、Gで割ることは事実上不可能です。イーサリアムにおける公開鍵暗号技術の一般的な仕組みは、秘密鍵*Pprivを使用してトランザクションに署名し、それが公開鍵Ppub = GPpriv*によって検証されるというものです。
アリスは2つの秘密鍵、*KprivとVpriv*を作成します。*Kpriv*はステルス・アドレスからお金を支払うために使用され、*Vprivはアリスに属するアドレスを表示するために使用されます。その後、アリスは公開鍵Kpub = GKprivとVpub = 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)*から得られるアドレス内の資産にアクセスできます。
アリスが「デイブの世界征服キャンペーンサービス」に下請けに出せるように、別々の表示鍵(viewing key)を用意しています。アリスはデイブに公開アドレスを知らせ、より多くのお金が利用可能になったときに彼女に知らせてもらうことには同意しますが、彼にキャンペーン資金を使わせたくはありません。
表示と支払いには別々の鍵が使用されるため、アリスはデイブに*Vprivを渡すことができます。そうすれば、デイブはS = RpubVpriv = GRprivVpriv*を計算し、その方法で公開鍵(Ppub = Kpub+G*hash(S))を取得できます。しかし、*Kpriv*がなければ、デイブは秘密鍵を取得できません。
要約すると、これらがさまざまな参加者によって知られている値です。
| アリス | 公開済み | ビル | デイブ |
|---|---|---|---|
| G | G | G | G |
| Kpriv | - | - | - |
| Vpriv | - | - | Vpriv |
| Kpub = GKpriv | Kpub | Kpub | Kpub |
| Vpub = GVpriv | Vpub | Vpub | Vpub |
| - | - | Rpriv | - |
| Rpub | Rpub | Rpub = GRpriv | Rpub |
| S = RpubVpriv = GRprivVpriv | - | S = RprivVpub = GRprivVpriv | S = 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) |
| 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が必要です。
ステルス・アドレスの動作を確認する
-
必要なツールをインストールします: Rust (opens in a new tab)とNode (opens in a new tab)。
-
GitHubリポジトリをクローンします。
git clone https://github.com/qbzzt/251022-stealth-addresses.git cd 251022-stealth-addresses -
前提条件をインストールし、Rustコードをコンパイルします。
cd src/rust-wasm rustup target add wasm32-unknown-unknown cargo install wasm-pack wasm-pack build --target web -
ウェブサーバーを起動します。
cd ../.. npm install npm run dev -
アプリケーション (opens in a new tab)にアクセスします。このアプリケーションページには2つのフレームがあります。1つはアリスのユーザーインターフェース用で、もう1つはビル用です。2つのフレームは通信しません。便宜上同じページにあるだけです。
-
アリスとして、Generate a Stealth Meta-Address(ステルスメタアドレスの生成)をクリックします。これにより、新しいステルス・アドレスと対応する秘密鍵が表示されます。ステルスメタアドレスをクリップボードにコピーします。
-
ビルとして、新しいステルスメタアドレスを貼り付け、Generate an address(アドレスの生成)をクリックします。これにより、アリスのために資金を提供するアドレスが得られます。
-
アドレスとビルの公開鍵をコピーし、アリスのユーザーインターフェースの「Private key for address generated by Bill」(ビルによって生成されたアドレスの秘密鍵)エリアに貼り付けます。これらのフィールドに入力すると、そのアドレスの資産にアクセスするための秘密鍵が表示されます。
-
オンライン計算機 (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)に似ています。パッケージ情報、依存関係の宣言などが含まれています。
[package]
name = "rust-wasm"
version = "0.1.0"
edition = "2024"
[dependencies]
eth-stealth-addresses = "0.1.0"
hex = "0.4.3"
wasm-bindgen = "0.2.104"
getrandom = { version = "0.2", features = ["js"] }
getrandom (opens in a new tab)パッケージはランダムな値を生成する必要があります。それは純粋なアルゴリズムの手段では実行できません。エントロピーの源として物理的プロセスへのアクセスが必要です。この定義は、実行中のブラウザに要求することでそのエントロピーを取得することを指定しています。
console_error_panic_hook = "0.1.7"
このライブラリ (opens in a new tab)は、WASMコードがパニックを起こして続行できない場合に、より意味のあるエラーメッセージを提供します。
[lib]
crate-type = ["cdylib", "rlib"]
WASMコードを生成するために必要な出力タイプです。
lib.rs
これが実際のRustコードです。
use wasm_bindgen::prelude::*;
RustからWASMパッケージを作成するための定義です。これらはこちら (opens in a new tab)に文書化されています。
use eth_stealth_addresses::{
generate_stealth_meta_address,
generate_stealth_address,
compute_stealth_key
};
eth-stealth-addressesライブラリ (opens in a new tab)から必要な関数です。
use hex::{decode,encode};
Rustは通常、値にバイト配列 (opens in a new tab)([u8; <size>])を使用します。しかし、JavaScriptでは通常、16進数の文字列を使用します。hexライブラリ (opens in a new tab)は、ある表現から別の表現への変換を行ってくれます。
#[wasm_bindgen]
JavaScriptからこの関数を呼び出せるようにWASMバインディングを生成します。
pub fn wasm_generate_stealth_meta_address() -> String {
複数のフィールドを持つオブジェクトを返す最も簡単な方法は、JSON文字列を返すことです。
let (address, spend_private_key, view_private_key) =
generate_stealth_meta_address();
generate_stealth_meta_address (opens in a new tab)は3つのフィールドを返します。
- メタアドレス(KpubとVpub)
- 表示用秘密鍵(Vpriv)
- 支払い用秘密鍵(Kpriv)
タプル (opens in a new tab)構文を使用すると、これらの値を再び分離できます。
format!("{{\"address\":\"{}\",\"view_private_key\":\"{}\",\"spend_private_key\":\"{}\"}}",
encode(address),
encode(view_private_key),
encode(spend_private_key)
)
}
format! (opens in a new tab)マクロを使用して、JSONエンコードされた文字列を生成します。hex::encode (opens in a new tab)を使用して、配列を16進数の文字列に変更します。
fn 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]>になります。
// decodeはResult<Vec<u8>, _>を返す
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()メソッドは、ResultをOptionに変換します。その値は、成功した場合はOk()の値、そうでない場合はNoneになります。最後に、クエスチョンマーク演算子 (opens in a new tab)は、Optionが空の場合、現在の関数を中止してNoneを返します。それ以外の場合は、値をアンラップしてそれを返します(この場合、vecに値を割り当てます)。
これはエラーを処理するための奇妙に複雑な方法のように見えますが、ResultとOptionは、すべてのエラーが何らかの形で確実に処理されるようにします。
if vec.len() != N { return None; }
バイト数が正しくない場合は失敗であり、Noneを返します。
// try_intoはvecを消費し、[u8; N]の作成を試みる
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)は、値を別の型(たとえば、ベクターから配列)に変換します。
Some(array)
}
Rustでは、関数の最後で値を返すときにreturn (opens in a new tab)キーワードを使用する必要はありません。
#[wasm_bindgen]
pub fn wasm_generate_stealth_address(stealth_address: &str) -> Option<String> {
この関数は、*VpubとKpub*の両方を含む公開メタアドレスを受け取ります。ステルス・アドレス、公開する公開鍵(Rpub)、および公開されたアドレスのどれがアリスに属している可能性があるかの識別を高速化する1バイトのスキャン値を返します。
スキャン値は共有シークレット(S = GRprivVpriv)の一部です。この値はアリスが利用でき、これを確認することは、*f(Kpub+G*hash(S))*が公開されたアドレスと等しいかどうかを確認するよりもはるかに高速です。
let (address, r_pub, scan) =
generate_stealth_address(&str_to_array::<66>(stealth_address)?);
ライブラリのgenerate_stealth_address (opens in a new tab)を使用します。
format!("{{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}}",
encode(address),
encode(r_pub),
encode(&[scan])
).into()
}
JSONエンコードされた出力文字列を準備します。
#[wasm_bindgen]
pub fn wasm_compute_stealth_key(
address: &str,
bill_pub_key: &str,
view_private_key: &str,
spend_private_key: &str
) -> Option<String> {
.
.
.
}
この関数は、ライブラリのcompute_stealth_key (opens in a new tab)を使用して、アドレスから引き出すための秘密鍵(Rpriv)を計算します。この計算には以下の値が必要です。
- アドレス(Address=f(Ppub))
- ビルによって生成された公開鍵(Rpub)
- 表示用秘密鍵(Vpriv)
- 支払い用秘密鍵(Kpriv)
#[wasm_bindgen(start)]
#[wasm_bindgen(start)] (opens in a new tab)は、WASMコードが初期化されるときにこの関数が実行されることを指定します。
pub fn main() {
console_error_panic_hook::set_once();
}
このコードは、パニック出力がJavaScriptコンソールに送信されることを指定します。実際の動作を確認するには、アプリケーションを使用して、ビルに無効なメタアドレスを与えます(16進数の数字を1つ変更するだけです)。JavaScriptコンソールに次のエラーが表示されます。
rust_wasm.js:236 panicked at /home/ori/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs:701:9:
assertion `left == right` failed
left: 0
right: 1
その後にスタックトレースが続きます。次に、ビルに有効なメタアドレスを与え、アリスに無効なアドレスまたは無効な公開鍵のいずれかを与えます。次のエラーが表示されます。
rust_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:
keys 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)が含まれています。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import wasm from "vite-plugin-wasm";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), wasm()],
})
2つのViteプラグインが必要です: react (opens in a new tab)とwasm (opens in a new tab)。
App.jsx
このファイルはアプリケーションのメインコンポーネントです。これは、それらのユーザーのユーザーインターフェースであるAliceとBillの2つのコンポーネントを含むコンテナです。WASMに関連する部分は初期化コードです。
import init from './rust-wasm/pkg/rust_wasm.js'
wasm-pack (opens in a new tab)を使用すると、ここで使用する2つのファイルが作成されます。実際のコードを含むwasmファイル(ここではsrc/rust-wasm/pkg/rust_wasm_bg.wasm)と、それを使用するための定義を含むJavaScriptファイル(ここではsrc/rust_wasm/pkg/rust_wasm.js)です。そのJavaScriptファイルのデフォルトエクスポートは、WASMを初期化するために実行する必要があるコードです。
function App() {
.
.
.
useEffect(() => {
const loadWasm = async () => {
try {
await init();
setWasmReady(true)
} catch (err) {
console.error('Error loading wasm:', err)
alert("Wasm error: " + err)
}
}
loadWasm()
}, []
)
useEffectフック (opens in a new tab)を使用すると、状態変数が変更されたときに実行される関数を指定できます。ここでは、状態変数のリストが空([])であるため、この関数はページが読み込まれたときに1回だけ実行されます。
エフェクト関数はすぐに戻る必要があります。WASMのinit(.wasmファイルを読み込む必要があるため時間がかかります)などの非同期コードを使用するには、内部のasync (opens in a new tab)関数を定義し、awaitなしで実行します。
Bill.jsx
これはビルのユーザーインターフェースです。アリスによって提供されたステルスメタアドレスに基づいてアドレスを作成するという、単一のアクションがあります。
import { wasm_generate_stealth_address } from './rust-wasm/pkg/rust_wasm.js'
デフォルトエクスポートに加えて、wasm-packによって生成されたJavaScriptコードは、WASMコード内のすべての関数に対して関数をエクスポートします。
<button onClick={() => {
setPublicAddress(JSON.parse(wasm_generate_stealth_address(stealthMetaAddress)))
}}>
WASM関数を呼び出すには、wasm-packによって作成されたJavaScriptファイルによってエクスポートされた関数を呼び出すだけです。
Alice.jsx
Alice.jsxのコードも同様ですが、アリスには2つのアクションがある点が異なります。
- メタアドレスの生成
- ビルによって公開されたアドレスの秘密鍵の取得
結論
ステルス・アドレスは万能薬ではありません。正しく使用する必要があります。しかし、正しく使用すれば、パブリックブロックチェーン上でプライバシーを実現できます。