📮

年賀状を全力でおめでたくしたらブロックチェーンに載せることになった【HackDay 2021 最優秀賞作品 技術解説 #1】

2021/03/31に公開

はじめに

皆さん、あけましておめでとうございます。ねりこです。

先日、2021.02.20 - 21で、Yahoo! JAPAN主催ハッカソン「Hack Day 2021 Online」に参加してきました。チーム名は「ポピポポピピプピリロピロリププププピーポ」です。

およそ70チームが24時間の開発と90秒の作品プレゼンを行い、私達の作品「ネオネンガ」が最優秀賞、Happy Hacking賞(視聴者投票1位)、LINE Blockchain賞、Clear ML賞の4冠獲得しました。皆様ほんとうにありがとうございました!

私達が今回作ったのはこちらの「年賀状」です。

今回の成果物

何を言っているかわからないと思いますが、年賀状をもっと心温まる感じにしたいという気持ちを突き詰めていったらこうなりました。
その経緯はチームメンバーがこちらに書いてくれています。ぜひ読んでみてください。
https://note.com/music_shio/n/n4d7fee947d2f

この記事では、それを実現する機能のうち主に自分が関わった部分の技術解説をしていこうと思います。よろしくお願いします。

これはなに

年賀状、まだいけると思いませんか?

今回の作品のコンセプトは

年賀状DX: もっと無駄なく、もっと気持ちが伝わるように

という感じです。伝統的年賀状の良さをそのままに、技術によって年賀状体験をもっと良くすることを目標にしました。そしてその結果、以下のような機能が生まれました。(太字は主に自分が関わったもの)

ネオネンガでできること

  • 年賀状をブロックチェーン上のトークン(NFT)として、所有権を厳格に管理
  • その年賀状を正当に所有する人にしか読めない暗号化
    • → よりパーソナルなメッセージを書ける!
  • 手書きメッセージをアニメーションとして紙面上に再生
    • → 気持ちがもっとリアルに伝わる
  • お年玉の埋め込み
    • → もっと感謝を
  • 年中いつでも、何枚でも年賀状を送れて、全部まとめて1/1に届く
    • → なにかあるごとにフレッシュな記憶で書ける

いかがでしょうか。よりあたたかい気持ちになれそうだと思いませんか?

これらの機能を24時間の制限時間内に実装するわけですが、自分はフロントエンドエンジニアとして、ブラウザ上での手書きアニメの記録と再生を実装したり、あるいはブロックチェーン環境の整備と統合なんかをやっていました。そして冒頭にお伝えしたとおりLINE Blockchain賞を頂けましたので、嬉しい限りです。
この記事では、自分の担当部分のうちブロックチェーンに関わるほうの解説をさせていただきます。

手書きアニメの記録と再生については、Part2をご覧ください!
https://zenn.dev/nerikosans/articles/0a2096854f4b7a

年賀状をブロックチェーンで管理しよう

年賀状を鋳造すること、もう一生ないと思うわ――

やりたいこと

年賀状って、そのままだと家族とか他の人にも読めちゃいますよね。
じゃあ、真の持ち主にしか読めないような暗号化をすれば、ほんとうにそのひとだけに届いて、もっと年賀状があたたかくなるのでは...!?
というのがモチベーションです。

これを実現するために考えたのが以下の方式です。

  • 年賀状ごとにランダムなシードを生成して暗号鍵とし、セキュアなDBに保存
  • 年賀状ごとにNFT(後述)を発行し、サービス側からその所有権を管理
  • 年賀状の復号がリクエストされたとき、対応するNFTの所有権をチェックし、一致する場合のみ、その年賀状の暗号鍵を引き出して復号
  • NFTの所有権は、年賀状の作成時には差出人にあり、1/1になった瞬間に宛先へ一斉に転送


こんな感じ

「セキュアなDB」には、EAGLYS社様の提供技術であるData Armor GATE DBを使用しました。ありがとうございます!

暗号化の技術については他チームメンバーが担当してくれたので、ここでは、ブロックチェーンを使って年賀状に対応するNFTを作って管理した方法をお話しします。

なお、ブロックチェーンの実装にあたっては、LINE社様の提供技術であるLINE Blockchainを用いており、また本作品はLINE Blockchain賞を頂いています。ありがたい限りです!

NFTってなに ―基礎的なこと

自分は今回ブロックチェーン技術を初めて触ったので、まず基本的な概念の勉強から入りました。
各用語について、自分なりにまとめて記そうと思います。ブロックチェーンの知識がある方は飛ばしてもらって構いません。

サービスとユーザー

開発者はLINE Blockchain Developers上で サービス を作成することができます。また、ユーザーは、LINEの暗号資産サービスであるBITMAXのユーザーです。サービスは、独自のトークン(後述)を発行し、ユーザーに配布することができます。

ウォレット

ウォレットは資産の入れ物であり、各ユーザーに1つずつ発行され(ユーザーウォレット)、サービスは必要に応じて複数のウォレットを作成することができます(サービスウォレット)。ウォレットはWallet Addressによって識別されます。各取引の際、取引の主体(サービスorユーザー)は、ウォレットに設定されたWallet Secretという文字列を提示することによって、自分がウォレットの所有者であることを証明します。

トークン

トークンはウォレットに入れる実際の資産であり、以下の3種類の資産タイプに分類されます。

1. Base Coin

Base Coinは、サービス間を横断して、プラットフォーム全体で価値を持つトークンです。日本でいうとに相当します。
LINE Blockchainには Cashew と呼ばれる開発テスト用のネットワークがあり、Cashew上のBase Coinは Test Coin(TC) です。

2. Fungible Token (FT)

Fungible Tokenとは、ほかの同種のトークンと代替可能なトークンのことです。代替可能とは、「ここにある100個のトークンも、あっちにある100個も価値が同じ」ということです。要するに「私が死んでも、代わりはいるもの」ということです。
日本でいうと例えばTポイントに相当します。Tポイントは、系列サービス独自のもので、かつ、100ポイントはどこでも100ポイントですよね。

3. Non-Fungible Token (NFT)

おまたせしました。NFTです。
Non-Fungible Tokenとは、ほかの同種のトークンと代替不可能なトークンのことです。
日本でいうと例えば映画のチケットに相当します。おなじ「映画のチケット」でも、他の席のだったり、時間が違ったりするので、交換はできないですよね。綾波は、綾波だけだッ!

以上の3種類です。

LINE Blockchain上では、サービス独自に、サービストークンというFTと、アイテムトークンというFTまたはNFTを発行することができます。
サービストークンはサービス上の通貨に相当し、Contract IDによって区別されます。今回は実際に「ポピコイン(POPIC)」というサービストークンを発行して、お年玉の送金に用いました。

アイテムトークンは財貨や商品としての役割に相当し、Token TypeToken Indexによって区別されます。Token TypeとToken Indexを連結した文字列を Token ID と言います。
今回は「NengajoOwnership」というNFTを発行し、年賀状の所有権に相当する価値として用いました。

作成と鋳造

サービストークンとアイテムトークンは、以下の手順でユーザーに送ります。

  1. 作成: 名前やfungibilityなど、トークンの情報を決めて、ネットワークに登録すること
  2. 鋳造(mint): 作成したトークンの実体を作り出すこと

つまり、「鋳造」によって作られる実体とは、classに対するinstanceであり、イデアに対する現象であると言えます。

作った年賀状にNFTを対応させるまで

背景説明が長くなってしまいました。それでは実際に、年賀状を管理するNFTを発行して、あたたかい年賀状を送信しましょう!

1. 作成

まずはLINE Blockchainでサービスを作成します(割愛)。
サービスができたら、Asset > Item Token > Create New から、新規のNFTを作ります。

NFTが生まれた!
NFTが生まれると、Contract IDとToken Typeが決定します。

2. 鋳造

さて、実際にユーザーから年賀状を作りたいというリクエストが来たタイミングで、所有権を発行しましょう!
なお、ここからはAPI操作をします。APIの呼び出し方はDocumentを参照してください。

鋳造リクエストをする

Non-fungibleアイテムトークンを鋳造するAPIを使用して、新しいNengajoOwnershipを鋳造します。暗号鍵を管理するkeyに使うために、鋳造したNFTのToken IDを取得することが目標です。

ここで、userId は差出人のLINE User IDです。(年賀状の作成時には、まだ所有権は差出人にあるため)

type TransactionResult = {
  txHash: string;
};

const contractId = CONTRACT_ID_NENGAJO_OWNERSHIP;
const tokenType = TOKEN_TYPE_NENGAJO_OWNERSHIP;
const path = `/v1/item-tokens/${contractId}/non-fungibles/${tokenType}/mint`;

const mintResult = (
  await this.build<TransactionResult>({
    method: 'POST',
    path,
    body: {
      toUserId: userId,
      name: TOKEN_NAME_NENGAJO_OWNERSHIP,
      ownerAddress: LINE_BLOCKCHAIN_PPP_WALLET,
      ownerSecret: LINE_BLOCKCHAIN_PPP_WALLET_SECRET,
    },
  })
).data.responseData;

自前のSDKの記法ではありますが、まずはこういう感じです。ちなみに自前SDKはaxiosをwrapしています。

ここで注意することは、鋳造リクエストを送ったあと、実際にネットワーク上で鋳造が行われるまでしばらく待たないといけないことです。
リクエスト時に確定した txHash を用いて、トランザクションの様子を確認しましょう。APIはトランザクションを取得するを使用します。

getRawTransaction(
  txHash: string
): Promise<BlockChainAPIResponse<RawTransaction>> {
  return this.build({
    method: 'GET',
    path: `/v1/transactions/${txHash}`,
  });
}
型情報

注) ハッカソン中に勘で作った型なので、正確性はやや怪しいです。

export type BlockChainAPIResponse<
  T extends Record<string, unknown>
> = AxiosResponse<{
  responseTime: string;
  statusCode: number;
  statusMessage: string;
  responseData: T;
}>;
export type RawTransactionLogEvent = {
  type: string;
  attributes: {
    key: string;
    value: string;
  }[];
};
export type RawTransactionLog = {
  msgIndex: number;
  log: string;
  events: RawTransactionLogEvent[];
};
export type RawTransaction = {
  height: number;
  txhash: string;
  logs: RawTransactionLog[];
};

LINE Blockchain APIの返り値型には常に statusCode が指定されていて、トランザクションが正常終了したときは 1002 です。

今回は statusCode1002 になるまでシンプルに確認しつづけることにしました。本当はもうちょっとsleepとかしたほうがいいかもしれません。

let txDataResult: RawTransaction | null = null;

while (true) {
  const tempData = (await this.getRawTransaction(mintResult.txHash)).data;
  const code = tempData.statusCode;

  if (code !== 1002) {
    txDataResult = tempData.responseData;
    break;
  }
}

トランザクションが正常終了したことを確認したら、トランザクションに記録されたEventを参照することでToken IDがわかります。
Eventにはtypeが決まっていて、鋳造結果はtypeが mint_nft のイベント内に attributes の形で格納されています。

const events = txDataResult.logs.map(lo => lo.events).flat();
const mintEvent = events.find(e => e.type === 'mint_nft');
const mintedTokenId =
  mintEvent?.attributes.find(attr => attr.key === 'token_id')?.value ?? '';
const mintedContractId =
  mintEvent?.attributes.find(attr => attr.key === 'contract_id')?.value ??
  '';

これで鋳造したNFT(NengajoOwnership)のToken IDがわかりました!おめでとうございます!
実際には、このToken IDをkeyとして、DataArmor上に暗号鍵を保存していますが、その部分は割愛します。

Proxyの設定 ―送達のために

さて、ここでひとつ罠がありまして、作成したNengajoOwnershipは、そのままではサービスウォレットの権限でユーザー間の転送ができません。つまり、年賀状の送達ができません。由々しき事態です。
これを実現するためには、ユーザー側から、「私の持つNengajoOwnershipについては、全部サービスに管理を委任します〜」という登録をしてもらわないといけません。これを Proxy の設定と呼びます。

Proxyの設定は以下の手順で行われます。

  1. サービスから、Proxy設定のためのセッショントークンをネットワークに対しリクエストする
  2. ネットワークから、セッショントークンリダイレクトURLが返却される
  3. ユーザーをリダイレクトURLに誘導し、承認の操作を行ってもらう
  4. サービスから、上記セッショントークンを用いて、Proxyの設定トランザクションを確定(コミット)する

順番に処理していきましょう。

まず、 アイテムトークンの管理権限を委任するためのセッショントークンを発行するを用いてセッショントークンを作ります。

ここで userId は差出人のLINE User IDで、 contractId は対応するNFTのContract IDです。

type ProxySessionResponse = {
  requestSessionToken: string;
  redirectUri: string;
};

async requestProxySessionToken(
  userId: string,
  contractId: string
): Promise<ProxySessionResponse> {
  const queryParams = new Map();
  queryParams.set('requestType', 'redirectUri');
  const landingUri = `${clientHost}/path-to-your-redirect-page`;

  const sessionRes = await this.build<ProxySessionResponse>({
    method: 'POST',
    path: `/v1/users/${userId}/item-tokens/${contractId}/request-proxy`,
    parameters: queryParams,
    body: {
      ownerAddress: LINE_BLOCKCHAIN_PPP_WALLET,
      landingUri,
    },
  });

  return sessionRes.data.responseData;
}

返却された requestSessionToken を一旦保管しつつ、ユーザーを redirectUri に飛ばして承認操作をしてもらいます。
(ちなみにこの requestSessionToken の保管にもDataArmorを使用しました!DataArmor様様だ〜)

ユーザーの操作が終わったかどうかの確認をしましょう。API: トークンproxy設定用のセッショントークンの状態を取得する

type ProxySessionStateResponse = {
  status: 'Authorized' | 'Unauthorized';
};

async validateProxySessionToken(
  requestSessionToken: string
): Promise<boolean> {
  const sessionStateRes = await this.build<ProxySessionStateResponse>({
    method: 'GET',
    path: `/v1/user-requests/${requestSessionToken}`,
  });

  const sessionStatus = sessionStateRes.data.responseData.status;
  return sessionStatus === 'Authorized';
}

ユーザーの承認が終わっていたら、実際にProxyを設定しましょう! API: ユーザーウォレットの署名トランザクションをコミットする

async commitProxySessionToken(requestSessionToken: string): Promise<void> {
  await this.build<TransactionResult>({
    method: 'POST',
    path: `/v1/user-requests/${requestSessionToken}/commit`,
  });
}

以上で、サービス側から、ユーザーの年賀状NFTを自由に移動することができるようになりました!長い道のりだった...

年賀状を宛先へ一斉送信する

1月1日になりました!あけましておめでとうございます!紅白の優勝チーム、意外でしたね〜〜!
さて、新年気分も冷めやらぬ中、我々にはみなさまの年賀状を送達するという重大な責務があります。早速行っていきましょう。

...と言いたいところですが、この部分はハッカソン中に終わりませんでした!残念...
以下には、おそらくこういう実装になるだろうというものを書きます。間違っていたらすみません。

使用するAPIは 委任されたnon-fungibleアイテムトークンを転送する(ユーザーウォレット) です。

async transferNengajo(
  fromUserId: string,
  toUserId: string,
  tokenIndex: string
): Promise<TransactionResult> {
  const contractId = CONTRACT_ID_NENGAJO_OWNERSHIP;
  const tokenType = TOKEN_TYPE_NENGAJO_OWNERSHIP;
  const path = `/v1/users/${fromUserId}/item-tokens/${contractId}/non-fungibles/${tokenType}/${tokenIndex}/transfer`;

  const apiResult = await this.build<TransactionResult>({
    method: 'POST',
    path,
    body: {
      toUserId,
      name: TOKEN_NAME_NENGAJO_OWNERSHIP,
      ownerAddress: LINE_BLOCKCHAIN_PPP_WALLET,
      ownerSecret: LINE_BLOCKCHAIN_PPP_WALLET_SECRET,
    },
  });

  return apiResult.data.responseData;
}

これで年賀状を送れるはずです。成し遂げましたね!!

あとがき

ということで、LINE Blockchain上でサービスを作成し、Non-fungible tokenを設定して、年賀状の所有権を預けて管理するにあたっての技術的な詳細をお伝えしました。
ドキュメントを読んでの勉強などはハッカソンの開発期間前から行って準備していたのですが、なかなか複雑で苦労しました...

とはいえひとまずは形にでき、さらにはLINE Blockchain賞という形で提供元の方に認めていただいて、たいへん嬉しく思います!
この技術記が、誰かのブロックチェーン開発に役立つこと、ブロックチェーン業界の発展に寄与することを願って、筆を置かせていただきます。
それではまた!

追記

なお、その後知ったのですが、ブロックチェーン上でのスマートコントラクトという技術を用いることで、ある条件下でのみ実行されるトランザクションが作れるようです。これを用いれば、「1/1になると自動で送信される」が実現できる気がしますが、執筆時点(2021/3/31)ではLINE Blockchain上では作成できないようです。まだまだ進化の余地があって最高ですね!

Discussion