Monadテストネットで1,960回の取引を行いました。実際に機能するプレイブックはこちら

Dev.to / 2026/4/30

📰 ニュース

要点

  • 著者は、単純なPythonスクリプトを実行して、1ラウンドの間にスリープを挟みながら20件の取引をバースト的に繰り返し送信することで、14日間Monadテストネットで検証を行った。

3月にメメコインで$400を失いました。午前2時、デスクで人生の選択を問い直しながら座っていると、「オンチェーンに関わるなら、いっそ生産的にオンチェーンに関わればいい」と思ったんです。そこで、寝ている間にMonadのテストネットをファームするスクリプトを書きました。2週間と1,960トランザクション後に、実際にうまくいったこと──そして最初に試して失敗した3つのアプローチ──はこちらです。

2週間後、ウォレットには1,960 tx。電気代はたぶん$7増えました。そして、誰も聞いていないのに、ものすごく具体的な「nonce管理」への意見を持つようになりました。以下、全部です。

TL;DR

  • 14日で1,960トランザクション、$0消費(テストネットのMONはファセットから無料でもらえる)
  • バーストモード(20 txを20秒で投入→60秒休止)は、定常的な垂れ流しより3倍うまくいった
  • nonce管理こそが失敗の最大要因でした──ガスでもRPCでもない
  • 失敗したトランザクションでもnonceはインクリメントされる。放っておくと痛い目を見る
  • トランザクションの多様性は重要だが、自分宛の送金でまず始められた

Why Monad?

Monadは高性能なEVM L1を作っています。$225Mを調達しました。テストネットは稼働中で、ファセットが無料のテストネットMONを配っています。もし彼らが多くのL1が最終的にやることをやるなら、初期のテストネット上の活動は意味を持つ可能性があります。

私はこの件に全財産を賭けているわけではありません。このスクリプトは実行コストがゼロです。最悪の場合、電気代を少し使って、nonce管理についてを“苦い学び”として得るだけ。たぶんMonadはエアドロップしないかもしれません──これは給料ではなく賭けです。

(背景として、私はシンガポールの月$5のVPSからこれを動かしています。MonadのRPCへのレイテンシは約180msで、想像以上に重要でした──バーストで20トランザクション送ると、往復のたびに積み上がっていくからです。)

Architecture

セットアップ全体は、恥ずかしいほどシンプルです。Pythonファイル1本、ウォレット1つ、RPCエンドポイント1つ。データベースなし。分散ワーカーなし。Kubernetesクラスターなし。

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Faucet     │────▶│  Wallet      │────▶│  Monad RPC  │
│  (free MON) │     │  (signs tx)  │     │  (testnet)  │
└─────────────┘     └──────────────┘     └─────────────┘
                           │
                    ┌──────▼──────┐
                    │  Burst Loop │
                    │  20 tx/min  │
                    └─────────────┘

バーストループはこう動きます。起床して、チェーンから現在のnonceを取得し、RPCが受け付けるのと同じくらい速く20トランザクションを投げる。そして60秒眠る。これを永遠に繰り返します。もし何かが失敗したら、チェーンからnonceを更新して、そのまま続行します。

The Code

from web3 import Web3
from eth_account import Account
import json, random, time, os

with open(os.path.expanduser('~/monad/wallet.json')) as f:
    wallet = json.load(f)

acct = Account.from_key(wallet['private_key'])
w3 = Web3(Web3.HTTPProvider('https://testnet-rpc.monad.xyz'))

def run_burst(rounds=20):
    nonce = w3.eth.get_transaction_count(acct.address)
    done = 0

    for i in range(rounds):
        tx = {
            'from': acct.address,
            'to': acct.address,
            'value': w3.to_wei(random.uniform(0.001, 0.01), 'ether'),
            'gas': 50000,
            'gasPrice': w3.eth.gas_price,
            'nonce': nonce,
            'chainId': 10143,
        }
        try:
            signed = acct.sign_transaction(tx)
            w3.eth.send_raw_transaction(signed.raw_transaction)
            nonce += 1
            done += 1
            time
.sleep(1)
        except Exception:
            time.sleep(2)
            nonce = w3.eth.get_transaction_count(acct.address)

    return done

以上です。フラッシュローンも、複雑なDeFiのやり取りもありません。ランダムな金額で自分自身へ送金するだけです。なぜこれが機能するのか――そしてなぜそれだけでは不十分なのか。

学んだこと

1. ノンス管理が静かな致命傷になる

最大の失敗要因は、ガスでもRPCのタイムアウトでも接続性でもありませんでした。ノンスの衝突でした。

何が起きるかというと、RPCにトランザクション#15を投げます。まだ確定していないうちに、次のノンスを求めるためにget_transaction_count()を実行します。返ってくるのは15です――#15がまだマイニングされていないからです。そこで、ノンス15の別のトランザクションを送ります。すると、同じノンスを持つ競合するトランザクションが2つになります。片方は成功し、もう片方は「nonce too low」で失敗するか、メンプールで詰まります。

気づくまでに、このせいで約200トランザクションを失いました。対策はシンプルですが、直感に反します。自分のインクリメントを決して信じてはいけません。常にチェーンに聞いてください。

# 間違い: 楽観的にインクリメントしてしまう
nonce += 1

# 正しい: 真実の出所から更新する
nonce = w3.eth.get_transaction_count(acct.address)

# さらに良い: 例外ハンドラ内でのみノンスを更新
try:
    signed = acct.sign_transaction(tx)
    w3.eth.send_raw_transaction((signed.raw_transaction)
    done += 1
except Exception:
    time.sleep(2)
    nonce = w3.eth.get_transaction_count(acct.address)

2秒のスリープは重要です。これがないと、すぐに同じ古いノンスが返ってきて、また失敗することがよくあります。

2. バースト > 点滴

比較のために、2つのモードを並行して動かしました:

  • 定常的な点滴: 3分に1トランザクション。安定しているものの、~20 tx/時程度です。24時間の実行で約480トランザクション。
  • バースト: 約20秒で20トランザクション、その後60秒休止。RPCはバーストには問題なく対応し、60秒の休止が確定時間を確保します。24時間の実行で約900トランザクション。

バースト方式は、同じ計算リソースでほぼ2倍の処理量を生み出しました。Monadの1秒ブロック時間がスループットをうまく処理しているのですが、ボトルネックはチェーン側ではなく私の側でした。

意外な発見が1つあります。60秒の休止は、単なるレート制限対策ではありませんでした。RPCに確定の伝播時間も与えていたため、次のバーストがクリーンで正確なノンスから開始できていました。

3. 自己送金が最小限で成立するトランザクション

自己送金(自分のアドレスにMONを送る)は、オンチェーン上で行える最も単純なアクションです。トランザクション履歴を生成し、ガスを消費し、あなたがアクティブであることを証明します。開始するには、これ以上ないほど適しています。

ただし、Monadの最終的なエアドロップ基準に「トランザクションの多様性」が含まれるなら――そしてほとんどのL1エアドロップがそうです――自己送金だけでは足りません。コントラクトのデプロイ、ERC-20の送金、ガバナンス行為などは、より本物の利用を示すシグナルになります。

v2では加重トランザクションプールを追加します。自己送金50% 、ERC-20送金30%(私が制御する2つ目のウォレット宛)、コントラクトデプロイ15%、デプロイ済みコントラクトとのランダムな相互作用5%です。重み付けにより自然に見せつつ、それでも自己送金量をベースとして維持します。

数字

日付 追加したトランザクション 累計
開始 0 0
2日目 +561 561
4日目 +35 596
5日目 +1,364 1,960

このデータから、目立つのは2つの点です。

1つ目は、2日目→4日目の間隔です。あの35トランザクションは、スクリプトがゆっくり動いていた結果ではありません。2つの別々の失敗によるものでした。あるときRPCエンドポイントが私をスロットリングし、6時間気づきませんでした。別のとき、スクリプトが未処理の例外でクラッシュしました(設定ファイルのパスのタイプミス――なぜか「すべてのもの」の中でそれ)そして、朝に確認するまで動かない状態のままでした。単純なヘルスチェックなら両方を検知できたでしょう。

2つ目は、596から1,960への5日目の急増です。これは、バーストモードに切り替えたタイミングです。バーストあたり20トランザクション × 数時間にまたがる60回超のバースト = 1セッションで1,200トランザクション以上。結論として、ボトルネックはチェーンではありませんでした。最初の実装の私の側がボトルネックでした。

失敗したこと

  • 間違ったRPC URL: 23時頃に、トランザクションが「nonce too low」でリバートし続ける理由をデバッグするのに3時間使いました。.envファイルにタイプミスがありました――testnet-rpc.monad.xyztestnet-rpc.monad.xyz ではなくなっていたのです。ええ、ほんとに。1文字のタイプミスを180分探し当てられなかったのは、答えが目の前でこちらを見ているのに、私は複雑な問題を探していたからです。

  • レート制限をガッツリ食らう: 最初のバージョンではトランザクション間に遅延を入れていませんでした。10分で200トランザクション投げた結果、IPがフラグ付けされ、6時間ブロックされました。各txの間にtime.sleep(1)、バースト間にtime.sleep(60)を追加したら、完全に解決しました。

  • マルチウォレット検知: 4日目に、同じスクリプトを2つの異なるウォレットで動かしてみました。「ウォレットが多いほどトランザクションが増えて、良いはず」と考えたからです。しかし、2時間以内に両方のウォレットがMonadの公開ダイバーシティダッシュボードで「似たトランザクションパターン」としてフラグ付けされました。シングルウォレットに戻しました。学び: 多様性は「何をするか」だけでなく、ウォレット同士がどれだけ違って見えるかに関わる、ということです。

  • 返却形式: {"translated": "翻訳されたHTML"}
  • 固定のガスリミット: コントラクトのデプロイスクリプトでは gas: 50000 を使っていました — 自己送金(self-transfer)には問題ありませんが、コントラクトのデプロイには ~200,000+ が必要です。デプロイトランザクションは、レシートを確認するまで何も起きたように見えず、そこに「out of gas(ガス不足)」と表示されていました。今は自己送金以外のすべてに対して gas = w3.eth.estimate_gas(tx) を使っています。

  • ヘルスチェックの欠如: スクリプトが夜間に2回落ちました。1回目は午前3時のRPCタイムアウト、2回目はディスク満杯エラー(ログが /tmp でいっぱいになった)です。どちらも、午前7時に目を覚ますまで気づきませんでした。単純な cron ジョブで「最終トランザクションのタイムスタンプが30分以上前」をチェックしていれば、どちらも検知できたはずです。今は pgrep -f monad_burst を15分ごとに実行し、生きていなければ再起動しています。

これを違うようにやるなら

  1. 初日からトランザクションの多様性を追加します。自己送金は簡単ですが、どの公開ダッシュボードでも目立ちません。
  2. 最初からすべてをログに残します。成功率をトランザクション種別ごとに追跡し始めたのは途中からでした。JSONLファイルが完璧で、トランザクションごとに1行、追記専用です。
  3. 最初の1時間からヘルスチェックを入れます。夜間のクラッシュが2回で、実行時間を12時間以上失いました。get_transaction_count() をチェックする cron ジョブなら、数分以内に気づけたはずです。
  4. 複数のRPCでテストします。Monadの公開エンドポイントは堅実ですが、バックアップがあればダウンタイムのうちの1つは防げたでしょう。
  5. 固定ではなく estimate_gas を使います。コントラクトデプロイで出た「out of gas」エラーは、単一の関数呼び出しで100%回避できました。

正直なところ、ランダムな45〜180秒の遅延が実際に最適なのか、それともタイミング運が良かっただけなのか分かりません。もっとデータを持っている方がいたら教えてください。

次に

もしあなたもMonadをファーミングしているなら、私が運営している小さなTelegramグループで毎日トランザクションログを投稿しています。来週はこのセットアップをL2にも適用する予定です — 同じアーキテクチャで、トランザクションの多様性をより良くします。

フルコード: github.com/vincentliu-eth/monad-farming-notes

テストネットをファーミングしていて犯した「唯一の」ミスで、他の人に警告したいことは何ですか? 下に書き込んでください — 私はスクリプトのv2のために現実の失敗談を集めていて、あなたの助言が採用されたら誰でもクレジットします。

Monadテストネットで1,960回の取引を行いました。実際に機能するプレイブックはこちら | AI Navigate