Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

アウトカムの最大化へ。開発組織の変遷と向き合い方

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyでプロダクトマネージャーをしている@kaa_a_zuです。

開発組織は、ITサービスを提供している企業にとって「エンジン的な存在」であり、プロジェクトや各メンバーの生産性に大きな影響を及ぼします。そんなエンジンは、事業の成長に伴って柔軟に変化させていく必要があると考えています。

Gaudiyでも、これまでに数度、開発組織のアップデートを重ねてきました。今のエンジンは、「仕様策定から開発、リリース、効果測定までをひとつのチームが行い、そのチームメンバー全員が責任を持ってアウトカムの最大化を図ることができる」ものになっています。

そこで今回は、2022年7月に行われた開発組織の体制移行を中心に、これまでの開発組織の変遷から今回の体制に至った背景、体制移行にあたって考慮した点などについて書こうと思います。

事業や人員拡大に伴い開発組織をアップデートする必要性を感じている方や、メンバーが最大限の能力を発揮できていないという課題感のあるエンジニアの方のご参考になれば幸いです。

※少し長いので、現在の開発組織の設計・運用だけ知りたい方は、「3. 開発組織体制の変遷」の終わりまで読み飛ばしてください。

1. Gaudiyのプロダクトと技術

まずはじめに、Gaudiyについて簡単にご紹介します。何をやっているかわかりやすく言うと、Web3のコミュニティSaaSをつくっている企業です。

Web3時代のファンプラットフォーム「Gaudiy Fanlink」というプロダクトを、エンタメIP(知的財産コンテンツ)を有する企業に提供して、クライアントの方々と一緒にファンの熱量を最大化させるコミュニティ運営に日々勤しんでいます。

service.gaudiy.com

Gaudiyが提供しているのはこのワンプロダクトですが、各IPファンの熱量を最大化させるための手段として NFT(代替不可能なトークン)やDID(分散型ID)といったブロックチェーン技術を利用しています。

とはいっても、下図のように、いわゆるWeb2の企業が使っている技術と大きな差はあるわけではありません。

Gaudiyが利用している技術スタックについての説明です。Frontendでは Next.js Typescript, ApolloClientが使われていてVercelにホスティングされてます。BackendではKotlinとGolangが使われていてCloudRunにホスティングされています。FrontendとBackendの間にBFF層として、TypescriptとApolloServerがあります。BlockchainはEthreumとPolygonが使われています。
Gaudiy Fanlinkを構成している技術スタック

よく「ブロックチェーンのことをよく知らなくても大丈夫ですか?」と聞かれますが、Gaudiyでは多くのWeb2企業出身のエンジニアが活躍しています。

techblog.gaudiy.com

2. Gaudiyの開発組織

そんなGaudiyですが、昨今のWeb3やNFTへの注目もあいまって、下図のようにエンジニアメンバーがどんどん増えています。

先日、シリーズBの資金調達が完了したこともあり、今後さらにエンジニア採用を加速させていく予定です。

エンジニアの採用計画についての説明です。2018年は2人、2020年は9人、2022年は19人となっています。そして2023年に50人、2024年に100人を予定しています。
2020年時点では9人だったエンジニアが現在では2倍以上になっている

このような事業フェーズの前進や組織拡大に伴って、開発組織を新たな形に移行する必要がでてきました。

3. 開発組織体制の変遷

ここで一度、創業期からの開発組織の体制について振り返ります。

Gaudiyではこれまでに二度、下図のように組織をアップデートしてきました。それぞれ、どのような背景や意図で組織体制を変えてきたのかについて説明します。

Gaudiyの開発組織の変遷(人数はPO・PM・Designer含む)

3-1. 【第1フェーズ】創業〜2021年5月

がむしゃらにプロダクトをつくる開発組織

<特徴>

  • Engineerは必要最小限のスコープである「プロジェクト」という単位にわかれる
  • POとDesignerが仕様をつくり、各プロジェクトでそれらを開発する

この頃は、まさに0→1のフェーズ。創業初期はブロックチェーンの顕在的なニーズが世の中にあまりなく、先行事例も少なかったため、PMFをめざす上で重要だったのは「いかに関係者に価値を理解してもらえるか」でした。

そこでまずは、クライアント企業に深く入り込み、個別の状況に対して理解と共感を深め、エンタメ企業に共通するペインを見つけながら、それを解決するプロダクトをつくる。そのプロセスが必要であり、戦略的に「Sales-Led Growth(セールス主導の成長)」でのプロダクト開発をしていました。

プロダクトが小さくメンバーも少なかったため、開発スタイルとしては、POとメンバーが密にコミュニケーションを取りながら、アウトプットを重視する形です。不確実な課題に対しての突破力が高いメンバーで構成されており、早いスピードで確実にアウトプットを積み上げていました

この開発体制によって、複数のクライアント企業様と一緒にコミュニティを提供できたことで、プロダクトの大枠をつくることができました。同時に、後に想定される「SaaSとしての急成長に備えた地盤を整えること」が課題となってきました。

3-2. 【第2フェーズ】2021年5月〜2022年7月

その課題に対応するために、2021年5月頃から、「継続的にコミュニティの機能をつくるExperienceチーム」「決済システムや他のプラットフォームと連携するためのSDK、IDシステムなどの基盤をつくるPlatformチーム」の2チームに分かれました。

地盤を整えるための開発組織

<特徴>

  • Engineerは機能をつくるチーム(Frontend Engineer中心)と、基盤をつくるチーム(Backend Engineer中心)にわかれる
  • POとDesignerが仕様をつくり、各チームでそれらを開発する
  • 各チームの開発スケジュールを管理するProject Managerのポジションが生まれる

この時代も、継続して戦略的なSales-Led Growthでの開発をしていました。セールスのニーズにあわせた開発をしていたため、運用するコミュニティの数も着実に増えていきました。また、増えてきたメンバーをまとめるため、プロジェクトマネージャーのポジションが新設され、個の集合からチーム感が強くなっていきました。

ここで課題となってきたのが、「SaaSとしての汎用性」と「1つ1つの機能の質」です。

Product-Led Growth(プロダクト主導の成長)ではなく、スピードとアウトプットを重視したSales-Led Growthでの開発を戦略的に採用してきたために、すべてのコミュニティに汎用的な最大のアウトカムは出せていない状態でした。

また他にも、以下のような課題が浮き彫りになってきました。

  • Designerのリソース不足で、仕様策定者のPOとDesignerの間で機能の細部要件が定まらず、開発に手戻りが生まれる
  • メンバーの増加に伴い、コミュニケーションが複雑になり、よりトップダウン開発の色が濃くなってしまった
  • なぜその仕様でその機能を作るのかの共通認識をメンバー全員が取れていない

3-3. 【第3フェーズ】2022年7月〜現在

こうした課題に対応するために、個々のチームが仕様策定からユーザーへの提供までを一貫して担い、それらが並存する組織体制に移行しました。これが現在の開発組織体制です。

アウトカムを最大化する開発組織

<特徴>

  • 仕様策定からユーザーに体験を届けるまでを一貫して担うため、EngineerはFrontend、Backendという分かれ方はせず、クロスファンクショナルなチーム構成になる。
  • POが事業進捗をふまえた開発の優先順位を決定し、PdMを中心にチーム全員で仕様をつくり、それらを開発/提供/効果検証する。
  • 特定の開発領域をもたないため、チーム名のつけかたを「寿司ネタ」にする。(社内公募で決めました。)

ひとつのプロダクトに対して、2022年9月時点で開発チームは3つです。現在は、横断的なチームは存在していません。

Gaudiyは、2022年の5月にシリーズBの1st、8月に2ndの調達をしました。次はいよいよシリーズCになります。

次のマイルストーンとして、私たちは「シリーズCでユニコーン企業になる」を本気で目指しており、その目標達成のために今までの開発の仕方を大きく変更する必要が出てきました。

ここからは、どんなことを考えて現在の開発組織を設計したか、そしてどのように組織運営をしているかについてお伝えしていきます。

4. 今回の組織体制移行において特に意識したこと

メンバーが増え、組織が拡大するにつれて、組織体制を変えることにはリスクが生じます。社員数が50人に満たないGaudiyでさえ、私はこの難易度が高いと感じていました。大前提、すべての理想を叶えられる組織体制は存在しないと思っています。

また基本的に、恩恵を享受する時には痛みも伴います。このようなトレードオフの関係になり得る重要な意思決定においては、特に意識しないといけないことが2つあると考えています。

1つ目に、絶対に譲れないコアな考え方を決めること、2つ目に関わるメンバーの納得感を得ることです。それぞれ具体的にお伝えします。

4-1. 譲れないコアな考え方を決める

今回の開発組織の体制移行に際して、以下3つをコアな考え方として定めました。

  1. アウトカムに責任が持てるチームであること
  2. すぐに新たなポジションは作らないこと
  3. 文化が継承されること

これらについて、順番に説明します。

4-1-1. アウトカムに責任が持てるチームであること

以前までは、アウトカムよりもアウトプットを重視した開発をしていました。ここからのフェーズでは多少のスピードを犠牲にしてでも、アウトカムを重視した開発をしていく必要があります。

ユーザーに対するアウトカムを最大化するためには、ユーザーが本当にやりたいことを探り、提供し、改善していくことが必要だと考えています。つまり、Whyを意識しながら仕様策定や微小な軌道修正を繰り返すことのできる開発組織の体制が必要です。

そのためには、POが細部まで仕様策定したものをチームが開発するといった社内受託感をなくし、開発チームが能動的にそれらについて考えることができ、アジリティ高く、タスク完結性を持って遂行可能な状況をつくる必要がありました。

  • アジリティ…企業のビジョンと一致している方向に、各々が意思決定ができている度合い
  • タスク完結性…各々が作業工程の一部ではなく最初から最後までに取り組めること

この状況を作るために、変更したのは3つです。

  1. 今までのPjM(Project Manager)というポジションをPdM(Product Manager)に変え、チーム内での最終的な意思決定の責任者に。
  2. 各チームが開発するプロダクトバックログを決める会議体を新設。
  3. 仕様策定からリリースまでを1つのチームだけで一貫して行えるクロスファンクショナルなメンバー構成に変更。

図示すると下図のようになります。

能動的に仕様部分を考えることができ、アジリティ高く、タスク完結性を持って遂行できる開発組織体制

当然かもしれませんが、タスク完結性がなければその決定に対してチームや個人が感じる責任は小さくなり、考えるプロセスにおいて他責感を生んでしまいます。

新しい体制は、チームがタスク完結性を持つ構造にしているため、否が応でも決定に対しての責任を持たなくてはいけない構造です。結果的に、アウトカムについてチームや個人がより意識をするため、ユーザーにとってより良いものが提供できるという状況が生まれています。

4-1-2. すぐに新たなポジションは作らないこと

組織体制を変えるタイミングでやってしまいがちなことのひとつが、新たなポジションを作ることだと思っています。

特定の課題を解決することの単純解が、ポジションの新設です。今回の議論でも「QAエンジニア」「Engineering Manager」「スクラムマスター」などの新たなポジションをつくりたいという声が上がりました。

結論、今回の体制移行に伴って新たなポジションをつくることはしませんでした。なぜかというと、ポジションをつくることは簡単でも、減らすことはとても難しいと考えているからです。

一度ポジションをつくってしまうと、そこにアサインされた人が「今まで持っていた裁量を失うこと」を恐れたり、アサインから外れることがモチベーションを落とす要因にもなり得ます。そのため、ポジションの新設による課題解決は、安易に選択すべきではないという結論にいたりました。

この辺りの「裁量とモチベーションの関係」については、米・ハーバード大学で心理学の教授をされている ダニエル・ギルバート氏 の著書で分かりやすく説明されています。

www.amazon.co.jp

改めてになりますが、ポジションをつくりたいという議論が出てきた時には、第一に仕組みで解決することができないかを模索し、試行錯誤をした後で本当に必要だったらポジションを新しく設ける、という流れが大事だと思っています。

4-1-3. 文化が継承されること

スタートアップの成長過程でよく言われているのが「30人の壁」「50人の壁」「100人の壁」です。

これらは組織規模がおおよそその人数になった時に、組織に対する満足度が低下し、結果として離職率が高まることを指します。これらの障壁の原因は、「文化の希薄化」と「1人当たりが持つ影響力の低下」だと言われています。

創業まもない少人数の時はプロダクトも小さく、ステークホルダーも少ないため、全員で意思決定をしてつくることに必死になります。その後、企業が有名になり組織が拡大していくと、途中で入社してきた経験豊富な優秀な人中心の構造になり古参メンバーがモヤを感じてしまったり、今まで知れていたことが知れなくなったり、以前と比べて組織に対する影響力が小さくなってしまいます。

Gaudiyの場合、まだエンジニアメンバーが20人未満だということもあり、そのまま進めることも可能でした。しかしながら、拡大スピードを考えると、早めに次の一手を打たないといけないような状況でした。

そのために各チームに企業文化を持っていて浸透させることができる古参メンバーが在籍する構成にしたり、全ての情報をアクセス可能にしたり、チーム間でのコミュニケーションが取れる(極論、取らなければいけない)開発の流れにしています。

このあたりは、開発組織だけでなく、GaudiyのバリューのひとつでもあるDAO(Decentralized Autonomous Organization:自律分散型組織)がベースになっています。以下の記事で詳しく説明しているので、よければご覧ください。

seleck.cc

note.com

4-2. 関わるメンバーの納得感を得る

組織体制の変更は、今まで慣れ親しんだ開発手法や今後のキャリアにも影響が出ます。そして、メンバーはロボットではなく、感情をもつ "人" であり、その誰しもがコンフォートゾーン(居心地の良い環境)を探してしまうものです。

その前提のもと、今回の移行に際しては、全開発メンバーにヒアリングをして意見を聞き、できる限りのwillを反映させました

組織体制の変更について考えている時は、どうしても視点が組織に向いてしまいがちですが、その視点だけではロマン(表層)的にしか描くことができません。チームや個人といった現場(本質)的視点を入れることで、より現実的で納得感が得られる体制を描けるのだなと学びました。

実際に、全員と数回の1on1を行うことはかなり大変でした。ただ、これから強い組織が成長するためには、今までのGaudiyを築いてきたメンバーがとにかく大切です。そのメンバーが持つモヤをなくし、組織基盤となる文化や考え方を新たなメンバーに浸透させることは、1on1を行う大変さに比べて何十、何百倍も大事だと思っていました。

それらを経て、最終的には社内AMA(Ask Me Anything)も開き、納得感を得て体制移行を完了できたのではないかと思っています。

5. 組織をワークさせるための工夫

すでに3ヶ月弱、この開発組織体制で動いていますが、実際にうまくワークしているのか?について最後に触れておきます。

結論、課題は山積みですが、今のところうまくワークしていると感じています。今までは開発組織の体制づくりに着目して説明をしてきましたが、ここからはこの組織体制における運用の工夫についてご紹介します。

5-1. 定期的に組織課題を改善するMTGを開く

組織課題は業務を重ねるにつれて顕在化し、日々更新されていきます。そこで、約2週間に1回の頻度で組織課題を改善をするミーティングを開いています。

この会議体の責任者はPdMですが、実際にそれぞれの解決に責任を持って旗を振る人は、その課題を挙げた人になることが多いです。今後も継続的にこのミーティングを行っていく必要があるなと感じています。

組織課題の管理DB

5-2. いつどこのチームが何に着手するのかを可視化する

ワンプロダクトを複数チームで開発する体制なので、他のチームが取り組んでいる開発領域を知り、お互いにコミュニケーションを取りながら開発をしていく必要があります。

そこで、他のチームがやっていること/今後やることをタイムライン形式で表示し、簡単に知ることができる状態にしています。

タイムライン形式でのプロダクトバックログ

また下図のように、弊社のプロダクトバックログには、今後開発をしたい機能(正確にはアウトカムベースのストーリー)があります。

これらをカンバン形式でも見ることができるため、開発メンバーが将来的にどういった機能が載るのかをイメージしながら、現在の開発ができるような状態にしています。(= 拡張性の考慮)

カンバン形式でのプロダクトバックログ

5-3. 通常業務禁止DAYで横断的にDX改善を行う

繰り返しになりますが、Gaudiyでは現在、横断的なチームは存在しません。

一般的に、横断的なチームがやることとしては、技術選定基準や開発方針の決定、難易度が高い開発、横断的に行う必要があるメトリクス計測の開発などがあると思います。

横断的なチームには、上述したようなことにフォーカスして着手できるメリットがある一方で、生じ得るデメリットとしては、組織として階層構造になってしまったり、横断的なチームが社内受託的な組織になってしまうなどが考えられます。

今のフェーズにおいては、できるだけ全員がオーナーシップを持ってレバレッジの効くDX改善に取り組むことが大事だと思っています。これを実現可能にしているのが、毎週水曜日にある「EMPOWER-DAY」です。

この日は午前が休み、午後は通常業務を原則してはいけない決まりになっています。この曜日を使って、チームに関係ない開発メンバーとのDX向上に取り組んでいます。具体的には以下のような取り組みが行われています。

開発チームが抱えている課題管理DB

なお、事業フェーズが進み組織が拡大する中で、横断的なチームが今後誕生する可能性はあります。Empower-Dayが誕生した背景や具体的な内容については、以下の記事で詳しく説明しています。

note.com

6. 現状の課題

上述したように、様々な仕組み化によって改善はしているものの、まだまだ多くの課題があります。

ここでは、今後解決していきたい課題についてお伝えします。

6-1. 意思決定責任の所在が明確化されないこと

開発組織が階層構造になっていないことや、特定のことを行う横断的な組織がないことにはデメリットがあります。

そのひとつが、全員がオーナーシップを持っているが故に、開発時の意思決定スピードが落ちることです。これについては、取り組みごとに最終意思決定者(≒責任者)を明確にするような形で推進しており、徐々に解消している最中です。

6-2. 開発時のコミュニケーション過多、コンフリクトが生じる

現在のプロダクトは小さく、機能もそこまで多くないため、チームごとの担当領域を定めていません。つまり、ワンプロダクトにおける体験ごとにタスク完結性を持って開発する運用になっています。

この運用(体制)では、他のチームが取り組んでいる開発領域を知り、お互いにコミュニケーションを取る必要がありますが、これには一定のコストがかかります。そして、これらの課題はチームが増えるごとにさらに大きくなっていくことが予想されます。

このコミュニケーションコストが許容できなくなるタイミングでは、逆コンウェイの法則に従って、担当する領域を明確化した機能単位のチームに移行することが望ましいのかなと現時点では考えています。

7. 今後に向けて

やはり、プロダクトと組織は切っても切り離せない関係にあると思っています。つまり、強い組織をつくることができれば、おのずと良いプロダクトをつくれる可能性が高くなるはずだと考えています。

ここからGaudiyは、さらに大きくなっていきます。2022年時点で20人ほどの開発組織も、2023年には50人、2024には100人に大きくしていき、グローバルに向けた挑戦をしていきます。

すごくおもしろいフェーズだと思うので、少しでも興味持ってくださった方はぜひカジュアルにお話ししたいです。

meety.net

エンジニア向けの採用ページもできたので、貼っておきます!

recruit.gaudiy.com

Server-Driven UIの採用背景と実装について

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyで、フロントエンドエンジニアをしているkodai(@r34b26)です。

Gaudiyでは、Airbnbが採用していることで有名な「SDUI(Server-Driven UI)」という設計手法を取り入れています。

先月のTech Blogでは、ユーザーに対してファンダムな体験を届けるために実践している、スキーマ駆動開発についてお伝えしました。

techblog.gaudiy.com

今回は少し視点を変えて、顧客やユーザーと対峙する社内メンバーに対して、ファンダムな体験を届けるために実践している、SDUIについてまとめてみます。

GaudiyでSDUIを取り入れた理由や、その実装方法なども書いてみたので、一事例としてよければご参考ください。

1. SDUI(Server-Driven UI)とは

SDUIはおそらく、Airbnbが使用する設計手法として、その名称とともに広がったのが始まりです。

medium.com

UIの表現方法をすべてサーバーサイドで決定し、クライアントに返す、という手法になります。

通常のアプリケーション開発では、レンダリングロジック、ルーティングなどをクライアント側で決めますが、SDUIの手法ではこれらをサーバー側で決定しています。

日本語だとこちらのnoteがわかりやすいです。

note.com

2. SDUIのメリット

先述したAirbnbの記事や、一般的に説明されているメリットとしては、以下になります。

  1. UIの変更にアプリケーションのデプロイを必要としない

    サーバー側のロジックや設定を変更することで、クライアント側の表示を変更できるため、クライアントアプリのデプロイが必要ありません。そのため、デプロイまでの社内プロセスや実行時間を待つことなく、UIに対する変更が可能です。

  2. アプリケーションの審査を必要としない

    とくにネイティブアプリでのメリットになりますが、UI更新にデプロイを必要としないため、Apple, Googleの審査を毎回待つことなく細かい変更や巻き戻しができます。これにより、社内でコントロール不可能なタイムロスを圧縮することが可能です。

  3. 複雑な表示切り替えに拡張可能

    レコメンドエンジンを用いたユーザーごとの表示切り替えによる、行動促進や広告配信ができます。また、ABテストや限定リリースなど、リッチなロジックによる制御を組みやすくなります。

つまり、クライアントの都合による変更反映までのタイムラグをできる限り減らすことができる。それがSDUIの最大のメリットです。

3. なぜGaudiyでSDUIを採用したのか

Gaudiyの話をすると、少なくとも現状はWebアプリケーション(PWA)のみで実装を進めており、ネイティブアプリの実装はしていません。またレコメンドエンジンに関しても、将来展望としてはありますが、現在は実装を行っていません。

それなのに、なぜSDUIを取り入れたのか。それは、顧客に提供するアプリケーションのカスタマイズ性を担保するためです。

こちらは、実際にGaudiyが提供している、各アプリケーションのホーム画面です。

実際のアプリケーション画面

同一ページ、同一ソースコードで実現していますが、表現がだいぶ異なっているのがわかると思います。

Gaudiyでは、ブロックチェーンを活用した「Gaudiy Fanlink」というプロダクトを通じて、さまざまなファン体験を総合的に提供していますが、そのコミュニティアプリは基本的に個別カスタマイズを行っていません。

プロダクトのめざす方向性や汎用性などの観点から、必要と判断された機能を追加する形で開発を進めていますが、クライアント企業によってはすべての機能を必要としていなかったり、段階的に機能を増やしていきたかったりするため、一部機能のみを提供する場合があります。

たとえば、

  • 提供する機能を増やしたい/減らしたい
  • ホームに表示するコンテンツを施策に合わせて変更したい
  • IPの世界観を壊さないような形で表現したい

といったクライアントの要求に対して、Bizサイドのメンバーがエンジニアを常に頼ることなく扱えるようにしたい。それを実現するための手段として、SDUIを実践することに決めました。

4. SDUIの実装方法

ここからは、GaudiyでどのようにSDUIを実装しているかをお伝えしていきます。

元々、BFF-Client間通信に使用していたApollo GraphQLを生かしつつ、Airbnbの実装例を参考にしながら弊社のユースケースに沿うように、レイアウト、UIパーツをGraphQL Schemaに落とし込んで表現しました。

これによって、

  • Fragment Colocationによるフェッチ最適化
  • 宣言的データフェッチによるReactの宣言的UIとの対応
    • 将来的に、宣言的なフレームワークを用いてネイティブに拡張可能

といった、データ効率とDXが高い開発が可能になりました。

では実際に、どのようなSchemaになったかを数点ご紹介していきたいと思います。

4-1. レイアウトのSchema

Gaudiyの現状のアプリでは、モバイルとデスクトップで、大きくレイアウトを変えることがあります。

具体的には、「モバイルで1列」「デスクトップで1列」「デスクトップで2列」の3パターンと、それぞれのレイアウトに対してトップに特別なコンテンツを置く・置かないの出し分けがあり、すべてのレイアウトは3 x 2=6パターンで表現されています。

デスクトップ・2列レイアウト

モバイル・1列レイアウト

これらを表現でき、また将来的にパターンが増えたり、タブレットなどに細かく対応する余地を持たせたSchemaとして、以下のように表現しました。

type LayoutsPerFormFactor {
  mobile: Layout!
  desktop: Layout!
}

union Layout = SingleColumnLayout | TwoColumnLayout

type SingleColumnLayout {
  mainTop: SingleSectionPlacement
  main: MultipleSectionsPlacement...
}

type TwoColumnLayout {
  mainTop: SingleSectionPlacement
  main: MultipleSectionsPlacement!
  sub: MultipleSectionsPlacement!
    ... 
}

抽象化したパターンがSchemaから把握できる形になっていて、わかりやすいかと思います。

4-2. UIパーツのSchema

前述したコミュニティごとの特性の違いから、ある機能単位での表示・非表示と、UI上の表現の違いを数パターンで表したいという要望がありました。

たとえば、以下のような同じ”トピック”機能に対して、ランキング形式でユーザー行動をより強く促す形と、フラットに表現する形で使い分けています。

ランキング形式で表現するパターン

フラットに表現するパターン

ここでは、機能ごとに出し分けする点と、機能ごとのpreview的に複数項目を表現する点から、機能ごとにセクションを分け、Arrayで子を持つように表現しました。

type SectionContainer {
  id: ID!
  sectionComponentType: SectionComponentType
  section: Section
    ...
}

enum SectionComponentType {
  TOPIC_CAROUSEL
  TOPIC_TRENDING
  ...
}

union Section =
    ChatSection
  | TopicSection
  ...

type TopicSection {
  title: String!
  items: [Topic!]!
    ...
}

type Topic {
  id: ID!
  label: String!
  description: String!
  imageUrl: String!
  createdAt: Int!
  isDisabled: Boolean!
  ...
}

機能の増減は Sectionで受け止め、機能ごとの見た目や並びのブレに関しては SectionComponentTypeで受け止めることで、unionが膨大に増えることを抑制しています。また、UIごとに必要になるfieldの違いに関しては、clientでのcolocationで定義することによって、over/under fetchが起きないようにしています。

このように、プロダクトの性質や、設計の採用目的によって重きを置くところが違うので、SDUIの思想をベースにしつつ、ユースケースにあった調整をした上で使用すると効果的だと考えています。

5. まとめ

今回はSDUIに関して、Gaudiyで実際にどう取り組んでいるかをまとめてみました。

特にフロントエンジニアの視点で考えると、Schema駆動でバックエンドと協力して設計・実装するのと同じくらい、もしくはそれ以上に、宣言するUIがデザイナーと認識の合った形になるように、こちらからヒアリングして提案することが大事になると思いました。

これから運用と拡張のフェーズを経て、引き続きSchemaを育てていきたいと思っています。

興味ある人がいれば、ぜひお話ししたいですー。

meety.net

Gaudiyの技術選定について知りたい方は、以下の記事をご参考ください。

techblog.gaudiy.com

UXの向上と開発の生産性を両立するスキーマ駆動開発

こんにちは!ファンと共に時代を進める、Web3スタートアップのGaudiyでエンジニアをしている勝又(@winor30)です。

Gaudiyのプロダクト開発において、最も大切にしていることの1つに、ファンダムな最高のユーザー体験を提供することがあります。

今回は、このファンダムなユーザー体験を提供しながら、プロダクトの成長速度を落とさないために取り組んでいる、UX中心のスキーマ駆動開発についてまとめてみます。

具体的な開発フローや工夫している点についてまとめたので、同じような課題を抱えるチームのご参考になれば嬉しいです!

1. 取り組みの背景

UXに直結するエンジニアリングの領域は、ユーザーが実際に触るフロントエンドが中心になります。そのため、よいユーザー体験を提供するには、フロントエンド開発にしっかりとリソースを割ける状態を作ることが大事だと考えています。

また、プロダクトが順調に成長するためには、よい体験を実現しながらも、バックエンドの領域に近いビジネスロジックを正しく実行したり、機能の拡張性や汎用性を担保することも必要です。

それらの観点から、第一にユーザー体験を、第二にプロダクトの成長速度を落とさないという目的から、スキーマ駆動開発を中心とした開発フローを取り入れました。

2. Gaudiyのアーキテクチャ構成

Gaudiyが提供するプロダクトのアーキテクチャは、下記のようなFE → BFF → BEのような構成になっています。

エンジニアによる実装範囲は特に明確になっていませんが、基本的にFEをフロントエンドエンジニアが、BEをバックエンドエンジニアが開発しています。BFFに関しては、集約や整形ぐらいのロジックしかないため、ケースバイケースで開発をしています。

https://cdn-ak.f.st-hatena.com/images/fotolife/g/gaudiy/20220310/20220310234806.png

3. UX中心のスキーマ駆動開発の概要

基本的には、下記4つの開発アクティビティを大切にしています。

  1. すべてのインターフェースをスキーマ駆動で開発する
  2. スキーマのズレを検知できるようにする
  3. システム全体の結合は開発初期の段階で行う
  4. FEのブロックにならないようにモックを返す

この意図としては、「FEのブロックにならないようにモックを返す」を基本方針とすることで、UXの向上や検証のために必要なフロントエンド開発に、最大限のリソースを割くことができる状態を作るためです。

また、開発初期の段階でBEやBFFをFEときちんと結合させ、実際に結合させた状態を先に作るようにしています。これによって、実際の挙動に限りなく近い状態にすることができ、UXの細かい部分をいつでも確認することができます。

このような状態を維持しながらBEの開発も行えるように、スキーマ駆動開発と継続的なズレの検知により、BEとFEが自律的に開発できる状態を作っていくことが大切だと考えています。

3-1. すべてのインターフェースをスキーマ駆動で開発

Gaudiyでは現在FE → BFFはGraphQLを、BFF → BEはRESTを利用してアプリケーション間の通信を行っています。

そのため、GraphQLではシンプルにGraphQLスキーマを、RESTではOpenAPIを利用してスキーマ駆動開発を行っています。

各スキーマについてはFE、BEのエンジニアが議論しながら作成し、それをPRに上げるところから開発を始めています。これによって、それぞれのエンジニアが自律的に開発でき、開発の生産性をかなり高めることができています

実際に行う際はLive Shareを利用して、同時編集でGraphQLのスキーマを作成することがおすすめです。

3-2. スキーマのズレを検知できるようにする

GraphQLやRESTは、必ずサーバー側とクライアント側が存在します。

そのため、いくらスキーマを利用していても、サーバーとクライアント側で異なるバージョンのスキーマが利用されてしまうことは起こり得ます。これが生じると結局バグが発生してしまうため、プルリクエスト時のCIなどでズレを検知できるようにすることが大切です。

Gaudiyではアプリケーションのコードをmonorepoに配置しており、クライアント側とサーバー側のコードがすべて同じRepositoryに存在しています。

そのため、GitHub Actionでスキーマに変更があったのか? また変更があった場合、生成されたコードでビルドは可能か? をチェックしており、それによってインターフェースのズレを検知できるようにしています。

下記はGithub Actionのサンプルになります。

name: Check OpenAPI Schema for bff

on:
  pull_request:
    types: [opened, synchronize]
    paths:
      # bffのコードが変更されたケースとbeのopenapi.yamlのスキーマが更新されたタイミングで発火する
      - 'projects/bff/**'
      - 'projects/be/openapi.yaml'

defaults:
  run:
    working-directory: ./projects/bff

jobs:
  check-openapi-schema:
    name: check code
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
...
      # openapi.yamlからコードを生成し、その後のstepでbuildをし、成功するかチェック
      - name: Generate API Client by OpenAPI Schema
        run: npx @openapitools/openapi-generator-cli generate -i ../../projects/be/openapi.yaml -g typescript-axios  -o ./src/datasources/be/openapi/
      - name: Check build
        run: npm run build

3-3. システム全体の結合は開発初期の段階で行う

開発初期の段階でFE、BFF、BEが通信を行っている状態をつくることによって、開発初期段階でモックを使っていたとしてもリアルに近いプロダクトをチームに提供でき、UXを高めることに繋げられます。

Gaudiyは、体験の検証やQAを頻繁にする組織なので、最新の状態で触ることのできる環境を常に用意することは非常に大事です。そのため、結合自体も早めに実施するということをやっています。

体験の検証やQAについては以下のblogに詳しく書いてあるので、ぜひ読んでいただければと思います。

techblog.gaudiy.com

また、経験上の話をすれば、開発終盤になってFEとBFFや、BFFとBEのI/Fが合わないみたいなケースがかなり多いと思っています。そのため、先にこのリスクを潰しつつ継続的に維持した方が、大きくズレる前に修正もしやすく、トータルのコストが低くなると考えています。

3-4. FEのブロックにならないようにモックを返す

開発初期でシステムの全体の結合を作るには、BEがデータを返却する必要があります。BEでデータを返却できるような状態を作るには、DBとの接続や別サービスとの接続、BEのビジネスロジックの実装などかなり工数がかかります。

しかし、BEの開発をFEに待ってもらうと、それこそUXの最大化に繋がりません。そのため、どういうデータを返すべきかをFEエンジニアと相談しながら、モックデータを返却するような形で、BEの初期は開発するべきだと考えています。

こうすることで、FEが自律的に開発することができ、よりUXの最大化に繋げやすくなります。

また、モックが返却するデータは、FEエンジニアや他のロールの人に相談しながら実際に利用するようなデータを作ることによって、初期段階でもプロダクトを触ったときの体験を少しでもリアルなものに近づけることができます。

4. 開発フローの具体的な流れ

前述の4つの開発アクティビティをベースにしつつ、基本的には下記のようなフローで案件の開発を行っています。

今回の記事を読んで興味がある方は、具体的にどのように開発しているかの解像度を上げるためにも、ぜひ読んでいただければと思います。

4-1. 開発の基本フロー

1:開発案件のストーリーマップとDDDを終わらせる

完璧なものを作る必要はありませんが、全体像やどういう方針で開発するかの認識を揃えておくことで、その後の開発の認識ズレがなく開発しやすいです。

2:FE・BEエンジニアでGraphQLのスキーマを設計し、プルリクエストを上げる

Live Shareなどを使って同時編集でスキーマを作成できるようにするといい感じです。

3:GraphQLスキーマからモックレスポンスを返すような実装をし、最速でFEと結合できる状態を作る

4:FEはGraphQLスキーマからQueryを生成したりして、実際に結合できる状態を作る

このとき、仮にBFFと結合できない場合は、MSW(Mock Service Worker)を利用するとBFFを待たずにlocal開発ができます。(ただし、開発・本番などのリモート環境では確認できません。)

5. BEはOpenAPIを設計し、OpenAPIに沿ったモックレスポンスを返すBEアプリケーションを開発する

6. OpenAPIスキーマからクライアントを自動生成させ、すべてモックレスポンスの状態でBFF ← → BE間を結合させる

7. BEを開発していく

4-2. 継続的に行っていくこと

また、継続的に行っていくこととして、実装するなかでスキーマが変わるときは、必ず担当の別領域の(BE → FE、FE → BE)エンジニアに報告するようにしています。

レビュアーにもアサインするとベターです。

5. さいごに

今回の記事では、Gaudiyで実践している「UX中心のスキーマ駆動開発フロー」についてまとめてみました。

この開発フローを取り入れてから、UXを高めるための開発にリソースを割けるようになり、開発チーム全体の生産性もかなり上がったなと感じています。よければぜひご参考になさってみてください。

もっと詳しく知りたい方がいたら、ぜひMeetyで気軽にご連絡いただけたらと思います!他社さんの取り組みもお伺いしてみたいです。

meety.net

ECDSA署名の数学的理解とCloud KMSによる実装

こんにちは!ファンと共に時代を進める、Web3スタートアップのGaudiyでエンジニアをしている椿(@mikr29028944)です。

先日、Gaudiyではサーバーサイドウォレットの構築やEthereumにおけるECDSA署名の実装を行いました。

そこで今回は、少しニッチではありますが「ECDSA署名」をテーマに、Gaudiyの事業背景から、ECDSAの数学的な処理とコードまでを、実例をふまえてお伝えしてみたいと思います。

はじめに断っておくと、僕は大学時代にzk-SNARKsの理論を研究していたため、代数学を学んだことはありますが、この領域における専門家ではありません。なので理解が誤っている部分があれば、ぜひご指摘いただけると嬉しいです。

Web3スタートアップで働くことに興味がある方や、ブロックチェーンを業務で扱うエンジニアの方にご参考になればと思い、詳しく書いていたら1万5千字を超えてしまいました…。

以下の目次から、興味あるパートだけでもよければご覧ください!

1. GaudiyがECDSA署名を扱う理由

Gaudiyでは、ブロックチェーンを活用したファンコミュニティサービスを開発・提供しています。

そのコミュニティでは、ユーザー体験を考慮して、これまではガス代のかからないプライベートチェーン上でNFTを発行していました。それを今回、ユーザー同士でアイテムのトレード(二次流通)を行ったり、NFTとしての価値を感じられる体験を提供するために、EthereumやPolygonといったパブリックチェーン上へと書き出す(ブリッジする)機能を実装することになりました。

しかし、すべてのデジタルアイテムを、ユーザーが好き勝手に書き出せてしまうのには問題があります

なぜなら、Gaudiyではマンガ、アニメ、ゲームなどのIP(知的財産コンテンツ)公式コミュニティをつくっており、そのコミュニティ内で配布されるアイテムもIPに紐づく画像などが多いため、著作権にも関わります。なので、あくまで「許可されているアイテムのみ書き出せる状態にしたい」というのが要件としてありました。

この課題を解決するため、サーバーサイドウォレットを作成して、Gaudiyからユーザーに対して「あるアイテムに対して書き出し可能な証明書」を発行することで、管理者の署名がないとアイテムを引き出せないようにする仕組みをつくることになりました。

ここで使われている「ECDSA」による署名や検証の方法について、Gaudiyでの実例も交えてお伝えしていきます。

2. ECDSA署名とは何か

ECDSA(Elliptic Curve Digital Signature Algorithm)とは、楕円曲線DSAとも呼ばれており、ビットコインやイーサリアムの文脈では特にトランザクションの署名に使われます。

▼こちらの記事がわかりやすいです。

zoom-blc.com

ECDSAを理解するには、前提知識として「楕円曲線暗号」を知る必要があるので、まずはその概要から説明していきます。(実例だけ知りたい方は、4. まで読み飛ばしていただいても大丈夫です。)

2-1. 前提知識としての楕円曲線暗号

2-1-1. ざっくりとした楕円離散対数問題

そもそも楕円曲線暗号とは、楕円曲線上で組み立てられた暗号プロトコル全般を指します。

近年、RSAに変わる公開鍵暗号として注目されており、RSAよりも短い鍵長で同じくらいの安全性を持つという特徴があります。実際に身近で使われている例としては、デジタル放送における映像コンテンツの著作権保護技術や、ICカード、SSL/TLSなどがあります。

後ほどより具体的な楕円曲線の話をしますが、この章では簡単に楕円曲線暗号の概念を解説したいと思います。

ある地図の上でゲームのキャラクターが歩き回ることを考えてみてください。

このゲームの世界では、地図から歩いて右方向に突き抜けても、反対側から同じ角度で出てくることにします。ゲームキャラクターが歩幅{P}で原点{O}から一定な角度をつけて歩くとします。2歩進むと{2P}の位置にいます。これを10回、100回…と続けても{10P,100P, 10^{100}P}と容易に位置を求めることができます。

しかしこの世界では、キャラクターが何歩歩いたかを忘れてしまった場合、今いる位置と自分の一歩から何歩歩いたかを求めようとすると、とても難しいことが知られています。一歩{P}が決まっている時に

歩数nから現在地nPを求めることは容易だが、現在地nPからnを求めることは難しい

という関係性が成り立ちます。一般に「有限体 \Bbb{F} _ {p}上の楕円曲線上の点 nP Pが与えらた時に nを求めよ」という問題を楕円曲線離散対数問題と言います。(地図を楕円曲線として変換しています)

また、この問題を利用して、楕円曲線暗号では nPを公開鍵、 nを秘密鍵として、「公開鍵から秘密鍵を現実的な時間で求めることが難しい」ことを安全性要件としています。

(以下の参考書から例を抜粋しています)

クラウドを支えるこれからの暗号技術 | 光成 滋生 |本 | 通販

2-1-2. 楕円曲線暗号

では実際に、代数的な側面から楕円曲線上の加算と楕円曲線離散対数問題について考えます。

 pを大きな素数として、有限体  \Bbb{F} _ p上の楕円曲線は、以下のような3次方程式で定義されます。

 4a^ 3+27b^ 2 \not = 0かつa, b \in \Bbb{F} _ pとする。以下では下式の楕円曲線を Eと表します。

 
\tag{*} y^2= x^3 +ax +b 

楕円曲線上の有理点の集合の全体は以下のように定義する加法について群をなすことが知られています。

曲線上の点 Aと点 Bを足した点 Dは、点 A Bを結ぶ直線が再び曲線と交わる点 C y座標の符号を反転した点で定義されます。点 A Bが同じ点である場合は、その点で接戦を引きます。また楕円曲線上の点として、無限遠点 Oと呼ばれる仮想的な点を定義します。さらに無限遠点 Oとある点 Pの加算結果は、点 Pになると定義します。

この時、曲線上の点 Gがある場合に、上記の方法でその点を k回加算する演算(スカラー倍算呼ぶ)と、自然数 kから点 W=k \times Gを満たす自然数 kを求める演算は、容易に求めることができます。一方で、このスカラー倍算の逆演算(2点 G Wから、 W = k \times Gを満たす自然数 kを求める問題)は楕円曲線離散対数問題と呼ばれ、自然数 kの桁数が大きくなるほど解くのが難しいと言われています。

まとめると、楕円曲線暗号は、この「楕円曲線上の離散対数問題」を用いて構成される暗号のことを言います。

次に、この楕円曲線上の離散対数問題を利用したECDSAによる署名と検証について考えていきます。

2-2. ECDSAによる署名と検証

アリスが自分の公開鍵を使って、ECDSAによる署名と検証を行うことを考えます。

  1. 公開設定:有限体 \Bbb{F} _ p上の楕円曲線 Eと点 P(素位数 lの巡回群の生成元)を選びます。また、ハッシュ関数 hを選びます。

  2. 鍵生成:まずアリスはランダムに a \in \Bbb{F}^{*} _ {l}を選び、 A = aPとします。このとき aをアリスの秘密鍵、 Aをアリスの公開鍵とします。

  3. 署名:ランダムに k \in \Bbb{F} _ {l}^{*}を選び、 kP x座標を rとします。平文 mに対して、以下のように s求めて (r, s) mに対する署名とします。以下では下式を s \equiv k^{-1}(h(m) + ar) \bmod lと書きます。

     \tag{1}  s \equiv {(h(m) + ar) \over k} \bmod l

  4. 検証: u \equiv s^{-1}h(m)  \bmod l v \equiv s^{-1}r \bmod lを計算し、 x座標に関して、 \bmod lで以下の式が成り立つことを検証します。

 
    \tag{*} uP +vA = k P

 A= aPより

 
    uP + vaP = kP

楕円曲線のスカラー倍算より

 
    (u + va)P = kP

 u, vより

 
    (s^{-1}h(m) +s^{-1}ra)P =  s^{-1}(h(m)+ra)P = kP

左辺に (1)を代入して、

 
    (k^{-1})^{-1}(h(m)+ar)^{-1}(h(m) +a r) = kP

となり、右辺の kPと一致することがわかり、正当な署名であることの検証ができました。では次に、ここで定義した変数や処理を用いて話を展開していきます。

3. EthereumにおけるECDSA署名

3-1. secp256k1と呼ばれる楕円曲線

先ほどECDSA署名の処理を構築した際に a, bなどの多数の変数を用いました。これらは、なんでもかんでも自由に決めていいわけではありません。脆弱性のあるパラメータで楕円曲線暗号を構築した場合、計算量が多項式時間で解けてしまう攻撃手法SSSA法、準指数関数時間で解けてしまうGHS法などのあらゆる攻撃手法によって、簡単に楕円曲線離散対数問題が解けてしまいます。こういった攻撃手法の適応条件に合わないようにパラメータを選定する必要があります。

NIST(⽶国⽴標準技術研究所)やSECG(The Standards for Efficient Cryptography Group)と呼ばれる楕円曲線暗号の商用的な標準仕様を査定している機関が、安全な楕円曲線暗号を作るための推奨パラメータを公表しています。詳しくはSEC 2: Recommended Elliptic Curve Domain Parametersをご覧ください。

Ethereumでは、SECGが推奨パラメータとして公表している中の一つである「secp256k1」を使用しています。この楕円曲線から、公開鍵や秘密鍵の生成がされています。パラメータ T = (p, a, b, G, n, h)はそれぞれ以下の値になり、 (*)で定義した楕円曲線に a, bを代入すると以下のような方程式になります。

 
y^2 = x^3 +7

後の章で nを用いた話があるのでここで説明しておくと、パラメーターに含まれる nとは、楕円曲線上の点 Gが与えられた時に n \times G  = Oとなる正の整数であり、この n Gの位数と呼びます。群の位数、元の位数の定義などはこちらの記事がわかりやすいです。

secp256k1でのパラメータ T = (p, a, b, G, n, h)は以下のような値になっています。

Recommended Parameters secp256k1

詳しくはこちらのRecommended Parameters secp256k1セクションをご覧ください。

3-2. EthereumでECDSA署名を扱う

では実際に、secp256k1の楕円曲線上のECDSA署名がどのように行われているのかをみていきます。

詳しいEthereumにおけるECDSA署名の仕様はこちらのEthereum Yellow Paperをご覧ください。

 
\text{signature} = (r, s, v)

EthereumにおけるトランザクションはECDSAによる署名がされ、32バイトの r値と s値、1バイトの v値を連結した65バイトを署名とします。 r値と s値に関しては先ほど記述した値になり、 v値に関しては x座標が rとなる点 Rを一意に定めるための値になります。

もし v 0の場合は R y座標の値が偶数、 v 1の場合は R y座標の値が奇数を表します。 v値に関しての詳細は、後述するEIP-155と一緒に説明します。またECDSA署名が有効な場合は、以下の条件が成り立ちます。

 
\begin{aligned}
0 < &r < \text{secp256k1n} \\ 
0 < &s < \text{secp256k1n}/2 +1
 \\ 
&v \in \lbrace 0, 1\rbrace
\end{aligned}

 \text{secp256k1n}とは、前述した n \times G  = Oとなる基点 Gの位数 nのことです。 rの取りうる範囲が 0から \text{secp256k1n}まで、 sの取りうる範囲が 0から \text{secp256k1n/2 +1}まで、 v 0 1の値を取ります。

 r \text{secp256k1n}までである理由はわかると思います。なぜなら、 \text{secp256k1n}は先ほどのECDSA署名の Gの位数 lにあたり、計算が \bmod l上でされているため、 r lを超えないことがわかります。ですが、 s \text{secp256k1n}までではなく、 \text{secp256k1n /2} +1までになっています。この理由は後述する「 sの反転」の章でEIP-2の概要説明と一緒に説明します。

また、署名から公開鍵の復元に関しては、平文 mのハッシュ値と r, s, vからリカバリできることがわかります。これに関しても後述します。

ここで記号を整理します。

  •  G:ベースポイント、基点と呼ばれる楕円曲線上の点(ECDSAによる署名と検証章の Pにあたる)
  •  h(m):平文 mのハッシュ値(ECDSAによる署名と検証章と同じ)
  •  k:署名ごとに異なるランダムで一時的な秘密鍵(ECDSAによる署名と検証章と同じ)
  •  n Gの位数 (ECDSAによる署名と検証章の lにあたる)
  •  r, s mに対する署名(ECDSAによる署名と検証章と同じ)

4. GaudiyではどのようにECDSA署名を行っているのか

ここからは、Gaudiyのユースケースを元にしたECDSA署名と検証フローを、実際のコードを添えて説明します。

4-1. 署名と検証の流れ

  1. digestと呼ばれるあるメッセージのSHA256ハッシュ化された値と、KMSに保存されているGaudiyの秘密鍵からKMSによる署名が生成されます
  2. この署名はASN.1と呼ばれる標準的なデータ構造から成るため、Unmarshalによって r, sをリカバリします
  3.  rに対する v値を計算し、 (r, s, v)を連結してそれをECDSAによる署名とします
  4. Ethereumのコントラクト上でこの署名が検証され、それによってリカバリされた公開鍵と署名を作成した公開鍵が一致するかを検証します

4-2. 詳細と実際のコード

4-2-1. Cloud KMSを使ったECDSA署名の構築

Gaudiyでは、秘密鍵の管理として、Cloud KMSを活用しています。

下記の記事にもあるのですが、プライベートキーを環境変数経由で渡す場合の問題点として

  • 秘密鍵を直接使えばプログラム以外の意図しない文脈で署名を行える
  • 秘密鍵を誰が使用したという履歴はどこにも残らない
  • 秘密鍵を直接持ち出せば退職者であっても署名を行える

が挙げられます。

zenn.dev

記事ではAWS KMS(Key Management System)を使ってこれらの問題を解決していますが、Gaudiyでは同じくEthereumの署名アルゴリズム、secp256k1をサポートしている Cloud KMS を使って解決することになりました。

Cloud KMSでは、非対称署名アルゴリズム、非対称暗号化アルゴリズムなどの様々なユースケースに対応できるように鍵のアルゴリズムを設定できます。

今回のケースでは楕円曲線署名を行いたいので、非対称署名アルゴリズムを選択し、曲線の種類をsecp256k1とします。Cloud KMS関する詳細はこちらをご覧ください。

Cloud KMSの鍵の扱いについて

まず最初にSignDigestという関数が署名全体の処理を行う関数になっています。引数には、署名者のアドレスとSHA256ハッシュ化させてメッセージを入れます。

このハッシュ化されたメッセージをAsymmetricSignRequestというオブジェクトを作成して、AsymmetricSignという関数を呼び、KMSによる署名を作成します。返ってきた値はASN.1の構造をしているのでrecoverRS関数を呼び、 r, s をリカバリします。

func (k *KMSSigner) SignDigest(ctx context.Context, address common.Address, digest []byte) ([]byte, error) {
  // KMSで署名を行うためのデータ
    req := &kmspb.AsymmetricSignRequest{
        Name: keyVersion,
        Digest: &kmspb.Digest{
            Digest: &kmspb.Digest_Sha256{
                Sha256: digest,
            },
        },
    }

    // KMSに保存されてる秘密鍵を用いて署名が作成される
    // この作成された署名は公開鍵を用いて検証することができる
    result, err := k.client.AsymmetricSign(ctx, req)
    if err != nil {
        return nil, err
    }

    // ASN.1データ構造のsignatureからrとsをリカバリする
    r, s, err := recoverRS(result.Signature)
    if err != nil {
        return nil, err
    }
 
    // 後半の処理
    ...
}

recoverRSの関数の中はこのようになっています。goの標準パッケージとしてasn1が提供されているので、単純にそのパッケージ内にあるUnmarshalを呼び出します。

// recover R and S from KMS signature
func recoverRS(signature []byte) (r *big.Int, s *big.Int, err error) {
    sig := new(struct {
        R *big.Int
        S *big.Int
    })

    _, err = asn1.Unmarshal(out.Signature, sig)
    if err != nil {
        return nil, err
    }

    // 処理
    ...

    return sig.R, sig.S, nil
}

4-2-2. sの反転について

急に sの反転についてとありますが、この話は、前述した sの取りうる範囲についてです。ECDSA署名が有効であるには、3つの条件がありました。そのひとつ s

 
0 < s < \text{secp256k1n/2 +1}

とすることが条件になります。EthereumのECDSA署名において、もし s \text{secp256k1n}の半分よりも大きい場合は s' = \text{secp256k1n} - s(この演算を「 sの反転」と呼ぶことにする)を計算し、この値を使ってECDSA署名を作る必要があります。

var (
    secp256k1N, _  = new(big.Int).SetString("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16)
    secp256k1halfN = new(big.Int).Div(secp256k1N, big.NewInt(2))
)

func recoverRS(signature []byte) (r *big.Int, s *big.Int, err error) {
    // 処理
    ...

    if sig.S.Cmp(secp256k1halfN) > 0 {
        sig.S = new(big.Int).Sub(secp256k1N, sig.S)
    }

    return sig.R, sig.S, nil
}

なぜ s 0 \lt s \lt \text{secp256k1n}ではダメなのでしょうか?これはEIP-2の中でこの問題が指摘されています。

github.com

EIP-2で言われている問題をそのまま翻訳すると

0 < s < secp256k1Nの範囲での任意のs値を持つトランザクションを許可すると、トランザクションの不正性が 懸念される。なぜなら、s値をsからsecp256k1N - sに反転し、v値を(27 → 28, 28 → 27)を反転させても結 果として得られる署名は有効である。

つまり、 sを反転して得られた s'から (r, s', v')を作成しても、異なる署名結果であるのに有効と判断されてしまうということになります。これについて実際に値を変化させて検証します。( v値については後述します)

前述の内容から sは以下のように求めることができました。

 
\tag{1}  s \equiv {(h(m) + ar) \over k} \bmod l

 sの反転」とは (1) sに対して、

 
s' = l -s

として s'とおきます。この s'から u'を求めると

 
\begin{aligned}
u' &\equiv s'^{-1}h(m) \bmod l  \\
&\equiv {1 \over l -s} h(m) \bmod l \\ 
&\equiv s^{-1} {s \over l- s} h(m) \bmod l \\ 
&\equiv  s^{-1} {s \over l- s} h(m) \bmod l - l (s^{-1}{1 \over l-s}h(m)) \bmod l \\
&\equiv s^{-1}{{s-l} \over l-s }h(m) 
\bmod l \\
&\equiv s^{-1} \lbrace -1  \times ({l-s \over l-s}) \rbrace h(m) \bmod l \\
&\equiv -s^{-1}h(m) \bmod l
\end{aligned}

となり、 u' uの関係は以下のようになります。

 
u' = -u

 vについても同様で、 v'= -vとなり、 u', v'から

 
\begin{aligned}
u'P +v'A &= -uP +-vA \\
&= -(uP+ vA) \\
&= -kP
\end{aligned}

点PとQのx座標の値は同じ

となるので、楕円曲線上の点のマイナスは y座標の正負をさせるだけで x座標の値は変わりません。

つまり、 (*)は両辺の x座標の点が同じかどうかを検証しているので、反転させた s'で評価をしても同じ検証結果が得られます。

そのためEIP-2では sの範囲を 0 \lt s \lt \text{secp256k1n}/2 +1に制限し、もしこの範囲外に sがある場合は、その署名を無効することでこのような問題を防いでいます。

4-2-3. v値について

 r, sをリカバリできたら最後に v値の計算です。 v値は r x座標の値とする曲線上の点を一意に定めるための値でした。 v \in { 0, 1}の範囲で (r, s, v)の有効性を検証し、有効性が示された時の v値がECDSA署名に含まれます。

今では v値が 0, 1の場合は古いバージョンとして存在していて、それに 27を足した 27, 28 v値として署名に含まれます。また v値についてメインネットやテストネットなどのネットワークを区別するために、chainIDを含めた v値がEIP-155で提案されています。今回はこの説明はしませんが、詳しく知りたい人はこちらをご覧ください。

 v 0 1のどちらかで正しい署名が作成できるので、2回検証を回して、それによってリカバリされるアドレスと署名者のアドレスを比較して同じ場合に、その値を v値にして最後に27を足して、署名の完成とします。

func (k *KMSSigner) SignDigest(ctx context.Context, address common.Address, digest []byte) ([]byte, error) {
    // 処理
    ...

    sig := make([]byte, 65)
    // r値
    copy(sig[:32], r.Bytes())
    // s値
    copy(sig[32:64], s.Bytes())
 
    for _, v := range []int{0, 1} {
        sigv := append(sig, byte(v))
        publicKey, err := secp256k1.RecoverPubkey(digest, sigv)

        if err != nil {
            return nil, err
        }

        verified := crypto.VerifySignature(publicKey, digest, sig[:len(sig)-1])
        addrA := common.BytesToAddress(crypto.Keccak256(publicKey[1:])[12:])

        if (verified && reflect.DeepEqual(addrA.Bytes(), address.Bytes())) {
            sig = append(sig, byte(v))
            break
        }
    }

    sig[64] += 27

    return sig, nil
}

以上、EthereumにおけるECDSA署名の作成方法について実際のコードを交えながら見ていきました。最後に、この得られた署名から公開鍵をどのようにリカバリするかを考えます。

4-2-4. ECDSA署名から公開鍵のリカバリ

署名の検証に関してはコントラクト上で行われます。ECDSA.recover によってアドレスが得られるので、得られたアドレスと署名者のアドレスを比較します。(この関数ではECDSA署名から公開鍵が得られ、内部でEthereumのアドレス形式に変換されています。)

address recoverdAddress = ECDSA.recover(digest, voucher.signature);
require(signerAddress == recoverdAddress, "address does not match");

実際にどのように公開鍵がリカバリされているのか見ていきます。

 r vから署名の際に選んだ公開鍵が一意に特定でき、それを Rとおく。

 
R= (r, y) = (u_1 + u_2a)P

 Rと署名とハッシュ化された平文 mに対して以下のように計算すると、

 
\begin{aligned}
Q &= r^{-1}(sR- h(m)P) \\
& = r^{-1}\lbrace s(u_1+u_2a)P - h(m)P \rbrace \\ 
&=r^{-1}\lbrace skP - h(m)P \rbrace \\
&= r^{-1} \lbrace {(h(m)+ ar) \over k } \times k P - h(m)P \rbrace \\
&= r^{-1}arP \\
&= aP
\end{aligned}

となり、公開鍵が導出されます。つまり、署名が正しければ導出された公開鍵と署名に使用した公開鍵が一致することがわかります。

実際 R x座標の rが満たすべき条件というのがあり、このSEC 1: Elliptic Curve Cryptographyで説明されています。詳しくはそちらをご覧ください。

以上より、ECDSA署名の作成と検証することができました。

5. さいごに

今回は、楕円曲線暗号の概要とCloud KMSを用いたECDSA署名の作成・検証方法をコードと一緒に見てきました。

冒頭にも書きましたが、大学で代数学を勉強した程度なので理解が間違っている部分等あるかもしれません。なので、もしそのような箇所があればぜひご指摘いただけると嬉しいです。

Gaudiyでは、このようにブロックチェーンが絡んだ実装もあれば、普通のWebアプリケーション開発の実装もあります。個人的には好きな分野なので、興味ある方いたらぜひお話ししたいです!

meety.net

ブロックチェーン知識は不要? 採用面談でよくあるQ&A集

f:id:hanahanayaman:20220401170428p:plain

こんにちは!エンタメ領域のDXを推進するブロックチェーンスタートアップ、Gaudiyでエンジニアをしているkei(@kei32bit)です。

Gaudiyの採用スタンスや具体的なスカウト手法についてはこれまでもご紹介してきましたが、今回は、実際のカジュアル面談や採用面談でエンジニアの方からよく聞かれる質問について深くフォーカスしていきたいと思います。

ブロックチェーンを扱っていると聞くと「何か特殊な経験が必要なのでは?」と思われる方が多いと思いますが、本記事では

  • 事前にブロックチェーンの知識を持っておく必要はない
  • ブロックチェーンを扱う企業にはレイヤーの違いがある
  • 重要なのはソフトウェアアーキテクチャの原則に基づいた開発経験

をお伝えできればなと思います!

1. ブロックチェーンって実際どういう風に業務で使われるの?

まずはじめに、実際のプロダクト開発でブロックチェーンをどう使っているの? という質問がよくあります。

Gaudiyのプロダクトは「ブロックチェーン技術を感じさせないような作り」になっているので、外から見るとピンと来ない方が多いかもしれません。

Gaudiyのプロダクト例 techblog.gaudiy.com

現在、GaudiyではプライベートチェーンでNFTを発行していますが、このNFTを発行する際にチェーンに書き込む処理の部分でブロックチェーン技術を利用しています。

が、それ以外のところでは、現状はブロックチェーンに関わる開発はあまり存在しません

というのもGaudiyはブロックチェーンを扱う企業ですが、アプリケーションレイヤーを扱っているので、ブロックチェーン自体の開発をごりごり推進する企業ではないからです。

f:id:kei32bit:20220401121006p:plain
ブロックチェーンにおけるアプリケーションレイヤー
ref. https://m13.co/article/crypto-and-the-consumer-the-road-ahead

アプリケーションレイヤー層にあたるGaudiyでは、ブロックチェーンはあくまでも数ある技術のひとつであり、ブロックチェーンを使わずに開発をするケースの方が多いです。

2. ソフトウェアアーキテクチャの考え方ってブロックチェーン業務でも使えるの?

結論からいうと、ソフトウェアアーキテクチャの原則に則った開発をGaudiyでは重要視しているため使えます。むしろ、開発する上でこちらの方がとても必要な考え方だと思っています

ブロックチェーン技術にはいくつか課題があるのですが、良いUXを提供するためにはソフトウェアアーキテクチャの原則に則った解決策を試行錯誤することが求められます。

具体例でいうと、以前、大量のNFTを発行するプロジェクトにて「NFTの発行が遅延しても、UXを損なわずにユーザーへのNFT配布をどう実現するか?」という課題に直面しました。

ブロックチェーンに書き込む(=NFTをmintする)際に遅延が発生するということはわりと有名な話ですが、UXを損なわずにユーザーがNFTを取得できる、という体験を提供するには工夫が必要でした。

この際のアーキテクチャ設計では「遅延配布」「非同期」といったワードをもとに、「NFTを書き出す処理だけTask Queueを使って非同期的に処理しつつ、ユーザーの受け取り画面にはメタデータ(画像など)を先に返却する」という解決策を選択しました。

ユーザーに対してはNFTの画像データを先に受け取ってもらうことでUXを向上させる、という目的を達成しつつ、非同期的に遅延配布することでNFTとして配布するという機能要件も達成しました。ですがここで、また新しい課題に直面しました。

それは、非同期処理を行うことで、nonce(ブロック生成時に必要な32bytesの数値。この数値が被ってしまうとブロックが生成できずに詰まってしまう)の制御が難しくなるという課題です。

これは非同期処理で制御せずに並列処理を行ってしまうと、nonce値が被ってしまう恐れがあるからです。

f:id:kei32bit:20220331210351p:plain
並列処理

この課題を抽象的に捉えると「直列処理」「排他制御」といったワードに変換できるので、当時は非同期処理の中で「直列処理で制御する」「並列処理を用いつつ、nonce値の予約を行うことで値の衝突を制御する」といった解決案を出すことができました。

f:id:kei32bit:20220331210412p:plain
直列処理

まとめると、今回の課題であった「NFTの発行が遅延しても、UXを損なわずにユーザーへのNFT配布をどう実現するか?」の解決策としては、以下の2点について抽象的に課題を捉える必要がありました。

1. NFTの書き出しが遅い => 遅延対応、非同期処理といった一つ上の抽象概念として課題を捉える

2. nonceの数値が被らないようにNFTをmintしたい => 並列処理ではなく直列処理で扱う、nonce値の事前確保を予約システムになぞらえて排他制御を加える、といった上位概念の解決策を当てはめてみる

具体例が長くなってしまいましたが、課題に対して事象を抽象化する思考とソフトウェアアーキテクチャに則った開発経験があれば、ブロックチェーン特有の技術課題にも対応できると考えています。

3. ブロックチェーン技術のキャッチアップってどれくらい必要?

さいごに、非常によくいただく質問ですが、結論からお伝えすると、入社前からブロックチェーンのキャッチアップは必要ありません

もちろん業務内容に応じてブロックチェーン技術の勉強が必要になりますが、Gaudiyでは以下のような文化があるため、業務内でのキャッチアップが可能です。なので、事前知識といったものは必要ありません。

3-1. ペアリサーチ

ペアプログラミングという開発手法がありますが、Gaudiyではリサーチ業務でもペアリサーチという形で取り入れています。

主に有識者とペアになってリサーチをするのですが、どんな内容でもラフに質問できるので、ブロックチェーンを学びながらリサーチを進めることができます。画面を共有しながら「なぜこの技術を調べてるのか」だったり「Twitterではこのあたりの有識者の方のコメントが勉強になる」だったり、自身のリサーチ力の基礎づくりにもなります。

以下の画像は、ペアリサーチをした際に異なるチェーン間でのスワップの仕組みについて順序を追って説明したときの図です。

f:id:kei32bit:20220331090818j:plain
例) NEARにおけるスワップの説明

Gaudiyでは現在、NFTをプライベートチェーンで発行・配布していますが、今後様々なパブリックチェーンにNFTを書き出す仕組みを進めており、「NFTを書き出すとは具体的にどういうことなのか?」を図を共有しながら説明したり質問を受けたりしました。

技術選定と同じで「なぜこの(ブロックチェーン)技術を使うのか」「メリット・デメリットはなにか」「ユーザー体験を損なわないか」といった議論は、ブロックチェーン技術を使った開発でも必須です。その前提知識の共有には、時間を費やしてしっかり理解する、というポリシーを持っています。

3-2. 超守-破離

「超守-破離」とは、Gaudiyの造語でCredo(行動規範)のひとつにもなっています。意味としては「まず先人の教えや原理原則に基づいた事例を学び、その上で新しいやり方がないか見つけ出す」といったものです。

  • 議論を始める前に、原理・原則や先人の事例をリサーチする「超守」を徹底する。
  • 過去の経験に過信せず、常に今のやり方を疑い、アンラーニングする。
  • 超守の上で、新しい概念や方法を導入し、積極的に新たな価値を生み出す。

どんなプロジェクトでも、新しい技術を使用する場合は必ず「超守」する時間をとるため、その中でブロックチェーン技術がどう使われるのかをキャッチアップできます。

具体例を挙げると、以前Gaudiyで「NFTのメタデータをどうやって生成するか?どこに保存するか?」を議論するケースがありました。 現在プライベートチェーンで発行・配布しているNFTを、今後パブリックチェーンに書き出す際に、メタデータ(画像など)をどこに置くかを前もって調査する必要があったからです。

この際も「超守」と呼ばれるリサーチの時間を取り、「そもそもNFTの資産性とはなにか?」や「オンチェーン/オフチェーンの定義とはなにか?」といった「NFTの資産性とメタデータがどう紐づくのか?」というテーマで調べました。

「超守」する際には必ず他メンバーとディスカッションしながら進めるため、何を調べれば良いのか分からないといった状況にならずに、リサーチの方向性について議論しながら進めることができます。

4. さいごに

今回は、ブロックチェーンを扱うスタートアップとして、エンジニアの方からよくいただく質問についてご紹介させていただきました。

冒頭でもご説明した通り、Gaudiyではブロックチェーンを扱っているものの、アプリケーションレイヤーでの開発を行っているため、ブロックチェーン自体をごりごり開発しているわけではないです。

ブロックチェーン特有の知識などは入社前には不要ですし、実際にエンジニアの約半数は入社後にペアリサーチなどを通じてキャッチアップしています。

ブロックチェーン企業の間にも扱うレイヤーの違いがあるため、一概には言えませんが、ブロックチェーン企業への応募に対してハードルを感じているエンジニアの方々にご参考になれば嬉しいです。

もっと詳細が知りたい、という方はぜひMeetyでお話しましょう!

meety.net

Gaudiyの技術選定などについて知りたい方は、以下の記事もご参考ください。

techblog.gaudiy.com

ブロックチェーンの勉強を始めてみたい方は、こちらもどうぞ。

techblog.gaudiy.com