EIP712合约与前端的交互流程

  • 黄金叶
  • 更新于 2022-08-05 16:09
  • 阅读 4063

这是一篇关于介绍 EIP712合约的文章: EIP712用百度的话术来说, 是一种更高级, 更安全的交易签名方法. 在许多大型的项目中都会涉及到..完全去中心化的 Uniswap也运用了这个机制, 把账户的授权利用签名给到路由合约。

● 这是一篇关于介绍 EIP712合约的文章: EIP712用百度的话术来说, 是一种更高级, 更安全的交易签名方法. 在许多大型的项目中都会涉及到..完全去中心化的 Uniswap也运用了这个机制, 把账户的授权利用签名给到路由合约

场景介绍

假设现在有一个需求. 商城系统里面的积分系统需要升级成代币, 让用户消费. 那么之前数据库中的积分就需要等比兑换成Token给到用户的钱包地址. 假设积分和token的兑换比例为 1:1..也就是1积分兑换1个Token

如果没有EIP712 在用户点击 兑换按钮时 调用系统的API接口扣除账户的积分..同时调用合约领取对应的Token数量..这种方式会存在数据库和链上的数据不同步..同时其他人也可以直接调用合约暴露对外的方法直接领取Token.

  • 扣除积分之后用户关闭了页面或者刷新了页面..那么就会出现扣除了积分, 但是没有Token到账

当然可以通过服务器中的脚本..将扣除积分的用户利用系统脚本去触发合约将Token转到用户地址, 但是这种做法对与平台的GAS消耗是挺大的..每一笔的积分兑换Token..平台都需要去支付一笔gas费..但是为了数据安全..不得不这样处理

如今使用EIP712, 在系统中设置一个私钥..用户在兑换的时候..后台直接计算积分..并扣除..同时生成对应的签名返给前端..前端将参数调用合约接口与合约进行交互.. 交互的流程图如下

image.png

后面的操作可以根据业务需求进行处理..比如生成签名之后, 用户没有确定钱包发起交易..系统可以利用定时器任务, 将没有交易哈希的签名对应的积分扣除数据进行回滚..毕竟这个操作不产生GAS费用.

实现

接下来就介绍一下怎么实现这个功能了 先直接看代码..然后再根据代码的实现..熟悉流程 ● EIP712的代码浅析交互流程图

EIP712合约示例代码解析图.png 图比较大, 需要清晰图, 建议新窗口打开, 或者下载之后查看

准备私钥与钱包地址

● 利用钱包或者钱包加密算法获取一个私钥以及对应的钱包地址 ● 私钥给到生成签名的脚本 ● 公钥给合约..在合约拿到签名参数时..是可以知道由哪个钱包地址生成的签名..用来确保与合约交互的数据是通过指定服务器计算过的数据 准备合约 ● 创建验签合约时需要确定作用域与需要参与签名的数据以及类型 ● 将作用域保存在EIP712Domain结构体中 并用keccak256("结构体名+数据类型+名称") 计算出哈希值

    • 这里我们假定这个合约用来验签商城. 作用域名称"SHOP"
      
      EIP712Domain{
      name: "SHOP",
      version: "1",
      chainId:97
      }

//计算Domain作用域相关参数的哈希值比记录 DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId)"), keccak256(bytes("SHOP")), keccak256(bytes("1")), 97 ) )

● 确定参与验签的数据 需要用户地址: 要将token转到目标地址, 数量:转移token的数量, 数据库中的订单id, 避免一个签名被多次使用
● 记录参与验签的数据结构哈希值

struct VerifyClaim { address userAddress; uint256 randNo; uint256 amount; } VERIFYCLAIM_TYPTHASH = keccak256("VerifyClaim(address userAddress,uint256 randNo,uint256 amount)"

# 脚本工作
●  引入 etherjs包
`<script src="ether5.2umd.50ed9.js" charset="utf-8"></script>`
●  确定验签的作用域, 根据合约格式

const domain = { name: "SHOP", version: "1", chainId: 97 }

● 整理与合约交互的参数格式 将合约中的结构体 整理成 结构体名[{参数名,参数类型...}]的格式

const types = { VerifyClaim: [ {name: "userAddress", type: "address"}, {name: "randNo", type: "uint256"}, {name: "amount", type: "uint256"} ] }

● 从系统中获取相关数据..计算出转移token数量, 订单id, 以及目标地址

let message = { userAddress: "0xE66Eb4D3845822568938664B06b676Dc2C33a6fB", randNo: "508091813966385152", amount: "10000" }

● 使用ethers库连接网络并获取签名

//这里用币安测试链为例 var RPC_URL = "https://data-seed-prebsc-1-s1.binance.org:8545/" var netWorkId = 97 var provider = new ethers.providers.StaticJsonRpcProvider(RPC_URL, netWorkId) //var walletSigner = provider.getSigner() this.signer = new ethers.Wallet("Private Key", provider)

//将作用域, 参数类型, 以及交互参数传入_signTypedData获取签名 const signResult = await this.signer._signTypedData(domain, types, message)

● 获取签名之后将签名切割..获得r, s, v参数

// signResult:0x7e7c845c626ea483f03890a2827236868a812777d7d9732954887aa95b8d015a66fb7c23527879704c5a622fee8f85adaf7c1b00057cceb2682064f04d2690e61c //剪切0x const signature = signResult.substring(2) //因为r和s都是32字节的..所以前面64位是参数r const r = "0x" + signature.substring(0, 64); //接着后面64位是参数s const s = "0x" + signature.substring(64, 128); //最后2位是v. 再将16进制转10进制 const v = parseInt(signature.substring(128, 130), 16); console.log("signature",message) console.log("v",v) console.log("s",s) console.log("r",r)

# 合约验签
● 在合约中暴露一个对外接口

function test( address _userAddress, uint256 _randNo, uint 256 _amount, uint8 _v, bytes32 _r, bytes32 _s ) public view returns(bool) { //根据合约中验签数据的结构体, 将参数整理成验签数据格式 //计算参数哈希 verifyclaimHash = keccak256( abi.encode( VERIFYCLAIM_TYPEHASH, _userAddress, _randNo, _amount ) )

bytes32 digest = keccak256( abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, verifyclaimHash ) )

//利用内置函数ecrecover获取签名的钱包判断是否与指定钱包相等 return ecrecover(digest, _v, _r, _s) == VERIFYADDRESS }

# 相关文件
源码:

// file: EIP712DomainExample.sol pragma solidity ^0.4.24;

contract Example {

struct Person {
    string name;
    address wallet;
}

struct Mail {
    Person from;
    Person to;
    string contents;
}

bytes32 constant PERSON_TYPEHASH = keccak256(
    "Person(string name,address wallet)"
);

bytes32 constant MAIL_TYPEHASH = keccak256(
    "Mail(Person from,Person to,string contents)Person(string name,address wallet)"
);

struct EIP712Domain {
    string  name;
    string  version;
    uint256 chainId;
}

struct VerifyClaim{
    address userAddress;
    uint256 randNo;
    uint256 amount;
}

bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
    "EIP712Domain(string name,string version,uint256 chainId)"
);

bytes32 constant VERIFYCLAIM_TYPEHASH = keccak256(
    "VerifyClaim(address userAddress,uint256 randNo,uint256 amount)"
);

bytes32 DOMAIN_SEPARATOR;

constructor () public {
    DOMAIN_SEPARATOR = hash(EIP712Domain({
        name: "VerifyClaim",
        version: '1',
        chainId: 97
    }));
}

function hash(Person person) internal pure returns (bytes32) {
    return keccak256(abi.encode(
        PERSON_TYPEHASH,
        keccak256(bytes(person.name)),
        person.wallet
    ));
}

function hash(Mail mail) internal pure returns (bytes32) {
    return keccak256(abi.encode(
        MAIL_TYPEHASH,
        hash(mail.from),
        hash(mail.to),
        keccak256(bytes(mail.contents))
    ));
}

function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {
    return keccak256(abi.encode(
        EIP712DOMAIN_TYPEHASH,
        keccak256(bytes(eip712Domain.name)),
        keccak256(bytes(eip712Domain.version)),
        eip712Domain.chainId
    ));
}

function hash(VerifyClaim verifyclaim) internal pure returns (bytes32) {
    return keccak256(abi.encode(
        VERIFYCLAIM_TYPEHASH,
        verifyclaim.userAddress,
        verifyclaim.randNo,
        verifyclaim.amount
    ));
}

function verify(VerifyClaim verifyclaim, uint8 v, bytes32 r, bytes32 s) internal view returns (bool) {
    // Note: we need to use `encodePacked` here instead of `encode`.
    bytes32 digest = keccak256(abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        hash(verifyclaim)
    ));
    return ecrecover(digest, v, r, s) == 0x53dE6A872435F5286BEFd0b6fB3bC06742aF8C8F;
}

function test(address _userAddress, uint256 _randNO, uint256 _amount, uint8 _v, bytes32 _r, bytes32 _s) public view returns (bool) {
    // Example signed message
    VerifyClaim memory verifyclaim = VerifyClaim({
        userAddress: _userAddress,
        randNo: _randNO,
        amount: _amount
    });
    assert(verify(verifyclaim, _v, _r, _s));
    return true;
}

}

/=======================================================================================================/

//file: test.html <body> <script src="ether5.2umd.50ed9.js" charset="utf-8"></script> <script> var RPC_URL = "https://data-seed-prebsc-1-s1.binance.org:8545/" var netWorkId = 97 async function sign(){

        const domain = {
            name: 'DOOMSDAY',
            version: '1',
            chainId: 97,
        }
        const types = {
            VerifyClaim: [
                {name: 'userAddress', type: 'address'},
                {name: 'randNo', type: 'uint256'},
                {name: 'amount', type: 'uint256'}
            ]
        }
        let _myAddress = "0xE66Eb4D3845822568938664B06b676Dc2C33a6fB"
        let message = {userAddress:_myAddress,randNo:"508091813966385152",amount:"100"}
        var provider = new ethers.providers.StaticJsonRpcProvider(RPC_URL, netWorkId);
        var walletSigner = provider.getSigner();
        this.signer = new ethers.Wallet(provider)

        console.log(this.signer);

        const signResult = await this.signer._signTypedData(domain, types, message)
        console.log(signResult);
        const signature = signResult.substring(2);
        const r = "0x" + signature.substring(0, 64);
        const s = "0x" + signature.substring(64, 128);
        const v = parseInt(signature.substring(128, 130), 16);
        console.log("signature",message)
        console.log("v",v)
        console.log("s",s)
        console.log("r",r)
    }
    setTimeout(sign,1000)
&lt;/script>

</body>

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

0 条评论

请先 登录 后评论
黄金叶
黄金叶
0xf8b2...537f
江湖只有他的大名,没有他的介绍。