web3.js签名交易时from地址随机变更,仅nonce为0时正常求助
解决web3.js签名以太坊交易时nonce非0导致from地址随机的问题
问题根源
- 签名流程错误:你对未签名交易的序列化结果做哈希后用
web3.eth.sign签名,但web3.eth.sign默认会给哈希添加以太坊消息前缀(\x19Ethereum Signed Message:\n32),导致签名后的r/s/v无法匹配交易的签名规范,最终还原出错误的from地址,nonce变化时交易哈希结构改变,错误被放大为随机地址。 - 手动计算v值逻辑错误:
ethereumjs-tx会根据chainId自动处理EIP-155的v值偏移,手动计算会破坏签名合法性。 - 提前设置空签名字段:未签名交易中提前设置
v/r/s为空值,干扰了交易的RLP编码结构,导致待签名哈希不正确。 - 手续费计算精度丢失:用浮点乘法计算txFee会导致精度损失,进而引发余额判断错误。
解决方案
- 构建交易时只传入核心字段(nonce、gasPrice、gasLimit、to、value、data、chainId),不提前设置
v/r/s。 - 获取交易的待签名原始哈希:使用
tx.hash(false)(false表示排除签名部分),这是以太坊交易签名的标准哈希。 - 用
web3.eth.sign签名该哈希,正确解析r/s/v后赋值给交易对象,让ethereumjs-tx自动处理chainId相关的v值偏移。 - 添加签名后地址验证步骤,确保还原的from地址与预期一致。
修正后的代码
import Transaction from "ethereumjs-tx"; import { toast } from "react-toastify"; import { bufferToHex, toBuffer } from "ethereumjs-util"; // 需要安装ethereumjs-util const createTx = (txData: any) => { // 指定chainId,让Transaction自动处理EIP-155签名规则 return new Transaction(txData, { chain: parseInt(txData.chainId.toString()) }); }; await web3.eth .getTransactionCount(target, "pending") .then(async (nonce1: number) => { const gasPrice: bigint = await web3.eth.getGasPrice(); const gasLimit: bigint = 21000n; // 普通转账标准gasLimit const txFee: bigint = gasPrice * gasLimit; // 用bigint直接计算总手续费,避免精度丢失 const funds: bigint = 10000000000000000n - txFee; const chainId: bigint = await web3.eth.getChainId(); const minEth: bigint = 1000000000000000n; const getBalanceBigInt = BigInt(getBalance); // 确保余额为bigint类型 if (getBalanceBigInt > minEth && getBalanceBigInt > funds + txFee && host) { // 构建基础交易数据,不包含签名字段 const txData = { nonce: web3.utils.toHex(nonce1), gasLimit: web3.utils.toHex(gasLimit), gasPrice: web3.utils.toHex(gasPrice), to: host, value: web3.utils.toHex(funds), data: "0x", chainId: web3.utils.toHex(chainId), }; const tx = createTx(txData); // 获取交易的待签名哈希(标准RLP哈希,不含签名) const txHash = bufferToHex(tx.hash(false)); console.log("待签名交易哈希:", txHash); await web3.eth .sign(txHash, target) .then(async (signed: string) => { // 解析签名结果:去除前缀,拆分r/s/v const sig = signed.slice(2); const r = `0x${sig.slice(0, 64)}`; const s = `0x${sig.slice(64, 128)}`; let v = parseInt(sig.slice(128, 130), 16); // 修正v值的基础偏移(确保符合以太坊签名规范) if (v < 27) { v += 27; } const vHex = web3.utils.toHex(v); console.log("r:", r); console.log("s:", s); console.log("v:", vHex); // 给交易对象赋值签名(转换为buffer格式) tx.r = toBuffer(r); tx.s = toBuffer(s); tx.v = toBuffer(vHex); // 验证还原的from地址是否正确 const recoveredAddress = bufferToHex(tx.getSenderAddress()); console.log("还原的from地址:", recoveredAddress); console.log("预期的from地址:", myAddr); if (recoveredAddress.toLowerCase() !== myAddr.toLowerCase()) { toast.error("签名地址不匹配"); return; } const txFin = "0x" + tx.serialize().toString("hex"); console.log("最终签名交易:", txFin); await web3.eth .sendSignedTransaction(txFin) .then((receipt: any) => { console.log("交易回执:", receipt); toast.success("交易成功"); }) .catch((error: any) => { console.log("发送失败:", error); toast.error("交易发送失败"); }); }) .catch((error: any) => { toast.error("签名失败"); console.log("签名错误:", error); }); } else { toast.error("余额不足或参数不合法"); } }) .catch((error: any) => { console.log("获取nonce失败:", error); toast.error("获取交易nonce失败"); });
关键优化点
- 用bigint直接计算手续费,避免浮点运算导致的精度丢失。
- 添加签名后地址验证,提前发现签名错误。
- 使用
ethereumjs-util工具函数处理缓冲区,避免手动字符串解析出错。 - 采用标准转账gasLimit(21000n),减少不必要的手续费支出。
内容的提问来源于stack exchange,提问作者Zurik Dev
相关产品推荐
相关产品推荐

