{"author":{"address":null,"user":"https://learnblockchain.cn/people/18158"},"content":{"body":"# 前言\r\n\r\n\u003e 本文主要实现EIP721类型化数据签名的智能合约的开发、测试、部署、交互，测试过程：涉及到前端通过ethers库和合约以及钱包的交互；\r\n\r\n# EIP712 类型化数据签名\r\n\r\n**定义**：一种以太坊改进提案，旨在提供一种更高级、更安全的类型化数据签名方法；\r\n\r\n#### 背景与重要性\r\n\r\n*   **链下签名，链上验证**：EIP712将签名过程从链上转移至链下，节省Gas费。这种链下消息签名、链上验证的形式，可以省去多余的approve操作。\r\n*   **提高安全性与可用性**：当支持EIP712的Dapp请求签名时，钱包会展示签名消息的原始数据，用户可以在验证数据符合预期之后签名。这使得用户能够更直观地了解他们正在签名的数据内容，而不是面对一串无法观察的编码。\r\n\r\n#### 工作流程\r\n\r\n1.  定义签名结构体数据类型\r\n2.  定义结构体的hash\r\n3.  定义编码数据\r\n4.  定义Domain Separator\r\n5.  生成摘要hash\r\n6.  验证签名者\r\n\r\n#### 应用场景\r\n\r\n*   **数字资产交易**：验证交易的合法性和完整性。\r\n*   **DApps身份验证**：确保用户身份的合法性。\r\n*   **智能合约的部署和调用**：验证调用者的身份和权限。\r\n\r\n# 合约开发\r\n\r\n    // SPDX-License-Identifier: MIT\r\n    // By 0xAA \r\n    pragma solidity ^0.8.0;\r\n\r\n    import {ECDSA} from \"@openzeppelin/contracts/utils/cryptography/ECDSA.sol\";\r\n\r\n    contract EIP712Storage {\r\n        using ECDSA for bytes32;\r\n\r\n        bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\r\n        bytes32 private constant STORAGE_TYPEHASH = keccak256(\"Storage(address spender,uint256 number)\");\r\n        bytes32 private DOMAIN_SEPARATOR;\r\n        uint256 number;\r\n        address owner;\r\n\r\n        constructor(){\r\n            DOMAIN_SEPARATOR = keccak256(abi.encode(\r\n                EIP712DOMAIN_TYPEHASH, // type hash\r\n                keccak256(bytes(\"EIP712Storage\")), // name\r\n                keccak256(bytes(\"1\")), // version\r\n                block.chainid, // chain id\r\n                address(this) // contract address\r\n            ));\r\n            owner = msg.sender;\r\n        }\r\n\r\n        /**\r\n         * @dev Store value in variable\r\n         */\r\n        function permitStore(uint256 _num, bytes memory _signature) public {\r\n            // 检查签名长度，65是标准r,s,v签名的长度\r\n            require(_signature.length == 65, \"invalid signature length\");\r\n            bytes32 r;\r\n            bytes32 s;\r\n            uint8 v;\r\n            // 目前只能用assembly (内联汇编)来从签名中获得r,s,v的值\r\n            assembly {\r\n                /*\r\n                前32 bytes存储签名的长度 (动态数组存储规则)\r\n                add(sig, 32) = sig的指针 + 32\r\n                等效为略过signature的前32 bytes\r\n                mload(p) 载入从内存地址p起始的接下来32 bytes数据\r\n                */\r\n                // 读取长度数据后的32 bytes\r\n                r := mload(add(_signature, 0x20))\r\n                // 读取之后的32 bytes\r\n                s := mload(add(_signature, 0x40))\r\n                // 读取最后一个byte\r\n                v := byte(0, mload(add(_signature, 0x60)))\r\n            }\r\n\r\n            // 获取签名消息hash\r\n            bytes32 digest = keccak256(abi.encodePacked(\r\n                \"\\x19\\x01\",\r\n                DOMAIN_SEPARATOR,\r\n                keccak256(abi.encode(STORAGE_TYPEHASH, msg.sender, _num))\r\n            )); \r\n            \r\n            address signer = digest.recover(v, r, s); // 恢复签名者\r\n            require(signer == owner, \"EIP712Storage: Invalid signature\"); // 检查签名\r\n\r\n            // 修改状态变量\r\n            number = _num;\r\n        }\r\n\r\n        /**\r\n         * @dev Return value \r\n         * @return value of 'number'\r\n         */\r\n        function retrieve() public view returns (uint256){\r\n            return number;\r\n        }    \r\n    }\r\n    # 编译指令\r\n    # npx hardhat compile\r\n\r\n# 合约部署\r\n\r\n**说明**：部署成功后会生成一个部署合约的地址（例：EIP712Storage合约地址： 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853）也会生成一个json文件，测试时会使用到\r\n\r\n    module.exports = async function ({getNamedAccounts,deployments}) {\r\n        const firstAccount = (await getNamedAccounts()).firstAccount;\r\n        const {deploy,log} = deployments;\r\n        const EIP712Storage=await deploy(\"EIP712Storage\",{\r\n            contract: \"EIP712Storage\",\r\n            from: firstAccount,\r\n            args: [],\r\n            log: true,\r\n            // waitConfirmations: 1,\r\n        })\r\n        console.log(\"EIP712Storage合约地址\",EIP712Storage.address)\r\n      }\r\n      module.exports.tags = [\"all\", \"EIP712Storage\"]\r\n    # 部署指令\r\n    # npx hardhat deploy\r\n\r\n# 合约本地测试（hardhat）\r\n\r\n## 步骤\r\n\r\n**合约部分**\r\n\r\n*   hardhat启动测试节点：`npx hardhat node` 会获取20个包含10000 ETH的测试账户\r\n*   编译合约 `npx hardhat deploy` 会返回一个合约地址和会在artifacts/contracts/下生成一个json文件\r\n\r\n**前端部分**\r\n\r\n*   启动前端项目 `npm start`\r\n*   把生成的json文件导入到项目中\r\n*   编写测试界面\r\n*   写功能\r\n\r\n#### 前端界面及测试代码\r\n\r\n    import React,{useState,useEffect}from \"react\";\r\n    import { ethers ,JsonRpcProvider,BrowserProvider} from \"ethers\";//\r\n    import EIP712Storage from \"../json/EIP712Storage.json\";//合约abi\r\n    let provider,signer,account,balance,chainid,signature,owner;\r\n    let contranAddress=\"0xa513E6E4b8f2a923D98304ec87F64353C4D5C853\";//合约地址\r\n    const EIP712StorageApp= ()=\u003e{\r\n        let [account,setAccount]=useState(\"0\");//钱包地址\r\n        let [balance,setBalance]=useState(0);//钱包余额\r\n        let [chainid,setChainid]=useState(\"0\");//链id\r\n        let [signature,setSignature]=useState();//签名信息\r\n        // 链接钱包\r\n        const connectWallet=async ()=\u003e{\r\n            if(window.ethereum){\r\n                provider = new ethers.BrowserProvider(window.ethereum);\r\n               \r\n                signer = await provider.getSigner();//签名者\r\n                account = await signer.getAddress();\r\n                setAccount(account)\r\n                balance = await provider.getBalance(account);//余额\r\n                setBalance(balance.toString())\r\n                chainid = await provider.getNetwork();//链id\r\n                setChainid(chainid.chainId.toString())\r\n               owner = await signer.getAddress();//所有者\r\n                console.log(signer,account,balance,chainid)\r\n            }else{\r\n                alert(\"请安装钱包\")\r\n            }\r\n        }\r\n        //获取签名\r\n        const  signPermit=async ()=\u003e{\r\n            const contract = new ethers.Contract(contranAddress, EIP712Storage.abi, signer);\r\n\r\n            console.log(\"合约地址\",await contract.getAddress())//合约\r\n            const domain = {\r\n                name: \"EIP712Domain\",\r\n                version: \"1\",\r\n                chainId: chainid,\r\n                verifyingContract: contranAddress,\r\n            };\r\n            const types = {\r\n                Permit: [\r\n                    { name: \"owner\", type: \"address\" },\r\n                    { name: \"spender\", type: \"address\" },\r\n                    { name: \"value\", type: \"uint256\" },\r\n                    { name: \"nonce\", type: \"uint256\" },\r\n                    { name: \"deadline\", type: \"uint256\" },\r\n                ],\r\n            };\r\n            const value = {\r\n                owner: account,\r\n                spender: account,\r\n                value: ethers.parseEther(\"22\"),//账22个\r\n                nonce: 0,\r\n                deadline: Math.floor(Date.now() / 1000) + 60 * 60 * 24,\r\n            };\r\n            const signature = await signer.signTypedData(domain, types, value);//签名信息\r\n            console.log(signature)\r\n            setSignature(signature)\r\n        }\r\n        useEffect(()=\u003e{\r\n            connectWallet();//自动链接钱包\r\n        },[])\r\n        return(\r\n            \u003cdiv\u003e\r\n                \u003ch1 className={`text-center font-black text-4xl`}\u003eEIP712前端测试\u003c/h1\u003e\r\n                \u003cbutton className={`w-28 h-10 p-2 rounded m-4 bg-blue-500 text-center text-white hover:bg-blue-700`} onClick={connectWallet}\u003e链接钱包\u003c/button\u003e\r\n                \u003cbutton  className={`w-28 h-10 p-2 rounded m-4 bg-blue-500 text-center text-white hover:bg-blue-700`} onClick={signPermit}\u003e签名消息\u003c/button\u003e\r\n                {/* \u003ch1 onClick={massage}\u003e钱包信息\u003c/h1\u003e */}\r\n                \u003cul className=\"ml-4\"\u003e\r\n                    \u003cli className={`mb-4`}\u003e\u003cspan className={`font-black`}\u003e地址：\u003c/span\u003e{account}\u003c/li\u003e\r\n                    \u003cli className={`mb-4`}\u003e\u003cspan className={`font-black`}\u003e余额：\u003c/span\u003e{balance} USDT\u003c/li\u003e\r\n                    \u003cli className={`mb-4`}\u003e\u003cspan className={`font-black`}\u003echainid：\u003c/span\u003e{chainid}\u003c/li\u003e\r\n                    \u003cli className={`mb-4`}\u003e\u003cspan className={`font-black`}\u003e签名信息：\u003c/span\u003e{signature}\u003c/li\u003e\r\n                \u003c/ul\u003e\r\n            \u003c/div\u003e\r\n        )\r\n    }\r\n    export default EIP712StorageApp\r\n\r\n#### 测试步骤\r\n\r\n**功能说明**：以上实现了一个链接钱包和获取数字签名hash功能的前端界面，打开界面自动链接钱包\r\n\r\n*   **项目启动**：`npm start`启动项目自动链接MataMask钱包界面\u003cbr\u003e\r\n    如何所示：\r\n*   **~~或者点击链接钱包按钮~~**：如图所示\r\n\r\n![屏幕截图 2025-01-17 150105.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9328582bf7b04d62a64dac0fc88fd127~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9\u0026rk3s=f64ab15b\u0026x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\u0026x-orig-expires=1741585178\u0026x-orig-sign=tNDbYillRxtiMvs7CJomB4dtfgI%3D)\r\n\r\n![屏幕截图 2025-01-17 155345.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/204303845e1d4894a6f6c77166755a23~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9\u0026rk3s=f64ab15b\u0026x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\u0026x-orig-expires=1741585178\u0026x-orig-sign=ZJizF2uMva9bdvhbD1nHEa34pRU%3D)\r\n\r\n*   **点击签名信息按钮**：调起metamask插件如图所示自定义的可以在消息中看到自定义的数据类型\r\n\r\n![屏幕截图 2025-01-17 155406.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/74747d13661f4814af7e11b882910c8e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9\u0026rk3s=f64ab15b\u0026x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\u0026x-orig-expires=1741585178\u0026x-orig-sign=np0uE0bowVD4n%2BTRkNq35dQundQ%3D)\r\n\r\n![屏幕截图 2025-01-17 155422.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7801000be2994588b05962b3da9bc0c7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9\u0026rk3s=f64ab15b\u0026x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\u0026x-orig-expires=1741585178\u0026x-orig-sign=vCF7mgX1buCESFqxcjxebuHAjig%3D)\r\n\r\n*   **点击确定按钮**：界面会返回签名的hash值\r\n\r\n![屏幕截图 2025-01-17 155442.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/1015029d72dd47f690e874bdb51ae76d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9\u0026rk3s=f64ab15b\u0026x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\u0026x-orig-expires=1741585178\u0026x-orig-sign=qusrOl0uT4S%2Bg0QCvkigr8TObuA%3D)\r\n\r\n# 总结\r\n\r\n以上就是EIP712 类型化数据签名合约的开发测试部署全部看流程，`注意`:启动npx hardhat node 节点后，需要把账号的私钥导入到metamask插件中。","title":"快速实现一种更先进、安全的签名方法之EIP712 类型化数据签名"},"history":null,"timestamp":1740980423,"version":1}