在以太坊及其构建的去中心化应用(Dapp)生态中,签名验证是一项至关重要的安全机制,它确保了只有私钥的持有者才能授权特定的交易或操作,从而保障了用户资产的安全和交易的不可否认性,本文将详细介绍以太坊中验证签名的核心原理、常用方法以及实际应用中的注意事项。
以太坊的签名验证基础是椭圆曲线数字签名算法(ECDSA),这个过程包含三个关键角色:
签名验证的核心思想是:给定一个消息、一个签名和一个公钥,如何验证这个签名确实是由与该公钥对应的私钥针对该消息生成的? 如果验证通过,则意味着签名者拥有该私钥,并且消息在签名后未被篡改。
在以太坊中,无论是交易签名还是个人签名(如EIP-712消息签名),验证过程都遵循相似的基本步骤:

获取原始消息(Message/Transaction Data):

"\x19Ethereum Signed Message:\n" message.length message)或EIP-712规范定义的encodeData结果。获取签名(Signature):
r(32字节)、s(32字节)和v(1字节)。v用于恢复公钥的y坐标奇偶性。从签名中恢复公钥(Recover Public Key):
v值用于确定唯一正确的那个。ecrecover预编译合约来实现这一功能,大多数以太坊开发库(如web3.js, ethers.js)都封装了这一底层操作。从恢复的公钥生成地址:
将上一步恢复出的公钥进行Keccak-256哈希,然后取后20字节作为地址。

比较地址:
from地址)进行比较。在实际开发中,我们通常不会直接调用ecrecover,而是使用成熟的库来简化操作,以下是几种主流的方法:
Web3.js是以太坊较早的JavaScript库,提供了签名验证功能。
const Web3 = require('web3');
const web3 = new Web3();
// 假设我们有以下数据
const message = "Hello, Ethereum!";
const signature = "0x..."; // 65字节的签名
const expectedAddress = "0x..."; // 签名者的地址
// 1. 对消息进行以太坊签名消息格式的处理
const messageHash = web3.utils.sha3("\x19Ethereum Signed Message:\n" message.length message);
// 或者对于EIP-712,使用特定编码
// 2. 使用ecrecover恢复地址
const recoveredAddress = web3.eth.accounts.recover(messageHash, signature);
// 3. 比较地址
console.log("Recovered Address:", recoveredAddress);
console.log("Expected Address:", expectedAddress);
if (recoveredAddress.toLowerCase() === expectedAddress.toLowerCase()) {
console.log("Signature is valid!");
} else {
console.log("Signature is invalid!");
}
Ethers.js是当前更推荐使用的现代、轻量级以太坊库,其API设计更友好,安全性也更高。
const { ethers } = require("ethers");
// 假设我们有以下数据
const message = "Hello, Ethereum!";
const signature = "0x..."; // 65字节的签名
const expectedAddress = "0x..."; // 签名者的地址
// 1. 直接使用ethers.utils.verifyMessage方法
// 注意:ethers会自动处理"以太坊签名消息"的前缀
const recoveredAddress = ethers.utils.verifyMessage(message, signature);
// 2. 比较地址
console.log("Recovered Address:", recoveredAddress);
console.log("Expected Address:", expectedAddress);
if (recoveredAddress.toLowerCase() === expectedAddress.toLowerCase()) {
console.log("Signature is valid!");
} else {
console.log("Signature is invalid!");
}
对于EIP-712结构化签名,Ethers.js提供了verifyTypedData方法:
const domain = {
name: 'Ether Mail',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
};
const types = {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' }
]
};
const value = {
name: 'Alice',
wallet: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
};
const typedDataSignature = "0x...";
const recoveredAddress = ethers.utils.verifyTypedData(domain, types, value, typedDataSignature);
在某些场景下,我们需要在智能合约中验证签名,例如实现一个只有特定地址才能调用的函数,或者允许用户通过签名授权操作。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SignatureVerifier {
function verify(
bytes32 messageHash,
bytes memory signature,
address expectedSigner
) public pure returns (bool) {
// 从签名中恢复地址
address recoveredSigner = _recoverSigner(messageHash, signature);
// 比较地址
return recoveredSigner == expectedSigner;
}
function _recoverSigner(bytes32 messageHash, bytes memory signature)
internal
pure
returns (address)
{
// 检查签名长度是否为65字节
require(signature.length == 65, "Invalid signature length");
// 提取r, s, v
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
// 调整v值(以太坊中v = 27或28,ecrecover期望v = 0或1)
if (v < 27) {
v = 27;
}
require(v == 27 || v == 28, "Invalid signature v value");
// 调用ecrecover预编译合约
return ecrecover(messageHash, v, r, s);
}
// 辅助函数:对消息进行以太坊签名消息哈希
function getMessageHash(string memory message) public pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", bytes(message).length, message));
}
}
v参数中加入链ID来防止跨链重放。"\x19Ethereum Signed Message:\n"),对于EIP-712,确保domain和types的定义与签名时一致。免责声明:本文为转载,非本网原创内容,不代表本网观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
如有疑问请发送邮件至:bangqikeconnect@gmail.com