乐闻世界logo
搜索文章和话题

Solidity 中签名验证(ECDSA)的原理和实现方式是什么?

3月6日 21:52

ECDSA(椭圆曲线数字签名算法)是以太坊中用于验证交易和消息签名的核心密码学算法。在 Solidity 中实现签名验证对于实现元交易、免 Gas 交易、权限验证等场景非常重要。

1. ECDSA 基本原理

以太坊使用 secp256k1 椭圆曲线进行签名,签名包含三个部分:

  • r:签名的 x 坐标
  • s:签名的证明
  • v:恢复标识符(27 或 28,或 0/1)
solidity
// 签名结构 struct Signature { bytes32 r; bytes32 s; uint8 v; }

2. 基础签名验证

使用 OpenZeppelin 的 ECDSA 库

solidity
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract SignatureVerification { using ECDSA for bytes32; // 验证签名者地址 function verifySignature( bytes32 messageHash, bytes memory signature ) public pure returns (address signer) { // 恢复签名者地址 signer = messageHash.recover(signature); return signer; } // 验证签名是否有效 function isValidSignature( bytes32 messageHash, bytes memory signature, address expectedSigner ) public pure returns (bool) { address recoveredSigner = messageHash.recover(signature); return recoveredSigner == expectedSigner; } }

3. 消息哈希处理

以太坊标准消息格式

solidity
contract MessageHashing { // 方式 1:使用 keccak256 直接哈希 function getMessageHash( address _to, uint256 _amount, uint256 _nonce ) public pure returns (bytes32) { return keccak256(abi.encodePacked(_to, _amount, _nonce)); } // 方式 2:使用标准以太坊消息格式(推荐) function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) { // 按照以太坊标准添加前缀 // "\x19Ethereum Signed Message:\n32" + messageHash return keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", _messageHash )); } // 完整的签名验证流程 function verify( address _signer, address _to, uint256 _amount, uint256 _nonce, bytes memory signature ) public pure returns (bool) { // 1. 构建消息哈希 bytes32 messageHash = getMessageHash(_to, _amount, _nonce); // 2. 添加以太坊消息前缀 bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash); // 3. 恢复签名者地址 address recoveredSigner = recoverSigner(ethSignedMessageHash, signature); // 4. 验证签名者 return recoveredSigner == _signer; } function recoverSigner( bytes32 _ethSignedMessageHash, bytes memory _signature ) public pure returns (address) { (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); return ecrecover(_ethSignedMessageHash, v, r, s); } function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v) { require(sig.length == 65, "Invalid signature length"); assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) } // 调整 v 值(MetaMask 等钱包通常返回 27/28) if (v < 27) { v += 27; } } }

4. 元交易(Meta-Transaction)实现

元交易允许用户在不支付 Gas 的情况下执行交易。

solidity
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract MetaTransaction is ReentrancyGuard { using ECDSA for bytes32; // 防止重放攻击的 nonce mapping(address => uint256) public nonces; // 执行元交易的地址(可以支付 Gas) address public relayer; // 域分隔符(EIP-712) bytes32 public DOMAIN_SEPARATOR; // EIP-712 类型哈希 bytes32 public constant META_TRANSACTION_TYPEHASH = keccak256("MetaTransaction(address from,address to,uint256 value,uint256 nonce,uint256 data)"); event MetaTransactionExecuted( address indexed from, address indexed to, bytes functionSignature, uint256 nonce ); constructor() { relayer = msg.sender; // 构建域分隔符 DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes("MetaTransaction")), keccak256(bytes("1")), block.chainid, address(this) )); } // 执行元交易 function executeMetaTransaction( address from, address to, bytes memory functionSignature, bytes32 sigR, bytes32 sigS, uint8 sigV ) external payable nonReentrant returns (bytes memory) { require(msg.sender == relayer, "Only relayer can execute"); // 构建 EIP-712 结构化数据哈希 bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode( META_TRANSACTION_TYPEHASH, from, to, msg.value, nonces[from], keccak256(functionSignature) )) )); // 恢复签名者 address signer = ecrecover(digest, sigV, sigR, sigS); require(signer == from, "Invalid signature"); require(signer != address(0), "Zero address signer"); // 增加 nonce 防止重放 nonces[from]++; // 执行目标调用 (bool success, bytes memory returnData) = to.call{value: msg.value}(functionSignature); require(success, "Meta transaction failed"); emit MetaTransactionExecuted(from, to, functionSignature, nonces[from] - 1); return returnData; } function getNonce(address from) external view returns (uint256) { return nonces[from]; } }

5. EIP-712 结构化数据签名

EIP-712 提供了更友好的签名体验,用户在签名时可以看到结构化数据。

solidity
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract EIP712Example is EIP712 { using ECDSA for bytes32; // 定义 Permit 类型哈希 bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); mapping(address => uint256) public nonces; constructor() EIP712("MyToken", "1") {} // 构建域分隔符 function DOMAIN_SEPARATOR() external view returns (bytes32) { return _domainSeparatorV4(); } // 验证 Permit 签名 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 hash = _hashTypedDataV4(structHash); address signer = hash.recover(v, r, s); require(signer == owner, "Invalid signature"); // 执行授权逻辑 _approve(owner, spender, value); } function _approve(address owner, address spender, uint256 value) internal { // 实现授权逻辑 } }

6. 多签钱包实现

solidity
contract MultiSigWallet { using ECDSA for bytes32; address[] public owners; mapping(address => bool) public isOwner; uint256 public requiredSignatures; struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 signatureCount; } Transaction[] public transactions; mapping(uint256 => mapping(address => bool)) public signatures; event TransactionSubmitted(uint256 indexed txId, address indexed to, uint256 value); event TransactionSigned(uint256 indexed txId, address indexed signer); event TransactionExecuted(uint256 indexed txId); modifier onlyOwner() { require(isOwner[msg.sender], "Not an owner"); _; } constructor(address[] memory _owners, uint256 _required) { require(_owners.length > 0, "Owners required"); require(_required > 0 && _required <= _owners.length, "Invalid required"); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(owner != address(0), "Invalid owner"); require(!isOwner[owner], "Owner not unique"); isOwner[owner] = true; owners.push(owner); } requiredSignatures = _required; } // 提交交易并验证签名 function submitTransaction( address _to, uint256 _value, bytes memory _data, bytes[] memory _signatures ) external onlyOwner returns (uint256 txId) { require(_signatures.length >= requiredSignatures, "Insufficient signatures"); // 构建交易哈希 bytes32 txHash = keccak256(abi.encodePacked( address(this), _to, _value, keccak256(_data), transactions.length )); bytes32 ethSignedHash = keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", txHash )); // 验证每个签名 address[] memory signers = new address[](_signatures.length); for (uint i = 0; i < _signatures.length; i++) { address signer = recoverSigner(ethSignedHash, _signatures[i]); require(isOwner[signer], "Signer not an owner"); // 检查重复签名 for (uint j = 0; j < i; j++) { require(signers[j] != signer, "Duplicate signature"); } signers[i] = signer; } txId = transactions.length; transactions.push(Transaction({ to: _to, value: _value, data: _data, executed: false, signatureCount: _signatures.length })); emit TransactionSubmitted(txId, _to, _value); // 自动执行 executeTransaction(txId); } function executeTransaction(uint256 _txId) internal { Transaction storage transaction = transactions[_txId]; require(!transaction.executed, "Already executed"); require(transaction.signatureCount >= requiredSignatures, "Insufficient signatures"); transaction.executed = true; (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data); require(success, "Transaction failed"); emit TransactionExecuted(_txId); } function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) internal pure returns (address) { return _ethSignedMessageHash.recover(_signature); } }

7. 签名重放攻击防护

solidity
contract ReplayProtection { using ECDSA for bytes32; // 已使用的签名记录 mapping(bytes32 => bool) public usedSignatures; // 用户的 nonce mapping(address => uint256) public nonces; // 链 ID uint256 public chainId; constructor() { chainId = block.chainid; } // 安全的签名验证(包含链 ID 和合约地址) function verifyWithReplayProtection( address _signer, bytes memory _signature, bytes memory _data ) external returns (bool) { uint256 nonce = nonces[_signer]; // 构建包含防重放信息的消息 bytes32 messageHash = keccak256(abi.encodePacked( chainId, address(this), _signer, nonce, _data )); bytes32 ethSignedMessageHash = keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", messageHash )); // 检查签名是否已使用 require(!usedSignatures[ethSignedMessageHash], "Signature already used"); // 恢复签名者 address recoveredSigner = ethSignedMessageHash.recover(_signature); require(recoveredSigner == _signer, "Invalid signature"); // 标记签名已使用 usedSignatures[ethSignedMessageHash] = true; // 增加 nonce nonces[_signer]++; return true; } }

8. 签名验证最佳实践

  1. 始终使用标准以太坊消息格式:添加 \x19Ethereum Signed Message:\n32 前缀
  2. 防止重放攻击:使用 nonce 和链 ID
  3. 验证签名长度:确保签名长度为 65 字节
  4. 检查签名者地址:确保恢复的地址不是零地址
  5. 使用 EIP-712:提供更好的用户体验和安全性
  6. 考虑使用 OpenZeppelin:经过审计的标准实现
solidity
// 完整的签名验证最佳实践 contract BestPracticeSignature { using ECDSA for bytes32; bytes32 public constant EIP712_DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); bytes32 public immutable DOMAIN_SEPARATOR; constructor() { DOMAIN_SEPARATOR = keccak256(abi.encode( EIP712_DOMAIN_TYPEHASH, keccak256(bytes("BestPractice")), keccak256(bytes("1")), block.chainid, address(this) )); } function verifyEIP712Signature( bytes32 structHash, bytes memory signature, address expectedSigner ) external view returns (bool) { bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, structHash )); address signer = digest.recover(signature); return signer == expectedSigner && signer != address(0); } }

标签:Solidity