🔐 以太坊安全:数字签名中的 v、r、s

本文深入探讨了以太坊中数字签名的关键组成部分:v、r 和 s,它们基于椭圆曲线数字签名算法(ECDSA),对于验证交易的真实性、完整性和授权至关重要。文章详细解释了这三个参数的生成、验证过程,包括在EVM中的应用,同时还讨论了实际应用场景,如meta-transactions、gasless approvals等,并强调了安全性考虑,如重放攻击和签名延展性。

在以太坊的世界中,信任是去中心化的,安全至关重要,数字签名是默默无闻的英雄,它们确保交易和消息是真实的、未被篡改的和经过授权的。在这些签名的核心,存在三个神秘的组成部分:vrs。这些参数植根于 椭圆曲线数字签名算法 (ECDSA),对于验证用户的身份和保护智能合约至关重要。无论你是区块链开发者、DeFi 爱好者还是好奇的学习者,理解 v、r 和 s 都是掌握以太坊如何维护其完整性的关键。在本综合指南中,我们将揭示这些组成部分的含义、它们如何工作以及它们在以太坊生态系统中的关键作用,包括图表、用户流程、代码片段和实际应用。

什么是 v、r 和 s?

当以太坊用户签署交易或消息时,他们的钱包会使用其私钥生成数字签名。此签名分为三个部分:

为什么它们很重要?

这些组件充当密码学粘合剂,将以太坊交易与其合法所有者绑定在一起。以下是它们至关重要的原因:

  • 身份验证:证明交易是由私钥持有者签署的。
  • 🔐 完整性:确保交易或消息没有被更改。
  • 🧾 不可否认性:防止签名者否认他们的行为。
  • 📩 公钥恢复:允许在不传输公钥的情况下推导出公钥,从而节省带宽。
  • 🌐 重放保护:降低签名在交易或网络中被重用的风险。

🔬v、r 和 s 如何工作:ECDSA 魔法

以太坊使用 secp256k1 椭圆曲线进行其密码学运算,这是一种平衡了安全性和效率的标准。让我们分解一下 v、r 和 s 是如何生成和验证的。

✍️签名生成

当用户签署消息或交易时,该过程遵循以下步骤:

  1. Hash 消息
keccak256(abi.encodePacked(...))

交易数据(例如,接收者、值或函数调用)使用 Keccak-256(以太坊的哈希算法)进行哈希处理,以生成固定大小的摘要。

2. 生成安全的随机数 k

  • 选择一个密码学上安全的随机数 k。
  • 这个数字对于签名的唯一性至关重要。

3. 计算曲线点,计算 r

  • (x, y) = k * G
  • r = x
  • 计算一个点 (x, y) = k * G,
  • 其中 G 是 secp256k1 曲线的生成器点。x 坐标变为 r。

4. 计算 v

  • y 的奇偶性(偶数/奇数)
  • 传统: 27 或 28
  • post-EIP-155, 它包含链 ID: v = chainId * 2 + 3536

5. 计算 s

  • s = k⁻¹ * (hash + r * privateKey) mod n
  • 其中 n 是曲线的阶数,确保签名与私钥相关联。

🧠 EVM 中的签名验证

在链上,以太坊虚拟机 (EVM) 使用 ecrecover 函数来验证签名:

address signer = ecrecover(messageHash, v, r, s);

这个函数:

  1. 使用 v 来解析两个可能的公钥中的哪一个(由于曲线的对称性)对应于签名。
  2. 使用 r、s 和消息哈希恢复公钥。
  3. 从公钥派生以太坊地址,并检查它是否与预期的签名者匹配。

如果恢复的地址匹配,则签名有效,并且合约继续执行所请求的操作。

可视化该过程:示意图

以下流程图说明了 ECDSA 签名生成和验证过程:

用户流程:实际签署和验证

逐步用户流程

  1. 准备交易:用户创建一笔交易或消息(例如,批准代币转账)。
  2. 链下签名:钱包对数据进行哈希处理,并使用私钥对其进行签名,从而生成 v、r、s。
  3. 提交到合约:签名的消息以及 v、r、s 一起被发送到智能合约(直接或通过中继器)。
  4. 链上验证:合约使用 ecrecover 来恢复签名者的地址并对其进行验证。
  5. 执行或回滚:如果有效,则合约执行该操作;否则,它会回滚。

代码片段:将 v、r 和 s 带入生活

使用 Ethers.js 进行链下签名

此 JavaScript 代码使用 Ethers.js 在链下签署消息,从而生成 v、r、s。

const { ethers } = require("ethers");

async function signMessage(message, privateKey, providerUrl) {
    const provider = new ethers.providers.JsonRpcProvider(providerUrl);
    const wallet = new ethers.Wallet(privateKey, provider);
    const messageHash = ethers.utils.hashMessage(message);
    const signature = await wallet.signMessage(message);
    const { v, r, s } = ethers.utils.splitSignature(signature);
    return { v, r, s, messageHash };
}

const message = "Authorize 100 tokens";
const privateKey = "0xYOUR_PRIVATE_KEY";
const providerUrl = "https://mainnet.infura.io/v3/YOUR_PROJECT_ID";

signMessage(message, privateKey, providerUrl)
    .then(result => console.log("Signature:", result))
    .catch(console.error);

使用 Solidity 进行链上验证

这个 Solidity 合约使用 v、r、s 验证签名。

const { ethers } = require("ethers");

async function signMessage(message, privateKey, providerUrl) {
    const provider = new ethers.providers.JsonRpcProvider(providerUrl);
    const wallet = new ethers.Wallet(privateKey, provider);
    const messageHash = ethers.utils.hashMessage(message);
    const signature = await wallet.signMessage(message);
    const { v, r, s } = ethers.utils.splitSignature(signature);
    return { v, r, s, messageHash };
}

const message = "Authorize 100 tokens";
const privateKey = "0xYOUR_PRIVATE_KEY";
const providerUrl = "https://mainnet.infura.io/v3/YOUR_PROJECT_ID";

signMessage(message, privateKey, providerUrl)
    .then(result => console.log("Signature:", result))
    .catch(console.error);

EIP-712 许可函数 (ERC-2612)

permit 函数实现了免 Gas 代币批准,利用 EIP-712 进行结构化数据签名。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract ERC20WithPermit is ERC20 {
    bytes32 public immutable DOMAIN_SEPARATOR;
    bytes32 public constant PERMIT_TYPEHASH = keccak256(
        "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
    );
    mapping(address => uint256) public nonces;

    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256(bytes(name)),
                keccak256(bytes("1")),
                block.chainid,
                address(this)
            )
        );
    }

    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        require(block.timestamp <= deadline, "Permit: expired");
        bytes32 structHash = keccak256(
            abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)
        );
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
        address signer = ECDSA.recover(digest, v, r, s);
        require(signer != address(0) && signer == owner, "Permit: invalid signature");
        _approve(owner, spender, value);
    }
}

使用 Ethers.js 签署 EIP-712 许可

此 JavaScript 代码签署上述合约的许可消息。

const { ethers } = require("ethers");

async function signPermit(ownerPrivateKey, tokenAddress, spender, value, deadline, providerUrl) {
    const provider = new ethers.providers.JsonRpcProvider(providerUrl);
    const wallet = new ethers.Wallet(ownerPrivateKey, provider);
    const domain = {
        name: "MyToken",
        version: "1",
        chainId: await provider.getNetwork().then(net => net.chainId),
        verifyingContract: tokenAddress
    };
    const types = {
        Permit: [\
            { name: "owner", type: "address" },\
            { name: "spender", type: "address" },\
            { name: "value", type: "uint256" },\
            { name: "nonce", type: "uint256" },\
            { name: "deadline", type: "uint256" }\
        ]
    };
    const tokenContract = new ethers.Contract(tokenAddress, ["function nonces(address) view returns (uint256)"], provider);
    const nonce = await tokenContract.nonces(wallet.address);
    const message = { owner: wallet.address, spender, value, nonce, deadline };
    const signature = await wallet._signTypedData(domain, types, message);
    const { v, r, s } = ethers.utils.splitSignature(signature);
    return { v, r, s };
}

🌍现实世界的应用

  1. 元交易
  • 用户链下签署交易,中继器提交交易,支付 Gas 费用。这在 Gas Station Network (GSN) 设置中很常见。
  • 示例:用户签署代币转账,dApp 中继器使用 v、r、s 提交它。

2. 免 Gas 批准 (EIP-2612)

  • permit 函数允许用户批准代币转账,而无需链上 approve 交易,从而节省 Gas。
  • 示例:去中心化交易所 (DEX) 提交用户签署的许可来转移代币。

3. 去中心化治理

  • DAO 使用签名进行链下投票,从而降低 Gas 成本。诸如 Snapshot 之类的平台依赖 v、r、s 进行投票验证。
  • 示例:用户签署一项治理提案投票,该投票在链上进行验证。

4. 多重签名钱包

  • 诸如 Gnosis Safe 之类的合约使用 v、r、s 来验证来自多个所有者的签名。
  • 示例:一个 2/2 多重签名钱包需要两个所有者都签署交易。

🚨 安全注意事项

  1. 重放攻击
  • 风险:如果未受到保护,签名可能会被重用。
  • 缓解:在消息哈希中包含一个 nonce(每次交易递增)和截止时间。EIP-155 将链 ID 添加到 v 以进行跨链保护。
  • 示例漏洞:Optimism 黑客攻击(2022 年 6 月)利用了缺少链 ID 的 EIP-155 之前的签名,允许重放交易,导致 2000 万个 OP 代币被盗。
  • 修复代码
function getTxHash(address _to, uint _amount, uint _nonce) public view returns (bytes32) {
    return keccak256(abi.encodePacked(address(this), _to, _amount, _nonce));
}function getTxHash(address _to, uint _amount, uint _nonce) public view returns (bytes32) {     return keccak256(abi.encodePacked(address(this), _to, _amount, _nonce)); }

2. 签名可延展性

  • 风险:攻击者可以修改 s 以创建有效的替代签名。
  • 缓解:确保 s 位于曲线阶数的下半部分 (s ≤ n/2)。OpenZeppelin 的 ECDSA 库强制执行此操作。

3. 网络钓鱼风险

  • 风险:如果被前端误导,用户可能会签署恶意消息。
  • 缓解:使用 EIP-712 在签名期间呈现人类可读的数据。

4. 抢先交易

  • 风险:签名可能会在预期之前被拦截和使用。
  • 缓解:在消息哈希中包含时间戳或区块号。

🧪 案例研究:Optimism 黑客攻击

在 2022 年 6 月,攻击者利用了 Optimism 链上 Gnosis Safe 钱包合约中的签名重放漏洞。该合约使用 EIP-155 之前的签名,该签名省略了链 ID,从而允许攻击者在 Optimism 上重放来自以太坊主网的交易。通过重复调用合约,攻击者生成了一个持有 2000 万个 OP 代币的地址,然后他们控制了该地址。此事件突出了在 v 中链 ID 和在消息哈希中使用 nonce 的关键需求。

✅ 开发人员的最佳实践

  • 使用 OpenZeppelin 的 ECDSA 库:它处理签名可延展性并提供安全的签名验证。
  • 实施 Nonce:通过包含和跟踪 nonce 来防止重放攻击。
  • 采用 EIP-712:确保签名是人类可读且特定于域的,以避免网络钓鱼。
  • 验证输入:检查 v 是否有效(27/28 或 0/1),并且 r、s 是否为非零。
  • 进行广泛的测试:使用 Hardhat 或 Foundry 等工具来模拟基于签名的攻击。

📘 结论

v、r 和 s 组件是以太坊数字签名系统的支柱,可在智能合约中实现安全、无需信任的交互。从免 Gas 批准到去中心化治理,这些参数支持创新的 dApp 设计,同时保持强大的安全性。通过掌握它们的生成、验证和安全注意事项,开发人员可以构建既用户友好又能抵抗攻击的应用程序。在 Remix 或 Hardhat 中尝试提供的代码片段,并探索以太坊黄皮书和 OpenZeppelin 文档等资源,以加深你的理解。

  • 原文链接: medium.com/@ankitacode11...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ankitacode11
ankitacode11
江湖只有他的大名,没有他的介绍。