ニーモニックでのNostr用の秘密鍵生成
これは「Nostr」のアドベントカレンダー(第一会場)12/23用に書いた記事です。
前日の12/22は電子馬さんのNostrについての覚書、次の12/24はkaijiさんのNostrCMSかHostrの話か、SNS開発するならNostrを使おう的な話のどれかとなっています。
以下は私のNostrタイムラインです。
ニーモニックセンテンス
暗号資産のウォレットではウォレットを管理するためにニーモニックセンテンス(シードフレーズとも呼ばれる)というのが使われているものがある。ニーモニックセンテンスとは例えば以下のような英単語などをランダムに12ワードあるいは24ワード並べたものである。
leader monkey parrot ring guide accident before fence cannon height naive bean
このようなウォレットではこのセンテンスから複数の秘密鍵を生成して資産の送受信を実現している。複数の秘密鍵が生成できるところがミソで、用途に応じてこれらを使い分けるようになっている。例えば受け取り用のアドレスを用途別に用意したり、お釣り用のアドレスとして使ったりする。同じアドレスを使い回すよりも複数のアドレスを使う方が流れが分かりにくくなり、少しだけプライバシーを守りやすくなる。
NostrとAlbyウォレット
ところでNostrではライトニングウォレットのアドレスを設定することで気軽にBitcoinをsat単位で投げることができる。なお、これはNostrではZapと呼んでいる。
そのときに設定するウォレットとしてはWallet of Satoshi(WoSと略されることが多い)が有名であるが、Albyというのも有名である。
このAlbyは上記のニーモニックセンテンスを使った鍵管理手法が取られており、Nostr用の秘密鍵も以前はここから生成することしかできなかった。(今は独自に秘密鍵を設定することも可能となっている。)
ただ、全自動で秘密鍵を生成してくれるため、本当にちゃんと計算して出力されているのかどうか分からなかったのと、せっかく複数生成できるのに1つしか使えないのはなんかもったいない(?)ような気がして自分でも計算して出せるようにしたいなと思っていた。
BIP 39
話を戻して、ニーモニックフレーズで使われる英単語は何でもよいというわけではなく、BIP 39という規格で2048種類の単語が決められている。(BIP = Bitcoin Improvement Proposal)
ここに Wordlists というのがあり、英単語や他の言語での単語が定義されている。例えば英単語の場合は以下のようになっている。
1 abandon
2 ability
3 able
...
2046 zero
2047 zone
2048 zoo
1〜2048という数値は11ビットで表現することができるため、12ワードの場合では132ビット、24ワードだと264ビットで表現できる。これをランダムな大きな整数として用いることで別の乱数を生成し、それを秘密鍵として使っているのである。ただし、一部はエラーチェック用として使われるため、前者は128ビット、後者は256ビットが実際に乱数として使われる部分となる。
BIP 32
この乱数から複数の乱数、つまり秘密鍵を生成する方法はBIP 32で決められている。
このWiki(リンク先を見てね)のMaster key generationの図が分かりやすい。ヒエラルキー構造となっており、右端のDepth = 3のところが実際のアドレスとして使われる。
SLIP-0044
そのヒエラルキーの上位部分で暗号資産などの種類別に分かれている。これを定義したものがBIP 44とSLIP-0044である。(SLIP = SatoshiLabs Improvement Proposals)
Coin typeのところが0だと、有名なBitcoin用となり、22だとMonacoin用となるといった具合である。そして1237にNostrがあるのであった。なお、BIP 44の方ではBitcoinとそれのtestnetしか定義されておらず、他のタイプはSLIP-0044で定義してねというスタンスのようだ。
NIP-06
ここでようやくNostrに辿り着いた。その定義がNIP-06である。(NIP = Nostr Implementation Possibilities)
ちなみに日本向けのリポジトリは以下となっているが、2023/12/22現在ではまだ翻訳されていない。(お前が翻訳しろ、と言われそうだが。)
Test vectorsというのが書かれており、これを使って計算が合っているかテストできる。冒頭に書いたleader monkey ...
のセンテンスはこれをそのまま書いたものだ。
そしてそのヒエラルキーでの位置を示すのが m/44'/1237'/<account>'/0/0
である。<account>
部分を0にしてm/44'/1237'/0'/0/0
の結果がTest vectorに書かれている。
mnemonic: leader monkey parrot ring guide accident before fence cannon height naive bean
private key (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a
nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp
public key (hex): 17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917
npub: npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l70ja5mhv8jjumytsd2x7nu
private key (hex)の部分が生のNostr秘密鍵で、256ビットの整数となっている。ここはエラーチェックのビットはない。nsecのところはユーザーが通常使う場合の秘密鍵表記でエラーチェック付きの文字列となっている。
なお、12ワードだと128ビットの長さであるが、そこから256ビットの乱数を生成しているので覚える/記録する量としては半分となるのがメリットであるが、強度としては24ワード使った方がよいと考えられる。
Go言語での実装
さて、これをプログラムで計算したいのであるが、Go言語ではすでにライブラリが提供されていた。
- GenerateSeedWords: 24ワードのニーモニックフレーズをランダムに生成する
- SeedFromWords: ニーモニックフレーズを256ビットなどのバイト列に変換する
- PrivateKeyFromSeed: そのバイト列から秘密鍵を生成する(
m/44'/1237'/0'/0/0
固定)
ということで無事m/44'/1237'/0'/0/0
の秘密鍵が生成できるところまで確認できた。実際にAlbyで使っていたニーモニックフレーズを使い、同じ秘密鍵が表示されることが確認できたため、最初の疑問点についてはクリアとなった。
複数の秘密鍵
m/44'/1237'/0'/0/0
だけでなく、m/44'/1237'/0'/0/1
やm/44'/1237'/0'/1/0
なども生成したい場合はどうすればよいのか。それはもう PrivateKeyFromSeed を改造したバージョンの関数を作るしかない。derivationPath の最後のbip32.FirstHardenedChild + 0
, 0
, 0
を変えればよいだけだ。Go言語をある程度ご存じであれば難しいことではないかと思うので、興味があれば試してみてはどうだろうか。
Nostrでのアカウント(?)使い分け
NostrではSNS等で一般的なアカウントというのはなく、ただただ秘密鍵があるだけというシンプルな設計となっている。そのため、いわゆるアカウント転生をしたい場合はこれまで使っていた秘密鍵を使わなくして、新しい秘密鍵を使うといった手法しかない。各投稿は秘密鍵によって署名されるため、移行先のアドレスを投稿に含めておけば他者からは移行したということが確認できる。ただこれは人間系で行う手順であり、機械で判別できるような移行したことを示すようなプロトコルというのは現状では存在しない。
あと、複数の秘密鍵を使い分けることもでるが各種クライアントはそういう用途では設計されていないケースが多く、結構面倒なことになる。bot用であればスクリプト等で読み込ませるため割と切り替えは簡単であるが、Webクライアントやスマートフォン用のアプリなどでは難しくなる。
npubマイニング
最後に余談となるが、BitcoinなどではVanity Addressといって、好みの文字列がアドレスに入っているものをひたすら探し出すということが行われることがある。Nostrでも同様に公開鍵であるnpub1の後に続く文字列がよさげなものを探したりもできる。これをnpubマイニングと呼んだりしている。
NIP-06により、非常にたくさんのアドレスが生成できるので、ここからnpubマイニングすることが可能である。ただし、さきほどのnip06.goをちょっと改造したぐらいだと遅すぎてなかなか長い文字列を探し当てることは難しい。また、普通のマイニングと比べるとHMACなどの処理を通す必要があるため、どうしても速度は遅くなる。それでも工夫することでだいぶ高速化することは可能であった。関数を時前のコードにインライン展開して、共通部分などを探してループの外に出すなどの作業が必要であった。ソースコードは公開できる状態であるが引用したコードのライセンス表記などが整理できていないため、そのあたりを整理して後日公開しようかと考えている。
以下は見つけた公開鍵の一部。
- npub10hachlgf27zf9p42wsgt9757h5lh0377wmvdjuytg7un70h8zefqvts756 (ohachigeに近い見た目)
- npub1ztmynk6vl44v8rrfp0rm686lh97n6hf4vf9jfj406q6xdn35mchqqnrxc0 (ztmy、何卒何卒です)
- npub1ya9zapuhkhymzgngygldc6q06kq4utyyeq6ajmqxml3kkl205dfsy7ht8x (ya9zap...って何?)
感想など
色々やってみましたが、今のところ複数の秘密鍵を使い分けるユースケースがあまり浮かんできていません。マスターとなるニーモニックフレーズを1つ覚えておけばなんとかなる、nsecが漏れてもマスターには被害がないといったメリットもあるので役に立つこともあるのではないかなといった感じです。自分としては移行が大変なので最初の秘密鍵を使い続けておりますが、今後別のやつに切り替えることもあるかもしれません。そのときはこの手法を使おうかなと考えております。
ここまで読んでいただき、ありがとうございました。今後ともNostrをよろしくね。
Discussion