プログラミング初心者が、量子コンピュータとブロックチェーンを使って"ノープラン"でNFTアプリをローンチした話
こんにちは、シュレディンガーのポエマーです。
量子コンピュータとブロックチェーンを使って俳句NFTを生成するアプリ、HaiQを開発したので紹介したいと思います!
(色々なトピックを詰め込んだので興味のあるパートだけ読んでください。)
良かったら覗いてみてください〜!無料でHaiQを取得できます!
世界中からアクセスいただいて面白い!
HaiQでは、今バズっている言葉やトレンド、流行ってる言葉から量子コンピュータが俳句を生成し、ブロックチェーンではその生成した俳句をNFTとして発行する役割を持ちます。
アプリの使い方、NFTの発行の仕方はこちら
HaiQのアーキテクチャ
HaiQの仕様
- バズってる32個のwordを用意し、それぞれのwordを5bitの乱数にmapする。これを3セット(五七五のため内訳は5文字のwordが2セット7文字のwordが1セット)用意する。
- 量子コンピュータに乱数を取得するJobを定期時刻に3セット投げる。(それぞれのJobは32個のword1セットに対応)
- 各乱数の順位が取得できるので順位が同じもの同士で五七五(HaiQ)を生成する。
- ユーザーはMyPageからMintボタンを押すことでHaiQをランダムにNFTとして手に入れることができます。
開発に用いた技術スタック
TypeScript, Python(GCPのCloud Functionsから量子コンピュータのためのオープンソースのSDK"Qiskit"を使うため)
Next.js
Firebase(Firestore, Storage, Functions, Authentication), Polygon(EVM互換のブロックチェーン), IPFS(分散的にHaiQの画像をホストするため)
NFT? 量子コンピュータ? なんそれ
NFTは唯一性のあるトークン
ビットコインやイーサリアムのような暗号資産をご存知の方は、以下の説明でご理解いただけるかと思います。
AさんがEtherを1枚、BさんがEtherを1枚持っていた場合、当然ながらそれらを交換しても代替可能なため同じ価値を持ちます。
しかし、AさんがとあるNFTを1枚、Bさんが同じ発行元から発行されたNFTを1枚持っていた場合、それらは代替不可能のため同じ価値を持ちません。tokenIdその他パラメータが追加され、1枚1枚が異なる情報を持つのが理由です。よってNon Fungible Tokenと名付けられています。
扱い方はビットコインやイーサリアムのような暗号資産同様、対応しているウォレットで管理することができます。(もちろん誰かに送ったり売ったりできます。)
HaiQのWebサイトはVercelでホストされてますが、何かあってサイトが消えてしまってもHaiQのNFTはブロックチェーン上に存在するため残り続けます。 すごいね!
量子コンピュータはもう実機が存在し、誰でもアクセスできる
量子コンピュータは数々の大企業や研究機関が開発しており、クラウド上に公開されているものも多くあります。自分が構築した量子回路をクラウド上の量子コンピュータに投げることでJobを実行し、終了すると結果が返ってきます。
公開されているとはいえど、発展途上であり、有用なタスクで現在のコンピュータの性能を上回った事例はありません。
※量子コンピュータは量子力学で動くコンピュータのことを言います。
今回は数ある量子コンピュータのうち、量子力学で動く回路モデル、IBMが提供するIBMQを採用しました。
量子力学が生み出した巨大な産物に触れ、1人でも興味を持っていただけたなら幸いです。また、このアプリで私には一切お金は入らないのでご安心ください。むしろ運営費で赤字です。(no planさんに負担していただいています。)
(量子コンピュータ導入の細かい話は別記事で)
HaiQを作るに至った経緯
僕は普段は量子情報と呼ばれる量子力学を情報分野に活かそうという研究をしています。(研究の方向性的には量子コンピュータより量子インターネット)
が、以前よりブロックチェーン技術には興味があり色々とキャッチアップしていましたが、開発の独学を行おうにも数値解析くらいでしかプログラムを扱わない僕にとってはわかりやすい資料も多くはありませんでした。
そこでブロックチェーンを世界最前線で研究開発されている no plan inc.という会社でたまたまインターンを募集しており、運良く参加させていただけたのでそこでWebの開発からブロックチェーンの開発フローまで学び、アウトプットとしてDAppをローンチしたという流れです。
量子情報という学問が生み出した産物の一つである量子コンピュータと、今をときめく?NFTでコラボして面白いものが作れないかということと、量子コンピュータは専門家のものだけではなく、今可能な形で量子コンピュータに触れつつ楽しめるアプリが作れるといいなという思いで開発しました。
開発について
5月から10月にかけて、CTOの方中心にレビューいただき学びながら開発させていただきました。(贅沢すぎマシタ)
ポイントとして、
- インターンの方針として学業優先ということで、研究を普段通り行いつつ、余った時間で開発を行う。おかげで学会や論文執筆と両立ができました。
- つまづいた部分の質問はいつでもでき、おかむーさん, serinuntiusさんの方への隔週報告が義務であとは基本的にはノープランで進めてOK。強制力が働かず、研究の状況で柔軟に開発にかける時間を変えられたので助かりました。
- 基本的にキーワードや大まかな方向性を示していただき、自分で調べ、考えながら必要なツールを探して開発を行う。自分主体で考え解決していく力が研究でも開発でも重要だというのを再認識しつつ、未熟な状況ではつまづく箇所が多く、質問がいつでもできるというのは有り難すぎる環境でした。
- 必ずしも細部の箇所まで指摘されない。これはかなり助かった部分で、私のコードは細かいところまで含めると改善できる箇所は無数にあったにも関わらず、重要度の高いポイントを優先的にレビューいただく形で、初学者のキャパシティーを考えてくださりました。(研究室から帰って夜からの開発が通常であったので時間の制約的にもありがたかったです。)
GitHub Flowで開発を進めました。全ての進捗はGithubで各作業ブランチを切ってプルリクし、レビューをいただきながら承認を得られたらmainブランチへマージするというものです。また、開発Taskをチケットで管理し、進捗状況を一眼でわかりやすくしていました。(私のわかりづらく読みづらいコードをレビューし続けてくださいました...すんません...)
5月は基本的にGitの扱い方から簡易的なWebアプリを作るといった内容でした。
そして6月から10月にかけてHaiQを開発していきました。
開発をする上で印象的だったポイント
フロントエンド
言語はTypeScript、フレームワークはNext.jsを用いて開発しました。UIはChakra UIを用いています。
TypeScriptには型でたくさん怒られました。数値を主として扱う自分としてはanyを避ける度に自分を褒めます。
Next.jsの機能はふんだんに使いました。Next.jsの凄さがわかった気がします。
HaiQの各詳細ページでは動的ルーティング、getStaticPathsとgetStaticPropsを用いてISR(Incremental Static Regeneration)を実装しています。
export const getStaticPaths = async () => {
const paths = await getAllHaiQIds();
console.log(paths);
return {
paths,
fallback: 'blocking',
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
if (!params) {
return {
notFound: true,
};
}
const postData = await getHaiQData(params.id as string);
if (!postData) {
return {
notFound: true,
};
}
console.log(postData);
return {
props: {
postData,
},
revalidate: 1,
};
};
HaiQは俳句の画像を生成しますが、縦書きの扱い方が厄介でした。
俳句なのでまず筆書きのようなフォントですが、はんなり明朝というフォントを採用しました。
(欲を言えばもっと筆感ゴリゴリのフォントがあれば良かったです。)
そして縦書きです。
writing-mode: vertical-rl;
縦書き自体は難しくないのですが、画像化すると文字が重なって崩れたり、HaiQの短冊ではOGPの横長サイズに適応しないなど、扱いが非常に難しかったです。
前者の問題は、伸ばし棒が横のままになっている問題が残ってしまいましたが、これは昔の日本人が、普通の文字はそのままなのに伸ばし棒「ー」だけ90°回転させるという例外処理にしてしまったため、諦めました。
後者の縦長な短冊のためOGPに適応できないという問題は、HaiQ用の画像、OGP用の横長画像を両方生成するというゴリ押しの手法で実装しました。
インターネットは縦書きに厳しすぎる
Firebase
アーキテクチャの図にも示されていると思いますが、Authentication, Firestore, Storage, Functionsを用いています。
onSnapshotがすごい
dbに書き込みが起きたら自動でフロントエンドに反映してくれます。
useEffect(() => {
haiQsRef.onSnapshot(async (snapshots) => {
const haiQPromises = snapshots.docs.map(async (change) => {
const userRef = change.data()['userRef'];
console.log(change.data());
const userDoc = await userRef.get();
console.log(userDoc.data());
const userData = userDoc.data();
const id = change.id;
const imageUrl = await firebase
.storage()
.ref(change.data().imagePath)
.getDownloadURL();
const sphereUrl = await firebase
.storage()
.ref(change.data().spherePath)
.getDownloadURL();
const profileImageUrl = userData.profileImageUrl;
const likeCount = change.data().likeCount;
return {
id,
imageUrl,
sphereUrl,
profileImageUrl,
likeCount,
haiQ: change.data().haiQ,
imagePath: change.data().imagePath,
spherePath: change.data().spherePath,
title: change.data().title,
userRef: change.data().userRef,
};
});
const haiQs = await Promise.all(haiQPromises);
console.log(haiQs);
setHaiQs(haiQs);
setLoading(false);
});
}, []);
runTransactionがすごい
常に整合性を担保して最新データに対してトランザクションが実行されます。
db.runTransaction(async (transaction) => {
//transaction
}).then(() => {
setIsMinting(false);
console.log('Transaction successfully committed!');
}).catch((error) => {
toast({
title: `Transaction failed`,
status: 'error',
isClosable: true,
});
setIsMinting(false);
console.log('Transaction failed: ', error);
});
Cloud Functionsで量子コンピュータのJob実行時間の不安定性をカバー
Cloud Functionsは量子コンピュータへJobを投げるために使用しました。
3つのサービスを用いて"量子コンピュータの現状の弱点である処理の長さ"を、"非同期処理"という形でカバーしました。
例えばこのJobでは16分ほどかかっています。(queueの状況によっては1時間かかったりも)
そのため、量子コンピュータでのJobの処理時間の長さと、Functionsの関数のキャパシティーを考えて、関数を
- Job(量子回路)を投げる
- Jobの実行状況をQiskitに用意されたjob_monitorで取得し、乱数セットを取得
- HaiQを生成
の3つに分けて実装しました。
スマートコントラクト開発
ブロックチェーンは自己主権性、耐検閲性、耐改ざん性を兼ね備えた状態マシンを目指しており、みんなが見える場所、空中に処理の記録を固定することができます。そしてプログラムも空中に固定することができ、これをスマートコントラクトと言います!すごい!
no plan inc.独自の最強ブロックチェーン学習カリキュラムに従って勉強
ブロックチェーン、スマコンに特化した資料から
フロントエンドからスマコン呼び出し方まで!!
残念ながら詳しい中身はお見せできませんが、スマコン開発のパートを書いていきます。(もう少し詳しい開発フローは別記事で書くかも)
スマコン開発環境として、Hardhat
階層としては以下のような形で開発を進めました。
haiq
├ contracts
├ frontend
スマートコントラクトはSolidityという言語で書いています。
基本的にはopenzeppelinが提供するERC721と呼ばれるNFTの規格に沿って書いています。
HaiQの画像本体はIPFSと呼ばれる分散型ストレージサービスで保持しています。
また、IPFSでホストされている画像にアクセスするのは時間がかかり、ユーザーからするとUXが悪くなります。 そこで画像はIPFSに保存されると同時に、GCPのStorageにも保存されています(ハイブリット型)。これによりWebサイトで表示される画像はGCP Storageから引っ張ってきているものになり、ストレス無くHaiQを見ることができます。 (なるほどなぁ。)また、HaiQ詳細ページからはIPFS上にホストされたHaiQにアクセスし、見ることができます。
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract HaiQ is ERC721, ERC721Enumerable, ERC721URIStorage, ReentrancyGuard {
mapping(uint256 => string) internal _tokenURIs;
constructor() ERC721("HaiQ", "HAIQ") {}
mapping(string => bool) _metadataExists;
function mint(
address to,
uint256 tokenId,
string memory uri
) public nonReentrant {
require(bytes(uri).length != 0, "ERC721Metadata: URI doesn't exist.");
require(
_metadataExists[uri] == false,
"ERC721Metadata: URI already exist."
);
_metadataExists[uri] = true;
super._mint(to, tokenId);
_setTokenURI(tokenId, uri);
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(from, to, tokenId);
}
function _burn(uint256 tokenId)
internal
override(ERC721, ERC721URIStorage)
{
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
そして、TypeChainというsolidityのコードから自動で.tsを生成してくれ、型もつけてくれるプラグインを使っています。
設定としては、以下のようにhardhat.config.tsファイルで行い、typechainはoutDirで生成先を設定できます。
typechain: {
outDir: '../frontend/src/types',
target: 'ethers-v5',
alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads?
externalArtifacts: ['externalArtifacts/*.json'], // optional array of glob patterns with external artifacts to process (for example external libs from node_modules)
},
デプロイ先はEthereumではなくPolygon(Matic)で行いました。ガス代安い...
また、EVM互換であるため、ネットワークの設定変えるだけで良いです。
ただ、デプロイするタイミングで何かhypeが集まっていたのか、いきなりガスが高騰してかなり時間がかかってしまい、焦りました。
contracts % npx hardhat run scripts/deploy.ts --network matic
そして先ほども言及しましたが、スマコンのプログラムはブロックチェーン(空中)に固定されているため(+verifyしてるため)、誰でも見ることができます!感動です!
物理の研究をしている人間から見たアプリ開発
普段は物理の研究をしていますが、今回、一つのWebアプリを0から作りローンチするというインターンを経て、プログラミング以外の知見も色々と溜まりました。
Slackで軽い議論を可視化することができたり、notionで知見や共有資料を整理したり、githubでコードの歴史を管理できたりというのは研究室や他の組織でも活かせるところは多いと感じました。(俗に言うDX路線。物理科の他の研究室とかそんなDXされてないと思いますがどうでしょう。そんなオープン文化じゃないからあんまり研究室事情知らない。)
こういう研究とは本質的に異なる部分で研究スピードに差がつくのはあまり嬉しくありません。そこら辺の感度は上げておいて損はないのかなと思います。
あとはインプット→アウトプットのサイクルが速くモチベーションの維持がしやすいと言うのはあると思います。
物理の研究しててアウトプットできる場といったら学会や論文執筆くらいですし、かなり長期でのサイクルなのでモチベーションの維持は場合によってはメンタルが必要とされます。比べて、例えばOSSプロジェクトであれば、改善案がマージされたらそれで一つのアウトプットとして残り、承認を得ることがます。そしてSNSとの親和性も高いと思います。例えばToDoアプリのようなものを作って、#駆け出しエンジニアと繋がりたい というハッシュタグを添えてSNSで発信すれば、誰かが見て反応してくれたりしますよね。
やはり研究というのは、常にわからないことと向き合いますし、自己肯定感は下がっていく傾向にあると思います(私の話、他の人もわりかしそうだと思う)。なのでアウトプットを増やしていきたいと思います。
量子情報に関わる記事や、インターンで学んだことを活かしてまた面白い量子コン/ブロックチェーンアプリ作れたらいいなと思います。
no plan株式会社は、ブロックチェーン、Webサイト、iOSアプリ、AndroidアプリなどWebサービス全般の開発から運用をワンストップで行っています。
Discussion