Go-Ethereum (Geth) 的 ethclient 包为向 Ethereum 网络发送的 JSON-RPC 请求提供了一个 API 封装,类似于 web3.js 和 ethers.js。
然而,JSON-RPC 的一些功能(如交易追踪)并未在 ethclient(以及 web3.js 和 ethers.js)的 API 中暴露。
本教程展示了在 ethclient 支持 JSON-RPC 调用以及不支持时,如何使用 ethclient 执行操作。
如下图所示,有时我们可以通过使用 ethclient 中的 API 来完成操作,但有时我们必须自己构建 RPC 调用:

在本教程的最后,我们将展示如何执行 blob 交易,这是 Ethereum 最近在 Decun 升级中新增支持的功能。
我们还将演示一些与 Ethereum 交易相关的概念,例如签名和验证数字签名。
先决条件
- 你的计算机上应已安装 Go 语言;如果未安装,请参阅下载说明。
- 具备一些基础的 Go 编程知识。
入门指南
我们将会在本教程中全程使用 Sepolia 网络,但所展示的内容也同样适用于主网或其他测试网。请确保你拥有 Sepolia 上的 ETH。
我们将在本教程中执行的操作:
- 获取网络上的建议 gas 价格
- 估算交易的 gas
- 构造并发送 EIP1559 原始交易
- 签名并验证 Ethereum 消息
- 检索账户的 nonce(交易数量)
- 追踪交易
- 最后,发送 EIP-4844 blob 交易
首先,创建一个新的项目文件夹,打开它并使用以下命令进行初始化:
go mod init eth-rpc
我们刚刚创建了项目模块。如果操作成功,你应该会看到一个如下所示的 ‘go.mod’ 文件

安装必要的依赖项:
go get -u github.com/ethereum/[email protected]
go get github.com/ethereum/go-ethereum/[email protected]
这将生成一个 ‘go.sum’ 文件。
故障排除提示:
如果你遇到与模块相关的问题,请尝试以下操作:
-
删除你的 ‘go.mod’ 和 ‘go.sum’ 文件,并使用
go mod init eth-rpc重新初始化。 -
运行
go mod tidy同步依赖项。 -
如果问题仍然存在,请使用
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"
"log"
"github.com/ethereum/go-ethereum/ethclient"
)
// getSuggestedGasPrice connects to an Ethereum node via RPC and retrieves the current suggested gas price.
func getSuggestedGasPrice(rpcUrl string) {
// Connect to the Ethereum network using the provided RPC URL.
client, err := ethclient.Dial(rpcUrl)
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
// Retrieve the currently suggested gas price for a new transaction.
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatalf("Failed to suggest gas price: %v", err)
}
// Print the suggested gas price to the terminal.
fmt.Println("Suggested Gas Price:", gasPrice.String())
}
现在更新 main.go 中的 main 函数:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
// get gas price on sepolia testnet. This was just added.
}
并运行:
go run .
我们使用 go run . 命令而不是 go run main.go 的原因,是为了编译并执行当前目录中属于同一个包的所有 Go 源文件。这包括 main.go 文件(包含 main 函数)以及任何其他文件,例如包含我们 getSuggestedGasPrice 函数的文件。
我们在后续将会一直使用这个命令。
运行该命令后,建议的 gas 价格应该会打印在终端上。请注意,它的单位是 Wei。

2. 估算交易的 gas 使用量
ethclient 还包含一个 EstimateGas 方法。它返回成功处理一笔交易所需要的 gas 量估算值。
EstimateGas 方法使用构建的 message 作为参数调用 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 tries to estimate the suggested amount of gas that is required to execute a given transaction.
func estimateGas(rpcUrl, from, to, data string, value uint64) uint64 {
// Establish an RPC connection to the specified RPC url
client, err := ethclient.Dial(rpcUrl)
if err != nil {
log.Fatalln(err)
}
var ctx = context.Background()
var (
fromAddr = common.HexToAddress(from) // Convert the from address from hex to an Ethereum address.
toAddr = common.HexToAddress(to) // Convert the to address from hex to an Ethereum address.
amount = new(big.Int).SetUint64(value) // Convert the value from uint64 to *big.Int.
bytesData []byte
)
// Encode the data if it's not already hex-encoded.
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)
}
}
// Create a message which contains information about the transaction.
msg := ethereum.CallMsg{
From: fromAddr,
To: &toAddr,
Gas: 0x00,
Value: amount,
Data: bytesData,
}
// Estimate the gas required for the transaction.
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) // This was just added.
fmt.Println("\nestimate gas for the transaction is:", eGas) // This was just added.
}
使用 go run . 运行代码
我们应该会得到以下结果:

3. 构造 EIP1559 原始交易
Ethereum 原始交易是未处理形式的交易,采用递归长度前缀 (RLP) 序列化方法进行编码。
Ethereum 执行层 (EL) 使用这种编码技术来序列化和反序列化数据。
原始交易数据是 nonce、接收者地址 (to)、交易金额 (value)、数据载荷 (data payload) 以及 gas limit 的编码。
交易类型
在手动为 Ethereum 创建原始交易时,有几种交易类型可供选择,从包含明确 gas price 设定的旧版 legacy 交易(也称为 type 0),到引入基础费用 (base fee)、优先费用 (priority fee/矿工小费) 以及最高每 gas 费用 (max fee per gas) 以提供更好 gas 价格可预测性的 EIP-1559 交易(type 2)。
基础费用由网络决定,并对区块内的所有交易保持固定。不过,它会根据网络拥堵情况在区块之间进行调整。你可以通过增加提供给矿工的优先费用(小费)来影响交易的优先级。
此外,还有 EIP-2930 交易(type 1) 和 EIP-4844 blob 交易(type 3,我们将在本文稍后讨论)。
在 Go 中使用 Geth 选择交易类型
Geth 客户端通过其 types 包支持这些多种交易类型。对于我们的目标,我们将重点关注对应于 EIP-1559 交易模型的 types.DynamicFeeTx。
整个过程不涉及发出任何 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 creates a raw EIP-1559 transaction and returns it as a hex string.
func createRawTransaction(rpcURL, to, data, privKey string, gasLimit, wei uint64) string {
// Connect to the Ethereum client using the provided RPC URL.
client, err := ethclient.Dial(rpcURL)
if err != nil {
log.Fatalln(err)
}
// Retrieve the chain ID for the target Ethereum network.
chainID, err := client.ChainID(context.Background())
if err != nil {
log.Fatalln(err)
}
// Suggest the base fee for inclusion in a block.
baseFee, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatalln(err)
}
// Suggest a gas tip cap (priority fee) for miner incentive.
priorityFee, err := client.SuggestGasTipCap(context.Background())
if err != nil {
log.Fatalln(err)
}
// Calculate the maximum gas fee cap, adding a 2 GWei margin to the base fee plus priority fee.
increment := new(big.Int).Mul(big.NewInt(2), big.NewInt(params.GWei))
gasFeeCap := new(big.Int).Add(baseFee, increment)
gasFeeCap.Add(gasFeeCap, priorityFee)
// Decode the provided private key.
pKeyBytes, err := hexutil.Decode("0x" + privKey)
if err != nil {
log.Fatalln(err)
}
// Convert the private key bytes to an ECDSA private key.
ecdsaPrivateKey, err := crypto.ToECDSA(pKeyBytes)
if err != nil {
log.Fatalln(err)
}
// Extract the public key from the ECDSA private key.
publicKey := ecdsaPrivateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("Error casting public key to ECDSA")
}
// Compute the Ethereum address of the signer from the public key.
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
// Retrieve the nonce for the signer's account, representing the transaction count.
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
// Prepare 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)
}
// Set up the transaction fields, including the recipient address, value, and gas parameters.
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,
}
// Create a new transaction object from the prepared data.
tx := types.NewTx(&txData)
// Sign the transaction with the private key of the sender.
signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), ecdsaPrivateKey)
if err != nil {
log.Fatalln(err)
}
// Encode the signed transaction into RLP (Recursive Length Prefix) format for transmission.
var buf bytes.Buffer
err = signedTx.EncodeRLP(&buf)
if err != nil {
log.Fatalln(err)
}
// Return the RLP-encoded transaction as a hexadecimal string.
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) // This was just added.
fmt.Println("\nRaw TX:\n", rawTxRLPHex) // This was just added.
}
原始交易将使用存储在 ‘privKey’ 变量中的私钥来创建。为了确保在 Sepolia 测试网上交易成功,请将其(私钥)替换为持有测试 Sepolia ETH 的私钥。
使用 go run . 运行代码
我们应该会得到原始交易,如下所示:

在下一节中,我们将向网络广播一笔原始交易。
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 represents the structure of the 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 sends a raw Ethereum transaction.
func sendRawTransaction(rawTx, rpcURL string) {
rawTxBytes, err := hex.DecodeString(rawTx)
if err != nil {
log.Fatalln(err)
}
// Initialize an empty Transaction struct to hold the decoded data.
tx := new(types.Transaction)
// Decode the raw transaction bytes from hexadecimal to a Transaction struct.
// This step converts the RLP (Recursive Length Prefix) encoded bytes back into
// a structured Transaction format understood by the Ethereum client.
err = rlp.DecodeBytes(rawTxBytes, &tx)
if err != nil {
log.Fatalln(err)
}
// Establish an RPC connection to the specified RPC url client, err := ethclient.Dial(rpcURL)
if err != nil {
log.Fatalln(err)
}
// Propagate the transaction
err = client.SendTransaction(context.Background(), tx)
if err != nil {
log.Fatalln(err)
}
// Unmarshal the transaction JSON into a struct
var txDetails Transaction
txBytes, err := tx.MarshalJSON()
if err != nil {
log.Fatalln(err)
}
if err := json.Unmarshal(txBytes, &txDetails); err != nil {
log.Fatalln(err)
}
// Add additional transaction details
txDetails.TransactionTime = tx.Time().Format(time.RFC822)
txDetails.TransactionCost = tx.Cost().String()
// Format some hexadecimal string fields to decimal string
convertFields := []string{"Nonce", "MaxPriorityFeePerGas", "MaxFeePerGas", "Value", "Type", "Gas"}
for _, field := range convertFields {
if err := convertHexField(&txDetails, field); err != nil {
log.Fatalln(err)
}
}
// Marshal the struct back to JSON
txJSON, err := json.MarshalIndent(txDetails, "", "\t")
if err != nil {
log.Fatalln(err)
}
// Print the entire JSON with the added fields
fmt.Println("\nRaw TX Receipt:\n", string(txJSON))
}
func convertHexField(tx *Transaction, field string) error {
// Get the type of the Transaction struct
typeOfTx := reflect.TypeOf(*tx)
// Get the value of the Transaction struct
txValue := reflect.ValueOf(tx).Elem()
// Parse the hexadecimal string as an integer
hexStr := txValue.FieldByName(field).String()
intValue, err := strconv.ParseUint(hexStr[2:], 16, 64)
if err != nil {
return err
}
// Convert the integer to a decimal string
decimalStr := strconv.FormatUint(intValue, 10)
// Check if the field exists
_, ok := typeOfTx.FieldByName(field)
if !ok {
return fmt.Errorf("field %s does not exist in Transaction struct", field)
}
// Set the field value to the decimal string
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) // This was just added.
}
使用 go run . 运行

我们可以在上图中看到交易回执
签名 Ethereum 消息(数字签名)
Ethereum 签名消息可用于创建验证系统。这是一种无需执行链上交易即可验证所有权或同意书的方法。
例如,如果用户 A 用其私钥签名了一条消息并提交给平台,平台将获取该用户的公开地址、消息及签名,并验证该签名是否确实由用户 A 签署;如果是,它就能作为授权平台执行某项操作的凭证(无论签名的原因是什么)。
Ethereum 消息签名利用 secp256k1 椭圆曲线数字签名算法 (ECDSA) 来实现密码学安全性。
Ethereum 签名消息还带有一个前缀,以便它们在网络中具有识别度且具有唯一性。
它的前缀是:\x19Ethereum Signed Message:\n" + len(message),然后在签名之前我们对 prefix+message 进行哈希处理:sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message)))。
Ethereum 还有一个 recovery ID,它被添加到签名的最后一个字节。签名的长度为 65 个字节,分为 3 个部分:v、r 和 s。r 是前 32 个字节,s 是接下来的 32 个字节,v 是代表 recovery ID 的一个字节。
在 Ethereum 中,recovery ID 要么是 27 (0x1b),要么是 28 (0x1c)。通常你会可以在所有 Ethereum 数字签名(或已签名的消息)的末尾看到这个值。
Geth 中用于签名的 crypto 包并不像 Metamask 的 personal_sign 那样自动添加 recovery ID,因此正如你在下面代码中看到的那样,在签名后我们必须使用 sig[64]+=27 手动添加它。
请注意,签名消息完全是链下且离线完成的。它不会发出任何 JSON-RPC 调用。
将以下代码添加至项目目录中的 ‘signMessage.go’ 文件中。
package mainimport (
"crypto/ecdsa"
"encoding/json"
"fmt"
"log"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
// SignatureResponse represents the structure of the signature response.
type SignatureResponse struct {
Address string `json:"address,omitempty"`
Msg string `json:"msg,omitempty"`
Sig string `json:"sig,omitempty"`
Version string `json:"version,omitempty"`
}
// signMessage signs a message using the provided private key.
func signMessage(message, privKey string) (string, string) {
// Convert the private key from hex to ECDSA format
ecdsaPrivateKey, err := crypto.HexToECDSA(privKey)
if err != nil {
log.Fatalln(err)
}
// Construct the message prefix
prefix := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message))) messageBytes := []byte(message)
// Hash the prefix and message using Keccak-256
hash := crypto.Keccak256Hash(prefix, messageBytes)
// Sign the hashed message
sig, err := crypto.Sign(hash.Bytes(), ecdsaPrivateKey)
if err != nil {
log.Fatalln(err)
}
// Adjust signature ID to Ethereum's format
sig[64] += 27
// Derive the public key from the private key
publicKeyBytes := crypto.FromECDSAPub(ecdsaPrivateKey.Public().(*ecdsa.PublicKey))
pub, err := crypto.UnmarshalPubkey(publicKeyBytes)
if err != nil {
log.Fatal(err)
}
rAddress := crypto.PubkeyToAddress(*pub)
// Construct the signature response
res := SignatureResponse{
Address: rAddress.String(),
Msg: message,
Sig: hexutil.Encode(sig),
Version: "2", }
// Marshal the response to JSON with proper formatting
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)
sig, sDetails := signMessage(data, privKey) // This was just added.
fmt.Println("\nsigned message:", sDetails) // This was just added.
}
当你运行代码时应该会得到以下结果:

5. 验证已签名 Ethereum 消息的签名
如上一节所述,我们可以离线签名并验证签名消息。要验证一个签名的消息,我们需要签名、签名者的地址以及原始消息。
下方的 verifySig 函数接收这些参数,将签名解码为字节,并移除 Ethereum recovery ID。原因在于用于签名和验证签名的 crypto 包会检查签名的 recovery ID(第 65 个字节)是否小于 4(我猜测这样做是为了不让其仅局限于 Ethereum 签名)。
之后,我们重构必要的参数(详情见下方代码),并调用 crypto.Ecrecover 函数,它的工作原理类似于 地址为 (0x01) 的 EVM Ecrecover 预编译合约,它会返回对消息进行签名(创建该签名)的地址。
在项目中创建一个 verifySignedMessage.go 文件并添加以下代码:
package main
import (
"fmt"
"log" "strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
// handleVerifySig verifies the signature against the provided public key and hash.
func verifySig(signature, address, message string) bool {
// Decode the signature into bytes
sig, err := hexutil.Decode(signature)
if err != nil {
log.Fatalln(err)
}
// Adjust signature to standard format (remove Ethereum's recovery ID)
sig[64] = sig[64] - 27
// Construct the message prefix
prefix := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message)))
data := []byte(message)
// Hash the prefix and data using Keccak-256
hash := crypto.Keccak256Hash(prefix, data)
// Recover the public key bytes from the signature
sigPublicKeyBytes, err := crypto.Ecrecover(hash.Bytes(), sig)
if err != nil {
log.Fatalln(err)
}
ecdsaPublicKey, err := crypto.UnmarshalPubkey(sigPublicKeyBytes)
if err != nil {
log.Fatalln(err)
}
// Derive the address from the recovered public key
rAddress := crypto.PubkeyToAddress(*ecdsaPublicKey)
// Check if the recovered address matches the provided address
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 { // This was just added.
fmt.Printf("\n%s signed %s\n", from, data)
} else {
fmt.Printf("\n%s did not sign %s\n", from, data)
}
}
我们现在可以确认私钥是否签署了该消息,结果是肯定的。运行 go run .:

练习:将不同的消息传递给 verifySig 函数。由于数据不正确,你应该会得到:0x571B102323C3b8B8Afb30619Ac1d36d85359fb84 did not sign Hello Ethereum!。
6. 检索账户的 nonce(交易数量)
要获取账户的 nonce,我们可以使用 “PendingNonceAt” 或 “NonceAt” 函数。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 fetches and prints the current and next nonce for a given Ethereum address.
func getNonce(address, rpcUrl string) (uint64, uint64) {
client, err := ethclient.Dial(rpcUrl)
if err != nil {
log.Fatalln(err)
}
// Retrieve the next nonce for the address
nextNonce, err := client.PendingNonceAt(context.Background(), common.HexToAddress(address))
if err != nil {
log.Fatalln(err)
}
var currentNonce uint64 // Variable to hold the current nonce.
if nextNonce > 0 {
currentNonce = nextNonce - 1
}
return currentNonce, nextNonce
}
更新 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) // This was just added.
fmt.Printf("\n%s current nonce: %v\n", to, cNonce) // This was just added.
fmt.Printf("%s next nonce: %v\n", to, nNonce) // This was just added.
}
使用 go run . 运行程序,你应该会看到以下结果:

7. 追踪交易
如前所述,我们将使用 Geth 的 ‘rpc’ 包来进行交易追踪,这是一项 ethclient 并不直接支持的功能。
通过追踪交易,我们可以将执行路径可视化,并在交易执行过程中深入了解任何生成的事件日志。
为此,我们将重点讨论两种主要方法:debug_traceTransaction,以及由 Otterscan 提供的自定义 RPC 方法 ots_traceTransaction(见下文解释)。
debug_traceTransaction 使用 Geth 的原生交易追踪机制,它接收交易哈希和追踪配置,用以指定要执行的追踪类型。Geth 拥有多种原生的 tracer,但我们将使用 “callTracer”。要查看所有可用的 Geth 原生 tracer,你可以随后阅读其 官方文档。
debug_traceTransaction 利用了 Geth 内置的交易追踪功能。它需要两个参数:
-
交易哈希,以及
-
追踪配置:指定追踪的细节,例如要捕获的信息类型。Geth 提供了各种原生 tracer,但在这个例子中,我们将关注 “callTracer”。该 tracer 会跟踪在交易执行期间执行的所有调用帧(函数调用)。
以下是一个使用 ‘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 // Define a variable to hold the RPC client.
err error // Variable to catch errors.
)
// Connect to the Ethereum RPC endpoint using the provided URL.
client, err = rpc.Dial(rpcUrl)
if err != nil {
log.Fatalln(err)
}
var result json.RawMessage // Variable to hold the raw JSON result of the call.
// Make the RPC call to trace the transaction using its hash. `ots_traceTransaction` is the method name.
err = client.CallContext(context.Background(), &result, "ots_traceTransaction", hash) // or use debug_traceTransaction with a supported RPC URL and params: hash, map[string]any{"tracer": "callTracer", "tracerConfig": map[string]any{"withLog": true}} for Geth tracing
if err != nil {
log.Fatalln(err)
}
// Marshal the result into a formatted JSON string
resBytes, err := json.MarshalIndent(result, " ", "\t")
if err != nil {
log.Fatalln(err)
}
return string(resBytes))
}
更新 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) // This was just added.
fmt.Println("\ntrace result:\n", res) // This was just added.
}
ots_traceTransaction 是由 Otterscan 开发的一个用于交易追踪的自定义 Ethereum JSON-RPC 方法,它不包含在 Geth 中。它只需交易哈希作为输入,并返回一个结构化的追踪输出,且不包含任何日志。
请注意,sepoliaRpcUrl 变量中的 Sepolia RPC URL 不支持 ots_traceTransaction 方法。在这个例子中,我们将使用存储在 mainnetRpcUrl 变量中的主网 RPC URL,因为该 URL 支持它。
运行该程序后,我们应该会看到追踪调用结果。

练习:修改 traceTx 函数,使用之前演示的 callTracer 配置调用 Geth 的 debug_traceTransaction。使用 sepoliaRpcUrl 和相对应的 Sepolia 交易哈希进行追踪。
你应该会看到一个与之前略有不同的追踪输出,如下所示:

8. 创建并发送 EIP-4844 blob 交易
Dencun 硬分叉刚在 Ethereum 上线,它引入了多个 EIP,其中一个就是带有全新交易类型的 EIP-4844,被称为 blob 交易(type 3)。
Blob 是 Binary Large Objects(二进制大型对象)的缩写。在 Ethereum 中,这是一种保存在 Ethereum 共识层(consensus layer)上的交易数据,而不是像其他交易那样在执行层。因此,你需要像 Prysm 这样的共识客户端来访问它们,而不是作为执行客户端的 Geth。
Blob 交易字段
Blob 交易具有与 EIP-1559 交易相似的字段,但新增了一些字段,如 blob_versioned_hashes(sha256 哈希向量),以及 max_fee_per_blob_gas(uint256),并且规定交易的 to 字段不能为 nil。
长度为 32 个字节的 blob 的版本化哈希 (versioned hash),是由开头代表版本号的单字节(目前为 0x01,当 Ethereum 转向全分片时可能会发生改变)以及其后跟随的 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 sidecars 单独管理,以节省普通交易执行所需的区块空间。
一个 Sidecar 可以包含一个或一组 blob(每个 128 字节)、一组它们对应的 KZG 承诺(每个 48 字节)以及一组它们对应的 KZG 证明(每个 48 字节)。
Blob 会在信标链上存储 18 天,之后便会被修剪掉。Rollup 网络可以自己存储 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 个,最大为 6 个。因此,如果这是区块中唯一的 blob 交易,从技术上讲一笔 blob 交易最多可以包含 6 个 blob。这也意味着包含这些 blob 的 sidecar 将包含与其中 blob 数量相同数量的 blob 承诺和版本化哈希,在本例中为 6 个。
Blob gas
Blob 交易使用一种名为 blob gas 的不同类型 gas,并具有以下参数:MAX_BLOB_GAS_PER_BLOCK 为 786,432;TARGET_BLOB_GAS_PER_BLOCK 为 393,216;以及 MIN_BLOB_BASE_FEE 为 1。
它与我们所知的现有交易 gas 是相互独立的。Blob gas 费用的定价机制与 EIP-1559 相似,即根据网络拥堵情况增减。如果前一个区块使用的 gas 超过 TARGET_BLOB_GAS_PER_BLOCK(约 3 个 blob),则费用增加;如果在前一个区块使用的较少,则费用减少。
请注意,blob 版本化哈希在执行层是作为引用指向 blob 而被存储的。但 blob 并不存储在执行层中。Blob 不需要优先费用(priority fee),因为 blob 数据不会被执行。
最后,在 Go 中创建一笔 blob 交易的步骤与普通交易非常相似,不同之处在于我们使用了 types.BlobTx 结构体,并传入如前文所述的与 blob 相关的字段。
创建一个 blobTx.go 文件并粘贴以下代码:
package transaction
import (
"context"
"fmt"
"regexp"
"strings"
"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"
)
// SendBlobTX sends a transaction with an EIP-4844 blob payload to the Ethereum network.
func sendBlobTX(rpcURL, toAddress, data, privKey string) (string, error) {
// Connect to the Ethereum client
client, err := ethclient.Dial(rpcURL)
if err != nil {
return "", fmt.Errorf("failed to dial RPC client: %s", err)
}
defer client.Close() // Ensure the connection is closed after completing the function
// Retrieve the current chain ID
chainID, err := client.ChainID(context.Background())
if err != nil {
return "", fmt.Errorf("failed to get chain ID: %s", err)
}
var Blob [131072]byte // Define a blob array to hold the large data payload, blobs are 128kb in length
// If necessary, convert the input data to a byte slice in hex format
var bytesData []byte
if data != "" {
// Check if the data is in hex format, with or without the '0x' prefix
if IsHexWithOrWithout0xPrefix(data) {
// Ensure the data has the '0x' prefix
if !strings.HasPrefix(data, "0x") {
data = "0x" + data
}
// Decode the hex-encoded data
bytesData, err = hexutil.Decode(data)
if err != nil {
return "", fmt.Errorf("failed to decode data: %s", err)
}
// Copy the decoded data into the blob array
copy(Blob[:], bytesData)
} else {
// If the data is not in hex format, copy it directly into the blob array
copy(Blob[:], data)
}
}
// Compute the commitment for the blob data using KZG4844 cryptographic algorithm
BlobCommitment, err := kzg4844.BlobToCommitment(Blob)
if err != nil {
return "", fmt.Errorf("failed to compute blob commitment: %s", err)
}
// Compute the proof for the blob data, which will be used to verify the transaction
BlobProof, err := kzg4844.ComputeBlobProof(Blob, BlobCommitment)
if err != nil {
return "", fmt.Errorf("failed to compute blob proof: %s", err)
}
// Prepare the sidecar data for the transaction, which includes the blob and its cryptographic proof
sidecar := types.BlobTxSidecar{
Blobs: []kzg4844.Blob{Blob},
Commitments: []kzg4844.Commitment{BlobCommitment},
Proofs: []kzg4844.Proof{BlobProof},
}
// Decode the sender's private key
pKeyBytes, err := hexutil.Decode("0x" + privKey)
if err != nil {
return "", fmt.Errorf("failed to decode private key: %s", err)
}
// Convert the private key into the ECDSA format
ecdsaPrivateKey, err := crypto.ToECDSA(pKeyBytes)
if err != nil {
return "", fmt.Errorf("failed to convert private key to ECDSA: %s", err)
}
// Compute the sender's address from the public key
fromAddress := crypto.PubkeyToAddress(ecdsaPrivateKey.PublicKey)
// Retrieve the nonce for the transaction
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
fmt.Println(nonce)
if err != nil {
return "", fmt.Errorf("failed to get nonce: %s", err)
}
// Create the transaction with the blob data and cryptographic proofs
tx, err := types.NewTx(&types.BlobTx{
ChainID: uint256.MustFromBig(chainID),
Nonce: nonce,
GasTipCap: uint256.NewInt(1e10), // max priority fee per gas
GasFeeCap: uint256.NewInt(50e10), // max fee per gas
Gas: 250000, // gas limit for the transaction
To: common.HexToAddress(toAddress), // recipient's address
Value: uint256.NewInt(0), // value transferred in the transaction
Data: nil, // No additional data is sent in this transaction
BlobFeeCap: uint256.NewInt(3e10), // fee cap for the blob data
BlobHashes: sidecar.BlobHashes(), // blob hashes in the transaction
Sidecar: &sidecar, // sidecar data in the transaction
}), err
if err != nil {
return "", fmt.Errorf("failed to create transaction: %s", err)
}
// Sign the transaction with the sender's private key
signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), ecdsaPrivateKey)
if err != nil {
return "", fmt.Errorf("failed to sign transaction: %s", err)
}
// Send the signed transaction to the Ethereum network
if err = client.SendTransaction(context.Background(), signedTx); err != nil {
return "", fmt.Errorf("failed to send transaction: %s", err)
}
// Return the transaction hash
txHash := signedTx.Hash().Hex()
return txHash, nil
}
// IsHexWithOrWithout0xPrefix checks if a string is hex with or without `0x` prefix using regular expression.
func IsHexWithOrWithout0xPrefix(data string) bool {
pattern := `^(0x)?[0-9a-fA-F]+$`
matched, _ := regexp.MatchString(pattern, data)
return matched
}
更新 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) // This was just added.
if err != nil {
log.Fatalln(err)
}
fmt.Println("\nBlob transaction hash:", blob) // This was just added.
}
在运行程序之前,请暂时注释掉 sendRawTransaction 和 traceTx 的函数调用。这是因为 sendRawTransaction 的挂起交易 (pending transaction) 在创建 blob 交易时会导致 nonce 冲突(nonce gap 错误),而且随后的 traceTx 调用也会让终端输出变得混乱。
完成上述操作后,使用 go run . 运行。你应该会获得交易哈希。

你可以在 Etherscan 上查阅它,这是我生成的一个:
总结
Go-Ethereum (Geth) 的 ethclient 包简化了许多与 Ethereum 常见的交互。然而,就像其他所有客户端一样,它并没有为所有的 Ethereum JSON-RPC API 提供方法,正如我们在交易追踪中看到的那样。在这种情况下,必须手动构建 JSON-RPC 调用。幸运的是,Geth 的 rpc 包让 Go 开发者的这一过程变得更加轻松。
原文发表于 2024 年 4 月 3 日