区块链 区块链技术 比特币公众号手机端

如何在本地签名以太坊交易并进行广播

概述

为什么 Quicknode 的 Ethereum 端点在被要求签名交易时会返回 unknown account?Ethereum JSON-RPC 接口为此提供了一个方法 eth_signTransaction,但通过托管基础设施调用它会产生以下响应:

{
	"jsonrpc": "2.0",
	"id": 1,
	"error": { "code": -32000, "message": "unknown account" }
}

这个响应看起来可能像一个 bug、权限问题或缺失的功能。实际上,节点报告它不知道该账户,是因为它不持有客户的私钥。

本指南解释了托管端点为何如此表现,并演示了替代模式。它通过 Viem 的本地账户对交易进行签名,并使用 eth_sendRawTransaction 广播序列化后的字节。这种分离使私钥不会进入托管基础设施。

四步本地签名流程。步骤 01 通过 Quicknode 端点准备 nonce、gas 和费用数据。步骤 02 在虚线密钥边界内、于你的机器上用私钥对负载进行签名,期间没有任何 RPC 调用。步骤 03 使用 eth_sendRawTransaction 广播序列化后的字节。步骤 04 确认回执状态。注释显示 eth_signTransaction 返回错误 -32000 unknown account,eth_accounts 返回空数组,且只有准备好的数据和已签名的字节跨越该边界。四步本地签名流程。步骤 01 通过 Quicknode 端点准备 nonce、gas 和费用数据。步骤 02 在虚线密钥边界内、于你的机器上用私钥对负载进行签名,期间没有任何 RPC 调用。步骤 03 使用 eth_sendRawTransaction 广播序列化后的字节。步骤 04 确认回执状态。注释显示 eth_signTransaction 返回错误 -32000 unknown account,eth_accounts 返回空数组,且只有准备好的数据和已签名的字节跨越该边界。

TL;DR

  • 检查账户:eth_accounts 返回空数组,因为 Quicknode 端点不管理客户密钥,所以 eth_signTransaction 无法签名
  • **准备:**使用端点获取待处理的 nonce、gas 估算值和当前费用
  • **本地签名:**用完整交易调用 Viem 本地账户的 account.signTransaction 方法;这一步可以离线运行
  • **广播:**使用 eth_sendRawTransaction 提交已签名的字节,而不暴露私钥
  • **验证:**等待回执并确认其状态报告成功

你将要做的事

  • 诊断为什么托管端点没有暴露可签名的账户
  • 使用 Viem 在本地签署一笔 Ethereum 交易并恢复其发送者
  • 在 Ethereum Sepolia 上准备、广播和验证一笔交易
  • 比较 ethers.js v6 中的相同工作流
  • 解读广播错误并选择合适的密钥存储模型

你需要准备的内容

  • 一个带有 Ethereum Sepolia 端点的 Quicknode 账户,用于读取 nonce 和 gas 费用以及广播
  • Node.js v20.6 或更高版本,因为示例使用了内置的 --env-file 标志
  • 一款代码编辑器,例如 VS Code
  • 一个持有少量 Sepolia ETH 作为 gas 的测试钱包,可从 Quicknode Multi-Chain Faucet 获取
  • 对 Ethereum 交易 和 Ethereum 账户 有基本了解

为什么端点无法替你签名

当节点不管理所提供的 from 地址的私钥时,eth_signTransaction 会返回 unknown account。只有当节点持有并解锁了该账户的密钥时,该方法才能工作。

这个方法诞生于运行节点和使用钱包是同一件事的时代。你运行自己的 Geth 实例,将密钥导入其 keystore,解锁账户,然后节点代表你签名。在那个设置中,节点就是你的钱包,因此让它签名是合理的。

托管基础设施采用不同的模式。一个端点在一组机器(机群)上为众多客户提供服务,运营者会更换和重新平衡这些机器。在面向互联网的节点上存储客户私钥会成为有吸引力的攻击目标,并将密钥可用性与不断变化的基础设施捆绑在一起。Quicknode 不存储客户密钥,这就是 eth_signTransaction 文档将该方法列为不支持的原因。

相关的 eth_accounts 方法让这一点变得具体而非停留在理论层面。它会报告节点可以为其签名的账户,你可以通过一次调用来检查。自己验证只需几行代码,所以在编写任何签名代码之前,让我们先搭建一个项目并亲自验证一下。

搭建项目

创建一个目录,安装经过测试的 Viem 和 tsx 版本(tsx 可以直接运行 TypeScript 文件,无需编译步骤或 tsconfig.json),并将项目标记为 ES 模块:

mkdir sign-locally && cd sign-locally
npm init -y
npm pkg set type=module
npm install --save-exact viem@2.55.16
npm install --save-dev --save-exact tsx@4.20.5

将你的端点和测试密钥保存在 .env 文件中。Node 通过 --env-file 原生读取该文件,因此不需要 dotenv 依赖:

QUICKNODE_ENDPOINT=https://your-endpoint.ethereum-sepolia.quiknode.pro/your-token/
PRIVATE_KEY=0xyour_test_private_key

你也可以添加 TO_ADDRESS,把广播示例发送到另一个 Sepolia 地址。如果省略它,脚本会把测试转账发回给签名者。

在创建交易脚本之前,先添加一个小型配置辅助模块。它在客户端或账户使用每个环境变量之前对其进行验证,并且只在调用脚本需要某个值时才要求提供该值。由于 tsx 直接运行 TypeScript 源码,示例通过其实际的 config.ts 文件名导入这个辅助模块。

创建 config.ts

import { isAddress, type Address, type Hex } from 'viem'

export function requireEndpoint(): string {
	const endpoint = process.env.QUICKNODE_ENDPOINT
	if (!endpoint) throw new Error('QUICKNODE_ENDPOINT is not set')

	const url = new URL(endpoint)
	if (url.protocol !== 'https:')
		throw new Error('QUICKNODE_ENDPOINT must use HTTPS')
	return endpoint
}

export function requirePrivateKey(): Hex {
	const privateKey = process.env.PRIVATE_KEY
	if (!privateKey || !/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
		throw new Error(
			'PRIVATE_KEY must be 0x followed by 64 hexadecimal characters'
		)
	}
	return privateKey as Hex
}

export function getToAddress(fallback: string): Address {
	const to = process.env.TO_ADDRESS ?? fallback
	if (!isAddress(to))
		throw new Error('TO_ADDRESS is not a valid Ethereum address')
	return to
}

使用一次性密钥

使用纯粹为测试而生成的密钥,只充入测试网 ETH。切勿将控制真实资金的私钥放入 .env 文件中,并在首次提交前将 .env 加入你的 .gitignore

确认节点不持有任何密钥

与其轻信上面的解释,不如直接询问端点。这个脚本做两件事:列出节点可以为其签名的账户,然后仍然请求节点为一笔交易签名,让你看到确切的错误。

创建 check-node-keys.ts

import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { sepolia } from 'viem/chains'
import { requireEndpoint, requirePrivateKey } from './config.ts'

async function main() {
	const account = privateKeyToAccount(requirePrivateKey())
	const client = createWalletClient({
		chain: sepolia,
		transport: http(requireEndpoint()),
	})

	// 询问节点管理哪些账户
	const accounts = await client.request({ method: 'eth_accounts' })
	console.log('accounts the node can sign for:', accounts)

	// 让节点为一个它并不持有的地址签名
	try {
		await client.request({
			method: 'eth_signTransaction' as never,
			params: [\
				{\
					from: account.address,\
					to: account.address,\
					gas: '0x5208',\
					gasPrice: '0x9184e72a000',\
					value: '0x9184e72a',\
					nonce: '0x1',\
				},\
			] as never,
		})
	} catch (error) {
		const rpcError = error as { details?: string; shortMessage?: string }
		console.log(
			'eth_signTransaction failed:',
			rpcError.details ?? rpcError.shortMessage
		)
	}
}

main().catch(error => {
	console.error(
		'failed:',
		error instanceof Error ? error.message.split('\n')[0] : error
	)
	process.exit(1)
})

运行它:

npx tsx --env-file=.env check-node-keys.ts
accounts the node can sign for: []
eth_signTransaction failed: unknown account

空数组表明端点没有暴露任何可签名的账户。Quicknode 不管理测试账户的密钥,因此 eth_signTransaction 无法为它的地址签名。unknown account 消息表示端点找不到与所提供的 from 地址对应的节点管理的密钥。

在本地签署 Ethereum 交易

密码学签名操作不需要节点。它将完整的交易负载与私钥结合在一起,因此 Viem 可以完全在本地账户中完成它。交易必须已经包含其 chain ID、nonce、gas 上限和费用字段。

创建 sign-locally.ts。它直接调用本地账户的 signTransaction 方法并手动提供每个字段,因此不会发生网络请求:

import {
	parseEther,
	parseTransaction,
	recoverTransactionAddress,
	type TransactionSerializedEIP1559,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { sepolia } from 'viem/chains'
import { requirePrivateKey } from './config.ts'

async function main() {
	const account = privateKeyToAccount(requirePrivateKey())

	// 所有字段都已提供,因此本地账户无需 transport 即可签名
	const serialized = (await account.signTransaction({
		chainId: sepolia.id,
		type: 'eip1559',
		to: '0x0000000000000000000000000000000000000000',
		value: parseEther('0.0001'),
		gas: 21000n,
		maxFeePerGas: 2_000_000_000n,
		maxPriorityFeePerGas: 1_000_000n,
		nonce: 0,
	})) as TransactionSerializedEIP1559

	console.log('signer address:        ', account.address)
	console.log('raw signed transaction:', serialized)

	const decoded = parseTransaction(serialized)
	console.log('\ndecoded type:   ', decoded.type)
	console.log('decoded chainId:', decoded.chainId)

	// 从已签名的负载和签名中恢复发送者
	const recovered = await recoverTransactionAddress({
		serializedTransaction: serialized,
	})
	console.log('\nrecovered signer:', recovered)
	console.log(
		'matches account: ',
		recovered.toLowerCase() === account.address.toLowerCase()
	)
}

main().catch(error => {
	console.error('failed:', error instanceof Error ? error.message : error)
	process.exit(1)
})

运行它:

npx tsx --env-file=.env sign-locally.ts
signer address:         <your test address>
raw signed transaction: 0x...

decoded type:    eip1559
decoded chainId: 11155111

recovered signer: <your test address>
matches account:  true

这段输出中有三点值得单独拿出来说明。

那串长长的十六进制字符串就是完整的交易,包含签名在内,任何人都可以转发它。chainId11155111 标识的是 Sepolia,它是已签名字节的一部分,因此这笔确切的交易在具有不同 chain ID 的网络上无效。EIP-155 为传统交易引入了 chain ID 重放保护。这里展示的 type 2 交易在其 EIP-1559 签名负载 中直接包含了 chain_id

最重要的是,recoverTransactionAddress 从序列化交易的签名负载和签名中恢复了发送者。任何人都可以在不了解私钥的情况下执行这项检查。节点在应用诸如 nonce、余额和 gas 有效性等有状态检查之前,也会执行同样的发送者恢复操作。私钥从未离开过运行此脚本的进程。

签名可以离线完成

因为 account.signTransaction 不使用 transport,持有密钥的机器不需要网络访问。高价值场景可以在物理隔离(air-gapped)的机器上签名,然后仅将生成的原始交易移动到联网机器上进行广播。

使用 eth_sendRawTransaction 广播

上一个脚本硬编码了 nonce 和费用,这对演示来说没问题,但不会被矿工打包。真实的交易需要你账户当前的 nonce 以及足以应对当前网络状况的费用,而这些恰恰是你无法在本地得知的事实。这正是端点发挥作用的地方:它提供这些数值,然后转发你已签名的字节。

Viem 的 prepareTransactionRequest action 会获取待处理的 nonce、估算 gas 并填入 EIP-1559 费用。一个库 action 可能会发出多个 RPC 请求。创建 send-signed-tx.ts

import {
	createPublicClient,
	createWalletClient,
	formatEther,
	http,
	parseEther,
	recoverTransactionAddress,
	type TransactionSerializedEIP1559,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { sepolia } from 'viem/chains'
import { getToAddress, requireEndpoint, requirePrivateKey } from './config.ts'

async function main() {
	const endpoint = requireEndpoint()
	const account = privateKeyToAccount(requirePrivateKey())
	const to = getToAddress(account.address)

	const publicClient = createPublicClient({
		chain: sepolia,
		transport: http(endpoint),
	})

	const walletClient = createWalletClient({
		account,
		chain: sepolia,
		transport: http(endpoint),
	})

	const endpointChainId = await publicClient.getChainId()
	if (endpointChainId !== sepolia.id) {
		throw new Error(
			`expected Sepolia chain ID ${sepolia.id}, received ${endpointChainId}`
		)
	}

	// 1. 向端点请求当前的 nonce、gas 和费用数据
	const request = await walletClient.prepareTransactionRequest({
		type: 'eip1559',
		to,
		value: parseEther('0.0001'),
	})

	console.log('1. prepared, unsigned')
	console.log('   nonce:', request.nonce)
	console.log('   gas:  ', request.gas)
	console.log('   maxFeePerGas:', request.maxFeePerGas)

	// 2. 通过本地账户签名。这一步不会发出 RPC 请求
	const serialized = (await account.signTransaction({
		chainId: request.chainId,
		type: 'eip1559',
		to: request.to,
		value: request.value,
		gas: request.gas,
		maxFeePerGas: request.maxFeePerGas,
		maxPriorityFeePerGas: request.maxPriorityFeePerGas,
		nonce: request.nonce,
	})) as TransactionSerializedEIP1559
	console.log('\n2. signed locally')
	console.log(
		'   recovered signer:',
		await recoverTransactionAddress({ serializedTransaction: serialized })
	)

	// 3. 广播已签名的字节。端点只负责转发,无法更改它们
	const hash = await publicClient.sendRawTransaction({
		serializedTransaction: serialized,
	})
	console.log('\n3. broadcast via eth_sendRawTransaction')
	console.log('   hash:', hash)

	const receipt = await publicClient.waitForTransactionReceipt({ hash })
	if (receipt.status !== 'success')
		throw new Error(`transaction reverted: ${hash}`)

	console.log('\n4. mined in block', receipt.blockNumber)
	console.log('   status:  ', receipt.status)
	console.log(
		'   fee paid:',
		formatEther(receipt.gasUsed * receipt.effectiveGasPrice),
		'ETH'
	)
}

main().catch(error => {
	console.error(
		'failed:',
		error instanceof Error ? error.message.split('\n')[0] : error
	)
	process.exit(1)
})

从 Quicknode Multi-Chain Faucet 为你的测试地址充值,然后运行:

npx tsx --env-file=.env send-signed-tx.ts
1. prepared, unsigned
   nonce: <current nonce>
   gas:   <estimated gas>
   maxFeePerGas: <current maximum fee>

2. signed locally
   recovered signer: <your test address>

3. broadcast via eth_sendRawTransaction
   hash: 0x...

4. mined in block <block number>
   status:   success
   fee paid: <amount> ETH

到达链上的交易与你签名的交易逐字节一致。端点可以拒绝转发它,但无法更改接收者、金额或费用,因为任何修改都会使签名失效。正是这一特性使得通过并非你自己运行的基础设施进行广播变得安全。

并发下的 nonce 处理

prepareTransactionRequest 从端点读取待处理的 nonce,这反映了该端点已经见过的交易。如果你的服务并发签署多笔交易,两个请求仍可能消耗相同的 nonce。大批量发送的服务会协调 nonce 分配并以原子方式递增它。详情请参阅 如何通过 Ethereum 交易管理 Nonce。

何时应将签名与广播分开

Viem 还提供了 sendTransaction,它在一次调用中完成准备、签名和广播。对于大多数应用代码而言,这是正确的选择,而且它做的正是你刚刚手动写出的那三个步骤。

当你需要掌控签名与发送之间的时间差时,将它们分开编写才有回报:

  • **离线签名。**在从不连接网络的机器上签名,然后将原始交易带到联网机器上进行广播。
  • **延迟或定时发送。**现在就签名,保存好字节,等到条件触发时再广播。签名在密码学上依然有效,但能否被打包仍取决于广播时的 nonce、余额和费用条件。
  • **替换卡住的交易。**使用相同的 nonce 并以更高的费用签署一笔替代交易,然后广播它以取代原交易。
  • **向多个目的地广播。**将相同的已签名字节同时发送到公共端点和私有中继。网络只能将该交易打包一次。
  • **发送前检查。**在这些字节到达网络之前,先解码并检查其确切内容,或者将其送入审批环节。

解读网络返回的错误

一旦你广播了已签名的字节,网络就会评估其中的字段。错误文本因执行客户端、提供商策略和客户端版本而异。下表列出了具有代表性的 Geth 风格或提供商消息,以及需要检查的字段。

消息 原因 解决方法
nonce too low: next nonce 4, tx nonce 0 该 nonce 已被一笔已上链的交易使用 使用 getTransactionCount 重新读取待处理 nonce,或在签名前使用 prepareTransactionRequest
already known 你两次广播了相同的已签名字节 将其视为来自该节点的幂等确认,然后继续监控交易哈希
replacement transaction underpriced 同一 nonce 上已有另一笔交易待处理,而你的费用提升幅度不够 大幅提高 maxFeePerGasmaxPriorityFeePerGas,而不是只增加一 wei
insufficient funds for gas * price + value 余额无法覆盖 value 加上 gas * maxFeePerGas 减少金额或为账户充值。消息会报告你现有的金额和所需的金额
transaction underpriced 交易不符合节点的交易池定价策略,这可能涉及费用上限或优先费 获取当前费用并检查结构化的 RPC 错误,而不是只匹配消息文本
intrinsic gas too low gas 上限低于该交易所需的最低值 普通转账至少使用 21,000,或调用 estimateGas
invalid chain ID 这些字节是为另一个网络签名的 使用你要广播到的那个网络的 chain ID 来签名

其中有两个值得进一步细看,因为它们的实际行为与字面读起来的意思不同。

already known 表示被查询的节点已经见过确切的交易字节。正在重试的广播方可以将其视为来自该节点的幂等确认,然后继续监控交易哈希。

节点可能会在未来 nonce 的交易排队等待缺失的 nonce 时将其保留。接受与否、队列容量和保留时间取决于执行客户端和提供商配置。例如,Geth 交易池设置 对排队的交易数量设置了上限,并应用可配置的生命周期。应用程序应该监控交易并在需要时重新广播,而不是假设节点会无限期地保留它。

在联网边界处检查链

离线签名者无法得知端点连接的是哪个网络。在线脚本会在准备交易之前检查端点的 chain ID。在把准备好的请求移交给独立签名者时,要保留这项检查。广播节点会拒绝为其他链签名的字节。

使用 ethers.js v6 签署并广播 Ethereum 交易

本地签名模式并不是 Viem 独有的。ethers.js v6 同样可以利用网络数据准备交易、通过本地密钥签名,并广播序列化后的交易。

安装经过测试的 ethers.js v6 版本:

npm install --save-exact ethers@6.17.0

创建 ethers-example.ts。provider 负责提供链数据、gas 估算和广播,而 wallet.signTransaction 则在本地完成签名:

import { JsonRpcProvider, Network, Transaction, Wallet, parseEther } from 'ethers'
import { getToAddress, requireEndpoint, requirePrivateKey } from './config.ts'

// ethers 按名称解析已注册的网络,因此 chain ID 不是硬编码的
const sepolia = Network.from('sepolia')

async function main() {
	const provider = new JsonRpcProvider(requireEndpoint())
	const wallet = new Wallet(requirePrivateKey(), provider)
	const to = getToAddress(wallet.address)
	const value = parseEther('0.0001')

	const network = await provider.getNetwork()
	if (network.chainId !== sepolia.chainId) {
		throw new Error(
			`expected Sepolia chain ID ${sepolia.chainId}, received ${network.chainId}`
		)
	}

	const fees = await provider.getFeeData()
	if (fees.maxFeePerGas === null || fees.maxPriorityFeePerGas === null) {
		throw new Error('the endpoint did not return EIP-1559 fee data')
	}

	const gasLimit = await provider.estimateGas({
		from: wallet.address,
		to,
		value,
	})
	const nonce = await provider.getTransactionCount(wallet.address, 'pending')

	// signTransaction 返回序列化后的已签名字节,但不进行广播
	const serialized = await wallet.signTransaction({
		to,
		value,
		gasLimit,
		maxFeePerGas: fees.maxFeePerGas,
		maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
		nonce,
		chainId: network.chainId,
		type: 2,
	})

	console.log('recovered signer:', Transaction.from(serialized).from)

	// broadcastTransaction 通过 eth_sendRawTransaction 发送这些字节
	const sent = await provider.broadcastTransaction(serialized)
	const receipt = await sent.wait()
	if (!receipt) throw new Error(`transaction was not mined: ${sent.hash}`)
	if (receipt.status !== 1)
		throw new Error(`transaction reverted: ${sent.hash}`)

	console.log('block:', receipt.blockNumber, 'status: success')
}

main().catch(error => {
	console.error(
		'failed:',
		error instanceof Error ? error.message.split('\n')[0] : error
	)
	process.exit(1)
})

使用以下命令运行:

npx tsx --env-file=.env ethers-example.ts

交易成功后,ethers.js 脚本会打印出如下形式的输出:

recovered signer: <your test address>
block: <block number> status: success

当两个示例使用同一个私钥时,ethers.js 恢复出的签名者地址与 Viem 相同。两者序列化后的交易和签名可能不同,因为每次运行读取的都是当时的 nonce、gas 和费用数据。

私钥应该存放在哪里

.env 文件适合一次性测试脚本,不适合任何涉及真实价值的场景。在你加固签名者的过程中,本指南中的这条边界始终不变:发生变化的只是执行签名的组件。

在本地开发中,继续使用专为测试生成的密钥,只充入测试网 ETH,并将其放在被 gitignore 忽略的 .env 中。

对于 Hardhat 或 Foundry 项目,使用加密机密保护 Hardhat 和 Foundry 中的私钥 介绍了如何用框架支持的加密存储取代明文密钥。

在服务器上,不要把密钥打进镜像或提交到仓库。在运行时从 AWS Secrets Manager、Google Secret Manager 或 HashiCorp Vault 等机密管理器中注入密钥,这样轮换密钥无需重新部署,密钥值也永远不会出现在构建产物或日志里。

对于任何涉及重要价值的场景,不要再在应用中直接处理原始密钥材料。密钥管理服务(KMS)或硬件安全模块(HSM)可以保管不可导出的密钥,并接受交易哈希来进行签名。这两个 TypeScript 库都为这种模式提供了集成点:Viem 来自 viem/accountstoAccount 接受自定义的 signTransaction 实现,而 ethers.js 允许你扩展 AbstractSigner。自定义签名者把签名请求发给 KMS 或 HSM,而准备和广播流程保持不变。

当资金由个人而非服务控制时,硬件钱包能提供同等的边界。密钥留在设备上,由设备完成签名,应用程序收到的则是同样格式的序列化已签名交易。

绝不要向端点发送私钥

没有任何合法的 RPC 方法会接受私钥。eth_signTransaction 有时被误认为是传递私钥的地方,但它并不是。任何要求你把密钥材料放进 RPC 请求中的工具或教程都是不安全的。

总结

你现在明白了为什么 eth_signTransaction 在托管端点上会失败,以及为什么这种失败恰恰保护了密钥边界。你构建了替代模式:通过端点准备当前的 nonce、gas 和费用数据,通过持有密钥的本地账户完成签名,再用 eth_sendRawTransaction 广播最终的字节。你还在网络执行其有状态有效性检查之前,从已签名的交易中恢复了发送者。

这个模式在规模扩大后依然成立。库可以是 Viem 或 ethers.js,签名者可以使用一次性 .env 密钥、加密存储、机密管理器、KMS 或硬件钱包。边界始终保持不变:密钥始终处于你的控制之下,端点只会接收到已经签好名的序列化字节。

同样的流程适用于每一条受支持的以太坊虚拟机(EVM)链。替换链配置和端点,然后用该网络的 chain ID 进行准备和签名。

后续步骤

  • 如何使用 Viem 发送交易 介绍合约调用和以太坊名称服务(ENS)收款人。
  • 如何通过 Ethereum 交易管理 Nonce 讲解了大批量发送服务的协调式 nonce 分配。
  • 如何以更高的 Gas 价格重新发送交易 演示了如何复用 nonce 并提高费用来替换一笔待处理交易。
  • 账户抽象与 ERC-4337 解释了 Ethereum Request for Comments 4337(ERC-4337)如何改变签名对象和验证路径:用户授权一个 UserOperation,bundler 再通过 EntryPoint 合约提交它。

常见问题

为什么 eth_signTransaction 返回 'unknown account'?

该方法要求节点用它为你的地址所管理的私钥来签名。Quicknode 端点不管理客户密钥,因此没有可用于签名的地址。在同一端点上调用 eth_accounts 会返回空数组。请改为通过本地账户签名,并用 eth_sendRawTransaction 广播。

eth_signTransaction 有可用的时候吗?

可以,在你自己运行的、keystore 中含有已解锁账户的节点上就可以,这也是该方法最初的设计用途。它不适合共享基础设施,因为它要求节点持有你的私钥,因此大多数公共提供商都不支持它。

签署交易需要端点吗?

密码学签名操作本身不需要端点。本指南用完整负载调用 Viem 本地账户的 account.signTransaction 方法,因此这一步可以离线完成。但要读取当前的 nonce、gas 和费用数据,以及广播已签名的交易,仍然需要端点。

端点能在广播之前修改我的交易吗?

不能。签名覆盖了交易的每一个字段,因此任何修改都会使其失效并被网络拒绝。端点可以拒绝转发你的交易,但它无法更改接收者、金额、gas 数值或 calldata。

一笔已签名的 Sepolia 交易能在主网上重放吗?

不能。文中展示的 EIP-1559 交易在其签名负载中包含了 Sepolia 的 chain ID 11155111,因此在具有不同 chain ID 的网络上无效。你可以通过解码原始交易并读取其 chainId 字段来验证这一点。

这种方法在其他 EVM 链上也适用吗?

适用。替换链配置和端点,然后用该网络的 chain ID 进行准备和签名。本地签名加原始广播的模式适用于所有受支持的 EVM 链。

  • 原文链接: quicknode.com/guides/eth...
  • 鸿途知科网 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
版权声明

本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门