本文深入探讨了以太坊中数字签名的关键组成部分:v、r 和 s,它们基于椭圆曲线数字签名算法(ECDSA),对于验证交易的真实性、完整性和授权至关重要。文章详细解释了这三个参数的生成、验证过程,包括在EVM中的应用,同时还讨论了实际应用场景,如meta-transactions、gasless approvals等,并强调了安全性考虑,如重放攻击和签名延展性。
在以太坊的世界中,信任是去中心化的,安全至关重要,数字签名是默默无闻的英雄,它们确保交易和消息是真实的、未被篡改的和经过授权的。在这些签名的核心,存在三个神秘的组成部分:v、r 和 s。这些参数植根于 椭圆曲线数字签名算法 (ECDSA),对于验证用户的身份和保护智能合约至关重要。无论你是区块链开发者、DeFi 爱好者还是好奇的学习者,理解 v、r 和 s 都是掌握以太坊如何维护其完整性的关键。在本综合指南中,我们将揭示这些组成部分的含义、它们如何工作以及它们在以太坊生态系统中的关键作用,包括图表、用户流程、代码片段和实际应用。
当以太坊用户签署交易或消息时,他们的钱包会使用其私钥生成数字签名。此签名分为三个部分:
这些组件充当密码学粘合剂,将以太坊交易与其合法所有者绑定在一起。以下是它们至关重要的原因:
以太坊使用 secp256k1 椭圆曲线进行其密码学运算,这是一种平衡了安全性和效率的标准。让我们分解一下 v、r 和 s 是如何生成和验证的。
当用户签署消息或交易时,该过程遵循以下步骤:
keccak256(abi.encodePacked(...))
交易数据(例如,接收者、值或函数调用)使用 Keccak-256(以太坊的哈希算法)进行哈希处理,以生成固定大小的摘要。
2. 生成安全的随机数 k
3. 计算曲线点,计算 r
k * G
r = x
4. 计算 v
v = chainId * 2 + 35
或 36
5. 计算 s
s = k⁻¹ * (hash + r * privateKey) mod n
在链上,以太坊虚拟机 (EVM) 使用 ecrecover 函数来验证签名:
address signer = ecrecover(messageHash, v, r, s);
这个函数:
如果恢复的地址匹配,则签名有效,并且合约继续执行所请求的操作。
以下流程图说明了 ECDSA 签名生成和验证过程:
ecrecover
来恢复签名者的地址并对其进行验证。此 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 合约使用 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);
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);
}
}
此 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 };
}
2. 免 Gas 批准 (EIP-2612):
permit
函数允许用户批准代币转账,而无需链上 approve
交易,从而节省 Gas。3. 去中心化治理:
4. 多重签名钱包:
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. 签名可延展性:
3. 网络钓鱼风险:
4. 抢先交易:
在 2022 年 6 月,攻击者利用了 Optimism 链上 Gnosis Safe 钱包合约中的签名重放漏洞。该合约使用 EIP-155 之前的签名,该签名省略了链 ID,从而允许攻击者在 Optimism 上重放来自以太坊主网的交易。通过重复调用合约,攻击者生成了一个持有 2000 万个 OP 代币的地址,然后他们控制了该地址。此事件突出了在 v 中链 ID 和在消息哈希中使用 nonce 的关键需求。
v、r 和 s 组件是以太坊数字签名系统的支柱,可在智能合约中实现安全、无需信任的交互。从免 Gas 批准到去中心化治理,这些参数支持创新的 dApp 设计,同时保持强大的安全性。通过掌握它们的生成、验证和安全注意事项,开发人员可以构建既用户友好又能抵抗攻击的应用程序。在 Remix 或 Hardhat 中尝试提供的代码片段,并探索以太坊黄皮书和 OpenZeppelin 文档等资源,以加深你的理解。
- 原文链接: medium.com/@ankitacode11...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!