快速实现一种更先进、安全的签名方法之EIP712 类型化数据签名

  • 木西
  • 发布于 2025-03-03 13:40
  • 阅读 87

前言本文主要实现EIP712类型化数据签名的智能合约的开发、测试、部署、交互,测试过程:涉及到前端通过ethers库和合约以及钱包的交互;EIP712类型化数据签名定义:一种以太坊改进提案,旨在提供一种更高级、更安全的类型化数据签名方法;背景与重要性链下签名,链上验证:EIP712

前言

本文主要实现EIP721类型化数据签名的智能合约的开发、测试、部署、交互,测试过程:涉及到前端通过ethers库和合约以及钱包的交互;

EIP712 类型化数据签名

定义:一种以太坊改进提案,旨在提供一种更高级、更安全的类型化数据签名方法;

背景与重要性

  • 链下签名,链上验证:EIP712将签名过程从链上转移至链下,节省Gas费。这种链下消息签名、链上验证的形式,可以省去多余的approve操作。
  • 提高安全性与可用性:当支持EIP712的Dapp请求签名时,钱包会展示签名消息的原始数据,用户可以在验证数据符合预期之后签名。这使得用户能够更直观地了解他们正在签名的数据内容,而不是面对一串无法观察的编码。

工作流程

  1. 定义签名结构体数据类型
  2. 定义结构体的hash
  3. 定义编码数据
  4. 定义Domain Separator
  5. 生成摘要hash
  6. 验证签名者

应用场景

  • 数字资产交易:验证交易的合法性和完整性。
  • DApps身份验证:确保用户身份的合法性。
  • 智能合约的部署和调用:验证调用者的身份和权限。

合约开发

// SPDX-License-Identifier: MIT
// By 0xAA 
pragma solidity ^0.8.0;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract EIP712Storage {
    using ECDSA for bytes32;

    bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 private constant STORAGE_TYPEHASH = keccak256("Storage(address spender,uint256 number)");
    bytes32 private DOMAIN_SEPARATOR;
    uint256 number;
    address owner;

    constructor(){
        DOMAIN_SEPARATOR = keccak256(abi.encode(
            EIP712DOMAIN_TYPEHASH, // type hash
            keccak256(bytes("EIP712Storage")), // name
            keccak256(bytes("1")), // version
            block.chainid, // chain id
            address(this) // contract address
        ));
        owner = msg.sender;
    }

    /**
     * @dev Store value in variable
     */
    function permitStore(uint256 _num, bytes memory _signature) public {
        // 检查签名长度,65是标准r,s,v签名的长度
        require(_signature.length == 65, "invalid signature length");
        bytes32 r;
        bytes32 s;
        uint8 v;
        // 目前只能用assembly (内联汇编)来从签名中获得r,s,v的值
        assembly {
            /*
            前32 bytes存储签名的长度 (动态数组存储规则)
            add(sig, 32) = sig的指针 + 32
            等效为略过signature的前32 bytes
            mload(p) 载入从内存地址p起始的接下来32 bytes数据
            */
            // 读取长度数据后的32 bytes
            r := mload(add(_signature, 0x20))
            // 读取之后的32 bytes
            s := mload(add(_signature, 0x40))
            // 读取最后一个byte
            v := byte(0, mload(add(_signature, 0x60)))
        }

        // 获取签名消息hash
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            keccak256(abi.encode(STORAGE_TYPEHASH, msg.sender, _num))
        )); 

        address signer = digest.recover(v, r, s); // 恢复签名者
        require(signer == owner, "EIP712Storage: Invalid signature"); // 检查签名

        // 修改状态变量
        number = _num;
    }

    /**
     * @dev Return value 
     * @return value of 'number'
     */
    function retrieve() public view returns (uint256){
        return number;
    }    
}
# 编译指令
# npx hardhat compile

合约部署

说明:部署成功后会生成一个部署合约的地址(例:EIP712Storage合约地址: 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853)也会生成一个json文件,测试时会使用到

module.exports = async function ({getNamedAccounts,deployments}) {
    const firstAccount = (await getNamedAccounts()).firstAccount;
    const {deploy,log} = deployments;
    const EIP712Storage=await deploy("EIP712Storage",{
        contract: "EIP712Storage",
        from: firstAccount,
        args: [],
        log: true,
        // waitConfirmations: 1,
    })
    console.log("EIP712Storage合约地址",EIP712Storage.address)
  }
  module.exports.tags = ["all", "EIP712Storage"]
# 部署指令
# npx hardhat deploy

合约本地测试(hardhat)

步骤

合约部分

  • hardhat启动测试节点:npx hardhat node 会获取20个包含10000 ETH的测试账户
  • 编译合约 npx hardhat deploy 会返回一个合约地址和会在artifacts/contracts/下生成一个json文件

前端部分

  • 启动前端项目 npm start
  • 把生成的json文件导入到项目中
  • 编写测试界面
  • 写功能

前端界面及测试代码

import React,{useState,useEffect}from "react";
import { ethers ,JsonRpcProvider,BrowserProvider} from "ethers";//
import EIP712Storage from "../json/EIP712Storage.json";//合约abi
let provider,signer,account,balance,chainid,signature,owner;
let contranAddress="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853";//合约地址
const EIP712StorageApp= ()=>{
    let [account,setAccount]=useState("0");//钱包地址
    let [balance,setBalance]=useState(0);//钱包余额
    let [chainid,setChainid]=useState("0");//链id
    let [signature,setSignature]=useState();//签名信息
    // 链接钱包
    const connectWallet=async ()=>{
        if(window.ethereum){
            provider = new ethers.BrowserProvider(window.ethereum);

            signer = await provider.getSigner();//签名者
            account = await signer.getAddress();
            setAccount(account)
            balance = await provider.getBalance(account);//余额
            setBalance(balance.toString())
            chainid = await provider.getNetwork();//链id
            setChainid(chainid.chainId.toString())
           owner = await signer.getAddress();//所有者
            console.log(signer,account,balance,chainid)
        }else{
            alert("请安装钱包")
        }
    }
    //获取签名
    const  signPermit=async ()=>{
        const contract = new ethers.Contract(contranAddress, EIP712Storage.abi, signer);

        console.log("合约地址",await contract.getAddress())//合约
        const domain = {
            name: "EIP712Domain",
            version: "1",
            chainId: chainid,
            verifyingContract: contranAddress,
        };
        const types = {
            Permit: [
                { name: "owner", type: "address" },
                { name: "spender", type: "address" },
                { name: "value", type: "uint256" },
                { name: "nonce", type: "uint256" },
                { name: "deadline", type: "uint256" },
            ],
        };
        const value = {
            owner: account,
            spender: account,
            value: ethers.parseEther("22"),//账22个
            nonce: 0,
            deadline: Math.floor(Date.now() / 1000) + 60 * 60 * 24,
        };
        const signature = await signer.signTypedData(domain, types, value);//签名信息
        console.log(signature)
        setSignature(signature)
    }
    useEffect(()=>{
        connectWallet();//自动链接钱包
    },[])
    return(
        <div>
            <h1 className={`text-center font-black text-4xl`}>EIP712前端测试</h1>
            <button className={`w-28 h-10 p-2 rounded m-4 bg-blue-500 text-center text-white hover:bg-blue-700`} onClick={connectWallet}>链接钱包</button>
            <button  className={`w-28 h-10 p-2 rounded m-4 bg-blue-500 text-center text-white hover:bg-blue-700`} onClick={signPermit}>签名消息</button>
            {/* <h1 onClick={massage}>钱包信息</h1> */}
            <ul className="ml-4">
                <li className={`mb-4`}><span className={`font-black`}>地址:</span>{account}</li>
                <li className={`mb-4`}><span className={`font-black`}>余额:</span>{balance} USDT</li>
                <li className={`mb-4`}><span className={`font-black`}>chainid:</span>{chainid}</li>
                <li className={`mb-4`}><span className={`font-black`}>签名信息:</span>{signature}</li>
            </ul>
        </div>
    )
}
export default EIP712StorageApp

测试步骤

功能说明:以上实现了一个链接钱包和获取数字签名hash功能的前端界面,打开界面自动链接钱包

  • 项目启动npm start启动项目自动链接MataMask钱包界面<br> 如何所示:
  • 或者点击链接钱包按钮:如图所示

屏幕截图 2025-01-17 150105.png

屏幕截图 2025-01-17 155345.png

  • 点击签名信息按钮:调起metamask插件如图所示自定义的可以在消息中看到自定义的数据类型

屏幕截图 2025-01-17 155406.png

屏幕截图 2025-01-17 155422.png

  • 点击确定按钮:界面会返回签名的hash值

屏幕截图 2025-01-17 155442.png

总结

以上就是EIP712 类型化数据签名合约的开发测试部署全部看流程,注意:启动npx hardhat node 节点后,需要把账号的私钥导入到metamask插件中。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
江湖只有他的大名,没有他的介绍。