在 Go 中创建原生以太坊交互:Blob 交易、交易追踪及其他

  • 0xE
  • 更新于 2小时前
  • 阅读 29

本教程展示了如何在 ethclient 支持 JSON-RPC 调用的情况下使用它的功能,以及在它不支持时该如何操作。

Go-Ethereum (Geth) 的 ethclient 包为以太坊网络提供了 JSON-RPC 请求的 API 封装,类似于 web3.js 和 ethers.js。

然而,JSON-RPC 的某些功能(如交易追踪)并未在 ethclient(包括 web3.js 和 ethers.js)的 API 中公开。

本教程展示了如何在 ethclient 支持 JSON-RPC 调用的情况下使用它的功能,以及在它不支持时该如何操作。

正如下图所示,有时我们可以通过使用 ethclient 中的 API 来完成操作,但有时我们需要手动编写 RPC 调用:

使用 ethclient 进行 json-rpc 调用的应用程序示意图

在本教程的最后,我们将展示如何执行 Blob 交易,这是以太坊在 Decun 升级中最近增加支持的一项功能。

我们还将进行一些与以太坊交易相关的概念操作,例如签名和验证数字签名。

前置条件

  • 电脑上应安装 Go 语言;如果尚未安装,请参阅下载说明
  • 需要具备一些基本的 Go 编程知识。

入门

在本教程中,我们将使用 Sepolia 网络,但这些操作同样适用于主网或其他测试网。确保拥有一些 Sepolia ETH。

本教程将进行的操作

  • 获取网络上的建议 gas 价格
  • 估算交易的 gas
  • 构建并发送 EIP1559 原始交易
  • 签名并验证以太坊消息
  • 检索账户的 nonce(交易数)
  • 跟踪交易
  • 最后,发送 EIP-4844 blob 交易

首先,创建一个新的项目文件夹,打开并初始化:

go mod init eth-rpc

刚刚我们创建了项目模块。如果成功,您应该会看到一个名为 go.mod 的文件,其内容如下:

module eth-rpc

go 1.22.0

安装必要的依赖项

go get -u github.com/ethereum/go-ethereum@v1.14.11
go get github.com/ethereum/go-ethereum/rpc@v1.14.11

这将生成一个 go.sum 文件。

故障排查提示

如果遇到与模块相关的问题,可以尝试以下步骤:

  1. 删除 go.mod 和 go.sum 文件,并使用 go mod init eth-rpc 重新初始化。
  2. 运行 go mod tidy 以同步依赖项。
  3. 如果问题仍然存在,可以使用 go clean -modcache 清除模块缓存,然后重复步骤 1 和步骤 2。

现在,将以下代码粘贴到项目中的 main.go 文件中:

package main
import "fmt"

const (
    sepoliaRpcUrl = "https://rpc.sepolia.ethpandaops.io" // sepolia rpc url
    mainnetRpcUrl = "https://rpc.builder0x69.io/"        // mainnet rpc url
    from = "0x571B102323C3b8B8Afb30619Ac1d36d85359fb84"
    to = "0x4924Fb92285Cb10BC440E6fb4A53c2B94f2930c5"
    data = "Hello Ethereum!"
    privKey = "2843e08c0fa87258545656e44955aa2c6ca2ebb92fa65507e4e5728570d36662"
    gasLimit = uint64(21500) // adjust this if necessary
    wei = uint64(0) // 0 Wei
)

func main() {

    fmt.Println("using ethclient...")

}

我们将在后续过程中不断更新 main.go 文件。

您可以使用以下命令运行程序:

go run main.go

现在,让我们开始创建项目的功能模块。

1. 获取网络上的建议 gas 价格

借助 Geth 的 ethclient 包,我们可以使用 SuggestGasPrice API 根据当前网络状况为交易设置合适的 gas 价格。

在底层,此方法调用了 eth_gasPrice JSON-RPC API。

在项目目录中创建一个名为 getGasPrice.go 的文件,并粘贴以下代码:

package main

import (
    "context"
    "fmt"
    "github.com/ethereum/go-ethereum/ethclient"
    "log"
)

// getSuggestedGasPrice 通过 RPC 连接到以太坊节点,并检索当前建议的 gas 价格。
func getSuggestedGasPrice(rpcUrl string) {
    // 使用提供的 RPC URL 连接到以太坊网络。
    client, err := ethclient.Dial(rpcUrl)
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum client: %v", err)
    }

    // 获取当前建议的 gas price。
    gasPrice, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        log.Fatalf("Failed to suggest gas price: %v", err)
    }

    // 将建议的 gas price 打印到终端。
    fmt.Println("Suggested Gas Price:", gasPrice.String())
}

现在更新 main.go 中的 main 函数:

func main() {
    fmt.Println("using ethclient...")

    getSuggestedGasPrice(sepoliaRpcUrl) 
    // 在 Sepolia 测试网获取 gas price。刚刚添加的功能。
}

然后运行以下命令:

go run .

我们使用 go run . 而不是 go run main.go 的原因是,为了编译并执行当前目录中属于同一包的所有 Go 源文件。这包括包含 main 函数的 main.go 文件以及其他文件,比如包含 getSuggestedGasPrice 函数的文件。

今后我们将使用此命令来运行代码。

运行命令后,建议的 gas price 应会显示在终端上。请注意,它的单位是 Wei。

using ethclient...
Suggested Gas Price: 18637941169

2. 估算交易的 gas 使用量

ethclient 还提供了一个 EstimateGas 方法。它返回成功处理交易所需的 gas 的估计值。

EstimateGas 方法通过构造的消息作为参数调用 eth_estimateGas JSON-RPC API。

创建一个 estimateGas.go 文件,并粘贴以下代码:

package main

import (
    "context"
    "log"
    "math/big"
    "strings"
    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/ethereum/go-ethereum/ethclient"
)

// estimateGas 尝试估算执行给定交易所需的建议 gas 量。

func estimateGas(rpcUrl, from, to, data string, value uint64) uint64 {

    // 建立与指定 RPC URL 的 RPC 连接
    client, err := ethclient.Dial(rpcUrl)
    if err != nil {
        log.Fatalln(err)
    }

    var ctx = context.Background()

    var (
        fromAddr  = common.HexToAddress(from)     // 将 from 地址从十六进制转换为以太坊地址。
        toAddr    = common.HexToAddress(to)       // 将 to 地址从十六进制转换为以太坊地址。
        amount    = new(big.Int).SetUint64(value) // 将值从 uint64 转换为 *big.Int。
        bytesData []byte
    )

    // 如果数据未以十六进制编码,则进行编码。
    if data != "" {
        if ok := strings.HasPrefix(data, "0x"); !ok {
            data = hexutil.Encode([]byte(data))
        }

        bytesData, err = hexutil.Decode(data)
        if err != nil {
            log.Fatalln(err)
        }
    }

    // 创建一个消息,包含交易的相关信息。
    msg := ethereum.CallMsg{
        From: fromAddr,
        To: &toAddr,
        Gas: 0x00,
        Value: amount,
        Data: bytesData,
    }

    // 估算交易所需的 gas 量。
    gas, err := client.EstimateGas(ctx, msg)
    if err != nil {
        log.Fatalln(err)
    }

    return gas
}
func main() {
    fmt.Println("using ethclient...")

    getSuggestedGasPrice(sepoliaRpcUrl)

    eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei) // 这只是刚刚添加的。
    fmt.Println("\nestimate gas for the transaction is:", eGas) // 这只是刚刚添加的。
}

运行代码:go run .

我们应该能得到如下结果:

using ethclient...
Suggested Gas Price: 4661901390

estimate gas for the transaction is: 21406

3. 构建 EIP1559 原始交易

以太坊原始交易是未处理形式的交易,使用递归长度前缀(RLP)序列化方法进行编码。

这种编码技术由以太坊执行层(EL)用于序列化和反序列化数据。

原始交易数据是对 nonce、接收者地址(to)、交易金额、data payload 和 gas limit 的编码。

交易类型

在手动创建以太坊原始交易时,有几种交易类型可供选择,从旧的传统交易(也称为类型 0),需要明确指定 gas price,到 EIP-1559 交易(类型 2),引入了基础费用、优先费用(矿工小费)和每单位 gas 的最大费用,以更好地预测 gas price。

基础费用由网络决定,在一个区块内对所有交易保持固定。然而,它会根据网络拥堵在区块之间进行调整。您可以通过增加提供给矿工的优先费用(小费)来影响交易的优先级。

此外,还有 EIP-2930 交易(类型 1)和 EIP-4844 blob 交易(类型 3,我们将在本文后面讨论)。

在 Go 中选择交易类型

Geth 客户端通过其 types 包支持这些不同的交易类型。对于我们的目的,我们将重点关注 types.DynamicFeeTx,它对应于 EIP-1559 交易模型。

整个过程不涉及任何 JSON-RPC 调用,我们只需构建交易,签名并使用 RLP 编码方案进行序列化。

创建一个 createEIP1559RawTX.go 文件,并粘贴以下代码:

package main

import (
    "bytes"
    "context"
    "crypto/ecdsa"
    "encoding/hex"
    "fmt"
    "log"
    "math/big"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/params"
)

// createRawTransaction 创建一个原始的 EIP-1559 交易并将其作为十六进制字符串返回。
func createRawTransaction(rpcURL, to, data, privKey string, gasLimit, wei uint64) string {

    // 使用提供的 RPC URL 连接到以太坊客户端。
    client, err := ethclient.Dial(rpcURL)
    if err != nil {
        log.Fatalln(err)
    }

    // 获取目标以太坊网络的链 ID。
    chainID, err := client.ChainID(context.Background())
    if err != nil {
        log.Fatalln(err)
    }

    // 提议包括在区块中的基础费用。
    baseFee, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        log.Fatalln(err)
    }

    // 提议矿工激励的 gas 小费上限(优先费用)。
    priorityFee, err := client.SuggestGasTipCap(context.Background())
    if err != nil {
        log.Fatalln(err)    
    }

    // 计算最大 gas 费用上限,在基础费用加上优先费用的基础上增加 2 GWei 的幅度。
    increment := new(big.Int).Mul(big.NewInt(2), big.NewInt(params.GWei))
    gasFeeCap := new(big.Int).Add(baseFee, increment)
    gasFeeCap.Add(gasFeeCap, priorityFee)

    // 解码提供的私钥。
    pKeyBytes, err := hexutil.Decode("0x" + privKey)
    if err != nil {
        log.Fatalln(err)
    }

    // 将私钥字节转换为 ECDSA 私钥。
    ecdsaPrivateKey, err := crypto.ToECDSA(pKeyBytes)
    if err != nil {
        log.Fatalln(err)
    }

    // 从 ECDSA 私钥中提取公钥。
    publicKey := ecdsaPrivateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)

    if !ok {
        log.Fatal("Error casting public key to ECDSA")
    }

    // 从公钥计算签名者的以太坊地址。
    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
    // 获取签名者账户的 nonce,表示交易计数。

    nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    if err != nil {
        log.Fatal(err)
    }

    // 准备 data payload。
    var hexData string
    if strings.HasPrefix(data, "0x") {
        hexData = data
    } else {
        hexData = hexutil.Encode([]byte(data))
    }
    bytesData, err := hexutil.Decode(hexData)
    if err != nil {
        log.Fatalln(err)
    }

    // 设置交易字段,包括接收者地址、金额和 gas 参数。
    toAddr := common.HexToAddress(to)
    amount := new(big.Int).SetUint64(wei)
    txData := types.DynamicFeeTx{
        ChainID: chainID,
        Nonce: nonce,
        GasTipCap: priorityFee,
        GasFeeCap: gasFeeCap,
        Gas: gasLimit,
        To: &toAddr,
        Value: amount,
        Data: bytesData,
    }

    // 从准备好的数据创建一个新的交易对象。
    tx := types.NewTx(&txData)
    // 使用发送者的私钥签名交易。
    signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), ecdsaPrivateKey)

    if err != nil {
        log.Fatalln(err)
    }

    // 将签名的交易编码为 RLP(递归长度前缀)格式以便传输。
    var buf bytes.Buffer
    err = signedTx.EncodeRLP(&buf)

    if err != nil {
        log.Fatalln(err)
    }

    // 将 RLP 编码的交易作为十六进制字符串返回。
    rawTxRLPHex := hex.EncodeToString(buf.Bytes())

    return rawTxRLPHex
}

更新 main.go 中的 main 函数:

func main() {    
    fmt.Println("using ethclient...") 

    getSuggestedGasPrice(sepoliaRpcUrl)  

    eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)        
    fmt.Println("\nestimate gas for the transaction is:", eGas) 

    rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei) // 这是刚刚添加的。                
    fmt.Println("\nRaw TX:\n", rawTxRLPHex) // 这是刚刚添加的。
}

原始交易将使用存储在 privKey 变量中的私钥创建。为了确保在 Sepolia 测试网上成功交易,请用持有测试 Sepolia ETH 的私钥替换它。

运行代码:go run .

我们应该能得到原始交易,如下所示:

Raw TX:
 b88002f87d83aa36a781a1830f42408501851968158253fc944924fb92285cb10bc440e6fb4a53c2b94f2930c5808f48656c6c6f20457468657265756d21c080a05cd93c6d2f42c526edd848db41ec49c640835d517a4a243e74e271aac9e76660a01d6977759514cb877cf1428d8005d45d9b6742070c9a9a7d5ce582d07e23fec2

我们将在下一部分中传播原始交易到网络。

4. 发送原始交易

在创建任何类型的原始交易后,我们可以使用 ethclient.SendTransaction 函数将其传播到网络中,该函数接受 RLP 解码的原始交易并进行 eth_sendRawTransaction JSON-RPC 调用。

以下是一些附加代码(Transaction 结构的 convertHexField 函数),虽然并不是强制性的,但有助于更好地打印交易结果。

创建一个名为 sendRawTX.go 的文件,并粘贴以下代码:

package main

import (    
    "context"    
    "encoding/hex"    
    "encoding/json"    
    "fmt"    
    "log"    
    "reflect"    
    "strconv"    
    "time"    
    "github.com/ethereum/go-ethereum/core/types"        
    "github.com/ethereum/go-ethereum/ethclient"        
    "github.com/ethereum/go-ethereum/rlp"
)

// Transaction 表示交易 JSON 的结构。
type Transaction struct {    
    Type                 string   `json:"type"`    
    ChainID              string   `json:"chainId"`    
    Nonce                string   `json:"nonce"`    
    To                   string   `json:"to"`    
    Gas                  string   `json:"gas"`    
    GasPrice             string   `json:"gasPrice,omitempty"`    
    MaxPriorityFeePerGas string   `json:"maxPriorityFeePerGas"`    
    MaxFeePerGas         string   `json:"maxFeePerGas"`    
    Value                string   `json:"value"`    
    Input                string   `json:"input"`    
    AccessList           []string `json:"accessList"`    
    V                    string   `json:"v"`    
    R                    string   `json:"r"`    
    S                    string   `json:"s"`    
    YParity              string   `json:"yParity"`    
    Hash                 string   `json:"hash"`    
    TransactionTime      string   `json:"transactionTime,omitempty"`    
    TransactionCost      string   `json:"transactionCost,omitempty"`
}

// sendRawTransaction 发送原始以太坊交易。
func sendRawTransaction(rawTx, rpcURL string) {            
    // 将十六进制字符串解码为字节
    rawTxBytes, err := hex.DecodeString(rawTx)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 初始化一个空的 Transaction 结构以保存解码数据。    
    tx := new(types.Transaction)    

    // 将原始交易字节从十六进制解码为 Transaction 结构。    
    // 此步骤将 RLP(递归长度前缀)编码的字节转换回以太坊客户端理解的结构化交易格式。    
    err = rlp.DecodeBytes(rawTxBytes, &tx)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 建立与指定 RPC URL 的 RPC 连接        
    client, err := ethclient.Dial(rpcURL)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 传播交易    
    err = client.SendTransaction(context.Background(), tx)        
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 将交易 JSON 解组到一个结构中    
    var txDetails Transaction    
    txBytes, err := tx.MarshalJSON()    
    if err != nil {        
        log.Fatalln(err)    
    }    
    if err := json.Unmarshal(txBytes, &txDetails); err != nil {        
        log.Fatalln(err)    
    }    

    // 添加其他交易详情        
    txDetails.TransactionTime = tx.Time().Format(time.RFC822)    
    txDetails.TransactionCost = tx.Cost().String()    

    // 将一些十六进制字符串字段格式化为十进制字符串    
    convertFields := []string{"Nonce", "MaxPriorityFeePerGas", "MaxFeePerGas", "Value", "Type", "Gas"}    
    for _, field := range convertFields {        
        if err := convertHexField(&txDetails, field); err != nil {            
            log.Fatalln(err)        
        }    
    }    

    // 将结构重新序列化为 JSON    
    txJSON, err := json.MarshalIndent(txDetails, "", "\t")    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 打印包含附加字段的整个 JSON    
    fmt.Println("\nRaw TX Receipt:\n", string(txJSON))
}

func convertHexField(tx *Transaction, field string) error {    

    // 获取 Transaction 结构的类型    
    typeOfTx := reflect.TypeOf(*tx)    

    // 获取 Transaction 结构的值    
    txValue := reflect.ValueOf(tx).Elem()    

    // 解析十六进制字符串为整数    
    hexStr := txValue.FieldByName(field).String()    

    intValue, err := strconv.ParseUint(hexStr[2:], 16, 64)    
    if err != nil {        
        return err    
    }    

    // 将整数转换为十进制字符串    
    decimalStr := strconv.FormatUint(intValue, 10)    

    // 检查字段是否存在    
    _, ok := typeOfTx.FieldByName(field)    
    if !ok {        
        return fmt.Errorf("field %s does not exist in Transaction struct", field)    
    }    

    // 将字段值设置为十进制字符串    
    txValue.FieldByName(field).SetString(decimalStr)    

    return nil
}

现在更新 main.go 中的 main 函数:

func main() {
    fmt.Println("using ethclient...")

    getSuggestedGasPrice(sepoliaRpcUrl)

    eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)
    fmt.Println("\nestimate gas for the transaction is:", eGas)

    rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)
    fmt.Println("\nRaw TX:\n", rawTxRLPHex)

    sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl) // 这是刚刚添加的。
}

运行命令 go run .

我们可以看到交易收据:

 {
        "type": "2",
        "chainId": "0xaa36a7",
        "nonce": "161",
        "to": "0x4924fb92285cb10bc440e6fb4a53c2b94f2930c5",
        "gas": "21500",
        "maxPriorityFeePerGas": "1000000",
        "maxFeePerGas": "7549641833",
        "value": "0",
        "input": "0x48656c6c6f20457468657265756d21",
        "accessList": [],
        "v": "0x0",
        "r": "0xcb68addaffb934d13be97edcc915411edeaf92a93b7f973e9af2f053727c40b5",
        "s": "0xb64d776f0e63d02d30087f3b6560886953e04d8ce6357e730c328e92f1f4d7d",
        "yParity": "0x0",
        "hash": "0x38ce60ce18dcdda8904d7d3daad1c6d1b7611a35b03e9086373175137895ff25",
        "transactionTime": "20 Nov 24 15:53 CST",
        "transactionCost": "162317299409500"
}

签署以太坊消息(数字签名)

以太坊签名的消息可以用于创建验证系统。这是一种在不执行链上交易的情况下验证所有权或同意的方法。

例如,如果用户 A 用他们的私钥签署一条消息并将其提交给一个平台,该平台将用户的公钥地址、消息和签名进行核实,确认签名确实是用户 A 签署的;如果是,这可以作为平台执行某些操作的授权(无论签署的原因是什么)。

以太坊消息签名利用 secp256k1 椭圆曲线数字签名算法(ECDSA)来确保加密安全性。

以太坊签名的消息还具有前缀,因此它们在网络中是可识别和唯一的。

前缀为:\x19Ethereum Signed Message:\n" + len(message),然后在签名之前对前缀+消息进行哈希处理:sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))

以太坊还有一个恢复 ID,添加到签名的最后一个字节。签名的长度为 65 字节,分为 3 个部分:v、r 和 s。r 是前 32 字节,s 是接下来的 32 字节,v 是表示恢复 ID 的一个字节。

恢复 ID 对于以太坊来说是 27(0x1b)或 28(0x1c)。你通常会在所有以太坊数字签名(或签名消息)的末尾看到这个。

用于签名的 Geth 的 crypto 包不会像 Metamask 的 personal_sign 那样添加恢复 ID,因此我们必须在签名后手动添加它,例如通过 sig[64]+=27

请注意,签署消息完全是在链外和离线完成的。它并不进行任何 JSON-RPC 调用。

在项目目录中添加以下代码到 signMessage.go 文件:

package main

import (    
    "crypto/ecdsa"    
    "encoding/json"    
    "fmt"    
    "log"    
    "github.com/ethereum/go-ethereum/common/hexutil"    
    "github.com/ethereum/go-ethereum/crypto"
)

// SignatureResponse 表示签名响应的结构。
type SignatureResponse struct {    
    Address string `json:"address,omitempty"`    
    Msg     string `json:"msg,omitempty"`    
    Sig     string `json:"sig,omitempty"`    
    Version string `json:"version,omitempty"`
}

// signMessage 使用提供的私钥签署消息。
func signMessage(message, privKey string) (string, string) {    
    // 将私钥从十六进制转换为 ECDSA 格式        
    ecdsaPrivateKey, err := crypto.HexToECDSA(privKey)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 构造消息前缀    
    prefix := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message)))    
    messageBytes := []byte(message)    

    // 使用 Keccak-256 哈希前缀和消息    
    hash := crypto.Keccak256Hash(prefix, messageBytes)    

    // 签署哈希后的消息    
    sig, err := crypto.Sign(hash.Bytes(), ecdsaPrivateKey)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 调整签名 ID 为以太坊的格式    
    sig[64] += 27    

    // 从私钥导出公钥        
    publicKeyBytes := crypto.FromECDSAPub(ecdsaPrivateKey.Public().(*ecdsa.PublicKey))    
    pub, err := crypto.UnmarshalPubkey(publicKeyBytes)    
    if err != nil {        
        log.Fatal(err)    
    }    
    rAddress := crypto.PubkeyToAddress(*pub)    

    // 构造签名响应    
    res := SignatureResponse{        
        Address: rAddress.String(),        
        Msg:     message,        
        Sig:     hexutil.Encode(sig),        
        Version: "2",    
    }    

    // 将响应序列化为 JSON,并进行适当格式化        
    resBytes, err := json.MarshalIndent(res, " ", "\t")    
    if err != nil {        
        log.Fatalln(err)    
    }    

    return res.Sig, string(resBytes)
}

接下来,更新 main.go 中的 main 函数:

func main() {    
    fmt.Println("using ethclient...")    

    getSuggestedGasPrice(sepoliaRpcUrl)     

    eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)         
    fmt.Println("\nestimate gas for the transaction is:", eGas)     

    rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)    
    fmt.Println("\nRaw TX:\n", rawTxRLPHex)     

    sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl) 

    _, sDetails := signMessage(data, privKey) // 这是刚刚添加的。    
    fmt.Println("\nsigned message:", sDetails) // 这是刚刚添加的。
}

运行代码时,你应该会得到如下输出:

signed message: {
        "address": "0x77866255306C550c4da58150Baf1b4D712C000F8",
        "msg": "Hello Ethereum!",
        "sig": "0xa282215687be37af1fceb93deb3c9b161ec487e31d1fd4656565578a4d98ef692ad44731fdc0ecce2ced3a64fac7ae35ed345f03037dc59faecc950cf59791ae1b",
        "version": "2"
 }

5. 验证签名的以太坊消息

如前所述,我们可以离线签署和验证签名的消息。要验证签名的消息,我们需要签名、签署者的地址和原始消息。

下面的 verifySig 函数接受这些参数,将签名解码为字节,并移除以太坊的恢复 ID。这样做的原因是因为用于签署和验证签名的 crypto 包检查签名的恢复 ID(第 65 字节)是否小于 4(我的猜测是为了不局限于以太坊签名)。

在此之后,我们重建必要的参数(代码中的详细信息),并调用 crypto.Ecrecover 函数,该函数的工作方式类似于 EVM 的 Ecrecover 预编译合约(地址为 0x01),返回签署消息的地址(创建签名的地址)。

创建一个名为 verifySignedMessage.go 的文件,并添加以下代码:

package main

import (    
    "fmt"    
    "log"    
    "strings"    
    "github.com/ethereum/go-ethereum/common/hexutil"    
    "github.com/ethereum/go-ethereum/crypto"
)

// handleVerifySig 验证签名与提供的公钥和哈希的匹配。
func verifySig(signature, address, message string) bool {    
    // 将签名解码为字节    
    sig, err := hexutil.Decode(signature)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 调整签名为标准格式(移除以太坊的恢复 ID)    
    sig[64] = sig[64] - 27    

    // 构造消息前缀    
    prefix := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message)))    
    data := []byte(message)    

    // 使用 Keccak-256 哈希前缀和数据    
    hash := crypto.Keccak256Hash(prefix, data)    

    // 从签名恢复公钥字节        
    sigPublicKeyBytes, err := crypto.Ecrecover(hash.Bytes(), sig)    
    if err != nil {        
        log.Fatalln(err)    
    }    
    ecdsaPublicKey, err := crypto.UnmarshalPubkey(sigPublicKeyBytes)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 从恢复的公钥导出地址    
    rAddress := crypto.PubkeyToAddress(*ecdsaPublicKey)    

    // 检查恢复的地址是否与提供的地址匹配    
    isSigner := strings.EqualFold(rAddress.String(), address)    

    return isSigner
}

接下来,更新 main.go 中的 main 函数:

func main() {    
    fmt.Println("using ethclient...")

    getSuggestedGasPrice(sepoliaRpcUrl)

    eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)
    fmt.Println("\nestimate gas for the transaction is:", eGas)

    rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)
    fmt.Println("\nRaw TX:\n", rawTxRLPHex)

    sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)

    sig, sDetails := signMessage(data, privKey)
    fmt.Println("\nsigned message:", sDetails)

    if isSigner := verifySig(sig, from, data); isSigner { // 这是刚刚添加的。        
        fmt.Printf("\n%s signed %s\n", from, data)    
    } else {        
        fmt.Printf("\n%s did not sign %s\n", from, data)    
    }
}

现在,我们可以确认私钥是否签署了消息,实际情况下它确实签署了。运行 go run .

0x571B102323C3b8B8Afb30619Ac1d36d85359fb84 signed Hello Ethereum!

将不同的消息传递给 verifySig 函数。你应该会得到以下输出:

0x571B102323C3b8B8Afb30619Ac1d36d85359fb84 did not sign Hello Ethereum!

因为数据不正确。

6. 获取账户的 Nonce(交易次数)

要获取账户的 nonce,我们可以使用 PendingNonceAtNonceAt 函数。PendingNonceAt 返回账户的下一个未使用的 nonce,而 NonceAt 返回账户的当前 nonce。

这两者之间的一个区别是,PendingNonceAt 只是获取下一个 nonce,而 NonceAt 尝试获取在特定区块号下的账户 nonce;如果没有传递区块号,它将返回账户在最后已知区块上的 nonce。

这两种方法都通过 eth_getTransactionCount 发起 JSON-RPC 调用;然而,第一个方法包含一个名为 “pending” 的第二个参数,而另一个方法则指定区块号。

现在,创建一个名为 getNonce.go 的文件,并粘贴以下代码:

package main

import (    
    "context"    
    "fmt"    
    "log"    
    "github.com/ethereum/go-ethereum/common"    
    "github.com/ethereum/go-ethereum/ethclient"
)

// getNonce 获取并打印给定以太坊地址的当前 nonce 和下一个 nonce。
func getNonce(address, rpcUrl string) (uint64, uint64) {    
    client, err := ethclient.Dial(rpcUrl)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 获取地址的下一个 nonce    
    nextNonce, err := client.PendingNonceAt(context.Background(), common.HexToAddress(address))    
    if err != nil {        
        log.Fatalln(err)    
    }    

    var currentNonce uint64 // 变量用于保存当前 nonce。    
    if nextNonce > 0 {        
        currentNonce = nextNonce - 1    
    }    

    return currentNonce, nextNonce
}

接下来,更新 main.go 中的 main 函数:

func main() {    
    fmt.Println("using ethclient...")    

    getSuggestedGasPrice(sepoliaRpcUrl)     

    eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)     
    fmt.Println("\nestimate gas for the transaction is:", eGas)     

    rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)    
    fmt.Println("\nRaw TX:\n", rawTxRLPHex)     

    sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)    

    sig, sDetails := signMessage(data, privKey)    
    fmt.Println("\nsigned message:", sDetails)    

    if isSigner := verifySig(sig, from, data); isSigner {        
        fmt.Printf("\n%s signed %s\n", from, data)    
    } else {        
        fmt.Printf("\n%s did not sign %s\n", from, data)    
    }    

    cNonce, nNonce := getNonce(to, sepoliaRpcUrl) // 这是刚刚添加的。    
    fmt.Printf("\n%s current nonce: %v\n", to, cNonce) // 这是刚刚添加的。  
    fmt.Printf("%s next nonce: %v\n", to, nNonce) // 这是刚刚添加的。  
}

运行代码时,使用命令 go run .,你应该会看到如下输出:

0x4924Fb92285Cb10BC440E6fb4A53c2B94f2930c5 current nonce: 4
0x4924Fb92285Cb10BC440E6fb4A53c2B94f2930c5 next nonce: 5

7. 追踪交易

如前所述,我们将使用 Geth 的 rpc 包进行交易追踪,这一功能并不直接支持 ethclient

通过追踪交易,我们可以可视化执行路径,并深入了解交易执行期间的任何事件日志。

在本例中,我们将专注于两个主要方法:debug_traceTransaction 和 Otterscan 提供的自定义 RPC 方法 ots_traceTransaction(后文将解释)。

debug_traceTransaction 使用 Geth 的原生交易追踪功能,接受交易哈希和一个追踪配置,指定要进行的追踪类型。Geth 有不同的原生追踪器,但我们将使用“callTracer”。要查看所有可用的 Geth 原生追踪器,可以稍后查看文档

debug_traceTransaction 利用 Geth 内置的交易追踪能力。它需要两个参数:

  1. 交易哈希
  2. 追踪配置:指定追踪的详细信息,例如要捕获的信息类型。Geth 提供多种原生追踪器,但在此示例中,我们将专注于“callTracer”。此追踪器跟踪交易执行期间执行的所有调用帧(函数调用)。

以下是使用“callTracer”配置生成的追踪示例:

client.CallContext(
    context.Background(), 
    &result, 
    "debug_traceTransaction", 
    "0xd12e31c3274ff32d5a73cc59e8deacbb0f7ac4c095385add3caa2c52d01164c1", 
    map[string]any{
        "tracer": "callTracer", 
        "tracerConfig": map[string]any{"withLog": true}}
)

配置参数(紧随交易哈希之后)指示连接的 Geth 节点执行调用追踪并包括生成的事件日志。

我们将使用 ots_traceTransaction(在代码后面解释)。

在项目中创建一个名为 traceTx.go 的文件,并粘贴以下代码:

package main

import (    
    "context"    
    "encoding/json"    
    "fmt"    
    "log"    
    "github.com/ethereum/go-ethereum/rpc"
)

func traceTx(hash, rpcUrl string) string {    
    var (        
        client *rpc.Client // 定义一个变量来保存 RPC 客户端。        
        err    error       // 用于捕获错误的变量。            
    )    

    // 使用提供的 URL 连接到以太坊 RPC 端点。    
    client, err = rpc.Dial(rpcUrl)    
    if err != nil {        
        log.Fatalln(err)    
    }    

    var result json.RawMessage // 用于保存调用的原始 JSON 结果的变量。    

    // 使用其哈希进行交易追踪的 RPC 调用。`ots_traceTransaction` 是方法名称。    
    err = client.CallContext(context.Background(), &result, "ots_traceTransaction", hash) // 或者使用 debug_traceTransaction 和支持的 RPC URL 和参数:hash, map[string]any{"tracer": "callTracer", "tracerConfig": map[string]any{"withLog": true}} 进行 Geth 追踪    
    if err != nil {        
        log.Fatalln(err)    
    }    

    // 将结果序列化为格式化的 JSON 字符串    
    resBytes, err := json.MarshalIndent(result, " ", "\t")    
    if err != nil {        
        log.Fatalln(err)    
    }    

    return string(resBytes)
}

接下来,更新 main.go 中的 main 函数:

func main() {    
    fmt.Println("using ethclient...")    

    getSuggestedGasPrice(sepoliaRpcUrl)     

    eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)     
    fmt.Println("\nestimate gas for the transaction is:", eGas)     

    rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)    
    fmt.Println("\nRaw TX:\n", rawTxRLPHex)     

    sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)    

    sig, sDetails := signMessage(data, privKey)    
    fmt.Println("\nsigned message:", sDetails)    

    if isSigner := verifySig(sig, from, data); isSigner {        
        fmt.Printf("\n%s signed %s\n", from, data)    } 
    else {        
        fmt.Printf("\n%s did not sign %s\n", from, data)    
    }    

    cNonce, nNonce := getNonce(to, sepoliaRpcUrl)    
    fmt.Printf("\n%s current nonce: %v\n", to, cNonce)    
    fmt.Printf("%s next nonce: %v\n", to, nNonce)    

    res := traceTx("0xd12e31c3274ff32d5a73cc59e8deacbb0f7ac4c095385add3caa2c52d01164c1", mainnetRpcUrl) // 这是刚刚添加的。  
    fmt.Println("\ntrace result:\n", res) // 这是刚刚添加的。  
}

ots_traceTransactionOtterscan 开发的自定义以太坊 JSON-RPC 方法,用于交易追踪,它不是 Geth 的一部分。它只需要交易哈希作为输入,并返回一个结构化的追踪输出,而不包含任何日志。

请注意,sepoliaRpcUrl 变量中的 Sepolia RPC URL 不支持 ots_traceTransaction 方法。对于本示例,我们将使用存储在 mainnetRpcUrl 变量中的主网 RPC URL,它支持该方法。

在运行程序后,你应该能够看到调用追踪的结果。

译者注:mainnetRpcUrl 我使用的是 mainnetRpcUrl = "https://docs-demo.quiknode.pro/"

trace result:
 [
        {
                "type": "CALL",
                "depth": 0,
                "from": "0x734bce0ca8f39c2f9768267390adf7df0d615db7",
                "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
                "value": "0x0",
                "input": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000065d8c6ab00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002540be40000000000000000000000000000000000000000000000023f01bbb8810da02b0900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000042dac17f958d2ee523a2206206994597c13d831ec7000064a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb80f51bb10119727a7e5ea3538074fb341f56b09ad000000000000000000000000000000000000000000000000000000000000",
                "output": "0x"
        },
        {
                "type": "CALL",
                "depth": 1,
                "from": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
                "to": "0x3416cf6c708da44db2624d63ea0aaef7113527c6",
                "value": "0x0",
                "input": "0x128acb080000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002540be400000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d2500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000734bce0ca8f39c2f9768267390adf7df0d615db7000000000000000000000000000000000000000000000000000000000000002bdac17f958d2ee523a2206206994597c13d831ec7000064a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000",
                "output": "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffdac209c9200000000000000000000000000000000000000000000000000000002540be400"
        },
        {
                "type": "CALL",
                "depth": 2,
                "from": "0x3416cf6c708da44db2624d63ea0aaef7113527c6",
                "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                "value": "0x0",
                "input": "0xa9059cbb0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad0000000000000000000000000000000000000000000000000000000253df636e",
                "output": "0x0000000000000000000000000000000000000000000000000000000000000001"
        },
        {
                "type": "DELEGATECALL",
                "depth": 3,
                "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                "to": "0x43506849d7c04f9138d1a2050bbf3a0c054402dd",
                "value": null,
                "input": "0xa9059cbb0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad0000000000000000000000000000000000000000000000000000000253df636e",
                "output": "0x0000000000000000000000000000000000000000000000000000000000000001"
        },
        {
                "type": "STATICCALL",
                "depth": 2,
                "from": "0x3416cf6c708da44db2624d63ea0aaef7113527c6",
                "to": "0xdac17f958d2ee523a2206206994597c13d831ec7",
                "value": null,
                "input": "0x70a082310000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c6",
                "output": "0x000000000000000000000000000000000000000000000000000022b030db664f"
        },
        {
                "type": "CALL",
                "depth": 2,
                "from": "0x3416cf6c708da44db2624d63ea0aaef7113527c6",
                "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
                "value": "0x0",
                "input": "0xfa461e33fffffffffffffffffffffffffffffffffffffffffffffffffffffffdac209c9200000000000000000000000000000000000000000000000000000002540be400000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000734bce0ca8f39c2f9768267390adf7df0d615db7000000000000000000000000000000000000000000000000000000000000002bdac17f958d2ee523a2206206994597c13d831ec7000064a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000",
                "output": "0x"
        },
        {
                "type": "CALL",
                "depth": 3,
                "from": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
                "to": "0x000000000022d473030f116ddee9f6b43ac78ba3",
                "value": "0x0",
                "input": "0x36c78516000000000000000000000000734bce0ca8f39c2f9768267390adf7df0d615db70000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c600000000000000000000000000000000000000000000000000000002540be400000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7",
                "output": "0x"
        },
        {
                "type": "CALL",
                "depth": 4,
                "from": "0x000000000022d473030f116ddee9f6b43ac78ba3",
                "to": "0xdac17f958d2ee523a2206206994597c13d831ec7",
                "value": "0x0",
                "input": "0x23b872dd000000000000000000000000734bce0ca8f39c2f9768267390adf7df0d615db70000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c600000000000000000000000000000000000000000000000000000002540be400",
                "output": "0x"
        },
        {
                "type": "STATICCALL",
                "depth": 2,
                "from": "0x3416cf6c708da44db2624d63ea0aaef7113527c6",
                "to": "0xdac17f958d2ee523a2206206994597c13d831ec7",
                "value": null,
                "input": "0x70a082310000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c6",
                "output": "0x000000000000000000000000000000000000000000000000000022b284e74a4f"
        },
        {
                "type": "CALL",
                "depth": 1,
                "from": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
                "to": "0xc840464a8c3324e0bdc9429439dde3a12205424a",
                "value": "0x0",
                "input": "0x128acb08000000000000000000000000734bce0ca8f39c2f9768267390adf7df0d615db700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000253df636e000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d2500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000400000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad000000000000000000000000000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb80f51bb10119727a7e5ea3538074fb341f56b09ad000000000000000000000000000000000000000000",
                "output": "0xfffffffffffffffffffffffffffffffffffffffffffffdc0fe4407f6d219cbe40000000000000000000000000000000000000000000000000000000253df636e"
        },
        {
                "type": "CALL",
                "depth": 2,
                "from": "0xc840464a8c3324e0bdc9429439dde3a12205424a",
                "to": "0x0f51bb10119727a7e5ea3538074fb341f56b09ad",
                "value": "0x0",
                "input": "0xa9059cbb000000000000000000000000734bce0ca8f39c2f9768267390adf7df0d615db700000000000000000000000000000000000000000000023f01bbf8092de6341c",
                "output": "0x0000000000000000000000000000000000000000000000000000000000000001"
        },
        {
                "type": "STATICCALL",
                "depth": 2,
                "from": "0xc840464a8c3324e0bdc9429439dde3a12205424a",
                "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                "value": null,
                "input": "0x70a08231000000000000000000000000c840464a8c3324e0bdc9429439dde3a12205424a",
                "output": "0x0000000000000000000000000000000000000000000000000000002469f1a7a9"
        },
        {
                "type": "DELEGATECALL",
                "depth": 3,
                "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                "to": "0x43506849d7c04f9138d1a2050bbf3a0c054402dd",
                "value": null,
                "input": "0x70a08231000000000000000000000000c840464a8c3324e0bdc9429439dde3a12205424a",
                "output": "0x0000000000000000000000000000000000000000000000000000002469f1a7a9"
        },
        {
                "type": "CALL",
                "depth": 2,
                "from": "0xc840464a8c3324e0bdc9429439dde3a12205424a",
                "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
                "value": "0x0",
                "input": "0xfa461e33fffffffffffffffffffffffffffffffffffffffffffffdc0fe4407f6d219cbe40000000000000000000000000000000000000000000000000000000253df636e000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000400000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad000000000000000000000000000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb80f51bb10119727a7e5ea3538074fb341f56b09ad000000000000000000000000000000000000000000",
                "output": "0x"
        },
        {
                "type": "CALL",
                "depth": 3,
                "from": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
                "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                "value": "0x0",
                "input": "0xa9059cbb000000000000000000000000c840464a8c3324e0bdc9429439dde3a12205424a0000000000000000000000000000000000000000000000000000000253df636e",
                "output": "0x0000000000000000000000000000000000000000000000000000000000000001"
        },
        {
                "type": "DELEGATECALL",
                "depth": 4,
                "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                "to": "0x43506849d7c04f9138d1a2050bbf3a0c054402dd",
                "value": null,
                "input": "0xa9059cbb000000000000000000000000c840464a8c3324e0bdc9429439dde3a12205424a0000000000000000000000000000000000000000000000000000000253df636e",
                "output": "0x0000000000000000000000000000000000000000000000000000000000000001"
        },
        {
                "type": "STATICCALL",
                "depth": 2,
                "from": "0xc840464a8c3324e0bdc9429439dde3a12205424a",
                "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                "value": null,
                "input": "0x70a08231000000000000000000000000c840464a8c3324e0bdc9429439dde3a12205424a",
                "output": "0x00000000000000000000000000000000000000000000000000000026bdd10b17"
        },
        {
                "type": "DELEGATECALL",
                "depth": 3,
                "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                "to": "0x43506849d7c04f9138d1a2050bbf3a0c054402dd",
                "value": null,
                "input": "0x70a08231000000000000000000000000c840464a8c3324e0bdc9429439dde3a12205424a",
                "output": "0x00000000000000000000000000000000000000000000000000000026bdd10b17"
        }
 ]

练习

traceTx 函数修改为使用 Geth 的 debug_traceTransaction,使用之前演示的 callTracer 配置。使用 sepoliaRpcUrl 和相应的 Sepolia 交易哈希进行追踪。

你应该会看到与之前略有不同的追踪输出:

译者注:

traceTx.go 需要修改的地方改为 err = client.CallContext(context.Background(), &result, "debug_traceTransaction", hash, map[string]any{"tracer": "callTracer", "tracerConfig": map[string]any{"withLog": true}})

main.go 中修改成

sepoliaRpcUrl = "https://docs-demo.quiknode.pro/"

res := traceTx("0x92a3761c6afddff637a8edd623d58009875245e78cd77fea810e087292817380", sepoliaRpcUrl)

结果为:

trace result:
 {
        "from": "0x571b102323c3b8b8afb30619ac1d36d85359fb84",
        "gas": "0x53fc",
        "gasUsed": "0x5258",
        "to": "0x4924fb92285cb10bc440e6fb4a53c2b94f2930c5",
        "input": "0x616c6c6168",
        "value": "0x0",
        "type": "CALL"
 }

8. 创建并发送 EIP-4844 Blob 交易

随着 Dencun 硬分叉的上线,以太坊引入了多个 EIP,其中 EIP-4844 是一项重要更新,推出了一种新的交易类型,称为 Blob 交易(类型 3)。

Blob 是 Binary Large Objects 的缩写。在以太坊的上下文中,Blob 代表一种在共识层上持久化的交易数据,而不是像其他交易那样在执行层上处理。因此,要访问这些 Blob 数据,需要使用共识客户端,如 Prysm,而不是执行客户端,例如 Geth。

Blob 交易字段

Blob 交易的字段与 EIP-1559 交易类似,但新增了以下字段:

  • blob_versioned_hashes (vector of sha256 hashes):由多个 SHA256 哈希组成的向量。
  • max_fee_per_blob_gas (uint256):每个 Blob gas 的最大费用。

此外,Blob 交易的 to 字段必须不能为空。

Blob 的版本化哈希为 32 字节。它由一个字节表示版本(当前为 0x01,随着以太坊全面分片,这个值可能会发生变化),后面跟着 Blob 的 KZG 承诺的 SHA256 哈希的最后 31 字节。

版本化哈希

/ go-ethereum/crypto/kzg4844/kzg4844.go
// CalcBlobHashV1 calculates the 'versioned blob hash' of a commitment.

func CalcBlobHashV1(hasher hash.Hash, commit *Commitment) (vh [32]byte) {
    if hasher.Size() != 32 {
    panic("wrong hash size")
    }

    hasher.Reset()
    hasher.Write(commit[:])
    hasher.Sum(vh[:0]) // save the commitment hash to `vh`
    vh[0] = 0x01 // set hash version

    return vh
}

Blob 存储在哪里?

Blob 的完整内容不会嵌入到区块中,也不会保存在执行层,因此无法通过 EVM 访问。相反,它们由信标链(共识层)单独管理,作为 Blob Sidecar 存储,以节省区块空间供正常交易执行使用。

Blob Sidecar 的结构

一个 Sidecar 可以包含以下内容:

  • 一个或多个 Blob(每个 128 字节)。
  • 它们对应的 KZG 承诺列表(每个 48 字节)。
  • 它们对应的 KZG 证明列表(每个 48 字节)。

Blob 在信标链上存储 18 天,之后会被剪除(Pruned)。对于 Rollup 系统来说,可以通过以下方式应对这一存储到期问题:

  1. 自行存储 Blob 数据。
  2. 使用点对点存储网络(P2P Storage)来保存 Blob。

Blob Sidecar

// go-ethereum/core/types/tx_blob.go
// BlobTxSidecar contains the blobs of a blob transaction.
type BlobTxSidecar struct {
    Blobs       []kzg4844.Blob       // Blobs
    Commitments []kzg4844.Commitment // Blob commitments
    Proofs      []kzg4844.Proof      // Blob KZG proofs
}

Blob 用于计算 KZG 承诺,Blob 及其 KZG 承诺一起用于计算 KZG 证明。该证明用于验证 Blob 与承诺的一致性。

Blob 的用途

Blob 的主要用途是处理 Layer 2 和 Rollup 的区块数据,代替使用用户也会使用的 calldata,从而避免对区块空间的竞争。通过使用单独的交易类型(Blob 交易),可以降低 Layer 2 和 Rollup 的成本。

不过,Blob 并不仅限于 Rollup,任何人都可以使用它们。我会在后面演示如何发送 Blob 交易。

每笔交易的 Blob 限制

  • 每笔交易可以包含多个 Blob。
  • 每个区块的目标是 3 个 Blob,但最大限制为 6 个 Blob。
  • 因此,理论上如果一个区块中只有一笔 Blob 交易,该交易可以包含最多 6 个 Blob。

这也意味着,包含这些 Blob 的 Sidecar 会包括与 Blob 数量相同的 Blob 承诺和版本化哈希。例如,若有 6 个 Blob,Sidecar 中就会包含 6 个对应的承诺和版本化哈希。

Blob Gas 概述

Blob 交易使用一种不同类型的费用,称为 Blob Gas,其主要参数包括:

  • MAX_BLOB_GAS_PER_BLOCK:786,432
  • TARGET_BLOB_GAS_PER_BLOCK:393,216
  • MIN_BLOB_BASE_FEE:1

Blob Gas 与我们熟悉的普通交易 Gas 是分开的。Blob Gas 收费机制类似于 EIP-1559,依据网络拥堵情况进行调整。当前一个区块使用的 Gas 超过 TARGET_BLOB_GAS_PER_BLOCK(约 3 个 blobs)时,Blob Gas 会增加;反之,当使用的 Gas 少于该目标时,费用则会降低。

值得注意的是,Blob 的版本化哈希作为对 blobs 的引用存储在执行层中,但 blobs 本身并不存储在执行层。由于 Blob 数据不会被执行,因此不需要优先费用(priority fee)。

在 Go 语言中,创建 Blob 交易的步骤与创建普通交易非常相似,只需使用 types.BlobTx 结构体,并传递与 Blob 相关的字段。

以下是创建 blobTx.go 文件的示例代码:

译者注:和原文相比,代码做了一点点修改,确保能运行。

package main

import (
    "context"
    "fmt"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/crypto/kzg4844"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/holiman/uint256"
    "regexp"
    "strings"
)

// SendBlobTX 向以太坊网络发送带有 EIP-4844 blob 负载的交易。

func sendBlobTX(rpcURL, toAddress, data, privKey string) (string, error) {
    // 连接到以太坊客户端
    client, err := ethclient.Dial(rpcURL)
    if err != nil {
        return "", fmt.Errorf("failed to dial RPC client: %s", err)
    }
    defer client.Close() // 确保在完成函数后关闭连接

    // 获取当前链 ID
    chainID, err := client.ChainID(context.Background())
    if err != nil {
        return "", fmt.Errorf("failed to get chain ID: %s", err)
    }

    // 定义 blob,使用 kzg4844.Blob 类型
    var blob kzg4844.Blob

    // 如有必要,将输入数据转换为十六进制格式的字节切片
    var bytesData []byte
    if data != "" {
        // 检查数据是否为十六进制格式,无论是否带有 '0x' 前缀
        if IsHexWithOrWithout0xPrefix(data) {
            // 确保数据带有 '0x' 前缀
            if !strings.HasPrefix(data, "0x") {
                data = "0x" + data
            }
            // 解码十六进制编码的数据
            bytesData, err = hexutil.Decode(data)
            if err != nil {
                return "", fmt.Errorf("failed to decode data: %s", err)
            }
            // 将解码后的数据复制到 blob 中
            copy(blob[:], bytesData) // 假设 kzg4844.Blob 是一个固定大小的数组
        } else {
            // 如果数据不是十六进制格式,直接复制到 blob 中
            copy(blob[:], data)
        }
    }

    // 使用 KZG4844 加密算法计算 blob 数据的承诺
    BlobCommitment, err := kzg4844.BlobToCommitment(&blob) // 注意这里的参数传递
    if err != nil {
        return "", fmt.Errorf("failed to compute blob commitment: %s", err)
    }

    // 计算 blob 数据的证明,该证明将用于验证交易
    BlobProof, err := kzg4844.ComputeBlobProof(&blob, BlobCommitment) // 注意这里的参数传递
    if err != nil {
        return "", fmt.Errorf("failed to compute blob proof: %s", err)
    }

    // 准备交易的 sidecar 数据,包括 blob 及其加密证明
    sidecar := types.BlobTxSidecar{
        Blobs:       []kzg4844.Blob{blob},
        Commitments: []kzg4844.Commitment{BlobCommitment},
        Proofs:      []kzg4844.Proof{BlobProof},
    }

    // 解码发送者的私钥
    pKeyBytes, err := hexutil.Decode("0x" + privKey)
    if err != nil {
        return "", fmt.Errorf("failed to decode private key: %s", err)
    }

    // 将私钥转换为 ECDSA 格式
    ecdsaPrivateKey, err := crypto.ToECDSA(pKeyBytes)
    if err != nil {
        return "", fmt.Errorf("failed to convert private key to ECDSA: %s", err)
    }

    // 从公钥计算发送者的地址
    fromAddress := crypto.PubkeyToAddress(ecdsaPrivateKey.PublicKey)

    // 获取交易的 nonce
    nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    if err != nil {
        return "", fmt.Errorf("failed to get nonce: %s", err)
    }

    // 创建带有 blob 数据和加密证明的交易
    tx, err := types.NewTx(&types.BlobTx{
        ChainID:    uint256.MustFromBig(chainID),
        Nonce:      nonce,
        GasTipCap:  uint256.NewInt(1e10),           // 最大优先费用每个 gas
        GasFeeCap:  uint256.NewInt(50e10),          // 最大费用每个 gas
        Gas:        250000,                         // 交易的 gas 限制
        To:         common.HexToAddress(toAddress), // 收件人的地址
        Value:      uint256.NewInt(0),              // 交易中转移的金额
        Data:       nil,                            // 此交易中没有发送额外数据
        BlobFeeCap: uint256.NewInt(3e10),           // blob 数据的费用上限
        BlobHashes: sidecar.BlobHashes(),           // 交易中的 blob 哈希
        Sidecar:    &sidecar,                       // 交易中的侧车数据
    }), err

    if err != nil {
        return "", fmt.Errorf("failed to create transaction: %s", err)
    }

    // 使用发送者的私钥签名交易
    signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), ecdsaPrivateKey)
    if err != nil {
        return "", fmt.Errorf("failed to sign transaction: %s", err)
    }

    // 将签名的交易发送到以太坊网络
    if err = client.SendTransaction(context.Background(), signedTx); err != nil {
        return "", fmt.Errorf("failed to send transaction: %s", err)
    }

    // 返回交易哈希
    txHash := signedTx.Hash().Hex()

    return txHash, nil
}

// IsHexWithOrWithout0xPrefix 使用正则表达式检查字符串是否为带或不带 `0x` 前缀的十六进制。
func IsHexWithOrWithout0xPrefix(data string) bool {
    pattern := `^(0x)?[0-9a-fA-F]+$`
    matched, _ := regexp.MatchString(pattern, data)
    return matched
}

更新 main.go 中的 main 函数:

func main() {    
    fmt.Println("using ethclient...")    

    getSuggestedGasPrice(sepoliaRpcUrl)     

    eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)     
    fmt.Println("\nestimate gas for the transaction is:", eGas)     

    rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)    
    fmt.Println("\nRaw TX:\n", rawTxRLPHex)     

    sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)    

    sig, sDetails := signMessage(data, privKey)    
    fmt.Println("\nsigned message:", sDetails)    

    if isSigner := verifySig(sig, from, data); isSigner {        
        fmt.Printf("\n%s signed %s\n", from, data)    } 
    else {        
        fmt.Printf("\n%s did not sign %s\n", from, data)    
    }    

    cNonce, nNonce := getNonce(to, sepoliaRpcUrl)    
    fmt.Printf("\n%s current nonce: %v\n", to, cNonce)    
    fmt.Printf("%s next nonce: %v\n", to, nNonce)    

    res := traceTx("0xd12e31c3274ff32d5a73cc59e8deacbb0f7ac4c095385add3caa2c52d01164c1", mainnetRpcUrl)  
    fmt.Println("\ntrace result:\n", res)

    blob, err := sendBlobTX(sepoliaRpcUrl, to, data, privKey) // 这是刚刚添加的。
    if err != nil {
        log.Fatalln(err)
    }

    fmt.Println("\nBlob transaction hash:", blob) // 这是刚刚添加的。
}

在运行程序之前,请暂时注释掉 sendRawTransactiontraceTx 函数的调用。这是因为从 sendRawTransaction 发送的待处理交易可能会导致在创建 Blob 交易时出现 nonce 冲突(nonce gap error),而后续的 traceTx 调用会混淆终端输出。

完成这些操作后,使用 go run . 命令运行程序。你应该能够获得交易哈希(transaction hash)。

Blob transaction hash: 0x35802814e24e8076348f177216de9cb764792d6e69aa9a4f26f614d7d65f3e90

你可以在 Etherscan 上查找它,这里是我创建的交易哈希: https://sepolia.etherscan.io/blob/0x0142681987b40afb99da6ab299794cd4ab4304c92bec12d2f375c0e52dbd7e9b?bid=481872

总结

Go-Ethereum(Geth)中的 ethclient 包简化了与以太坊的许多常见交互。然而,与其他客户端一样,它并没有提供所有以太坊 JSON-RPC API 的方法,正如我们在交易追踪中所见。在这种情况下,需要手动构造 JSON-RPC 调用。幸运的是,Geth 的 rpc 包为 Go 开发者提供了便利,使得这一过程更加简单。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
0xE
0xE
0x59f6...a17e
17年进入币圈,做过FHE,联盟链,现在是智能合约开发者。 刨根问底探链上真相,品味坎坷悟Web3人生。