在solidity中方验证椭圆曲线签名智能合约代码
在Solidity中恢复消息签名者地址:
一般来说,ECDSA签名由r和s两个参数组成。以太坊中的签名包括名为v的第三个参数,可以使用它来验证哪个帐户的私钥用于对消息进行签名,以及交易的发送者。Solidity提供了一个内置函数ecrecover,它接受消息message以及r、s和v参数,并返回用于对消息签名的地址。
提取签名参数r、s和v:
web3.js生成的签名是r, s和v的串联,所以第一步是将这些参数分开。你可以在客户端执行此操作,但在智能合约内部将这些参数分开意味着你需要发送一个签名参数message(已签名)而不是三个参数r、s和v。在solidity语言中将字节数组message拆分为三个参数r、s和v很困难,因此我们使用内联汇编(inline assembly)来完成下述函数中的工作splitSignature(本节末尾完整合约中的第三个函数).
计算消息哈希值:
智能合约需要确切知道签署了哪些参数,因此它必须从参数重新创建消息并将其用于签名验证。函数prefixed和函数recoverSigner在合约claimPayment中实现此操作。
完整代码如下(附详细注释):
pragma solidity >=0.7.0 <0.9.0; contract ReceivePays{ address owner = msg.sender;//存储调用合约者的地址 //mapping类似于散列表和字典,只能声明为状态变量,不支持迭代,支持嵌套 mapping(uint256 => bool) usedNonces;//记录nonce的使用情况,标识nonce的唯一性 //构造函数,可为空,部署合约时调用,仅调用一次 constructor() payable{} function claimPayment(uint256 amount,uint256 nonce,bytes memory signature) external{ //require()中判断条件为true则继续,为false则退出该function,回退该function内所有更改 require(!usedNonces[nonce]);//判断当前传入nonce是否被使用过 usedNonces[nonce] = true; //abi.encodePacked(...) returns (bytes memory) //this(当前合约类型):当前function调用者地址,可显式转换为address或address payable类型 //对message进行椭圆曲线加密 bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender,amount,nonce,this))); //验证签名signature是否和加密消息message所返回的公钥地址相同,即验证签名的正确性 require(recoverSigner(message,signature) == owner); //签名正确性验证通过后转账 payable(msg.sender).transfer(amount); } //selfdestruct(address payable recipient):销毁当前合约,将其资金发送到给定地址 function shutdown() external{ require(msg.sender == owner); selfdestruct(payable(msg.sender)); } //assembly{}为solidity设置的内联汇编语言,用于以一种底层方式访问EVM虚拟机 // := 是内联汇编语言语法,且solidity支持return多个返回值,用括号括起来即可 //mload()是内联汇编语言中的操作码,类似于封装好的函数直接调用即可,mload(p)表示mem[p...(p+32)] //mem[a...b)表示将位置a到位置b的memory字节内容分离出来,add(a,b)表示a+b function splitSignature(bytes memory sig) internal pure returns(uint8 v,bytes32 r,bytes32 s){ require(sig.length == 65); assembly{ r:=mload(add(sig,32)) s:=mload(add(sig,64)) v:=byte(0,mload(add(sig,96))) } return (v,r,s); } //ecrecover(bytes32 hash,uint8 v,bytes32 r,bytes32 s) returns (address) //从椭圆曲线签名中恢复与公钥关联的地址,错误返回零 function recoverSigner(bytes32 message,bytes memory sig) internal pure returns(address){ (uint8 v,bytes32 r,bytes32 s) = splitSignature(sig); return ecrecover(message,v,r,s); } //以太坊有两种信息传递,一种是交易(涉及转账,外部账户与合约账户之间,或,外部账户与外部账户之间的消息传递) //另一种是消息(指合约与合约之间的消息传递),对这两种信息加密调用的函数不同,但同样的输入可能会有同样的输出 //为避免这种碰撞(即两种编码得到的结果相同),为解决这种情况,选择在对消息加密时 //格式为("\x19Ethereum Signed Message:\n" + len(message),message) //如下所示,此处的len(hash)=32,而对交易加密时,不作特殊处理 //这解释了上文中claimPayment中的prefixed(keccak256(abi.encodePacked(msg.sender,amount,nonce,this)))没有加前缀的原因 function prefixed(bytes32 hash) internal pure returns(bytes32){ return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32",hash)); //keccak256()椭圆曲线加密,abi.encodePacked()对输入数据编码 } }
来源(solidity官方英文文档0.8.13):https://docs.soliditylang.org/en/v0.8.13/solidity-by-example.html#recovering-the-message-signer-in-solidity
solidity官方中文文档0.8.0:https://learnblockchain.cn/docs/solidity/solidity-by-example.html#id13
其他知识解释:
(1)为什么签名前要加"\x19Ethereum Signed Message:\n":https://www.cnblogs.com/wanghui-garcia/p/9642492.html
(2)内联汇编(inline assembly)简介及其语法:https://blog.csdn.net/shjuzhen/article/details/80941432
(3)keccak256():https://docs.soliditylang.org/en/v0.8.13/cheatsheet.html#global-variables
(4)abi.encodePacked():https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#non-standard-packed-mode
(5)ecrecover():https://docs.soliditylang.org/en/v0.8.13/cheatsheet.html#global-variables