以太坊交易签名:使用 JavaScript 深入实践指南


在去中心化应用(DApp)的世界里,与以太坊区块链进行交互是核心环节,而交易,作为区块链上状态变更的唯一方式,其安全性至关重要,交易的“签名”过程,就是将你的所有权(私钥)与交易内容绑定,证明你授权了这笔交易,本文将深入探讨如何使用 JavaScript 来完成以太坊交易的签名,从核心概念到具体代码实践,带你全面掌握这一关键技能。

核心概念:为什么需要签名?

想象一下,你要从自己的银行账户转账给朋友,你不能仅仅告诉银行“我要转100元给张三”,因为这无法证明是你本人发起的请求,你需要输入密码、指纹或进行人脸识别——这就是“签名”。

以太坊交易也是如此,一笔交易包含以下关键信息:

  • 发送者地址
  • 接收者地址
  • 转账金额
  • Gas Limit 和 Gas Price
  • 数据字段

这些信息是公开的,任何人都可以构造一笔交易,如果没有签名,任何人都可以冒用你的地址发起交易,窃取你的资产。

签名的作用

  1. 授权:证明这笔交易确实由私钥的持有者发起,即地址的拥有者。
  2. 防篡改:签名是交易内容和私钥通过特定算法(椭圆曲线算法 ECDSA)生成的唯一值,一旦交易内容被修改,签名将立即失效。

在以太坊中,签名后的交易会被打包进一个 RLP 编码的数据结构中,广播到整个网络,等待矿工打包确认。

签名前的准备工作:工具与库

在 JavaScript 生态中,我们有几个强大的库可以简化以太坊的开发和签名过程,最常用的是 web3.jsethers.js

  • web3.js:历史最悠久的库,功能全面,但 API 相对复杂。
  • ethers.js:一个更现代、更轻量级的库,API 设计更优雅,文档清晰,是目前社区更推荐的选择。

安装: 在你的项目中,通过 npm 或 yarn 安装 ethers.js

npm install ethersyarn add ethers

签名流程详解(使用 ethers.js)

使用 JavaScript 签名交易,通常遵循以下四个步骤:

创建钱包/获取私钥

签名需要私钥,在开发测试时,你可以直接从私钥创建一个钱包对象。️ 警告:在实际应用中,绝对不要在前端代码中硬编码私钥! 这会暴露你的资产,私钥应安全存储,例如通过 MetaMask 扩展让用户自己管理。

import { ethers } from "ethers";
// 假设这是用户的私钥(仅用于演示,切勿在生产环境使用!)
const privateKey = "0x你的私钥...";
// 从私钥创建一个钱包对象
const wallet = new ethers.Wallet(privateKey);
console.log("钱包地址:", wallet.address);

构建交易对象

交易对象是你要对什么内容进行签名的“原材料”,它包含了我们前面提到的所有交易细节。

// 假设我们要向这个地址发送 0.1 个 ETH
const recipientAddress = "0x接收方地址...";
// 创建一个提供者,用于获取当前网络的 Gas Price 等信息
// 在测试网或主网,你需要连接到一个真实的节点,如 Infura 或 Alchemy
const provider = new ethers.providers.JsonRpcProvider("https://rpc.ankr.com/eth_sepolia"); // 示例:Sepolia 测试网
// 获取当前建议的 Gas Price
const gasPrice = await provider.getGasPrice();
// 构建交易对象
const transaction = {
  to: recipientAddress,
  value: ethers.utils.parseEther("0.1"), // 将 0.1 ETH 转换为 Wei (1 ETH = 10^18 Wei)
  gasPrice: gasPrice,
  gasLimit: 21000, // 转账 ETH 的标准 Gas Limit
  nonce: await provider.getTransactionCount(wallet.address, 'latest'), // 获取当前 nonce,防止交易重放
  chainId: 11155111 // Sepolia 测试网的 Chain ID
};

执行签名

有了钱包和交易对象,签名就变得非常简单。wallet.signTransaction() 方法会处理所有复杂的加密计算。

// 对交易进行签名
const signedTransaction = await wallet.signTransaction(transaction);
console.log("签名后的交易:", signedTransaction);
// 输出示例:
// 0xf86a808082... (一长串以 '0x' 开头的十六进制字符串)

这个长长的字符串就是签名后的交易数据,它包含了原始交易内容和你的签名信息,可以直接广播到以太坊网络。

广播交易

签名完成后,交易还没有被记录在区块链上,你需要将其发送到网络,由矿工处理。

// 发送签名后的交易到网络
const txResponse = await provider.sendTransaction(signedTransaction);
console.log("交易已发送,交易哈希:", txResponse.hash);
// 等待交易被打包
const txReceipt = await txResponse.wait();
console.log("交易已确认,区块号:", txReceipt.blockNumber);

至此,一笔完整的以太坊交易从签名到上链的全过程就结束了。

完整代码示例

import { ethers } from "ethers";
async function signAndSendTransaction() {
  // 1. 准备工作
  const privateKey = "0x你的私钥..."; // ️ 替换为你的私钥
  const recipientAddress = "0x接收方地址..."; // ️ 替换为接收方地址
  const wallet = new ethers.Wallet(privateKey);
  console.log("使用钱包地址:", wallet.address);
  // 连接到以太坊测试网 (这里以 Sepolia 为例)
  const provider = new ethers.providers.JsonRpcProvider("https://rpc.ankr.com/eth_sepolia");
  const signer = wallet.connect(provider); // 将钱包连接到提供者,使其成为签名者
  // 2. 构建交易
  const gasPrice = await provider.getGasPrice();
  const nonce = await provider.getTransactionCount(wallet.address, 'latest');
  const transaction = {
    to: recipientAddress,
    value: ethers.utils.parseEther("0.01"),
    gasPrice: gasPrice,
    gasLimit: 21000,
    nonce: nonce,
    chainId: 11155111 // Sepolia Chain ID
  };
  console.log("构建的交易对象:", transaction);
  // 3. 签名交易
  console.log("正在签名交易...");
  const signedTx = await signer.signTransaction(transaction);
  console.log("签名成功!签名后的交易:", signedTx);
  // 4. 广播交易
  console.log("正在广播交易...");
  const txResponse = await provider.sendTransaction(signedTx);
  console.log("交易已发送,等待确认... Tx Hash:", txResponse.hash);
  // 等待交易确认
  const receipt = await txResponse.wait();
  console.log(" 交易已成功确认!区块号:", receipt.blockNumber);
}
// 执行函数
signAndSendTransaction().catch(console.error);

最佳实践与安全注意事项

  1. 私钥安全至上:永远不要在前端代码、日志或版本控制系统中暴露私钥,在生产环境中,应使用硬件钱包(如 Ledger, Trezor)或通过安全的身份验证机制(如 MetaMask)让用户签名。
  2. 使用可靠的节点服务:不要依赖公共节点进行生产部署,它们可能不稳定或被监控,使用 Infura、Alchemy 或自己搭建的私有节点。
  3. 处理异步操作:所有与区块链的交互(获取 Gas Price、Nonce、发送交易等)都是异步的,务必使用 async/await.then() 正确处理。
  4. 错误处理:网络拥堵、Gas Price 不足、Nonce 错误等都可能导致交易失败,代码中应有完善的错误处理逻辑。