使用 Go 语言与以太坊区块链进行交互(下篇):私钥生成与交易签名

·

在上一篇文章中,我们介绍了如何使用 Go 语言与以太坊区块链建立连接、获取当前区块编号以及发送交易(私钥存储在节点上)。本文将继续深入讲解如何利用 go-ethereum 库生成私钥、对交易进行签名,并最终将其发送到以太坊网络。

本文假设您已了解如何建立与以太坊节点的连接。若需回顾,请参考前文内容。同时,关于 Go 开发环境的搭建,您可通过搜索引擎获取相关指南。

私钥的生成方法

在以太坊中,私钥的生成主要有两种方式:从现有字符串还原或全新生成。

从十六进制字符串还原私钥

如果您已有一个私钥的十六进制字符串,可以使用以下代码将其还原为私钥对象:

privKey, err := crypto.HexToECDSA("您的私钥字符串")
if err != nil {
    fmt.Println(err)
} else {
    // 后续操作
}

生成全新的私钥

若需要生成一个新的私钥,可以使用 GenerateKey 函数:

privKey, err := crypto.GenerateKey()
if err != nil {
    fmt.Println(err)
} else {
    // 后续操作
}

获取公钥与以太坊地址

生成或还原私钥后,您可以进一步获取对应的公钥和以太坊地址:

publicKey := privKey.PublicKey
address := crypto.PubkeyToAddress(publicKey).Hex()

交易签名的步骤

成功获取私钥后,接下来需要对交易进行签名。这一过程主要包括三个步骤:创建交易对象、生成签名器(Signer)对象,以及使用私钥进行签名。

创建交易对象

首先,需要构建一个交易对象,指定以下参数:

amount := big.NewInt(1)        // 交易金额
gasLimit := uint64(90000)      // Gas 限制
gasPrice := big.NewInt(0)      // Gas 价格
data := []byte{}               // 附加数据
tx := types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data)

生成签名器对象

根据网络类型选择合适的签名器。例如,对于主网可以使用 HomesteadSigner,而对于测试网(如 Rinkeby)则需要使用 EIP155Signer 并指定链 ID:

// 适用于主网的签名器
signer := types.HomesteadSigner{}

// 适用于测试网的 EIP155 签名器(以链 ID=4 的 Rinkeby 为例)
// signer := types.NewEIP155Signer(big.NewInt(4))

使用私钥进行签名

最后,使用私钥和签名器对交易进行签名:

signedTx, err := types.SignTx(tx, signer, privKey)
if err != nil {
    fmt.Println(err)
}
注意:签名函数并非位于 crypto 包中,而是在 types 包内。这是因为签名器需要处理交易数据的特定格式,例如 EIP155 签名器会调整 v 值(增加 chainId * 2)。

发送已签名的交易

go-ethereum 的 ethclient 提供了 SendTransaction 方法(对应 eth_sendRawTransaction),但其设计不返回交易哈希。为了获取交易哈希,我们可以自定义一个 SendRawTransaction 函数:

func (ec *Client) SendRawTransaction(ctx context.Context, tx *types.Transaction) (common.Hash, error) {
    var txHash common.Hash
    data, err := rlp.EncodeToBytes(tx)
    if err != nil {
        return txHash, err
    }
    err = ec.rpcClient.CallContext(ctx, &txHash, "eth_sendRawTransaction", common.ToHex(data))
    return txHash, err
}

如果您不想新增函数,也可以通过计算序列化交易的哈希值来获取交易编号:

// 序列化已签名的交易
serializedTx, _ := rlp.EncodeToBytes(signedTx)
// 计算交易哈希(即交易编号)
txHash := crypto.Keccak256Hash(serializedTx)

完整交易发送示例

结合以上步骤,以下是发送一笔已签名交易的完整代码示例:

amount := big.NewInt(1)
gasLimit := uint64(90000)
gasPrice := big.NewInt(0)
data := []byte{}
tx := types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data)

signer := types.HomesteadSigner{}
signedTx, _ := types.SignTx(tx, signer, privKey)

txHash, err := client.SendRawTransaction(context.TODO(), signedTx)
if err != nil {
    fmt.Println("发送失败:", err)
} else {
    fmt.Println("交易哈希:", txHash.String())
}

实际测试案例

以下是在 Rinkeby 测试网络上进行实际测试时使用的参数:

amount := big.NewInt(100000000000)  // 0.0000001 Ether
gasLimit := uint64(90000)
gasPrice := big.NewInt(1000000000)  // 1 Gwei
data := []byte("发送自 Go 程序")

使用 EIP155 签名器进行签名(Rinkeby 的链 ID 为 4):

signer := types.NewEIP155Signer(big.NewInt(4))
signedTx, _ := types.SignTx(tx, signer, privKey)

txHash, err := client.SendRawTransaction(context.TODO(), signedTx)
if err != nil {
    fmt.Println(err.Error())
} else {
    fmt.Println(txHash.String())
}

交易发送后,您可以在 Etherscan 上查看交易状态。

不同以太坊网络的链 ID 各不相同。例如,主网链 ID 为 1,Ropsten 为 3,Rinkeby 为 4,Goerli 为 5。具体可参考 EIP155 标准文档。

进阶应用与优化建议

在实际开发中,您可能需要处理更复杂的场景,例如批量发送交易、动态调整 Gas 价格以应对网络拥堵,或使用硬件钱包进行签名。此外,合理设置 Gas 限制和 Gas 价格对交易的成功率和成本至关重要。建议根据网络状况动态获取当前推荐的 Gas 价格,并定期更新您的发送策略以优化用户体验。

👉 查看实时 Gas 价格与网络状态

常见问题

1. 为什么需要自定义 SendRawTransaction 函数?

标准库的 SendTransaction 方法不返回交易哈希,而交易哈希是跟踪交易状态的关键标识。通过自定义函数,我们可以直接获取该哈希值,便于后续查询和日志记录。

2. 如何选择正确的签名器?

选择签名器取决于目标网络。对于主网,使用 HomesteadSigner;对于测试网,需使用 NewEIP155Signer 并传入对应的链 ID。错误的选择会导致交易被网络拒绝。

3. 交易一直处于 pending 状态可能是什么原因?

最常见的原因是 Gas 价格设置过低,导致矿工优先处理其他报酬更高的交易。此外,Nonce 值设置错误或网络拥堵也可能造成延迟。建议使用推荐的 Gas 价格查询工具实时调整。

4. 私钥在程序中如何安全存储?

在实际生产环境中,应避免将私钥硬编码在代码中。推荐使用环境变量、加密配置文件或专业的安全存储服务(如云厂商的密钥管理服务)来管理敏感信息。

5. 如何验证交易是否成功上链?

获取交易哈希后,您可以通过以太坊区块链浏览器(如 Etherscan)输入哈希值查询交易状态。此外,也可以使用 Go 程序定期轮询节点的 eth_getTransactionReceipt 方法获取交易收据,其中包含执行状态等信息。

6. 遇到“invalid sender”错误该如何解决?

这通常表示签名失败或签名器选择错误。请检查私钥格式是否正确、签名器是否与目标网络匹配,并确保交易参数(如 Nonce)设置无误。

总结

本文详细介绍了使用 go-ethereum 库生成私钥、签名交易并发送到以太坊网络的完整流程。通过理解核心概念和实际代码示例,您应已掌握与以太坊交互的关键技能。请注意,文中仅展示了部分核心代码,完整示例可在代码仓库中获取。