5月29日 22:35
What is the principle and implementation of signature verification (ECDSA) in Solidity?
ECDSA (Elliptic Curve Digital Signature Algorithm) is the core cryptographic algorithm used in Ethereum for verifying transaction and message signatures. Implementing signature verification in Solidity is crucial for implementing meta-transactions, gasless transactions, permission verification, and other scenarios.
1. Basic ECDSA Principles
Ethereum uses the secp256k1 elliptic curve for signatures. A signature consists of three parts:
- r: The x-coordinate of the signature
- s: The proof of the signature
- v: The recovery identifier (27 or 28, or 0/1)
solidity// Signature structure struct Signature { bytes32 r; bytes32 s; uint8 v; }
2. Basic Signature Verification
Using OpenZeppelin's ECDSA Library
solidityimport "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract SignatureVerification { using ECDSA for bytes32; // Verify signer address function verifySignature( bytes32 messageHash, bytes memory signature ) public pure returns (address signer) { // Recover signer address signer = messageHash.recover(signature); return signer; } // Verify if signature is valid function isValidSignature( bytes32 messageHash, bytes memory signature, address expectedSigner ) public pure returns (bool) { address recoveredSigner = messageHash.recover(signature); return recoveredSigner == expectedSigner; } }
3. Message Hash Processing
Ethereum Standard Message Format
soliditycontract MessageHashing { // Method 1: Direct keccak256 hash function getMessageHash( address _to, uint256 _amount, uint256 _nonce ) public pure returns (bytes32) { return keccak256(abi.encodePacked(_to, _amount, _nonce)); } // Method 2: Standard Ethereum message format (recommended) function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) { // Add prefix according to Ethereum standard // "\x19Ethereum Signed Message:\n32" + messageHash return keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", _messageHash )); } // Complete signature verification process function verify( address _signer, address _to, uint256 _amount, uint256 _nonce, bytes memory signature ) public pure returns (bool) { // 1. Build message hash bytes32 messageHash = getMessageHash(_to, _amount, _nonce); // 2. Add Ethereum message prefix bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash); // 3. Recover signer address address recoveredSigner = recoverSigner(ethSignedMessageHash, signature); // 4. Verify signer 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))) } // Adjust v value (MetaMask and other wallets usually return 27/28) if (v < 27) { v += 27; } } }
4. Meta-Transaction Implementation
Meta-transactions allow users to execute transactions without paying Gas.
solidityimport "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract MetaTransaction is ReentrancyGuard { using ECDSA for bytes32; // Nonce to prevent replay attacks mapping(address => uint256) public nonces; // Address that can execute meta-transactions (can pay Gas) address public relayer; // Domain separator (EIP-712) bytes32 public DOMAIN_SEPARATOR; // EIP-712 type hash 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; // Build domain separator 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) )); } // Execute meta-transaction 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"); // Build EIP-712 structured data hash bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode( META_TRANSACTION_TYPEHASH, from, to, msg.value, nonces[from], keccak256(functionSignature) )) )); // Recover signer address signer = ecrecover(digest, sigV, sigR, sigS); require(signer == from, "Invalid signature"); require(signer != address(0), "Zero address signer"); // Increment nonce to prevent replay nonces[from]++; // Execute target call (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 Structured Data Signing
EIP-712 provides a more user-friendly signing experience where users can see structured data when signing.
solidityimport "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract EIP712Example is EIP712 { using ECDSA for bytes32; // Define Permit type hash 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") {} // Build domain separator function DOMAIN_SEPARATOR() external view returns (bytes32) { return _domainSeparatorV4(); } // Verify Permit signature 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"); // Execute approval logic _approve(owner, spender, value); } function _approve(address owner, address spender, uint256 value) internal { // Implement approval logic } }
6. Multi-Signature Wallet Implementation
soliditycontract 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; } // Submit transaction and verify signatures function submitTransaction( address _to, uint256 _value, bytes memory _data, bytes[] memory _signatures ) external onlyOwner returns (uint256 txId) { require(_signatures.length >= requiredSignatures, "Insufficient signatures"); // Build transaction hash bytes32 txHash = keccak256(abi.encodePacked( address(this), _to, _value, keccak256(_data), transactions.length )); bytes32 ethSignedHash = keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", txHash )); // Verify each signature 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"); // Check for duplicate signatures 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); // Auto-execute 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. Signature Replay Attack Protection
soliditycontract ReplayProtection { using ECDSA for bytes32; // Record of used signatures mapping(bytes32 => bool) public usedSignatures; // User nonces mapping(address => uint256) public nonces; // Chain ID uint256 public chainId; constructor() { chainId = block.chainid; } // Secure signature verification (includes chain ID and contract address) function verifyWithReplayProtection( address _signer, bytes memory _signature, bytes memory _data ) external returns (bool) { uint256 nonce = nonces[_signer]; // Build message containing replay protection info bytes32 messageHash = keccak256(abi.encodePacked( chainId, address(this), _signer, nonce, _data )); bytes32 ethSignedMessageHash = keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", messageHash )); // Check if signature has been used require(!usedSignatures[ethSignedMessageHash], "Signature already used"); // Recover signer address recoveredSigner = ethSignedMessageHash.recover(_signature); require(recoveredSigner == _signer, "Invalid signature"); // Mark signature as used usedSignatures[ethSignedMessageHash] = true; // Increment nonce nonces[_signer]++; return true; } }
8. Best Practices for Signature Verification
- Always use standard Ethereum message format: Add
\x19Ethereum Signed Message:\n32prefix - Prevent replay attacks: Use nonce and chain ID
- Verify signature length: Ensure signature length is 65 bytes
- Check signer address: Ensure recovered address is not zero address
- Use EIP-712: Provide better user experience and security
- Consider using OpenZeppelin: Audited standard implementation
solidity// Complete best practice signature verification 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); } }