在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語言中將位元組陣列
計算訊息雜湊值:
智慧合約需要確切知道簽署了哪些引數,因此它必須從引數重新建立訊息並將其用於簽名驗證。函式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