AllIsHackedOff

Just a memo, just a progress

チームの未来をTrustする

※ この記事は「LayerXアドベントカレンダー2023春」の20日目の記事です。 今回は、「チームの未来をTrustする」をテーマにお送りします。1日前の記事はMone Haginoさんの記事「「誰もいない場所で戦う」「ハッカーであれ」「後悔最小化」―LayerXで4ヶ月間インターンして学んだ3つのこと|Mone Hagino|note」でした。明日はMDMのEM、Shun Masudaさんの予定です!

こんにちは、LayerX Fintech事業部のサルバです。普段は三井物産デジタル・アセットマネジメントに出向し、個人向け安定資産投資サービスALTERNAの開発のリーダーをやっています。

まずは宣伝なのですが、ALTERNAは絶賛、特典付きの事前登録を受付中です。

alterna-z.com

みなさまに是非登録いただけると嬉しいです。期待に答えられるようにがんばります。

開発のよもやま話は山のようにあるのですけれど、今年のテーマの「Trustful Team」について、 三井物産デジタル・アセットマネジメントで開発する日々の中で考えていることなどを書きたいと思います。 前半は主に透明性について、後半はチームの成長をシビアにただ確実にTrustしていくことについて書きます。

目次

  • 透明性はopenさを育む
  • 透明な情報から未来を引き出す
  • 未来志向で、時間の力を信じて託す

透明さはopenさを育む

LayerX羅針盤にあるように、情報の透明性は個人のアウトプットと、それを統合したチームのアウトプットにとって非常に大切な要素です。

情報の透明性・オープン性

事業も組織も、そして個人も時間とともに変わっていくため、この透明性というのも(そもそも1つの指標で測れるものではありませんけれど)時間とともに変わっていきます。 なので、情報の透明性を担保する方法も定期的に見直すことはどうしても必要になってしまいます。Fintech事業部の丸野さんの煽り気味のタイトルの記事にあるように、 note.com

「一番透明性を求める人が、知りたいときにアクセスできる状態」

であるとか、

情報のアクセシビリティを上げることで、1人1人のパフォーマンスが最大化すると信じること(=チームへの強い信頼)

という基本的な理念についてチームで目線を揃えた上で(ここは結構大変、日々の凡事徹底が問われます)、足元で日々改善しないといけません。

透明性(ある意味でのopeness) を保つことの実践は、個人と組織に必然的に顧客志向と柔軟で新しいことに取り組む心(open-mindedness)を育むと思っています。 すこし具体度を上げて言うと

  • プロダクト開発の過程で生み出されるアウトプットのレビュー(とプロセス)
  • 不具合/障害などの報告対応(とプロセス)
  • プロダクトのイケてなさ/イケてるところを指摘しあう試み
  • 戦略やOKRの策定

などで、意思決定の元になったデータや考え方(前提)が明らかであることは、端的に誰かからのフィードバックを受けやすくなるという意味でよいことだと思います。 反対に言えば、前提がよくわからないとフィードバックがそもそも成立しません(例:「そこは実はこういう背景があって」とか「歴史的にこうなっていて」などが後出しされるなど)。 透明性を保つための試みは非常に草の根のコミュニケーションだとわたし自身は考えています。

チームに情報を提供する側は、受け手側の負荷が低い方法(フォーマット・言い回しなど含む)で情報を出していくことと、浸透させるのに時間がかかる情報は何度でも意志を持って言い続ける必要があります。

受け止める側やフィードバックする側は発信側をリスペクトしつつ、前提や意図を汲み取って前向きなフィードバックをすることを心がける必要があります。意思疎通の土台があると透明性の力をさらに大きく使うことができるので、思考プロセスのうち重要な部分をいかに速く的確に伝えるかは、透明性を保ちつつ議論を速くすすめるために大切だと考えます。

透明な情報から未来を引き出す

プロダクトの企画や仕様に「なぜやるか」をファクトや考え方・前提と共に書くのは非常に大切だし、MTGならば、意図を引き出すようなファシリテシーションや、発言の頭で意図をはっきりと伝えられると心地よく透明性を保てるように思えます。

実際に「単なる興味本位で聞くんですけどとか」「〜の観点で話すんですけど」とか「議論のために言うんですけど」などの枕詞をつけた会話はとても心地良いと感じます。特に、リモートワークが当たり前になってからは、より気をつけなければと思ったりします。

そして、各メンバーの思考プロセスや発言の中には

  • 「今はできないけれど、こういう姿を目指したい」
  • 「松・竹・梅プランがあるけれど、速く出して改善したいからとりあえず梅で」

といった、未来への視点が入っていることがあるかなと思います。未来には(たとえ漠然としていても)メンバーの思いがこもっているので、 これをただ足元の効率性を優先して後に回すのではなく、受け手側がその意志をより受け止めることで、チームが進むべき方向性についても目線を揃えやすくなると思います。

未来への意志を尊重し、時間の力を信じて託す

チームが届けられるプロダクトの提供価値について、あるアイデアの中には未来への視点が含まれています。 未来には必ず不確実性があるので、誰かが良いと思った未来(意図)を汲み取って、willがある人に託すことでチーム自体を成長させる。

そういう改善サイクルをうまく回す地道な活動だけが、着実にチームを強くするなと最近は感じることがあります。 そう感じたきっかけとしては、ALTERNAチームの振り返り会に関する反省があります。

チームを運用していると

  • チームの課題(Problem)として上がったことに対するTryがすぐには効果を上げない
  • プロダクトの大きな変更の前段になるテスト的な施策が芳しい効果を上げない

ということは多々あると思っていて、効率を優先するとひどく近視眼的に「意味がないからやめよう」とただ未来を骨抜きにしてしまうことがあるのですけれど(実際にALTERNAでも結構ありました)

  • 効果が出るまでの期間、時間軸
  • 効果が出ない理由
  • 施策の本来の意図と顧客(やチーム)の課題

ここの認識をまず揃えていくことと、明らかな失敗ならばすぐに方針転換をする柔軟性が大切だと思います。

良い改善サイクルを回すためには、未来像を明確にしておくことが大切で、その土台があると、チームやプロダクトの現状に対してシビアな指摘をしあい、チームの未来をTrustすることができると思います。正確な現状把握と未来志向で、よい開発をしていきたいなと、これを書きながら改めて思いました。

Fintech事業部では5年後、10年後の金融の世界を見据えながら個人向け資産運用サービスALTERNAを開発してくれる仲間を募集中です。

jobs.layerx.co.jp

factbaseに未来を見据えるための基盤づくりに協力してくれる仲間も募集しています。

jobs.layerx.co.jp

事前登録も受け付けてます(メールアドレス or LINEの友達登録だけで完了)のでよろしくおねがいします。 alterna-z.com

新プロダクト開発のお気持ち合わせ、使うためのインセプションデッキ作成

 

この記事は、【2022 LayerX Advent Calendar(概念) 18日目の記事です。

前回はSaaS事業部の羽倉さんの記事でした。

次回は、SaaS事業部のswatchさんの記事が出る予定です!

 

こんにちは。LayerXFintech事業部でリードエンジニアをやっているサルバ (@MasashiSalvador)です。最近の悩み事は日々増える積ん読です。

 

現在はLayerXから三井物産デジタル・アセットマネジメント(以下MDMと呼びます)に出向してアセットマネジメント業務、証券業務の効率化、投資家向けオルタナ投資サービスの開発を行っています。

 

MDMに出向しているメンバーの働き方はざべすさんの下記の記事に詳しいです。

note.com

 

 

今回はMDM問わず、LayerXのプロダクト開発でよく行う「インセプションデッキの作成」について、何をどうやって、どういうところに気をつけて作っているかについて書きたいと思います。

 

インセプションデッキとは

プロジェクトの目的や背景、優先順位を簡潔に伝えるためのドキュメントです。

プロジェクトを進める上でのプロジェクトメンバーの共通認識を記すために作ります。

型(フォーマット)が決まっていて、型に当てはめながら議論してドキュメントに起こすことでメンバー感問わずプロジェクト関係者の目線や認識が揃っていくことにいつも驚かされます。

 

インセプションデッキは

  • わたしたちはなぜここにいるのか?(=プロジェクトのミッション)
  • エレベーターピッチ(=プロダクトの特徴を簡潔に述べるもの)
  • やること/やらないことリスト
  • トレードオフスライダー(=大上段の優先度の明確化)

を含むのですが、中には「なぜここにいるのか?」などこっ恥ずかしい、プロジェクトが進んでしまったあとだと話しづらいものもあります。

この"話しづらさ"は端的にちゃぶ台返し的になってしまう(ので議論のコストが大きい)のもそうですし、プロジェクトの進行とともに忙しくなる中で都度問いかけると端的に進まなくなってしまうのもあると思います。

※もちろん、議論できるチーム状態は最高だし、すべきなのですが、前提や方向性が揃っていればそれに越したことはないよねというお話

 

僕のこれまでの経験でいうと

  • 自社の新規プロダクト開発で関係メンバー全員で
  • MDMで(もちろんLayerX以外の出向メンバー込で)関係メンバー全員で
  • (コンサルをやっていたころは)クライアント含め関係メンバー全員で

インセプションデッキを作成しました。

 

参考資料

github.com

 

dev.classmethod.jp

 

blog.nextscape.net

実際の作り方/作っているもの

0. 参加メンバーの時間を確保する(2.5h - 4h、半日くらいのイメージ)

・会議の司会を決めておく

1. 材料を集める

 ・議論のための材料を精査する

  ・競合や先行サービスの調査

  ・潜在ユーザへのインタビュー

  ・定量データの収拾

2. テンプレートを印刷(もしくはデジタルで参加者の手元で作業できるようにして)ワークショップ形式で議論を進める。参加者のワーク結果をホワイトボードに張り出してお気持ちを言っていくようにする。

・人数が多ければグループ分けをして、各位の想いを発表していくいく

・その場で文言に落とし込む

3. (2)の結果をキレイにする。

 

議論の場はスクラム開発における振り返り会に近いかもしれません。メンバー全員が気軽に意見を述べられる場を作ることが大切です。

 

オンライン開催する場合はMiroやGoogle Jamboardなどを使って、とにかくカジュアルに思ったことを書き出して発表できる場を設定します。

 

認識を揃える。と言っても何もない状態からただはじめるだけではいいアウトプットがでないので、司会はワークショップのインセプションデッキのフォーマットを眺めておいて、潜在顧客の声など、議論の際にファクトとして使えそうなものを逆算して集めておきます。

 

型どおりにすべて埋めることが目的ではないので、プロジェクトに必要そうなアウトプットをあらかじめ抜粋しておいて、タイムテーブルをくんでしまうのがいいと思います。

 

下記で(★)をつけたものはだいたいどのプロジェクトでも作成しておいたほうがよいと思います。LayerXの場合はインセプションデッキの雛形がGoogle Driveで共有されており、プロジェクトごとに必要なものを抜粋して作成するようにしています。

 

インセプションデッキの結果だけではなく、議論の過程=参加者のワークシートがスキャンされて残されているので、議論の場の熱気やコンテクストも読み取ることができます(プロダクト開発のノウハウが組織に蓄積されていっている感があります)

 

インセプションデッキの型

  • (★)わたしたちはなぜここにいるのか?
  • (★)エレベーターピッチ
  • (★)やること/やらないことリスト
  • (★)トレードオフスライダー
  • パッケージデザイン
  • プロジェクトコミュニティ
  • 技術的な解決策
  • etc

 

実際のタイムテーブルの例としては(これだと多分押してしまいますが)下記のようになります。

 

14:00 - 14:10 わたしたちはなぜここにいるのか? のシートを参加者個人で埋める

14:10 - 14:20 ひとりずつシートを張り出し、お気持ちを述べて認識を合わせる

14:20 - 14:25 認識を合わせて、文言に落とし込む

14:25 - 14:35 エレベーターピッチ のシートを参加者個人で埋める

14:35 - 14:45 ひとりずつシートを張り出し、お気持ちを述べて認識を合わせる

14:45 - 14:50 認識を合わせて、文言に落とし込む

14:50 - 14:55 休憩

... 以下完成するまで繰り返し

気をつけている点

  • 作ることが目的ではないので、どう「使うか」を考える
    • どういう文言を入れると、あとでどういう議論につながるかを考えてまとめる
      • 「例:New-ProductはAな商品を提供する」とインセプションデッキに記載
      • 後日「これって本当にAになってるのかな?」「A度がたりないんじゃない?」などと施策やプロダクト品質の話をする時に使える
  • やる/やらないリストには「進め方」や「思い」のようなものも入れる
    • 「過剰に品質を優先して納期を妥協することはしない」
    • 「競合を過度に意識しすぎない」
    • 「お客様ひとりひとりの声を聞く(聞ける状態を作る)」
    • など
  • ひとりひとりのワークシートの発表の中で言葉に込めた「お気持ち」を深堀りして聞く/「お気持ち」を言いやすい議事進行をする
    • 初っ端からきれいな文言に落とし込むのは(誰にとっても)難しい
      • シュッととかイケてるとか個人ワークで書いてもよい。大事なのはその中のお気持ちをすり合わせること

 

他にもある気がしますが、気づいたらまた追記したりTwitterに書きたいと思います。

 

ethers.jsをconsoleから使えるタスクランナーbuidlers.devに触ってみる

サンプルのインストール

ローカルにbuidlerをインストール

yarn init
# global installするとtypescript互換モードが使えなくなる感あるのでローカルにいれます
yarn add --dev @nomiclabs/buidler

起動 (builderとtypoしても優しく訂正してくれる...はず)

yarn buidler

起動時のダイアログ

888               d8b      888 888
888               Y8P      888 888
888                        888 888
88888b.  888  888 888  .d88888 888  .d88b.  888d888
888 "88b 888  888 888 d88" 888 888 d8P  Y8b 888P"
888  888 888  888 888 888  888 888 88888888 888
888 d88P Y88b 888 888 Y88b 888 888 Y8b.     888
88888P"   "Y88888 888  "Y88888 888  "Y8888  888

👷 Welcome to Buidler v1.0.1 👷‍‍

? What do you want to do? …
❯ Create a sample project # こいつを選択
  Create an empty buidler.config.js
  Quit

サンプルプロジェクトで使いたいライブラリをインストール

yarn add --save @nomiclabs/buidler-truffle5 @nomiclabs/buidler-web3 web3

コントラクトのコンパイル(built-inタスクの実行)

# デフォルトで用意されているcompileタスクが実行される
yarn buidler compile
# contracts下がコンパイルされ、build下に成果物(artifactファイル)が生成される

コントラクトをコンパイルするとbuildしたにbuidlerのartifactファイルが生成される。build成果物自体はtruffleのものと違いコンパクトではあるが networks の項目がないのでこのファイル他のコンポーネントコントラクトと通信するために使い回せるかは謎である。

{
  "contractName": "Greeter",
  "abi": [ /* 略 */ ],
  "bytecode": "0x608060405234801561001057600080fd5b506040516104e03803806104e08339818101604052602081101561003357600080fd5b810190808051604051939291908464010000000082111561005357600080fd5b8382019150602082018581111561006957600080fd5b825186600182028301116401000000008211171561008657600080fd5b8083526020830192505050908051906020019080838360005b838110156100ba57808201518184015260208101905061009f565b50505050905090810190601f1680156100e75780820380516001836020036101000a031916815260200191505b506040525050506100fd8161010360201b60201c565b506101c2565b806000908051906020019061011992919061011d565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061015e57805160ff191683800117855561018c565b8280016001018555821561018c579182015b8281111561018b578251825591602001919060010190610170565b5b509050610199919061019d565b5090565b6101bf91905b808211156101bb5760008160009055506001016101a3565b5090565b90565b61030f806101d16000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae3217146100f6575b600080fd5b6100f46004803603602081101561005157600080fd5b810190808035906020019064010000000081111561006e57600080fd5b82018360208201111561008057600080fd5b803590602001918460018302840111640100000000831117156100a257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050509192919290505050610179565b005b6100fe610193565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561013e578082015181840152602081019050610123565b50505050905090810190601f16801561016b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b806000908051906020019061018f929190610235565b5050565b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561022b5780601f106102005761010080835404028352916020019161022b565b820191906000526020600020905b81548152906001019060200180831161020e57829003601f168201915b5050505050905090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061027657805160ff19168380011785556102a4565b828001600101855582156102a4579182015b828111156102a3578251825591602001919060010190610288565b5b5090506102b191906102b5565b5090565b6102d791905b808211156102d35760008160009055506001016102bb565b5090565b9056fea265627a7a723158204b258d436f26c71ba0028877c11fb5e10e2229c35e34015e83b7ca402052b2ed64736f6c634300050b0032",
  "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae3217146100f6575b600080fd5b6100f46004803603602081101561005157600080fd5b810190808035906020019064010000000081111561006e57600080fd5b82018360208201111561008057600080fd5b803590602001918460018302840111640100000000831117156100a257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050509192919290505050610179565b005b6100fe610193565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561013e578082015181840152602081019050610123565b50505050905090810190601f16801561016b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b806000908051906020019061018f929190610235565b5050565b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561022b5780601f106102005761010080835404028352916020019161022b565b820191906000526020600020905b81548152906001019060200180831161020e57829003601f168201915b5050505050905090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061027657805160ff19168380011785556102a4565b828001600101855582156102a4579182015b828111156102a3578251825591602001919060010190610288565b5b5090506102b191906102b5565b5090565b6102d791905b808211156102d35760008160009055506001016102bb565b5090565b9056fea265627a7a723158204b258d436f26c71ba0028877c11fb5e10e2229c35e34015e83b7ca402052b2ed64736f6c634300050b0032",
  "linkReferences": {},
  "deployedLinkReferences": {}
}

サンプルコード中に含まれているタスクの実行

truffle buidler accconts
# builder.config.jsにかかれているacccountsタスクが起動する
# 20個のアカウントが表示されるはず

デフォルトではBuidler EVM Networkに接続してコントラクトのデプロイやテストの実行などを行う。Builder EVMのネットワークはtaskを走らせる度に立ち上がる buidler.config.jsでよしなに gasfromのアドレスを指定することができる。

buidler.config.jsの例

module.exports = {
  defaultNetworks: "buidlerevm",
  networks: {
    localhost: {
      url: 'http://127.0.0.1:8545'
    },
    buidlerevm: {
      gas: 0,
      gasPrice: 0,
      accounts: [ // 指定しないと20個のアカウントに10000EHがデフォルトで付与されて用意される
        { 
          privateKey: "0xB3BB3A1EB4EBAAE568DB332251B77F5A029E44DB3C1CA9F39056B29C86ADC62C",
          balance: "0x" + `${55 * (10**10)}`,
        }
      ],
      blockGasLimit: 99 * (10**5),
      hardfork: "constantinople",
    },
  },
};

デプロイ

run yarn buidler run scripts/sample-script.js

sample-script.jsは下記 Truffleとそれほど書き方は変わらない。artifactファイルを読み込んで諸々の操作を行う。ただスクリプトを走らせているだけなのでカスタマイズはTruffleのマイグレーションよりは効くだろう。ただし、順序制御的なものは導入されていない

// We require the Buidler Runtime Environment explicitly here. This is optional
// when running the script with `buidler run <script>`: you'll find the Buidler
// Runtime Environment's members available as global variable in that case.
const env = require("@nomiclabs/buidler");

async function main() {
  // You can run Buidler tasks from a script.
  // For example, we make sure everything is compiled by running "compile"
  await env.run("compile");

  // We require the artifacts once our contracts are compiled
  const Greeter = env.artifacts.require("Greeter");
  const greeter = await Greeter.new("Hello, world!");

  console.log("Greeter address:", greeter.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

デプロイの出力

All contracts have already been compiled, skipping compilation.
All contracts have already been compiled, skipping compilation.
Greeter address: 0x7c2C195CD6D34B8F845992d380aADB2730bB9C6F
✨  Done in 3.41s.

buidlerは起動時にconfigファイルを指定できるので、ビルド対象のコントラクトディレクトリ、ビルド成果物の配備先などもよしなに切り替えることができる。一例としてsolidity 0.4.xと0.5.xを共存させる場合の例が公式チュートリアルには記載されている

builder.config_xxxx.js

module.exports = {
  paths: {
    sources: "./contracts/5", // 0.5系を配備するディレクトリ
    // test
    // cache
    // artifacts
    // root などを指定可能
  }
  /* この下にネットワークの設定など */
};

設定ファイルの読み替え

yarn buidler compile --config builder.config_xxxx.js

テスト実行時のスタックトレース

1) Contract: ERC721
       like an ERC721
         transfers
           via safeTransferFrom
             to a contract that does not implement the required function
               reverts:
     Error: Transaction reverted: function selector was not recognized and there's no fallback function
      at ERC721Mock.<unrecognized-selector> (contracts/mocks/ERC721Mock.sol:9)
      at ERC721Mock._checkOnERC721Received (contracts/token/ERC721/ERC721.sol:334)
      at ERC721Mock._safeTransferFrom (contracts/token/ERC721/ERC721.sol:196)
      at ERC721Mock.safeTransferFrom (contracts/token/ERC721/ERC721.sol:179)
      at ERC721Mock.safeTransferFrom (contracts/token/ERC721/ERC721.sol:162)
      at TruffleContract.safeTransferFrom (node_modules/@nomiclabs/truffle-contract/lib/execute.js:157:24)
      at Context.<anonymous> (test/token/ERC721/ERC721.behavior.js:262:30)

  2) Contract: ERC721
       internal functions
         _burn(address, uint256)
           with minted token
             with burnt token
               reverts when burning a token id that has been deleted:
     TypeError: this.token.methods.aurn(address,uint256) is not a function
      at Context.<anonymous> (test/token/ERC721/ERC721.test.js:82:58)
      at process._tickCallback (internal/process/next_tick.js:68:7)

同じテストをTruffleで実行したときの出力

1) Contract: ERC721
       like an ERC721
         transfers
           via safeTransferFrom
             to a contract that does not implement the required function
               reverts:
     Error: Returned error: VM Exception while processing transaction: revert
     at PromiEvent (node_modules/truffle/build/webpack:/packages/contract/lib/promievent.js:9:1)
      at TruffleContract.safeTransferFrom (node_modules/truffle/build/webpack:/packages/contract/lib/execute.js:169:1)
      at Context.<anonymous> (test/token/ERC721/ERC721.behavior.js:262:30)

  2) Contract: ERC721
       internal functions
         _burn(address, uint256)
           with minted token
             with burnt token
               reverts when burning a token id that has been deleted:
     TypeError: this.token.methods.aurn(address,uint256) is not a function
      at Context.<anonymous> (test/token/ERC721/ERC721.test.js:82:58)
      at process._tickCallback (internal/process/next_tick.js:68:7)

と、たしかにTruffleよりも親切そうだ

roppongi.rs #1 を開催しました

2019/07/30にroppongi.rs #1 を開催しました。発表者のみなさん、参加者の皆さん、会場を貸してくださったDMMさんありがとうございました。

roppongirs.connpass.com

僕自身はRustは初学者ですし、C++等もそれほど触ったことはないので、 * イベントを開催することで詳しい人達と知り合う * 実際のユースケースを聞いて理解を深める * 自分の独学のケツ叩きをする

この辺りを達成するために、界隈の皆様から刺激を頂きたいなと思って開催しました。 お互いに刺激を与え合う場としてのイベントがあることはなんだかんだで大切だと思うので、僕自身もみなさんに刺激を与えられるようなトークやLTができればと思ってます。

開催に際しての考え

  • 初心者も上級者も楽しく交流できるようなRustのコミュニティを六本木界隈に作りたい
    • 願わくば参加者同士が一緒に仕事するようになったり、何かしら新しいライブラリ / ツールなど作っていけると○ですよね。
  • 初等的(elementary) でありたい
    • 最先端ばかりを追うのではなく、初学者も入りやすく、中上級者も改めて理解楽しく理解を深められるようなコンテンツを増やしたい
    • 初等 != 初級(e.g. 「初等整数論
      • elementaryな話題 / 本質的な話題を楽しむ
  • 作ること / コードを書くことを楽しめるイベントでありたい
    • いろいろな領域のユースケース
    • 実装上のハマりどころ
    • 設計上のハマりどころ
    • etc
  • 続けることはなんだかんだで大事
    • 10回,20回...と続けてコミュニティを継続することが大事だと思うので、定期開催していきたい
    • スピーカーの方も増やしていきたい

トーク内容

Rustがいかにエンジニアの脳を楽にさせているかをC++初心者が語る〜関数篇〜 by @natsu_no_yuki

C++歴15年の初心者(!?)である@natsu_no_yuki さんがExcelを使いながらC++で考えないと行けないメモリ管理についてのアレコレ、Rustが何を解決してくれるかを解説していただきました。 (C++er怖い...)

esa-pages.io

余談ですがesa.io便利そうですね...

なぜBlockchainはRustを選ぶのか by @jkcomment

ブロックチェーンを含むさまざまなユースケースを紹介していただきました

speakerdeck.com

Rustで実装されたLibra Move言語とは by @cipepser

Facebookが発表したLibra上で使えるMove言語(スマートコントラクト開発言語)について発表いただきました。 言語処理系の実装ってRustだとどれくらいやりやすいんだろう?というのが気になりました 。

speakerdeck.com

その他

EthereumのTransactionのRLPエンコード / デコード

Ethereumでdappを作る際にトランザクションへの署名をよしなに実装する必要があるのだが、署名前と後でRLP形式のデータがどう変化しているか一応見てみることにした

利用した便利ツール

  1. RLPコマンド (npm install -g で グローバルに入れるとCLIが入る) www.npmjs.com
rlp decode 0x...
rlp encode [] # encodeしたい構造

こんな感じで使える。達人は目デコードできると思うが、普通の人間ならCLIを使ったほうが早いだろう

  1. RLP形式のトランザクションをJavaScirptのオブジェクトに直してくれる君 www.npmjs.com
const txDecoder = require('ethereum-tx-decoder');

decoded  = txDecoder.decodeTx("0xf88a808502540be4008401c9c38094a6a1b8a7015fb40e7a4cb1f30ffce4456ba30a2b80a460fe47b100000000000000000000000000000000000000000000000000000000000003e71ca0cec088f5e937f5cfa89eca38a71f768dd66974cafae1a4dc5572acd4e664e541a01ecbdc7f1332d99fb41d170587c2b6ea45c9e85e8c43ad8fa32a22157c3597a4")
console.log(decoded)

こんな感じで使える

署名前と署名後のトランザクション

署名にはethereum-tx を使うことにして、下記のように署名を生成する

const SimpleStorage = artifacts.require("./SimpleStorage.sol")
const EthereumTx = require('ethereumjs-tx')

# ローカル環境の秘密鍵です。あしからず...
const signerAddress = "4fb1e3cd8de0392b648ec044f75d9bc58b2d7a32"
const signerPrivateKey = Buffer.from('6116d8f7e9174f1e838aec3492ac57e9a1868d579ede4b4f6b0c757519f2591a', 'hex')

// この辺は適当...
const gasLimit = 30000000
const gasPriceGwei = '10'

module.exports = async function(callback) {
    try {
        var nonce = await web3.eth.getTransactionCount(signerAddress)

        let simpleStorage = new web3.eth.Contract(SimpleStorage.abi, SimpleStorage.address, {})
        var method = simpleStorage.methods.set(999)

        var rawTx = {
            nonce: web3.utils.toHex(nonce),
            from: signerAddress,
            gasPrice: web3.utils.toHex(web3.utils.toWei(gasPriceGwei, 'gwei')),
            gasLimit: web3.utils.toHex(gasLimit), 
            to: SimpleStorage.address,
            value: new web3.utils.BN(web3.utils.toWei('0', 'ether')),
            data: method.encodeABI(),
        }
        // トランザクションオブジェクト生成
        let tx = new EthereumTx(rawTx)
        // 署名前
        console.log('0x' + tx.serialize().toString('hex')
        tx.sign(signerPrivateKey);
        // 署名後
        console.log('0x' + tx.serialize().toString('hex')
    } catch () {
    }
}

で、得られたトランザクション(のRLPエンコード)をデコードしていく

署名前のトランザクション

// raw (RLP encoded tx)
0xf84a808502540be4008401c9c38094a6a1b8a7015fb40e7a4cb1f30ffce4456ba30a2b80a460fe47b100000000000000000000000000000000000000000000000000000000000003e71c8080
// rlp decode
[ '',
  '02540be400', 
  '01c9c380',
  'a6a1b8a7015fb40e7a4cb1f30ffce4456ba30a2b',
  '',
  '60fe47b100000000000000000000000000000000000000000000000000000000000003e7',
  '1c', // 16 + 12 = 28?
  '',
  '' ]
// decoded tx as json
{ nonce: 0,
  gasPrice: BigNumber { _bn: <BN: 2540be400> },
  gasLimit: BigNumber { _bn: <BN: 1c9c380> },
  to: '0xa6a1b8a7015fb40e7a4cb1f30ffce4456ba30a2b',
  value: BigNumber { _bn: <BN: 0> },
  data:
   '0x60fe47b100000000000000000000000000000000000000000000000000000000000003e7',
  v: 28,
  r: '0x',
  s: '0x' }

署名後のトランザクション

// tx (serialized)
0xf88a808502540be4008401c9c38094a6a1b8a7015fb40e7a4cb1f30ffce4456ba30a2b80a460fe47b100000000000000000000000000000000000000000000000000000000000003e71ca0cec088f5e937f5cfa89eca38a71f768dd66974cafae1a4dc5572acd4e664e541a01ecbdc7f1332d99fb41d170587c2b6ea45c9e85e8c43ad8fa32a22157c3597a4

// rlp decoded
[ '',
  '02540be400',
  '01c9c380',
  'a6a1b8a7015fb40e7a4cb1f30ffce4456ba30a2b',
  '',
  '60fe47b100000000000000000000000000000000000000000000000000000000000003e7',
  '1c',
  'cec088f5e937f5cfa89eca38a71f768dd66974cafae1a4dc5572acd4e664e541',
  '1ecbdc7f1332d99fb41d170587c2b6ea45c9e85e8c43ad8fa32a22157c3597a4' ]

// decoded rlp as JSON
{ nonce: 0,
  gasPrice: BigNumber { _bn: <BN: 2540be400> },
  gasLimit: BigNumber { _bn: <BN: 1c9c380> },
  to: '0xa6a1b8a7015fb40e7a4cb1f30ffce4456ba30a2b',
  value: BigNumber { _bn: <BN: 0> },
  data:
   '0x60fe47b100000000000000000000000000000000000000000000000000000000000003e7',
  v: 28,
  r:
   '0xcec088f5e937f5cfa89eca38a71f768dd66974cafae1a4dc5572acd4e664e541',
  s:
   '0x1ecbdc7f1332d99fb41d170587c2b6ea45c9e85e8c43ad8fa32a22157c3597a4' }

分かりきっていたことな気がするが、署名するとv,r,s (ecrecoverなどに使うパラメタ)が入る

KtorのCORSがchromeのPreflight Requestに対してうまく働かなくてハマった問題

経緯

Kotlinを書いているうち - 適度にシュッとしている点 - Nullableをハンドリングできる点 - Collection系の関数があるところ - IntelliJとの相性抜群 などに惹かれたのでAPIでも書いてみようと軽そうなKtorを触ることに。

発見した問題

ChromeからのCORSのリクエスト、特にPreflight Requestが通らない。 Chromeのインスペクタから Copy as cURLしてやると、下記のリクエストで 403 が返ってくる

curl 'http://localhost:8080/sample' -X OPTIONS -H 'Access-Control-Request-Method: GET' -H 'Origin: http://localhost:8878' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en,ja-JP;q=0.9,ja;q=0.8,en-GB;q=0.7,en-US;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' -H 'Accept: */*' -H 'Connection: keep-alive' -H 'Access-Control-Request-Headers: authorization,crossdomain,x-csrf-token' --compressed

解決法

公式ドキュメントを読んでもそれらしき内容はないし、ISSUEもない、ということでなぜ 403 なのかをコードを追って確認することに。 CORS - Servers - Ktor

    private suspend fun ApplicationCall.respondPreflight(origin: String) {
        if (!corsCheckRequestMethod() || !corsCheckRequestHeaders()) {
            respond(HttpStatusCode.Forbidden) // ここで 403が返ってるっぽい
            return
        }

        accessControlAllowOrigin(origin)
        accessControlAllowCredentials()
        response.header(HttpHeaders.AccessControlAllowMethods, methodsListHeaderValue)
        response.header(HttpHeaders.AccessControlAllowHeaders, headersListHeaderValue)
        accessControlMaxAge()
        respond(HttpStatusCode.OK)

    }

ので corsCheckRequestHeader を追うことに

    private fun ApplicationCall.corsCheckRequestHeaders(): Boolean {
        val requestHeaders = request.headers.getAll(HttpHeaders.AccessControlRequestHeaders)?.flatMap { it.split(",") }?.map { it.trim().toLowerCase() } ?: emptyList()

        return !requestHeaders.any { it !in headers }
    }

Access-Control-Request-Headers で指定されたヘッダに許可していないものが入っていないか調べているらしい。Chromeから送られるリクエストは 'Access-Control-Request-Headers: authorization,crossdomain,x-csrf-token' の3つのヘッダーを含んでいるが、これらはKtorのデフォルト設定だと許可されていない

    class Configuration {
        companion object {
            val CorsDefaultMethods = setOf(HttpMethod.Get, HttpMethod.Post, HttpMethod.Head)

            // https://www.w3.org/TR/cors/#simple-header
            val CorsDefaultHeaders: Set<String> = TreeSet(String.CASE_INSENSITIVE_ORDER).apply {
                addAll(listOf(
                        HttpHeaders.CacheControl,
                        HttpHeaders.ContentLanguage,
                        HttpHeaders.ContentType,
                        HttpHeaders.Expires,
                        HttpHeaders.LastModified,
                        HttpHeaders.Pragma
                ))
            }

見た感じデフォルトで許可すべきヘッダはw3cの仕様で決まっている?(ここは追っていない とにかく、許可ヘッダを足す必要があるため、若干ウォークアラウンド感があるが下記のように設定を追加し事なきをえた

           header("CrossDomain")
            header("X-CSRF-Token")

これがブラウザごとに異なる、とかだと結構設定が面倒なので、Ktor側でうまく吸収して欲しい...

KotlinでPrivate Methodをテスト

[2] の方法で実施しようとしたら trySetAccessible が解決できなくて gradle test がエラーになった。 IntelliJでは補完が効いているので謎である。詰まっていても先に進まないので[2]の方法を試したらうまくいった。

    @Test
    fun test6() {
        val tc = TitleConferrer()

        val method = tc::class.memberFunctions.find { it.name == "hoge" }
        val actual = method?.let {
            it.isAccessible = true
            println(it.toString())
            it.call(tc)
        }

find で見つけるのはなんだか効率悪そうなので、抽象化されたメソッドがあるかと思ったが補完候補の中には見当たらなかったので一旦スルー

java.lang.reflect.Methodkotlin.reflect の差異だろうか

References

  1. Kotlinのリフレクション(protected/privateメソッド呼び出し) - White Box技術部
  2. Kotlinでprivateメソッドをテストする

Memo

java.lang.reflect.Methodkotlin.reflect の差異については理解を深めておく 基本的には kotlin.reflect を使えばいいのだろうか