本教程展示了如何在 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 调用:
在本教程的最后,我们将展示如何执行 Blob 交易,这是以太坊在 Decun 升级中最近增加支持的一项功能。
我们还将进行一些与以太坊交易相关的概念操作,例如签名和验证数字签名。
在本教程中,我们将使用 Sepolia 网络,但这些操作同样适用于主网或其他测试网。确保拥有一些 Sepolia ETH。
首先,创建一个新的项目文件夹,打开并初始化:
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 文件。
故障排查提示
如果遇到与模块相关的问题,可以尝试以下步骤:
现在,将以下代码粘贴到项目中的 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
现在,让我们开始创建项目的功能模块。
借助 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
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
以太坊原始交易是未处理形式的交易,使用递归长度前缀(RLP)序列化方法进行编码。
这种编码技术由以太坊执行层(EL)用于序列化和反序列化数据。
原始交易数据是对 nonce、接收者地址(to)、交易金额、data payload 和 gas limit 的编码。
在手动创建以太坊原始交易时,有几种交易类型可供选择,从旧的传统交易(也称为类型 0),需要明确指定 gas price,到 EIP-1559 交易(类型 2),引入了基础费用、优先费用(矿工小费)和每单位 gas 的最大费用,以更好地预测 gas price。
基础费用由网络决定,在一个区块内对所有交易保持固定。然而,它会根据网络拥堵在区块之间进行调整。您可以通过增加提供给矿工的优先费用(小费)来影响交易的优先级。
此外,还有 EIP-2930 交易(类型 1)和 EIP-4844 blob 交易(类型 3,我们将在本文后面讨论)。
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
我们将在下一部分中传播原始交易到网络。
在创建任何类型的原始交易后,我们可以使用 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"
}
如前所述,我们可以离线签署和验证签名的消息。要验证签名的消息,我们需要签名、签署者的地址和原始消息。
下面的 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!
因为数据不正确。
要获取账户的 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 获取并打印给定以太坊地址的当前 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
如前所述,我们将使用 Geth 的 rpc
包进行交易追踪,这一功能并不直接支持 ethclient
。
通过追踪交易,我们可以可视化执行路径,并深入了解交易执行期间的任何事件日志。
在本例中,我们将专注于两个主要方法:debug_traceTransaction
和 Otterscan 提供的自定义 RPC 方法 ots_traceTransaction
(后文将解释)。
debug_traceTransaction
使用 Geth 的原生交易追踪功能,接受交易哈希和一个追踪配置,指定要进行的追踪类型。Geth 有不同的原生追踪器,但我们将使用“callTracer”。要查看所有可用的 Geth 原生追踪器,可以稍后查看文档。
debug_traceTransaction
利用 Geth 内置的交易追踪能力。它需要两个参数:
以下是使用“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_traceTransaction
是 Otterscan 开发的自定义以太坊 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"
}
随着 Dencun 硬分叉的上线,以太坊引入了多个 EIP,其中 EIP-4844 是一项重要更新,推出了一种新的交易类型,称为 Blob 交易(类型 3)。
Blob 是 Binary Large Objects 的缩写。在以太坊的上下文中,Blob 代表一种在共识层上持久化的交易数据,而不是像其他交易那样在执行层上处理。因此,要访问这些 Blob 数据,需要使用共识客户端,如 Prysm,而不是执行客户端,例如 Geth。
Blob 交易的字段与 EIP-1559 交易类似,但新增了以下字段:
此外,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 的完整内容不会嵌入到区块中,也不会保存在执行层,因此无法通过 EVM 访问。相反,它们由信标链(共识层)单独管理,作为 Blob Sidecar 存储,以节省区块空间供正常交易执行使用。
Blob Sidecar 的结构
一个 Sidecar 可以包含以下内容:
Blob 在信标链上存储 18 天,之后会被剪除(Pruned)。对于 Rollup 系统来说,可以通过以下方式应对这一存储到期问题:
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 的主要用途是处理 Layer 2 和 Rollup 的区块数据,代替使用用户也会使用的 calldata,从而避免对区块空间的竞争。通过使用单独的交易类型(Blob 交易),可以降低 Layer 2 和 Rollup 的成本。
不过,Blob 并不仅限于 Rollup,任何人都可以使用它们。我会在后面演示如何发送 Blob 交易。
这也意味着,包含这些 Blob 的 Sidecar 会包括与 Blob 数量相同的 Blob 承诺和版本化哈希。例如,若有 6 个 Blob,Sidecar 中就会包含 6 个对应的承诺和版本化哈希。
Blob 交易使用一种不同类型的费用,称为 Blob Gas,其主要参数包括:
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) // 这是刚刚添加的。
}
在运行程序之前,请暂时注释掉 sendRawTransaction
和 traceTx
函数的调用。这是因为从 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 开发者提供了便利,使得这一过程更加简单。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!