文章目录

  • 一、Dapp 验签登录
    • web3.eth.accounts.recover(signtxt,sig) 具体实现过程
    • golang的实现
  • 二、token EIP712Domain
    • Domain 格式
    • Permit 格式
        • 套格式
      • 如何签名
        • node签名
        • 网页小狐狸签名
    • 根据Dai的代码修改的demo

需求

  • 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 thatconst 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] -= 27pubKey, 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 onEIP712Domain: [{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 arg2let mark := msize                         // end of memory ensures zeromstore(0x40, add(mark, 288))              // update free memory pointermstore(mark, 0x20)                        // bytes type data offsetmstore(add(mark, 0x20), 224)              // bytes size (padded)calldatacopy(add(mark, 0x40), 0, 224)     // bytes payloadlog4(mark, 288,                           // calldatashl(224, shr(224, calldataload(0))), // msg.sigcaller,                              // msg.sendercalldataload(4),                     // arg1calldataload(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) externalfunction 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 onEIP712Domain: [{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>

以太坊签名,验证签名, EIP712domain Permit授权并转账相关推荐

  1. .NET Core 使用RSA算法 加密/解密/签名/验证签名

    前言 前不久移植了支付宝官方的SDK,以适用ASP.NET Core使用支付宝支付,但是最近有好几位用户反应在Linux下使用会出错,调试发现是RSA加密的错误,下面具体讲一讲. RSA在.NET C ...

  2. 以太坊智能合约开发:让合约接受转账

    以太坊智能合约开发:让合约接受转账 在以太坊智能合约开发中,通常会有向合约地址进行转账的需求,那么有几种向合约地址进行转账的方式呢? 有三种方式: 部署合约时转账 调用合约提供的方法 直接向合约地址进 ...

  3. geth 转账_以太坊1 - 私有链部署、挖矿、转账

    总结一下以太坊私有链搭建的过程,已经遇到的问题. 我们使用了LINUX,MAC OSX,WINDOWS三种平台,运行go-ethereum. 一.go语言安装 LINUX 命令行输入sudo gedi ...

  4. springboot操作以太坊(eth),使用web3j,转账等

    开发以太坊prc客户端: 本次使用的是<MetaMask>钱包 1,安装node.js 2,安装ganache-cli,开启本地web3j的测试服务 安装命令:npm install -g ...

  5. (三)以太坊创建多重签名钱包

    (1)继续在私链做实验,首先创建3个账号,并且都分配一些以太币 (2)点击新增钱包-选择多重签名的钱包合约 (3)创建成功 (4)给钱包存入一定量的以太币,然后从多重钱包向其他账号发送11个以太币 ( ...

  6. PHP如何验证以太坊签名

    以太坊有一个非常强大的JavaScript生态系统.有一些很棒的开源项目,比如ethereumjs-util,它提供了一个用以太坊帐户签名的即插即用功能. JavaScript的一个缺点是,在许多领域 ...

  7. 区块链以太坊以及hyperledger总结

    https://learnblockchain.cn/ 1.什么是智能合约?它有什么特点? 就是具有交互能力而且能够在区块链中传递的合约 一个由计算机代码控制的以太币账户 特点: 公开透明.能即时与区 ...

  8. 理解 以太坊Serenity - 第一部分: 深度抽象

    Origin post by Vitalik Buterin, on December 24th, 2015 我们已经公开继续改进以太坊协议的计划和长期开发路线图相当长一段时间了,这个做法也是来自于从 ...

  9. 《精通以太坊》预言机

    [本文摘自<精通以太坊>一书第11章预言机部分] 在本章中,我们将讨论预言机(oracle),它是可以为以太坊智能合约提供外部数据源的系统. "oracle"一词来自希 ...

最新文章

  1. 大师Martin Fowler强烈推荐的一本书
  2. axure8 事件改变样式_15. 教你零基础搭建小程序:小程序事件绑定(2)
  3. python为什么要使用闭包
  4. import numpy as np_纪录27个NumPy操作
  5. 苹果自带相册打马赛克_如果你用苹果手机!学会这3个技巧,就能让手机变得更加好用...
  6. Mysql 8.0 安装教程 Linux Centos7
  7. Bootstrap-组件-2
  8. lisp读取天正轴号_第2天:Python 基础语法
  9. 螺栓预紧力_斯姆勒知识讲解:螺栓预紧力的计算
  10. 如何JOPtionPane的showConfirmDialog对话框button设置监视器
  11. android 字体css样式,css字体设置
  12. 软件测试——Postman Script脚本功能
  13. c#万能视频播放器(附代码)
  14. linux unip命令
  15. Hadoop 表和字段
  16. GWAS研究和多基因评分
  17. TI DSP芯片SCI模块的波特率自适应
  18. 争议不断的AI绘画,靠什么成为了顶流?
  19. Python与人工神经网络(5)——交叉熵成本函数
  20. 如何在Linux虚拟器里新建跟目录,虚拟机linux 6 增加根目录

热门文章

  1. linux入门(二【粉丝版--隐私】)
  2. 无线电能传输 wpt 磁耦合谐振 过零检测 matlab simulink仿真 pwm MOSFET,过零检测模块 基于二极管整流的无线电能传输设计
  3. Java基础-简聊类与对象
  4. 2022 Aug 18 刷题log
  5. Android百度地图雷达效果,地图导航实测:百度地图路线雷达圈粉“老司机”
  6. 【Codeforces 549F】Yura and Developers | 单调栈、启发式合并、二分
  7. 因此,吉尔伯特教授建议
  8. 《途客圈创业记:不疯魔,不成活》一一2.11 途客圈旅行助手
  9. unicode 生僻字_生僻字打不出来怎么办小编教你解决办法
  10. PS流(ISO13818和GB28181)分析