在上一篇文章中,我们介绍了如何使用 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 价格,并定期更新您的发送策略以优化用户体验。
常见问题
1. 为什么需要自定义 SendRawTransaction 函数?
标准库的 SendTransaction 方法不返回交易哈希,而交易哈希是跟踪交易状态的关键标识。通过自定义函数,我们可以直接获取该哈希值,便于后续查询和日志记录。
2. 如何选择正确的签名器?
选择签名器取决于目标网络。对于主网,使用 HomesteadSigner;对于测试网,需使用 NewEIP155Signer 并传入对应的链 ID。错误的选择会导致交易被网络拒绝。
3. 交易一直处于 pending 状态可能是什么原因?
最常见的原因是 Gas 价格设置过低,导致矿工优先处理其他报酬更高的交易。此外,Nonce 值设置错误或网络拥堵也可能造成延迟。建议使用推荐的 Gas 价格查询工具实时调整。
4. 私钥在程序中如何安全存储?
在实际生产环境中,应避免将私钥硬编码在代码中。推荐使用环境变量、加密配置文件或专业的安全存储服务(如云厂商的密钥管理服务)来管理敏感信息。
5. 如何验证交易是否成功上链?
获取交易哈希后,您可以通过以太坊区块链浏览器(如 Etherscan)输入哈希值查询交易状态。此外,也可以使用 Go 程序定期轮询节点的 eth_getTransactionReceipt 方法获取交易收据,其中包含执行状态等信息。
6. 遇到“invalid sender”错误该如何解决?
这通常表示签名失败或签名器选择错误。请检查私钥格式是否正确、签名器是否与目标网络匹配,并确保交易参数(如 Nonce)设置无误。
总结
本文详细介绍了使用 go-ethereum 库生成私钥、签名交易并发送到以太坊网络的完整流程。通过理解核心概念和实际代码示例,您应已掌握与以太坊交互的关键技能。请注意,文中仅展示了部分核心代码,完整示例可在代码仓库中获取。