这是一篇关于介绍 EIP712合约的文章: EIP712用百度的话术来说, 是一种更高级, 更安全的交易签名方法. 在许多大型的项目中都会涉及到..完全去中心化的 Uniswap也运用了这个机制, 把账户的授权利用签名给到路由合约。
● 这是一篇关于介绍 EIP712合约的文章: EIP712用百度的话术来说, 是一种更高级, 更安全的交易签名方法. 在许多大型的项目中都会涉及到..完全去中心化的 Uniswap也运用了这个机制, 把账户的授权利用签名给到路由合约
假设现在有一个需求. 商城系统里面的积分系统需要升级成代币, 让用户消费. 那么之前数据库中的积分就需要等比兑换成Token给到用户的钱包地址. 假设积分和token的兑换比例为 1:1..也就是1积分兑换1个Token
如果没有EIP712 在用户点击 兑换按钮时 调用系统的API接口扣除账户的积分..同时调用合约领取对应的Token数量..这种方式会存在数据库和链上的数据不同步..同时其他人也可以直接调用合约暴露对外的方法直接领取Token.
当然可以通过服务器中的脚本..将扣除积分的用户利用系统脚本去触发合约将Token转到用户地址, 但是这种做法对与平台的GAS消耗是挺大的..每一笔的积分兑换Token..平台都需要去支付一笔gas费..但是为了数据安全..不得不这样处理
如今使用EIP712, 在系统中设置一个私钥..用户在兑换的时候..后台直接计算积分..并扣除..同时生成对应的签名返给前端..前端将参数调用合约接口与合约进行交互.. 交互的流程图如下
后面的操作可以根据业务需求进行处理..比如生成签名之后, 用户没有确定钱包发起交易..系统可以利用定时器任务, 将没有交易哈希的签名对应的积分扣除数据进行回滚..毕竟这个操作不产生GAS费用.
接下来就介绍一下怎么实现这个功能了 先直接看代码..然后再根据代码的实现..熟悉流程 ● EIP712的代码浅析交互流程图
图比较大, 需要清晰图, 建议新窗口打开, 或者下载之后查看
● 利用钱包或者钱包加密算法获取一个私钥以及对应的钱包地址 ● 私钥给到生成签名的脚本 ● 公钥给合约..在合约拿到签名参数时..是可以知道由哪个钱包地址生成的签名..用来确保与合约交互的数据是通过指定服务器计算过的数据 准备合约 ● 创建验签合约时需要确定作用域与需要参与签名的数据以及类型 ● 将作用域保存在EIP712Domain结构体中 并用keccak256("结构体名+数据类型+名称") 计算出哈希值
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)
</script>
</body>
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!