気難しいBlockchainへの対処法

この記事は[レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)の24日目の記事となります。

こんにちは
株式会社レコチョクでweb3関連のバックエンドの開発をしている荻原と申します。

この記事が公開されるのは12月24日ですが、みなさんのお気に入りのクリスマスソングはなんですか？自分はMariah Careyの「All I Want for Christmas Is You」が流れてきたら必ず口ずさんでしまい、一生頭から離れません。
Aメロ？の「I don't want a lot for Christmas There is just one thing I need」のあとエンダあ〜〜〜〜〜のハモリの方に行くまでがお決まりです。

さて、自分は業務ではNFTのスマートコントラクトやNFTをmint、transferするAPIを開発しています。

タイトルの「気難しいBlockchain」という部分に関しては、共感してくれる方もいればどういう意味だ？と思った方もいるかもしれません。「気難しい」というのはあくまで私自身の感想なのでまず最初に説明させてください。

ご存知の通り、Blockchainは非中央集権の仕組みによりデータの偽造や複製が実質不可能といった特長があります。しかし開発する中で、それと引き換えに処理に時間がかかったりノードによって調子が悪かったりという一筋縄ではいかない場面に直面してきました。細かいことを言うと処理に時間がかかるのはBlockchainの同一時間帯の利用者が多いことによるものであったりノードの調子が悪いのもノードの問題ですが、ユーザー目線ではBlockchainは気難しいなーと感じることが度々あったため、このようなタイトルにしてみました。

今回はそんなBlockchainを利用する上で遭遇した困難とどのように対処しているかということを紹介します。

## 目次
[1. 開発環境](#1-開発環境)
[2. 処理に時間がかかる&並列処理ができない](#2-処理に時間がかかる並列処理ができない)
[3. nonce too low](#3-nonce-too-low)
[4. 429 Too Many Requests](#4-429-too-many-requests)
[5. ContractLogicError](#5-contractlogicerror)
[6. ガス代が急変動していることによって不足する](#6-ガス代が急変動していることによって不足する)
[7. Gas Stationが使えなくなる](#7-gas-stationが使えなくなる)
[8. さいごに](#8-さいごに)

## 1. 開発環境
今回紹介するのはPolygonまたはそのテストネットのMumbaiにおける内容です。
利用しているライブラリ等のバージョンは以下の通りです。Blockchainとの疎通には[web3.py](https://web3py.readthedocs.io/en/stable/)を利用しています。

```
openzeppelin/contracts 4.9.3
solidity 0.8.19
web3.py  5.31.0
```

## 2. 処理に時間がかかる&並列処理ができない
最初に紹介した通りで、トランザクションの処理には時間がかかります。トランザクションの内容やBlockchainの混み具合にもよりますが、自分の経験上NFTのmintやtransferでは1トランザクションあたり30秒〜1分程度かかっていることが多かったです。Blockchainが混雑しているときなどはもっとかかることもあります。

レコチョクの[mupla](https://mupla.jp/)では、ユーザーはMATICやEOAを管理する必要がなく、レコチョク側で一括管理するという仕組みを取っています。そのため、1つのEOAがトランザクションを発行していました。

そして1つのEOAは1つのトランザクション処理中に並行して別のトランザクションを発行・処理することはできません。仮にトランザクションが完了する前に別のトランザクションを発行すると後者で処理が上書きされ、1つ目のトランザクションは破棄されます。

私たちのシステムではトランザクションのシングルプロセスでの発行を実現するために、APIでは通常と同様にリクエストを受け付け、その後非同期に1EOAに対してシングルプロセスでトランザクションを発行するという形を取っています。

トランザクション処理に時間がかかるということに対しては、1トランザクションあたりの処理時間を変えることはできませんが、トランザクションを発行できるEOAを複数用意することで複数トランザクションを発行した際に全体としてかかる時間を短縮することができています。

![スクリーンショット 2023-11-29 10.57.16 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534740/3825c527-3081-7858-9791-e92066b14062.png)

また、トランザクションの処理に時間がかかる原因として、設定したガス代が低すぎたために優先度が下がり、処理されない場合もあります。そのため、処理に時間がかかっている場合は自動でガス代を再設定しトランザクションを上書きできるようにしています。

## 3. nonce too low
これはRPCエンドポイントのレスポンスで返されるものです。
自分は一番よく遭遇してきたエラーの1つです。Blockchainあるあるを言う機会があったら早く言いたいです。

nonce too lowの説明の前にまず、私たちのシステム上でのトランザクションを発行する流れについて説明します。[web3.py](https://web3py.readthedocs.io/en/stable/)を使った実装に則って説明しています。また、図の右側は対応するJSON-RPC methodです。

![スクリーンショット 2023-11-29 19.03.04 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534740/b29ce9e2-af77-ee63-2e37-92b5db9ab9fd.png)

最も重要なのは上から4番目の`eth_sendRawTransaction`で、これを実行することでBlockchain上でトランザクションが発行されます。逆に言えば`eth_sendRawTransaction`を実行する前もしくは実行時にエラーになるとトランザクションは発行できていないということになります。そのため`eth_sendRawTransaction`が実行できていればBlockchain上でのトランザクションの進行状態を確認し、完了すれば終了、`eth_sendRawTransaction`が実行できていなければ再度自動でトランザクションを発行するという仕組みを取っていました。

nonce too lowが発生する流れとしては、まず`eth_sendRawTransaction`を叩く際に指定するnonceをBlockchain側から`eth_getTransactionCount`を利用して取得します。そのEOAがこれまでに発行したトランザクション数を数え、それを指定します。例えば100個トランザクションが発行されていたら次に発行するトランザクションのnonceは100です。

nonceは重複したり決まった値より大きいものを指定したりすることはできません。そのため、指定したnonceですでにトランザクション処理が完了している場合にnonce too lowが発生します。

発生するパターンとしては大きく3つありました。

### トランザクション完了後、ノード間の伝播が遅く、アクセスしたノードで更新前のnonceが取れてしまう

![スクリーンショット 2023-11-29 19.06.51 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534740/1ca4baf5-7e76-e7a1-9dd7-758436cdc55d.png)

### 前のトランザクション完了前にnonceを取得し、トランザクションの上書きをしようとしたところ、`eth_sendRawTransaction`実行直前に前のトランザクション処理が完了し、先に取得していたnonceが古いものになってしまう

![スクリーンショット 2023-11-29 19.25.31 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534740/e3313344-e725-1aca-f150-4bd784a3efff.png)

これら2つのパターンには、nonceを`eth_getTransactionCount`を使ってBlockchainから取得するのではなく、システム内のDBでトランザクションが完了しているnonceを保持しておき、そこから取得することで対処しています。1つ目のパターンはトランザクション完了時にDB上のnonceが更新されるので未然に防ぐことが可能です。2つ目は一度nonce too lowは出ますが、その後自動でトランザクションの進行状態を確認し、完了していればDBのnonceを更新することで自動で対処が可能です。

### `eth_sendRawTransaction`でトランザクション発行成功にしているにも関わらずnonce too lowのレスポンスが返ってくる

こちら関しては、それは聞いてないよです。ノードのバグなのか詳しい原因は不明ですが、まさにBlockchainの気難しいところが出てしまっていますね。

前提としていた「`eth_sendRawTransaction`でエラーが返ってきたらトランザクション発行に失敗している」を崩すものです。トランザクションが発行されてnonceの値が変わっていますが、DB上では変わっていないので次にトランザクションを発行しようとすると本当のnonce too lowになってしまいます。
このような実装にしていた理由は、トランザクションが未発行であれば進行状態をチェックする必要がないので無駄な処理を省くためでしたが、`eth_sendRawTransaction`のレスポンスに関わらずすべてのトランザクションの進行状態をチェックすることで対応しました。

## 4. 429 Too Many Requests
これは通常のAPIを叩く際にも発生するものと同様で、RPCエンドポイントから返されます。
Read Contractも含め、短時間にRPCエンドポイントに対して制限を超えるリクエストをすると発生します。

前章の図からわかる通り、1つのトランザクションを発行するにもRPCエンドポイントへ何度もリクエストをしています。できるだけ短時間に多くのリクエストを実行したいところですが、そうするとこのエラーが発生してしまいます。

このエラーへの対処法としては例えばSaaSを使っている場合はプランを変える、RPCエンドポイントの接続先を分散する、自分でノードを立てる等が考えられますが、現状はリクエストが集中することが多くないため、2つのRPCエンドポイントに分散し、かつこのエラーの発生時に自動で再実行する仕組みを取っています。

## 5. ContractLogicError
これは[web3.py](https://web3py.readthedocs.io/en/stable/)で定義されているExceptionです。スマコンで実装している`require()`に引っかかった際等に出ます。
例えばOpenZeppelinのERC721を転送する際に転送元に誤ったEOAを指定すると`ERC721: transfer from incorrect owner`（[OpenZeppelin ERC721](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/49c0e4370d0cc50ea6090709e3835a3091e33ee2/contracts/token/ERC721/ERC721.sol#L366)より）と表示されます。

これはリクエストエラーではあるのですが、nonce too lowと同様にBlockchainの処理が遅いことが原因の場合があります。

例えばNFTの転送の場合を考えてみます。とあるtokenをEOA：A→Bに転送しようところ、トランザクション処理に時間がかかっているためガスを上げてトランザクションを上書きするとします。その際、2度目の`eth_sendRawTransaction`の直前にトランザクション処理が完了しているとtokenの所有者はBとなっているにも関わらずAを転送元として指定していることになるためContractLogicErrorが発生します。

![スクリーンショット 2023-11-29 19.27.49 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534740/d647fae6-ebb5-3f2b-da1e-29483767aba6.png)

このようなパターンのContractLogicErrorにも、エラーが出た際に自動でトランザクションの進行状態をチェックして完了していれば終了にできる実装にしておくことで対応しています。

## 6. ガス代が急変動していることによって不足する
ガス代が急変動していると、Gas Stationから予想のガス代を取得した後に適正値が変わりエラーになることがあります。

エラーの種類は色々あります。

### replacement transaction underpriced
これはRPCエンドポイントからのレスポンスです。
Pending状態のトランザクションの上書きをする際に1回目に指定したガス代よりも2回目に指定したガス代が安くなっており、トランザクションが発行できずこのエラーが返ってくることがあります。

ガス代を再設定するのは、トランザクション実行の優先度が下がっている場合にガス代を上げることで処理を速めるためだということが前提となっているため、エラーになると思われます。

![スクリーンショット 2023-11-20 18.47.02 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534740/cc58e0b0-9427-73f5-c1f9-5da23cc34485.png)

### max fee per gas less than block base fee
こちらはERC1559のmax fee per gasを取得した後にblock base feeが上がり、指定したmax fee per gasがblock base feeを下回ると発生します。block base feeとはBlock毎に1つ前のBlockの混み具合から決められる基本料金のようなものです。これにmax fee per gasで指定した値を超えない範囲で手数料が上乗せされます。そのため、max fee per gasがblock base feeを下回っているとトランザクションの実行ができないため、エラーとなります。

### out of gas
ガス代が上がってしまい、途中で設定した上限に達するとトランザクションが完了せず、このエラーが返ります。ちなみに、ガスは使ってしまっているのでトランザクションが完了しなくてもガス代は戻ってきません。

これらのパターンでエラーが出たときは、自動で最新のガス代を再取得し再実行することで対応可能です。

## 7. Gas Stationが使えなくなる
これはBlockchainの調子は全く関係ないですが、一度PolygonのGas StationのURLが突然変わっており、古い方が使えなくなっているということがありました。自分の知る限り、事前のアナウンスもなくひっそりと[公式サイト](https://wiki.polygon.technology/docs/tools/faucets/polygon-gas-station/)が更新されていただけのようでした。
また、Mumbaiは証明書の有効期限切れになることが度々あります。

恐らく滅多に起きることではないですし未然に防ぐのは難しいとは思いますが、一応このようなこともあったよという紹介です。

## 8. さいごに
今回はBlockchainを利用していて困ったことを紹介しました。

色々なパターンを紹介しましたが、種明かしをしてしまえば細かいところの違いはあれど、大抵トランザクションの処理に時間がかかることが要因となりエラーが起きています。そして自動でトランザクションの状態をチェックし、上書きできるようなシステム設計にすることである程度は対応できています。

Blockchain周りの開発をしようとしている方の参考になれば幸いです。
最後まで読んでいただきありがとうございました。

私はカラオケに行ってエンダあ〜〜〜〜〜してきます。

明日の[レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)は25日目「エンジニアではない人事がエンジニア新卒採用で気を付けていること」です。
お楽しみに！

## 参考
- [openzeppelin-contracts ERC721](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/49c0e4370d0cc50ea6090709e3835a3091e33ee2/contracts/token/ERC721/ERC721.sol#L366)
- [Polygon Gas Station](https://wiki.polygon.technology/docs/tools/faucets/polygon-gas-station/)
- [web3.py](https://web3py.readthedocs.io/en/stable/)

---

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。 
 
https://techblog.recochoku.jp/10081