文章目录
需求:
- dapp 签名/验签登录 主要针对中心化接口鉴权;小狐狸签名时最好能让用户看到签名内容
- 学习EIP712Domain
一、Dapp 验签登录
参考链接
第二十九课 如何实现MetaMask签名授权后DAPP一键登录功能?
以太坊签名数据以及验证
两种签名
1、直接对内容签名(小狐狸可以看到hello)
web3.personal.sign(web3.fromUtf8("hello"));
2、对内容sha3后签名(小狐狸看到的是一串hash,没法看到hello)
web3.personal.sign(web3.utils.sha3("hello"));
可以通过ecRecover 验证签名
web3.eth.personal.ecRecover(signtxt,sig)
上面两种签名,
- 第一种的签名结果,在合约中ecRecover会验证失败,
- 第二种可以,但是小狐狸签名时看不到内容
如果外部验签(比如后台),使用 personal.ecRecover 需要节点支持,开放了personal, infura就没开放,所以无法使用
后面同事发现了方法,可以用infura节点验签
web3.eth.accounts.recover(signtxt,sig)
web3.eth.accounts.recover(signtxt,sig) 具体实现过程
源码 https://github.com/ChainSafe/web3.js/tree/1.x/packages
web3-eth-accounts
//上面调用时两个参数,所以preFixed是false
//再直接最后的 Account.recover(message, signature);
Accounts.prototype.recover = function recover(message, signature, preFixed) {
var args = [].slice.apply(arguments);
if (_.isObject(message)) {
return this.recover(message.messageHash, Account.encodeSignature([message.v, message.r, message.s]), true);
}
if (!preFixed) {
message = this.hashMessage(message);
}
if (args.length >= 4) {
preFixed = args.slice(-1)[0];
preFixed = _.isBoolean(preFixed) ? !!preFixed : false;
return this.recover(message, Account.encodeSignature(args.slice(1, 4)), preFixed); // v, r, s
}
return Account.recover(message, signature);
};
//内部有加前缀!
Accounts.prototype.hashMessage = function hashMessage(data) {
var messageHex = utils.isHexStrict(data) ? data : utils.utf8ToHex(data);
var messageBytes = utils.hexToBytes(messageHex);
var messageBuffer = Buffer.from(messageBytes);
var preamble = '\x19Ethereum Signed Message:\n' + messageBytes.length;
var preambleBuffer = Buffer.from(preamble);
var ethMessage = Buffer.concat([preambleBuffer, messageBuffer]);
return Hash.keccak256s(ethMessage);
};
Account.recover(message, signature);
源码 https://github.com/maiavictor/eth-lib
const recover = (hash, signature) => {
const vals = decodeSignature(signature);
const vrs = { v: Bytes.toNumber(vals[0]), r: vals[1].slice(2), s: vals[2].slice(2) };
const ecPublicKey = secp256k1.recoverPubKey(new Buffer(hash.slice(2), "hex"), vrs, vrs.v < 2 ? vrs.v : 1 - vrs.v % 2); // because odd vals mean v=0... sadly that means v=0 means v=1... I hate that
const publicKey = "0x" + ecPublicKey.encode("hex", false).slice(2);
const publicHash = keccak256(publicKey);
const address = toChecksum("0x" + publicHash.slice(-40));
return address;
};
如有需要node中测试,可以将上面代码(hashMessage/recover)直接扣出来用即可 下面是相关导包
需要两个库
- npm i web3-utils
- npm i eth-lib
const utils = require('web3-utils');
const Hash = require('eth-lib/lib/hash');
const Bytes = require("eth-lib/lib/bytes");
const decodeSignature = hex => [Bytes.slice(64, Bytes.length(hex), hex), Bytes.slice(0, 32, hex), Bytes.slice(32, 64, hex)];
const elliptic = require("elliptic");
const secp256k1 = new elliptic.ec("secp256k1");
const { keccak256, keccak256s } = require("eth-lib/lib/hash");
const toChecksum = address => {
const addressHash = keccak256s(address.slice(2));
let checksumAddress = "0x";
for (let i = 0; i < 40; i++) checksumAddress += parseInt(addressHash[i + 2], 16) > 7 ? address[i + 2].toUpperCase() : address[i + 2];
return checksumAddress;
};
golang的实现
参考 以太坊go-ethereum签名部分源码解析 https://blog.csdn.net/weixin_30407613/article/details/99244163
func verifySig(from, sigHex string, msg []byte) bool {
fromAddr := common.HexToAddress(from)
sig := hexutil.MustDecode(sigHex)
if sig[64] != 27 && sig[64] != 28 {
return false
}
sig[64] -= 27
pubKey, err := crypto.SigToPub(signHash(msg), sig)
if err != nil {
return false
}
recoveredAddr := crypto.PubkeyToAddress(*pubKey)
fmt.Println("addr: ", recoveredAddr)
return fromAddr == recoveredAddr
}
func signHash(data []byte) []byte {
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
return crypto.Keccak256([]byte(msg))
}
使用
verifySig("0x0000000000000000000000000000000000000000",encodedTxStr,[]byte("hello"))
也可以使用EIP712,小狐狸签名时用户也可以看到实际内容
二、token EIP712Domain
参考链接
ethereum/EIPs
metamask-sign-typed-data-v4 该链接查看页面底部的Example/JavaScript
eip712的概念查看文档…
这里主要说明怎么用,根据自己需求扩展
Domain 格式
这个格式能不能改,没测
constructor(uint256 chainId_) public {
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId_,
address(this)
));
}
//合约中验签
//下面格式"\x19\x01",DOMAIN_SEPARATOR,
//就像eth_sign 中的 hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
function shaInfo(address holder,address spender,uint256 nonce,uint256 expiry,uint256 value)public view returns(bytes32){
bytes32 digest =
keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH,
holder,
spender,
nonce,
expiry,
value))
));
return digest;
}
Permit 格式
该格式实际可以随意修改,Permit和Domain实际就是对应的结构体,结构体名称就是type
参考eip-712例子 Example.sol
- typeHash就对结构体和所有属性类型进行keccak256
- DOMAIN_SEPARATOR 是对有值的结构体进行 keccak256,作用是xxxx (避免滥用, 712里面也加了chainId和nonce)
struct Person {
string name;
address wallet;
}
bytes32 constant PERSON_TYPEHASH = keccak256("Person(string name,address wallet)");
所以,如果想仿着改,还是很容易的…
套格式
可参考 example.js
- domain/message 是两个结构体的实际内容
- types 是两个结构体的结构
- primaryType: ‘Permit’, 签名的message的type
const msgParams = JSON.stringify({
domain: {
name: 'TDai Stablecoin',
version: '1',
chainId: 4,
verifyingContract: '0xddaAd340b0f1Ef65169Ae5E41A8b10776a75482d',
},
// Defining the message signing data content.
message: {
holder: '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4',
spender: '0x0fC5025C764cE34df352757e82f7B5c4Df39A836',
nonce: 1,
expiry: 1640966400,
value: 10000,
},
// Refers to the keys of the *types* object below.
primaryType: 'Permit',
types: {
// TODO: Clarify if EIP712Domain refers to the domain the contract is hosted on
EIP712Domain: [
{name: 'name', type: 'string'},
{name: 'version', type: 'string'},
{name: 'chainId', type: 'uint256'},
{name: 'verifyingContract', type: 'address'},
],
Permit: [
{name: 'holder', type: 'address'},
{name: 'spender', type: 'address'},
{name: 'nonce', type: 'uint256'},
{name: 'expiry', type: 'uint256'},
{name: 'value', type: 'uint256'}
],
},
});
如何签名
node签名
参考eip-712例子 Example.js
//在example.js中加这些,就可以直接node example.js 看结果了
console.log("获取私钥")
const privateKey =ethUtil.toBuffer("0x503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb");
const address = ethUtil.privateToAddress(privateKey);
console.log(ethUtil.bufferToHex(address));
const sig = ethUtil.ecsign(signHash(), privateKey);
console.log("签名后的的信息");
console.log(sig);
console.log("--- "+ethUtil.bufferToHex(sig.r)+" ; "+ethUtil.bufferToHex(sig.s)+" ; "+sig.v);
let mailHash = encodeData(typedData.primaryType,typedData.message);
console.log(ethUtil.bufferToHex(ethUtil.keccak256(mailHash)));
ethers 签名
//ethers
const ethers = require('ethers');
const config = require('../config/constants');
const { TypedDataEncoder } = require('ethers');
let customHttpProvider = new ethers.JsonRpcProvider(config.rpc);
let wallet = new ethers.Wallet(config.filler_prikey, customHttpProvider);
async function ethersSign() {
console.log('ethers--------')
let test = {
"domain": {
"name": "TDai Stablecoin",
"version": "1",
"chainId": 4,
"verifyingContract": "0xddaAd340b0f1Ef65169Ae5E41A8b10776a75482d",
},
// Defining the message signing data content.
"data": {
"holder": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"spender": "0x0fC5025C764cE34df352757e82f7B5c4Df39A836",
"nonce": 1,
"expiry": 1640966400,
"value": 10000,
},
// Refers to the keys of the *types* object below.
"primaryType": "Permit",
"types": {
// TODO: Clarify if EIP712Domain refers to the domain the contract is hosted on
// "EIP712Domain": [
// { "name": "name", "type": "string" },
// { "name": "version", "type": "string" },
// { "name": "chainId", "type": "uint256" },
// { "name": "verifyingContract", "type": "address" },
// ],
"Permit": [
{ "name": "holder", "type": "address" },
{ "name": "spender", "type": "address" },
{ "name": "nonce", "type": "uint256" },
{ "name": "expiry", "type": "uint256" },
{ "name": "value", "type": "uint256" }
],
},
}
const encoder = TypedDataEncoder.from(test.types);
console.log(encoder.primaryType);
console.log(encoder.encode(test.data));
console.log(TypedDataEncoder.getPrimaryType(test.types));
console.log(TypedDataEncoder.hash(test.domain, test.types, test.data));
const signature = wallet.signingKey.sign(TypedDataEncoder.hash(test.domain, test.types, test.data))
console.log(signature.toJSON())
}
网页小狐狸签名
参考 metamask-sign-typed-data-v4
rpc
- signTypedData_v1
- signTypedData_v3
- signTypedData_v4
这三种都可以, 具体下面有demo
var params = [from, msgParams]
var method = 'eth_signTypedData_v4'
web3.currentProvider.sendAsync({
method,
params,
from,
}, function (err, result) {
});
根据Dai的代码修改的demo
包括合约和前端代码
DAI.sol
有部分改动,
- 精度改成2
- Permit内容有修改,改成传入多少value,就授权多少value,而不是-1
- 注意chainId要填对应的,否则小狐狸不给签… 如rinkeby是4
/**
*Submitted for verification at Etherscan.io on 2019-11-14
*/
// hevm: flattened sources of /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/dai.sol
pragma solidity =0.5.12;
contract LibNote {
event LogNote(
bytes4 indexed sig,
address indexed usr,
bytes32 indexed arg1,
bytes32 indexed arg2,
bytes data
) anonymous;
modifier note {
_;
assembly {
// log an 'anonymous' event with a constant 6 words of calldata
// and four indexed topics: selector, caller, arg1 and arg2
let mark := msize // end of memory ensures zero
mstore(0x40, add(mark, 288)) // update free memory pointer
mstore(mark, 0x20) // bytes type data offset
mstore(add(mark, 0x20), 224) // bytes size (padded)
calldatacopy(add(mark, 0x40), 0, 224) // bytes payload
log4(mark, 288, // calldata
shl(224, shr(224, calldataload(0))), // msg.sig
caller, // msg.sender
calldataload(4), // arg1
calldataload(36) // arg2
)
}
}
}
contract Dai is LibNote {
// --- Auth ---
mapping (address => uint) public wards;
function rely(address guy) external note auth { wards[guy] = 1; }
function deny(address guy) external note auth { wards[guy] = 0; }
modifier auth {
require(wards[msg.sender] == 1, "Dai/not-authorized");
_;
}
// --- ERC20 Data ---
string public constant name = "TDai Stablecoin";
string public constant symbol = "TDAI";
string public constant version = "1";
uint8 public constant decimals = 2;
uint256 public totalSupply;
mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;
mapping (address => uint) public nonces;
event Approval(address indexed src, address indexed guy, uint wad);
event Transfer(address indexed src, address indexed dst, uint wad);
// --- Math ---
function add(uint x, uint y) internal pure returns (uint z) {
require((z = x + y) >= x);
}
function sub(uint x, uint y) internal pure returns (uint z) {
require((z = x - y) <= x);
}
// --- EIP712 niceties ---
bytes32 public DOMAIN_SEPARATOR;
// bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,uint256 value)");
bytes32 public constant PERMIT_TYPEHASH = 0x63f12011971eae53910a7ea124c7d16788b74790706dc6d7358718ff7ce8dd13;
constructor(uint256 chainId_) public {
wards[msg.sender] = 1;
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId_,
address(this)
));
mint(msg.sender,1000000);
}
// --- Token ---
function transfer(address dst, uint wad) external returns (bool) {
return transferFrom(msg.sender, dst, wad);
}
function transferFrom(address src, address dst, uint wad)
public returns (bool)
{
require(balanceOf[src] >= wad, "Dai/insufficient-balance");
if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) {
require(allowance[src][msg.sender] >= wad, "Dai/insufficient-allowance");
allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad);
}
balanceOf[src] = sub(balanceOf[src], wad);
balanceOf[dst] = add(balanceOf[dst], wad);
emit Transfer(src, dst, wad);
return true;
}
function mint(address usr, uint wad) public {
balanceOf[usr] = add(balanceOf[usr], wad);
totalSupply = add(totalSupply, wad);
emit Transfer(address(0), usr, wad);
}
function burn(address usr, uint wad) external {
require(balanceOf[usr] >= wad, "Dai/insufficient-balance");
if (usr != msg.sender && allowance[usr][msg.sender] != uint(-1)) {
require(allowance[usr][msg.sender] >= wad, "Dai/insufficient-allowance");
allowance[usr][msg.sender] = sub(allowance[usr][msg.sender], wad);
}
balanceOf[usr] = sub(balanceOf[usr], wad);
totalSupply = sub(totalSupply, wad);
emit Transfer(usr, address(0), wad);
}
function approve(address usr, uint wad) external returns (bool) {
allowance[msg.sender][usr] = wad;
emit Approval(msg.sender, usr, wad);
return true;
}
// --- Alias ---
function push(address usr, uint wad) external {
transferFrom(msg.sender, usr, wad);
}
function pull(address usr, uint wad) external {
transferFrom(usr, msg.sender, wad);
}
function move(address src, address dst, uint wad) external {
transferFrom(src, dst, wad);
}
// --- Approve by signature ---
function permit(address holder, address spender, uint256 nonce, uint256 expiry,
uint256 value, uint8 v, bytes32 r, bytes32 s) external
{
bytes32 digest =
keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH,
holder,
spender,
nonce,
expiry,
value))
));
require(holder != address(0), "Dai/invalid-address-0");
require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit");
require(expiry == 0 || now <= expiry, "Dai/permit-expired");
require(nonce == nonces[holder]++, "Dai/invalid-nonce");
allowance[holder][spender] = value;
emit Approval(holder, spender, value);
}
function shaInfo(address holder,address spender,uint256 nonce,uint256 expiry,uint256 value)public view returns(bytes32){
bytes32 digest =
keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH,
holder,
spender,
nonce,
expiry,
value))
));
return digest;
}
}
transferFromDai.sol
pragma solidity =0.5.12;
interface IToken {
function balanceOf(address _owner) external view returns (uint256 balance);
function transfer(address _to, uint256 _value) external returns (bool success);
function transferFrom(address _from, address _to, uint256 _value) external returns
(bool success);
function approve(address _spender, uint256 _value) external returns (bool success);
function allowance(address _owner, address _spender) external view returns
(uint256 remaining);
function nonces(address)external view returns(uint256 n);
function permit(address holder, address spender, uint256 nonce, uint256 expiry, uint256 allowed, uint8 v, bytes32 r, bytes32 s) external;
}
contract transferFromDai{
event Zero(address addr,uint256 zero);
event Nnn(address addr,uint256);
//这个代币是上面发布的dai,
address tokenAddr = 0xddaAd340b0f1Ef65169Ae5E41A8b10776a75482d;
//这个时间写死2022,方便测试不用修改..
uint256 time2022 = 1640966400;
// function permit(address holder, address spender, uint256 nonce, uint256 expiry,
// bool allowed, uint8 v, bytes32 r, bytes32 s) external
function deposit(uint256 nonce, uint256 value, uint8 v, bytes32 r, bytes32 s)public{
IToken token = IToken(tokenAddr);
if(token.allowance(msg.sender,address(this)) ==0){
emit Zero(address(6),6);
token.permit(msg.sender,address(this),nonce,time2022,value,v,r,s);
}else{
emit Nnn(address(2),2);
}
token.transferFrom(msg.sender,address(this),value);
}
function Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)public{}
}
前端代码
domainParams.js
//如自己部署,注意修改 domain内的chainId,contract
//修改message中的实际签名信息 holder/spender
//修改abi和地址
const msgParams = JSON.stringify({
domain: {
name: 'TDai Stablecoin',
version: '1',
chainId: 4,
verifyingContract: '0xddaAd340b0f1Ef65169Ae5E41A8b10776a75482d',
},
// Defining the message signing data content.
message: {
holder: '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4',
spender: '0x0fC5025C764cE34df352757e82f7B5c4Df39A836',
nonce: 1,
expiry: 1640966400,
value: 10000,
},
// Refers to the keys of the *types* object below.
primaryType: 'Permit',
types: {
// TODO: Clarify if EIP712Domain refers to the domain the contract is hosted on
EIP712Domain: [
{name: 'name', type: 'string'},
{name: 'version', type: 'string'},
{name: 'chainId', type: 'uint256'},
{name: 'verifyingContract', type: 'address'},
],
Permit: [
{name: 'holder', type: 'address'},
{name: 'spender', type: 'address'},
{name: 'nonce', type: 'uint256'},
{name: 'expiry', type: 'uint256'},
{name: 'value', type: 'uint256'}
],
},
});
const TEST_ADDR = '0x0fC5025C764cE34df352757e82f7B5c4Df39A836';
const TEST_ABI = [];
const DAI_ADDR = '0xddaAd340b0f1Ef65169Ae5E41A8b10776a75482d';
const DAI_ABI = [];
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--<input type="text" placeholder="nonce" id="edit_nonce">-->
<input type="text" placeholder="数量" id="edit_num">
<button onclick="sign()">签名并充值代币</button>
<br />
<br />
<input type="text" placeholder="增发数量" id="edit_mint">
<button onclick="mint()">增发dai</button>
</body>
</html>
<script src="https://cdn.bootcdn.net/ajax/libs/web3/1.3.0/web3.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="./js/meta/domainParams.js"></script>
<script>
window.onload = function () {
wallet()
}
let account0 = '';
function wallet() {
console.log(window.ethereum)
if (window.ethereum) {
Web3 = new Web3(ethereum);
try {
ethereum.enable();
} catch (error) {
}
} else if (typeof Web3 !== 'undefined') {
Web3 = new Web3(Web3.currentProvider);
} else {
Web3 = new Web3(new Web3.providers.HttpProvider('https://rinkeby.infura.io/v3/-'));
}
Web3.eth.getAccounts().then(function (res) {
account0 = res[0];
console.log("账号: " + account0);
});
}
async function sign() {
let from = account0;
console.log("签名并发送交易");
let tokenContract = new Web3.eth.Contract(DAI_ABI,DAI_ADDR);
let nonce = await tokenContract.methods.nonces(from).call();
console.log(nonce);
let num = $('#edit_num').val();
num = Number.parseInt(num);
let tempObj = JSON.parse(msgParams);
tempObj.message.nonce = nonce;
tempObj.message.value = num;
tempObj.message.holder = from;
// var params = [from, msgParams]
var params = [from, JSON.stringify(tempObj)]
var method = 'eth_signTypedData_v4'
Web3.currentProvider.sendAsync({
method,
params,
from,
}, function (err, result) {
console.log("签名结果");
console.log(err);
console.log(result);
let signResult = result.result;
let r = signResult.slice(0, 66)
let s = '0x' + signResult.slice(66, 130)
let v = '0x' + signResult.slice(130, 132)
let contract = new Web3.eth.Contract(TEST_ABI,TEST_ADDR);
contract.methods.deposit(nonce,num,v,r,s).send({from:from},function (err,r) {
console.log("发送结果: ")
console.log(err);
console.log(r);
});
})
}
function mint() {
let num = $('#edit_mint').val();
num = Number.parseInt(num,16).toString(16);
console.log(num)
let tokenContract = new Web3.eth.Contract(DAI_ABI,DAI_ADDR);
tokenContract.methods.mint(account0,num).send({from:account0})
}
</script>