赞
踩
参考链接
第二十九课 如何实现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)
上面两种签名,
如果外部验签(比如后台),使用 personal.ecRecover 需要节点支持,开放了personal, infura就没开放,所以无法使用
后面同事发现了方法,可以用infura节点验签
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)直接扣出来用即可 下面是相关导包
需要两个库
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;
};
参考 以太坊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,小狐狸签名时用户也可以看到实际内容
参考链接
ethereum/EIPs
metamask-sign-typed-data-v4 该链接查看页面底部的Example/JavaScript
eip712的概念查看文档…
这里主要说明怎么用,根据自己需求扩展
这个格式能不能改,没测
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和Domain实际就是对应的结构体,结构体名称就是type
参考eip-712例子 Example.sol
struct Person {
string name;
address wallet;
}
bytes32 constant PERSON_TYPEHASH = keccak256("Person(string name,address wallet)");
所以,如果想仿着改,还是很容易的…
可参考 example.js
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'} ], }, });
参考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 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
这三种都可以, 具体下面有demo
var params = [from, msgParams]
var method = 'eth_signTypedData_v4'
web3.currentProvider.sendAsync({
method,
params,
from,
}, function (err, result) {
});
包括合约和前端代码
DAI.sol
有部分改动,
/** *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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。