Foundry Cast 恢复浏览器钱包签名:cast wallet sign --browser的 personal-message 与 EIP-712 支持解析
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
cast wallet sign是 Foundry 中用于对消息与 EIP-712 结构化数据离线签名的核心命令。本文围绕仓库变更记录 restore-cast-browser-wallet-signing.md 展开,剖析"恢复浏览器钱包 personal-message 与 EIP-712 签名"这一补丁在cast wallet sign中的落地实现:包括--browser参数的引入、两条签名路径的源码细节、地址解析的配套恢复,以及可验证的 CLI 测试用例,帮助读者在使用浏览器钱包(如 MetaMask 类扩展)完成离线签名时,准确理解其能力边界与正确用法。
变更记录解读:一次"恢复"而非"新增"
该变更记录正文只有一句话:
Restored browser wallet personal-message and EIP-712 signing for
cast wallet sign.
关键词是Restored(恢复)。这说明浏览器钱包签名能力并非首次引入,而是在某次迭代中被破坏或移除后重新补齐。同类修复在.changelog目录中成组出现,可以从侧面还原这次的修复范围:
- restore-cast-browser-wallet-readonly-commands.md:恢复了
cast wallet address、cast call、cast access-list、cast estimate四个只读命令的浏览器钱包地址解析; - restore-cast-browser-wallet-signing.md(本文主题):恢复了
cast wallet sign的浏览器钱包personal-message(personal_sign 风格消息签名)与EIP-712 结构化数据签名; - cast-browser-legacy-type.md:确保浏览器钱包请求中保留显式交易类型(包括 legacy 交易),保证发送时类型不被丢失。
三份记录共同勾勒出一次完整的"浏览器钱包能力回归":只读命令恢复取地址,cast wallet sign恢复两种签名,交易发送恢复显式类型。本文聚焦中间这一环。
cast wallet sign子命令:本地钱包与浏览器钱包的统一入口
命令定义与参数
在 crates/cast/src/cmd/wallet/mod.rs 中,WalletSubcommands::Sign是cast wallet sign的 CLI 定义(visible_alias = "s"),其核心参数如下:
| 参数 | 类型/默认值 | 说明 |
|---|---|---|
message | 必填字符串 | 要签名的消息、typed data 或哈希。以0x开头视为十六进制编码,签名前先解码为字节;否则按原始字节处理 |
--data | bool | 将 message 视为 JSON 形式的 EIP-712 typed data |
--from-file | bool,requires = "data" | 将 message 视为包含 JSON typed data 的文件路径,必须与--data搭配 |
--no-hash | bool,与--data互斥 | 将 message 视为原始 32 字节哈希,直接签名不再二次哈希 |
wallet | WalletOpts(flatten) | 本地钱包选项:--private-key、--mnemonic、--account、--ledger、--aws等 |
browser | BrowserWalletOpts(flatten) | 浏览器钱包选项:--browser等 |
命令的典型用法(来自命令自身的文档注释):
cast wallet sign "hello" --account dev cast wallet sign "hello" --private-key $PK cast wallet sign --data --from-file typed_data.json --ledger执行流程:先浏览器后本地
从 mod.rs 的Sign分支可以看到,签名路径按"是否指定浏览器钱包"二选一:
- 先解析 typed data:
let typed_data = data.then(|| parse_typed_data(&message, from_file)).transpose()?; - 若
browser.run::<alloy_network::Ethereum>().await?返回了浏览器钱包 signer,则走浏览器路径;否则走wallet.signer().await?的本地路径; - 无论哪条路径,签名完成后统一输出十六进制签名
0x{signature}。
CLI 测试 wallet_keystore.rs 中的browser_wallet_commands_expose_browser_option用例专门断言了cast wallet sign --help输出中包含--browser选项,证明该参数对用户可见且已接入帮助文档。
浏览器钱包的两条签名路径
路径一:personal-message 签名(sign_message)
当 message 是普通消息时,源码调用:
browser.sign_message(&hex_str_to_bytes(&message)?).await?这里的关键辅助函数hex_str_to_bytes(mod.rs)负责统一消息编码:
- 以
0x开头的字符串:hex::decode解码为原始字节; - 不以
0x开头:直接s.as_bytes()按 UTF-8 原始字节处理。
解码后的字节会按 Ethereum Signed Message 规范加前缀并哈希后再签名(personal_sign 语义),这与本地钱包路径wallet.sign_message(...)完全一致,保证同一消息由本地密钥与浏览器钱包签名得到的结果一致、可互相验证。
路径二:EIP-712 结构化数据签名(sign_dynamic_typed_data)
当指定--data(或--data --from-file <file>)时,源码先通过parse_typed_data解析:
fn parse_typed_data(message: &str, from_file: bool) -> Result<TypedData> { if from_file { Ok(fs::read_json_file(Path::new(message))?) } else { Ok(serde_json::from_str(message)?) } }--data '<json>':将 JSON 字符串直接反序列化为TypedData;--data --from-file typed_data.json:从文件读取 JSON。
随后浏览器路径调用browser.sign_dynamic_typed_data(typed_data).await?,按 EIP-712 规范对结构化数据计算哈希并请求浏览器钱包签名。典型用法:
# 直接传 JSON 字符串 cast wallet sign --data '{"types":{...},"primaryType":"Mail",...}' --browser # 从文件读取 cast wallet sign --data --from-file typed_data.json --browser浏览器钱包的硬性限制:不支持 raw hash
源码在进入分支前有一个前置校验(mod.rs):
if browser.browser && no_hash { eyre::bail!("Raw hash signing is not supported with a browser wallet"); }即--no-hash(直接对 32 字节哈希签名)与--browser互斥。原因是浏览器钱包只能以"加前缀后哈希"或"EIP-712 哈希"的方式签名,无法接受一个已算好的裸哈希。如果同时传入--browser --no-hash,命令会直接报错退出,而不是静默产生语义错误的签名。这也是本补丁恢复的两条路径与本地钱包路径在能力上的唯一分界。
配套恢复:浏览器钱包的地址解析
签名结果本身需要配合签名者地址使用(例如cast wallet verify或链上校验),因此浏览器钱包的地址解析能力是这次回归中不可分割的一部分。
在 mod.rs 的Address分支中,地址解析顺序为:
- 显式传入
PRIVATE_KEY:直接用私钥派生地址; - 否则若指定
--browser:browser.run::<alloy_network::Ethereum>().await?返回浏览器钱包 signer,取其address(); - 否则走本地
wallet.signer().await?.address()。
与签名恢复同步,restore-cast-browser-wallet-readonly-commands.md 记录了cast wallet address、cast call、cast access-list、cast estimate四个只读命令同样恢复了浏览器钱包地址解析。这意味着在一个完整工作流中:
# 1. 用浏览器钱包解析地址 ADDR=$(cast wallet address --browser) # 2. 用浏览器钱包签名 EIP-712 数据 cast wallet sign --data --from-file order.json --browser # 3. 离线验证签名归属 cast wallet verify --address "$ADDR" --data --from-file order.json <signature>每一步都由--browser串联,本地私钥全程不落盘。
输出形态:默认、verbose 与 JSON
签名结果的输出在 mod.rs 中按 verbosity 分三种形态:
| 场景 | 输出 |
|---|---|
默认(verbosity == 0) | 仅输出0x<signature>,便于脚本捕获 |
verbose(-v,非 JSON) | 依次输出Successfully signed!、Message:、Address:、0x<signature> |
verbose + JSON(-v --json) | 输出信封结构{"message":..., "address":..., "signature":...} |
测试 wallet_signing.rs 中的wallet_sign_json与wallet_sign_json_verbose分别验证了默认 JSON 模式只输出裸签名、verbose JSON 模式输出含message/address/signature三个字段的信封,为脚本化集成提供了确定性保证。
测试证据:签名一致性与验证闭环
crates/cast/tests/cli/wallet_signing.rs 是cast wallet sign的 CLI 测试集,可以验证本次恢复所覆盖能力的正确性基线:
wallet_sign_message_utf8_data:以私钥0x...01对"test"签名,断言输出固定签名,并用cast wallet verify验证成功;对"other msg"验证失败。这证明 personal-message 路径的哈希规则是确定性的、可复现的;wallet_sign_message_hex_data:对0x前缀消息先解码再签名;wallet_sign_and_verify_message_hex_data:以标准测试助记词test test test ... junk走完 private-key → address → sign → verify 全链路。
浏览器钱包本身依赖浏览器交互(CLI 在等待连接时会输出 "Waiting for browser wallet connection" 提示,见 keychain.rs 的测试断言),因此自动化测试以本地密钥路径验证签名语义;浏览器路径与本地路径共用同一套sign_message/sign_dynamic_typed_data语义与输出格式,保证了两者在签名结果层面的一致性。
使用注意事项小结
--browser与--no-hash互斥:浏览器钱包无法签裸哈希,组合使用会直接报错;--from-file依赖--data:clap层面通过requires = "data"强制约束;- 消息编码规则:
0x前缀按十六进制解码,否则按 UTF-8 原始字节,两条签名路径(本地/浏览器)规则完全一致; - EIP-712 数据格式:必须是合法的 JSON typed data(含
types、primaryType等字段),可内联传入或从文件读取; - 地址配套使用:签名地址通过
cast wallet address --browser获取,或使用 verbose 输出中的address字段,配合cast wallet verify完成离线验证闭环。
本次补丁虽小,却补齐了cast wallet sign在浏览器钱包场景下的完整签名能力——personal-message 与 EIP-712 两条路径都恢复了与本地钱包一致的语义,配合只读命令的地址解析回归,使得"浏览器钱包全程参与、私钥永不接触终端"的工作流在 Cast 中重新变得可行。
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考