以太坊签名,验证签名, EIP712domain Permit授权并转账


需求

  • 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)));
网页小狐狸签名

参考 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>



猜你喜欢

转载自blog.csdn.net/zgf1991/article/details/113247362