Rust で WebAssembly を書くと、ファイルがでかい。いつものよく聞く話である。
本当にそうなのか、自作の heika(Symbol の Rust のライブラリ)を使って、好きな文字で始まるアドレスを探すページを作って測ってみた。ついでに、見た目は Claude に丸投げした。シンギュラリティってすばらしい✨。
ソースはここにある。
作ったもの
NA や NARUTO のような先頭の文字を入れて「探す」を押すと、鍵を作ってはアドレスにして比べるのを、見つかるまで繰り返す。
- 鍵はページの中で作り、どこにも送らない。保存もしない
- CPU のコアの数だけ Web Worker を立てて、並列に探す
- 試した数、速さ、見つかるまでの見込み、いま見つかっているはずの確率を出す
スマホでも、NARUTO で始まるアドレスが1分もかからずに見つかった。いいじゃなぁい🎉
大きさ
先に結果から。公開しているもの(trunk build --release、wasm-opt 済み)の大きさである。
| ファイル | そのまま | gzip |
|---|---|---|
| 画面の WASM(Leptos 込み) | 313KB | 126KB |
| 画面の JS | 38KB | 7KB |
| Worker の WASM(heika で探すところ) | 98KB | 57KB |
| Worker の JS | 16KB | 4KB |
| CSS | 5KB | 2KB |
全部で gzip して約 200KB。そのうち、heika で鍵を作ってアドレスにするところは約 60KB で、残りのほとんどは画面を作る Leptos である。
画像1枚くらいなので、さほど「でかい」とは言えないと思う。ケータイ小説の時代じゃあるまいに。
段階ごとの大きさ
heika で探すところだけを、段階ごとに測るとこうなる。
opt-level = 3 |
opt-level = "s" |
opt-level = "z" |
|
|---|---|---|---|
| cargo のビルドそのまま | 130KB | 126KB | 144KB |
| wasm-bindgen のあと | 110KB | 109KB | 125KB |
| wasm-opt のあと | 96KB | 84KB | 89KB |
| gzip | 56KB | 53KB | 55KB |
"s" や "z" にしても、wasm-opt と gzip のあとでは数 KB しか変わらなかった。鍵を作る速さのほうが大事なので、opt-level = 3 のままにしている。
なかなか効果あったのは、リリースの設定と wasm-opt である。
[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"
opt-level = 3
中身を見ると、約 34KB はコードではなくデータだった。たぶんだけどどうせ、Curve25519 の計算を速くするための表である。コードそのものは 60KB くらいになる。
速さ
Symbol のアドレスは、秘密鍵 → 公開鍵 → SHA3-256 → RIPEMD-160 → Base32 で作る。一番重いのは、秘密鍵から公開鍵を作る楕円曲線の計算である。
ほかのチェーンの vanitygen には、「前の鍵に1足して、公開鍵にも点を1つ足す」という近道を使うものがある。楕円曲線の掛け算を毎回しなくて済むので、ずっと速い。ところが Symbol(Ed25519)の秘密鍵は、使う前に SHA-512 でハッシュする決まりになっている。秘密鍵に1足すと、ハッシュしたあとの値はまったく別物になるので、この近道は使えない。毎回まじめに掛け算をするしかない。
なので、速くするには数で押すしかない。Web Worker をコアの数(navigator.hardwareConcurrency)だけ立てて、それぞれに探させている。Worker どうしでやりとりすることはないので、メモリは共有しない。共有しなければ、GitHub Pages では設定できない COOP/COEP のヘッダーもいらない。
各 Worker は、500 個試すごとに画面に数を送り、見つかったら結果を送る。止めるときは、画面が Worker ごと terminate() する。
ハマったところ
「探す」を押しても始まらない
最初の版は、画面が Worker を作ってすぐに先頭の文字を送っていた。ところが Worker は、WASM を非同期で読み終えてから、ようやくメッセージを受け取る準備をする。準備ができる前に届いたメッセージは捨てられるので、どの Worker も何も受け取らずに待ち続けていた。カウンタが動かないので「遅いなー」と思っていたら、そもそも始まっていなかった。
Worker の準備ができたら "ready" を送り、画面はそれを待ってから先頭の文字を渡すようにして直った。
Worker の初期化の警告
trunk の data-loader-shim が作る Worker の読み込み用スクリプトは、wasm-bindgen の古い呼び方で初期化するので、警告が出る。ゆーて3行なのでと Claude がぱぱっと書いてくれた。
importScripts("./worker.js");
wasm_bindgen({ module_or_path: "./worker_bg.wasm" });
どこに置いても動くように
GitHub Pages では /heika/examples/web-vanitygen/ の下に置くので、Trunk.toml でパスを相対にしている。
[build]
public_url = "./"
見た目は Claude に丸投げした
今どきデザインはもはや身だしなみみたいなものなので、見た目は Claude に丸投げした。
伝えたのは、だいたいこれだけである。
- すっごい薄みずいろとふつうに薄みずいろの、細い縦のしましま
- おしゃれで落ち着きのある、かわいい感じ
- ダークモードには合わせなくていい
出てきたのは、しましまの背景に、少し透けた白い角丸のカード、丸ゴシック、押すと沈むボタン、見つかったときにぽんと出る点線の枠、という見た目だった。スマホの幅では、数字のカードが縦に並ぶ。頼んでいないのに、見つかっているはずの確率をしましまのバーで出すところまで作ってくれた。
口を出したのは、しまの太さくらいである(4px から 6px にした)。
それくらいしか突っ込みどころが無くてくやしい……私いらない子になっちゃう……捨てないでくれClaude Code……
TypeScript の SDK と比べて
Symbol には公式の JavaScript/TypeScript の SDK(symbol-sdk v3)がある。実は v3 も、署名などの暗号の計算は WASM(symbol-crypto-wasm)でやっている。WASM を使うこと自体は同じである。
違いは組み立て方にある。symbol-sdk は Node.js でもブラウザでも動くように、WASM を読み込むところが Node 用(symbol-crypto-wasm-node)とブラウザ用(symbol-crypto-wasm-web)に分かれている。バンドラーにそのまま任せると Node 用が選ばれるので、ブラウザ用に差し替える設定が要る。
2023 年の symbol-sdk 3.1.0 のころの記事では、Next.js(webpack)で NormalModuleReplacementPlugin を使って Node 用をブラウザ用に差し替え、experiments で asyncWebAssembly などを有効にして動かしている。同じ記事には、Vue や Nuxt ではうまくいかず、Next.js なら動くと教わって試した、という経緯も書かれている。
Turbopack になった今の Next.js では、そのまま使うと Module not found: Can't resolve 'fs' で止まる。next.config の turbopack.resolveAlias で Node 用をブラウザ用に差し替え、依存の bitcore 系をサーバー側で external にすれば動く、という記事がある。
バンドラーが変わるたびに、差し替えの書き方も変わっている。
どちらも、いろいろな環境で動かすための作りなので、悪いというわけではない。一度設定してしまえば、ちゃんと動く。
一方で、今回のように全部 Rust で書くと、そもそもこの手の設定がいらない。trunk が Rust のビルド、wasm-bindgen、wasm-opt までやってくれて、JS のバンドラーの設定は1行も書いていない。画面も Worker も鍵の計算も、全部 Rust の型でつながっているので、もうすんごい楽。
そのかわり、画面も Rust(Leptos)で書くことになるし、npm のパッケージをそのまま使うこともできない。既存の React や Next.js のアプリに組み込みたいなら、symbol-sdk のほうが素直だと思う。素直だとは思うけど、ああいう試行錯誤を自力でやりたいかは別なので、その折は Claude に一任もとい丸投げしちゃいたいな〜👀✨チラッ。
おわりに
Rust の WASM は、案外気をつければでかくない。heika の部分は gzip で約 60KB だった。
Leptos は SPA でもないページには大げさかと思ったが、Worker から数が届くたびにいくつもの表示を書き換えるので、シグナルで済むのは楽だった。その分の大きさは、上の表のとおりである。
公開は、タグを打つと GitHub Actions がビルドして GitHub Pages に出すようにした。
