以下は筆者の現時点での理解を表すものにすぎません
=====================
最近、主にフロントエンドのEOA walletログインの画面を作りました。blockChain開発に触れ始めた、とも言えるでしょうし、ちょうどEVMが実際にどのように操作されるのかにも触れたと言えます。

1回のwallet login
1回とは言っても、実際にウォレット側で確認が必要なインタラクションは2回あります——1回はページ側から開始されるウォレットアドレス取得の確認、もう1回はサーバー側が認証message/呼び出しドメイン/nonceなど、プロトコル構造に合ったもの(現在使っている構造はEIP-4361を模倣したものです。ただ、実際には個人的にはここはUI/追跡可能性の設計寄りで、実際にはnonceがリクエストの混同を防ぐことを保証できればよい気がします)などのmessageをウォレットに送って検証を要求するものです。
なぜ先にウォレットアドレスを取得してからでないと検証を開始できないのでしょうか?おそらく、ブラウザとウォレットの両方が本質的には信頼できないためです。もし中間のやり取りで直接与えられるデータがすべて信頼できないと考えるなら、ウォレットによって確認されたアドレスだけが信頼できるものとなり、その後で次のウォレット検証へ進める、ということになります。
このウォレットを制御しているのは私だ!
あなたのウォレットアドレスは何ですか?
ここでは実際には、ブラウザとウォレット間のインタラクションで、どのウォレットアドレスを選択してWebサイトに提供する権限を与えるかを確認するためのものです。
このプロジェクトでは実際にwagmiのconnectを使ってウォレットとのインタラクションをリクエストしています。
// wagmi configexport const wagmiConfig = createConfig({ ..., connectors: [injected({ shimDisconnect: true })], ...});
// get wagmi injected connectorconst injectedConnector = useMemo( () => connectors.find((connector) => connector.id === "injected") ?? connectors[0], [connectors] );// connect walletconnect({ connector: injectedConnector });
// auto get wallet connect infoconst { address, chainId, isConnected } = useAccount();ここでもwagmiのuseAccountを通して、ウォレットで確認された後に更新される情報を自動的に取得しています。
では、あなたが言っているこのウォレットはあなたのものですか?
この情報を認証してもらう必要があります
認証すべきアドレスが分かったので、標準プロトコルSIWE(Sign-In with Ethereum)のログイン署名messageを準備できます。だいたい次のような形式のmessageです:
localhost:3000 wants you to sign in with your Ethereum account:0xYourWalletAddress
Sign in to web3walletLogin with this wallet.
URI: http://localhost:3000Version: 1Chain ID: 1Nonce: <server-issued nonce>Issued At: <generated timestamp>その後、このmessageに対してEIP-191の標準化を行い、他の署名や通常のトランザクションと区別できるようにします:
"\x19Ethereum Signed Message:\n" + len(message) + message
実装上は、wagmiのsignMessageAsyncを使ってウォレットへの実際の認証リクエストを行っています。
const { signMessageAsync, isPending: isSigning } = useSignMessage();
// make a siwe messageconst siweMessage = new SiweMessage({ domain: window.location.host, address, statement: "Sign in to web3walletLogin with this wallet.", uri: siteOrigin, version: "1", chainId, nonce });const preparedMessage = siweMessage.prepareMessage();// send sign request to walletconst signature = await signMessageAsync({ message: preparedMessage });これは私の秘密鍵だけが作成できる認証です
このmessageがウォレットに届いて認証を要求するとき、ウォレットは自分の署名によって、以前提供したアドレスに対する制御権を検証できるようにする必要があります。このとき、ウォレットは自分の秘密鍵で先ほど送られてきた情報のhashを暗号化しつつ、この署名がウォレットアドレスとリクエストmessageだけを知っているWebサイトによって検証可能であることも保証する必要があります。ここで必要になるのは当然、一連の数学的変換によってこの検証方法の妥当性を保証することです。もちろん、ここではひとまずwagmiのアルゴリズムを使って返ってきたsignを検証しています(
// use the message (the client send) and sign(the wallet back)const verifyResponse = await fetch("/api/auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: preparedMessage, signature }) });
// sign message verifyconst siwe = new SiweMessage(body.message);const result = await siwe.verify({ signature: body.signature, domain: expectedDomain, nonce: siwe.nonce });
// the fail resultif (!result.success) { return NextResponse.json({ error: "invalid signature" }, { status: 401 }); }実際の検算については?
それを見るには、EOAウォレットの署名で使われるsecp256k1曲線を見る必要があります。EOAウォレットにとっては、おおよそ次のようなものです:
- EOA 秘密鍵:256-bit程度の乱数 d
- 公開鍵:楕円曲線上の点 Q = d * G
- アドレス:keccak256(publicKey) の後ろ20バイト
- 署名:ECDSA over secp256k1
ただ、実は私もこの部分の実際の計算はあまり理解していません(。ただ、署名とmessage hashから得られる公開鍵Qから秘密鍵dへ逆算できないのは、要するに程度の計算量(secp256k1は256bit級の曲線)を要する楕円曲線離散対数問題を解くことに相当するから、ということくらいは分かっています。
このように、最終的に返ってきたsign()と自分が送ったmessage hashから公開鍵をrecoverし、その公開鍵から計算されたアドレスが、以前提供されたアドレスと一致する場合、このウォレットがそのアドレスに対する制御権を持っていることが証明されます。
結び
blockChainの実開発の始まりとしては、だいたいこんな感じでしょう。正直、実際の秘密鍵署名と署名リカバリの実装を見ていると、昔acmを勉強していた頃の感覚が少しありました(
ただ、入口になるプロジェクトとしてはちょうどよく、自分のblockChainへの学習意欲を引き上げてくれた気がします。
この記事が役に立ったときは、ぜひ他の人に共有してください!
一部の情報は古い可能性があります





