儲かりそうで儲からない"自称"Botterの話その2「改造! Tendermint!!」
こんにちは ,Aasahiです。
この記事は仮想通貨Botter Advent Calendar2022 22日目の記事です。
初めに
今回の記事はAtomic Arb戦略の一つであるノード改造に関するひとつの簡単なアイディアを簡単に紹介しようと思います。(日本で今Atomic Arbやってる人なんか10人ぐらいしかいないと思うので需要はないですが…)
Atomic Arbにおいて,勝つために必要なことは何かと聞かれたら
「他のBotより速く養分Txを検知して,より速くアビトラTxをバリデーター(ブロックプロポーザー)にTxを届けること」
ただ,これだけです。
そのための一つに要素として,公式RPCを使わずに,自分のノードを立ててブロックチェーンに参加する方法がありますが,通常のノードクライアントを使用するだけでは,他のノードにスピードで差をつけることができません。そこで行うのがノード改造です。
自分はAtomic Arbでそこそこ稼げるようになることを目指して,主にTendermintというノードクライアントに,9~11月くらいにかけて,独自の修正を加えていました。そんな自分が行った実装の一部を紹介するような記事になります。
結果はご存知の通りうまくいきませんでしたが,いい挑戦になったかなと思ってます。なので,今回紹介する通り実装しても勝てません… つまり儲かりません😜
ちなみにRosさんはノード改造,パス裁定のRust化,独自BDNの開発などを1ヶ月半で行ってました。短期間でこれだけのことに挑戦して,実装できるその発想と技術力はさすがとしか言いようがないです!
本当にすごい!強スギィ!
(今回紹介することはRosさんの足元にも及ばない,はっきり言ってしょぼいことです)
Tendermintとは?
Tendermintとは,主にCosmos系のプロジェクトで使われるノードクライアントの一部です。(この説明で合っているのでしょうか?Cosmosガチ勢に怒られそう...)
Cosmos系のノードは大きく分けて,アプリケーションレイヤーとコンセンサスレイヤーを分かれています。p2pやコンセンサス周りを担当するコンセンサスレイヤーとTxを解釈して,ステートを更新するアプリケーションレイヤーです。このコンセンサスレイヤーに該当するソフトウェアをTendermintやTedermint Coreと言ったりします。
このように分けることで新しくL1ブロックチェーンを開発しようと思った人は,アプリケーションの開発のみに専念することが可能になります。p2pやコンセンサス周りの設計はTendermintが勝手にやってくれるからそれを利用するだけです。また,Tendermintを使用して作られたブロックチェーンはIBCなどと言って,相互に通信が可能になるなどのメリットがあるっぽいです。
Tx検知から送信まで
Atomic Arbにおいて,養分Txを検知して,アビトラTx送る必要がありますが,Tendermintでは内部どのような操作が行われているのでしょうか?実際に見ていきましょう。
RPCを使って検知し,Txを送信する
通常のTendermintを用いて作られたブロックチェーンにおいて,RPCを使用してTxを検知,送信しようとすると,以下の図のような経路を通ることになります。
Tendermintにおいては,Txがmempoolに追加される前に,そのTxが正常であるかどうかのチェック(CheckTx)がアプリケーションレイヤーを通して行われます。そして,そのチェックを通して問題ないとされたTxがmempoolに追加されます。
加えて,mempoolを監視することでTxを検知し,RPCを用いてアビトラTxをブロードキャストしようとすると,そのTxのチェック(CheckTx)がアプリケーションレイヤーでもう一度行われた後に,ブロードキャストされるような流れになります。
自分が今回やろうとしていること
自分が試みようとしたことは,Txの検知から送信まで全てTendermint(コンセンサスレイヤー)内で完結させることです。即ち,Tendermintにコードに埋め込んだり修正したりして,アプリケーションレイヤーで行われるTxのチェック(CheckTx)を二つカットすることで,Tx検知から送信までの時間が速くなるのでは?と考えました。
↓図にするとこんな感じ↓
Let's 改造
やることが決まったので,早速取り組んで見ましょう!
まずやることはTendermintのソースコード(公式はGo言語)を読み込んで,どの関数がどんな操作をしているかを理解することです。ここが異様に時間を食います。
自分が色々見た中でここのコードをいじるのが使い勝手が良さそうだと思いました。(https://github.com/tendermint/tendermint/mempool/v0/reactor.go)
func (memR *Reactor) Receive(e p2p.Envelope) {
memR.Logger.Debug("Receive", "src", e.Src, "chId", e.ChannelID, "msg", e.Message)
switch msg := e.Message.(type) {
case *protomem.Txs:
protoTxs := msg.GetTxs()
if len(protoTxs) == 0 {
memR.Logger.Error("received empty txs from peer", "src", e.Src)
return
}
txInfo := mempool.TxInfo{SenderID: memR.ids.GetForPeer(e.Src)}
if e.Src != nil {
txInfo.SenderP2PID = e.Src.ID()
}
var err error
for _, tx := range protoTxs {
ntx := types.Tx(tx)
err = memR.mempool.CheckTx(ntx, nil, txInfo)
if errors.Is(err, mempool.ErrTxInCache) {
memR.Logger.Debug("Tx already exists in cache", "tx", ntx.String())
} else if err != nil {
memR.Logger.Info("Could not check tx", "tx", ntx.String(), "err", err)
}
}
default:
memR.Logger.Error("unknown message type", "src", e.Src, "chId", e.ChannelID, "msg", e.Message)
memR.Switch.StopPeerForError(e.Src, fmt.Errorf("mempool cannot handle message of type: %T", e.Message))
return
}
// broadcasting happens from go routines per peer
}
どんな関数かというと,Txを受け取ると,それをアプリケーションレイヤーにチェックしてもらうような処理がなされます。
18行目のmemR.mempool.CheckTx(ntx, nil, txInfo)が
先述したCheckTxがその処理に該当します。
すなわち,この処理をすっ飛ばして,任意のTxをブロードキャストできるような処理をかければ,目的が達成できますね!
それでは,やってみましょう!
まず,ブロードキャストする関数を探して,他のディレクトリから持ってきます。
ということで探してきました。多分これです。
(https://github.com/tendermint/tendermint/p2p/switch.go)
func (sw *Switch) Broadcast(chID byte, msgBytes []byte) chan bool {
sw.Logger.Debug("Broadcast", "channel", chID, "msgBytes", log.NewLazySprintf("%X", msgBytes))
peers := sw.peers.List()
var wg sync.WaitGroup
wg.Add(len(peers))
successChan := make(chan bool, len(peers))
for _, peer := range peers {
go func(p Peer) {
defer wg.Done()
success := p.Send(chID, msgBytes)
successChan <- success
}(peer)
}
go func() {
wg.Wait()
close(successChan)
}()
return successChan
}
この関数を先ほどのReceive関数に埋め込んで,完成です!
へい,お待ち!(↓完成品↓)
func (memR *Reactor) Receive(e p2p.Envelope) {
memR.Logger.Debug("Receive", "src", e.Src, "chId", e.ChannelID, "msg", e.Message)
switch msg := e.Message.(type) {
case *protomem.Txs:
protoTxs := msg.GetTxs()
if len(protoTxs) == 0 {
memR.Logger.Error("received empty txs from peer", "src", e.Src)
return
}
txInfo := mempool.TxInfo{SenderID: memR.ids.GetForPeer(e.Src)}
if e.Src != nil {
txInfo.SenderP2PID = e.Src.ID()
}
var err error
for _, tx := range protoTxs {
ntx := types.Tx(tx)
//受け取ったトランザクションから,トランザクションを生成
original_tx := hogehoge(ntx)
//接続してあるPeerにブロードキャスト
memR.BaseReactor.Switch.Broadcast(mempool.MempoolChannel, original_tx)
}
default:
memR.Logger.Error("unknown message type", "src", e.Src, "chId", e.ChannelID, "msg", e.Message)
memR.Switch.StopPeerForError(e.Src, fmt.Errorf("mempool cannot handle message of type: %T", e.Message))
return
}
// broadcasting happens from go routines per peer
}
これでアプリケーションレイヤーを経由することなく,Txをブロードキャストできるようになりましたね!
hogehoge()に関しては,Txの生成方法は人それぞれだと思うので,自分で書いてみてください!
比較
それでは,どんくらい早くなったか計測してみましょう!
今回の計測ではhogehoge()の部分は特に工夫せず,特定のTxが来たら,空のTxを送信するように書きます。
そして,3パターン用意して比較したいと思います。
パターン1: Txを受け取ったら,CheckTxを通さずにブロードキャストする
パターン2:1とほぼ同じで,ブロードキャスト時のみCheckTxを通す
パターン3: RPCを利用してmempoolを監視し,Txを送信する
1と2を比較することで,CheckTxがどの程度影響を与えているのか
1と3と比較することで,一般的な方法とどの程度差をつけられるかを測定します。
もちろん,PCのスペックなどの環境次第で計測結果は大きく変わってくることはご了承ください。今回はMacbook Pro 2020のpythonの仮想環境上(pystarportとかいうやつ)でノードを動かした場合になります。
おっ!そこそこ違いが出ましたね!
通常のRPCを使う一般的な方法が遅いのは一目瞭然ですね。
CheckTxが一回挟むだけ,自分のローカルの環境でも0.6msぐらい差があることがわかります。(PCのスペックを上げれば,こんなの誤差になるかもですが…)
最後に
ここまでは結構簡単に書いてますが,hogehoge()のところの実装が一番大事です。
例えば,
・Txを検出してから,任意のTxを生成するアルゴリズム(パス裁定,取引量最適化など)の実装
・Txの追い越しが発生しないような処理の実装
などのAtomic Arbする上での一般的な処理も必要ですし,
・TxをTendemintが通信できるような型にキャストする実装
などの通常ならば,アプリケーションレイヤーがやってくれそうな処理をTendermint側で改めて実装するというCosmos系のノード特有の処理も必要になります。加えて,Go言語の制約も色々あって,ちょ〜メンドくさかったです。
そこまでやる必要があるのかって?
必要があると「信じて」やるんです😡!!!!
ちなみに,自分はAtomic Arbで勝つべく,(Rosさんには全く及ばないけど)この他にも色々やってみましたが,他のAtomic Arb勢にかないませんでした…
「信じて」やった結果がこれですよ🤮
ここまで書いておいてアレですが,ノードの内部処理の改造よりネットワーク周りの検討をした方が絶対勝てるようになると思います。そっちの方が削れる秒数が圧倒的に多いです。
最近は市況的にRosさんのレベルで出来高がギリギリ?プラスになるくらいらしいので,自分が参戦しても稼げるわけないですよね…
余談: Huff Languageについて
最初はAdvent CalenderにHuff Langanuage に関することを書こうと思っていたのですが,とっても凄そうな方が素晴らしい記事を書いてくださいました!
なので,自分が途中まで書いたゴミみたいな記事はお蔵入りです。
😇👋<サヨナラ~
自分は11月後半頃,たまたまHuffの存在を知って,少しHuffでスマコンを書いたりしていたので,感じたことを3つほど共有させていただきます。
「32byteが正義」
HuffでスマコンをかくとABIを無視した形でTxのinput領域を記述できます。しかし,OPCODEが32byteづつ解釈するもの(calldataloadやmload)が多いので,変に縮めてかくと,そのinputの部分を32byteに変形する作業が必要になって,その分手間が増えてGas代が余計にかかるという印象です。
「それほどガスを節約できない」
Huffでbot用のスマコンを書いたとしても,Atomic Arbや Liquidationを実際に実行する部分は外部,すなわち,Huffで書かれていない公式コントラクトを実行するので,そこを節約できるわけではないのです。
ただ,スマコン内部でパス裁定や取引量の計算などを行っている場合は,そこら辺の処理を低レベルから書けるので,Huffが効いてくるかもしれないです。
「For文書くのにコツがいる」
基本スタックマシンを操作するだけなので,For文なんて高級なものはもちろん用意されていません。SWAP,DUP,JUMPあたりを駆使して,繰り返しの処理を実現させる必要があります。EVMの気持ちになりましょう!
あとがき
12月初めに,内定先から
「暗号資産触ったらどうなるか….わかるよね?(意訳)」
と言われてしまい,Botterとして,お先真っ暗な状況に追い込まれてしましました。
ただ,リプをいただく中で,「まだワンチャンあるのでは?」と思えてきたので,3月まではAtomic Arb以外のことを挑戦しつつ,仮想通貨以外にプログラムを書いて稼ぐことができる方法を探っていこうかなと思ってます。
と言って何も思いつきません!!誰か教えてください(切実)
競馬予想Bot??アフィ?? 何もわからん…
もしダメだったら4~5月にお気持ちポエムを書いてBotter人生終了です。対戦ありがとうございました。