监听以太坊mempool

以太坊mempool(内存池)是已经提交、随时可以打包、但还没有成功打包的交易,它随时都可能被打包广播。

默认mempool是公开的,任何人都可以访问。这给MEV提供了非常重要的基础。而监听mempool里的交易,也是非常重要非常基础的操作。

这一切,靠的是geth提供的API(不是标准API ,是geth特有的API)。

标准API列表:

https://ethereum.github.io/execution-apis/api-documentation/

geth特有API eth_subscribe

https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub#newpendingtransactions

所谓的“监听”,其实就是订阅(subscribe),利用eth_subscribe接口订阅类型为newPendingTransactions的消息

发送请求

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "eth_subscribe",
  "params": [
    "newPendingTransactions"
  ]
}

响应

{"jsonrpc":"2.0","id":2,"result":"0xc3b33aa549fb9a60e95d21862596617c"}
{
  "jsonrpc":"2.0",
  "method":"eth_subscription",
  "params":{
    "subscription":"0xc3b33aa549fb9a60e95d21862596617c",
    "result":"0xd6fdc5cc41a9959e922f30cb772a9aef46f4daea279307bc5f7024edc4ccd7fa"
  }
}

它会返回一个标示(用于取消订阅),之后如果有新的pending的交易,会把交易hash发送过来。只有交易hash没有细节,其他的参数,需要你自己再查询一次。

Go代码如下

package main

import (
    "context"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/ethclient/gethclient"
    "github.com/ethereum/go-ethereum/rpc"
    "log"
    "os"
    "os/signal"
    "syscall"
)

func watch() {
    wss := os.Getenv("WSS")

    rc, err := rpc.Dial(wss)
    if err != nil {
        log.Printf("failed to dial: %v", err)
        return
    }
    log.Printf("connected to %s", wss)
    gc := gethclient.New(rc)

    hashes := make(chan common.Hash, 100)
    _, err = gc.SubscribePendingTransactions(context.Background(), hashes)
    if err != nil {
        log.Printf("failed to SubscribePendingTransactions: %v", err)
        return
    }
    log.Print("subscribed pending txs now")
    for {
        select {
        case hash := <-hashes:
            log.Printf("received tx %s", hash)
        }
    }
}

func main() {
    go watch()
    signalChan := make(chan os.Signal, 1)
    signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
    <-signalChan
}

当然了,先知道hash其实什么也干不了,还需要再查询一次,拿到具体的数据才行。

好消息是,从v1.11.0geth直接就添加了新的SubscribeFullPendingTransactions接口,可以直接拿到transaction了,简直不要太幸福。

package main

import (
    "context"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/ethclient/gethclient"
    "github.com/ethereum/go-ethereum/rpc"
    "log"
    "os"
    "os/signal"
    "syscall"
)

func watch() {
    wss := os.Getenv("WSS")

    rc, err := rpc.Dial(wss)
    if err != nil {
        log.Printf("failed to dial: %v", err)
        return
    }
    log.Printf("connected to %s", wss)
    gc := gethclient.New(rc)

    transactions := make(chan *types.Transaction, 100)
    _, err = gc.SubscribeFullPendingTransactions(context.Background(), transactions)
    if err != nil {
        log.Printf("failed to SubscribePendingTransactions: %v", err)
        return
    }
    log.Print("subscribed pending txs now")
    for {
        select {
        case transaction := <-transactions:
            // 这里的transaction是完整数据,可以直接使用
            txBytes, err := transaction.MarshalJSON()
            if err != nil {
                continue
            }
            log.Printf("received tx %s", string(txBytes))
        }
    }
}

func main() {
    go watch()
    signalChan := make(chan os.Signal, 1)
    signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
    <-signalChan
}

打印的transaction内容是如下格式

{
  "type": "0x2",
  "chainId": "0x14a33",
  "nonce": "0x5e1",
  "to": "0x290b54a504a3b0cb21888e3e405afc1b2946598c",
  "gas": "0xdc2d",
  "gasPrice": null,
  "maxPriorityFeePerGas": "0x59682f00",
  "maxFeePerGas": "0x59682f64",
  "value": "0x0",
  "input": "0xa9059cbb000000000000000000000000f3902d46ffa3730f4733b19e5fca5acd8057316a0000000000000000000000000000000000000000000000000de0b6b3a7640000",
  "accessList": [],
  "v": "0x1",
  "r": "0x5783d7fc6ac88e39593de34eff58b059d2bb6045227eea277969a0c4778ec63",
  "s": "0x591ac696363bcaa4943b30ab680c8a7df58d27dd1b28f1a90e0345aaca0cd0b4",
  "hash": "0xb0d0b538207efc9da6daf97b80e1e5e6c3a18a8a7aa45c84f413af3b0ea2c89d"
}

可见是签过名直接可以用的。

祝朋友们玩得开心,可以加我推特交流 @alexgiantwhale