今回は、ECDSAによる署名作成と承認について、実際に操作しながら説明していきたいと思います。
私の勉強も兼ねて書いた記事ですので、間違い等あれば色々教えてくださいね。
ECDSAによる署名作成と検証の概要
ECDSAによる署名作成と検証は、仕様によって実装の仕方や注意点が異なります。
一般的に使用されているECDSAの署名と検証をサクッと説明すると以下の通りです。
- 署名するコンテンツに秘密鍵(Private Key)を用いて署名する(署名作成)
- 署名したコンテンツと署名を用いて演算すると、公開鍵(アドレス)が導出できる。
- 署名した人と公開鍵の持ち主が一緒かどうかを検証する。(承認)
一般的に使用されているECDSAの検証では、ブロックチェーンを操作する人が署名をする人でした。
一方で、今回説明するECDSAの署名と検証は、以下の通りです。
- 署名するコンテンツに事前に秘密鍵を用いて署名する(署名作成)
- 署名に用いた公開鍵をブロックチェーンに保存する。
- 署名したコンテンツと事前に作成した署名を演算すると、公開鍵(アドレス)が導出できる。
- 導出された公開鍵と事前に保存された公開鍵が一緒かどうかを検証する。(承認)
encodeとencode packについて
色んなプロジェクトのコントラクトを見ていたら、コントラクト内におけるdigest(署名したコンテンツ)の作り方が異なることに気づきました。
それがencodeとencode packの2種類です。
このエンコードの仕方が異なると、事前に作成する署名の作り方も異なります。
abi.encodePackedは、32バイトにパディングされないため、エンコードしたい値に32バイト以下のものを使用すると衝突が発生する可能性があります。
そのため、今回のように衝突を避ける必要があって、特にkeccak256を使用する際は、abi.encodeを使用する必要があります。
structについて
ECDSAの検証に用いる署名は、下記のような構造体を取ってるものが大半でした。
struct signature {
bytes32 r;
bytes32 s;
uint8 v;
}
でも、この署名ってストレージに保存する事はなく基本的にメモリーとして使用すると思うんです。
それであればuint8ではなくuint256の方がガス効率が良いと思うのです。
仮に下のような構造体を考えた場合もメモリーとして使用するのであれば、上記同様に全てuint256の方がガス効率が良いと思うんですよね。
struct signature {
bytes32 r;
bytes32 s;
uint8 v;
uint8 x;
uint8 y;
}
そもそもメモリーとして使うのであれば、型や可読性を省けば、構造体にする理由って何もない気がするんですが、どうなんでしょうか。
有識者の方、是非とも教えていただけないでしょうか。ドキュメント投げつけてくれるだけでも泣いて喜びます
ECDSAによるAllowListの実装について
では、本題に行きたいと思います。
今回はNFTプロジェクトのAllowlistを題材に説明していきたいと思います。
チェックするデータとしては、セールフェーズ、アドレスとそのアドレスに許可された枚数になります。
なお、ECDSAが分かりやすいように、体験できるページを作りました。
これを操作しながら読み進めてもらえると分かりやすいかと思います。
https://eggdragons.com/dapps/ecdsa
さらっと作ったので、ちょっとだけ挙動がおかしいときがあります。
今回作成するECDSAの全体像は、こちらになります。
AllowList
まずは、Allowlistを作っていきます。
今回のケースでは、セールフェーズがフリーミント、プレセール、セールの三段階あるとします。
各セールごとにアドレスと枚数を格納していきます。
今回のテストサイトでは、必ずフリーミントフェーズに、最低1つ以上のデータブロックを入力してください。
ランダムボタンを押すと、自動的にランダムで生成されたアドレスと枚数が入力されます。
準備ができたら、『Set Allowlist』ボタンを押してください。
Message
Allowlistの情報を元に署名するコンテンツ(message)が出来上がります。
ここで使用しているコードはこちらになります。
const hashMessage = keccak256(
toBuffer(
ethers.utils.defaultAbiCoder.encode(
["address", "uint256", "uint256"],
[address, quantity, salePhase]
)
)
)
Sing
次に署名するアドレスを作っていきます。
プライベートキーはランダムで作成することができますので、必ずそれを使うようにしてくださいね!
また、プライベートキーが定まれば、プライベートキーからアドレスを算出することができます。
その後、署名したいコンテンツ(message)を選択してください。
const privateKey = crypto.randomBytes(32);
const address = ethers.utils.getAddress(privateToAddress(privateKey).toString("hex"));
Signature
署名したいコンテンツにプライベートキーを用いて、署名(signature)が作成されました。
const bufferSignature = ecsign(hashMessage, signerPrivateKey);
const signature = {
r: bufferToHex(bufferSignature.r),
s: bufferToHex(bufferSignature.s),
v: bufferSignature.v,
};
Argument
ここでは、実際にdappsからブロックチェーンにおくる変数を入力してECDSAの検証テストをすることができます。
Argumentは、ECDSAの検証に必要な変数を入力する場所になっています。
実装する際は、SalePhaseはコントラクトから、アドレスはmsgから取得するのが良いかな?って思ってます。
Setボタンを押すことで、最初に入力したデータブロックに紐づいたデータが自動で入力されます。
試しにSet freemintAを押してください。
すると、自動的に引数が入力されると思います。
Check at contract
ここでは、実際にブロックチェーンを通してテストすることができます。
Check Contractを押すと、ここで作ったsigner addressとArgumentの値がコントラクトに送られます。
その後、OpenzepplinのECDSAで検証された結果が返ってきます。
※read contractになっているのでガス代はかかりません。
サイト内での検証も盛り込みたかったのですが、今はまだ未実装です。
Jump to etherscan
Jump to etherscanのボタンを押してもらうと、実際に使用しているコントラクトをEtherScanで見ることが出来ます。
NFTプロジェクトに採用する際のサンプルもまだ用意できていません。時間が出来たら更新しますね。
合わせてチェックしてみてね!