Checkpoint 2:账户生成与 Sepolia 部署排错

挑战地址:https://speedrunethereum.com/challenge/tokenization

本文是 Speedrun Ethereum Tokenization 系列第一篇,记录 Checkpoint 2 中账户生成和 Sepolia 部署的两个问题。

隐私说明:本文不包含真实钱包地址、交易哈希、部署 URL、IPFS 哈希或本地绝对路径。

系列目录

背景

这个挑战要求使用 Scaffold-ETH 2 和 Foundry 完成一个 ERC-721 NFT 项目 YourCollectible,流程包括:

  1. 编写和编译 ERC-721 合约
  2. 生成部署账户
  3. 部署到本地链
  4. 部署到 Sepolia
  5. 更新前端合约地址和 ABI

Checkpoint 2 中遇到了两个问题:

  • yarn generate 无法生成部署账户
  • 部署到 Sepolia 的交易被打包,但执行失败

两个问题都需要结合工具版本、输出流和链上 gas 数据来分析。

环境信息

本次排查环境如下:

1
2
3
4
5
6
7
8
9
10
11
12
操作系统: macOS
终端: zsh
Node.js: v26.7.0
Yarn: 4.13.0
Foundry: 1.8.5
Solidity 编译器: 0.8.37
Next.js: 16.2.12
Vercel CLI: 62.1.0
网络: Sepolia
Chain ID: 11155111
项目框架: Scaffold-ETH 2
智能合约框架: Foundry

项目目录:

1
/path/to/challenge-tokenization

问题一(Checkpoint 2:部署到测试网前):yarn generate 报 Could not extract private key from output

当前 Challenge 步骤:Checkpoint 2 - Deploy to a Testnet

此时已经完成 ERC-721 合约,准备生成部署账户、连接测试网并部署到 Sepolia。

1. 复现命令

进入正确项目目录:

1
cd "/path/to/challenge-tokenization"

执行:

1
yarn generate

终端输出:

1
2
3
🔑 Generating new wallet...

❌ Could not extract private key from output

一开始很容易误以为是“新终端没有加载环境”,或者 node、yarn、cast 没有安装。

但检查后确认:

1
2
3
4
node  -> /opt/homebrew/bin/node
yarn -> /opt/homebrew/bin/yarn
cast -> ~/.foundry/bin/cast
forge -> ~/.foundry/bin/forge

所有命令都能正常找到,所以问题不在终端 PATH。

2. 定位问题脚本

yarn generate 最终执行的是:

1
packages/foundry/scripts-js/generateKeystore.js

关键代码如下:

1
2
3
4
5
6
7
8
9
const newWalletResult = spawnSync("cast", ["wallet", "new"], {
encoding: "utf-8",
});

const privateKey = newWalletResult.stdout
.split("\n")
.find((line) => line.includes("Private key:"))
?.split(":")[1]
?.trim();

这段代码做了一件很常见但不够稳定的操作:

从命令的人类可读文本输出中提取私钥。

它假设 cast wallet new 会把下面这行写到 stdout:

1
Private key: 0x...

然而在 Foundry 1.8.5 中,输出流发生了变化。

3. 检查 cast wallet new 的真实输出

在当前 Foundry 版本中,执行:

1
cast wallet new

实际输出可以分成两个部分。

stdout:

1
0x<address>	0x<private-key>

stderr:

1
2
3
Successfully created new keypair.
Address: 0x<address>
Private key: 0x<private-key>

使用 Node.js 的 spawnSync() 时,stdout 和 stderr 会被分别保存:

  • newWalletResult.stdout 只有地址和私钥的制表符分隔行
  • newWalletResult.stderr 才包含 Private key: 标签

但项目脚本只搜索:

1
newWalletResult.stdout

所以它永远找不到 Private key:,最终进入:

1
2
3
4
if (!privateKey) {
console.error("\n❌ Could not extract private key from output");
process.exit(1);
}

4. 根因总结

这不是钱包生成失败,而是:

1
2
3
4
5
6
7
8
9
钱包已经成功生成
↓
Foundry 把标签输出到 stderr
↓
项目脚本只读取 stdout
↓
无法提取私钥
↓
脚本主动退出

同样的问题还存在于:

1
packages/foundry/scripts-js/selectOrCreateKeystore.js

也就是说,即使绕过 yarn generate,之后在部署流程中选择“创建新 keystore”,也可能再次遇到同样的错误。

5. 快速解决方案:直接创建 keystore

如果目标只是继续完成挑战,可以不修改项目,直接使用 Foundry 的 keystore 命令:

1
2
3
cd "/path/to/challenge-tokenization"

cast wallet new sepolia-deployer

终端会要求输入并确认 keystore 密码。密码不会显示,这是正常行为。

生成的文件位于:

1
~/.foundry/keystores/sepolia-deployer

查看账户:

1
yarn account

在列表中选择 sepolia-deployer,然后复制地址去 Sepolia faucet 领取测试 ETH。

这种方式完全绕过了有问题的私钥文本解析代码。

6. 永久修复方案:读取 JSON 输出

更稳妥的修复方式是使用:

1
cast wallet new --json

Foundry 返回的结构类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"schema_version": 1,
"success": true,
"data": [
{
"address": "0x...",
"public_key": "0x...",
"private_key": "0x..."
}
],
"errors": [],
"warnings": []
}

把原来的调用:

1
2
3
const newWalletResult = spawnSync("cast", ["wallet", "new"], {
encoding: "utf-8",
});

改为:

1
2
3
const newWalletResult = spawnSync("cast", ["wallet", "new", "--json"], {
encoding: "utf-8",
});

把原来的文本搜索:

1
2
3
4
5
const privateKey = newWalletResult.stdout
.split("\n")
.find((line) => line.includes("Private key:"))
?.split(":")[1]
?.trim();

替换为:

1
2
3
4
5
6
7
8
9
let privateKey;

try {
const payload = JSON.parse(newWalletResult.stdout);
privateKey = payload?.data?.[0]?.private_key;
} catch (error) {
console.error("\n❌ Could not parse wallet output:", error);
process.exit(1);
}

永久修复时需要同时修改:

1
2
packages/foundry/scripts-js/generateKeystore.js
packages/foundry/scripts-js/selectOrCreateKeystore.js

因为两个入口都使用了相同的旧解析逻辑。

7. 问题一验证结果

使用临时 HOME 验证了两个方案:

1
2
3
4
5
cast wallet new sepolia-deployer
-> 成功创建加密 keystore

cast wallet new --json
-> 成功解析 private_key 并导入 keystore

这说明问题确实出在输出解析,而不是钱包生成本身。


问题二(Checkpoint 2:部署到测试网):Sepolia 部署交易被打包,但执行失败

当前 Challenge 步骤:Checkpoint 2 - Deploy to a Testnet

账户已经准备好并领取了 Sepolia 测试 ETH,现在正在真正执行 forge script --broadcast 部署合约。

1. 部署命令

账户创建完成后,我执行了:

1
yarn deploy --network sepolia --account sepolia-deployer

脚本显示 keystore 列表,手动选择:

1
1. sepolia-deployer

随后合约编译和模拟都成功:

1
2
3
4
5
Compiler run successful!
Script ran successfully.

YourCollectible deployed at:
<FAILED_DEPLOY_CONTRACT_ADDRESS>

但真正广播后,交易失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
❌ [Failed]

Hash:
<FAILED_DEPLOY_TX_HASH>

Contract:
YourCollectible

Contract Address:
<FAILED_DEPLOY_CONTRACT_ADDRESS>

Paid:
0.001894991 ETH

2. 先确认 --account 是否是问题

检查发现,项目的 yarn deploy 包装脚本只识别:

1
2
3
--network
--file
--keystore

它不识别 --account。

所以在命令:

1
yarn deploy --network sepolia --account sepolia-deployer

中,--account sepolia-deployer 被忽略了。

不过因为之后脚本弹出了 keystore 列表,并手动选择了:

1
1. sepolia-deployer

所以这次实际使用的账户是正确的。

结论:

1
2
--account 参数确实不该用在这里
但它不是本次交易失败的原因

以后使用项目的 Yarn 包装命令时,应写:

1
yarn deploy --network sepolia --keystore sepolia-deployer

3. 查询链上交易回执

执行:

1
2
3
cast receipt \
<FAILED_DEPLOY_TX_HASH> \
--rpc-url sepolia

关键字段:

1
2
3
4
status: 0 (failed)
gasUsed: 1894991
effectiveGasPrice: 1000000000
logs: []

而广播交易中的 gas limit 也是:

1
gasLimit: 1894991

也就是:

1
2
gasUsed == gasLimit
status == 0

这是非常典型的交易耗尽 gas 后被回滚的表现。

4. 为什么不是合约业务逻辑 revert

如果构造函数正常执行,但其中某个 require 或条件失败,通常会看到明确的 revert 原因,而不是刚好消耗完所有 gas。

这次回执里:

  • status 失败
  • logs 为空
  • gasUsed 正好等于发送时的 gasLimit

因此更像是 EVM 在执行过程中用完了 gas,而不是某个 Solidity 条件主动 revert。

5. Foundry 的模拟结果

Foundry 模拟时显示:

1
2
new YourCollectible gas: 约 1,296,570
Estimated total gas used for script: 1,894,991

Foundry 因此发出:

1
gasLimit: 1,894,991

从本地模拟结果看,这个 gas 似乎足够。

但链上执行给出了不同的结果。

6. 使用链上 gas 估算验证

对同一部署字节码使用 Sepolia 进行只读调用,得到以下结果:

1
2
3
4
5
1,894,991 gas  -> out of gas
5,000,000 gas -> out of gas
8,000,000 gas -> out of gas
9,500,000 gas -> out of gas
10,113,853 gas -> 成功

这说明当前 Sepolia 实际执行这个创建交易时,需要大约:

1
10.11M gas

而 Foundry 只发送了:

1
1.895M gas

两者差距约为五倍以上。

7. 问题二根因总结

本次失败的核心原因是:

1
2
3
4
5
6
7
8
9
Foundry 1.8.5 在当前 Sepolia 环境下对部署 gas 的估算偏低
↓
广播交易时 gas limit 只有约 1.895M
↓
Sepolia 实际执行需要约 10.11M
↓
交易耗尽 gas
↓
status = 0,交易回滚

账户余额没有问题。链上查询时余额约为:

1
0.098105009 ETH

失败交易消耗了 nonce 0,所以下一次部署会使用 nonce 1,并生成一个新的合约地址。

8. 解决方案:提高 gas 估算倍数

不再使用当前不带 gas 倍数的 yarn deploy 包装命令,而是直接运行 forge script。

进入 Foundry 包目录:

1
cd "/path/to/challenge-tokenization/packages/foundry"

执行:

1
2
3
4
5
6
forge script script/Deploy.s.sol \
--rpc-url sepolia \
--account sepolia-deployer \
--broadcast \
--ffi \
--gas-estimate-multiplier 800

参数说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
forge script script/Deploy.s.sol
运行部署脚本

--rpc-url sepolia
连接 foundry.toml 中配置的 Sepolia RPC

--account sepolia-deployer
使用 ~/.foundry/keystores/sepolia-deployer

--broadcast
真正发送交易,而不是只做模拟

--ffi
允许脚本执行文件操作,例如写入 deployments JSON

--gas-estimate-multiplier 800
把 gas 估算扩大 8 倍

验证过的结果:

1
2
3
4
5
6
7
--gas-estimate-multiplier 700
-> gas limit 10,203,802
-> 已高于当前 Sepolia 成功所需的 10,113,853

--gas-estimate-multiplier 800
-> gas limit 约 11.66M
-> 留出更充足的余量

执行后会提示输入 keystore 密码:

1
Enter keystore password:

输入密码时终端不会显示字符。

9. 部署成功后更新前端 ABI

看到类似下面输出时,表示链上部署成功:

1
ONCHAIN EXECUTION COMPLETE & SUCCESS

然后仍然在:

1
packages/foundry

目录执行:

1
node scripts-js/generateTsAbis.js

或者:

1
make generate-abis

这一步只更新:

1
packages/nextjs/contracts/deployedContracts.ts

它不会再次部署合约,也不需要重新执行 yarn deploy。


下一篇

Checkpoint 2 完成后,继续阅读:

Checkpoint 3 - Mint NFTs

返回:完整排错路线